Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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: 1 addition & 1 deletion src/api/NativeRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ export interface Spec extends TurboModule {

// Auth
passAlongAuthToken(authToken?: string | null): void;
pauseAuthRetries(pauseRetry: boolean): void;

// Wake app -- android only
wakeApp(): void;


// REQUIRED for RCTEventEmitter
addListener(eventName: string): void;
removeListeners(count: number): void;
Expand Down
81 changes: 64 additions & 17 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 @@ -910,6 +933,22 @@ export class Iterable {
);
}

/**
* Pause the authentication retry mechanism.
*
* @param pauseRetry - Whether to pause the authentication retry mechanism
*
* @example
* ```typescript
* Iterable.pauseAuthRetries(true);
* ```
*/
static pauseAuthRetries(pauseRetry: boolean) {
Iterable?.logger?.log('pauseAuthRetries');

RNIterableAPI.pauseAuthRetries(pauseRetry);
}

/**
* Sets up event handlers for various Iterable events.
*
Expand All @@ -931,7 +970,7 @@ export class Iterable {
* @internal
*/
private static setupEventHandlers() {
//Remove all listeners to avoid duplicate listeners
// Remove all listeners to avoid duplicate listeners
RNEventEmitter.removeAllListeners(IterableEventName.handleUrlCalled);
RNEventEmitter.removeAllListeners(IterableEventName.handleInAppCalled);
RNEventEmitter.removeAllListeners(
Expand Down Expand Up @@ -983,7 +1022,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 +1043,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 +1074,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
38 changes: 32 additions & 6 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 @@ -206,13 +206,38 @@ export class IterableConfig {
*/
authHandler?: () => Promise<IterableAuthResponse | 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.
*
* By default, you will be able to see info level logs printed in IDE when running the app.
*/
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 @@ -342,6 +367,7 @@ export class IterableConfig {
dataRegion: this.dataRegion,
pushPlatform: this.pushPlatform,
encryptionEnforced: this.encryptionEnforced,
retryPolicy: this.retryPolicy,
};
}
}
39 changes: 39 additions & 0 deletions src/core/enums/IterableAuthFailureReason.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* The reason for the failure of an authentication attempt.
*
* This is generally related to JWT token validation.
*/
export enum IterableAuthFailureReason {
/**
* An auth token's expiration must be less than one year from its issued-at
* time.
*/
AUTH_TOKEN_EXPIRATION_INVALID = 'AUTH_TOKEN_EXPIRATION_INVALID',
/** The token has expired. */
AUTH_TOKEN_EXPIRED = 'AUTH_TOKEN_EXPIRED',
/** Token has an invalid format (failed a regular expression check). */
AUTH_TOKEN_FORMAT_INVALID = 'AUTH_TOKEN_FORMAT_INVALID',
/** `onAuthTokenRequested` threw an exception. */
AUTH_TOKEN_GENERATION_ERROR = 'AUTH_TOKEN_GENERATION_ERROR',
/** Any other error not captured by another constant. */
AUTH_TOKEN_GENERIC_ERROR = 'AUTH_TOKEN_GENERIC_ERROR',
/** Iterable has invalidated this token and it cannot be used. */
AUTH_TOKEN_INVALIDATED = 'AUTH_TOKEN_INVALIDATED',
/** The request to Iterable's API did not include a JWT authorization header. */
AUTH_TOKEN_MISSING = 'AUTH_TOKEN_MISSING',
/** `onAuthTokenRequested` returned a null JWT token. */
AUTH_TOKEN_NULL = 'AUTH_TOKEN_NULL',
/**
* Iterable could not decode the token's payload (`iat`, `exp`, `email`,
* or `userId`).
*/
AUTH_TOKEN_PAYLOAD_INVALID = 'AUTH_TOKEN_PAYLOAD_INVALID',
/** Iterable could not validate the token's authenticity. */
AUTH_TOKEN_SIGNATURE_INVALID = 'AUTH_TOKEN_SIGNATURE_INVALID',
/**
* The token doesn't include an `email` or a `userId`. Or, one of these
* values is included, but it references a user that isn't in the Iterable
* project.
*/
AUTH_TOKEN_USER_KEY_INVALID = 'AUTH_TOKEN_USER_KEY_INVALID',
}
15 changes: 15 additions & 0 deletions src/core/enums/IterableRetryBackoff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* The type of backoff to use when retrying a request.
*/
export enum IterableRetryBackoff {
/**
* Linear backoff (each retry will wait for a fixed interval)
* TODO: check with @Ayyanchira if this is correct
*/
LINEAR = 'LINEAR',
/**
* Exponential backoff (each retry will wait for an interval that increases exponentially)
* TODO: check with @Ayyanchira if this is correct
*/
EXPONENTIAL = 'EXPONENTIAL',
}
2 changes: 2 additions & 0 deletions src/core/enums/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export * from './IterableActionSource';
export * from './IterableAuthFailureReason';
export * from './IterableAuthResponseResult';
export * from './IterableDataRegion';
export * from './IterableEventName';
export * from './IterableLogLevel';
export * from './IterablePushPlatform';
export * from './IterableRetryBackoff';
18 changes: 18 additions & 0 deletions src/core/types/IterableAuthFailure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { IterableAuthFailureReason } from "../enums/IterableAuthFailureReason";

/**
* The details of an auth failure.
*/
export interface IterableAuthFailure {
/** `userId` or `email` of the signed-in user */
userKey: string;

/** The `authToken` which caused the failure */
failedAuthToken: string;

/** The timestamp of the failed request */
failedRequestTime: number;

/** Indicates a reason for failure */
failureReason: IterableAuthFailureReason;
}
Loading