Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
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
106 changes: 70 additions & 36 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
39 changes: 32 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,27 @@ 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 which is called when an error occurs while validating a 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 +233,11 @@ export class IterableConfig {
*/
logLevel: IterableLogLevel = IterableLogLevel.info;

/**
* The retry policy to use when retrying a request.
*/
retryPolicy?: IterableRetryPolicy;

/**
* Set whether the React Native SDK should print function calls to console.
*
Expand Down
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,
/** The token has expired. */
AUTH_TOKEN_EXPIRED,
/** Token has an invalid format (failed a regular expression check). */
AUTH_TOKEN_FORMAT_INVALID,
/** `onAuthTokenRequested` threw an exception. */
AUTH_TOKEN_GENERATION_ERROR,
/** Any other error not captured by another constant. */
AUTH_TOKEN_GENERIC_ERROR,
/** Iterable has invalidated this token and it cannot be used. */
AUTH_TOKEN_INVALIDATED,
/** The request to Iterable's API did not include a JWT authorization header. */
AUTH_TOKEN_MISSING,
/** `onAuthTokenRequested` returned a null JWT token. */
AUTH_TOKEN_NULL,
/**
* Iterable could not decode the token's payload (`iat`, `exp`, `email`,
* or `userId`).
*/
AUTH_TOKEN_PAYLOAD_INVALID,
/** Iterable could not validate the token's authenticity. */
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,
}
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;
}
16 changes: 16 additions & 0 deletions src/core/types/IterableRetryPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { IterableRetryBackoff } from "../enums/IterableRetryBackoff";

/**
* The policy for retrying an authentication attempt.
*/
export interface IterableRetryPolicy {
/** Number of consecutive JWT refresh retries the SDK should attempt */
maxRetry: number;
/**
* Duration between JWT refresh retries in seconds
* (starting point for retry backoff)
*/
retryInterval: number;
/** The backoff pattern to apply between retry attempts. */
retryBackoff: IterableRetryBackoff;
}
2 changes: 2 additions & 0 deletions src/core/types/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './IterableAuthFailure';
export * from './IterableEdgeInsetDetails';
export * from './IterableRetryPolicy';
9 changes: 8 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,24 @@ export {
} from './core/classes';
export {
IterableActionSource,
IterableAuthFailureReason,
IterableAuthResponseResult,
IterableDataRegion,
IterableEventName,
IterableLogLevel,
IterablePushPlatform,
IterableRetryBackoff,
} from './core/enums';
export {
useAppStateListener,
useDeviceOrientation,
type IterableDeviceOrientation,
} from './core/hooks';
export { type IterableEdgeInsetDetails } from './core/types';
export type {
IterableAuthFailure,
IterableEdgeInsetDetails,
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if EdgeInsetDetails falls under JWT changes. But is it for the inapp display issue?

IterableRetryPolicy,
} from './core/types';
export {
IterableHtmlInAppContent,
IterableInAppCloseSource,
Expand Down
Loading