Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8975ac1
chore: update Iterable API dependency version to 3.6.1
lposen Oct 7, 2025
ab8efaa
feat: implement onAuthFailure handling and pauseAuthRetries method in…
lposen Oct 7, 2025
1972fbb
feat: fix nullpointerexception on com.iterable.iterableapi.IterableIn…
lposen Oct 7, 2025
2cd8253
refactor: remove commented-out encryptionEnforced code from Serializa…
lposen Oct 7, 2025
fd74351
Merge branch 'jwt/MOB-10946-task-2-authfailure-and-retrypolicy-ts-cla…
lposen Oct 7, 2025
ef86b15
refactor: fix onAuthFailure call
lposen Oct 7, 2025
372df66
Merge branch 'jwt/MOB-10946-task-2-authfailure-and-retrypolicy-ts-cla…
lposen Oct 7, 2025
a7804e5
refactor: simplify authHandler type and standardize IterableAuthFailu…
lposen Oct 7, 2025
21ca59d
Merge branch 'jwt/MOB-10946-task-2-authfailure-and-retrypolicy-ts-cla…
lposen Oct 7, 2025
18cba09
Merge branch 'jwt/MOB-10946-task-2-authfailure-and-retrypolicy-ts-cla…
lposen Oct 7, 2025
050ad22
feat: enhance JWT error handling with detailed alerts for auth failures
lposen Oct 7, 2025
60bc88e
Merge branch 'jwt/MOB-10946-task-2-authfailure-and-retrypolicy-ts-cla…
lposen Oct 7, 2025
5fbb9c9
Merge branch 'jwt/MOB-10946-task-2-authfailure-and-retrypolicy-ts-cla…
lposen Oct 9, 2025
5e4e579
Merge branch 'jwt/MOB-10946-task-2-authfailure-and-retrypolicy-ts-cla…
lposen Oct 10, 2025
3737d57
fix: improve null safety in IterableInAppMessage.fromViewToken method
lposen Oct 10, 2025
7b8fdb1
Merge branch 'jwt/MOB-10946-task-2-authfailure-and-retrypolicy-ts-cla…
lposen Oct 10, 2025
67a806d
fix: removed comment description that no longer applies
lposen Oct 13, 2025
b31ffd7
Merge branch 'jwt/MOB-10946-task-2-authfailure-and-retrypolicy-ts-cla…
lposen Oct 14, 2025
ed20041
Merge branch 'jwt/master' into jwt/MOB-10947-task-3-android-retrypoli…
lposen Oct 14, 2025
0191b7b
chore: removed onTokenRegistrationFailed method as per PR comment
lposen Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
api "com.iterable:iterableapi:3.5.2"
api "com.iterable:iterableapi:3.6.1"
// api project(":iterableapi") // links to local android SDK repo rather than by release
}

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import com.iterable.iterableapi.AuthFailure;
import com.iterable.iterableapi.InboxSessionManager;
import com.iterable.iterableapi.IterableAction;
import com.iterable.iterableapi.IterableActionContext;
Expand Down Expand Up @@ -572,19 +573,33 @@ public String onAuthTokenRequested() {
}
}

@Override
public void onAuthFailure(AuthFailure authFailure) {
// Create a JSON object for the authFailure object
JSONObject messageJson = new JSONObject();
try {
messageJson.put("userKey", authFailure.userKey);
messageJson.put("failedAuthToken", authFailure.failedAuthToken);
messageJson.put("failedRequestTime", authFailure.failedRequestTime);
messageJson.put("failureReason", authFailure.failureReason.name());
WritableMap eventData = Serialization.convertJsonToMap(messageJson);
sendEvent(EventName.handleAuthFailureCalled.name(), eventData);
} catch (JSONException e) {
IterableLogger.v(TAG, "Failed to set authToken");
}
}

public void pauseAuthRetries(boolean pauseRetry) {
IterableApi.getInstance().pauseAuthRetries(pauseRetry);
}

@Override
public void onTokenRegistrationSuccessful(String authToken) {
IterableLogger.v(TAG, "authToken successfully set");
// MOB-10422: Pass successhandler to event listener
sendEvent(EventName.handleAuthSuccessCalled.name(), null);
}

@Override
public void onTokenRegistrationFailed(Throwable object) {
IterableLogger.v(TAG, "Failed to set authToken");
sendEvent(EventName.handleAuthFailureCalled.name(), null);
}

public void addListener(String eventName) {
// Keep: Required for RN built in Event Emitter Calls.
}
Expand Down
36 changes: 28 additions & 8 deletions android/src/main/java/com/iterable/reactnative/Serialization.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.iterable.iterableapi.IterableInboxSession;
import com.iterable.iterableapi.IterableLogger;
import com.iterable.iterableapi.RNIterableInternal;
import com.iterable.iterableapi.RetryPolicy;

import org.json.JSONArray;
import org.json.JSONException;
Expand Down Expand Up @@ -94,7 +95,7 @@ static CommerceItem commerceItemFromMap(JSONObject itemMap) throws JSONException
categories[i] = categoriesArray.getString(i);
}
}

return new CommerceItem(itemMap.getString("id"),
itemMap.getString("name"),
itemMap.getDouble("price"),
Expand Down Expand Up @@ -216,9 +217,17 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte

configBuilder.setDataRegion(iterableDataRegion);
}

if (iterableContextJSON.has("encryptionEnforced")) {
configBuilder.setEncryptionEnforced(iterableContextJSON.optBoolean("encryptionEnforced"));

if (iterableContextJSON.has("retryPolicy")) {
JSONObject retryPolicyJson = iterableContextJSON.getJSONObject("retryPolicy");
int maxRetry = retryPolicyJson.getInt("maxRetry");
long retryInterval = retryPolicyJson.getLong("retryInterval");
String retryBackoff = retryPolicyJson.getString("retryBackoff");
RetryPolicy.Type retryPolicyType = RetryPolicy.Type.LINEAR;
if (retryBackoff.equals("EXPONENTIAL")) {
retryPolicyType = RetryPolicy.Type.EXPONENTIAL;
}
configBuilder.setAuthRetryPolicy(new RetryPolicy(maxRetry, retryInterval, retryPolicyType));
Comment on lines +221 to +230
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 see RetryPolicy coming all the way from TS and getting applied at core level!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was all you from the previous work we did 🥇

}

return configBuilder;
Expand Down Expand Up @@ -257,7 +266,13 @@ static JSONObject actionContextToJson(IterableActionContext iterableActionContex
}

static IterableInboxSession.Impression inboxImpressionFromMap(JSONObject impressionMap) throws JSONException {
return new IterableInboxSession.Impression(impressionMap.getString("messageId"),
// Add null check for messageId to prevent NullPointerException
String messageId = impressionMap.optString("messageId", null);
if (messageId == null || messageId.isEmpty()) {
throw new JSONException("messageId is null or empty");
}

return new IterableInboxSession.Impression(messageId,
impressionMap.getBoolean("silentInbox"),
impressionMap.optInt("displayCount", 0),
(float) impressionMap.optDouble("duration", 0)
Expand All @@ -271,8 +286,13 @@ static List<IterableInboxSession.Impression> impressionsFromReadableArray(Readab
JSONArray impressionJsonArray = convertArrayToJson(array);

for (int i = 0; i < impressionJsonArray.length(); i++) {
JSONObject impressionObj = impressionJsonArray.getJSONObject(i);
list.add(inboxImpressionFromMap(impressionObj));
try {
JSONObject impressionObj = impressionJsonArray.getJSONObject(i);
list.add(inboxImpressionFromMap(impressionObj));
} catch (JSONException e) {
// Skip invalid entries instead of failing completely
IterableLogger.w(TAG, "Skipping invalid impression at index " + i + ": " + e.getLocalizedMessage());
}
}
} catch (JSONException e) {
IterableLogger.e(TAG, "Failed converting to JSONObject");
Expand All @@ -286,7 +306,7 @@ static List<IterableInboxSession.Impression> impressionsFromReadableArray(Readab
// ---------------------------------------------------------------------------------------
// region React Native JSON conversion methods
// obtained from https://gist.github.com/viperwarp/2beb6bbefcc268dee7ad

static WritableMap convertJsonToMap(JSONObject jsonObject) throws JSONException {
WritableMap map = new WritableNativeMap();

Expand Down
7 changes: 7 additions & 0 deletions android/src/newarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.iterable.iterableapi.AuthFailure;
import com.iterable.iterableapi.IterableLogger;

public class RNIterableAPIModule extends NativeRNIterableAPISpec {
private final ReactApplicationContext reactContext;
Expand Down Expand Up @@ -217,6 +219,11 @@ public void passAlongAuthToken(@Nullable String authToken) {
moduleImpl.passAlongAuthToken(authToken);
}

@Override
public void pauseAuthRetries(boolean pauseRetry) {
moduleImpl.pauseAuthRetries(pauseRetry);
}

public void sendEvent(@NonNull String eventName, @Nullable Object eventData) {
moduleImpl.sendEvent(eventName, eventData);
}
Expand Down
5 changes: 5 additions & 0 deletions android/src/oldarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ public void passAlongAuthToken(@Nullable String authToken) {
moduleImpl.passAlongAuthToken(authToken);
}

@ReactMethod
public void pauseAuthRetries(boolean pauseRetry) {
moduleImpl.pauseAuthRetries(pauseRetry);
}


public void sendEvent(@NonNull String eventName, @Nullable Object eventData) {
moduleImpl.sendEvent(eventName, eventData);
Expand Down
22 changes: 21 additions & 1 deletion example/src/hooks/useIterableApp.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { StackNavigationProp } from '@react-navigation/stack';
import {
type FunctionComponent,
createContext,
useCallback,
useContext,
useState,
type FunctionComponent,
} from 'react';
import { Alert } from 'react-native';

Expand Down Expand Up @@ -135,6 +135,10 @@ export const IterableAppProvider: FunctionComponent<

config.onJWTError = (authFailure) => {
console.error('Error fetching JWT:', authFailure);
Alert.alert(
`Error fetching JWT: ${authFailure.failureReason}`,
`Token: ${authFailure.failedAuthToken}`
);
};

config.urlHandler = (url: string) => {
Expand Down Expand Up @@ -162,6 +166,22 @@ 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`);
// },
// });
// };

setItblConfig(config);

const key = apiKey ?? process.env.ITBL_API_KEY;
Expand Down
22 changes: 11 additions & 11 deletions src/inApp/classes/IterableInAppMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,19 +135,19 @@ export class IterableInAppMessage {
* @returns A new instance of `IterableInAppMessage` populated with data from the `viewToken`.
*/
static fromViewToken(viewToken: ViewToken) {
const inAppMessage = viewToken.item.inAppMessage as IterableInAppMessage;
const inAppMessage = viewToken?.item?.inAppMessage as IterableInAppMessage;

return new IterableInAppMessage(
inAppMessage.messageId,
inAppMessage.campaignId,
inAppMessage.trigger,
inAppMessage.createdAt,
inAppMessage.expiresAt,
inAppMessage.saveToInbox,
inAppMessage.inboxMetadata,
inAppMessage.customPayload,
inAppMessage.read,
inAppMessage.priorityLevel
inAppMessage?.messageId,
inAppMessage?.campaignId,
inAppMessage?.trigger,
inAppMessage?.createdAt,
inAppMessage?.expiresAt,
inAppMessage?.saveToInbox,
inAppMessage?.inboxMetadata,
inAppMessage?.customPayload,
inAppMessage?.read,
inAppMessage?.priorityLevel
);
}

Expand Down
30 changes: 22 additions & 8 deletions src/inbox/components/IterableInboxMessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,30 @@ export const IterableInboxMessageList = ({
function getRowInfosFromViewTokens(
viewTokens: Array<ViewToken>
): Array<IterableInboxImpressionRowInfo> {
return viewTokens.map(function (viewToken) {
const inAppMessage = IterableInAppMessage.fromViewToken(viewToken);
return viewTokens
Copy link
Member

Choose a reason for hiding this comment

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

Not sure what viewToken is. Is it limited to

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's given by the onViewableItemsChanged callback from FlatList, a react-native component used later. The callback just shows what rows are in view.

At times, item was not defined for some reason... hence the fix.

.filter((viewToken) => {
// Filter out viewTokens that don't have valid items or inAppMessage
return viewToken?.item?.inAppMessage?.messageId;
})
.map(function (viewToken) {
try {
const inAppMessage = IterableInAppMessage.fromViewToken(viewToken);

const impression = {
messageId: inAppMessage.messageId,
silentInbox: inAppMessage.isSilentInbox(),
} as IterableInboxImpressionRowInfo;
const impression = {
messageId: inAppMessage?.messageId,
silentInbox: inAppMessage?.isSilentInbox(),
} as IterableInboxImpressionRowInfo;

return impression;
});
return impression;
} catch (error) {
// Log the error and return null to be filtered out
console.warn('Failed to create impression from ViewToken:', error);
return null;
}
})
.filter(
(impression) => impression !== null
) as Array<IterableInboxImpressionRowInfo>;
}

const inboxSessionViewabilityConfig: ViewabilityConfig = {
Expand Down