Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ object NativeMessagingInAppModuleImpl {

private val inAppMessagingModule: ModuleMessagingInApp?
get() = kotlin.runCatching { CustomerIO.instance().inAppMessaging() }.getOrNull()
val inAppEventListener = ReactInAppEventListener()
val inAppEventListener = ReactInAppEventListener.instance

/**
* Adds InAppMessaging module to native Android SDK based on configuration provided by customer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.customer.messaginginapp.type.InAppMessage
* React Native bridge for Customer.io in-app messaging events.
* Converts native SDK events to JavaScript compatible format.
*/
class ReactInAppEventListener : InAppEventListener {
class ReactInAppEventListener private constructor() : InAppEventListener {
// Event emitter function to send events to React Native layer
private var eventEmitter: ((ReadableMap) -> Unit)? = null

Expand All @@ -30,6 +30,9 @@ class ReactInAppEventListener : InAppEventListener {
actionValue: String? = null,
actionName: String? = null,
) {
// Get the emitter, return early if not set
val emitter = eventEmitter ?: return

val data = buildMap {
put("eventType", eventType)
put("messageId", message.messageId)
Expand All @@ -38,7 +41,7 @@ class ReactInAppEventListener : InAppEventListener {
actionName?.let { put("actionName", it) }
}

eventEmitter?.invoke(Arguments.makeNativeMap(data))
emitter.invoke(Arguments.makeNativeMap(data))
}

override fun errorWithMessage(message: InAppMessage) = emitInAppEvent(
Expand Down Expand Up @@ -66,4 +69,9 @@ class ReactInAppEventListener : InAppEventListener {
eventType = "messageShown",
message = message,
)

companion object {
// Singleton instance with public visibility for direct access by Expo plugin
val instance: ReactInAppEventListener by lazy { ReactInAppEventListener() }
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package io.customer.reactnative.sdk.logging

import android.os.Build
import android.util.Log
import com.facebook.react.bridge.ReactApplicationContext
import io.customer.reactnative.sdk.NativeCustomerIOLoggingSpec
import io.customer.reactnative.sdk.util.onlyForLegacyArch

/**
* React Native module implementation for Customer.io Logging Native SDK
Expand All @@ -15,14 +13,6 @@ class NativeCustomerIOLoggingModule(
) : NativeCustomerIOLoggingSpec(reactContext) {
override fun getName(): String = NativeCustomerIOLoggingModuleImpl.NAME

// true if the app is currently running under armeabi/armeabi-v7a ABIs.
// We check only the first ABI in SUPPORTED_ABIS because the first one is most preferred ABI.
private val isABIArmeabi: Boolean by lazy {
Build.SUPPORTED_ABIS?.firstOrNull()
?.lowercase()
?.contains("armeabi") == true
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Good job cleaning this up!


/**
* Executes the given block and logs any uncaught exceptions using Android logger to protect
* against unexpected crashes and failures.
Expand All @@ -36,57 +26,21 @@ class NativeCustomerIOLoggingModule(
}
}

/**
* Executes the given action only if the current ABI supports it.
* Skips execution on armeabi/armeabi-v7a to prevent C++ crashes on unsupported architectures.
*/
private fun runOnSupportedAbi(action: () -> Unit) {
runWithTryCatch {
if (isABIArmeabi) {
// Skip execution on armeabi-v7a to avoid known native (C++) crashes on unsupported ABIs.
// This ensures stability on lower-end or legacy devices by preventing risky native calls.
return@runWithTryCatch
}

action()
}
}

override fun initialize() {
runWithTryCatch {
super.initialize()
if (isABIArmeabi) {
Log.i(
"[CIO]",
"Native logging is disabled on armeabi/armeabi-v7a ABI to avoid native crashes (Supported ABIs: ${Build.SUPPORTED_ABIS?.joinToString()})"
)
}
runOnSupportedAbi {
NativeCustomerIOLoggingModuleImpl.setLogEventEmitter { data ->
emitOnCioLogEvent(data)
}
NativeCustomerIOLoggingModuleImpl.setLogEventEmitter { data ->
emitOnCioLogEvent(data)
}
}
}

override fun invalidate() {
runOnSupportedAbi {
runWithTryCatch {
NativeCustomerIOLoggingModuleImpl.invalidate()
}
runWithTryCatch {
super.invalidate()
}
}

override fun addListener(eventName: String?) {
runOnSupportedAbi {
onlyForLegacyArch("addListener")
}
}

override fun removeListeners(count: Double) {
runOnSupportedAbi {
onlyForLegacyArch("removeListeners")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package io.customer.reactnative.sdk.messaginginapp

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import io.customer.reactnative.sdk.NativeCustomerIOMessagingInAppSpec
import io.customer.reactnative.sdk.util.onlyForLegacyArch

/**
* React Native module implementation for Customer.io In-App Messaging Native SDK
Expand All @@ -30,12 +28,4 @@ class NativeMessagingInAppModule(
override fun dismissMessage() {
NativeMessagingInAppModuleImpl.dismissMessage()
}

override fun addListener(eventName: String?) {
onlyForLegacyArch("addListener")
}

override fun removeListeners(count: Double) {
onlyForLegacyArch("removeListeners")
}
}
37 changes: 15 additions & 22 deletions src/customerio-inapp.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
NativeEventEmitter,
type EventSubscription,
type TurboModule,
} from 'react-native';
import { type EventSubscription, type TurboModule } from 'react-native';
import { InlineInAppMessageView } from './components';
import { NativeLoggerListener } from './native-logger-listener';
import NativeCustomerIOMessagingInApp, {
Expand All @@ -11,22 +7,13 @@ import NativeCustomerIOMessagingInApp, {
import type { InAppMessageEventType } from './types';
import { callNativeModule, ensureNativeModule } from './utils/native-bridge';

// Constant value used for emitting all events for in-app from native modules
const InAppEventListenerEventName = 'InAppEventListener';

/**
* Ensures all methods defined in codegen spec are implemented by the public module
*
* @internal
*/
interface NativeInAppSpec
extends Omit<
CodegenSpec,
| keyof TurboModule
| 'onInAppEventReceived'
| 'addListener'
| 'removeListeners'
> {}
extends Omit<CodegenSpec, keyof TurboModule | 'onInAppEventReceived'> {}

// Reference to the native CustomerIO Data Pipelines module for SDK operations
const nativeModule = ensureNativeModule(NativeCustomerIOMessagingInApp);
Expand Down Expand Up @@ -59,16 +46,22 @@ class CustomerIOInAppMessaging implements NativeInAppSpec {

return withNativeModule((native) => {
try {
// Try new arch TurboModule event listener (not available in old arch)
// Register TurboModule event listener and return subscription.
// This method is generated by codegen in the native modules.
// Wrapped in try-catch due to previous reports of crashes on certain Android architectures.
return native.onInAppEventReceived(emitter);
} catch {
} catch (error) {
NativeLoggerListener.warn(
'Falling back to legacy event emitter (likely using old architecture). ' +
'Switch to new architecture for better performance and event handling.'
'Failed to attach in-app event listener:',
error
);
// Fallback to old arch NativeEventEmitter when new arch method fails
const eventEmitter = new NativeEventEmitter(native);
return eventEmitter.addListener(InAppEventListenerEventName, emitter);
// Return a no-op subscription to maintain backwards compatibility
return {
Copy link
Contributor

Choose a reason for hiding this comment

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

If new-arch is now mandatory, can we throw an explicit error here so customers know they must enable TurboModules? otherwise it would be silent failures for them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Apps using old arch won't compile so the failure wouldn't be silent.

remove: () => {},
eventType: '',
key: 0,
subscriber: null as any,
} as EventSubscription;
}
});
}
Expand Down
20 changes: 5 additions & 15 deletions src/native-logger-listener.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

On old architecture:
- First call: onCioLogEvent() throws → logs warning → sets isInitialized = true
- Subsequent calls: Early return at line 23 prevents any retry

Once marked initialized, the system never attempts to reconnect, is this the intent here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Old arch is removed in next PRs in stack

Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { NativeEventEmitter } from 'react-native';
import NativeCustomerIOLogging from './specs/modules/NativeCustomerIOLogging';
import { CioLogLevel } from './types';
import { callNativeModule, ensureNativeModule } from './utils/native-bridge';
Expand Down Expand Up @@ -56,21 +55,12 @@ export class NativeLoggerListener {

withNativeModule((native) => {
try {
// Try new arch TurboModule log listener (not available in old arch)
// Register TurboModule event listener and return subscription.
// This method is generated by codegen in the native modules.
// Wrapped in try-catch due to previous reports of crashes on certain Android architectures.
native.onCioLogEvent(logHandler);
} catch {
// Fallback to old arch NativeEventEmitter when new arch method fails
try {
// Use try-catch to prevent crashes for cases where native module may
// not be available instantly
const bridge = new NativeEventEmitter(native);
bridge.addListener('CioLogEvent', logHandler);
} catch (error) {
NativeLoggerListener.warn(
'Failed to attach old arch log listener:',
error
);
}
} catch (error) {
NativeLoggerListener.warn('Failed to attach log listener:', error);
}
});
this.isInitialized = true;
Expand Down
6 changes: 0 additions & 6 deletions src/specs/modules/NativeCustomerIOLogging.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TurboModuleRegistry, type TurboModule } from 'react-native';
import type {
Double,
EventEmitter,
UnsafeObject,
} from 'react-native/Libraries/Types/CodegenTypes';
Expand All @@ -18,11 +17,6 @@ import type {
/** TurboModule interface for CustomerIO logging native operations */
export interface Spec extends TurboModule {
readonly onCioLogEvent: EventEmitter<UnsafeObject>;
// Old architecture support: EventEmitter requires these methods for proper functionality
/** @internal - Registers an event listener for old architecture EventEmitter */
addListener: (eventName: string) => void;
/** @internal - Removes event listeners for old architecture EventEmitter */
removeListeners: (count: Double) => void;
}

export default TurboModuleRegistry.getEnforcing<Spec>(
Expand Down
6 changes: 0 additions & 6 deletions src/specs/modules/NativeCustomerIOMessagingInApp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TurboModuleRegistry, type TurboModule } from 'react-native';
import type {
Double,
EventEmitter,
UnsafeObject,
} from 'react-native/Libraries/Types/CodegenTypes';
Expand All @@ -16,11 +15,6 @@ import type {
export interface Spec extends TurboModule {
dismissMessage(): void;
readonly onInAppEventReceived: EventEmitter<UnsafeObject>;
// Old architecture support: EventEmitter requires these methods for proper functionality
/** @internal - Registers an event listener for old architecture EventEmitter */
addListener: (eventName: string) => void;
/** @internal - Removes event listeners for old architecture EventEmitter */
removeListeners: (count: Double) => void;
}

export default TurboModuleRegistry.getEnforcing<Spec>(
Expand Down
Loading