Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,11 @@ class StartRequestCache: RequestCache {
}

class ReceiveReceiptsRequestCache: RequestCache {
// Keep receive receipts requests for up to 30 days.
static let OneMonthInSeconds = TimeInterval(60 * 60 * 24 * 30)
// Sent receipts stay as dedup markers (not only pending retries), so re-emits after relaunch aren't re-sent.
static let ThreeDaysInSeconds = TimeInterval(60 * 60 * 24 * 3)

init() {
super.init(cacheKey: OS_LIVE_ACTIVITIES_EXECUTOR_RECEIVE_RECEIPTS_KEY, ttl: ReceiveReceiptsRequestCache.OneMonthInSeconds)
super.init(cacheKey: OS_LIVE_ACTIVITIES_EXECUTOR_RECEIVE_RECEIPTS_KEY, ttl: ReceiveReceiptsRequestCache.ThreeDaysInSeconds)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ public class OneSignalLiveActivitiesManagerImpl: NSObject, OSLiveActivities {
Task {
for await content in activity.contentUpdates {
// Don't track a live activity started / updated "in app" without a notification
if let notificationId = activity.content.state.onesignal?.notificationId {
if let notificationId = content.state.onesignal?.notificationId {
OneSignalLiveActivitiesManagerImpl.addReceiveReceipts(notificationId: notificationId, activityType: "\(activityType)", activityId: activity.attributes.onesignal.activityId)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class OSRequestLiveActivityReceiveReceipts: OneSignalRequest, OSLiveActivityRequ
var activityType: String
var activityId: String
var requestSuccessful: Bool
var shouldForgetWhenSuccessful: Bool = true
// Kept after success so the persisted cache suppresses re-sends across relaunches.
var shouldForgetWhenSuccessful: Bool = false

func prepareForExecution() -> Bool {
guard let appId = OneSignalIdentifiers.currentAppId else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,22 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {

override func tearDownWithError() throws { }

func testAppendSetStartTokenWithSuccessfulRequest() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
// Subscribes a user, then resets the client so tests assert only on the requests they make.
private func setUpSubscribedUser() -> MockOneSignalClient {
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
return mockClient
}

func testAppendSetStartTokenWithSuccessfulRequest() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = setUpSubscribedUser()

let request = OSRequestSetStartToken(key: "my-activity-type", token: "my-token")
mockClient.setMockResponseForRequest(request: String(describing: request), response: [String: Any]())
Expand All @@ -79,13 +85,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testRemoveStartTokenWithSuccessfulRequest() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request = OSRequestRemoveStartToken(key: "my-activity-type")
mockClient.setMockResponseForRequest(request: String(describing: request), response: [String: Any]())
Expand All @@ -104,13 +104,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testSetUpdateTokenWithSuccessfulRequest() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token")
mockClient.setMockResponseForRequest(request: String(describing: request), response: [String: Any]())
Expand All @@ -131,13 +125,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testRemoveUpdateTokenWithSuccessfulRequest() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request = OSRequestRemoveUpdateToken(key: "my-activity-id")
mockClient.setMockResponseForRequest(request: String(describing: request), response: [String: Any]())
Expand All @@ -156,13 +144,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testReceiveReceiptsWithSuccessfulRequest() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request = OSRequestLiveActivityReceiveReceipts(key: "notification-id", activityType: "my-activity-type", activityId: "my-activity-id")
mockClient.setMockResponseForRequest(request: String(describing: request), response: [String: Any]())
Expand All @@ -173,21 +155,17 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
mockDispatchQueue.waitForDispatches(2)

/* Then */
XCTAssertEqual(executor.receiveReceipts.items.count, 0)
// The sent receipt is kept as a dedup marker so the same notificationId isn't reported again.
XCTAssertEqual(executor.receiveReceipts.items.count, 1)
XCTAssertTrue(executor.receiveReceipts.items["notification-id"]?.requestSuccessful ?? false)
XCTAssertEqual(mockClient.executedRequests.count, 1)
XCTAssertTrue(mockClient.executedRequests[0] == request)
}

func testClickEventWithSuccessfulRequest() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request = OSRequestLiveActivityClicked(key: "unique-click-id", activityType: "my-activity-type", activityId: "my-activity-id", notificationId: "my-notif-id")
mockClient.setMockResponseForRequest(request: String(describing: request), response: [String: Any]())
Expand Down Expand Up @@ -225,13 +203,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testRequestStaysInCacheForRetryableError() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request = OSRequestSetStartToken(key: "my-activity-type", token: "my-token")
mockClient.setMockFailureResponseForRequest(request: String(describing: request), error: OneSignalClientError(code: 500, message: "not-important", responseHeaders: nil, response: nil, underlyingError: nil))
Expand All @@ -252,13 +224,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testRequestRemovedFromCacheForNonRetryableError() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request = OSRequestSetStartToken(key: "my-activity-type", token: "my-token")
mockClient.setMockFailureResponseForRequest(request: String(describing: request), error: OneSignalClientError(code: 401, message: "not-important", responseHeaders: nil, response: nil, underlyingError: nil))
Expand Down Expand Up @@ -319,13 +285,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testSetStartRequestNotExecutedWithSameActivityTypeAndToken() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request1 = OSRequestSetStartToken(key: "my-activity-type", token: "my-token")
let request2 = OSRequestSetStartToken(key: "my-activity-type", token: "my-token")
Expand All @@ -348,13 +308,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testSetStartRequestNotExecutedWithSameActivityTypeAndDiffToken() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request1 = OSRequestSetStartToken(key: "my-activity-type", token: "my-token-1")
let request2 = OSRequestSetStartToken(key: "my-activity-type", token: "my-token-2")
Expand All @@ -378,13 +332,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testSetStartRequestFollowedByRemoveStartIsSuccessful() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request1 = OSRequestSetStartToken(key: "my-activity-type", token: "my-token-1")
let request2 = OSRequestRemoveStartToken(key: "my-activity-type")
Expand All @@ -407,13 +355,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testSetUpdateRequestNotExecutedWithSameActivityIdAndToken() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request1 = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token")
let request2 = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token")
Expand All @@ -436,13 +378,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testSetUpdateRequestNotExecutedWithSameActivityIdAndDiffToken() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request1 = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token-1")
let request2 = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token-2")
Expand All @@ -466,13 +402,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testSetUpdateRequestFollowedByRemoveUpdateIsSuccessful() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request1 = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token-1")
let request2 = OSRequestRemoveUpdateToken(key: "my-activity-id")
Expand All @@ -495,13 +425,7 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
func testReceiveReceiptsRequestNotExecutedWithSameNotificationId() throws {
/* Setup */
let mockDispatchQueue = MockDispatchQueue()
let mockClient = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(mockClient)
OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id")
OneSignalUserManagerImpl.sharedInstance.start()
// Wait for any user setup requests to complete
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2)
mockClient.reset()
let mockClient = setUpSubscribedUser()

let request1 = OSRequestLiveActivityReceiveReceipts(key: "my-notification-id", activityType: "my-activity-type-1", activityId: "my-activity-id-1")
let request2 = OSRequestLiveActivityReceiveReceipts(key: "my-notification-id", activityType: "my-activity-type-2", activityId: "my-activity-id-2")
Expand All @@ -515,8 +439,42 @@ final class OSLiveActivitiesExecutorTests: XCTestCase {
mockDispatchQueue.waitForDispatches(3)

/* Then */
XCTAssertEqual(executor.receiveReceipts.items.count, 0)
// request2 is suppressed as a duplicate of request1, which is kept as a dedup marker.
XCTAssertEqual(executor.receiveReceipts.items.count, 1)
XCTAssertEqual(mockClient.executedRequests.count, 1)
XCTAssertTrue(mockClient.executedRequests[0] == request1)
}

/**
A receive receipt must be sent at most once per device, even across app launches. The second executor
reloads the persisted cache with in-memory state gone, simulating a relaunch where ActivityKit re-emits
the active activity's current content and the same notificationId is reported again.
*/
func testReceiveReceiptsNotResentForSameNotificationIdAfterRelaunch() throws {
/* Setup */
let mockClient = setUpSubscribedUser()

let request1 = OSRequestLiveActivityReceiveReceipts(key: "my-notification-id", activityType: "my-activity-type", activityId: "my-activity-id")
let request2 = OSRequestLiveActivityReceiveReceipts(key: "my-notification-id", activityType: "my-activity-type", activityId: "my-activity-id")
mockClient.setMockResponseForRequest(request: String(describing: request1), response: [String: Any]())
mockClient.setMockResponseForRequest(request: String(describing: request2), response: [String: Any]())

/* When */
// First launch: the receipt is sent and succeeds.
let firstLaunch = MockDispatchQueue()
let executor1 = OSLiveActivitiesExecutor(requestDispatch: firstLaunch)
executor1.append(request1)
firstLaunch.waitForDispatches(2)
XCTAssertEqual(mockClient.executedRequests.count, 1)

// Relaunch: a fresh executor reloads the persisted cache; ActivityKit re-emits the same content.
let secondLaunch = MockDispatchQueue()
let executor2 = OSLiveActivitiesExecutor(requestDispatch: secondLaunch)
executor2.append(request2)
secondLaunch.waitForDispatches(1)

/* Then */
XCTAssertEqual(mockClient.executedRequests.count, 1, "Receive receipt must not be re-sent for the same notificationId after relaunch")
XCTAssertTrue(mockClient.executedRequests[0] == request1)
}
}
Loading