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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,6 @@ docs/
# Local Netlify folder
.netlify
.metals/

# Agents
.pi/
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
5 changes: 5 additions & 0 deletions android/src/newarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions android/src/oldarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 2 additions & 3 deletions example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions ios/RNIterableAPI/RNIterableAPI.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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];
Expand Down
22 changes: 22 additions & 0 deletions ios/RNIterableAPI/ReactIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ import React
IterableAPI.disableDeviceForCurrentUser()
}

@objc(registerDeviceToken:)
public func registerDeviceToken(token: String) {
ITBInfo()
guard let tokenData = data(fromHex: token) else {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the token is empty, data(fromHex:) will not fail here because it returns an empty Data().

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)
{
Expand Down Expand Up @@ -599,6 +609,18 @@ import React

private let inboxSessionManager = InboxSessionManager()

private func data(fromHex hex: String) -> Data? {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data(fromHex:) returns a non-nil empty Data for empty input, so registerDeviceToken("") falls through and calls IterableAPI.register(token: Data()). That registers an empty APNS token with no error surfaced. Add an hex.isEmpty guard at the top of data(fromHex:) (or in registerDeviceToken before the call) and log the same ITBError so callers see something in their logs.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Odd-length input gets silently truncated. The last hex char is consumed by high and the loop exits on low == nil, so we return a token shorter than the source with no error. APNS tokens are always even length, but customers paste tokens from unpredictable places and a malformed input should fail loudly. Reject inputs whose count isn't a multiple of 2 (return nil) so the existing ITBError branch fires.

return nil
}
data.append(UInt8(highValue << 4 | lowValue))
}
return data
}

@objc func initialize(
withApiKey apiKey: String,
config configDict: NSDictionary,
Expand Down
4 changes: 4 additions & 0 deletions src/__mocks__/MockRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/api/NativeRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
11 changes: 11 additions & 0 deletions src/core/classes/Iterable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
20 changes: 20 additions & 0 deletions src/core/classes/Iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.).
Expand Down
13 changes: 13 additions & 0 deletions src/core/classes/IterableApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/core/classes/IterableApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading