diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 57cf9a0b8..afc31677f 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -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; @@ -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; @@ -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); + } + } + @Override public void onTokenRegistrationSuccessful(String authToken) { IterableLogger.v(TAG, "authToken successfully set"); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index f145bab10..99b4a7028 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -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); } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index c3a72339b..8c14fbfdf 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -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); diff --git a/example/.env.example b/example/.env.example index e1efc4169..f3c92b2e2 100644 --- a/example/.env.example +++ b/example/.env.example @@ -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 \ No newline at end of file +ITBL_ID=replace_this_with_your_user_id_or_email diff --git a/example/README.md b/example/README.md index 4ba5d0e6d..f827f8ac6 100644 --- a/example/README.md +++ b/example/README.md @@ -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_: @@ -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. @@ -57,10 +61,9 @@ To add an API key, do the following: 4. Fill in the followsing fields: - 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 diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index d648dd25c..86c5f7776 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -16,6 +16,7 @@ import { IterableLogLevel, IterableRetryBackoff, IterableAuthFailureReason, + type IterableGenerateJwtTokenArgs, } from '@iterable/react-native-sdk'; import { Route } from '../constants/routes'; @@ -86,6 +87,8 @@ const IterableAppContext = createContext({ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const isEmail = (id: string) => EMAIL_REGEX.test(id); + export const IterableAppProvider: FunctionComponent< React.PropsWithChildren > = ({ children }) => { @@ -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); @@ -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. @@ -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); @@ -232,7 +244,7 @@ export const IterableAppProvider: FunctionComponent< return Promise.resolve(true); }); }, - [apiKey, getUserId, login] + [apiKey, getUserId, login, userId] ); const logout = useCallback(() => { diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index 91955f797..e6c1de1e6 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -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 } @@ -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 } diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index f04b08e42..6ebffaf40 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -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:) @@ -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, @@ -480,7 +483,7 @@ import React @objc(passAlongAuthToken:) public func passAlongAuthToken(authToken: String?) { ITBInfo() - passedAuthToken = authToken + self.passedAuthToken = authToken authHandlerSemaphore.signal() } @@ -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( + 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)) @@ -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 } @@ -710,7 +771,4 @@ extension ReactIterableAPI: IterableAuthDelegate { } } } - - public func onTokenRegistrationFailed(_ reason: String?) { - } } diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index c7f325677..990cad786 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -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 => { + return await new Promise((resolve) => { + resolve('mock-jwt-token'); + }); + } + ); + static async getInAppMessages(): Promise { return await new Promise((resolve) => { resolve(MockRNIterableAPI.messages); diff --git a/src/__tests__/IterableAuthManager.test.ts b/src/__tests__/IterableAuthManager.test.ts new file mode 100644 index 000000000..2d46e705c --- /dev/null +++ b/src/__tests__/IterableAuthManager.test.ts @@ -0,0 +1,186 @@ +import { MockRNIterableAPI } from '../__mocks__/MockRNIterableAPI'; +import { IterableAuthManager } from '../core/classes/IterableAuthManager'; +import type { IterableGenerateJwtTokenArgs } from '../core/types/IterableGenerateJwtTokenArgs'; + +describe('IterableAuthManager', () => { + let authManager: IterableAuthManager; + + beforeEach(() => { + jest.clearAllMocks(); + authManager = new IterableAuthManager(); + }); + + describe('generateJwtToken', () => { + test('should generate JWT token with email', async () => { + // GIVEN an auth manager and JWT options with email + const opts: IterableGenerateJwtTokenArgs = { + secret: 'test-secret', + duration: 3600000, // 1 hour in milliseconds + email: 'test@example.com', + }; + + // WHEN generateJwtToken is called + const token = await authManager.generateJwtToken(opts); + + // THEN the method should be called with the correct parameters + expect(MockRNIterableAPI.generateJwtToken).toHaveBeenCalledWith(opts); + // AND should return a token + expect(token).toBe('mock-jwt-token'); + }); + + test('should generate JWT token with userId', async () => { + // GIVEN an auth manager and JWT options with userId + const opts: IterableGenerateJwtTokenArgs = { + secret: 'test-secret', + duration: 3600000, // 1 hour in milliseconds + userId: 'user123', + }; + + // WHEN generateJwtToken is called + const token = await authManager.generateJwtToken(opts); + + // THEN the method should be called with the correct parameters + expect(MockRNIterableAPI.generateJwtToken).toHaveBeenCalledWith(opts); + // AND should return a token + expect(token).toBe('mock-jwt-token'); + }); + + test('should generate JWT token with custom duration', async () => { + // GIVEN an auth manager and JWT options with custom duration + const opts: IterableGenerateJwtTokenArgs = { + secret: 'test-secret', + duration: 86400000, // 1 day in milliseconds + email: 'test@example.com', + }; + + // WHEN generateJwtToken is called + const token = await authManager.generateJwtToken(opts); + + // THEN the method should be called with the correct parameters + expect(MockRNIterableAPI.generateJwtToken).toHaveBeenCalledWith(opts); + // AND should return a token + expect(token).toBe('mock-jwt-token'); + }); + + test('should handle generateJwtToken with different secret', async () => { + // GIVEN an auth manager and JWT options with different secret + const opts: IterableGenerateJwtTokenArgs = { + secret: 'another-secret-key', + duration: 7200000, // 2 hours in milliseconds + userId: 'user456', + }; + + // WHEN generateJwtToken is called + const token = await authManager.generateJwtToken(opts); + + // THEN the method should be called with the correct parameters + expect(MockRNIterableAPI.generateJwtToken).toHaveBeenCalledWith(opts); + // AND should return a token + expect(token).toBe('mock-jwt-token'); + }); + + test('should call native generateJwtToken only once per invocation', async () => { + // GIVEN an auth manager and JWT options + const opts: IterableGenerateJwtTokenArgs = { + secret: 'test-secret', + duration: 3600000, + email: 'test@example.com', + }; + + // WHEN generateJwtToken is called + await authManager.generateJwtToken(opts); + + // THEN the native method should be called exactly once + expect(MockRNIterableAPI.generateJwtToken).toHaveBeenCalledTimes(1); + }); + + test('should handle multiple generateJwtToken calls', async () => { + // GIVEN an auth manager and multiple JWT requests + const opts1: IterableGenerateJwtTokenArgs = { + secret: 'secret1', + duration: 3600000, + email: 'user1@example.com', + }; + const opts2: IterableGenerateJwtTokenArgs = { + secret: 'secret2', + duration: 7200000, + userId: 'user2', + }; + + // WHEN generateJwtToken is called multiple times + const token1 = await authManager.generateJwtToken(opts1); + const token2 = await authManager.generateJwtToken(opts2); + + // THEN both calls should succeed + expect(token1).toBe('mock-jwt-token'); + expect(token2).toBe('mock-jwt-token'); + // AND the native method should be called twice with correct parameters + expect(MockRNIterableAPI.generateJwtToken).toHaveBeenCalledTimes(2); + expect(MockRNIterableAPI.generateJwtToken).toHaveBeenNthCalledWith( + 1, + opts1 + ); + expect(MockRNIterableAPI.generateJwtToken).toHaveBeenNthCalledWith( + 2, + opts2 + ); + }); + }); + + describe('passAlongAuthToken', () => { + test('should call native passAlongAuthToken with token', async () => { + // GIVEN an auth manager and a token + const token = 'test-token'; + + // WHEN passAlongAuthToken is called + await authManager.passAlongAuthToken(token); + + // THEN the native method should be called with the token + expect(MockRNIterableAPI.passAlongAuthToken).toHaveBeenCalledWith(token); + }); + + test('should call native passAlongAuthToken with null', async () => { + // GIVEN an auth manager + + // WHEN passAlongAuthToken is called with null + await authManager.passAlongAuthToken(null); + + // THEN the native method should be called with null + expect(MockRNIterableAPI.passAlongAuthToken).toHaveBeenCalledWith(null); + }); + + test('should call native passAlongAuthToken with undefined', async () => { + // GIVEN an auth manager + + // WHEN passAlongAuthToken is called with undefined + await authManager.passAlongAuthToken(undefined); + + // THEN the native method should be called with undefined + expect(MockRNIterableAPI.passAlongAuthToken).toHaveBeenCalledWith( + undefined + ); + }); + }); + + describe('pauseAuthRetries', () => { + test('should call native pauseAuthRetries with true', () => { + // GIVEN an auth manager + + // WHEN pauseAuthRetries is called with true + authManager.pauseAuthRetries(true); + + // THEN the native method should be called with true + expect(MockRNIterableAPI.pauseAuthRetries).toHaveBeenCalledWith(true); + }); + + test('should call native pauseAuthRetries with false', () => { + // GIVEN an auth manager + + // WHEN pauseAuthRetries is called with false + authManager.pauseAuthRetries(false); + + // THEN the native method should be called with false + expect(MockRNIterableAPI.pauseAuthRetries).toHaveBeenCalledWith(false); + }); + }); +}); diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 391fadbb7..d7aeb69f6 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -117,6 +117,12 @@ export interface Spec extends TurboModule { // Auth passAlongAuthToken(authToken?: string | null): void; pauseAuthRetries(pauseRetry: boolean): void; + generateJwtToken(opts: { + secret: string; + duration: number; + userId: string | null; + email: string | null; + }): Promise; // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 9ef784679..75321d4bc 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -957,7 +957,7 @@ export class Iterable { (promiseResult as IterableAuthResponse).authToken ); - const timeoutId = setTimeout(() => { + setTimeout(() => { if ( authResponseCallback === IterableAuthResponseResult.SUCCESS ) { @@ -976,8 +976,6 @@ export class Iterable { IterableLogger?.log('No callback received from native layer'); } }, 1000); - // Use unref() to prevent the timeout from keeping the process alive - timeoutId.unref(); } else if (typeof promiseResult === 'string') { //If promise only returns string Iterable.authManager.passAlongAuthToken(promiseResult as string); diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index fe2b446a3..dea34daaf 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -8,6 +8,7 @@ import type { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppD import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import type { IterableInAppShowResponse } from '../../inApp/enums/IterableInAppShowResponse'; import type { IterableInboxImpressionRowInfo } from '../../inbox/types/IterableInboxImpressionRowInfo'; +import type { IterableGenerateJwtTokenArgs } from '../types/IterableGenerateJwtTokenArgs'; import { IterableAttributionInfo } from './IterableAttributionInfo'; import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; @@ -351,6 +352,17 @@ export class IterableApi { return RNIterableAPI.passAlongAuthToken(authToken); } + /** + * Generate a JWT token for the current user. + * + * @param opts - The options for generating a JWT token + * @returns A Promise that resolves to the generated JWT token + */ + static generateJwtToken(opts: IterableGenerateJwtTokenArgs): Promise { + IterableLogger.log('generateJwtToken: ', opts); + return RNIterableAPI.generateJwtToken(opts); + } + // ---- End AUTH ---- // // ====================================================== // @@ -507,7 +519,7 @@ export class IterableApi { // ---- End IN-APP ---- // // ====================================================== // - // ======================= MOSC ======================= // + // ======================== MISC ======================== // // ====================================================== // /** diff --git a/src/core/classes/IterableAuthManager.ts b/src/core/classes/IterableAuthManager.ts index 44ece1af0..ccb496912 100644 --- a/src/core/classes/IterableAuthManager.ts +++ b/src/core/classes/IterableAuthManager.ts @@ -1,5 +1,6 @@ -import { IterableAuthResponse } from './IterableAuthResponse'; +import type { IterableGenerateJwtTokenArgs } from '../types/IterableGenerateJwtTokenArgs'; import { IterableApi } from './IterableApi'; +import { IterableAuthResponse } from './IterableAuthResponse'; /** * Manages the authentication for the Iterable SDK. @@ -41,4 +42,41 @@ export class IterableAuthManager { ): Promise { return IterableApi.passAlongAuthToken(authToken); } + + /** + * Generate a JWT token for the current user. + * + * This only needs to be used if JWT was enabled when [creating your API key](https://app.iterable.com/settings/apiKeys). + * + * To create a JWT enabled API key: + * 1. Go to Iterable's [**API key page**](https://app.iterable.com/settings/apiKeys) + * 2. Click **+ New API key** in the top right corner + * 3. Fill in the following fields: + * - **Name**: A descriptive name for the API key + * - **Type**: _Mobile_ (IMPORTANT: This must be _Mobile_ for the RN SDK) + * - **JWT authentication**: Check to enable JWT authentication. + * 4. Click **Create API Key** + * 5. The generated **API key** will be used in `Iterable.initialize`, and the + * **JWT secret** will be used in `IterableApi.generateJwtToken`. + * + * @param opts - Options for generating the JWT token. + * + * @example + * ```typescript + * const jwtToken = await IterableApi.generateJwtToken({ + * secret: 'your-jwt-secret', + * duration: 1000 * 60 * 60 * 24, // 1 day + * userId: 'your-iterable-user-id', + * }); + * // OR + * const jwtToken = await IterableApi.generateJwtToken({ + * secret: 'your-jwt-secret', + * duration: 1000 * 60 * 60 * 24, // 1 day + * email: 'me@gmail.com', + * }); + * ``` + */ + generateJwtToken(opts: IterableGenerateJwtTokenArgs): Promise { + return IterableApi.generateJwtToken(opts); + } } diff --git a/src/core/types/IterableGenerateJwtTokenArgs.ts b/src/core/types/IterableGenerateJwtTokenArgs.ts new file mode 100644 index 000000000..46b883554 --- /dev/null +++ b/src/core/types/IterableGenerateJwtTokenArgs.ts @@ -0,0 +1,28 @@ +interface IterableGenerateJwtTokenArgsBase { + /* The secret key for generating the JWT token. */ + secret: string; + /* The duration of the JWT token in milliseconds. */ + duration: number; +} + +/** + * Arguments for generating a JWT token + * Must specify either email OR userId, but not both + */ +export type IterableGenerateJwtTokenArgs = + | (IterableGenerateJwtTokenArgsBase & { + /** + * The **email** which was used in **`Iterable.initialize`**. + * + * NOTE: Either `userId` or `email` must be provided. + */ + email: string; + }) + | (IterableGenerateJwtTokenArgsBase & { + /** + * The **Iterable user ID** which was used in **`Iterable.initialize`**. + * + * NOTE: Either `userId` or `email` must be provided. + */ + userId: string; + }); diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 7659a76e4..245625d98 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -1,3 +1,4 @@ export * from './IterableAuthFailure'; export * from './IterableEdgeInsetDetails'; +export * from './IterableGenerateJwtTokenArgs'; export * from './IterableRetryPolicy'; diff --git a/src/index.tsx b/src/index.tsx index 240ac51f5..eb0e187d7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -30,6 +30,7 @@ export { export type { IterableAuthFailure, IterableEdgeInsetDetails, + IterableGenerateJwtTokenArgs, IterableRetryPolicy, } from './core/types'; export { diff --git a/yarn.lock b/yarn.lock index eb5b0a861..348741043 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3555,24 +3555,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/parser@npm:6.21.0" - dependencies: - "@typescript-eslint/scope-manager": 6.21.0 - "@typescript-eslint/types": 6.21.0 - "@typescript-eslint/typescript-estree": 6.21.0 - "@typescript-eslint/visitor-keys": 6.21.0 - debug: ^4.3.4 - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 162fe3a867eeeffda7328bce32dae45b52283c68c8cb23258fb9f44971f761991af61f71b8c9fe1aa389e93dfe6386f8509c1273d870736c507d76dd40647b68 - languageName: node - linkType: hard - "@typescript-eslint/parser@npm:^7.1.1": version: 7.18.0 resolution: "@typescript-eslint/parser@npm:7.18.0"