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 @@ -6,6 +6,9 @@
import android.net.Uri;
import android.os.Bundle;

import java.time.Duration;
import java.util.Date;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Promise;
Expand Down Expand Up @@ -37,6 +40,7 @@
import com.iterable.iterableapi.IterableLogger;
import com.iterable.iterableapi.IterableUrlHandler;
import com.iterable.iterableapi.RNIterableInternal;
import com.iterable.iterableapi.util.IterableJwtGenerator;

import org.json.JSONArray;
import org.json.JSONException;
Expand Down Expand Up @@ -593,6 +597,28 @@ public void pauseAuthRetries(boolean pauseRetry) {
IterableApi.getInstance().pauseAuthRetries(pauseRetry);
}

public void generateJwtToken(ReadableMap opts, Promise promise) {
try {
String secret = opts.getString("secret");
long durationMs = (long) opts.getDouble("duration");
String userId = opts.hasKey("userId") && !opts.isNull("userId") ? opts.getString("userId") : null;
String email = opts.hasKey("email") && !opts.isNull("email") ? opts.getString("email") : null;

// Validate that exactly one of userId or email is provided
if ((userId != null && email != null) || (userId == null && email == null)) {
promise.reject("E_INVALID_ARGS", "The token must include a userId or email, but not both.", (Throwable) null);
return;
}

// Use the Android SDK's Duration-based JWT generator
Duration duration = Duration.ofMillis(durationMs);
String token = IterableJwtGenerator.generateToken(secret, duration, email, userId);
promise.resolve(token);
} catch (Exception e) {
promise.reject("E_JWT_GENERATION_FAILED", "Failed to generate JWT: " + e.getMessage(), e);
}
Comment on lines +600 to +619
Copy link

Choose a reason for hiding this comment

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

Function with high complexity (count = 7): generateJwtToken [qlty:function-complexity]

Comment on lines +600 to +619
Copy link

Choose a reason for hiding this comment

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

Function with high complexity (count = 7): generateJwtToken [qlty:function-complexity]

Comment on lines +600 to +619
Copy link

Choose a reason for hiding this comment

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

Function with high complexity (count = 7): generateJwtToken [qlty:function-complexity]

}

@Override
public void onTokenRegistrationSuccessful(String authToken) {
IterableLogger.v(TAG, "authToken successfully set");
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 @@ -224,6 +224,11 @@ public void pauseAuthRetries(boolean pauseRetry) {
moduleImpl.pauseAuthRetries(pauseRetry);
}

@Override
public void generateJwtToken(ReadableMap opts, Promise promise) {
moduleImpl.generateJwtToken(opts, promise);
}

public void sendEvent(@NonNull String eventName, @Nullable Object eventData) {
moduleImpl.sendEvent(eventName, eventData);
}
Expand Down
4 changes: 4 additions & 0 deletions android/src/oldarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ public void pauseAuthRetries(boolean pauseRetry) {
moduleImpl.pauseAuthRetries(pauseRetry);
}

@ReactMethod
public void generateJwtToken(ReadableMap opts, Promise promise) {
moduleImpl.generateJwtToken(opts, promise);
}

public void sendEvent(@NonNull String eventName, @Nullable Object eventData) {
moduleImpl.sendEvent(eventName, eventData);
Expand Down
13 changes: 9 additions & 4 deletions example/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
# 4. Fill in the following fields:
# - Name: A descriptive name for the API key
# - Type: Mobile
# - JWT authentication: Leave **unchecked** (IMPORTANT)
# - JWT authentication: Whether or not you want to use JWT
# 5. Click "Create API Key"
# 6. Copy the generated API key
# 7. Replace the placeholder text next to `ITBL_API_KEY=` with the copied API key
# 6. Copy the generated API key and replace the placeholder text next to
# `ITBL_API_KEY=` with the copied API key
# 7. If you chose to enable JWT authentication, copy the JWT secret and and
# replace the placeholder text next to `ITBL_JWT_SECRET=` with the copied
# JWT secret
ITBL_API_KEY=replace_this_with_your_iterable_api_key
# Your JWT Secret, created when making your API key (see above)
ITBL_JWT_SECRET=replace_this_with_your_jwt_secret

# Your Iterable user ID or email address
ITBL_ID=replace_this_with_your_user_id_or_email
ITBL_ID=replace_this_with_your_user_id_or_email
17 changes: 10 additions & 7 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ _example app directory_. To do so, run the following:

```bash
cd ios
pod install
bundle install
bundle exec pod install
```

Once this is done, `cd` back into the _example app directory_:
Expand All @@ -40,12 +41,15 @@ In it, you will find:

```shell
ITBL_API_KEY=replace_this_with_your_iterable_api_key
ITBL_JWT_SECRET=replace_this_with_your_jwt_secret
ITBL_ID=replace_this_with_your_user_id_or_email
```

Replace `replace_this_with_your_iterable_api_key` with your _mobile_ Iterable API key,
and replace `replace_this_with_your_user_id_or_email` with the email or user id
that you use to log into Iterable.
Replace `replace_this_with_your_iterable_api_key` with your **_mobile_ Iterable
API key**, replace `replace_this_with_your_jwt_secret` with your **JWT Secret**
(if you have a JWT-enabled API key) and replace
`replace_this_with_your_user_id_or_email` with the **email or user id** that you
use to log into Iterable.

Follow the steps below if you do not have a mobile Iterable API key.

Expand All @@ -57,10 +61,9 @@ To add an API key, do the following:
4. Fill in the followsing fields:
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

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

Corrected spelling of 'followsing' to 'following'.

Suggested change
4. Fill in the followsing fields:
4. Fill in the following fields:

Copilot uses AI. Check for mistakes.
- Name: A descriptive name for the API key
- Type: Mobile
- JWT authentication: Leave **unchecked** (IMPORTANT)
- JWT authentication: Check to enable JWT authentication. If enabled, will need to create a [JWT generator](https://support.iterable.com/hc/en-us/articles/360050801231-JWT-Enabled-API-Keys#sample-python-code-for-jwt-generation) to generate the JWT token.
5. Click "Create API Key"
6. Copy the generated API key

6. Copy the generated API key and JWT secret into your _.env_ file

## Step 3: Start the Metro Server

Expand Down
50 changes: 31 additions & 19 deletions example/src/hooks/useIterableApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
IterableLogLevel,
IterableRetryBackoff,
IterableAuthFailureReason,
type IterableGenerateJwtTokenArgs,
} from '@iterable/react-native-sdk';

import { Route } from '../constants/routes';
Expand Down Expand Up @@ -86,6 +87,8 @@ const IterableAppContext = createContext<IterableAppProps>({

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const isEmail = (id: string) => EMAIL_REGEX.test(id);

export const IterableAppProvider: FunctionComponent<
React.PropsWithChildren<unknown>
> = ({ children }) => {
Expand All @@ -112,8 +115,7 @@ export const IterableAppProvider: FunctionComponent<

setLoginInProgress(true);

const isEmail = EMAIL_REGEX.test(id);
const fn = isEmail ? Iterable.setEmail : Iterable.setUserId;
const fn = isEmail(id) ? Iterable.setEmail : Iterable.setUserId;

fn(id);
setIsLoggedIn(true);
Expand All @@ -123,7 +125,7 @@ export const IterableAppProvider: FunctionComponent<
}, [userId]);

const initialize = useCallback(
(navigation: Navigation) => {
async (navigation: Navigation) => {
const config = new IterableConfig();

config.inAppDisplayInterval = 1.0; // Min gap between in-apps. No need to set this in production.
Expand Down Expand Up @@ -173,21 +175,31 @@ export const IterableAppProvider: FunctionComponent<

config.inAppHandler = () => IterableInAppShowResponse.show;

// NOTE: Uncomment to test authHandler failure
// config.authHandler = () => {
// console.log(`authHandler`);

// return Promise.resolve({
// authToken: 'SomethingNotValid',
// successCallback: () => {
// console.log(`authHandler > success`);
// },
// // This is not firing
// failureCallback: () => {
// console.log(`authHandler > failure`);
// },
// });
// };
config.authHandler = async () => {
const id = userId ?? process.env.ITBL_ID;
const idType = isEmail(id as string) ? 'email' : 'userId';
const secret = process.env.ITBL_JWT_SECRET ?? '';
const duration = 1000 * 60 * 60 * 24;

const jwtArgs: IterableGenerateJwtTokenArgs =
idType === 'email'
? { secret, duration, email: id as string }
: { secret, duration, userId: id as string };

const jwtToken = await Iterable.authManager.generateJwtToken(jwtArgs);

return Promise.resolve({
authToken: jwtToken,
// authToken: 'SomethingNotValid', // NOTE: Uncomment to test authHandler failure
successCallback: () => {
console.log(`authHandler > success`);
},
// This is not firing
failureCallback: () => {
console.log(`authHandler > failure`);
},
});
};

setItblConfig(config);

Expand Down Expand Up @@ -232,7 +244,7 @@ export const IterableAppProvider: FunctionComponent<
return Promise.resolve(true);
});
},
[apiKey, getUserId, login]
[apiKey, getUserId, login, userId]
);

const logout = useCallback(() => {
Expand Down
20 changes: 20 additions & 0 deletions ios/RNIterableAPI/RNIterableAPI.mm
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,21 @@ - (void)pauseAuthRetries:(BOOL)pauseRetry {
[_swiftAPI pauseAuthRetries:pauseRetry];
}

- (void)generateJwtToken:(JS::NativeRNIterableAPI::SpecGenerateJwtTokenOpts &)opts
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
NSMutableDictionary *optsDict = [NSMutableDictionary new];
optsDict[@"secret"] = opts.secret();
optsDict[@"duration"] = @(opts.duration());
if (opts.userId()) {
optsDict[@"userId"] = opts.userId();
}
if (opts.email()) {
optsDict[@"email"] = opts.email();
}
[_swiftAPI generateJwtToken:optsDict resolver:resolve rejecter:reject];
}

- (void)wakeApp {
// Placeholder function -- this method is only used in Android
}
Expand Down Expand Up @@ -507,6 +522,11 @@ - (void)wakeApp {
[_swiftAPI pauseAuthRetries:pauseRetry];
}

RCT_EXPORT_METHOD(generateJwtToken : (NSDictionary *)opts resolve : (
RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) {
[_swiftAPI generateJwtToken:opts resolver:resolve rejecter:reject];
}

RCT_EXPORT_METHOD(wakeApp) {
// Placeholder function -- this method is only used in Android
}
Expand Down
74 changes: 66 additions & 8 deletions ios/RNIterableAPI/ReactIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ import React
ITBError("Could not find message with id: \(messageId)")
return
}
IterableAPI.track(inAppOpen: message, location: InAppLocation.from(number: locationNumber as NSNumber))
IterableAPI.track(
inAppOpen: message, location: InAppLocation.from(number: locationNumber as NSNumber))
}

@objc(trackInAppClick:location:clickedUrl:)
Expand Down Expand Up @@ -414,8 +415,10 @@ import React
templateId: Double
) {
ITBInfo()
let finalCampaignId: NSNumber? = (campaignId as NSNumber).intValue <= 0 ? nil : campaignId as NSNumber
let finalTemplateId: NSNumber? = (templateId as NSNumber).intValue <= 0 ? nil : templateId as NSNumber
let finalCampaignId: NSNumber? =
(campaignId as NSNumber).intValue <= 0 ? nil : campaignId as NSNumber
let finalTemplateId: NSNumber? =
(templateId as NSNumber).intValue <= 0 ? nil : templateId as NSNumber
IterableAPI.updateSubscriptions(
emailListIds,
unsubscribedChannelIds: unsubscribedChannelIds,
Expand Down Expand Up @@ -480,7 +483,7 @@ import React
@objc(passAlongAuthToken:)
public func passAlongAuthToken(authToken: String?) {
ITBInfo()
passedAuthToken = authToken
self.passedAuthToken = authToken
authHandlerSemaphore.signal()
}

Expand All @@ -490,6 +493,62 @@ import React
IterableAPI.pauseAuthRetries(pauseRetry)
}

@objc(generateJwtToken:resolver:rejecter:)
public func generateJwtToken(
_ opts: NSDictionary,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
ITBInfo()

// Extract parameters
guard let secret = opts["secret"] as? String else {
rejecter("E_INVALID_ARGS", "secret is required", nil)
return
}

guard let durationMs = opts["duration"] as? Double else {
rejecter("E_INVALID_ARGS", "duration is required", nil)
return
}

let userId = opts["userId"] as? String
let email = opts["email"] as? String

// Validate that exactly one of userId or email is provided
if (userId != nil && email != nil) || (userId == nil && email == nil) {
rejecter("E_INVALID_ARGS", "The token must include a userId or email, but not both.", nil)
return
}

// Calculate iat and exp
let iat = Int(Date().timeIntervalSince1970)
let durationSeconds = Int(durationMs / 1000)
let exp = iat + durationSeconds

let token: String
if let userId = userId {
token = IterableTokenGenerator.generateJwtForUserId(
secret: secret,
iat: iat,
exp: exp,
userId: userId
)
} else if let email = email {
token = IterableTokenGenerator.generateJwtForEial(
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

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

Corrected spelling of 'generateJwtForEial' to 'generateJwtForEmail'.

Suggested change
token = IterableTokenGenerator.generateJwtForEial(
token = IterableTokenGenerator.generateJwtForEmail(

Copilot uses AI. Check for mistakes.
secret: secret,
iat: iat,
exp: exp,
email: email
)
} else {
rejecter("E_INVALID_ARGS", "Either userId or email must be provided", nil)
return
}

resolver(token)
}

// MARK: Private
private var shouldEmit = false
private let _methodQueue = DispatchQueue(label: String(describing: ReactIterableAPI.self))
Expand Down Expand Up @@ -537,7 +596,9 @@ import React
iterableConfig.inAppDelegate = self
}

if let authHandlerPresent = configDict["authHandlerPresent"] as? Bool, authHandlerPresent {
if let authHandlerPresent = configDict["authHandlerPresent"] as? Bool,
authHandlerPresent == true
{
iterableConfig.authDelegate = self
}

Expand Down Expand Up @@ -710,7 +771,4 @@ extension ReactIterableAPI: IterableAuthDelegate {
}
}
}

public func onTokenRegistrationFailed(_ reason: String?) {
}
}
13 changes: 13 additions & 0 deletions src/__mocks__/MockRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ export class MockRNIterableAPI {

static pauseAuthRetries = jest.fn();

static generateJwtToken = jest.fn(
async (_opts: {
secret: string;
duration: number;
userId?: string;
email?: string;
}): Promise<string> => {
return await new Promise((resolve) => {
resolve('mock-jwt-token');
});
}
);

static async getInAppMessages(): Promise<IterableInAppMessage[] | undefined> {
return await new Promise((resolve) => {
resolve(MockRNIterableAPI.messages);
Expand Down
Loading