diff --git a/android/build.gradle b/android/build.gradle index a4f938ffc..d546cce98 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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 } diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 981358be6..57cf9a0b8 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -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; @@ -572,6 +573,26 @@ 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"); @@ -579,12 +600,6 @@ public void onTokenRegistrationSuccessful(String authToken) { 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. } diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 3a1f536a6..92c549554 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -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; @@ -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"), @@ -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)); } return configBuilder; @@ -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) @@ -271,8 +286,13 @@ static List 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"); @@ -286,7 +306,7 @@ static List 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(); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 4386e0d7f..f145bab10 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -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; @@ -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); } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 27b04ea17..c3a72339b 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -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); diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index f19daef5d..e561b22f8 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -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'; @@ -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) => { @@ -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; diff --git a/src/inApp/classes/IterableInAppMessage.ts b/src/inApp/classes/IterableInAppMessage.ts index 8a5b816bf..c77b08e63 100644 --- a/src/inApp/classes/IterableInAppMessage.ts +++ b/src/inApp/classes/IterableInAppMessage.ts @@ -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 ); } diff --git a/src/inbox/components/IterableInboxMessageList.tsx b/src/inbox/components/IterableInboxMessageList.tsx index 95d6707c5..b74cc7097 100644 --- a/src/inbox/components/IterableInboxMessageList.tsx +++ b/src/inbox/components/IterableInboxMessageList.tsx @@ -95,16 +95,30 @@ export const IterableInboxMessageList = ({ function getRowInfosFromViewTokens( viewTokens: Array ): Array { - return viewTokens.map(function (viewToken) { - const inAppMessage = IterableInAppMessage.fromViewToken(viewToken); + return viewTokens + .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; } const inboxSessionViewabilityConfig: ViewabilityConfig = {