diff --git a/Source/ARTLocalDevice.m b/Source/ARTLocalDevice.m index 0fd5233ad..98e938179 100644 --- a/Source/ARTLocalDevice.m +++ b/Source/ARTLocalDevice.m @@ -37,6 +37,7 @@ NSString *const ARTAPNSDeviceDefaultTokenType = @"default"; NSString *const ARTAPNSDeviceLocationTokenType = @"location"; +NSString *const ARTAPNSDevicePushToStartTokenType = @"pushToStart"; NSString* ARTAPNSDeviceTokenKeyOfType(NSString *tokenType) { return [ARTAPNSDeviceTokenKey stringByAppendingFormat:@"-%@", tokenType ?: ARTAPNSDeviceDefaultTokenType]; @@ -106,7 +107,8 @@ + (instancetype)deviceWithStorage:(id)storage logger:(nullable NSArray *supportedTokenTypes = @[ ARTAPNSDeviceDefaultTokenType, - ARTAPNSDeviceLocationTokenType + ARTAPNSDeviceLocationTokenType, + ARTAPNSDevicePushToStartTokenType ]; for (NSString *tokenType in supportedTokenTypes) { @@ -139,6 +141,7 @@ - (void)resetDetails { _identityTokenDetails = nil; [self setAPNSDeviceToken:nil tokenType:ARTAPNSDeviceDefaultTokenType]; [self setAPNSDeviceToken:nil tokenType:ARTAPNSDeviceLocationTokenType]; + [self setAPNSDeviceToken:nil tokenType:ARTAPNSDevicePushToStartTokenType]; }]; } diff --git a/Source/ARTPush.m b/Source/ARTPush.m index f200258f4..5ad74da0c 100644 --- a/Source/ARTPush.m +++ b/Source/ARTPush.m @@ -70,6 +70,10 @@ + (void)didFailToRegisterForLocationNotificationsWithError:(NSError *)error real return [ARTPushInternal didFailToRegisterForLocationNotificationsWithError:error realtime:realtime]; } +- (void)registerPushToStartToken:(NSData *)token { + [_internal registerPushToStartToken:token]; +} + - (void)activate { [_internal activate]; } @@ -253,6 +257,10 @@ + (void)didFailToRegisterForLocationNotificationsWithError:(NSError *)error rest }]; } +- (void)registerPushToStartToken:(NSData *)token { + [_rest setAndPersistAPNSDeviceTokenData:token tokenType:ARTAPNSDevicePushToStartTokenType]; +} + - (void)activate { [self getActivationMachine:^(ARTPushActivationStateMachine *_Nullable stateMachine) { [stateMachine sendEvent:[ARTPushActivationEventCalledActivate new]]; diff --git a/Source/ARTWrapperSDKProxyPush.m b/Source/ARTWrapperSDKProxyPush.m index 88b19fb40..2a3ac7e99 100644 --- a/Source/ARTWrapperSDKProxyPush.m +++ b/Source/ARTWrapperSDKProxyPush.m @@ -27,6 +27,10 @@ - (instancetype)initWithPush:(ARTPush *)push proxyOptions:(ARTWrapperSDKProxyOpt #if TARGET_OS_IOS +- (void)registerPushToStartToken:(nonnull NSData *)token { + [self.underlyingPush registerPushToStartToken:token]; +} + - (void)activate { [self.underlyingPush activate]; } diff --git a/Source/PrivateHeaders/Ably/ARTLocalDevice+Private.h b/Source/PrivateHeaders/Ably/ARTLocalDevice+Private.h index 271b8ba12..50ddcf56e 100644 --- a/Source/PrivateHeaders/Ably/ARTLocalDevice+Private.h +++ b/Source/PrivateHeaders/Ably/ARTLocalDevice+Private.h @@ -13,6 +13,7 @@ extern NSString *const ARTClientIdKey; extern NSString *const ARTAPNSDeviceDefaultTokenType; extern NSString *const ARTAPNSDeviceLocationTokenType; +extern NSString *const ARTAPNSDevicePushToStartTokenType; NSString* ARTAPNSDeviceTokenKeyOfType(NSString * _Nullable tokenType); diff --git a/Source/PrivateHeaders/Ably/ARTPush+Private.h b/Source/PrivateHeaders/Ably/ARTPush+Private.h index 236f2ac40..759efdfff 100644 --- a/Source/PrivateHeaders/Ably/ARTPush+Private.h +++ b/Source/PrivateHeaders/Ably/ARTPush+Private.h @@ -44,6 +44,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)didFailToRegisterForLocationNotificationsWithError:(NSError *)error realtime:(ARTRealtime *)realtime; +- (void)registerPushToStartToken:(NSData *)token; + - (void)activate; - (void)deactivate; diff --git a/Source/include/Ably/ARTPush.h b/Source/include/Ably/ARTPush.h index fd5154d0c..d25922b0a 100644 --- a/Source/include/Ably/ARTPush.h +++ b/Source/include/Ably/ARTPush.h @@ -92,6 +92,16 @@ NS_ASSUME_NONNULL_BEGIN /// Call this method if you got an error calling `CLLocationManager.startMonitoringLocationPushes(completion:)`. + (void)didFailToRegisterForLocationNotificationsWithError:(NSError *)error realtime:(ARTRealtime *)realtime; +// Live Activity push-to-start token + +/** + * Registers a Live Activity push-to-start token (obtained from `Activity.pushToStartTokenUpdates`) with Ably, adding it to the existing device registration. Use this when the device has already been activated (i.e. `-activate` has completed). The token is stored in `ARTLocalDevice`'s push recipient and synced to Ably. + * You should implement `-[ARTPushRegistererDelegate didUpdateAblyPush:]` to handle success or failure of this operation. + * + * @param token The push-to-start token data, as delivered by `Activity.pushToStartTokenUpdates`. + */ +- (void)registerPushToStartToken:(NSData *)token NS_SWIFT_NAME(registerPushToStartToken(_:)); + /** * Activates the device for push notifications with APNS, obtaining a unique identifier from it. Subsequently registers the device with Ably and stores the `ARTLocalDevice.identityTokenDetails` in local storage. * You should implement `-[ARTPushRegistererDelegate didActivateAblyPush:]` to handle success or failure of this operation. diff --git a/Test/AblyTests/Tests/PushActivationStateMachineTests.swift b/Test/AblyTests/Tests/PushActivationStateMachineTests.swift index de6b4170e..57e88c767 100644 --- a/Test/AblyTests/Tests/PushActivationStateMachineTests.swift +++ b/Test/AblyTests/Tests/PushActivationStateMachineTests.swift @@ -600,6 +600,38 @@ class PushActivationStateMachineTests: XCTestCase { try reusableTestsWrapper__Activation_state_machine__State_WaitingForNewPushDeviceDetails__on_Event_CalledDeactivate__reusableTestsRsh3d2(testCase: .should_fire_DeregistrationFailed_event) } + // A push-to-start token added to an already-activated device (as done by + // -[ARTPush registerPushToStartToken:]) is synced to Ably via a PATCH whose + // recipient carries the token under the "pushToStart" apnsDeviceTokens key. + func test__100__Activation_state_machine__State_WaitingForNewPushDeviceDetails__on_Event_GotPushDeviceDetails__with_push_to_start_token__PATCHes_recipient() throws { + beforeEach__Activation_state_machine__State_WaitingForNewPushDeviceDetails() + + let expectedToken = "70757368-746f-2d73-7461-7274" + stateMachine.rest.device.setAndPersistAPNSDeviceToken(expectedToken, tokenType: ARTAPNSDevicePushToStartTokenType) + defer { stateMachine.rest.device.setAndPersistAPNSDeviceToken(nil, tokenType: ARTAPNSDevicePushToStartTokenType) } + + waitUntil(timeout: testTimeout) { done in + stateMachine.transitions = { event, _, _ in + if event is ARTPushActivationEventRegistrationSynced { + stateMachine.transitions = nil + done() + } + } + stateMachine.send(ARTPushActivationEventGotPushDeviceDetails()) + } + + let request = try XCTUnwrap(httpExecutor.requests.first { req in + req.httpMethod == "PATCH" && req.url?.path.hasPrefix("/push/deviceRegistrations/") == true + }, "Should have a PATCH \"/push/deviceRegistrations/{id}\" request") + let rawBody = try XCTUnwrap(request.httpBody, "Request should have a body") + let decodedBody = try XCTUnwrap(try stateMachine.rest.defaultEncoder.decode(rawBody), "Decode request body failed") + let body = try XCTUnwrap(decodedBody as? NSDictionary, "Request body is invalid") + let push = try XCTUnwrap(body.value(forKey: "push") as? [String: Any], "Body should have a push") + let recipient = try XCTUnwrap(push["recipient"] as? [String: Any], "push should have a recipient") + let apnsDeviceTokens = try XCTUnwrap(recipient["apnsDeviceTokens"] as? [String: String], "recipient should have apnsDeviceTokens") + XCTAssertEqual(apnsDeviceTokens["pushToStart"], expectedToken) + } + enum TestCase_ReusableTestsTestStateWaitingForRegistrationSyncThrough { case on_Event_CalledActivate case on_Event_RegistrationSynced diff --git a/Test/AblyTests/Tests/PushTests.swift b/Test/AblyTests/Tests/PushTests.swift index c050863f4..40b94253e 100644 --- a/Test/AblyTests/Tests/PushTests.swift +++ b/Test/AblyTests/Tests/PushTests.swift @@ -23,6 +23,12 @@ class PushTests: XCTestCase { static let tokenString = tokenData.map { String(format: "%02x", $0) }.joined() } + enum TestPushToStartToken { + static let tokenBase64 = "cHVzaC10by1zdGFydCB0b2tlbg==" + static let tokenData = Data(base64Encoded: tokenBase64, options: [])! + static let tokenString = tokenData.map { String(format: "%02x", $0) }.joined() + } + // XCTest invokes this method before executing the first test in the test suite. We use it to ensure that the global variables are initialized at the same moment, and in the same order, as they would have been when we used the Quick testing framework. override class var defaultTestSuite: XCTestSuite { _ = rest @@ -223,6 +229,26 @@ class PushTests: XCTestCase { }) } + // Live Activity push-to-start token + + // registerPushToStartToken stores the token under the push-to-start token type and + // sends a GotPushDeviceDetails event to the state machine (which, on an already-activated + // device, syncs the recipient to Ably via a PATCH). + func test__100__push_to_start__registerPushToStartToken_stores_token_and_sends_GotPushDeviceDetails() { + defer { rest.push.internal.activationMachine.onEvent = nil } + waitUntil(timeout: testTimeout) { done in + rest.push.internal.activationMachine.onEvent = { event, _ in + if event is ARTPushActivationEventGotPushDeviceDetails { + done() + } + } + rest.push.registerPushToStartToken(TestPushToStartToken.tokenData) + } + let expectedTokenKey = "ARTAPNSDeviceToken-pushToStart" // ARTAPNSDeviceTokenKeyOfType("pushToStart") + expect(storage.keysWritten.keys).to(contain([expectedTokenKey])) + XCTAssertEqual(storage.keysWritten.at(expectedTokenKey)?.value as? String, TestPushToStartToken.tokenString) + } + // RSH8 func test__008__LocalDevice__has_a_device_method_that_returns_a_LocalDevice() { let _: ARTLocalDevice = ARTRest(key: "fake:key").device