Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ ios/generated
android/generated

# Iterable
.env.local
.env
.xcode.env.local
coverage/
Expand Down
16 changes: 12 additions & 4 deletions example/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@
# 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?
# Defaults to true
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

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

The comment 'Defaults to true' is misleading. According to the code in useIterableApp.tsx, JWT is only enabled when explicitly set to the string 'true', not by default. This comment should be removed or clarified to state that this value must be explicitly set.

Suggested change
# Defaults to true
# Must be set to 'true' to enable JWT authentication

Copilot uses AI. Check for mistakes.
ITBL_IS_JWT_ENABLED=true

# Your Iterable user ID or email address
ITBL_ID=replace_this_with_your_user_id_or_email
ITBL_ID=replace_this_with_your_user_id_or_email
21 changes: 14 additions & 7 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_:
Expand All @@ -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.

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +77 to +104
Copy link

Choose a reason for hiding this comment

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

Found 2 issues:

1. Function with high complexity (count = 7): generateToken [qlty:function-complexity]


2. Function with many parameters (count = 4): generateToken [qlty:function-parameters]

}
}
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +17 to +22
Copy link

Choose a reason for hiding this comment

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

Function with many parameters (count = 5): generateJwtToken [qlty:function-parameters]

) {
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"
}
}

Original file line number Diff line number Diff line change
@@ -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() {
Copy link
Member

Choose a reason for hiding this comment

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

Package creation example! Great for team to understand how its done!


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<String, ReactModuleInfo> = HashMap()
moduleInfos[JwtTokenModule.NAME] = ReactModuleInfo(
JwtTokenModule.NAME,
JwtTokenModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false // isCxxModule
)
moduleInfos
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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())
Copy link
Member

Choose a reason for hiding this comment

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

Good to know this is how packages are added

}

override fun getJSMainModuleName(): String = "index"
Expand Down
112 changes: 112 additions & 0 deletions example/ios/IterableJwtGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// IterableJwtGenerator.swift
// ReactNativeSdkExample
//
// Utility class to generate JWTs for use with the Iterable API
//

import CryptoKit
import Foundation

@objcMembers public final class IterableJwtGenerator: NSObject {
Copy link
Member

Choose a reason for hiding this comment

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

Until I saw this, I was under the impression it will be using native layer's sample app code. Then I realized how! This makes sense. So now sample app in RN has its own native layer implementation! End to end native to TS layer work is implemented here! Great one Loren!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

YAY! So glad you approve 😁


/// 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
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

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

The Payload struct defines hardcoded default values for iat and exp that are overridden by the initializer parameters. These default values should be removed since they are misleading and never used (the struct is always initialized with explicit values from the function parameters). The same issue exists in the generateJwtForUserId function.

Copilot uses AI. Check for mistakes.

}
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<SHA256>.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<SHA256>.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"])
}
}
}
Loading