Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bd32975
feat: add authentication failure and retry policy enums and types
lposen Oct 7, 2025
50ef0e6
chore: update eslint-config-prettier and add prettier-eslint dependency
lposen Oct 7, 2025
af0065e
feat: export new authentication and retry policy types in index files
lposen Oct 7, 2025
762f333
feat: enhance IterableConfig with JWT error handling and retry policy…
lposen Oct 7, 2025
cfe1de2
feat: add onAuthFailure and pauseAuthRetries methods to Iterable class
lposen Oct 7, 2025
7fde4e7
feat: implement retry policy and JWT error handling in IterableAppPro…
lposen Oct 7, 2025
5810a0a
feat: improve JWT error handling and enhance IterableConfig with addi…
lposen Oct 7, 2025
e85d660
refactor: remove onAuthFailure method and update event handler setup …
lposen Oct 7, 2025
c32447f
chore: remove unused index.ts file from hooks directory
lposen Oct 7, 2025
6d8c45a
refactor: simplify authHandler type and standardize IterableAuthFailu…
lposen Oct 7, 2025
a63b9dc
refactor: remove onJWTErrorPresent flag from IterableConfig to stream…
lposen Oct 7, 2025
786a079
Merge branch 'jwt/master' into jwt/MOB-10946-task-2-authfailure-and-r…
lposen Oct 9, 2025
f1d10cb
chore: update yarn.lock
lposen Oct 9, 2025
65cb551
feat: add authentication manager to Iterable class
lposen Oct 10, 2025
5bbb5fc
refactor: remove pauseAuthRetries method from Iterable class
lposen Oct 10, 2025
92fbff1
chore: disable TSDoc syntax rule for IterableRetryBackoff enum
lposen Oct 10, 2025
e94100f
feat: add pauseAuthRetries method to authentication manager and enhan…
lposen Oct 10, 2025
841f63f
Merge branch 'jwt/master' into jwt/MOB-10946-task-2-authfailure-and-r…
lposen Oct 10, 2025
1dbd4e2
Merge branch 'jwt/master' into jwt/MOB-10946-task-2-authfailure-and-r…
lposen Oct 13, 2025
71ac5c4
docs: add better comments to IterableAuthManager
lposen Oct 13, 2025
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
15 changes: 14 additions & 1 deletion example/src/hooks/useIterableApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
IterableConfig,
IterableInAppShowResponse,
IterableLogLevel,
IterableRetryBackoff,
} from '@iterable/react-native-sdk';

import { Route } from '../constants/routes';
Expand Down Expand Up @@ -96,7 +97,9 @@ export const IterableAppProvider: FunctionComponent<
const [apiKey, setApiKey] = useState<string | undefined>(
process.env.ITBL_API_KEY
);
const [userId, setUserId] = useState<string | null>(process.env.ITBL_ID ?? null);
const [userId, setUserId] = useState<string | null>(
process.env.ITBL_ID ?? null
);
const [loginInProgress, setLoginInProgress] = useState<boolean>(false);

const getUserId = useCallback(() => userId ?? process.env.ITBL_ID, [userId]);
Expand Down Expand Up @@ -124,6 +127,16 @@ export const IterableAppProvider: FunctionComponent<

config.inAppDisplayInterval = 1.0; // Min gap between in-apps. No need to set this in production.

config.retryPolicy = {
maxRetry: 5,
retryInterval: 10,
retryBackoff: IterableRetryBackoff.LINEAR,
};

config.onJWTError = (authFailure) => {
console.error('Error fetching JWT:', authFailure);
};

config.urlHandler = (url: string) => {
const routeNames = [Route.Commerce, Route.Inbox, Route.User];
for (const route of routeNames) {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,13 @@
"commitlint": "^19.6.1",
"del-cli": "^5.1.0",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jest": "^28.9.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-tsdoc": "^0.3.0",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"prettier-eslint": "^16.4.2",
"react": "19.0.0",
"react-native": "0.79.3",
"react-native-builder-bob": "^0.40.4",
Expand Down
2 changes: 2 additions & 0 deletions src/api/NativeRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export interface Spec extends TurboModule {

// Auth
passAlongAuthToken(authToken?: string | null): void;
onAuthFailure(authFailure: { userKey: string; failedAuthToken: string; failedRequestTime: number; failureReason: string }): void;
pauseAuthRetries(pauseRetry: boolean): void;

// Wake app -- android only
wakeApp(): void;
Expand Down
118 changes: 80 additions & 38 deletions src/core/classes/Iterable.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
Linking,
NativeEventEmitter,
Platform,
} from 'react-native';
import { Linking, NativeEventEmitter, Platform } from 'react-native';

import { buildInfo } from '../../itblBuildInfo';

Expand All @@ -13,7 +9,8 @@ import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage';
import { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource';
import { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppDeleteSource';
import { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation';
import { IterableAuthResponseResult, IterableEventName } from '../enums';
import { IterableAuthResponseResult } from '../enums/IterableAuthResponseResult';
import { IterableEventName } from '../enums/IterableEventName';

// Add this type-only import to avoid circular dependency
import type { IterableInAppManager } from '../../inApp/classes/IterableInAppManager';
Expand All @@ -25,6 +22,7 @@ import { IterableAuthResponse } from './IterableAuthResponse';
import type { IterableCommerceItem } from './IterableCommerceItem';
import { IterableConfig } from './IterableConfig';
import { IterableLogger } from './IterableLogger';
import type { IterableAuthFailure } from '../types/IterableAuthFailure';

const RNEventEmitter = new NativeEventEmitter(RNIterableAPI);

Expand Down Expand Up @@ -79,8 +77,11 @@ export class Iterable {
// Lazy initialization to avoid circular dependency
if (!this._inAppManager) {
// Import here to avoid circular dependency at module level
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports
const { IterableInAppManager } = require('../../inApp/classes/IterableInAppManager');

const {
IterableInAppManager,
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports
} = require('../../inApp/classes/IterableInAppManager');
this._inAppManager = new IterableInAppManager();
}
return this._inAppManager;
Expand Down Expand Up @@ -357,7 +358,13 @@ export class Iterable {
Iterable?.logger?.log('getAttributionInfo');

return RNIterableAPI.getAttributionInfo().then(
(dict: { campaignId: number; templateId: number; messageId: string } | null) => {
(
dict: {
campaignId: number;
templateId: number;
messageId: string;
} | null
) => {
if (dict) {
return new IterableAttributionInfo(
dict.campaignId as number,
Expand Down Expand Up @@ -398,7 +405,11 @@ export class Iterable {
static setAttributionInfo(attributionInfo?: IterableAttributionInfo) {
Iterable?.logger?.log('setAttributionInfo');

RNIterableAPI.setAttributionInfo(attributionInfo as unknown as { [key: string]: string | number | boolean; } | null);
RNIterableAPI.setAttributionInfo(
attributionInfo as unknown as {
[key: string]: string | number | boolean;
} | null
);
}

/**
Expand Down Expand Up @@ -477,7 +488,9 @@ export class Iterable {
static updateCart(items: IterableCommerceItem[]) {
Iterable?.logger?.log('updateCart');

RNIterableAPI.updateCart(items as unknown as { [key: string]: string | number | boolean }[]);
RNIterableAPI.updateCart(
items as unknown as { [key: string]: string | number | boolean }[]
);
}

/**
Expand Down Expand Up @@ -529,7 +542,11 @@ export class Iterable {
) {
Iterable?.logger?.log('trackPurchase');

RNIterableAPI.trackPurchase(total, items as unknown as { [key: string]: string | number | boolean }[], dataFields as { [key: string]: string | number | boolean } | undefined);
RNIterableAPI.trackPurchase(
total,
items as unknown as { [key: string]: string | number | boolean }[],
dataFields as { [key: string]: string | number | boolean } | undefined
);
}

/**
Expand Down Expand Up @@ -698,7 +715,10 @@ export class Iterable {
static trackEvent(name: string, dataFields?: unknown) {
Iterable?.logger?.log('trackEvent');

RNIterableAPI.trackEvent(name, dataFields as { [key: string]: string | number | boolean } | undefined);
RNIterableAPI.trackEvent(
name,
dataFields as { [key: string]: string | number | boolean } | undefined
);
}

/**
Expand Down Expand Up @@ -746,7 +766,10 @@ export class Iterable {
) {
Iterable?.logger?.log('updateUser');

RNIterableAPI.updateUser(dataFields as { [key: string]: string | number | boolean }, mergeNestedObjects);
RNIterableAPI.updateUser(
dataFields as { [key: string]: string | number | boolean },
mergeNestedObjects
);
}

/**
Expand Down Expand Up @@ -911,34 +934,45 @@ export class Iterable {
}

/**
* Sets up event handlers for various Iterable events.
* A callback function that is called when an authentication failure occurs.
*
* This method performs the following actions:
* - Removes all existing listeners to avoid duplicate listeners.
* - Adds listeners for URL handling, custom actions, in-app messages, and authentication.
* @param authFailure - The auth failure details
*
* Event Handlers:
* - `handleUrlCalled`: Invokes the URL handler if configured, with a delay on Android to allow the activity to wake up.
* - `handleCustomActionCalled`: Invokes the custom action handler if configured.
* - `handleInAppCalled`: Invokes the in-app handler if configured and sets the in-app show response.
* - `handleAuthCalled`: Invokes the authentication handler if configured and handles the promise result.
* - `handleAuthSuccessCalled`: Sets the authentication response callback to success.
* - `handleAuthFailureCalled`: Sets the authentication response callback to failure.
* @example
* ```typescript
* Iterable.onAuthFailure({
* userKey: '1234567890',
* failedAuthToken: '1234567890',
* failedRequestTime: 1234567890,
* failureReason: IterableAuthFailureReason.AUTH_TOKEN_EXPIRED,
* });
* ```
*/
static onAuthFailure(authFailure: IterableAuthFailure) {
Iterable?.logger?.log('onAuthFailure');

RNIterableAPI.onAuthFailure(authFailure);
}

/**
* Pause the authentication retry mechanism.
*
* Helper Functions:
* - `callUrlHandler`: Calls the URL handler and attempts to open the URL if the handler returns false.
* @param pauseRetry - Whether to pause the authentication retry mechanism
*
* @internal
* @example
* ```typescript
* Iterable.pauseAuthRetries(true);
* ```
*/
private static setupEventHandlers() {
//Remove all listeners to avoid duplicate listeners
RNEventEmitter.removeAllListeners(IterableEventName.handleUrlCalled);
RNEventEmitter.removeAllListeners(IterableEventName.handleInAppCalled);
RNEventEmitter.removeAllListeners(
IterableEventName.handleCustomActionCalled
);
RNEventEmitter.removeAllListeners(IterableEventName.handleAuthCalled);
static pauseAuthRetries(pauseRetry: boolean) {
Iterable?.logger?.log('pauseAuthRetries');

RNIterableAPI.pauseAuthRetries(pauseRetry);
}

/** * @internal
*/
private static setupEventHandlers() {
if (Iterable.savedConfig.urlHandler) {
RNEventEmitter.addListener(IterableEventName.handleUrlCalled, (dict) => {
const url = dict.url;
Expand Down Expand Up @@ -983,7 +1017,7 @@ export class Iterable {
let authResponseCallback: IterableAuthResponseResult;
RNEventEmitter.addListener(IterableEventName.handleAuthCalled, () => {
// MOB-10423: Check if we can use chain operator (?.) here instead

// Asks frontend of the client/app to pass authToken
Iterable.savedConfig.authHandler!()
.then((promiseResult) => {
// Promise result can be either just String OR of type AuthResponse.
Expand All @@ -1004,6 +1038,8 @@ export class Iterable {
} else if (
authResponseCallback === IterableAuthResponseResult.FAILURE
) {
// We are currently only reporting JWT related errors. In
// the future, we should handle other types of errors as well.
if ((promiseResult as IterableAuthResponse).failureCallback) {
(promiseResult as IterableAuthResponse).failureCallback?.();
}
Expand Down Expand Up @@ -1033,8 +1069,14 @@ export class Iterable {
);
RNEventEmitter.addListener(
IterableEventName.handleAuthFailureCalled,
() => {
(authFailureResponse: IterableAuthFailure) => {
// Mark the flag for above listener to indicate something failed.
// `catch(err)` will only indicate failure on high level. No actions
// should be taken inside `catch(err)`.
authResponseCallback = IterableAuthResponseResult.FAILURE;

// Call the actual JWT error with `authFailure` object.
Iterable.savedConfig?.onJWTError?.(authFailureResponse);
}
);
}
Expand Down
49 changes: 42 additions & 7 deletions src/core/classes/IterableConfig.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { type IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage';
import { IterableInAppShowResponse } from '../../inApp/enums';
import {
IterableDataRegion,
IterableLogLevel,
IterablePushPlatform,
} from '../enums';
import { IterableInAppShowResponse } from '../../inApp/enums/IterableInAppShowResponse';
import { IterableDataRegion } from '../enums/IterableDataRegion';
import { IterableLogLevel } from '../enums/IterableLogLevel';
import { IterablePushPlatform } from '../enums/IterablePushPlatform';
import type { IterableAuthFailure } from '../types/IterableAuthFailure';
import type { IterableRetryPolicy } from '../types/IterableRetryPolicy';
import { IterableAction } from './IterableAction';
import type { IterableActionContext } from './IterableActionContext';
import type { IterableAuthResponse } from './IterableAuthResponse';
Expand Down Expand Up @@ -204,7 +204,28 @@ export class IterableConfig {
* @returns A promise that resolves to an `IterableAuthResponse`, a `string`,
* or `undefined`.
*/
authHandler?: () => Promise<IterableAuthResponse | string | undefined>;
authHandler?: () => Promise<
IterableAuthResponse | IterableAuthFailure | string | undefined
>;

/**
* A callback function that is called when the SDK encounters an error while
* validing the JWT.
*
* The retry for JWT should be automatically handled by the native SDK, so
* this is just for logging/transparency purposes.
*
* @param authFailure - The details of the auth failure.
*
* @example
* ```typescript
* const config = new IterableConfig();
* config.onJWTError = (authFailure) => {
* console.error('Error fetching JWT:', authFailure);
* };
* ```
*/
onJWTError?: (authFailure: IterableAuthFailure) => void;

/**
* Set the verbosity of Android and iOS project's log system.
Expand All @@ -213,6 +234,12 @@ export class IterableConfig {
*/
logLevel: IterableLogLevel = IterableLogLevel.info;

/**
* Configuration for JWT refresh retry behavior.
* If not specified, the SDK will use default retry behavior.
*/
retryPolicy?: IterableRetryPolicy;

/**
* Set whether the React Native SDK should print function calls to console.
*
Expand Down Expand Up @@ -332,6 +359,13 @@ export class IterableConfig {
*/
// eslint-disable-next-line eqeqeq
authHandlerPresent: this.authHandler != undefined,
/**
* A boolean indicating if an onJWTError handler is present.
*
* TODO: Figure out if this is purposeful
*/
// eslint-disable-next-line eqeqeq
onJWTErrorPresent: this.onJWTError != undefined,
/** The log level for the SDK. */
logLevel: this.logLevel,
expiringAuthTokenRefreshPeriod: this.expiringAuthTokenRefreshPeriod,
Expand All @@ -342,6 +376,7 @@ export class IterableConfig {
dataRegion: this.dataRegion,
pushPlatform: this.pushPlatform,
encryptionEnforced: this.encryptionEnforced,
retryPolicy: this.retryPolicy,
};
}
}
Loading