diff --git a/.gitignore b/.gitignore index 0587e162d..5fa606727 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,6 @@ docs/ # Local Netlify folder .netlify .metals/ + +# Agents +.pi/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 213da558f..d3fb7fc14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Unreleased + +### Updates +- Added `Iterable.registerDeviceToken(token)` to re-enable push for the current device. + - Android accepts an FCM token string. + - iOS accepts a continuous hex string representation of the APNS token. + ## 3.0.0 ### Updates diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index caef783e6..b25432d4b 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -373,6 +373,11 @@ public void disableDeviceForCurrentUser() { IterableApi.getInstance().disablePush(); } + public void registerDeviceToken(String token) { + IterableLogger.v(TAG, "registerDeviceToken"); + IterableApi.getInstance().registerDeviceToken(token); + } + public void handleAppLink(String uri, Promise promise) { IterableLogger.printInfo(); promise.resolve(IterableApi.getInstance().handleAppLink(uri)); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 9abe0d070..fb71142b8 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -137,6 +137,11 @@ public void disableDeviceForCurrentUser() { moduleImpl.disableDeviceForCurrentUser(); } + @Override + public void registerDeviceToken(String token) { + moduleImpl.registerDeviceToken(token); + } + @Override public void handleAppLink(String uri, Promise promise) { moduleImpl.handleAppLink(uri, promise); diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index ce0a6280c..c2f9b3161 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -138,6 +138,11 @@ public void disableDeviceForCurrentUser() { moduleImpl.disableDeviceForCurrentUser(); } + @ReactMethod + public void registerDeviceToken(String token) { + moduleImpl.registerDeviceToken(token); + } + @ReactMethod public void handleAppLink(String uri, Promise promise) { moduleImpl.handleAppLink(uri, promise); diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index 33b69e5e6..bc7357c4a 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 77E3B5772EA71A4B001449CE /* IterableJwtGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5742EA71A4B001449CE /* IterableJwtGenerator.swift */; }; 77E3B5782EA71A4B001449CE /* JwtTokenModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5752EA71A4B001449CE /* JwtTokenModule.mm */; }; 77E3B5792EA71A4B001449CE /* JwtTokenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5762EA71A4B001449CE /* JwtTokenModule.swift */; }; - 7C8CB9778D44155D232C3690 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FC8A71BC6B8F9B2B3CF98A77 /* libPods-ReactNativeSdkExample.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; DDD9C96E1785FEF10EEE61A5 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 627B7C7165CE568DB5CB8F50 /* libPods-ReactNativeSdkExample.a */; }; @@ -439,7 +438,7 @@ "-ObjC", "-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = iterable.reactnativesdk.example; + PRODUCT_BUNDLE_IDENTIFIER = com.iterable.reactnativesdk.example; PRODUCT_NAME = ReactNativeSdkExample; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SWIFT_OBJC_BRIDGING_HEADER = "ReactNativeSdkExample-Bridging-Header.h"; @@ -470,7 +469,7 @@ "-ObjC", "-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = iterable.reactnativesdk.example; + PRODUCT_BUNDLE_IDENTIFIER = com.iterable.reactnativesdk.example; PRODUCT_NAME = ReactNativeSdkExample; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SWIFT_OBJC_BRIDGING_HEADER = "ReactNativeSdkExample-Bridging-Header.h"; diff --git a/example/ios/ReactNativeSdkExample/ReactNativeSdkExample.entitlements b/example/ios/ReactNativeSdkExample/ReactNativeSdkExample.entitlements index 0c67376eb..903def2af 100644 --- a/example/ios/ReactNativeSdkExample/ReactNativeSdkExample.entitlements +++ b/example/ios/ReactNativeSdkExample/ReactNativeSdkExample.entitlements @@ -1,5 +1,8 @@ - + + aps-environment + development + diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index 025c4dc15..965b2d81e 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -230,6 +230,10 @@ - (void)disableDeviceForCurrentUser { [_swiftAPI disableDeviceForCurrentUser]; } +- (void)registerDeviceToken:(NSString *)token { + [_swiftAPI registerDeviceToken:token]; +} + - (void)getLastPushPayload:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [_swiftAPI getLastPushPayload:resolve rejecter:reject]; @@ -512,6 +516,10 @@ - (void)wakeApp { [_swiftAPI disableDeviceForCurrentUser]; } +RCT_EXPORT_METHOD(registerDeviceToken : (NSString *)token) { + [_swiftAPI registerDeviceToken:token]; +} + RCT_EXPORT_METHOD(getLastPushPayload : (RCTPromiseResolveBlock) resolve reject : (RCTPromiseRejectBlock)reject) { [_swiftAPI getLastPushPayload:resolve rejecter:reject]; diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index c7b5fc745..1f2c33421 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -144,6 +144,16 @@ import React IterableAPI.disableDeviceForCurrentUser() } + @objc(registerDeviceToken:) + public func registerDeviceToken(token: String) { + ITBInfo() + guard let tokenData = data(fromHex: token) else { + ITBError("Could not convert token to Data: invalid hex string") + return + } + IterableAPI.register(token: tokenData) + } + @objc(getLastPushPayload:rejecter:) public func getLastPushPayload(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { @@ -599,6 +609,18 @@ import React private let inboxSessionManager = InboxSessionManager() + private func data(fromHex hex: String) -> Data? { + var data = Data() + var chars = hex.makeIterator() + while let high = chars.next(), let low = chars.next() { + guard let highValue = high.hexDigitValue, let lowValue = low.hexDigitValue else { + return nil + } + data.append(UInt8(highValue << 4 | lowValue)) + } + return data + } + @objc func initialize( withApiKey apiKey: String, config configDict: NSDictionary, diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index 708ab56ad..60013bdbb 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -34,6 +34,10 @@ export class MockRNIterableAPI { static disableDeviceForCurrentUser = jest.fn(); + static registerDeviceToken = jest.fn((token: string): void => { + MockRNIterableAPI.token = token; + }); + static trackPushOpenWithCampaignId = jest.fn(); static updateCart = jest.fn(); diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 86b8913e6..01fa45113 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -120,6 +120,7 @@ export interface Spec extends TurboModule { // Device management disableDeviceForCurrentUser(): void; + registerDeviceToken(token: string): void; getLastPushPayload(): Promise<{ [key: string]: string | number | boolean; } | null>; diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index 20ad805d7..01c24b13d 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -142,6 +142,17 @@ describe('Iterable', () => { }); }); + describe('registerDeviceToken', () => { + it('should register the device token for the current user', () => { + // GIVEN a device token + const token = 'test-device-token'; + // WHEN Iterable.registerDeviceToken is called + Iterable.registerDeviceToken(token); + // THEN corresponding method is called on RNIterableAPI + expect(MockRNIterableAPI.registerDeviceToken).toBeCalledWith(token); + }); + }); + describe('getLastPushPayload', () => { it('should return the last push payload', async () => { const result = { var1: 'val1', var2: true }; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 983fab49b..d29a90c7f 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -345,6 +345,26 @@ export class Iterable { IterableApi.disableDeviceForCurrentUser(); } + /** + * Register the device's token for the current user, re-enabling push notifications. + * + * @param token - The device token to register. + * On Android, pass the Firebase Cloud Messaging (FCM) token string. + * On iOS, pass the Apple Push Notification service (APNS) token as a continuous hex string. + * + * @example + * ```typescript + * // Android + * Iterable.registerDeviceToken('fcm-token-string'); + * + * // iOS + * Iterable.registerDeviceToken('abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'); + * ``` + */ + static registerDeviceToken(token: string) { + IterableApi.registerDeviceToken(token); + } + /** * Get the payload of the last push notification with which the user * opened the application (by clicking an action button, etc.). diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts index 77a229c58..8f419f90d 100644 --- a/src/core/classes/IterableApi.test.ts +++ b/src/core/classes/IterableApi.test.ts @@ -247,6 +247,19 @@ describe('IterableApi', () => { }); }); + describe('registerDeviceToken', () => { + it('should call RNIterableAPI.registerDeviceToken with the token', () => { + // GIVEN a device token + const token = 'test-device-token'; + + // WHEN registerDeviceToken is called + IterableApi.registerDeviceToken(token); + + // THEN RNIterableAPI.registerDeviceToken is called with the token + expect(MockRNIterableAPI.registerDeviceToken).toBeCalledWith(token); + }); + }); + describe('updateUser', () => { it('should call RNIterableAPI.updateUser with data fields and merge flag', () => { // GIVEN data fields and merge flag diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 88820c9f3..2fad862a7 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -137,6 +137,16 @@ export class IterableApi { return RNIterableAPI.disableDeviceForCurrentUser(); } + /** + * Register the device token for the current user, re-enabling push notifications. + * + * @param token - On Android, the FCM token string. On iOS, a continuous hex string representation of the APNS token. + */ + static registerDeviceToken(token: string) { + IterableLogger.log('registerDeviceToken'); + return RNIterableAPI.registerDeviceToken(token); + } + /** * Save data to the current user's Iterable profile. *