From fa3c7506923f29501db500cc10a7dd0fce7b5296 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Fri, 18 Oct 2024 17:47:26 -0500 Subject: [PATCH 01/14] OSReadYourWriteData Implementation Motivation: encapsulating object to keep rywTokens & rywDelays together Override equality --- .../Consistency/OSReadYourWriteData.swift | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSReadYourWriteData.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSReadYourWriteData.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSReadYourWriteData.swift new file mode 100644 index 000000000..a9cf045b2 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSReadYourWriteData.swift @@ -0,0 +1,60 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +@objcMembers +public class OSReadYourWriteData: NSObject { + public let rywToken: String? + public let rywDelay: NSNumber? + + public init(rywToken: String?, rywDelay: NSNumber?) { + self.rywToken = rywToken + self.rywDelay = rywDelay + } + + // Override `isEqual` for custom equality comparison. + override public func isEqual(_ object: Any?) -> Bool { + guard let other = object as? OSReadYourWriteData else { + return false + } + + let tokensAreEqual = (self.rywToken == other.rywToken) || (self.rywToken == nil && other.rywToken == nil) + let delaysAreEqual = (self.rywDelay?.isEqual(to: other.rywDelay ?? 0) ?? false) || (self.rywDelay == nil && other.rywDelay == nil) + + return tokensAreEqual && delaysAreEqual + } + + // Override `hash` to maintain hashability. + // This is because two equal objects must have the same hash value. + // Since we are overriding isEqual we must also override `hash` + override public var hash: Int { + var hasher = Hasher() + hasher.combine(rywToken) + hasher.combine(rywDelay) + return hasher.finalize() + } +} + From eb459463cac9c368903cf030d9e3bba1050a617c Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Tue, 10 Sep 2024 12:49:52 -0500 Subject: [PATCH 02/14] OSConsistencyManager & related classes Motivation: Manages (kafka) offsets that function as read-your-write tokens for more accurate segment membership calculation. The manager works based on conditions & offsets. Offsets are stored in a nested map indexed by a unique id (e.g. `onesignalId`) and offset key (e.g. `USER_UPDATE`). This allows us to track offsets on a per-user basis (e.g. handle switching users). Conditions work by creating a blocking mechanism with customizable offset retrieval until a pre-defined condition is met (e.g. at least two specific offsets are available). OSCondition interface: allows extensibility for future applications to control offset blocking mechanism in consistency use-cases. --- .../Source/Consistency/OSCondition.swift | 16 + .../Consistency/OSConsistencyKeyEnum.swift | 12 + .../Consistency/OSConsistencyManager.swift | 88 ++++ .../OSConsistencyManagerTests.swift | 387 ++++++++++++++++++ 4 files changed, 503 insertions(+) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSCondition.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyKeyEnum.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSCondition.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSCondition.swift new file mode 100644 index 000000000..58983846a --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSCondition.swift @@ -0,0 +1,16 @@ +// +// OSCondition.swift +// OneSignalOSCore +// +// Created by Rodrigo Gomez-Palacio on 9/10/24. +// Copyright © 2024 OneSignal. All rights reserved. +// + +import Foundation + +@objc public protocol OSCondition: AnyObject { + // Each conforming class will provide its unique ID + var conditionId: String { get } + func isMet(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> Bool + func getNewestToken(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> OSReadYourWriteData? +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyKeyEnum.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyKeyEnum.swift new file mode 100644 index 000000000..fc67803b9 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyKeyEnum.swift @@ -0,0 +1,12 @@ +// +// OSConsistencyKeyEnum.swift +// OneSignalOSCore +// +// Created by Rodrigo Gomez-Palacio on 9/10/24. +// Copyright © 2024 OneSignal. All rights reserved. +// + +import Foundation + +// Protocol for enums with Int raw values. +public protocol OSConsistencyKeyEnum: RawRepresentable where RawValue == Int { } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift new file mode 100644 index 000000000..79362c351 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift @@ -0,0 +1,88 @@ +// +// OSConsistencyManager.swift +// OneSignalOSCore +// +// Created by Rodrigo Gomez-Palacio on 9/10/24. +// Copyright © 2024 OneSignal. All rights reserved. +// + +import Foundation + +@objc public class OSConsistencyManager: NSObject { + // Singleton instance + @objc public static let shared = OSConsistencyManager() + + private let queue = DispatchQueue(label: "com.consistencyManager.queue") + private var indexedTokens: [String: [NSNumber: OSReadYourWriteData]] = [:] + private var indexedConditions: [String: [(OSCondition, DispatchSemaphore)]] = [:] // Index conditions by condition id + + // Private initializer to prevent multiple instances + private override init() {} + + // Used for testing + public func reset() { + indexedTokens = [:] + indexedConditions = [:] + } + + // Function to set the token in a thread-safe manner + public func setRywTokenAndDelay(id: String, key: any OSConsistencyKeyEnum, value: OSReadYourWriteData) { + queue.sync { + let nsKey = NSNumber(value: key.rawValue) + if self.indexedTokens[id] == nil { + self.indexedTokens[id] = [:] + } + self.indexedTokens[id]?[nsKey] = value + self.checkConditionsAndComplete(forId: id) // Only check conditions for this specific ID + } + } + + // Register a condition and block the caller until the condition is met + @objc public func getRywTokenFromAwaitableCondition(_ condition: OSCondition, forId id: String) -> OSReadYourWriteData? { + let semaphore = DispatchSemaphore(value: 0) + queue.sync { + if self.conditions[id] == nil { + self.conditions[id] = [] + } + self.conditions[id]?.append((condition, semaphore)) + self.checkConditionsAndComplete(forId: id) + } + semaphore.wait() // Block until the condition is met + return queue.sync { + return condition.getNewestToken(indexedTokens: self.indexedTokens) + } + } + + // Method to resolve conditions by condition ID (e.g. OSIamFetchReadyCondition.ID) + @objc public func resolveConditionsWithID(id: String) { + guard let conditionList = conditions[id] else { return } + var completedConditions: [(OSCondition, DispatchSemaphore)] = [] + for (condition, semaphore) in conditionList { + if (condition.conditionId == id) { + semaphore.signal() + completedConditions.append((condition, semaphore)) + } + } + conditions[id]?.removeAll { condition, semaphore in + completedConditions.contains(where: { $0.0 === condition && $0.1 == semaphore }) + } + } + + // Private method to check conditions for a specific id (unique ID like onesignalId) + private func checkConditionsAndComplete(forId id: String) { + guard let conditionList = conditions[id] else { return } + var completedConditions: [(OSCondition, DispatchSemaphore)] = [] + for (condition, semaphore) in conditionList { + if condition.isMet(indexedTokens: indexedTokens) { + print("Condition met for id: \(id)") + semaphore.signal() + completedConditions.append((condition, semaphore)) + } else { + print("Condition not met for id: \(id)") + } + } + conditions[id]?.removeAll { condition, semaphore in + completedConditions.contains(where: { $0.0 === condition && $0.1 == semaphore }) + } + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift new file mode 100644 index 000000000..215c298f9 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift @@ -0,0 +1,387 @@ +// +// OSConsistencyManagerTests.swift +// UnitTests +// +// Created by rodrigo on 9/10/24. +// Copyright © 2024 Hiptic. All rights reserved. +// + +import Foundation +import XCTest +import OneSignalOSCore + +class OSConsistencyManagerTests: XCTestCase { + var consistencyManager: OSConsistencyManager! + + override func setUp() { + super.setUp() + // Use the shared instance of OSConsistencyManager + consistencyManager = OSConsistencyManager.shared + } + + override func tearDown() { + consistencyManager.reset() + super.tearDown() + } + + // Test: setRywToken updates the token correctly + func testSetRywTokenUpdatesTokenCorrectly() { + let expectation = self.expectation(description: "Condition met") + + // Given + let id = "test_id" + let key = OSIamFetchOffsetKey.userUpdate + let rywToken = "123" + let rywDelay = 500 + let rywData = OSReadYourWriteData(rywToken: rywToken, rywDelay: rywDelay as NSNumber) + + // Set the token + consistencyManager.setRywTokenAndDelay( + id: id, + key: key, + value: rywData + ) + + // Create a condition that expects the value to be set + let condition = TestMetCondition(expectedTokens: [id: [NSNumber(value: key.rawValue): rywData]]) + + // Register the condition + DispatchQueue.global().async { + let rywDataFromCondition = self.consistencyManager.getRywTokenFromAwaitableCondition(condition, forId: id) + + // Assert that the result is the same as the value set + XCTAssertEqual(rywDataFromCondition, rywData, "Objects are not equal") + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0, handler: { error in + if let error = error { + XCTFail("Test timed out: \(error)") + } + }) + } + + // Test: registerCondition completes when the condition is met + func testRegisterConditionCompletesWhenConditionIsMet() { + let expectation = self.expectation(description: "Condition met") + + let id = "test_id" + let key = OSIamFetchOffsetKey.userUpdate + let rywToken = "123" + let rywDelay = 500 as NSNumber + let value = OSReadYourWriteData(rywToken: rywToken, rywDelay: rywDelay) + + // Set the token to meet the condition + consistencyManager.setRywTokenAndDelay( + id: id, + key: key, + value: value + ) + + // Create a condition that expects the value to be set + let condition = TestMetCondition(expectedTokens: [id: [NSNumber(value: key.rawValue): value]]) + + let rywTokenFromCondition = consistencyManager.getRywTokenFromAwaitableCondition(condition, forId: id) + + XCTAssertNotNil(rywTokenFromCondition) + XCTAssertEqual(rywTokenFromCondition, value) + expectation.fulfill() + + waitForExpectations(timeout: 1, handler: nil) + } + + // Test: registerCondition does not complete when condition is not met + func testRegisterConditionDoesNotCompleteWhenConditionIsNotMet() { + // Given a condition that will never be met + let condition = TestUnmetCondition() + let id = "test_id" + let rywDelay = 500 as NSNumber + + // Start on a background queue to simulate async behavior + DispatchQueue.global().async { + // Register the condition asynchronously + let rywData = self.consistencyManager.getRywTokenFromAwaitableCondition(condition, forId: id) + + // Since the condition will never be met, rywToken should remain nil + XCTAssertNil(rywData) + + // Set an unrelated token to verify that the unmet condition still doesn't complete + self.consistencyManager.setRywTokenAndDelay( + id: "unrelated_id", + key: OSIamFetchOffsetKey.userUpdate, + value: OSReadYourWriteData(rywToken: "unrelated", rywDelay: rywDelay) + ) + + // newest token should still be nil as the condition is not met + XCTAssertNil(rywData) + } + + // Use a short delay to let the async behavior complete without waiting indefinitely + DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { + XCTAssertTrue(true) // Simulate some async action completing without hanging + } + } + + func testSetRywTokenWithoutAnyCondition() { + // Given + let id = "test_id" + let key = OSIamFetchOffsetKey.userUpdate + let value = "123" + let rywDelay = 500 as NSNumber + + consistencyManager.setRywTokenAndDelay( + id: id, + key: key, + value: OSReadYourWriteData(rywToken: value, rywDelay: rywDelay) + ) + + // There is no condition registered, so we just check that no errors occur + XCTAssertTrue(true) // If no errors occur, this test will pass + } + + func testMultipleConditionsWithDifferentKeys() { + let expectation1 = self.expectation(description: "UserUpdate condition met") + let expectation2 = self.expectation(description: "SubscriptionUpdate condition met") + + // Given + let id = "test_id" + let userUpdateKey = OSIamFetchOffsetKey.userUpdate + let subscriptionUpdateKey = OSIamFetchOffsetKey.subscriptionUpdate + let userUpdateRywToken = "123" + let userUpdateRywDelay = 1 as NSNumber + let userUpdateRywData = OSReadYourWriteData(rywToken: userUpdateRywToken, rywDelay: userUpdateRywDelay) + let subscriptionUpdateRywToken = "456" + let subscriptionUpdateRywDelay = 1 as NSNumber + let subscriptionUpdateRywData = OSReadYourWriteData(rywToken: subscriptionUpdateRywToken, rywDelay: subscriptionUpdateRywDelay) + + // Create a serial queue to prevent race conditions + let serialQueue = DispatchQueue(label: "com.consistencyManager.test.serialQueue") + + // Register two conditions for different keys + let userUpdateCondition = TestMetCondition(expectedTokens: [id: [NSNumber(value: userUpdateKey.rawValue): userUpdateRywData]]) + let subscriptionCondition = TestMetCondition(expectedTokens: [id: [NSNumber(value: subscriptionUpdateKey.rawValue): subscriptionUpdateRywData]]) + + // Set the userUpdate token first and verify its condition + serialQueue.async { + self.consistencyManager.setRywTokenAndDelay( + id: id, + key: userUpdateKey, + value: userUpdateRywData + ) + + // Introduce a short delay before checking the condition to ensure the token is set + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + let newestUserUpdateToken = self.registerConditionWithTimeout(userUpdateCondition, forId: id) + XCTAssertEqual(newestUserUpdateToken, userUpdateRywData) + expectation1.fulfill() + } + } + + // Set the subscriptionUpdate token separately and verify its condition after a short delay + serialQueue.asyncAfter(deadline: .now() + 1.0) { + self.consistencyManager.setRywTokenAndDelay( + id: id, + key: subscriptionUpdateKey, + value: OSReadYourWriteData(rywToken: subscriptionUpdateRywToken, rywDelay: subscriptionUpdateRywDelay) + ) + + // Introduce a short delay before checking the condition to ensure the token is set + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + let subscriptionRywData = self.registerConditionWithTimeout(subscriptionCondition, forId: id) + XCTAssertEqual(subscriptionRywData?.rywToken, subscriptionUpdateRywToken) + expectation2.fulfill() + } + } + + // Wait for both expectations to be fulfilled + wait(for: [expectation1, expectation2], timeout: 3.0) + } + + private func registerConditionWithTimeout(_ condition: OSCondition, forId id: String) -> OSReadYourWriteData? { + // This function wraps the registerCondition method with a timeout for testing + let semaphore = DispatchSemaphore(value: 0) + var result: OSReadYourWriteData? + + DispatchQueue.global().async { + result = self.consistencyManager.getRywTokenFromAwaitableCondition(condition, forId: id) + semaphore.signal() // Signal once the condition is met + } + + // Wait for up to 2 seconds to prevent hanging in tests + if semaphore.wait(timeout: .now() + 2.0) == .timedOut { + XCTFail("Condition was not met within the timeout period") + return nil + } + + return result + } + + func testConditionMetImmediatelyAfterTokenAlreadySet() { + let expectation = self.expectation(description: "Condition met immediately") + + // Given + let id = "test_id" + let key = OSIamFetchOffsetKey.userUpdate + let rywToken = "123" + let rywDelay = 500 as NSNumber + let value = OSReadYourWriteData(rywToken: rywToken, rywDelay: rywDelay) + + // First, set the token + consistencyManager.setRywTokenAndDelay( + id: id, + key: key, + value: value + ) + + // Now, register a condition expecting the token that was already set + let condition = TestMetCondition(expectedTokens: [id: [NSNumber(value: key.rawValue): value]]) + + // Register the condition + DispatchQueue.global().async { + let rywData = self.consistencyManager.getRywTokenFromAwaitableCondition(condition, forId: id) + + // Assert that the result is immediately the same as the value set, without waiting + XCTAssertEqual(rywData, value) + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0, handler: nil) + } + + func testConcurrentUpdatesToTokens() { + let expectation = self.expectation(description: "Concurrent updates handled correctly") + + let id = "test_id" + let key = OSIamFetchOffsetKey.userUpdate + let rywToken1 = "123" + let rywToken2 = "456" + let rywDelay = 0 as NSNumber + let value1 = OSReadYourWriteData(rywToken: rywToken1, rywDelay: rywDelay) + let value2 = OSReadYourWriteData(rywToken: rywToken2, rywDelay: rywDelay) + + // Set up concurrent queues + let queue1 = DispatchQueue(label: "com.test.queue1", attributes: .concurrent) + let queue2 = DispatchQueue(label: "com.test.queue2", attributes: .concurrent) + + // Perform concurrent token updates + queue1.async { + self.consistencyManager.setRywTokenAndDelay( + id: id, + key: key, + value: OSReadYourWriteData(rywToken: rywToken1, rywDelay: rywDelay) + ) + } + + queue2.async { + self.consistencyManager.setRywTokenAndDelay( + id: id, + key: key, + value: OSReadYourWriteData(rywToken: rywToken2, rywDelay: rywDelay) + ) + } + + // Allow some time for the updates to happen + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + // Check that the most recent value was correctly set + let condition = TestMetCondition(expectedTokens: [id: [NSNumber(value: key.rawValue): value2]]) + let rywData = self.consistencyManager.getRywTokenFromAwaitableCondition(condition, forId: id) + + XCTAssertEqual(rywData?.rywToken, "456") + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0, handler: nil) + } +} + + +// Mock implementation of OSCondition that simulates a condition that isn't met +class TestUnmetCondition: NSObject, OSCondition { + // class-level constant for the ID + public static let CONDITIONID = "TestUnmetCondition" + + public var conditionId: String { + return TestUnmetCondition.CONDITIONID + } + + func isMet(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> Bool { + return false // Always returns false to simulate an unmet condition + } + + func getNewestToken(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> OSReadYourWriteData? { + return nil + } +} + +// Mock implementation of OSCondition for cases where the condition is met +class TestMetCondition: NSObject, OSCondition { + private let expectedTokens: [String: [NSNumber: OSReadYourWriteData]] + + // class-level constant for the ID + public static let CONDITIONID = "TestMetCondition" + + public var conditionId: String { + return TestMetCondition.CONDITIONID + } + + init(expectedTokens: [String: [NSNumber: OSReadYourWriteData]]) { + self.expectedTokens = expectedTokens + } + + func isMet(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> Bool { + print("Expected tokens: \(expectedTokens)") + print("Actual tokens: \(indexedTokens)") + + // Check if all the expected tokens are present in the actual tokens + for (id, expectedTokenMap) in expectedTokens { + guard let actualTokenMap = indexedTokens[id] else { + print("No tokens found for id: \(id)") + return false + } + + // Check if all expected keys (e.g., userUpdate, subscriptionUpdate) are present with the correct value + for (expectedKey, expectedValue) in expectedTokenMap { + guard let actualValue = actualTokenMap[expectedKey] else { + print("Key \(expectedKey) not found in actual tokens") + return false + } + + if actualValue != expectedValue { + print("Mismatch for key \(expectedKey): expected \(expectedValue.rywToken), found \(actualValue.rywToken)") + return false + } + } + } + + print("Condition met for id") + return true + } + + func getNewestToken(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> OSReadYourWriteData? { + var dataBasedOnNewestRywToken: OSReadYourWriteData? = nil + + // Loop through the token maps and compare the values + for tokenMap in indexedTokens.values { + // Flatten all OSReadYourWriteData objects into an array + let allDataObjects = tokenMap.values.compactMap { $0 } + + // Find the object with the max rywToken (if available) + let maxTokenObject = allDataObjects.max { + ($0.rywToken ?? "") < ($1.rywToken ?? "") + } + + // Safely unwrap and compare rywToken values + if let maxToken = maxTokenObject?.rywToken, + let currentMaxToken = dataBasedOnNewestRywToken?.rywToken { + if maxToken > currentMaxToken { + dataBasedOnNewestRywToken = maxTokenObject + } + } else if maxTokenObject != nil { + // If dataBasedOnNewestRywToken is nil, assign the current max token object + dataBasedOnNewestRywToken = maxTokenObject + } + } + return dataBasedOnNewestRywToken + } + +} From 05d327d6c9cbb8bb28277b64a1b6edf6d449c8d6 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Tue, 10 Sep 2024 12:52:04 -0500 Subject: [PATCH 03/14] IamFetch consistency use-case implementation Motivation: custom condition to block offset retrieval until condition is met: user & subscription offsets are both available or just the user offset is available. This is because we always make a user update call (update the session count) but not always for subscription. --- .../IamFetch/OSIamFetchOffsetKey.swift | 34 +++++++ .../IamFetch/OSIamFetchReadyCondition.swift | 92 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchOffsetKey.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchOffsetKey.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchOffsetKey.swift new file mode 100644 index 000000000..915f7b275 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchOffsetKey.swift @@ -0,0 +1,34 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import Foundation + +public enum OSIamFetchOffsetKey: Int, OSConsistencyKeyEnum { + case userCreate = 0 + case userUpdate = 1 + case subscriptionUpdate = 2 +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift new file mode 100644 index 000000000..5f5a606c1 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift @@ -0,0 +1,92 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +@objc public class OSIamFetchReadyCondition: NSObject, OSCondition { + // the id used to index the token map (e.g. onesignalId) + private let id: String + private var hasSubscriptionUpdatePending: Bool = false + + // Singleton shared instance initialized with default empty id + private static var instance: OSIamFetchReadyCondition? + + // Method to get or initialize the shared instance + @objc public static func sharedInstance(withId id: String) -> OSIamFetchReadyCondition { + if instance == nil { + instance = OSIamFetchReadyCondition(id: id) + } + return instance! + } + + // Private initializer to prevent external instantiation + private init(id: String) { + self.id = id + } + + // Expose the constant to Objective-C + @objc public static let CONDITIONID: String = "OSIamFetchReadyCondition" + + public var conditionId: String { + return OSIamFetchReadyCondition.CONDITIONID + } + + public func setSubscriptionUpdatePending(value: Bool) { + hasSubscriptionUpdatePending = value + } + + public func isMet(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> Bool { + guard let tokenMap = indexedTokens[id] else { return false } + + let userCreateTokenSet = tokenMap[NSNumber(value: OSIamFetchOffsetKey.userCreate.rawValue)] != nil + let userUpdateTokenSet = tokenMap[NSNumber(value: OSIamFetchOffsetKey.userUpdate.rawValue)] != nil + let subscriptionTokenSet = tokenMap[NSNumber(value: OSIamFetchOffsetKey.subscriptionUpdate.rawValue)] != nil + + if (userCreateTokenSet) { + return true; + } + + if (hasSubscriptionUpdatePending) { + return userUpdateTokenSet && subscriptionTokenSet + } + return userUpdateTokenSet + } + + public func getNewestToken(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> OSReadYourWriteData? { + // Check if the token map for the given `id` exists + guard let tokenMap = indexedTokens[id] else { return nil } + + // Flatten all OSReadYourWriteData objects into an array + let allDataObjects = tokenMap.values.compactMap { $0 } + + // Find the object with the max rywToken (if available) + let maxTokenObject = allDataObjects.max { + ($0.rywToken ?? "") < ($1.rywToken ?? "") + } + + return maxTokenObject + } + +} From 6f404096a85f50f44a967ef1a8b6801983114ffd Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Tue, 10 Sep 2024 12:52:37 -0500 Subject: [PATCH 04/14] Update project.pbxproj Motivation: commit new file references --- .../OneSignal.xcodeproj/project.pbxproj | 283 +++++++++++++++++- 1 file changed, 282 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 970382b96..87921f196 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -233,7 +233,15 @@ 475F474F2B8E3B5400EC05B3 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 475F471E2B8E398D00EC05B3 /* OneSignalLiveActivities.framework */; }; 475F47502B8E3B5400EC05B3 /* OneSignalLiveActivities.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 475F471E2B8E398D00EC05B3 /* OneSignalLiveActivities.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 47A885CD2BB317B300ED91FA /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A885CC2BB317B300ED91FA /* AnyCodable.swift */; }; + 5B053FBC2CAE07EB002F30C4 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; }; + 5B053FC32CAE0843002F30C4 /* OSConsistencyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1DE672C90C23E00CA8807 /* OSConsistencyManagerTests.swift */; }; 5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B58E4F6237CE7B4009401E0 /* UIDeviceOverrider.m */; }; + 5B58F09E2CC1B5C700298493 /* OSReadYourWriteData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B58F09D2CC1B5C700298493 /* OSReadYourWriteData.swift */; }; + 5BC1DE5C2C90B7E600CA8807 /* OSConsistencyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1DE5B2C90B7E600CA8807 /* OSConsistencyManager.swift */; }; + 5BC1DE5E2C90B80E00CA8807 /* OSCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1DE5D2C90B80E00CA8807 /* OSCondition.swift */; }; + 5BC1DE602C90B83900CA8807 /* OSConsistencyKeyEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1DE5F2C90B83900CA8807 /* OSConsistencyKeyEnum.swift */; }; + 5BC1DE622C90B85A00CA8807 /* OSIamFetchOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1DE612C90B85A00CA8807 /* OSIamFetchOffsetKey.swift */; }; + 5BC1DE642C90BB9000CA8807 /* OSIamFetchReadyCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1DE632C90BB9000CA8807 /* OSIamFetchReadyCondition.swift */; }; 7A123295235DFE3B002B6CE3 /* OutcomeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A123294235DFE3B002B6CE3 /* OutcomeTests.m */; }; 7A1232A2235E1743002B6CE3 /* OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 912411F11E73342200E41FD7 /* OneSignal.m */; }; 7A2E90622460DA1500B3428C /* OutcomeIntegrationV2Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E90612460DA1500B3428C /* OutcomeIntegrationV2Tests.m */; }; @@ -844,6 +852,20 @@ remoteGlobalIDString = 475F471D2B8E398D00EC05B3; remoteInfo = OneSignalLiveActivities; }; + 5B053FBD2CAE07EB002F30C4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 37747F8B19147D6400558FAD /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3C115160289A259500565C41; + remoteInfo = OneSignalOSCore; + }; + 5B053FC42CAE08A1002F30C4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 37747F8B19147D6400558FAD /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEF5CCF02539321A0003E9CC; + remoteInfo = UnitTestApp; + }; DE12F3F4289B28C4002F63AA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 37747F8B19147D6400558FAD /* Project object */; @@ -1358,8 +1380,16 @@ 475F47412B8E3A0A00EC05B3 /* OSRequestRemoveStartToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OSRequestRemoveStartToken.swift; path = Source/Requests/OSRequestRemoveStartToken.swift; sourceTree = ""; }; 475F47482B8E3A4400EC05B3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = OneSignalLiveActivitiesFramework/Info.plist; sourceTree = ""; }; 47A885CC2BB317B300ED91FA /* AnyCodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnyCodable.swift; path = Source/AnyCodable.swift; sourceTree = ""; }; + 5B053FB82CAE07EB002F30C4 /* OneSignalOSCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OneSignalOSCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5B58E4F3237CE7B3009401E0 /* UIDeviceOverrider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIDeviceOverrider.h; sourceTree = ""; }; 5B58E4F6237CE7B4009401E0 /* UIDeviceOverrider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIDeviceOverrider.m; sourceTree = ""; }; + 5B58F09D2CC1B5C700298493 /* OSReadYourWriteData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSReadYourWriteData.swift; sourceTree = ""; }; + 5BC1DE5B2C90B7E600CA8807 /* OSConsistencyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSConsistencyManager.swift; sourceTree = ""; }; + 5BC1DE5D2C90B80E00CA8807 /* OSCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCondition.swift; sourceTree = ""; }; + 5BC1DE5F2C90B83900CA8807 /* OSConsistencyKeyEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSConsistencyKeyEnum.swift; sourceTree = ""; }; + 5BC1DE612C90B85A00CA8807 /* OSIamFetchOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIamFetchOffsetKey.swift; sourceTree = ""; }; + 5BC1DE632C90BB9000CA8807 /* OSIamFetchReadyCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIamFetchReadyCondition.swift; sourceTree = ""; }; + 5BC1DE672C90C23E00CA8807 /* OSConsistencyManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSConsistencyManagerTests.swift; sourceTree = ""; }; 7A123294235DFE3B002B6CE3 /* OutcomeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OutcomeTests.m; sourceTree = ""; }; 7A12EBD523060A6F005C4FA5 /* OSSessionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSSessionManager.m; sourceTree = ""; }; 7A12EBD623060A6F005C4FA5 /* OSSessionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSSessionManager.h; sourceTree = ""; }; @@ -1783,6 +1813,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5B053FB52CAE07EB002F30C4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B053FBC2CAE07EB002F30C4 /* OneSignalOSCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 911E2CB71E398AB3003112A4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1971,6 +2009,7 @@ 3CC063A52B6D7A8E002BB07F /* OneSignalCoreTests */, 3CC063EC2B6D7FE8002BB07F /* OneSignalUserTests */, 3C01518F2C2E298F0079E076 /* OneSignalInAppMessagesTests */, + 5B053FB92CAE07EB002F30C4 /* OneSignalOSCoreTests */, 4735424B2B8F93340016DB4C /* OneSignalLiveActivitiesTests */, 37747F9419147D6500558FAD /* Products */, ); @@ -2000,6 +2039,7 @@ DEBA2A1A2C20E35E00E234DB /* OneSignalNotificationsTests.xctest */, 3C01518E2C2E298E0079E076 /* OneSignalInAppMessagesTests.xctest */, 3C8544B62C5AEFF600F542A9 /* OneSignalOSCoreMocks.framework */, + 5B053FB82CAE07EB002F30C4 /* OneSignalOSCoreTests.xctest */, ); name = Products; sourceTree = ""; @@ -2053,6 +2093,7 @@ 3C115178289A272F00565C41 /* Source */ = { isa = PBXGroup; children = ( + 5BC1DE652C90BC9F00CA8807 /* Consistency */, 3C115163289A259500565C41 /* OneSignalOSCore.h */, 3C115188289ADEA300565C41 /* OSModelStore.swift */, 3C115186289ADE7700565C41 /* OSModelStoreListener.swift */, @@ -2304,6 +2345,35 @@ name = OneSignalLiveActivitiesFramework; sourceTree = ""; }; + 5B053FB92CAE07EB002F30C4 /* OneSignalOSCoreTests */ = { + isa = PBXGroup; + children = ( + 5BC1DE672C90C23E00CA8807 /* OSConsistencyManagerTests.swift */, + ); + path = OneSignalOSCoreTests; + sourceTree = ""; + }; + 5BC1DE652C90BC9F00CA8807 /* Consistency */ = { + isa = PBXGroup; + children = ( + 5BC1DE662C90BCD100CA8807 /* IamFetch */, + 5BC1DE5B2C90B7E600CA8807 /* OSConsistencyManager.swift */, + 5BC1DE5D2C90B80E00CA8807 /* OSCondition.swift */, + 5BC1DE5F2C90B83900CA8807 /* OSConsistencyKeyEnum.swift */, + 5B58F09D2CC1B5C700298493 /* OSReadYourWriteData.swift */, + ); + path = Consistency; + sourceTree = ""; + }; + 5BC1DE662C90BCD100CA8807 /* IamFetch */ = { + isa = PBXGroup; + children = ( + 5BC1DE612C90B85A00CA8807 /* OSIamFetchOffsetKey.swift */, + 5BC1DE632C90BB9000CA8807 /* OSIamFetchReadyCondition.swift */, + ); + path = IamFetch; + sourceTree = ""; + }; 7A5A818324899050002E07C8 /* Models */ = { isa = PBXGroup; children = ( @@ -3416,6 +3486,25 @@ productReference = 475F471E2B8E398D00EC05B3 /* OneSignalLiveActivities.framework */; productType = "com.apple.product-type.framework"; }; + 5B053FB72CAE07EB002F30C4 /* OneSignalOSCoreTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5B053FC22CAE07EB002F30C4 /* Build configuration list for PBXNativeTarget "OneSignalOSCoreTests" */; + buildPhases = ( + 5B053FB42CAE07EB002F30C4 /* Sources */, + 5B053FB52CAE07EB002F30C4 /* Frameworks */, + 5B053FB62CAE07EB002F30C4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5B053FBE2CAE07EB002F30C4 /* PBXTargetDependency */, + 5B053FC52CAE08A1002F30C4 /* PBXTargetDependency */, + ); + name = OneSignalOSCoreTests; + productName = OneSignalOSCoreTests; + productReference = 5B053FB82CAE07EB002F30C4 /* OneSignalOSCoreTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 911E2CB91E398AB3003112A4 /* UnitTests */ = { isa = PBXNativeTarget; buildConfigurationList = 911E2CC41E398AB3003112A4 /* Build configuration list for PBXNativeTarget "UnitTests" */; @@ -3634,7 +3723,7 @@ 37747F8B19147D6400558FAD /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1520; + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 0800; ORGANIZATIONNAME = Hiptic; TargetAttributes = { @@ -3700,6 +3789,10 @@ LastSwiftMigration = 1520; ProvisioningStyle = Automatic; }; + 5B053FB72CAE07EB002F30C4 = { + CreatedOnToolsVersion = 15.4; + TestTargetID = DEF5CCF02539321A0003E9CC; + }; 911E2CB91E398AB3003112A4 = { CreatedOnToolsVersion = 8.1; DevelopmentTeam = 99SW8E36CT; @@ -3805,6 +3898,7 @@ 473542492B8F93330016DB4C /* OneSignalLiveActivitiesTests */, DEBA2A192C20E35E00E234DB /* OneSignalNotificationsTests */, 3C01518D2C2E298E0079E076 /* OneSignalInAppMessagesTests */, + 5B053FB72CAE07EB002F30C4 /* OneSignalOSCoreTests */, ); }; /* End PBXProject section */ @@ -3883,6 +3977,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5B053FB62CAE07EB002F30C4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 911E2CB81E398AB3003112A4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4050,18 +4151,24 @@ buildActionMask = 2147483647; files = ( DEFB3E652BB7346D00E65DAD /* OSLiveActivities.swift in Sources */, + 5BC1DE602C90B83900CA8807 /* OSConsistencyKeyEnum.swift in Sources */, 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */, 3C11518B289ADEEB00565C41 /* OSEventProducer.swift in Sources */, 3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */, + 5BC1DE5E2C90B80E00CA8807 /* OSCondition.swift in Sources */, + 5BC1DE5C2C90B7E600CA8807 /* OSConsistencyManager.swift in Sources */, 3C115189289ADEA300565C41 /* OSModelStore.swift in Sources */, 3C115185289ADE4F00565C41 /* OSModel.swift in Sources */, 3CF1A5632C669EA40056B3AA /* OSNewRecordsState.swift in Sources */, 3C448BA22936B474002F96BC /* OSBackgroundTaskManager.swift in Sources */, + 5B58F09E2CC1B5C700298493 /* OSReadYourWriteData.swift in Sources */, 3C115187289ADE7700565C41 /* OSModelStoreListener.swift in Sources */, + 5BC1DE642C90BB9000CA8807 /* OSIamFetchReadyCondition.swift in Sources */, 3CE5F9E3289D88DC004A156E /* OSModelStoreChangedHandler.swift in Sources */, 3C2D8A5928B4C4E300BE41F6 /* OSDelta.swift in Sources */, 4710EA532B8FCFB200435356 /* OSDispatchQueue.swift in Sources */, DEFB3E672BB735B500E65DAD /* OSStubLiveActivities.swift in Sources */, + 5BC1DE622C90B85A00CA8807 /* OSIamFetchOffsetKey.swift in Sources */, 3C11518D289AF5E800565C41 /* OSModelChangedHandler.swift in Sources */, 3C8E6DF928A6D89E0031E48A /* OSOperationExecutor.swift in Sources */, ); @@ -4174,6 +4281,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5B053FB42CAE07EB002F30C4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B053FC32CAE0843002F30C4 /* OSConsistencyManagerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 911E2CB61E398AB3003112A4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -4620,6 +4735,16 @@ target = 475F471D2B8E398D00EC05B3 /* OneSignalLiveActivities */; targetProxy = 475F47512B8E3B5400EC05B3 /* PBXContainerItemProxy */; }; + 5B053FBE2CAE07EB002F30C4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3C115160289A259500565C41 /* OneSignalOSCore */; + targetProxy = 5B053FBD2CAE07EB002F30C4 /* PBXContainerItemProxy */; + }; + 5B053FC52CAE08A1002F30C4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEF5CCF02539321A0003E9CC /* UnitTestApp */; + targetProxy = 5B053FC42CAE08A1002F30C4 /* PBXContainerItemProxy */; + }; DE12F3F5289B28C4002F63AA /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 3C115160289A259500565C41 /* OneSignalOSCore */; @@ -6303,6 +6428,152 @@ }; name = Test; }; + 5B053FBF2CAE07EB002F30C4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 99SW8E36CT; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalOSCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp"; + }; + name = Release; + }; + 5B053FC02CAE07EB002F30C4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 99SW8E36CT; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalOSCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp"; + }; + name = Debug; + }; + 5B053FC12CAE07EB002F30C4 /* Test */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 99SW8E36CT; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalOSCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp"; + }; + name = Test; + }; CA2951B62167F4120064227A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -8673,6 +8944,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 5B053FC22CAE07EB002F30C4 /* Build configuration list for PBXNativeTarget "OneSignalOSCoreTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5B053FBF2CAE07EB002F30C4 /* Release */, + 5B053FC02CAE07EB002F30C4 /* Debug */, + 5B053FC12CAE07EB002F30C4 /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 911E2CC41E398AB3003112A4 /* Build configuration list for PBXNativeTarget "UnitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( From b07df39a50353f45e73656da0faf395c3f47cd59 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Tue, 17 Sep 2024 15:40:38 -0500 Subject: [PATCH 05/14] Move `getTimeFocusedElapsed` from `OneSignalTracker` to `OSSessionManager` Motivation: move into `OSSessionManager` to be accessible from other modules --- .../Source/OutcomeEvents/OSSessionManager.h | 4 +++ .../Source/OutcomeEvents/OSSessionManager.m | 19 +++++++++++ .../OneSignalSDK/Source/OneSignalTracker.m | 33 +++---------------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OSSessionManager.h b/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OSSessionManager.h index a1616f737..8b89e685e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OSSessionManager.h +++ b/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OSSessionManager.h @@ -40,6 +40,10 @@ + (void)resetSharedSessionManager; +- (NSTimeInterval)getTimeFocusedElapsed; + +- (void)setLastOpenedTime:(NSTimeInterval)lastOpened; + @property (nonatomic) id _Nullable delegate; @property AppEntryAction appEntryState; diff --git a/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OSSessionManager.m b/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OSSessionManager.m index 23edf01ed..004ccff80 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OSSessionManager.m +++ b/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OSSessionManager.m @@ -42,6 +42,7 @@ @implementation OSSessionManager NSDate *_sessionLaunchTime; AppEntryAction _appEntryState = APP_CLOSE; +static NSTimeInterval lastOpenedTime; + (OSSessionManager*)sharedSessionManager { if (!_sessionManager) @@ -49,6 +50,24 @@ + (OSSessionManager*)sharedSessionManager { return _sessionManager; } +- (void)setLastOpenedTime:(NSTimeInterval)lastOpened { + lastOpenedTime = lastOpened; +} + +- (NSTimeInterval)getTimeFocusedElapsed { + if (!lastOpenedTime) + return -1; + + NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + NSTimeInterval timeElapsed = now - lastOpenedTime; + + // Time is invalid if below 0 or over a day (86400 seconds) + if (timeElapsed < 0 || timeElapsed > 86400) + return -1; + + return timeElapsed; +} + + (void)resetSharedSessionManager { _sessionManager = nil; } diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m index 7c77d1d94..88172d99a 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m @@ -50,19 +50,8 @@ + (NSString *)mExternalIdAuthToken; @implementation OneSignalTracker -static NSTimeInterval lastOpenedTime; static BOOL lastOnFocusWasToBackground = YES; -+ (void)resetLocals { - [OSFocusTimeProcessorFactory resetUnsentActiveTime]; - lastOpenedTime = 0; - lastOnFocusWasToBackground = YES; -} - -+ (void)setLastOpenedTime:(NSTimeInterval)lastOpened { - lastOpenedTime = lastOpened; -} - + (void)onFocus:(BOOL)toBackground { // return if the user has not granted privacy permissions if ([OSPrivacyConsentController requiresUserPrivacyConsent]) @@ -88,7 +77,7 @@ + (void)applicationBecameActive { if (OSSessionManager.sharedSessionManager.appEntryState != NOTIFICATION_CLICK) OSSessionManager.sharedSessionManager.appEntryState = APP_OPEN; - lastOpenedTime = [NSDate date].timeIntervalSince1970; + [OSSessionManager.sharedSessionManager setLastOpenedTime:[[NSDate date] timeIntervalSince1970]]; // on_session tracking when resumming app. if ([OneSignal shouldStartNewSession]) @@ -104,7 +93,7 @@ + (void)applicationBackgrounded { [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"Application Backgrounded started"]; [self updateLastClosedTime]; - let timeElapsed = [self getTimeFocusedElapsed]; + let timeElapsed = [OSSessionManager.sharedSessionManager getTimeFocusedElapsed]; if (timeElapsed < -1) return; @@ -124,7 +113,7 @@ + (void)applicationBackgrounded { // The on_focus call is made right away. + (void)onSessionEnded:(NSArray *)lastInfluences { [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"onSessionEnded started"]; - let timeElapsed = [self getTimeFocusedElapsed]; + let timeElapsed = [OSSessionManager.sharedSessionManager getTimeFocusedElapsed]; let focusCallParams = [self createFocusCallParams:lastInfluences onSessionEnded:true]; let timeProcessor = [OSFocusTimeProcessorFactory createTimeProcessorWithInfluences:lastInfluences focusEventType:END_SESSION]; @@ -141,7 +130,7 @@ + (void)onSessionEnded:(NSArray *)lastInfluences { } + (OSFocusCallParams *)createFocusCallParams:(NSArray *)lastInfluences onSessionEnded:(BOOL)onSessionEnded { - let timeElapsed = [self getTimeFocusedElapsed]; + let timeElapsed = [OSSessionManager.sharedSessionManager getTimeFocusedElapsed]; NSMutableArray *focusInfluenceParams = [NSMutableArray new]; for (OSInfluence *influence in lastInfluences) { @@ -159,20 +148,6 @@ + (OSFocusCallParams *)createFocusCallParams:(NSArray *)lastInflu onSessionEnded:onSessionEnded]; } -+ (NSTimeInterval)getTimeFocusedElapsed { - if (!lastOpenedTime) - return -1; - - let now = [NSDate date].timeIntervalSince1970; - let timeElapsed = now - (int)(lastOpenedTime + 0.5); - - // Time is invalid if below 1 or over a day - if (timeElapsed < 0 || timeElapsed > 86400) - return -1; - - return timeElapsed; -} - + (void)updateLastClosedTime { let now = [NSDate date].timeIntervalSince1970; [OneSignalUserDefaults.initStandard saveDoubleForKey:OSUD_APP_LAST_CLOSED_TIME withValue:now]; From e7d65f695c0c9f09d977227fbabb27dd6f17d384 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Tue, 17 Sep 2024 15:49:53 -0500 Subject: [PATCH 06/14] Modify `OSRequestGetInAppMessages` method to include new header values Motivation: we need to pass the offset, session duration, & retry count to the GET IAM request. --- .../Requests/OSInAppMessagingRequests.h | 2 +- .../Requests/OSInAppMessagingRequests.m | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.h index c37b4bbf1..047592e63 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.h @@ -29,7 +29,7 @@ #import "OSInAppMessageClickResult.h" @interface OSRequestGetInAppMessages : OneSignalRequest -+ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId; ++ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId withSessionDuration:(NSNumber * _Nonnull)sessionDuration withRetryCount:(NSNumber *)retryCount withRywToken:(NSString *)rywToken; @end @interface OSRequestInAppMessageViewed : OneSignalRequest diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m index 6f837a243..58d0455f7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m @@ -28,9 +28,25 @@ of this software and associated documentation files (the "Software"), to deal #import "OSInAppMessagingRequests.h" @implementation OSRequestGetInAppMessages -+ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId { ++ (instancetype _Nonnull) withSubscriptionId:(NSString * _Nonnull)subscriptionId + withSessionDuration:(NSNumber * _Nonnull)sessionDuration + withRetryCount:(NSNumber *)retryCount + withRywToken:(NSString *)rywToken +{ let request = [OSRequestGetInAppMessages new]; request.method = GET; + let headers = [NSMutableDictionary new]; + + if (sessionDuration != nil) { + // convert to ms & round + sessionDuration = @(round([sessionDuration doubleValue] * 1000)); + headers[@"OneSignal-Session-Duration" ] = [sessionDuration stringValue]; + } + headers[@"OneSignal-RYW-Token"] = rywToken; + headers[@"OneSignal-Retry-Count"] = [retryCount stringValue]; + + request.additionalHeaders = headers; + NSString *appId = [OneSignalConfigManager getAppId]; request.path = [NSString stringWithFormat:@"apps/%@/subscriptions/%@/iams", appId, subscriptionId]; return request; From 9ac1053fb862ec7f16fca0233210af511c01740f Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Tue, 17 Sep 2024 16:33:52 -0500 Subject: [PATCH 07/14] OSMessagingController Retry Logic Motivation: retry logic should work as follows: 1. initial request with offset --> failure 2. get retry limit & retry after from response & start retrying 3. if hit limit, make one final request with the offset: 0 The final request tells the backend, just show me what you got. Example: If the retry limit is 3 & we never get a successful response retrying, we should end up with 5 total requests. --- .../Controller/OSMessagingController.m | 164 ++++++++++++++++-- .../Source/Consistency/OSCondition.swift | 33 +++- .../Consistency/OSConsistencyKeyEnum.swift | 33 +++- .../Consistency/OSConsistencyManager.swift | 52 ++++-- 4 files changed, 236 insertions(+), 46 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m index d8418d5ed..cf64cef1b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m @@ -1,7 +1,7 @@ /** * Modified MIT License * - * Copyright 2017 OneSignal + * Copyright 2024 OneSignal * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -35,8 +35,14 @@ #import "OSInAppMessagePrompt.h" #import "OSInAppMessagingRequests.h" #import "OneSignalWebViewManager.h" +#import "OneSignalTracker.h" #import #import "OSSessionManager.h" +#import "OneSignalOSCore/OneSignalOSCore-Swift.h" + +static NSInteger const DEFAULT_RETRY_AFTER_SECONDS = 1; // Default 1 second retry delay +static NSInteger const DEFAULT_RETRY_LIMIT = 0; // If not returned by backend, don't retry +static NSInteger const IAM_FETCH_DELAY_BUFFER = 0.5; // Fallback value if ryw_delay is nil: delay by 500 ms to increase the probability of getting a 200 & not having to retry @implementation OSInAppMessageWillDisplayEvent @@ -242,22 +248,70 @@ - (void)updateInAppMessagesFromCache { } - (void)getInAppMessagesFromServer:(NSString *)subscriptionId { - [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer"]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer"]; - if (!subscriptionId) { - [self updateInAppMessagesFromCache]; - return; - } - - OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId]; - [OneSignalCoreImpl.sharedClient executeRequest:request onSuccess:^(NSDictionary *result) { + if (!subscriptionId) { + [self updateInAppMessagesFromCache]; + return; + } + + OSConsistencyManager *consistencyManager = [OSConsistencyManager shared]; + NSString *onesignalId = OneSignalUserManagerImpl.sharedInstance.onesignalId; + + if (!onesignalId) { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Failed to get in app messages due to no OneSignal ID"]; + return; + } + + OSIamFetchReadyCondition *condition = [OSIamFetchReadyCondition sharedInstanceWithId:onesignalId]; + OSReadYourWriteData *rywData = [consistencyManager getRywTokenFromAwaitableCondition:condition forId:onesignalId]; + + // We need to delay the first request by however long the backend is telling us (`ryw_delay`) + // This will help avoid unnecessary retries & can be easily adjusted from the backend + NSTimeInterval rywDelayInSeconds; + if (rywData.rywDelay) { + rywDelayInSeconds = [rywData.rywDelay doubleValue] / 1000.0; + } else { + rywDelayInSeconds = IAM_FETCH_DELAY_BUFFER; + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rywDelayInSeconds * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + // Initial request + [self attemptFetchWithRetries:subscriptionId + rywData:rywData + attempts:@0 // Starting with 0 attempts + retryLimit:nil]; // Retry limit to be set dynamically on first failure + }); + }); +} + + +- (void)attemptFetchWithRetries:(NSString *)subscriptionId + rywData:(OSReadYourWriteData *)rywData + attempts:(NSNumber *)attempts + retryLimit:(NSNumber *)retryLimit { + NSNumber *sessionDuration = @([OSSessionManager.sharedSessionManager getTimeFocusedElapsed]); + NSString *rywToken = rywData.rywToken; + NSNumber *rywDelay = rywData.rywDelay; + + // Create the request with the current attempt count + OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId + withSessionDuration:sessionDuration + withRetryCount:attempts + withRywToken:rywToken]; + + __block NSNumber *blockRetryLimit = retryLimit; + + [OneSignalCoreImpl.sharedClient executeRequest:request + onSuccess:^(NSDictionary *result) { dispatch_async(dispatch_get_main_queue(), ^{ [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer success"]; - if (result[@"in_app_messages"]) { // when there are no IAMs, will this still be there? - let messages = [NSMutableArray new]; + if (result[@"in_app_messages"]) { + NSMutableArray *messages = [NSMutableArray new]; for (NSDictionary *messageJson in result[@"in_app_messages"]) { - let message = [OSInAppMessageInternal instanceWithJson:messageJson]; + OSInAppMessageInternal *message = [OSInAppMessageInternal instanceWithJson:messageJson]; if (message) { [messages addObject:message]; } @@ -266,11 +320,89 @@ - (void)getInAppMessagesFromServer:(NSString *)subscriptionId { [self updateInAppMessagesFromServer:messages]; return; } + }); + } + onFailure:^(NSError *error) { + NSDictionary *errorInfo = error.userInfo[@"returned"]; + NSNumber *statusCode = errorInfo[@"httpStatusCode"]; + NSDictionary* responseHeaders = errorInfo[@"headers"]; + + if (!statusCode) { + [self updateInAppMessagesFromCache]; + return; + } + + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"getInAppMessagesFromServer failure: %@", error.localizedDescription]]; + + NSInteger code = [statusCode integerValue]; + if (code == 425 || code == 429) { // 425 Too Early or 429 Too Many Requests + NSInteger retryAfter = [responseHeaders[@"Retry-After"] integerValue] ?: DEFAULT_RETRY_AFTER_SECONDS; - // TODO: Check this request and response. If no IAMs returned, should we really get from cache? - // This is the existing implementation but it could mean this user has no IAMs? - - // Default is using cached IAMs in the messaging controller + // Dynamically set the retry limit from the header, if not already set + if (!blockRetryLimit) { + blockRetryLimit = @([responseHeaders[@"OneSignal-Retry-Limit"] integerValue] ?: DEFAULT_RETRY_LIMIT); + } + + if ([attempts integerValue] < [blockRetryLimit integerValue]) { + NSInteger nextAttempt = [attempts integerValue] + 1; // Increment attempts + [self retryAfterDelay:retryAfter + subscriptionId:subscriptionId + rywData:rywData + attempts:@(nextAttempt) + retryLimit:blockRetryLimit]; + } else { + // Final attempt without rywToken + [self fetchInAppMessagesWithoutToken:subscriptionId]; + } + } else if (code >= 500 && code <= 599) { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Server error, skipping retries"]; + [self updateInAppMessagesFromCache]; + } else { + [self updateInAppMessagesFromCache]; + } + }]; +} + +- (void)retryAfterDelay:(NSInteger)retryAfter + subscriptionId:(NSString *)subscriptionId + rywData:(OSReadYourWriteData *)rywData + attempts:(NSNumber *)attempts + retryLimit:(NSNumber *)retryLimit { + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryAfter * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + [self attemptFetchWithRetries:subscriptionId + rywData:rywData + attempts:attempts + retryLimit:retryLimit]; + }); +} + +- (void)fetchInAppMessagesWithoutToken:(NSString *)subscriptionId { + NSNumber *sessionDuration = @([OSSessionManager.sharedSessionManager getTimeFocusedElapsed]); + + OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId + withSessionDuration:sessionDuration + withRetryCount:nil + withRywToken:nil]; // No retries for the final attempt + + [OneSignalCoreImpl.sharedClient executeRequest:request + onSuccess:^(NSDictionary *result) { + dispatch_async(dispatch_get_main_queue(), ^{ + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Final attempt without token success"]; + if (result[@"in_app_messages"]) { + NSMutableArray *messages = [NSMutableArray new]; + + for (NSDictionary *messageJson in result[@"in_app_messages"]) { + OSInAppMessageInternal *message = [OSInAppMessageInternal instanceWithJson:messageJson]; + if (message) { + [messages addObject:message]; + } + } + + [self updateInAppMessagesFromServer:messages]; + return; + } [self updateInAppMessagesFromCache]; }); } onFailure:^(NSError *error) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSCondition.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSCondition.swift index 58983846a..c63807819 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSCondition.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSCondition.swift @@ -1,10 +1,29 @@ -// -// OSCondition.swift -// OneSignalOSCore -// -// Created by Rodrigo Gomez-Palacio on 9/10/24. -// Copyright © 2024 OneSignal. All rights reserved. -// +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ import Foundation diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyKeyEnum.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyKeyEnum.swift index fc67803b9..a70364fd7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyKeyEnum.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyKeyEnum.swift @@ -1,10 +1,29 @@ -// -// OSConsistencyKeyEnum.swift -// OneSignalOSCore -// -// Created by Rodrigo Gomez-Palacio on 9/10/24. -// Copyright © 2024 OneSignal. All rights reserved. -// +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ import Foundation diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift index 79362c351..0f12f7397 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift @@ -1,12 +1,32 @@ -// -// OSConsistencyManager.swift -// OneSignalOSCore -// -// Created by Rodrigo Gomez-Palacio on 9/10/24. -// Copyright © 2024 OneSignal. All rights reserved. -// +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ import Foundation +import OneSignalCore @objc public class OSConsistencyManager: NSObject { // Singleton instance @@ -41,10 +61,10 @@ import Foundation @objc public func getRywTokenFromAwaitableCondition(_ condition: OSCondition, forId id: String) -> OSReadYourWriteData? { let semaphore = DispatchSemaphore(value: 0) queue.sync { - if self.conditions[id] == nil { - self.conditions[id] = [] + if self.indexedConditions[id] == nil { + self.indexedConditions[id] = [] } - self.conditions[id]?.append((condition, semaphore)) + self.indexedConditions[id]?.append((condition, semaphore)) self.checkConditionsAndComplete(forId: id) } semaphore.wait() // Block until the condition is met @@ -55,7 +75,7 @@ import Foundation // Method to resolve conditions by condition ID (e.g. OSIamFetchReadyCondition.ID) @objc public func resolveConditionsWithID(id: String) { - guard let conditionList = conditions[id] else { return } + guard let conditionList = indexedConditions[id] else { return } var completedConditions: [(OSCondition, DispatchSemaphore)] = [] for (condition, semaphore) in conditionList { if (condition.conditionId == id) { @@ -63,25 +83,25 @@ import Foundation completedConditions.append((condition, semaphore)) } } - conditions[id]?.removeAll { condition, semaphore in + indexedConditions[id]?.removeAll { condition, semaphore in completedConditions.contains(where: { $0.0 === condition && $0.1 == semaphore }) } } // Private method to check conditions for a specific id (unique ID like onesignalId) private func checkConditionsAndComplete(forId id: String) { - guard let conditionList = conditions[id] else { return } + guard let conditionList = indexedConditions[id] else { return } var completedConditions: [(OSCondition, DispatchSemaphore)] = [] for (condition, semaphore) in conditionList { if condition.isMet(indexedTokens: indexedTokens) { - print("Condition met for id: \(id)") + OneSignalLog.onesignalLog(.LL_INFO, message: "Condition met for id: \(id)") semaphore.signal() completedConditions.append((condition, semaphore)) } else { - print("Condition not met for id: \(id)") + OneSignalLog.onesignalLog(.LL_INFO, message: "Condition not met for id: \(id)") } } - conditions[id]?.removeAll { condition, semaphore in + indexedConditions[id]?.removeAll { condition, semaphore in completedConditions.contains(where: { $0.0 === condition && $0.1 == semaphore }) } } From d09d4648e3442d34f84af31ecf6f2463bc042efa Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Tue, 17 Sep 2024 16:17:50 -0500 Subject: [PATCH 08/14] Update executors to set offsets on consistency manager Motivation: executors make the update or create requests and get back an offset which is saved in the consistency manager --- .../OSPropertyOperationExecutor.swift | 17 ++++++++- .../OSSubscriptionOperationExecutor.swift | 37 +++++++++++++++++-- .../Source/Executors/OSUserExecutor.swift | 15 ++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 5e1ea285c..74ad0826c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -245,7 +245,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { OSBackgroundTaskManager.beginBackgroundTask(backgroundTaskIdentifier) } - OneSignalCoreImpl.sharedClient().execute(request) { _ in + OneSignalCoreImpl.sharedClient().execute(request) { response in // On success, remove request from cache, and we do need to hydrate // TODO: We need to hydrate after all ? What why ? self.dispatchQueue.async { @@ -255,6 +255,21 @@ class OSPropertyOperationExecutor: OSOperationExecutor { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) } } + if let onesignalId = request.identityModel.onesignalId { + if let rywToken = response?["ryw_token"] as? String + { + let rywDelay = response?["ryw_delay"] as? NSNumber + + OSConsistencyManager.shared.setRywTokenAndDelay( + id: onesignalId, + key: OSIamFetchOffsetKey.userUpdate, + value: OSReadYourWriteData(rywToken: rywToken, rywDelay: rywDelay) + ) + } else { + // handle a potential regression where ryw_token is no longer returned by API + OSConsistencyManager.shared.resolveConditionsWithID(id: OSIamFetchReadyCondition.CONDITIONID) + } + } } onFailure: { error in OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor update properties request failed with error: \(error.debugDescription)") self.dispatchQueue.async { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 84b60ad3b..2ef43dafc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -284,19 +284,35 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSSubscriptionOperationExecutor: executeCreateSubscriptionRequest making request: \(request)") - OneSignalCoreImpl.sharedClient().execute(request) { result in + OneSignalCoreImpl.sharedClient().execute(request) { response in // On success, remove request from cache (even if not hydrating model), and hydrate model self.dispatchQueue.async { self.addRequestQueue.removeAll(where: { $0 == request}) OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) - guard let response = result?["subscription"] as? [String: Any] else { + guard let response = response?["subscription"] as? [String: Any] else { OneSignalLog.onesignalLog(.LL_ERROR, message: "Unabled to parse response to create subscription request") if inBackground { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) } return } + + if let onesignalId = request.identityModel.onesignalId { + if let rywToken = response["ryw_token"] as? String + { + let rywDelay = response["ryw_delay"] as? NSNumber + OSConsistencyManager.shared.setRywTokenAndDelay( + id: onesignalId, + key: OSIamFetchOffsetKey.subscriptionUpdate, + value: OSReadYourWriteData(rywToken: rywToken, rywDelay: rywDelay) + ) + } else { + // handle a potential regression where ryw_token is no longer returned by API + OSConsistencyManager.shared.resolveConditionsWithID(id: OSIamFetchReadyCondition.CONDITIONID) + } + } + request.subscriptionModel.hydrate(response) if inBackground { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) @@ -393,7 +409,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { OSBackgroundTaskManager.beginBackgroundTask(backgroundTaskIdentifier) } - OneSignalCoreImpl.sharedClient().execute(request) { _ in + OneSignalCoreImpl.sharedClient().execute(request) { response in // On success, remove request from cache. No model hydration occurs. // For example, if app restarts and we read in operations between sending this off and getting the response self.dispatchQueue.async { @@ -403,6 +419,21 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) } } + + if let onesignalId = OneSignalUserManagerImpl.sharedInstance.onesignalId { + if let rywToken = response?["ryw_token"] as? String + { + let rywDelay = response?["ryw_delay"] as? NSNumber + OSConsistencyManager.shared.setRywTokenAndDelay( + id: onesignalId, + key: OSIamFetchOffsetKey.subscriptionUpdate, + value: OSReadYourWriteData(rywToken: rywToken, rywDelay: rywDelay) + ) + } else { + // handle a potential regression where ryw_token is no longer returned by API + OSConsistencyManager.shared.resolveConditionsWithID(id: OSIamFetchReadyCondition.CONDITIONID) + } + } } onFailure: { error in OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor update subscription request failed with error: \(error.debugDescription)") self.dispatchQueue.async { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 43ad5b3d1..2bc072dcb 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -258,6 +258,21 @@ extension OSUserExecutor { } else { self.executePendingRequests() } + + if let onesignalId = request.identityModel.onesignalId { + if let rywToken = response["ryw_token"] as? String + { + let rywDelay = response["ryw_delay"] as? NSNumber + OSConsistencyManager.shared.setRywTokenAndDelay( + id: onesignalId, + key: OSIamFetchOffsetKey.userCreate, + value: OSReadYourWriteData(rywToken: rywToken, rywDelay: rywDelay) + ) + } else { + // handle a potential regression where ryw_token is no longer returned by API + OSConsistencyManager.shared.resolveConditionsWithID(id: OSIamFetchReadyCondition.CONDITIONID) + } + } } OSOperationRepo.sharedInstance.paused = false } onFailure: { error in From 924d8a176dc90d2c96fb62bfe4c0a432c426e173 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Tue, 17 Sep 2024 16:02:26 -0500 Subject: [PATCH 09/14] Flush delta queue on start Motivation: we need to flush the delta queue on start in order to immediately trigger updates that would return a RYW token so we can unblock IAM fetch as soon as possible. --- .../OneSignalOSCore/Source/OSOperationRepo.swift | 7 ++++++- .../Source/OneSignalUserManagerImpl.swift | 6 +++--- .../OneSignalUserTests/OneSignalUserTests.swift | 11 ++++++----- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index 338fadd50..2e280825f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -107,7 +107,7 @@ public class OSOperationRepo: NSObject { // TODO: We can make this method internal once there is no manual adding of a Delta except through stores. This can happen when session data and purchase data use the model / store / listener infrastructure. */ - public func enqueueDelta(_ delta: OSDelta) { + public func enqueueDelta(_ delta: OSDelta, flush: Bool = false) { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { return } @@ -115,8 +115,13 @@ public class OSOperationRepo: NSObject { self.dispatchQueue.async { OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSOperationRepo enqueueDelta: \(delta)") self.deltaQueue.append(delta) + // Persist the deltas (including new delta) to storage OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_OPERATION_REPO_DELTA_QUEUE_KEY, withValue: self.deltaQueue) + + if flush { + self.flushDeltaQueue() + } } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 77ecd4c79..710d3724b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -551,7 +551,7 @@ extension OneSignalUserManagerImpl { userExecutor!.executePendingRequests() OSOperationRepo.sharedInstance.paused = false - updatePropertiesDeltas(property: .session_count, value: 1) + updatePropertiesDeltas(property: .session_count, value: 1, flush: true) // Fetch the user's data if there is a onesignal_id if let onesignalId = onesignalId { @@ -567,7 +567,7 @@ extension OneSignalUserManagerImpl { /// It enqueues an OSDelta to the Operation Repo. /// /// - Parameter property:Expected inputs are `.session_time"`, `.session_count"`, and `.purchases"`. - func updatePropertiesDeltas(property: OSPropertiesSupportedProperty, value: Any) { + func updatePropertiesDeltas(property: OSPropertiesSupportedProperty, value: Any, flush: Bool = false) { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "updatePropertiesDeltas") else { return } @@ -583,7 +583,7 @@ extension OneSignalUserManagerImpl { property: property.rawValue, value: value ) - OSOperationRepo.sharedInstance.enqueueDelta(delta) + OSOperationRepo.sharedInstance.enqueueDelta(delta, flush: flush) } /// Time processors forward the session time to this method. diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index 3a4bf1745..5e5673676 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -87,10 +87,8 @@ final class OneSignalUserTests: XCTestCase { /* When */ OneSignalUserManagerImpl.sharedInstance.sendSessionTime(100) - - // This adds a `session_count` property with value of 1 - // It also sets `refresh_device_metadata` to `true` - OneSignalUserManagerImpl.sharedInstance.startNewSession() + + OneSignalUserManagerImpl.sharedInstance.updatePropertiesDeltas(property: .session_count, value: 1, flush: false) OneSignalUserManagerImpl.sharedInstance.setLanguage("lang_1") @@ -108,7 +106,6 @@ final class OneSignalUserTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.addTags(["a": "a", "b": "b", "c": "c"]) - OneSignalUserManagerImpl.sharedInstance.startNewSession() let purchases = [ ["sku": "sku1", "amount": "1.25", "iso": "USD"], @@ -118,6 +115,10 @@ final class OneSignalUserTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.sendPurchases(purchases as [[String: AnyObject]]) OneSignalUserManagerImpl.sharedInstance.setLocation(latitude: 111.111, longitude: 222.222) + + // This adds a `session_count` property with value of 1 + // It also sets `refresh_device_metadata` to `true` + OneSignalUserManagerImpl.sharedInstance.startNewSession() /* Then */ From b8aeed2714a835bead54de30d7e587998f31d1b5 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Thu, 3 Oct 2024 12:21:46 -0700 Subject: [PATCH 10/14] Update UnitTestApp TestPlan xctestplan Created a new target OneSignalOSCoreTests --- .../UnitTestApp/UnitTestApp_TestPlan_Full.xctestplan | 7 +++++++ .../UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Full.xctestplan b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Full.xctestplan index 92e20a510..2f416718f 100644 --- a/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Full.xctestplan +++ b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Full.xctestplan @@ -111,6 +111,13 @@ "identifier" : "3C01518D2C2E298E0079E076", "name" : "OneSignalInAppMessagesTests" } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "5B053FB72CAE07EB002F30C4", + "name" : "OneSignalOSCoreTests" + } } ], "version" : 1 diff --git a/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan index 3f3d11a33..9ebb1d676 100644 --- a/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan +++ b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan @@ -56,6 +56,13 @@ "identifier" : "911E2CB91E398AB3003112A4", "name" : "UnitTests" } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "5B053FB72CAE07EB002F30C4", + "name" : "OneSignalOSCoreTests" + } } ], "version" : 1 From 82e468b2884c4fdb8b25f3a8e08fad0f23dc7115 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Wed, 9 Oct 2024 12:29:34 -0600 Subject: [PATCH 11/14] Update OSIamFetchReadyCondition to sharedInstance to waist on sub update Motivation: if we have a subscription update enqueued, we need to consider it. Otherwise we should just consider the userUpdateTokenSet. --- .../IamFetch/OSIamFetchReadyCondition.swift | 2 +- .../Source/OSSubscriptionModelStoreListener.swift | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift index 5f5a606c1..3a5021eb2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift @@ -50,7 +50,7 @@ @objc public static let CONDITIONID: String = "OSIamFetchReadyCondition" public var conditionId: String { - return OSIamFetchReadyCondition.CONDITIONID + return OSIamFetchReadyCondition.CONDITIONID } public func setSubscriptionUpdatePending(value: Bool) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift index 6ffe8c9ac..d633e5e38 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift @@ -60,6 +60,17 @@ class OSSubscriptionModelStoreListener: OSModelStoreListener { } func getUpdateModelDelta(_ args: OSModelChangedArgs) -> OSDelta? { + // The OSIamFetchReadyCondition needs to know whether there is a subscription update pending + // If so, we use this to await (in IAM-fetch) on its respective RYW token to be set. This is necessary + // because we always make a user property update call but we DON'T always make a subscription update call. + // The user update call increases the session_count while the subscription update would update + // something like the app_version. If the app_version hasn't changed since the last session, there + // wouldn't be an update needed (among other system-level properties). + if let onesignalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.onesignalId { + let condition = OSIamFetchReadyCondition.sharedInstance(withId: onesignalId) + condition.setSubscriptionUpdatePending(value: true) + } + return OSDelta( name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: OneSignalUserManagerImpl.sharedInstance.user.identityModel.modelId, From 4f7a9dd21f67a41fde68b0ba1d3f815c4e33979c Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Wed, 9 Oct 2024 16:20:07 -0600 Subject: [PATCH 12/14] Update OneSignalClient to include headers --- .../Source/API/OneSignalClient.m | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m index d9066e347..4edd5bf05 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m @@ -189,24 +189,29 @@ - (void)prettyPrintDebugStatementWithRequest:(OneSignalRequest *)request { } NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - - [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with parameters: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, jsonString]]; + + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with parameters: %@ and headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, jsonString, request.additionalHeaders]]; } - (void)handleJSONNSURLResponse:(NSURLResponse*)response data:(NSData*)data error:(NSError*)error isAsync:(BOOL)async withRequest:(OneSignalRequest *)request onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock { NSHTTPURLResponse* HTTPResponse = (NSHTTPURLResponse*)response; NSInteger statusCode = [HTTPResponse statusCode]; + NSDictionary *headers = [HTTPResponse allHeaderFields]; NSError* jsonError = nil; NSMutableDictionary* innerJson; if (data != nil && [data length] > 0) { innerJson = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError]; innerJson[@"httpStatusCode"] = [NSNumber numberWithLong:statusCode]; + innerJson[@"headers"] = headers; + + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"network request (%@) with URL %@ and headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, request.additionalHeaders]]; + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"network response (%@) with URL %@: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, innerJson]]; if (jsonError) { if (failureBlock != nil) - failureBlock([NSError errorWithDomain:@"OneSignal Error" code:statusCode userInfo:@{@"returned" : jsonError}]); + failureBlock([NSError errorWithDomain:@"OneSignal Error" code:statusCode userInfo:@{@"returned" : jsonError, @"headers": headers}]); // Add headers to error block return; } } @@ -224,14 +229,15 @@ - (void)handleJSONNSURLResponse:(NSURLResponse*)response data:(NSData*)data erro } else if (failureBlock != nil) { // Make sure to send all the infomation available to the client if (innerJson != nil && error != nil) - failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson, @"error": error}]); + failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson, @"error": error, @"headers": headers}]); else if (innerJson != nil) - failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson}]); + failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson, @"headers": headers}]); else if (error != nil) - failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"error" : error}]); + failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"error" : error, @"headers": headers}]); else - failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:nil]); + failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"headers": headers}]); } } + @end From 663edc13c4adfee3ea430c943a9d94850507f745 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Wed, 23 Oct 2024 14:21:28 -0500 Subject: [PATCH 13/14] Run swiftlint --fix Motivation: fix all lint issues --- .../IamFetch/OSIamFetchReadyCondition.swift | 14 ++++---- .../Consistency/OSConsistencyManager.swift | 6 ++-- .../Consistency/OSReadYourWriteData.swift | 1 - .../Source/OSOperationRepo.swift | 2 +- .../MockNewRecordsState.swift | 4 --- .../OSConsistencyManagerTests.swift | 33 +++++++++---------- .../OSPropertyOperationExecutor.swift | 2 +- .../OSSubscriptionOperationExecutor.swift | 6 ++-- .../Source/Executors/OSUserExecutor.swift | 2 +- .../OSSubscriptionModelStoreListener.swift | 2 +- .../Source/OneSignalUserManagerImpl.swift | 2 +- .../OneSignalUserTests.swift | 5 ++- 12 files changed, 36 insertions(+), 43 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift index 3a5021eb2..b915de506 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift @@ -25,7 +25,7 @@ THE SOFTWARE. */ -@objc public class OSIamFetchReadyCondition: NSObject, OSCondition { +@objc public class OSIamFetchReadyCondition: NSObject, OSCondition { // the id used to index the token map (e.g. onesignalId) private let id: String private var hasSubscriptionUpdatePending: Bool = false @@ -48,11 +48,11 @@ // Expose the constant to Objective-C @objc public static let CONDITIONID: String = "OSIamFetchReadyCondition" - + public var conditionId: String { return OSIamFetchReadyCondition.CONDITIONID } - + public func setSubscriptionUpdatePending(value: Bool) { hasSubscriptionUpdatePending = value } @@ -63,12 +63,12 @@ let userCreateTokenSet = tokenMap[NSNumber(value: OSIamFetchOffsetKey.userCreate.rawValue)] != nil let userUpdateTokenSet = tokenMap[NSNumber(value: OSIamFetchOffsetKey.userUpdate.rawValue)] != nil let subscriptionTokenSet = tokenMap[NSNumber(value: OSIamFetchOffsetKey.subscriptionUpdate.rawValue)] != nil - - if (userCreateTokenSet) { - return true; + + if userCreateTokenSet { + return true } - if (hasSubscriptionUpdatePending) { + if hasSubscriptionUpdatePending { return userUpdateTokenSet && subscriptionTokenSet } return userUpdateTokenSet diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift index 0f12f7397..ac7129de8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift @@ -38,7 +38,7 @@ import OneSignalCore // Private initializer to prevent multiple instances private override init() {} - + // Used for testing public func reset() { indexedTokens = [:] @@ -72,13 +72,13 @@ import OneSignalCore return condition.getNewestToken(indexedTokens: self.indexedTokens) } } - + // Method to resolve conditions by condition ID (e.g. OSIamFetchReadyCondition.ID) @objc public func resolveConditionsWithID(id: String) { guard let conditionList = indexedConditions[id] else { return } var completedConditions: [(OSCondition, DispatchSemaphore)] = [] for (condition, semaphore) in conditionList { - if (condition.conditionId == id) { + if condition.conditionId == id { semaphore.signal() completedConditions.append((condition, semaphore)) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSReadYourWriteData.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSReadYourWriteData.swift index a9cf045b2..287d660ca 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSReadYourWriteData.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSReadYourWriteData.swift @@ -57,4 +57,3 @@ public class OSReadYourWriteData: NSObject { return hasher.finalize() } } - diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index 2e280825f..8d85d17b0 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -118,7 +118,7 @@ public class OSOperationRepo: NSObject { // Persist the deltas (including new delta) to storage OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_OPERATION_REPO_DELTA_QUEUE_KEY, withValue: self.deltaQueue) - + if flush { self.flushDeltaQueue() } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift index 4a1c8d278..25a6444f7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift @@ -42,10 +42,6 @@ public class MockNewRecordsState: OSNewRecordsState { super.add(key, overwrite) } - override public func canAccess(_ key: String) -> Bool { - return super.canAccess(key) - } - public func get(_ key: String?) -> [MockNewRecord] { return records.filter { $0.key == key } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift index 215c298f9..9a686d644 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift @@ -121,7 +121,7 @@ class OSConsistencyManagerTests: XCTestCase { XCTAssertTrue(true) // Simulate some async action completing without hanging } } - + func testSetRywTokenWithoutAnyCondition() { // Given let id = "test_id" @@ -138,7 +138,7 @@ class OSConsistencyManagerTests: XCTestCase { // There is no condition registered, so we just check that no errors occur XCTAssertTrue(true) // If no errors occur, this test will pass } - + func testMultipleConditionsWithDifferentKeys() { let expectation1 = self.expectation(description: "UserUpdate condition met") let expectation2 = self.expectation(description: "SubscriptionUpdate condition met") @@ -215,7 +215,7 @@ class OSConsistencyManagerTests: XCTestCase { return result } - + func testConditionMetImmediatelyAfterTokenAlreadySet() { let expectation = self.expectation(description: "Condition met immediately") @@ -247,7 +247,7 @@ class OSConsistencyManagerTests: XCTestCase { waitForExpectations(timeout: 2.0, handler: nil) } - + func testConcurrentUpdatesToTokens() { let expectation = self.expectation(description: "Concurrent updates handled correctly") @@ -294,16 +294,15 @@ class OSConsistencyManagerTests: XCTestCase { } } - // Mock implementation of OSCondition that simulates a condition that isn't met class TestUnmetCondition: NSObject, OSCondition { // class-level constant for the ID public static let CONDITIONID = "TestUnmetCondition" - + public var conditionId: String { return TestUnmetCondition.CONDITIONID } - + func isMet(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> Bool { return false // Always returns false to simulate an unmet condition } @@ -316,49 +315,49 @@ class TestUnmetCondition: NSObject, OSCondition { // Mock implementation of OSCondition for cases where the condition is met class TestMetCondition: NSObject, OSCondition { private let expectedTokens: [String: [NSNumber: OSReadYourWriteData]] - + // class-level constant for the ID public static let CONDITIONID = "TestMetCondition" - + public var conditionId: String { return TestMetCondition.CONDITIONID } - + init(expectedTokens: [String: [NSNumber: OSReadYourWriteData]]) { self.expectedTokens = expectedTokens } - + func isMet(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> Bool { print("Expected tokens: \(expectedTokens)") print("Actual tokens: \(indexedTokens)") - + // Check if all the expected tokens are present in the actual tokens for (id, expectedTokenMap) in expectedTokens { guard let actualTokenMap = indexedTokens[id] else { print("No tokens found for id: \(id)") return false } - + // Check if all expected keys (e.g., userUpdate, subscriptionUpdate) are present with the correct value for (expectedKey, expectedValue) in expectedTokenMap { guard let actualValue = actualTokenMap[expectedKey] else { print("Key \(expectedKey) not found in actual tokens") return false } - + if actualValue != expectedValue { print("Mismatch for key \(expectedKey): expected \(expectedValue.rywToken), found \(actualValue.rywToken)") return false } } } - + print("Condition met for id") return true } - + func getNewestToken(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> OSReadYourWriteData? { - var dataBasedOnNewestRywToken: OSReadYourWriteData? = nil + var dataBasedOnNewestRywToken: OSReadYourWriteData? // Loop through the token maps and compare the values for tokenMap in indexedTokens.values { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 74ad0826c..a8dc7c072 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -259,7 +259,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { if let rywToken = response?["ryw_token"] as? String { let rywDelay = response?["ryw_delay"] as? NSNumber - + OSConsistencyManager.shared.setRywTokenAndDelay( id: onesignalId, key: OSIamFetchOffsetKey.userUpdate, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 2ef43dafc..090c43dd7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -297,7 +297,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } return } - + if let onesignalId = request.identityModel.onesignalId { if let rywToken = response["ryw_token"] as? String { @@ -312,7 +312,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { OSConsistencyManager.shared.resolveConditionsWithID(id: OSIamFetchReadyCondition.CONDITIONID) } } - + request.subscriptionModel.hydrate(response) if inBackground { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) @@ -419,7 +419,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) } } - + if let onesignalId = OneSignalUserManagerImpl.sharedInstance.onesignalId { if let rywToken = response?["ryw_token"] as? String { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 2bc072dcb..3ca2e4c39 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -258,7 +258,7 @@ extension OSUserExecutor { } else { self.executePendingRequests() } - + if let onesignalId = request.identityModel.onesignalId { if let rywToken = response["ryw_token"] as? String { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift index d633e5e38..a58b1ac95 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift @@ -70,7 +70,7 @@ class OSSubscriptionModelStoreListener: OSModelStoreListener { let condition = OSIamFetchReadyCondition.sharedInstance(withId: onesignalId) condition.setSubscriptionUpdatePending(value: true) } - + return OSDelta( name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: OneSignalUserManagerImpl.sharedInstance.user.identityModel.modelId, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 710d3724b..d1ad50a08 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -529,7 +529,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { guard let externalId = user.identityModel.externalId, let jwtExpiredHandler = self.jwtExpiredHandler else { return } - jwtExpiredHandler(externalId) { [self] (newToken) -> Void in + jwtExpiredHandler(externalId) { [self] (newToken) in guard user.identityModel.externalId == externalId else { return } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index 5e5673676..9165e31d7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -87,7 +87,7 @@ final class OneSignalUserTests: XCTestCase { /* When */ OneSignalUserManagerImpl.sharedInstance.sendSessionTime(100) - + OneSignalUserManagerImpl.sharedInstance.updatePropertiesDeltas(property: .session_count, value: 1, flush: false) OneSignalUserManagerImpl.sharedInstance.setLanguage("lang_1") @@ -106,7 +106,6 @@ final class OneSignalUserTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.addTags(["a": "a", "b": "b", "c": "c"]) - let purchases = [ ["sku": "sku1", "amount": "1.25", "iso": "USD"], ["sku": "sku2", "amount": "3.99", "iso": "USD"] @@ -115,7 +114,7 @@ final class OneSignalUserTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.sendPurchases(purchases as [[String: AnyObject]]) OneSignalUserManagerImpl.sharedInstance.setLocation(latitude: 111.111, longitude: 222.222) - + // This adds a `session_count` property with value of 1 // It also sets `refresh_device_metadata` to `true` OneSignalUserManagerImpl.sharedInstance.startNewSession() From 75eab0f355d5ad281fb5e675d3e7ac4210d54db3 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomez Palacio Date: Wed, 23 Oct 2024 16:13:22 -0500 Subject: [PATCH 14/14] Clarifying comment re: why we track RYW tokens for user create as well Motivation: On fresh installs, we don't yet have a user or a subscription to update so we wouldn't get a RYW token until those requests are executed (5 seconds later when the operations are processed) --- .../Source/Consistency/IamFetch/OSIamFetchOffsetKey.swift | 3 +++ .../Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift | 3 +++ 2 files changed, 6 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchOffsetKey.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchOffsetKey.swift index 915f7b275..c14a5f73f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchOffsetKey.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchOffsetKey.swift @@ -28,6 +28,9 @@ import Foundation public enum OSIamFetchOffsetKey: Int, OSConsistencyKeyEnum { + // We track user create tokens as well because on fresh installs, we don't have a user or subscription + // to update, which would lead to a 5 second delay until the subsequent user & subscription update calls + // give us RYW tokens case userCreate = 0 case userUpdate = 1 case subscriptionUpdate = 2 diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift index b915de506..66751e3a0 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/IamFetch/OSIamFetchReadyCondition.swift @@ -60,6 +60,9 @@ public func isMet(indexedTokens: [String: [NSNumber: OSReadYourWriteData]]) -> Bool { guard let tokenMap = indexedTokens[id] else { return false } + // We track user create tokens as well because on fresh installs, we don't have a user or subscription + // to update, which would lead to a 5 second delay until the subsequent user & subscription update calls + // give us RYW tokens let userCreateTokenSet = tokenMap[NSNumber(value: OSIamFetchOffsetKey.userCreate.rawValue)] != nil let userUpdateTokenSet = tokenMap[NSNumber(value: OSIamFetchOffsetKey.userUpdate.rawValue)] != nil let subscriptionTokenSet = tokenMap[NSNumber(value: OSIamFetchOffsetKey.subscriptionUpdate.rawValue)] != nil