Skip to content

Commit d3ed7f9

Browse files
authored
feat: improve initialization state handling and expo auto init support (#504)
1 parent e6d37e4 commit d3ed7f9

8 files changed

Lines changed: 99 additions & 43 deletions

File tree

android/cio-core.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
dependencies {
2-
implementation "io.customer.android:datapipelines:$cioAndroidSDKVersion"
3-
implementation "io.customer.android:messaging-push-fcm:$cioAndroidSDKVersion"
4-
implementation "io.customer.android:messaging-in-app:$cioAndroidSDKVersion"
2+
api "io.customer.android:datapipelines:$cioAndroidSDKVersion"
3+
api "io.customer.android:messaging-push-fcm:$cioAndroidSDKVersion"
4+
api "io.customer.android:messaging-in-app:$cioAndroidSDKVersion"
55
}

android/src/main/java/io/customer/reactnative/sdk/NativeCustomerIOModuleImpl.kt

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,31 @@ internal object NativeCustomerIOModuleImpl {
2929
private val logger: Logger
3030
get() = SDKComponent.logger
3131

32-
private fun customerIO(): CustomerIO? = runCatching {
32+
// Returns CustomerIO instance if initialized, null otherwise, with configurable failure handling.
33+
private inline fun getSDKInstanceOrNull(
34+
onFailure: (exception: Throwable) -> Unit = {}
35+
): CustomerIO? = runCatching {
3336
// If the SDK is not initialized, `CustomerIO.instance()` throws an exception
3437
CustomerIO.instance()
35-
}.onFailure {
36-
logger.error("Customer.io instance not initialized")
37-
}.getOrNull()
38+
}.onFailure(onFailure).getOrNull()
39+
40+
// Returns CustomerIO instance if initialized, null otherwise, logging error on failure.
41+
private fun requireSDKInstance(): CustomerIO? = getSDKInstanceOrNull {
42+
logger.error("CustomerIO SDK is not initialized. Please call initialize() first.")
43+
}
3844

3945
fun initialize(
4046
reactContext: ReactApplicationContext,
4147
sdkConfig: ReadableMap?,
4248
promise: Promise?
4349
) {
50+
// Skip initialization if already initialized
51+
if (getSDKInstanceOrNull() != null) {
52+
logger.info("CustomerIO SDK is already initialized. Skipping initialization.")
53+
promise?.resolve(true)
54+
return
55+
}
56+
4457
try {
4558
val packageConfig = sdkConfig.toMap()
4659
val cdpApiKey = packageConfig.getTypedValue<String>(
@@ -98,7 +111,7 @@ internal object NativeCustomerIOModuleImpl {
98111
}
99112

100113
fun clearIdentify() {
101-
customerIO()?.clearIdentify()
114+
requireSDKInstance()?.clearIdentify()
102115
}
103116

104117
fun identify(params: ReadableMap?) {
@@ -111,39 +124,39 @@ internal object NativeCustomerIOModuleImpl {
111124
}
112125

113126
userId?.let {
114-
customerIO()?.identify(userId, traits.toMap())
127+
requireSDKInstance()?.identify(userId, traits.toMap())
115128
} ?: run {
116-
customerIO()?.profileAttributes = traits.toMap()
129+
requireSDKInstance()?.profileAttributes = traits.toMap()
117130
}
118131
}
119132

120133
fun track(name: String?, properties: ReadableMap?) {
121134
val eventName = assertNotNull(name) ?: return
122135

123-
customerIO()?.track(eventName, properties.toMap())
136+
requireSDKInstance()?.track(eventName, properties.toMap())
124137
}
125138

126139
fun setDeviceAttributes(attributes: ReadableMap?) {
127-
customerIO()?.deviceAttributes = attributes.toMap()
140+
requireSDKInstance()?.deviceAttributes = attributes.toMap()
128141
}
129142

130143
fun setProfileAttributes(attributes: ReadableMap?) {
131-
customerIO()?.profileAttributes = attributes.toMap()
144+
requireSDKInstance()?.profileAttributes = attributes.toMap()
132145
}
133146

134147
fun screen(title: String?, properties: ReadableMap?) {
135148
val screenTitle = assertNotNull(title) ?: return
136149

137-
customerIO()?.screen(screenTitle, properties.toMap())
150+
requireSDKInstance()?.screen(screenTitle, properties.toMap())
138151
}
139152

140153
fun registerDeviceToken(token: String?) {
141154
val deviceToken = assertNotNull(token) ?: return
142155

143-
customerIO()?.registerDeviceToken(deviceToken)
156+
requireSDKInstance()?.registerDeviceToken(deviceToken)
144157
}
145158

146159
fun deleteDeviceToken() {
147-
customerIO()?.deleteDeviceToken()
160+
requireSDKInstance()?.deleteDeviceToken()
148161
}
149162
}

api-extractor-output/customerio-reactnative.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export class CustomerIO {
7878
// (undocumented)
7979
static readonly inAppMessaging: CustomerIOInAppMessaging;
8080
static readonly initialize: (config: CioConfig) => Promise<void>;
81+
// @deprecated
8182
static readonly isInitialized: () => boolean;
8283
// (undocumented)
8384
static readonly pushMessaging: CustomerIOPushMessaging;

example/src/App.tsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ContentNavigator } from '@screens';
77
import { Storage } from '@services';
88
import { appTheme } from '@utils';
99
import { CioConfig, CioPushPermissionStatus, CustomerIO, InAppMessageEvent, InAppMessageEventType } from 'customerio-reactnative';
10-
import FlashMessage, { showMessage } from 'react-native-flash-message';
10+
import FlashMessage from 'react-native-flash-message';
1111
import { AppEnvValues } from './env';
1212

1313
export default function App({ appName }: { appName: string }) {
@@ -133,16 +133,8 @@ export default function App({ appName }: { appName: string }) {
133133
CustomerIO.clearIdentify();
134134
},
135135
onTrackEvent: (eventPayload) => {
136-
if (CustomerIO.isInitialized()) {
137-
console.log('Tracking event', eventPayload);
138-
CustomerIO.track(eventPayload.name, eventPayload.properties);
139-
} else {
140-
showMessage({
141-
message: 'CustomerIO not initialized',
142-
description: 'Please set the CustomerIO config',
143-
type: 'danger',
144-
});
145-
}
136+
console.log('Tracking event', eventPayload);
137+
CustomerIO.track(eventPayload.name, eventPayload.properties);
146138
},
147139
onProfileAttributes(attributes) {
148140
console.log('Setting profile attributes', attributes);
@@ -154,10 +146,8 @@ export default function App({ appName }: { appName: string }) {
154146
},
155147
onScreenChange(screenName) {
156148
// See 'src/screens/content-navigator.tsx' for the how we implemented screen auto-tracking
157-
if (CustomerIO.isInitialized()) {
158-
console.log('Tracking screen change', screenName);
159-
CustomerIO.screen(screenName);
160-
}
149+
console.log('Tracking screen change', screenName);
150+
CustomerIO.screen(screenName);
161151
},
162152
async onPushNotificationRequestPermisionButtonPress(): Promise<void> {
163153
console.log('Requesting push notification permission');

ios/wrappers/NativeCustomerIO.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ import CioMessagingInApp
66
@objc(NativeCustomerIO)
77
public class NativeCustomerIO: NSObject {
88
private let logger: CioInternalCommon.Logger = DIGraphShared.shared.logger
9+
/// Checks whether the CustomerIO SDK has been initialized.
10+
/// Returns `true` if the SDK has been successfully initialized, `false` otherwise.
11+
private var isInitialized: Bool { CustomerIO.shared.implementation != nil }
12+
13+
/// Ensures that the CustomerIO SDK is initialized before performing operations.
14+
/// Logs an error and returns false if the SDK is not initialized.
15+
private func ensureInitialized() -> Bool {
16+
guard isInitialized else {
17+
logger.error("CustomerIO SDK is not initialized. Please call initialize() first.")
18+
return false
19+
}
20+
return true
21+
}
922

1023
@objc
1124
func initialize(
@@ -14,6 +27,13 @@ public class NativeCustomerIO: NSObject {
1427
resolve: @escaping (RCTPromiseResolveBlock),
1528
reject: @escaping (RCTPromiseRejectBlock)
1629
) {
30+
// Skip initialization if already initialized
31+
if isInitialized {
32+
logger.info("CustomerIO SDK is already initialized. Skipping initialization.")
33+
resolve(true)
34+
return
35+
}
36+
1737
do {
1838
let packageSource = args["packageSource"] as? String
1939
let packageVersion = args["packageVersion"] as? String
@@ -50,6 +70,8 @@ public class NativeCustomerIO: NSObject {
5070

5171
@objc
5272
func identify(_ params: [String: Any]?) {
73+
guard ensureInitialized() else { return }
74+
5375
let userId = params?["userId"] as? String
5476
let traits = params?["traits"] as? [String: Any]
5577

@@ -70,36 +92,43 @@ public class NativeCustomerIO: NSObject {
7092

7193
@objc
7294
func clearIdentify() {
95+
guard ensureInitialized() else { return }
7396
CustomerIO.shared.clearIdentify()
7497
}
7598

7699
@objc
77100
func track(_ name: String, properties: [String: Any]?) {
101+
guard ensureInitialized() else { return }
78102
CustomerIO.shared.track(name: name, properties: properties)
79103
}
80104

81105
@objc
82106
func screen(_ title: String, properties: [String: Any]?) {
107+
guard ensureInitialized() else { return }
83108
CustomerIO.shared.screen(title: title, properties: properties)
84109
}
85110

86111
@objc
87112
func setProfileAttributes(_ attributes: [String: Any]) {
113+
guard ensureInitialized() else { return }
88114
CustomerIO.shared.profileAttributes = attributes
89115
}
90116

91117
@objc
92118
func setDeviceAttributes(_ attributes: [String: Any]) {
119+
guard ensureInitialized() else { return }
93120
CustomerIO.shared.deviceAttributes = attributes
94121
}
95122

96123
@objc
97124
func registerDeviceToken(_ token: String) {
125+
guard ensureInitialized() else { return }
98126
CustomerIO.shared.registerDeviceToken(token)
99127
}
100128

101129
@objc
102130
func deleteDeviceToken() {
131+
guard ensureInitialized() else { return }
103132
CustomerIO.shared.deleteDeviceToken()
104133
}
105134
}

ios/wrappers/inapp/ReactInAppEventListener.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import CioMessagingInApp
44
* React Native bridge for Customer.io in-app messaging events.
55
* Converts native SDK events to JavaScript compatible format.
66
*/
7-
class ReactInAppEventListener: InAppEventListener {
7+
public class ReactInAppEventListener: InAppEventListener {
88
// Shared instance for global access
9-
static let shared = ReactInAppEventListener()
9+
public static let shared = ReactInAppEventListener()
1010
// Event emitter function to send events to React Native layer
1111
private var eventEmitter: (([String: Any?]) -> Void)?
1212

src/customerio-cdp.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@ const nativeModule = ensureNativeModule(NativeModule);
2525

2626
// Wrapper function that ensures SDK is initialized before calling native methods
2727
const withNativeModule = <R>(fn: (native: CodegenSpec) => R): R => {
28-
if (!_initialized) {
29-
throw new Error(
30-
'CustomerIO SDK must be initialized before calling any methods. Please call CustomerIO.initialize() first.'
31-
);
32-
}
3328
return callNativeModule(nativeModule, fn);
3429
};
3530

@@ -155,9 +150,18 @@ export class CustomerIO {
155150
return withNativeModule((native) => native.deleteDeviceToken());
156151
};
157152

158-
/** Check if the CustomerIO SDK has been initialized. */
153+
/**
154+
* Check if the CustomerIO SDK has been initialized.
155+
* @deprecated This method will be removed in a future version. If you need this functionality, please contact us.
156+
*/
159157
static readonly isInitialized = () => _initialized;
160158

161159
static readonly inAppMessaging = new CustomerIOInAppMessaging();
162160
static readonly pushMessaging = new CustomerIOPushMessaging();
163161
}
162+
163+
// Initialize native logger when this module loads to ensure it's always available.
164+
// Since customerio-cdp.ts is the main SDK entry point and always imported,
165+
// this guarantees logger initialization even when native-logger-listener.ts
166+
// isn't directly accessed, also supporting auto-initialization in Expo apps.
167+
NativeLoggerListener.initNativeLogger();

src/native-logger-listener.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,35 @@ export class NativeLoggerListener {
6060
native.onCioLogEvent(logHandler);
6161
} catch {
6262
// Fallback to old arch NativeEventEmitter when new arch method fails
63-
const bridge = new NativeEventEmitter(native);
64-
bridge.addListener('CioLogEvent', logHandler);
63+
try {
64+
// Use try-catch to prevent crashes for cases where native module may
65+
// not be available instantly
66+
const bridge = new NativeEventEmitter(native);
67+
bridge.addListener('CioLogEvent', logHandler);
68+
} catch (error) {
69+
NativeLoggerListener.warn(
70+
'Failed to attach old arch log listener:',
71+
error
72+
);
73+
}
6574
}
6675
});
6776
this.isInitialized = true;
6877
}
6978

70-
static warn(message: string) {
79+
// Logs warning messages in development mode only, with CIO prefix
80+
static warn(message: string, error?: unknown) {
7181
if (__DEV__) {
72-
console.warn(this.loggerPrefix + message);
82+
console.warn(this.loggerPrefix + message, error);
83+
}
84+
}
85+
86+
// Initializes native logger listener with error handling
87+
static initNativeLogger() {
88+
try {
89+
NativeLoggerListener.initialize();
90+
} catch (error) {
91+
NativeLoggerListener.warn('Failed to initialize native logger:', error);
7392
}
7493
}
7594
}

0 commit comments

Comments
 (0)