Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
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.10"
api "com.iterable:iterableapi:3.6.1"
// api project(":iterableapi") // links to local android SDK repo rather than by release
}

14 changes: 4 additions & 10 deletions example/src/hooks/useIterableApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ export const IterableAppProvider: FunctionComponent<

config.logLevel = IterableLogLevel.debug;

// Add network debugging to help identify the source of socket warnings
console.log('Iterable SDK initialized with network debugging enabled');

config.retryPolicy = {
maxRetry: 5,
retryInterval: 10,
Expand All @@ -168,6 +171,7 @@ export const IterableAppProvider: FunctionComponent<
// Initialize app
return Iterable.initialize(key, config)
.then((isSuccessful) => {
console.log('Iterable.initialize success', isSuccessful);
setIsInitialized(isSuccessful);

if (!isSuccessful)
Expand All @@ -187,16 +191,6 @@ 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]
Expand Down
Empty file added example_implementation.md
Empty file.
204 changes: 176 additions & 28 deletions ios/RNIterableAPI/ReactIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,9 @@ import React
private var passedAuthToken: String?
private var authHandlerSemaphore = DispatchSemaphore(value: 0)

// For new architecture: stored launch options
private var storedLaunchOptions: [UIApplication.LaunchOptionsKey: Any]?

private let inboxSessionManager = InboxSessionManager()

@objc func initialize(
Expand All @@ -505,7 +508,7 @@ import React
rejecter: @escaping RCTPromiseRejectBlock
) {
ITBInfo()
let launchOptions = createLaunchOptions()
// let launchOptions = createLaunchOptionsHybrid()
guard let configDictTyped = configDict as? [AnyHashable: Any] else {
rejecter("E_INVALID_CONFIG", "configDict could not be cast to [AnyHashable: Any]", nil)
return
Expand Down Expand Up @@ -540,16 +543,14 @@ import React
name: Notification.Name.iterableInboxChanged, object: nil)

DispatchQueue.main.async {
IterableAPI.initialize2(
IterableAPI.initialize(
apiKey: apiKey,
launchOptions: launchOptions,
config: iterableConfig,
apiEndPointOverride: apiEndPointOverride
) { result in
resolver(result)
}
launchOptions: nil,
config: iterableConfig
)

IterableAPI.setDeviceAttribute(name: "reactNativeSDKVersion", value: version)
resolver(true)
}
}

Expand All @@ -568,26 +569,173 @@ import React
IterableAPI.pauseAuthRetries(pauseRetry)
}

private func createLaunchOptions() -> [UIApplication.LaunchOptionsKey: Any]? {
guard let bridge = self.bridge else {
return nil
}
return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: bridge.launchOptions)
}

private static func createLaunchOptions(bridgeLaunchOptions: [AnyHashable: Any]?)
-> [UIApplication.LaunchOptionsKey: Any]?
{
guard let bridgeLaunchOptions = bridgeLaunchOptions,
let remoteNotification = bridgeLaunchOptions[
UIApplication.LaunchOptionsKey.remoteNotification.rawValue]
else {
return nil
}
var result = [UIApplication.LaunchOptionsKey: Any]()
result[UIApplication.LaunchOptionsKey.remoteNotification] = remoteNotification
return result
}
// @objc(setLaunchOptions:)
// public func setLaunchOptions(launchOptions: NSDictionary?) {
// ITBInfo()
// // Store launch options for new architecture
// // This allows the app to explicitly set launch options when using the new architecture
// self.storedLaunchOptions = launchOptions as? [UIApplication.LaunchOptionsKey: Any]
// }

// @objc(detectLaunchOptions:rejecter:)
// public func detectLaunchOptions(resolver: @escaping RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
// ITBInfo()
// // Auto-detect launch options from the current app state
// if let detectedOptions = detectLaunchOptionsFromAppState() {
// resolver(detectedOptions)
// } else {
// resolver(nil)
// }
// }

// private func createLaunchOptionsHybrid() -> [UIApplication.LaunchOptionsKey: Any]? {
// // First try: Bridge-based approach (works for old architecture)
// if let bridgeOptions = createLaunchOptions() {
// return bridgeOptions
// }

// // Second try: For new architecture (bridgeless), use stored launch options
// if let storedOptions = storedLaunchOptions {
// return storedOptions
// }

// // Third try: Auto-detect launch options from the system
// if let autoDetectedOptions = autoDetectLaunchOptions() {
// return autoDetectedOptions
// }

// // Fourth try: Detect launch options from current app state
// if let appStateOptions = detectLaunchOptionsFromAppState() {
// return appStateOptions
// }

// // Fallback: return nil if no launch options are available
// return nil
// }

// private func createLaunchOptions() -> [UIApplication.LaunchOptionsKey: Any]? {
// // Safely access bridge to avoid null pointer crashes
// guard let bridge = self.bridge, bridge.launchOptions != nil else {
// return nil
// }
// return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: bridge.launchOptions)
// }

// private func autoDetectLaunchOptions() -> [UIApplication.LaunchOptionsKey: Any]? {
// var launchOptions: [UIApplication.LaunchOptionsKey: Any] = [:]

// // Try to detect launch options from the current app state
// if #available(iOS 13.0, *) {
// // For iOS 13+, try to get launch options from connected scenes
// if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {

// // Check for ongoing user activities
// if let userActivity = UIApplication.shared.userActivity {
// launchOptions[.userActivityType] = userActivity.activityType
// if let userInfo = userActivity.userInfo {
// launchOptions[.userActivityUserInfo] = userInfo
// }
// }

// // Try to detect if app was launched from a notification
// if let notificationResponse = getCurrentNotificationResponse() {
// launchOptions[.remoteNotification] = notificationResponse
// }
// }
// } else {
// // For iOS 12 and below, try to get from app delegate
// if let appDelegate = UIApplication.shared.delegate {
// // Check for ongoing user activities
// if let userActivity = UIApplication.shared.userActivity {
// launchOptions[.userActivityType] = userActivity.activityType
// if let userInfo = userActivity.userInfo {
// launchOptions[.userActivityUserInfo] = userInfo
// }
// }

// // Try to detect if app was launched from a notification
// if let notificationResponse = getCurrentNotificationResponse() {
// launchOptions[.remoteNotification] = notificationResponse
// }
// }
// }

// // Check for recent push notifications from Iterable SDK
// if let lastPushPayload = IterableAPI.lastPushPayload {
// launchOptions[.remoteNotification] = lastPushPayload
// }

// // Return launch options if we found any
// return launchOptions.isEmpty ? nil : launchOptions
// }

// private func getCurrentNotificationResponse() -> [AnyHashable: Any]? {
// // Try to get the current notification response from the app's state
// // This is a best-effort approach to detect if the app was launched from a notification

// // Check if there's a recent notification in the app's state
// if let appDelegate = UIApplication.shared.delegate {
// // Try to access notification info from the app delegate
// // This is app-specific and may not always be available
// return nil
// }

// return nil
// }

// MARK: - Launch Options Detection from App State

// private func detectLaunchOptionsFromAppState() -> [UIApplication.LaunchOptionsKey: Any]? {
// var launchOptions: [UIApplication.LaunchOptionsKey: Any] = [:]

// // Check for ongoing user activities
// if let userActivity = UIApplication.shared.userActivity {
// launchOptions[.userActivityType] = userActivity.activityType
// if let userInfo = userActivity.userInfo {
// launchOptions[.userActivityUserInfo] = userInfo
// }
// }

// // Check for recent push notifications from Iterable SDK
// if let lastPushPayload = IterableAPI.lastPushPayload {
// launchOptions[.remoteNotification] = lastPushPayload
// }

// // Check for URL schemes that might have launched the app
// if let url = detectCurrentURL() {
// launchOptions[.url] = url
// }

// return launchOptions.isEmpty ? nil : launchOptions
// }

// private func detectCurrentURL() -> URL? {
// // Try to detect if the app was launched from a URL
// // This is a best-effort approach

// // Check if there's a current URL in the app's state
// if let appDelegate = UIApplication.shared.delegate {
// // Try to access URL info from the app delegate
// // This is app-specific and may not always be available
// return nil
// }

// return nil
// }

// private static func createLaunchOptions(bridgeLaunchOptions: [AnyHashable: Any]?)
// -> [UIApplication.LaunchOptionsKey: Any]?
// {
// guard let bridgeLaunchOptions = bridgeLaunchOptions,
// let remoteNotification = bridgeLaunchOptions[
// UIApplication.LaunchOptionsKey.remoteNotification.rawValue]
// else {
// return nil
// }
// var result = [UIApplication.LaunchOptionsKey: Any]()
// result[UIApplication.LaunchOptionsKey.remoteNotification] = remoteNotification
// return result
// }
}

extension ReactIterableAPI: IterableURLDelegate {
Expand Down
4 changes: 4 additions & 0 deletions src/api/NativeRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ export interface Spec extends TurboModule {
onAuthFailure(authFailure: { userKey: string; failedAuthToken: string; failedRequestTime: number; failureReason: string }): void;
pauseAuthRetries(pauseRetry: boolean): void;

// Launch options for new architecture
setLaunchOptions(launchOptions?: { [key: string]: unknown } | null): void;
detectLaunchOptions(): Promise<{ [key: string]: unknown } | null>;

// Wake app -- android only
wakeApp(): void;

Expand Down
51 changes: 49 additions & 2 deletions src/core/classes/Iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,47 @@ export class Iterable {
RNIterableAPI.setEmail(email, authToken);
}

/**
* Set launch options for the new architecture (bridgeless).
* This method should be called before initializing the SDK when using the new architecture.
*
* @param launchOptions - The launch options from the scene delegate
*
* @example
* ```typescript
* // Call this in your scene delegate before initializing the SDK
* Iterable.setLaunchOptions(launchOptions);
* Iterable.initialize(apiKey, config);
* ```
*/
static setLaunchOptions(launchOptions?: { [key: string]: unknown } | null) {
Iterable?.logger?.log('setLaunchOptions: ' + JSON.stringify(launchOptions));

RNIterableAPI.setLaunchOptions(launchOptions);
}

/**
* Auto-detect launch options from the current app state.
* This method attempts to detect launch options automatically without requiring manual setup.
*
* @returns Promise that resolves to detected launch options or null
*
* @example
* ```typescript
* // Auto-detect launch options before initializing
* const launchOptions = await Iterable.detectLaunchOptions();
* if (launchOptions) {
* Iterable.setLaunchOptions(launchOptions);
* }
* Iterable.initialize(apiKey, config);
* ```
*/
static async detectLaunchOptions(): Promise<{ [key: string]: unknown } | null> {
Iterable?.logger?.log('detectLaunchOptions: attempting to auto-detect launch options');

return await RNIterableAPI.detectLaunchOptions();
}

/**
* Get the email associated with the current user.
*
Expand Down Expand Up @@ -986,6 +1027,7 @@ export class Iterable {

Iterable.savedConfig.authHandler!()
.then((promiseResult) => {
console.log(`🚀 > Iterable > setupEventHandlers > promiseResult:`, promiseResult);
// Promise result can be either just String OR of type AuthResponse.
// If type AuthReponse, authToken will be parsed looking for `authToken` within promised object. Two additional listeners will be registered for success and failure callbacks sent by native bridge layer.
// Else it will be looked for as a String.
Expand All @@ -995,6 +1037,7 @@ export class Iterable {
);

setTimeout(() => {
console.log(`🚀 > Iterable > setupEventHandlers > authResponseCallback`, authResponseCallback);
if (
authResponseCallback === IterableAuthResponseResult.SUCCESS
) {
Expand Down Expand Up @@ -1022,7 +1065,10 @@ export class Iterable {
);
}
})
.catch((e) => Iterable?.logger?.log(e));
.catch((e) => {
Iterable?.logger?.log(e);
console.log(`🚀 > Iterable > setupEventHandlers > catch`, e);
});
});

RNEventEmitter.addListener(
Expand All @@ -1033,7 +1079,8 @@ export class Iterable {
);
RNEventEmitter.addListener(
IterableEventName.handleAuthFailureCalled,
() => {
(e: unknown) => {
console.log(`🚀 > Iterable > setupEventHandlers > handleAuthFailureCalled`, e);
authResponseCallback = IterableAuthResponseResult.FAILURE;
}
);
Expand Down
Loading