From 2dcb96984580bdabb0d2230922f7f4aefc045f08 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 17:01:43 -0700 Subject: [PATCH 01/12] chore: update yarn.lock and enhance example configuration documentation --- example/.env.example | 15 +++++++++++---- example/README.md | 21 ++++++++++++++------- yarn.lock | 18 ------------------ 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/example/.env.example b/example/.env.example index e1efc4169..ada1a4c4f 100644 --- a/example/.env.example +++ b/example/.env.example @@ -9,11 +9,18 @@ # 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 +# Is your api token JWT Enabled +ITBL_IS_JWT_ENABLED=true # 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..819bc6826 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,18 @@ 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_IS_JWT_ENABLED=true 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) +- Set `ITBL_IS_JWT_ENABLED` to true if you have a JWT-enabled key, and false if you do not. +- 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. @@ -54,12 +61,12 @@ To add an API key, do the following: 1. Sign into your Iterable account 2. Go to [Integrations > API Keys](https://app.iterable.com/settings/apiKeys) 3. Click "New API Key" in the top right corner - 4. Fill in the followsing fields: + 4. Fill in the following 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/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" From 1bfa9d55bd69a08be5d13a09ead6b49c3cb824a6 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 17:04:24 -0700 Subject: [PATCH 02/12] chore: add .env.local to .gitignore and update example configuration comments --- .gitignore | 1 + example/.env.example | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 356417c19..f042551bd 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ ios/generated android/generated # Iterable +.env.local .env .xcode.env.local coverage/ diff --git a/example/.env.example b/example/.env.example index ada1a4c4f..9d1ff99bc 100644 --- a/example/.env.example +++ b/example/.env.example @@ -19,7 +19,8 @@ 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 -# Is your api token JWT Enabled +# Is your api token JWT Enabled? +# Defaults to true ITBL_IS_JWT_ENABLED=true # Your Iterable user ID or email address From d471800c0d829e928334f19244f1c74b754ae840 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 17:15:31 -0700 Subject: [PATCH 03/12] refactor: small updates to ReactIterable.swift --- ios/RNIterableAPI/ReactIterableAPI.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index f04b08e42..80c0a9cab 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() } @@ -537,7 +540,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 } From 6a9972af66641ae024672baeac0a98d3f998d4ef Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 18:24:47 -0700 Subject: [PATCH 04/12] feat: implement JWT generation module for React Native in example --- .../example/IterableJwtGenerator.java | 106 ++++++++++++++++ .../reactnativesdk/example/JwtTokenModule.kt | 37 ++++++ .../reactnativesdk/example/JwtTokenPackage.kt | 34 +++++ .../reactnativesdk/example/MainApplication.kt | 1 + .../ReactNativeSdkExample-Bridging-Header.h | 3 + .../IterableJwtGenerator.swift | 118 ++++++++++++++++++ .../ReactNativeSdkExample/JwtTokenModule.m | 25 ++++ .../JwtTokenModule.swift | 40 ++++++ example/src/hooks/useIterableApp.tsx | 75 ++++++++--- example/src/utility/NativeJwtTokenModule.ts | 45 +++++++ example/src/utility/index.ts | 2 + src/core/classes/Iterable.ts | 4 +- yarn.lock | 16 +++ 13 files changed, 484 insertions(+), 22 deletions(-) create mode 100644 example/android/app/src/main/java/iterable/reactnativesdk/example/IterableJwtGenerator.java create mode 100644 example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenModule.kt create mode 100644 example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenPackage.kt create mode 100644 example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift create mode 100644 example/ios/ReactNativeSdkExample/JwtTokenModule.m create mode 100644 example/ios/ReactNativeSdkExample/JwtTokenModule.swift create mode 100644 example/src/utility/NativeJwtTokenModule.ts create mode 100644 example/src/utility/index.ts diff --git a/example/android/app/src/main/java/iterable/reactnativesdk/example/IterableJwtGenerator.java b/example/android/app/src/main/java/iterable/reactnativesdk/example/IterableJwtGenerator.java new file mode 100644 index 000000000..4fa760e33 --- /dev/null +++ b/example/android/app/src/main/java/iterable/reactnativesdk/example/IterableJwtGenerator.java @@ -0,0 +1,106 @@ +package com.iterable; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.Base64.Encoder; + +/** +* Utility class to generate JWTs for use with the Iterable API +* +* @author engineering@iterable.com +*/ +public class IterableJwtGenerator { + static Encoder encoder = Base64.getUrlEncoder().withoutPadding(); + + private static final String algorithm = "HmacSHA256"; + + // Iterable enforces a 1-year maximum token lifetime + private static final Duration maxTokenLifetime = Duration.ofDays(365); + + private static long millisToSeconds(long millis) { + return millis / 1000; + } + + private static final String encodedHeader = encoder.encodeToString( + "{\"alg\":\"HS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8) + ); + + /** + * Generates a JWT from the provided secret, header, and payload. Does not + * validate the header or payload. + * + * @param secret Your organization's shared secret with Iterable + * @param payload The JSON payload + * + * @return a signed JWT + */ + public static String generateToken(String secret, String payload) { + try { + String encodedPayload = encoder.encodeToString( + payload.getBytes(StandardCharsets.UTF_8) + ); + String encodedHeaderAndPayload = encodedHeader + "." + encodedPayload; + + // HMAC setup + Mac hmac = Mac.getInstance(algorithm); + SecretKeySpec keySpec = new SecretKeySpec( + secret.getBytes(StandardCharsets.UTF_8), algorithm + ); + hmac.init(keySpec); + + String signature = encoder.encodeToString( + hmac.doFinal( + encodedHeaderAndPayload.getBytes(StandardCharsets.UTF_8) + ) + ); + + return encodedHeaderAndPayload + "." + signature; + + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Generates a JWT (issued now, expires after the provided duration). + * + * @param secret Your organization's shared secret with Iterable. + * @param duration The token's expiration time. Up to one year. + * @param email The email to included in the token, or null. + * @param userId The userId to include in the token, or null. + * + * @return A JWT string + */ + public static String generateToken( + String secret, Duration duration, String email, String userId) { + + if (duration.compareTo(maxTokenLifetime) > 0) + throw new IllegalArgumentException( + "Duration must be one year or less." + ); + + if ((userId != null && email != null) || (userId == null && email == null)) + throw new IllegalArgumentException( + "The token must include a userId or email, but not both." + ); + + long now = millisToSeconds(System.currentTimeMillis()); + + String payload; + if (userId != null) + payload = String.format( + "{ \"userId\": \"%s\", \"iat\": %d, \"exp\": %d }", + userId, now, now + millisToSeconds(duration.toMillis()) + ); + else + payload = String.format( + "{ \"email\": \"%s\", \"iat\": %d, \"exp\": %d }", + email, now, now + millisToSeconds(duration.toMillis()) + ); + + return generateToken(secret, payload); + } +} diff --git a/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenModule.kt b/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenModule.kt new file mode 100644 index 000000000..8a6f7f018 --- /dev/null +++ b/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenModule.kt @@ -0,0 +1,37 @@ +package iterable.reactnativesdk.example + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.Promise +import com.iterable.IterableJwtGenerator +import java.time.Duration + +class JwtTokenModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + override fun getName(): String { + return NAME + } + + @ReactMethod + fun generateJwtToken( + secret: String, + durationMs: Double, + email: String?, + userId: String?, + promise: Promise + ) { + try { + val duration = Duration.ofMillis(durationMs.toLong()) + val token = IterableJwtGenerator.generateToken(secret, duration, email, userId) + promise.resolve(token) + } catch (e: Exception) { + promise.reject("JWT_GENERATION_ERROR", e.message, e) + } + } + + companion object { + const val NAME = "JwtTokenModule" + } +} + diff --git a/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenPackage.kt b/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenPackage.kt new file mode 100644 index 000000000..e05384909 --- /dev/null +++ b/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenPackage.kt @@ -0,0 +1,34 @@ +package iterable.reactnativesdk.example + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider + +class JwtTokenPackage : BaseReactPackage() { + + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return if (name == JwtTokenModule.NAME) { + JwtTokenModule(reactContext) + } else { + null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + val moduleInfos: MutableMap = HashMap() + moduleInfos[JwtTokenModule.NAME] = ReactModuleInfo( + JwtTokenModule.NAME, + JwtTokenModule.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false // isCxxModule + ) + moduleInfos + } + } +} + diff --git a/example/android/app/src/main/java/iterable/reactnativesdk/example/MainApplication.kt b/example/android/app/src/main/java/iterable/reactnativesdk/example/MainApplication.kt index d0fba2035..9c004c88b 100644 --- a/example/android/app/src/main/java/iterable/reactnativesdk/example/MainApplication.kt +++ b/example/android/app/src/main/java/iterable/reactnativesdk/example/MainApplication.kt @@ -20,6 +20,7 @@ class MainApplication : Application(), ReactApplication { PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) + add(JwtTokenPackage()) } override fun getJSMainModuleName(): String = "index" diff --git a/example/ios/ReactNativeSdkExample-Bridging-Header.h b/example/ios/ReactNativeSdkExample-Bridging-Header.h index 339994e93..856694030 100644 --- a/example/ios/ReactNativeSdkExample-Bridging-Header.h +++ b/example/ios/ReactNativeSdkExample-Bridging-Header.h @@ -2,3 +2,6 @@ // Use this file to import your target's public headers that you would like to // expose to Swift. // + +#import +#import diff --git a/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift b/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift new file mode 100644 index 000000000..c0f0344e0 --- /dev/null +++ b/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift @@ -0,0 +1,118 @@ +// +// IterableJwtGenerator.swift +// ReactNativeSdkExample +// +// Utility class to generate JWTs for use with the Iterable API +// + +import CryptoKit +import Foundation + +class IterableJwtGenerator { + + private static let algorithm = "HS256" + private static let maxTokenLifetimeMs: Int64 = 365 * 24 * 60 * 60 * 1000 // 1 year in milliseconds + + private static func millisToSeconds(_ millis: Int64) -> Int64 { + return millis / 1000 + } + + /// Base64 URL encode without padding + private static func base64UrlEncode(_ data: Data) -> String { + let base64 = data.base64EncodedString() + return + base64 + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } + + private static let encodedHeader: String = { + let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" + let headerData = header.data(using: .utf8)! + return base64UrlEncode(headerData) + }() + + /// Generates a JWT from the provided secret and payload + /// - Parameters: + /// - secret: Your organization's shared secret with Iterable + /// - payload: The JSON payload + /// - Returns: A signed JWT + static func generateToken(secret: String, payload: String) throws -> String { + guard let payloadData = payload.data(using: .utf8) else { + throw NSError( + domain: "JWTGenerator", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid payload"]) + } + + let encodedPayload = base64UrlEncode(payloadData) + let encodedHeaderAndPayload = "\(encodedHeader).\(encodedPayload)" + + guard let secretData = secret.data(using: .utf8), + let messageData = encodedHeaderAndPayload.data(using: .utf8) + else { + throw NSError( + domain: "JWTGenerator", code: 2, + userInfo: [NSLocalizedDescriptionKey: "Invalid secret or message"]) + } + + // HMAC-SHA256 signature + let key = SymmetricKey(data: secretData) + let signature = HMAC.authenticationCode(for: messageData, using: key) + let signatureData = Data(signature) + let encodedSignature = base64UrlEncode(signatureData) + + return "\(encodedHeaderAndPayload).\(encodedSignature)" + } + + /// Generates a JWT (issued now, expires after the provided duration) + /// - Parameters: + /// - secret: Your organization's shared secret with Iterable + /// - durationMs: The token's expiration time in milliseconds. Up to one year. + /// - email: The email to include in the token, or nil + /// - userId: The userId to include in the token, or nil + /// - Returns: A JWT string + static func generateToken(secret: String, durationMs: Int64, email: String?, userId: String?) + throws -> String + { + guard durationMs <= maxTokenLifetimeMs else { + throw NSError( + domain: "JWTGenerator", code: 3, + userInfo: [NSLocalizedDescriptionKey: "Duration must be one year or less."]) + } + + let hasEmail = email != nil && !email!.isEmpty + let hasUserId = userId != nil && !userId!.isEmpty + + guard (hasEmail && !hasUserId) || (!hasEmail && hasUserId) else { + throw NSError( + domain: "JWTGenerator", code: 4, + userInfo: [ + NSLocalizedDescriptionKey: "The token must include a userId or email, but not both." + ]) + } + + let now = millisToSeconds(Int64(Date().timeIntervalSince1970 * 1000)) + let exp = now + millisToSeconds(durationMs) + + var payloadDict: [String: Any] = [ + "iat": now, + "exp": exp, + ] + + if let userId = userId { + payloadDict["userId"] = userId + } else if let email = email { + payloadDict["email"] = email + } + + guard let payloadData = try? JSONSerialization.data(withJSONObject: payloadDict, options: []), + let payload = String(data: payloadData, encoding: .utf8) + else { + throw NSError( + domain: "JWTGenerator", code: 5, + userInfo: [NSLocalizedDescriptionKey: "Failed to serialize payload"]) + } + + return try generateToken(secret: secret, payload: payload) + } +} diff --git a/example/ios/ReactNativeSdkExample/JwtTokenModule.m b/example/ios/ReactNativeSdkExample/JwtTokenModule.m new file mode 100644 index 000000000..10390bc43 --- /dev/null +++ b/example/ios/ReactNativeSdkExample/JwtTokenModule.m @@ -0,0 +1,25 @@ +// +// JwtTokenModule.m +// ReactNativeSdkExample +// +// React Native module bridge for JWT token generation +// + +#import + +@interface RCT_EXTERN_MODULE(JwtTokenModule, NSObject) + +RCT_EXTERN_METHOD(generateJwtToken:(NSString *)secret + durationMs:(double)durationMs + email:(NSString *)email + userId:(NSString *)userId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +@end + diff --git a/example/ios/ReactNativeSdkExample/JwtTokenModule.swift b/example/ios/ReactNativeSdkExample/JwtTokenModule.swift new file mode 100644 index 000000000..5c121143b --- /dev/null +++ b/example/ios/ReactNativeSdkExample/JwtTokenModule.swift @@ -0,0 +1,40 @@ +// +// JwtTokenModule.swift +// ReactNativeSdkExample +// +// React Native module to generate JWT tokens +// + +import Foundation +import React + +@objc(JwtTokenModule) +class JwtTokenModule: NSObject { + + @objc + static func requiresMainQueueSetup() -> Bool { + return false + } + + @objc + func generateJwtToken( + _ secret: String, + durationMs: Double, + email: String?, + userId: String?, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) { + do { + let token = try IterableJwtGenerator.generateToken( + secret: secret, + durationMs: Int64(durationMs), + email: email, + userId: userId + ) + resolve(token) + } catch { + reject("JWT_GENERATION_ERROR", error.localizedDescription, error) + } + } +} diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index d648dd25c..08516aa48 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -20,6 +20,7 @@ import { import { Route } from '../constants/routes'; import type { RootStackParamList } from '../types/navigation'; +import NativeJwtTokenModule from '../utility/NativeJwtTokenModule'; type Navigation = StackNavigationProp; @@ -86,9 +87,18 @@ const IterableAppContext = createContext({ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const getIsEmail = (id: string) => EMAIL_REGEX.test(id); + export const IterableAppProvider: FunctionComponent< React.PropsWithChildren > = ({ children }) => { + console.log('process.env.ITBL_JWT_SECRET', process.env.ITBL_JWT_SECRET); + console.log('process.env.ITBL_ID', process.env.ITBL_ID); + console.log( + 'process.env.ITBL_IS_JWT_ENABLED', + process.env.ITBL_IS_JWT_ENABLED + ); + console.log('process.env.ITBL_API_KEY', process.env.ITBL_API_KEY); const [returnToInboxTrigger, setReturnToInboxTrigger] = useState(false); const [isInboxTab, setIsInboxTab] = useState(false); @@ -105,6 +115,21 @@ export const IterableAppProvider: FunctionComponent< const getUserId = useCallback(() => userId ?? process.env.ITBL_ID, [userId]); + const getJwtToken = useCallback(async () => { + const id = userId ?? process.env.ITBL_ID; + const idType = getIsEmail(id as string) ? 'email' : 'userId'; + const secret = process.env.ITBL_JWT_SECRET ?? ''; + const duration = 1000 * 60 * 60 * 24; // 1 day in milliseconds + const jwtToken = await NativeJwtTokenModule.generateJwtToken( + secret, + duration, + idType === 'email' ? (id as string) : null, // Email (can be null if userId is provided) + idType === 'userId' ? (id as string) : null // UserId (can be null if email is provided) + ); + + return jwtToken; + }, [userId]); + const login = useCallback(() => { const id = userId ?? process.env.ITBL_ID; @@ -112,10 +137,9 @@ export const IterableAppProvider: FunctionComponent< setLoginInProgress(true); - const isEmail = EMAIL_REGEX.test(id); - const fn = isEmail ? Iterable.setEmail : Iterable.setUserId; + const fn = getIsEmail(id) ? Iterable.setEmail : Iterable.setUserId; - fn(id); + fn(id, process.env.ITBL_JWT_SECRET); setIsLoggedIn(true); setLoginInProgress(false); @@ -173,23 +197,36 @@ 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`); - // }, - // }); - // }; + console.log('getJwtToken', getJwtToken()); + + if ( + process.env.ITBL_IS_JWT_ENABLED === 'true' && + process.env.ITBL_JWT_SECRET + ) { + console.log('CONFIGURED AUTH HANDLER'); + config.authHandler = async () => { + console.log(`authHandler`); + + const token = await getJwtToken(); + + console.log(`🚀 > IterableAppProvider > token:`, token); + + return Promise.resolve({ + // authToken: 'SomethingNotValid', + authToken: token, + successCallback: () => { + console.log(`authHandler > success`); + }, + // This is not firing + failureCallback: () => { + console.log(`authHandler > failure`); + }, + }); + }; + } setItblConfig(config); + console.log(`🚀 > IterableAppProvider > config:`, config); const key = apiKey ?? process.env.ITBL_API_KEY; @@ -232,7 +269,7 @@ export const IterableAppProvider: FunctionComponent< return Promise.resolve(true); }); }, - [apiKey, getUserId, login] + [apiKey, getUserId, login, getJwtToken] ); const logout = useCallback(() => { diff --git a/example/src/utility/NativeJwtTokenModule.ts b/example/src/utility/NativeJwtTokenModule.ts new file mode 100644 index 000000000..464cdc37b --- /dev/null +++ b/example/src/utility/NativeJwtTokenModule.ts @@ -0,0 +1,45 @@ +import { NativeModules, TurboModuleRegistry } from 'react-native'; +import type { TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { + generateJwtToken( + secret: string, + durationMs: number, + email: string | null, + userId: string | null + ): Promise; +} + +// Try to use TurboModule if available (New Architecture) +// Fall back to NativeModules (Old Architecture) +const isTurboModuleEnabled = + '__turboModuleProxy' in global && + (global as Record).__turboModuleProxy != null; + +let JwtTokenModule: Spec | null = null; + +try { + JwtTokenModule = isTurboModuleEnabled + ? TurboModuleRegistry.getEnforcing('JwtTokenModule') + : NativeModules.JwtTokenModule; +} catch { + // Module not available - will throw error when used + console.warn('JwtTokenModule native module is not available yet'); +} + +// Create a proxy that throws a helpful error when methods are called +const createModuleProxy = (): Spec => { + const handler: ProxyHandler = { + get(_target, prop) { + if (!JwtTokenModule) { + throw new Error( + `JwtTokenModule native module is not available. Make sure the native module is properly linked and the app has been rebuilt.\n\nFor iOS: Add Swift files to Xcode project (see SETUP_GUIDE.md)\nFor Android: Ensure JwtTokenPackage is registered in MainApplication.kt` + ); + } + return JwtTokenModule[prop as keyof Spec]; + }, + }; + return new Proxy({} as Spec, handler); +}; + +export default createModuleProxy(); diff --git a/example/src/utility/index.ts b/example/src/utility/index.ts new file mode 100644 index 000000000..fe6e37ed9 --- /dev/null +++ b/example/src/utility/index.ts @@ -0,0 +1,2 @@ +export { default as NativeJwtTokenModule } from './NativeJwtTokenModule'; +export { JwtTokenExample } from './JwtTokenExample'; 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/yarn.lock b/yarn.lock index 348741043..8a1310796 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1958,9 +1958,11 @@ __metadata: "@react-navigation/native": ^7.1.14 "@react-navigation/native-stack": ^7.0.0 "@react-navigation/stack": ^7.4.2 + "@types/crypto-js": ^4.2.2 "@types/jest": ^29.5.13 "@types/react": ^19.0.0 "@types/react-test-renderer": ^19.0.0 + crypto-js: ^4.2.0 react: 19.0.0 react-native: 0.79.3 react-native-builder-bob: ^0.30.2 @@ -3339,6 +3341,13 @@ __metadata: languageName: node linkType: hard +"@types/crypto-js@npm:^4.2.2": + version: 4.2.2 + resolution: "@types/crypto-js@npm:4.2.2" + checksum: 727daa0d2db35f0abefbab865c23213b6ee6a270e27e177939bbe4b70d1e84c2202d9fac4ea84859c4b4d49a4ee50f948f601327a39b69ec013288018ba07ca5 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -5492,6 +5501,13 @@ __metadata: languageName: node linkType: hard +"crypto-js@npm:^4.2.0": + version: 4.2.0 + resolution: "crypto-js@npm:4.2.0" + checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774 + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" From ad4878247fa0b57a31ce30cd21b8211e5bf837b7 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 18:26:11 -0700 Subject: [PATCH 05/12] chore: remove unused crypto-js and @types/crypto-js dependencies from yarn.lock --- yarn.lock | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8a1310796..348741043 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1958,11 +1958,9 @@ __metadata: "@react-navigation/native": ^7.1.14 "@react-navigation/native-stack": ^7.0.0 "@react-navigation/stack": ^7.4.2 - "@types/crypto-js": ^4.2.2 "@types/jest": ^29.5.13 "@types/react": ^19.0.0 "@types/react-test-renderer": ^19.0.0 - crypto-js: ^4.2.0 react: 19.0.0 react-native: 0.79.3 react-native-builder-bob: ^0.30.2 @@ -3341,13 +3339,6 @@ __metadata: languageName: node linkType: hard -"@types/crypto-js@npm:^4.2.2": - version: 4.2.2 - resolution: "@types/crypto-js@npm:4.2.2" - checksum: 727daa0d2db35f0abefbab865c23213b6ee6a270e27e177939bbe4b70d1e84c2202d9fac4ea84859c4b4d49a4ee50f948f601327a39b69ec013288018ba07ca5 - languageName: node - linkType: hard - "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -5501,13 +5492,6 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.2.0": - version: 4.2.0 - resolution: "crypto-js@npm:4.2.0" - checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774 - languageName: node - linkType: hard - "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" From 5df923fb87a9d241014d8ce0ab69d6fff7fa1fda Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 19:35:28 -0700 Subject: [PATCH 06/12] feat: add IterableJwtGenerator and JwtTokenModule for JWT token generation in React Native --- example/ios/IterableJwtGenerator.swift | 229 ++++++++++++++++++ .../JwtTokenModule.m => JwtTokenModule.mm} | 0 .../JwtTokenModule.swift | 0 .../project.pbxproj | 30 ++- .../IterableJwtGenerator.swift | 118 --------- example/src/hooks/useIterableApp.tsx | 63 ++--- 6 files changed, 272 insertions(+), 168 deletions(-) create mode 100644 example/ios/IterableJwtGenerator.swift rename example/ios/{ReactNativeSdkExample/JwtTokenModule.m => JwtTokenModule.mm} (100%) rename example/ios/{ReactNativeSdkExample => }/JwtTokenModule.swift (100%) delete mode 100644 example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift diff --git a/example/ios/IterableJwtGenerator.swift b/example/ios/IterableJwtGenerator.swift new file mode 100644 index 000000000..a2854014f --- /dev/null +++ b/example/ios/IterableJwtGenerator.swift @@ -0,0 +1,229 @@ +// +// IterableJwtGenerator.swift +// ReactNativeSdkExample +// +// Utility class to generate JWTs for use with the Iterable API +// + +import CryptoKit +import Foundation + +// class IterableJwtGenerator { + +// private static let algorithm = "HS256" +// private static let maxTokenLifetimeMs: Int64 = 365 * 24 * 60 * 60 * 1000 // 1 year in milliseconds + +// private static func millisToSeconds(_ millis: Int64) -> Int64 { +// return millis / 1000 +// } + +// /// Base64 URL encode without padding +// private static func base64UrlEncode(_ data: Data) -> String { +// let base64 = data.base64EncodedString() +// return +// base64 +// .replacingOccurrences(of: "+", with: "-") +// .replacingOccurrences(of: "/", with: "_") +// .replacingOccurrences(of: "=", with: "") +// } + +// private static let encodedHeader: String = { +// let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" +// let headerData = header.data(using: .utf8)! +// return base64UrlEncode(headerData) +// }() + +// /// Generates a JWT from the provided secret and payload +// /// - Parameters: +// /// - secret: Your organization's shared secret with Iterable +// /// - payload: The JSON payload +// /// - Returns: A signed JWT +// static func generateToken(secret: String, payload: String) throws -> String { +// guard let payloadData = payload.data(using: .utf8) else { +// throw NSError( +// domain: "JWTGenerator", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid payload"]) +// } + +// let encodedPayload = base64UrlEncode(payloadData) +// let encodedHeaderAndPayload = "\(encodedHeader).\(encodedPayload)" + +// guard let secretData = secret.data(using: .utf8), +// let messageData = encodedHeaderAndPayload.data(using: .utf8) +// else { +// throw NSError( +// domain: "JWTGenerator", code: 2, +// userInfo: [NSLocalizedDescriptionKey: "Invalid secret or message"]) +// } + +// // HMAC-SHA256 signature +// let key = SymmetricKey(data: secretData) +// let signature = HMAC.authenticationCode(for: messageData, using: key) +// let signatureData = Data(signature) +// let encodedSignature = base64UrlEncode(signatureData) + +// return "\(encodedHeaderAndPayload).\(encodedSignature)" +// } + +// /// Generates a JWT (issued now, expires after the provided duration) +// /// - Parameters: +// /// - secret: Your organization's shared secret with Iterable +// /// - durationMs: The token's expiration time in milliseconds. Up to one year. +// /// - email: The email to include in the token, or nil +// /// - userId: The userId to include in the token, or nil +// /// - Returns: A JWT string +// static func generateToken(secret: String, durationMs: Int64, email: String?, userId: String?) +// throws -> String +// { +// guard durationMs <= maxTokenLifetimeMs else { +// throw NSError( +// domain: "JWTGenerator", code: 3, +// userInfo: [NSLocalizedDescriptionKey: "Duration must be one year or less."]) +// } + +// let hasEmail = email != nil && !email!.isEmpty +// let hasUserId = userId != nil && !userId!.isEmpty + +// guard (hasEmail && !hasUserId) || (!hasEmail && hasUserId) else { +// throw NSError( +// domain: "JWTGenerator", code: 4, +// userInfo: [ +// NSLocalizedDescriptionKey: "The token must include a userId or email, but not both." +// ]) +// } + +// let now = millisToSeconds(Int64(Date().timeIntervalSince1970 * 1000)) +// let exp = now + millisToSeconds(durationMs) + +// var payloadDict: [String: Any] = [ +// "iat": now, +// "exp": exp, +// ] + +// if let userId = userId { +// payloadDict["userId"] = userId +// } else if let email = email { +// payloadDict["email"] = email +// } + +// guard let payloadData = try? JSONSerialization.data(withJSONObject: payloadDict, options: []), +// let payload = String(data: payloadData, encoding: .utf8) +// else { +// throw NSError( +// domain: "JWTGenerator", code: 5, +// userInfo: [NSLocalizedDescriptionKey: "Failed to serialize payload"]) +// } + +// return try generateToken(secret: secret, payload: payload) +// } +// } + +// +// IterableTokenGenerator.swift +// swift-sdk +// +// Created by Apple on 22/10/24. +// Copyright © 2024 Iterable. All rights reserved. +// + +@objcMembers public final class IterableJwtGenerator: NSObject { + + /// Base64 URL encode without padding (URL-safe base64 encoding for JWT) + private static func urlEncodedBase64(_ data: Data) -> String { + let base64 = data.base64EncodedString() + return + base64 + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } + + public static func generateJwtForEmail(secret: String, iat: Int, exp: Int, email: String) + -> String + { + struct Header: Encodable { + let alg = "HS256" + let typ = "JWT" + } + + struct Payload: Encodable { + var email = "" + var iat = Int(Date().timeIntervalSince1970) + var exp = Int(Date().timeIntervalSince1970) + 60 + + } + let headerJsonData = try! JSONEncoder().encode(Header()) + let headerBase64 = urlEncodedBase64(headerJsonData) + + let payloadJsonData = try! JSONEncoder().encode(Payload(email: email, iat: iat, exp: exp)) + let payloadBase64 = urlEncodedBase64(payloadJsonData) + + let toSign = Data((headerBase64 + "." + payloadBase64).utf8) + + if #available(iOS 13.0, *) { + let privateKey = SymmetricKey(data: Data(secret.utf8)) + let signature = HMAC.authenticationCode(for: toSign, using: privateKey) + let signatureBase64 = urlEncodedBase64(Data(signature)) + + let token = [headerBase64, payloadBase64, signatureBase64].joined(separator: ".") + + return token + } + return "" + } + + public static func generateJwtForUserId(secret: String, iat: Int, exp: Int, userId: String) + -> String + { + struct Header: Encodable { + let alg = "HS256" + let typ = "JWT" + } + + struct Payload: Encodable { + var userId = "" + var iat = Int(Date().timeIntervalSince1970) + var exp = Int(Date().timeIntervalSince1970) + 60 + + } + let headerJsonData = try! JSONEncoder().encode(Header()) + let headerBase64 = urlEncodedBase64(headerJsonData) + + let payloadJsonData = try! JSONEncoder().encode(Payload(userId: userId, iat: iat, exp: exp)) + let payloadBase64 = urlEncodedBase64(payloadJsonData) + + let toSign = Data((headerBase64 + "." + payloadBase64).utf8) + + if #available(iOS 13.0, *) { + let privateKey = SymmetricKey(data: Data(secret.utf8)) + let signature = HMAC.authenticationCode(for: toSign, using: privateKey) + let signatureBase64 = urlEncodedBase64(Data(signature)) + + let token = [headerBase64, payloadBase64, signatureBase64].joined(separator: ".") + + return token + } + return "" + } + + public static func generateToken( + secret: String, durationMs: Int64, email: String?, userId: String? + ) throws -> String { + // Convert durationMs from milliseconds to seconds + let durationSeconds = Double(durationMs) / 1000.0 + let currentTime = Date().timeIntervalSince1970 + + if userId != nil { + return generateJwtForUserId( + secret: secret, iat: Int(currentTime), + exp: Int(currentTime + durationSeconds), userId: userId!) + } else if email != nil { + return generateJwtForEmail( + secret: secret, iat: Int(currentTime), + exp: Int(currentTime + durationSeconds), email: email!) + } else { + throw NSError( + domain: "JWTGenerator", code: 6, + userInfo: [NSLocalizedDescriptionKey: "No email or userId provided"]) + } + } +} diff --git a/example/ios/ReactNativeSdkExample/JwtTokenModule.m b/example/ios/JwtTokenModule.mm similarity index 100% rename from example/ios/ReactNativeSdkExample/JwtTokenModule.m rename to example/ios/JwtTokenModule.mm diff --git a/example/ios/ReactNativeSdkExample/JwtTokenModule.swift b/example/ios/JwtTokenModule.swift similarity index 100% rename from example/ios/ReactNativeSdkExample/JwtTokenModule.swift rename to example/ios/JwtTokenModule.swift diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index 74e4dc4c9..766c9b681 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -10,6 +10,9 @@ 00E356F31AD99517003FC87E /* ReactNativeSdkExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779227332DFA3FB500D69EC0 /* AppDelegate.swift */; }; + 77E3B5772EA71A4B001449CE /* IterableJwtGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5742EA71A4B001449CE /* IterableJwtGenerator.swift */; }; + 77E3B5782EA71A4B001449CE /* JwtTokenModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5752EA71A4B001449CE /* JwtTokenModule.mm */; }; + 77E3B5792EA71A4B001449CE /* JwtTokenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5762EA71A4B001449CE /* JwtTokenModule.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 81F6A9EA0E1CCC1AD730C5D9 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 56080B9DEED42A97AD1B3D5C /* libPods-ReactNativeSdkExample.a */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; @@ -39,6 +42,9 @@ 779227312DFA3FB500D69EC0 /* ReactNativeSdkExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExample-Bridging-Header.h"; sourceTree = ""; }; 779227322DFA3FB500D69EC0 /* ReactNativeSdkExampleTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExampleTests-Bridging-Header.h"; sourceTree = ""; }; 779227332DFA3FB500D69EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ReactNativeSdkExample/AppDelegate.swift; sourceTree = ""; }; + 77E3B5742EA71A4B001449CE /* IterableJwtGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableJwtGenerator.swift; sourceTree = ""; }; + 77E3B5752EA71A4B001449CE /* JwtTokenModule.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = JwtTokenModule.mm; sourceTree = ""; }; + 77E3B5762EA71A4B001449CE /* JwtTokenModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JwtTokenModule.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; EA19B65827A1D757CC5AAC97 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -114,6 +120,9 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + 77E3B5742EA71A4B001449CE /* IterableJwtGenerator.swift */, + 77E3B5752EA71A4B001449CE /* JwtTokenModule.mm */, + 77E3B5762EA71A4B001449CE /* JwtTokenModule.swift */, 13B07FAE1A68108700A75B9A /* ReactNativeSdkExample */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* ReactNativeSdkExampleTests */, @@ -268,10 +277,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; @@ -307,10 +320,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; @@ -332,6 +349,9 @@ buildActionMask = 2147483647; files = ( 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */, + 77E3B5772EA71A4B001449CE /* IterableJwtGenerator.swift in Sources */, + 77E3B5782EA71A4B001449CE /* JwtTokenModule.mm in Sources */, + 77E3B5792EA71A4B001449CE /* JwtTokenModule.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -535,10 +555,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -611,10 +628,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift b/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift deleted file mode 100644 index c0f0344e0..000000000 --- a/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// IterableJwtGenerator.swift -// ReactNativeSdkExample -// -// Utility class to generate JWTs for use with the Iterable API -// - -import CryptoKit -import Foundation - -class IterableJwtGenerator { - - private static let algorithm = "HS256" - private static let maxTokenLifetimeMs: Int64 = 365 * 24 * 60 * 60 * 1000 // 1 year in milliseconds - - private static func millisToSeconds(_ millis: Int64) -> Int64 { - return millis / 1000 - } - - /// Base64 URL encode without padding - private static func base64UrlEncode(_ data: Data) -> String { - let base64 = data.base64EncodedString() - return - base64 - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "=", with: "") - } - - private static let encodedHeader: String = { - let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" - let headerData = header.data(using: .utf8)! - return base64UrlEncode(headerData) - }() - - /// Generates a JWT from the provided secret and payload - /// - Parameters: - /// - secret: Your organization's shared secret with Iterable - /// - payload: The JSON payload - /// - Returns: A signed JWT - static func generateToken(secret: String, payload: String) throws -> String { - guard let payloadData = payload.data(using: .utf8) else { - throw NSError( - domain: "JWTGenerator", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid payload"]) - } - - let encodedPayload = base64UrlEncode(payloadData) - let encodedHeaderAndPayload = "\(encodedHeader).\(encodedPayload)" - - guard let secretData = secret.data(using: .utf8), - let messageData = encodedHeaderAndPayload.data(using: .utf8) - else { - throw NSError( - domain: "JWTGenerator", code: 2, - userInfo: [NSLocalizedDescriptionKey: "Invalid secret or message"]) - } - - // HMAC-SHA256 signature - let key = SymmetricKey(data: secretData) - let signature = HMAC.authenticationCode(for: messageData, using: key) - let signatureData = Data(signature) - let encodedSignature = base64UrlEncode(signatureData) - - return "\(encodedHeaderAndPayload).\(encodedSignature)" - } - - /// Generates a JWT (issued now, expires after the provided duration) - /// - Parameters: - /// - secret: Your organization's shared secret with Iterable - /// - durationMs: The token's expiration time in milliseconds. Up to one year. - /// - email: The email to include in the token, or nil - /// - userId: The userId to include in the token, or nil - /// - Returns: A JWT string - static func generateToken(secret: String, durationMs: Int64, email: String?, userId: String?) - throws -> String - { - guard durationMs <= maxTokenLifetimeMs else { - throw NSError( - domain: "JWTGenerator", code: 3, - userInfo: [NSLocalizedDescriptionKey: "Duration must be one year or less."]) - } - - let hasEmail = email != nil && !email!.isEmpty - let hasUserId = userId != nil && !userId!.isEmpty - - guard (hasEmail && !hasUserId) || (!hasEmail && hasUserId) else { - throw NSError( - domain: "JWTGenerator", code: 4, - userInfo: [ - NSLocalizedDescriptionKey: "The token must include a userId or email, but not both." - ]) - } - - let now = millisToSeconds(Int64(Date().timeIntervalSince1970 * 1000)) - let exp = now + millisToSeconds(durationMs) - - var payloadDict: [String: Any] = [ - "iat": now, - "exp": exp, - ] - - if let userId = userId { - payloadDict["userId"] = userId - } else if let email = email { - payloadDict["email"] = email - } - - guard let payloadData = try? JSONSerialization.data(withJSONObject: payloadDict, options: []), - let payload = String(data: payloadData, encoding: .utf8) - else { - throw NSError( - domain: "JWTGenerator", code: 5, - userInfo: [NSLocalizedDescriptionKey: "Failed to serialize payload"]) - } - - return try generateToken(secret: secret, payload: payload) - } -} diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index 08516aa48..caa6392f7 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -92,13 +92,6 @@ const getIsEmail = (id: string) => EMAIL_REGEX.test(id); export const IterableAppProvider: FunctionComponent< React.PropsWithChildren > = ({ children }) => { - console.log('process.env.ITBL_JWT_SECRET', process.env.ITBL_JWT_SECRET); - console.log('process.env.ITBL_ID', process.env.ITBL_ID); - console.log( - 'process.env.ITBL_IS_JWT_ENABLED', - process.env.ITBL_IS_JWT_ENABLED - ); - console.log('process.env.ITBL_API_KEY', process.env.ITBL_API_KEY); const [returnToInboxTrigger, setReturnToInboxTrigger] = useState(false); const [isInboxTab, setIsInboxTab] = useState(false); @@ -139,7 +132,7 @@ export const IterableAppProvider: FunctionComponent< const fn = getIsEmail(id) ? Iterable.setEmail : Iterable.setUserId; - fn(id, process.env.ITBL_JWT_SECRET); + fn(id); setIsLoggedIn(true); setLoginInProgress(false); @@ -197,36 +190,19 @@ export const IterableAppProvider: FunctionComponent< config.inAppHandler = () => IterableInAppShowResponse.show; - console.log('getJwtToken', getJwtToken()); - if ( process.env.ITBL_IS_JWT_ENABLED === 'true' && process.env.ITBL_JWT_SECRET ) { console.log('CONFIGURED AUTH HANDLER'); config.authHandler = async () => { - console.log(`authHandler`); - const token = await getJwtToken(); - - console.log(`🚀 > IterableAppProvider > token:`, token); - - return Promise.resolve({ - // authToken: 'SomethingNotValid', - authToken: token, - successCallback: () => { - console.log(`authHandler > success`); - }, - // This is not firing - failureCallback: () => { - console.log(`authHandler > failure`); - }, - }); + // return 'SomethingNotValid'; // Uncomment this to test the failure callback + return token; }; } setItblConfig(config); - console.log(`🚀 > IterableAppProvider > config:`, config); const key = apiKey ?? process.env.ITBL_API_KEY; @@ -240,33 +216,36 @@ export const IterableAppProvider: FunctionComponent< .then((isSuccessful) => { setIsInitialized(isSuccessful); - if (!isSuccessful) - return Promise.reject('`Iterable.initialize` failed'); + console.log('🚀 > IterableAppProvider > isSuccessful:', isSuccessful); - if (getUserId()) { + if (!isSuccessful) { + // return Promise.reject('`Iterable.initialize` failed'); + throw new Error('`Iterable.initialize` failed'); + } else if (getUserId()) { login(); } return isSuccessful; }) .catch((err) => { - console.error( - '`Iterable.initialize` failed with the following error', - err - ); - setIsInitialized(false); - setLoginInProgress(false); - return Promise.reject(err); + console.log(`🚀 > IterableAppProvider > err:`, err); + // console.error( + // '`Iterable.initialize` failed with the following error', + // err + // ); + // setIsInitialized(false); + // setLoginInProgress(false); + // return Promise.reject(err); }) .finally(() => { // For some reason, ios is throwing an error on initialize. // To temporarily fix this, we're using the finally block to login. // MOB-10419: Find out why initialize is throwing an error on ios - setIsInitialized(true); - if (getUserId()) { - login(); - } - return Promise.resolve(true); + // setIsInitialized(true); + // if (getUserId()) { + // login(); + // } + // return Promise.resolve(true); }); }, [apiKey, getUserId, login, getJwtToken] From 40c4bad1c298024084ff4302f787d9d76a04aa7c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:20:35 -0700 Subject: [PATCH 07/12] fix: ensure login is called during initialization and improve error handling in IterableAppProvider --- example/src/hooks/useIterableApp.tsx | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index caa6392f7..a8baac7ab 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -141,6 +141,8 @@ export const IterableAppProvider: FunctionComponent< const initialize = useCallback( (navigation: Navigation) => { + login(); + const config = new IterableConfig(); config.inAppDisplayInterval = 1.0; // Min gap between in-apps. No need to set this in production. @@ -219,8 +221,7 @@ export const IterableAppProvider: FunctionComponent< console.log('🚀 > IterableAppProvider > isSuccessful:', isSuccessful); if (!isSuccessful) { - // return Promise.reject('`Iterable.initialize` failed'); - throw new Error('`Iterable.initialize` failed'); + return Promise.reject('`Iterable.initialize` failed'); } else if (getUserId()) { login(); } @@ -228,24 +229,23 @@ export const IterableAppProvider: FunctionComponent< return isSuccessful; }) .catch((err) => { - console.log(`🚀 > IterableAppProvider > err:`, err); - // console.error( - // '`Iterable.initialize` failed with the following error', - // err - // ); - // setIsInitialized(false); - // setLoginInProgress(false); - // return Promise.reject(err); + console.error( + '`Iterable.initialize` failed with the following error', + err + ); + setIsInitialized(false); + setLoginInProgress(false); + return Promise.reject(err); }) .finally(() => { // For some reason, ios is throwing an error on initialize. // To temporarily fix this, we're using the finally block to login. // MOB-10419: Find out why initialize is throwing an error on ios - // setIsInitialized(true); - // if (getUserId()) { - // login(); - // } - // return Promise.resolve(true); + setIsInitialized(true); + if (getUserId()) { + login(); + } + return Promise.resolve(true); }); }, [apiKey, getUserId, login, getJwtToken] From fea6232036965163a50540790cc0c1812bbb52ff Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:23:03 -0700 Subject: [PATCH 08/12] refactor: remove commented-out code from IterableJwtGenerator.swift to clean up the implementation --- example/ios/IterableJwtGenerator.swift | 117 ------------------------- 1 file changed, 117 deletions(-) diff --git a/example/ios/IterableJwtGenerator.swift b/example/ios/IterableJwtGenerator.swift index a2854014f..e9f56d7ba 100644 --- a/example/ios/IterableJwtGenerator.swift +++ b/example/ios/IterableJwtGenerator.swift @@ -8,123 +8,6 @@ import CryptoKit import Foundation -// class IterableJwtGenerator { - -// private static let algorithm = "HS256" -// private static let maxTokenLifetimeMs: Int64 = 365 * 24 * 60 * 60 * 1000 // 1 year in milliseconds - -// private static func millisToSeconds(_ millis: Int64) -> Int64 { -// return millis / 1000 -// } - -// /// Base64 URL encode without padding -// private static func base64UrlEncode(_ data: Data) -> String { -// let base64 = data.base64EncodedString() -// return -// base64 -// .replacingOccurrences(of: "+", with: "-") -// .replacingOccurrences(of: "/", with: "_") -// .replacingOccurrences(of: "=", with: "") -// } - -// private static let encodedHeader: String = { -// let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" -// let headerData = header.data(using: .utf8)! -// return base64UrlEncode(headerData) -// }() - -// /// Generates a JWT from the provided secret and payload -// /// - Parameters: -// /// - secret: Your organization's shared secret with Iterable -// /// - payload: The JSON payload -// /// - Returns: A signed JWT -// static func generateToken(secret: String, payload: String) throws -> String { -// guard let payloadData = payload.data(using: .utf8) else { -// throw NSError( -// domain: "JWTGenerator", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid payload"]) -// } - -// let encodedPayload = base64UrlEncode(payloadData) -// let encodedHeaderAndPayload = "\(encodedHeader).\(encodedPayload)" - -// guard let secretData = secret.data(using: .utf8), -// let messageData = encodedHeaderAndPayload.data(using: .utf8) -// else { -// throw NSError( -// domain: "JWTGenerator", code: 2, -// userInfo: [NSLocalizedDescriptionKey: "Invalid secret or message"]) -// } - -// // HMAC-SHA256 signature -// let key = SymmetricKey(data: secretData) -// let signature = HMAC.authenticationCode(for: messageData, using: key) -// let signatureData = Data(signature) -// let encodedSignature = base64UrlEncode(signatureData) - -// return "\(encodedHeaderAndPayload).\(encodedSignature)" -// } - -// /// Generates a JWT (issued now, expires after the provided duration) -// /// - Parameters: -// /// - secret: Your organization's shared secret with Iterable -// /// - durationMs: The token's expiration time in milliseconds. Up to one year. -// /// - email: The email to include in the token, or nil -// /// - userId: The userId to include in the token, or nil -// /// - Returns: A JWT string -// static func generateToken(secret: String, durationMs: Int64, email: String?, userId: String?) -// throws -> String -// { -// guard durationMs <= maxTokenLifetimeMs else { -// throw NSError( -// domain: "JWTGenerator", code: 3, -// userInfo: [NSLocalizedDescriptionKey: "Duration must be one year or less."]) -// } - -// let hasEmail = email != nil && !email!.isEmpty -// let hasUserId = userId != nil && !userId!.isEmpty - -// guard (hasEmail && !hasUserId) || (!hasEmail && hasUserId) else { -// throw NSError( -// domain: "JWTGenerator", code: 4, -// userInfo: [ -// NSLocalizedDescriptionKey: "The token must include a userId or email, but not both." -// ]) -// } - -// let now = millisToSeconds(Int64(Date().timeIntervalSince1970 * 1000)) -// let exp = now + millisToSeconds(durationMs) - -// var payloadDict: [String: Any] = [ -// "iat": now, -// "exp": exp, -// ] - -// if let userId = userId { -// payloadDict["userId"] = userId -// } else if let email = email { -// payloadDict["email"] = email -// } - -// guard let payloadData = try? JSONSerialization.data(withJSONObject: payloadDict, options: []), -// let payload = String(data: payloadData, encoding: .utf8) -// else { -// throw NSError( -// domain: "JWTGenerator", code: 5, -// userInfo: [NSLocalizedDescriptionKey: "Failed to serialize payload"]) -// } - -// return try generateToken(secret: secret, payload: payload) -// } -// } - -// -// IterableTokenGenerator.swift -// swift-sdk -// -// Created by Apple on 22/10/24. -// Copyright © 2024 Iterable. All rights reserved. -// - @objcMembers public final class IterableJwtGenerator: NSObject { /// Base64 URL encode without padding (URL-safe base64 encoding for JWT) From fd76f089078d3a63e5e7c8797154e452bc3ddbfc Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:29:00 -0700 Subject: [PATCH 09/12] refactor: remove unnecessary login calls and clean up error handling in IterableAppProvider --- example/src/hooks/useIterableApp.tsx | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index a8baac7ab..daa56244d 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -20,7 +20,7 @@ import { import { Route } from '../constants/routes'; import type { RootStackParamList } from '../types/navigation'; -import NativeJwtTokenModule from '../utility/NativeJwtTokenModule'; +import NativeJwtTokenModule from '../NativeJwtTokenModule'; type Navigation = StackNavigationProp; @@ -141,7 +141,9 @@ export const IterableAppProvider: FunctionComponent< const initialize = useCallback( (navigation: Navigation) => { - login(); + if (getUserId()) { + login(); + } const config = new IterableConfig(); @@ -222,8 +224,6 @@ export const IterableAppProvider: FunctionComponent< if (!isSuccessful) { return Promise.reject('`Iterable.initialize` failed'); - } else if (getUserId()) { - login(); } return isSuccessful; @@ -236,19 +236,9 @@ export const IterableAppProvider: FunctionComponent< setIsInitialized(false); setLoginInProgress(false); return Promise.reject(err); - }) - .finally(() => { - // For some reason, ios is throwing an error on initialize. - // To temporarily fix this, we're using the finally block to login. - // MOB-10419: Find out why initialize is throwing an error on ios - setIsInitialized(true); - if (getUserId()) { - login(); - } - return Promise.resolve(true); }); }, - [apiKey, getUserId, login, getJwtToken] + [getUserId, apiKey, login, getJwtToken] ); const logout = useCallback(() => { From b663dceacad8e730b82782ac6b6ad304fe91d573 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:31:48 -0700 Subject: [PATCH 10/12] refactor: remove console log statements to streamline IterableAppProvider --- example/src/hooks/useIterableApp.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index daa56244d..a4e1062b1 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -198,7 +198,6 @@ export const IterableAppProvider: FunctionComponent< process.env.ITBL_IS_JWT_ENABLED === 'true' && process.env.ITBL_JWT_SECRET ) { - console.log('CONFIGURED AUTH HANDLER'); config.authHandler = async () => { const token = await getJwtToken(); // return 'SomethingNotValid'; // Uncomment this to test the failure callback @@ -220,8 +219,6 @@ export const IterableAppProvider: FunctionComponent< .then((isSuccessful) => { setIsInitialized(isSuccessful); - console.log('🚀 > IterableAppProvider > isSuccessful:', isSuccessful); - if (!isSuccessful) { return Promise.reject('`Iterable.initialize` failed'); } From a58be2aad7e2eae11ab4ba5635c4080ab4e0db1b Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:34:17 -0700 Subject: [PATCH 11/12] feat: introduce NativeJwtTokenModule for JWT token generation and remove old utility index --- example/src/{utility => }/NativeJwtTokenModule.ts | 0 example/src/utility/index.ts | 2 -- 2 files changed, 2 deletions(-) rename example/src/{utility => }/NativeJwtTokenModule.ts (100%) delete mode 100644 example/src/utility/index.ts diff --git a/example/src/utility/NativeJwtTokenModule.ts b/example/src/NativeJwtTokenModule.ts similarity index 100% rename from example/src/utility/NativeJwtTokenModule.ts rename to example/src/NativeJwtTokenModule.ts diff --git a/example/src/utility/index.ts b/example/src/utility/index.ts deleted file mode 100644 index fe6e37ed9..000000000 --- a/example/src/utility/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as NativeJwtTokenModule } from './NativeJwtTokenModule'; -export { JwtTokenExample } from './JwtTokenExample'; From 13012d971a202b7bd395ccf06448f0c5eef271db Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 21 Oct 2025 09:02:21 -0700 Subject: [PATCH 12/12] refactor: enhance JWT generation methods and update .env.example comments for clarity --- example/.env.example | 2 +- example/ios/IterableJwtGenerator.swift | 63 ++++++++++---------------- example/src/hooks/useIterableApp.tsx | 3 +- 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/example/.env.example b/example/.env.example index 9d1ff99bc..5b5663df6 100644 --- a/example/.env.example +++ b/example/.env.example @@ -20,7 +20,7 @@ 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 # Is your api token JWT Enabled? -# Defaults to true +# Must be set to 'true' to enable JWT authentication ITBL_IS_JWT_ENABLED=true # Your Iterable user ID or email address diff --git a/example/ios/IterableJwtGenerator.swift b/example/ios/IterableJwtGenerator.swift index e9f56d7ba..8e4a9cbe7 100644 --- a/example/ios/IterableJwtGenerator.swift +++ b/example/ios/IterableJwtGenerator.swift @@ -10,6 +10,11 @@ import Foundation @objcMembers public final class IterableJwtGenerator: NSObject { + private struct Header: Encodable { + let alg = "HS256" + let typ = "JWT" + } + /// Base64 URL encode without padding (URL-safe base64 encoding for JWT) private static func urlEncodedBase64(_ data: Data) -> String { let base64 = data.base64EncodedString() @@ -20,24 +25,12 @@ import Foundation .replacingOccurrences(of: "=", with: "") } - public static func generateJwtForEmail(secret: String, iat: Int, exp: Int, email: String) - -> String - { - struct Header: Encodable { - let alg = "HS256" - let typ = "JWT" - } - - struct Payload: Encodable { - var email = "" - var iat = Int(Date().timeIntervalSince1970) - var exp = Int(Date().timeIntervalSince1970) + 60 - - } + /// Generic JWT generation helper that works with any Encodable payload + private static func generateJwt(secret: String, payload: T) -> String { let headerJsonData = try! JSONEncoder().encode(Header()) let headerBase64 = urlEncodedBase64(headerJsonData) - let payloadJsonData = try! JSONEncoder().encode(Payload(email: email, iat: iat, exp: exp)) + let payloadJsonData = try! JSONEncoder().encode(payload) let payloadBase64 = urlEncodedBase64(payloadJsonData) let toSign = Data((headerBase64 + "." + payloadBase64).utf8) @@ -54,38 +47,28 @@ import Foundation return "" } - public static func generateJwtForUserId(secret: String, iat: Int, exp: Int, userId: String) + public static func generateJwtForEmail(secret: String, iat: Int, exp: Int, email: String) -> String { - struct Header: Encodable { - let alg = "HS256" - let typ = "JWT" - } - struct Payload: Encodable { - var userId = "" - var iat = Int(Date().timeIntervalSince1970) - var exp = Int(Date().timeIntervalSince1970) + 60 - + var email: String + var iat: Int + var exp: Int } - let headerJsonData = try! JSONEncoder().encode(Header()) - let headerBase64 = urlEncodedBase64(headerJsonData) - let payloadJsonData = try! JSONEncoder().encode(Payload(userId: userId, iat: iat, exp: exp)) - let payloadBase64 = urlEncodedBase64(payloadJsonData) - - let toSign = Data((headerBase64 + "." + payloadBase64).utf8) - - if #available(iOS 13.0, *) { - let privateKey = SymmetricKey(data: Data(secret.utf8)) - let signature = HMAC.authenticationCode(for: toSign, using: privateKey) - let signatureBase64 = urlEncodedBase64(Data(signature)) - - let token = [headerBase64, payloadBase64, signatureBase64].joined(separator: ".") + return generateJwt(secret: secret, payload: Payload(email: email, iat: iat, exp: exp)) + } - return token + public static func generateJwtForUserId(secret: String, iat: Int, exp: Int, userId: String) + -> String + { + struct Payload: Encodable { + var userId: String + var iat: Int + var exp: Int } - return "" + + return generateJwt(secret: secret, payload: Payload(userId: userId, iat: iat, exp: exp)) } public static func generateToken( diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index a4e1062b1..c215ff22f 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -235,7 +235,8 @@ export const IterableAppProvider: FunctionComponent< return Promise.reject(err); }); }, - [getUserId, apiKey, login, getJwtToken] + // eslint-disable-next-line react-hooks/exhaustive-deps + [getUserId, apiKey, login, getJwtToken, userId] ); const logout = useCallback(() => {