@attentive-mobile/attentive-react-native-sdk 2.0.0-beta.5 → 2.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -11
- package/android/build.gradle +4 -1
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveNotificationStore.kt +60 -0
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentivePushHelper.kt +15 -7
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt +370 -140
- package/android/src/test/kotlin/com/attentivereactnativesdk/AttentiveNotificationStoreTest.kt +103 -0
- package/attentive-react-native-sdk.podspec +1 -1
- package/ios/AttentiveReactNativeSdk.mm +17 -2
- package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/xcuserdata/zheref.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/Bridging/ATTNNativeSDK.swift +116 -46
- package/ios/Bridging/AttentiveSDKManager.swift +196 -27
- package/ios/Podfile +1 -1
- package/lib/commonjs/NativeAttentiveReactNativeSdk.js +1 -1
- package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -1
- package/lib/commonjs/eventTypes.js.map +1 -1
- package/lib/commonjs/index.js +50 -17
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeAttentiveReactNativeSdk.js +2 -2
- package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -1
- package/lib/module/eventTypes.js.map +1 -1
- package/lib/module/index.js +50 -18
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +12 -1
- package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +46 -18
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/NativeAttentiveReactNativeSdk.ts +69 -52
- package/src/index.tsx +53 -17
package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
package com.attentivereactnativesdk
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
4
5
|
import android.util.Log
|
|
5
6
|
import android.view.ViewGroup
|
|
6
7
|
import androidx.annotation.NonNull
|
|
7
8
|
import androidx.annotation.Nullable
|
|
8
9
|
import com.attentive.androidsdk.AttentiveConfig
|
|
9
|
-
import com.attentive.androidsdk.
|
|
10
|
+
import com.attentive.androidsdk.AttentiveSdk
|
|
10
11
|
import com.attentive.androidsdk.UserIdentifiers
|
|
11
12
|
import com.attentive.androidsdk.creatives.Creative
|
|
12
13
|
import com.attentive.androidsdk.events.AddToCartEvent
|
|
14
|
+
import com.attentive.androidsdk.events.Cart
|
|
13
15
|
import com.attentive.androidsdk.events.CustomEvent
|
|
14
16
|
import com.attentive.androidsdk.events.Item
|
|
15
17
|
import com.attentive.androidsdk.events.Order
|
|
16
18
|
import com.attentive.androidsdk.events.Price
|
|
17
19
|
import com.attentive.androidsdk.events.ProductViewEvent
|
|
18
20
|
import com.attentive.androidsdk.events.PurchaseEvent
|
|
21
|
+
import com.attentive.androidsdk.push.TokenFetchResult
|
|
22
|
+
import com.facebook.react.bridge.Arguments
|
|
19
23
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
20
24
|
import com.facebook.react.bridge.ReactMethod
|
|
21
25
|
import com.facebook.react.bridge.ReadableArray
|
|
@@ -23,7 +27,9 @@ import com.facebook.react.bridge.ReadableMap
|
|
|
23
27
|
import com.facebook.react.bridge.UiThreadUtil
|
|
24
28
|
import com.facebook.react.bridge.Promise
|
|
25
29
|
import com.facebook.react.bridge.Callback
|
|
30
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
26
31
|
import com.attentivereactnativesdk.debug.AttentiveDebugHelper
|
|
32
|
+
import com.attentive.androidsdk.AttentiveLogLevel
|
|
27
33
|
import java.math.BigDecimal
|
|
28
34
|
import java.security.InvalidParameterException
|
|
29
35
|
import java.util.Currency
|
|
@@ -50,22 +56,45 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
50
56
|
return NAME
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* TypeScript-facing initialize() — intentionally a no-op on Android.
|
|
61
|
+
*
|
|
62
|
+
* On Android, AttentiveSdk.initialize() MUST be called from your Application.onCreate()
|
|
63
|
+
* (native code) so that lifecycle observers (AppLaunchTracker, etc.) are registered
|
|
64
|
+
* before the React Native bridge is ready. Calling this from TypeScript on Android has
|
|
65
|
+
* no effect and will not initialize the SDK.
|
|
66
|
+
*
|
|
67
|
+
* Required native setup in your Application class:
|
|
68
|
+
* ```kotlin
|
|
69
|
+
* override fun onCreate() {
|
|
70
|
+
* super.onCreate()
|
|
71
|
+
* val config = AttentiveConfig.Builder()
|
|
72
|
+
* .applicationContext(this)
|
|
73
|
+
* .domain("YOUR_ATTENTIVE_DOMAIN")
|
|
74
|
+
* .mode(AttentiveConfig.Mode.PRODUCTION)
|
|
75
|
+
* .build()
|
|
76
|
+
* AttentiveSdk.initialize(config)
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* See the README.md "Android Native Initialization" section for the full guide.
|
|
81
|
+
* All other SDK operations (identify, recordEvent, push) are handled from TypeScript as normal.
|
|
82
|
+
*/
|
|
53
83
|
override fun initialize(
|
|
54
84
|
attentiveDomain: String,
|
|
55
85
|
mode: String,
|
|
56
86
|
skipFatigueOnCreatives: Boolean,
|
|
57
87
|
enableDebugger: Boolean
|
|
58
88
|
) {
|
|
59
|
-
// Initialize debug helper
|
|
60
89
|
debugHelper.initialize(enableDebugger)
|
|
61
90
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
.
|
|
68
|
-
|
|
91
|
+
Log.w(
|
|
92
|
+
TAG,
|
|
93
|
+
"[AttentiveSDK] initialize() called from TypeScript is a NO-OP on Android. " +
|
|
94
|
+
"You must call AttentiveSdk.initialize(config) from your Application.onCreate() " +
|
|
95
|
+
"so that lifecycle observers are registered before the React Native bridge is ready. " +
|
|
96
|
+
"See README.md § 'Android Native Initialization' for the required setup."
|
|
97
|
+
)
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
override fun triggerCreative(creativeId: String?) {
|
|
@@ -77,7 +106,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
77
106
|
currentActivity.window.decorView.rootView as ViewGroup
|
|
78
107
|
// The following calls edit the view hierarchy so they must run on the UI thread
|
|
79
108
|
UiThreadUtil.runOnUiThread {
|
|
80
|
-
creative = Creative(attentiveConfig,
|
|
109
|
+
creative = Creative(attentiveConfig!!, rootView, currentActivity)
|
|
81
110
|
creative?.trigger(null, creativeId)
|
|
82
111
|
if (debugHelper.isDebuggingEnabled()) {
|
|
83
112
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -153,15 +182,29 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
153
182
|
override fun recordProductViewEvent(items: ReadableArray, deeplink: String?) {
|
|
154
183
|
Log.i(TAG, "Sending product viewed event")
|
|
155
184
|
|
|
185
|
+
val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
|
|
186
|
+
|
|
156
187
|
val itemsList = buildItems(items)
|
|
157
|
-
val productViewEvent = ProductViewEvent.Builder(itemsList).deeplink(deeplink).build()
|
|
188
|
+
val productViewEvent = ProductViewEvent.Builder().items(itemsList).deeplink(deeplink).build()
|
|
158
189
|
|
|
159
|
-
|
|
190
|
+
try {
|
|
191
|
+
AttentiveSdk.recordEvent(productViewEvent)
|
|
192
|
+
} catch (e: Exception) {
|
|
193
|
+
Log.e(
|
|
194
|
+
TAG,
|
|
195
|
+
"[AttentiveSDK] recordProductViewEvent failed — SDK may not be initialized. " +
|
|
196
|
+
"On Android, call AttentiveSdk.initialize(config) from Application.onCreate() " +
|
|
197
|
+
"before recording events. Error: ${e.message}"
|
|
198
|
+
)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
160
201
|
|
|
161
202
|
if (debugHelper.isDebuggingEnabled()) {
|
|
162
203
|
val debugData = mutableMapOf<String, Any>()
|
|
163
204
|
debugData["items_count"] = itemsList.size.toString()
|
|
164
205
|
debugData["deeplink"] = deeplink ?: ""
|
|
206
|
+
debugData["all_items"] = itemsDebugData
|
|
207
|
+
itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
|
|
165
208
|
debugHelper.showDebugInfo("Product View Event", debugData)
|
|
166
209
|
}
|
|
167
210
|
}
|
|
@@ -173,19 +216,37 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
173
216
|
cartCoupon: String?
|
|
174
217
|
) {
|
|
175
218
|
Log.i(TAG, "Sending purchase event")
|
|
176
|
-
val order = Order.Builder(orderId).build()
|
|
177
219
|
|
|
220
|
+
val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
|
|
221
|
+
|
|
222
|
+
val order = Order.Builder().orderId(orderId).build()
|
|
178
223
|
val itemsList = buildItems(items)
|
|
179
|
-
val
|
|
224
|
+
val purchaseBuilder = PurchaseEvent.Builder(itemsList, order)
|
|
225
|
+
if (!cartId.isNullOrEmpty()) {
|
|
226
|
+
purchaseBuilder.cart(Cart.Builder().cartId(cartId).cartCoupon(cartCoupon).build())
|
|
227
|
+
}
|
|
228
|
+
val purchaseEvent = purchaseBuilder.build()
|
|
180
229
|
|
|
181
|
-
|
|
230
|
+
try {
|
|
231
|
+
AttentiveSdk.recordEvent(purchaseEvent)
|
|
232
|
+
} catch (e: Exception) {
|
|
233
|
+
Log.e(
|
|
234
|
+
TAG,
|
|
235
|
+
"[AttentiveSDK] recordPurchaseEvent failed — SDK may not be initialized. " +
|
|
236
|
+
"On Android, call AttentiveSdk.initialize(config) from Application.onCreate() " +
|
|
237
|
+
"before recording events. Error: ${e.message}"
|
|
238
|
+
)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
182
241
|
|
|
183
242
|
if (debugHelper.isDebuggingEnabled()) {
|
|
184
243
|
val debugData = mutableMapOf<String, Any>()
|
|
185
244
|
debugData["items_count"] = itemsList.size.toString()
|
|
186
245
|
debugData["order_id"] = orderId
|
|
187
|
-
if (cartId
|
|
188
|
-
if (cartCoupon
|
|
246
|
+
if (!cartId.isNullOrEmpty()) debugData["cart_id"] = cartId!!
|
|
247
|
+
if (!cartCoupon.isNullOrEmpty()) debugData["cart_coupon"] = cartCoupon!!
|
|
248
|
+
debugData["all_items"] = itemsDebugData
|
|
249
|
+
itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
|
|
189
250
|
debugHelper.showDebugInfo("Purchase Event", debugData)
|
|
190
251
|
}
|
|
191
252
|
}
|
|
@@ -193,15 +254,30 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
193
254
|
override fun recordAddToCartEvent(items: ReadableArray, deeplink: String?) {
|
|
194
255
|
Log.i(TAG, "Sending add to cart event")
|
|
195
256
|
|
|
257
|
+
// Extract raw debug data before building items so all bridge fields are preserved
|
|
258
|
+
val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
|
|
259
|
+
|
|
196
260
|
val itemsList = buildItems(items)
|
|
197
|
-
val addToCartEvent = AddToCartEvent.Builder(itemsList).deeplink(deeplink).build()
|
|
261
|
+
val addToCartEvent = AddToCartEvent.Builder().items(itemsList).deeplink(deeplink).build()
|
|
198
262
|
|
|
199
|
-
|
|
263
|
+
try {
|
|
264
|
+
AttentiveSdk.recordEvent(addToCartEvent)
|
|
265
|
+
} catch (e: Exception) {
|
|
266
|
+
Log.e(
|
|
267
|
+
TAG,
|
|
268
|
+
"[AttentiveSDK] recordAddToCartEvent failed — SDK may not be initialized. " +
|
|
269
|
+
"On Android, call AttentiveSdk.initialize(config) from Application.onCreate() " +
|
|
270
|
+
"before recording events. Error: ${e.message}"
|
|
271
|
+
)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
200
274
|
|
|
201
275
|
if (debugHelper.isDebuggingEnabled()) {
|
|
202
276
|
val debugData = mutableMapOf<String, Any>()
|
|
203
277
|
debugData["items_count"] = itemsList.size.toString()
|
|
204
278
|
debugData["deeplink"] = deeplink ?: ""
|
|
279
|
+
debugData["all_items"] = itemsDebugData
|
|
280
|
+
itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
|
|
205
281
|
debugHelper.showDebugInfo("Add To Cart Event", debugData)
|
|
206
282
|
}
|
|
207
283
|
}
|
|
@@ -212,9 +288,19 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
212
288
|
throw IllegalArgumentException("The CustomEvent 'properties' field cannot be null.")
|
|
213
289
|
}
|
|
214
290
|
val propertiesMap = convertToStringMap(properties.toHashMap())
|
|
215
|
-
val customEvent = CustomEvent.Builder(type
|
|
291
|
+
val customEvent = CustomEvent.Builder().type(type).properties(propertiesMap).build()
|
|
216
292
|
|
|
217
|
-
|
|
293
|
+
try {
|
|
294
|
+
AttentiveSdk.recordEvent(customEvent)
|
|
295
|
+
} catch (e: Exception) {
|
|
296
|
+
Log.e(
|
|
297
|
+
TAG,
|
|
298
|
+
"[AttentiveSDK] recordCustomEvent failed — SDK may not be initialized. " +
|
|
299
|
+
"On Android, call AttentiveSdk.initialize(config) from Application.onCreate() " +
|
|
300
|
+
"before recording events. Error: ${e.message}"
|
|
301
|
+
)
|
|
302
|
+
return
|
|
303
|
+
}
|
|
218
304
|
|
|
219
305
|
if (debugHelper.isDebuggingEnabled()) {
|
|
220
306
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -243,37 +329,67 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
243
329
|
//
|
|
244
330
|
// These methods provide Android push notification support.
|
|
245
331
|
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
// but may require SDK upgrade or custom implementation for full functionality.
|
|
249
|
-
//
|
|
250
|
-
// The iOS implementation uses APNs; Android uses Firebase Cloud Messaging (FCM).
|
|
332
|
+
// Push: Uses AttentiveSdk.getPushTokenWithCallback (SDK 2.1.x) to fetch FCM token and register with Attentive.
|
|
333
|
+
// See: https://github.com/attentive-mobile/attentive-android-sdk/blob/main/README.md
|
|
251
334
|
// ==========================================================================
|
|
252
335
|
|
|
253
336
|
/**
|
|
254
|
-
* Request push notification permission
|
|
337
|
+
* Request push notification permission and fetch the FCM token via the Attentive SDK.
|
|
255
338
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* Uses [AttentivePushHelper] for the actual request.
|
|
339
|
+
* Uses [AttentiveSdk.getPushTokenWithCallback] so the SDK requests permission (when
|
|
340
|
+
* requestPermission = true) and registers the token with Attentive.
|
|
259
341
|
*/
|
|
260
342
|
override fun registerForPushNotifications() {
|
|
261
343
|
Log.i(TAG, "📱 [AttentiveSDK] registerForPushNotifications called (Android)")
|
|
262
344
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
345
|
+
val application = reactApplicationContext.applicationContext as? Application
|
|
346
|
+
if (application == null) {
|
|
347
|
+
Log.w(TAG, " Application context is null; cannot fetch push token.")
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
AttentiveSdk.getPushTokenWithCallback(application, true, createPushTokenCallback())
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private fun createPushTokenCallback(): AttentiveSdk.PushTokenCallback =
|
|
355
|
+
object : AttentiveSdk.PushTokenCallback {
|
|
356
|
+
override fun onSuccess(result: TokenFetchResult) {
|
|
357
|
+
UiThreadUtil.runOnUiThread {
|
|
358
|
+
val token = result.token
|
|
359
|
+
Log.i(TAG, "🎫 [AttentiveSDK] Push token fetched successfully (preview): ${token.take(16)}...")
|
|
360
|
+
|
|
361
|
+
// Emit the token to JS so the app can store it (e.g. for display in Settings)
|
|
362
|
+
// and mirror the iOS behavior where APNs delivers the token via a "register" event.
|
|
363
|
+
try {
|
|
364
|
+
reactApplicationContext
|
|
365
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
366
|
+
.emit("AttentiveDeviceToken", token)
|
|
367
|
+
Log.i(TAG, "📡 [AttentiveSDK] AttentiveDeviceToken event emitted to JS")
|
|
368
|
+
} catch (e: Exception) {
|
|
369
|
+
Log.w(TAG, "⚠️ [AttentiveSDK] Could not emit AttentiveDeviceToken event: ${e.message}")
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (debugHelper.isDebuggingEnabled()) {
|
|
373
|
+
val debugData = mutableMapOf<String, Any>()
|
|
374
|
+
debugData["platform"] = "Android"
|
|
375
|
+
debugData["token_preview"] = "${token.take(16)}..."
|
|
376
|
+
debugData["has_token"] = token.isNotEmpty()
|
|
377
|
+
debugData["permission_granted"] = result.permissionGranted
|
|
378
|
+
debugHelper.showDebugInfo("Push Token (AttentiveSdk)", debugData)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
268
381
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
382
|
+
override fun onFailure(exception: Exception) {
|
|
383
|
+
UiThreadUtil.runOnUiThread {
|
|
384
|
+
Log.e(TAG, "❌ [AttentiveSDK] getPushTokenWithCallback failed: ${exception.message}", exception)
|
|
385
|
+
if (debugHelper.isDebuggingEnabled()) {
|
|
386
|
+
val debugData = mutableMapOf<String, Any>()
|
|
387
|
+
debugData["error"] = exception.message ?: "Unknown error"
|
|
388
|
+
debugHelper.showDebugInfo("Push Token Error", debugData)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
274
391
|
}
|
|
275
392
|
}
|
|
276
|
-
}
|
|
277
393
|
|
|
278
394
|
/**
|
|
279
395
|
* Returns the current push notification authorization status for Android.
|
|
@@ -283,7 +399,10 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
283
399
|
*/
|
|
284
400
|
override fun getPushAuthorizationStatus(promise: Promise) {
|
|
285
401
|
try {
|
|
286
|
-
val status = AttentivePushHelper.getAuthorizationStatus(
|
|
402
|
+
val status = AttentivePushHelper.getAuthorizationStatus(
|
|
403
|
+
reactApplicationContext,
|
|
404
|
+
reactApplicationContext.currentActivity
|
|
405
|
+
)
|
|
287
406
|
Log.d(TAG, "getPushAuthorizationStatus: $status")
|
|
288
407
|
promise.resolve(status)
|
|
289
408
|
} catch (e: Exception) {
|
|
@@ -295,10 +414,13 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
295
414
|
/**
|
|
296
415
|
* Register the device token (FCM token) with the Attentive backend.
|
|
297
416
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
417
|
+
* The Attentive Android SDK does not expose an API that accepts a token string; it registers
|
|
418
|
+
* by fetching the FCM token from Firebase and sending it to Attentive. When the app passes
|
|
419
|
+
* its own FCM token (e.g. from Firebase Messaging in JS), that token matches the one the SDK
|
|
420
|
+
* will fetch. This method therefore triggers [AttentiveSdk.updatePushPermissionStatus], which
|
|
421
|
+
* fetches the current FCM token and registers it with Attentive, so push targeting works.
|
|
300
422
|
*
|
|
301
|
-
* @param token The FCM registration token from Firebase
|
|
423
|
+
* @param token The FCM registration token from Firebase (used for logging; registration uses SDK fetch)
|
|
302
424
|
* @param authorizationStatus Push authorization status (used for consistency with iOS)
|
|
303
425
|
*/
|
|
304
426
|
override fun registerDeviceToken(token: String, authorizationStatus: String) {
|
|
@@ -308,22 +430,22 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
308
430
|
Log.i(TAG, " Authorization status: $authorizationStatus")
|
|
309
431
|
|
|
310
432
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
433
|
+
val application = reactApplicationContext.applicationContext
|
|
434
|
+
if (application == null) {
|
|
435
|
+
Log.w(TAG, " Application context is null; cannot register push token with Attentive.")
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
// Forward registration to the Attentive SDK. It will fetch the FCM token (same as app-provided
|
|
439
|
+
// token when the app gets it from Firebase) and register it with the Attentive backend.
|
|
440
|
+
AttentiveSdk.updatePushPermissionStatus(application)
|
|
441
|
+
Log.i(TAG, " Attentive SDK updatePushPermissionStatus invoked (token will be fetched and registered)")
|
|
319
442
|
|
|
320
443
|
if (debugHelper.isDebuggingEnabled()) {
|
|
321
444
|
val debugData = mutableMapOf<String, Any>()
|
|
322
445
|
debugData["token_preview"] = "${token.take(16)}..."
|
|
323
446
|
debugData["token_length"] = token.length.toString()
|
|
324
447
|
debugData["authorization_status"] = authorizationStatus
|
|
325
|
-
debugData["
|
|
326
|
-
debugData["implementation_status"] = "manual_required"
|
|
448
|
+
debugData["registration_triggered"] = true
|
|
327
449
|
debugHelper.showDebugInfo("Device Token (Android)", debugData)
|
|
328
450
|
}
|
|
329
451
|
} catch (e: Exception) {
|
|
@@ -339,15 +461,17 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
339
461
|
}
|
|
340
462
|
|
|
341
463
|
/**
|
|
342
|
-
* Register the device token with callback for
|
|
464
|
+
* Register the device token with callback for flow consistency with iOS.
|
|
343
465
|
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
466
|
+
* Triggers the same registration as [registerDeviceToken] via [AttentiveSdk.updatePushPermissionStatus].
|
|
467
|
+
* The Attentive Android SDK does not expose a registration API with a completion callback, so this
|
|
468
|
+
* callback is invoked after registration has been triggered (token is fetched by the SDK and sent
|
|
469
|
+
* to Attentive). Success here means the registration request was triggered, not that the backend
|
|
470
|
+
* responded successfully.
|
|
347
471
|
*
|
|
348
472
|
* @param token The FCM registration token
|
|
349
473
|
* @param authorizationStatus Push authorization status
|
|
350
|
-
* @param callback Callback invoked after registration
|
|
474
|
+
* @param callback Callback invoked after registration has been triggered
|
|
351
475
|
*/
|
|
352
476
|
override fun registerDeviceTokenWithCallback(
|
|
353
477
|
token: String,
|
|
@@ -359,19 +483,18 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
359
483
|
Log.i(TAG, " Authorization status: $authorizationStatus")
|
|
360
484
|
|
|
361
485
|
try {
|
|
362
|
-
//
|
|
486
|
+
// Trigger real registration (SDK fetches FCM token and registers with Attentive)
|
|
363
487
|
registerDeviceToken(token, authorizationStatus)
|
|
364
488
|
|
|
365
|
-
//
|
|
489
|
+
// Callback is invoked after triggering registration; Android SDK does not provide backend result
|
|
366
490
|
val responseData = mapOf(
|
|
367
491
|
"success" to true,
|
|
368
492
|
"token" to "${token.take(16)}...",
|
|
369
493
|
"platform" to "Android",
|
|
370
|
-
"sdk_version" to "1.
|
|
371
|
-
"
|
|
494
|
+
"sdk_version" to "2.1.1",
|
|
495
|
+
"registration_triggered" to true
|
|
372
496
|
)
|
|
373
497
|
|
|
374
|
-
// Invoke callback with: data, url, response, error
|
|
375
498
|
callback.invoke(
|
|
376
499
|
responseData, // data
|
|
377
500
|
null, // url (not available in Android SDK)
|
|
@@ -379,7 +502,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
379
502
|
null // error
|
|
380
503
|
)
|
|
381
504
|
|
|
382
|
-
Log.i(TAG, "📥 [AttentiveSDK] Callback invoked
|
|
505
|
+
Log.i(TAG, "📥 [AttentiveSDK] Callback invoked after registration triggered")
|
|
383
506
|
|
|
384
507
|
if (debugHelper.isDebuggingEnabled()) {
|
|
385
508
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -416,62 +539,63 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
416
539
|
*
|
|
417
540
|
* @param authorizationStatus Current push authorization status
|
|
418
541
|
*/
|
|
419
|
-
override fun handleRegularOpen(authorizationStatus: String) {
|
|
542
|
+
override fun handleRegularOpen(authorizationStatus: String) { // Meant to be NOOP
|
|
420
543
|
Log.i(TAG, "🌉 [AttentiveSDK] handleRegularOpen called (Android)")
|
|
421
544
|
Log.i(TAG, " Authorization status: $authorizationStatus")
|
|
422
|
-
Log.i(TAG, " Tracking regular app open event...")
|
|
423
|
-
|
|
424
|
-
try {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
545
|
+
// Log.i(TAG, " Tracking regular app open event...")
|
|
546
|
+
|
|
547
|
+
// try {
|
|
548
|
+
// // Attentive Android SDK 1.0.1 doesn't have a built-in handleRegularOpen method
|
|
549
|
+
// // Track app open as custom event
|
|
550
|
+
|
|
551
|
+
// // Option 1: Track as custom event
|
|
552
|
+
|
|
553
|
+
// Log.i(TAG, " Tracking regular open as custom event 'app_open' with properties")
|
|
554
|
+
|
|
555
|
+
// val properties = mapOf(
|
|
556
|
+
// "event_type" to "app_open",
|
|
557
|
+
// "authorization_status" to authorizationStatus,
|
|
558
|
+
// "platform" to "Android"
|
|
559
|
+
// )
|
|
560
|
+
|
|
561
|
+
// try {
|
|
562
|
+
// Log.i(TAG, " Attempting to track custom event for regular app open")
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
// val customEvent = CustomEvent.Builder()
|
|
566
|
+
// .type("app_open")
|
|
567
|
+
// .properties(properties)
|
|
568
|
+
// .build()
|
|
569
|
+
|
|
570
|
+
// Log.i(TAG, " Custom event built successfully, recording event...")
|
|
571
|
+
|
|
572
|
+
// AttentiveSdk.recordEvent(customEvent)
|
|
573
|
+
|
|
574
|
+
// Log.i(TAG, "✅ [AttentiveSDK] handleRegularOpen completed (tracked as custom event)")
|
|
575
|
+
// Log.i(TAG, " Event sent to Attentive backend")
|
|
576
|
+
// } catch (e: Exception) {
|
|
577
|
+
// Log.w(TAG, "⚠️ [AttentiveSDK] Could not track app open as custom event: ${e.message}")
|
|
578
|
+
// Log.i(TAG, " App open tracking requires manual implementation or SDK upgrade")
|
|
579
|
+
// }
|
|
580
|
+
|
|
581
|
+
// if (debugHelper.isDebuggingEnabled()) {
|
|
582
|
+
// val debugData = mutableMapOf<String, Any>()
|
|
583
|
+
// debugData["authorization_status"] = authorizationStatus
|
|
584
|
+
// debugData["event_type"] = "regular_open"
|
|
585
|
+
// debugData["platform"] = "Android"
|
|
586
|
+
// debugData["sdk_version"] = "2.1.1"
|
|
587
|
+
// debugHelper.showDebugInfo("Regular Open Event", debugData)
|
|
588
|
+
// }
|
|
589
|
+
// } catch (e: Exception) {
|
|
590
|
+
// Log.e(TAG, "❌ [AttentiveSDK] Error in handleRegularOpen: ${e.message}", e)
|
|
591
|
+
|
|
592
|
+
// if (debugHelper.isDebuggingEnabled()) {
|
|
593
|
+
// val debugData = mutableMapOf<String, Any>()
|
|
594
|
+
// debugData["error"] = e.message ?: "Unknown error"
|
|
595
|
+
// debugData["error_type"] = e.javaClass.simpleName
|
|
596
|
+
// debugHelper.showDebugInfo("Regular Open Error", debugData)
|
|
597
|
+
// }
|
|
598
|
+
// }
|
|
475
599
|
}
|
|
476
600
|
|
|
477
601
|
/**
|
|
@@ -505,12 +629,12 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
505
629
|
}
|
|
506
630
|
|
|
507
631
|
try {
|
|
508
|
-
val customEvent =
|
|
509
|
-
"push_open"
|
|
510
|
-
properties
|
|
511
|
-
|
|
632
|
+
val customEvent = CustomEvent.Builder()
|
|
633
|
+
.type("push_open")
|
|
634
|
+
.properties(properties)
|
|
635
|
+
.build()
|
|
512
636
|
|
|
513
|
-
|
|
637
|
+
AttentiveSdk.recordEvent(customEvent)
|
|
514
638
|
|
|
515
639
|
Log.i(TAG, "✅ [AttentiveSDK] handlePushOpen completed (tracked as custom event)")
|
|
516
640
|
Log.i(TAG, " Push open event sent to Attentive backend")
|
|
@@ -525,7 +649,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
525
649
|
debugData["event_type"] = "push_open"
|
|
526
650
|
debugData["platform"] = "Android"
|
|
527
651
|
debugData["payload_keys"] = payload.keys.joinToString(", ")
|
|
528
|
-
debugData["sdk_version"] = "1.
|
|
652
|
+
debugData["sdk_version"] = "2.1.1"
|
|
529
653
|
debugHelper.showDebugInfo("Push Open Event", debugData)
|
|
530
654
|
}
|
|
531
655
|
} catch (e: Exception) {
|
|
@@ -571,12 +695,12 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
571
695
|
}
|
|
572
696
|
|
|
573
697
|
try {
|
|
574
|
-
val customEvent =
|
|
575
|
-
"foreground_push"
|
|
576
|
-
properties
|
|
577
|
-
|
|
698
|
+
val customEvent = CustomEvent.Builder()
|
|
699
|
+
.type("foreground_push")
|
|
700
|
+
.properties(properties)
|
|
701
|
+
.build()
|
|
578
702
|
|
|
579
|
-
|
|
703
|
+
AttentiveSdk.recordEvent(customEvent)
|
|
580
704
|
|
|
581
705
|
Log.i(TAG, "✅ [AttentiveSDK] handleForegroundPush completed (tracked as custom event)")
|
|
582
706
|
Log.i(TAG, " Foreground push event sent to Attentive backend")
|
|
@@ -591,7 +715,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
591
715
|
debugData["event_type"] = "foreground_push"
|
|
592
716
|
debugData["platform"] = "Android"
|
|
593
717
|
debugData["payload_keys"] = payload.keys.joinToString(", ")
|
|
594
|
-
debugData["sdk_version"] = "1.
|
|
718
|
+
debugData["sdk_version"] = "2.1.1"
|
|
595
719
|
debugHelper.showDebugInfo("Foreground Push Event", debugData)
|
|
596
720
|
}
|
|
597
721
|
} catch (e: Exception) {
|
|
@@ -659,6 +783,89 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
659
783
|
handleForegroundPush(userInfo, "authorized")
|
|
660
784
|
}
|
|
661
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Returns the push notification payload that launched the app from a killed state
|
|
788
|
+
* (i.e. the user tapped a notification while the app was not running) and clears it
|
|
789
|
+
* so it is only delivered once.
|
|
790
|
+
*
|
|
791
|
+
* The host app's `MainActivity.onCreate` is responsible for storing the payload in
|
|
792
|
+
* [AttentiveNotificationStore] when a push-tap Intent is detected before the React
|
|
793
|
+
* Native bridge is ready.
|
|
794
|
+
*
|
|
795
|
+
* Returns a [Promise] that resolves to a `ReadableMap` containing the notification data,
|
|
796
|
+
* or `null` if the app was not launched from a push notification tap.
|
|
797
|
+
*
|
|
798
|
+
* @param promise Promise to resolve with the notification payload map or null.
|
|
799
|
+
*/
|
|
800
|
+
override fun getInitialPushNotification(promise: Promise) {
|
|
801
|
+
Log.d(TAG, "getInitialPushNotification called!")
|
|
802
|
+
|
|
803
|
+
// try {
|
|
804
|
+
// val payload = AttentiveNotificationStore.getAndClear()
|
|
805
|
+
// if (payload == null) {
|
|
806
|
+
// Log.d(TAG, "getInitialPushNotification: no pending initial notification")
|
|
807
|
+
// promise.resolve(null)
|
|
808
|
+
// return
|
|
809
|
+
// }
|
|
810
|
+
//
|
|
811
|
+
// Log.i(TAG, "getInitialPushNotification: returning stored notification with keys=${payload.keys}")
|
|
812
|
+
// val result = Arguments.createMap()
|
|
813
|
+
// payload.forEach { (key, value) -> result.putString(key, value) }
|
|
814
|
+
// promise.resolve(result)
|
|
815
|
+
// } catch (e: Exception) {
|
|
816
|
+
// Log.e(TAG, "getInitialPushNotification: error — ${e.message}", e)
|
|
817
|
+
// promise.reject("INITIAL_PUSH_ERROR", "Failed to retrieve initial push notification: ${e.message}", e)
|
|
818
|
+
// }
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ==========================================================================
|
|
822
|
+
// MARK: - NativeEventEmitter support
|
|
823
|
+
// ==========================================================================
|
|
824
|
+
// These stubs are required by React Native's NativeEventEmitter on the old
|
|
825
|
+
// architecture. Without them the bridge logs a warning about missing methods.
|
|
826
|
+
|
|
827
|
+
@ReactMethod
|
|
828
|
+
fun addListener(eventName: String) {
|
|
829
|
+
// No-op: listener bookkeeping is handled by the JS NativeEventEmitter.
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
@ReactMethod
|
|
833
|
+
fun removeListeners(count: Double) {
|
|
834
|
+
// No-op: listener bookkeeping is handled by the JS NativeEventEmitter.
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Extracts a human-readable list of item maps from the raw bridge array for debug display.
|
|
839
|
+
*
|
|
840
|
+
* This intentionally reads all fields from the original [ReadableArray] rather than from
|
|
841
|
+
* the built [Item] objects so that every field sent from TypeScript (including optional ones)
|
|
842
|
+
* is visible in the debugger — even fields that may be skipped during SDK item construction.
|
|
843
|
+
*
|
|
844
|
+
* @param rawItems The raw item array as received from the React Native bridge.
|
|
845
|
+
* @return A list of maps, one per item, containing all present fields.
|
|
846
|
+
*/
|
|
847
|
+
private fun extractItemsDebugData(rawItems: ReadableArray): List<Map<String, Any>> {
|
|
848
|
+
val result = mutableListOf<Map<String, Any>>()
|
|
849
|
+
for (i in 0 until rawItems.size()) {
|
|
850
|
+
val rawItem = rawItems.getMap(i) ?: continue
|
|
851
|
+
val itemData = mutableMapOf<String, Any>()
|
|
852
|
+
|
|
853
|
+
rawItem.getString("productId")?.let { itemData["productId"] = it }
|
|
854
|
+
rawItem.getString("productVariantId")?.let { itemData["productVariantId"] = it }
|
|
855
|
+
rawItem.getString("price")?.let { itemData["price"] = it }
|
|
856
|
+
rawItem.getString("currency")?.let { itemData["currency"] = it }
|
|
857
|
+
rawItem.getString("name")?.let { itemData["name"] = it }
|
|
858
|
+
rawItem.getString("productImage")?.let { itemData["productImage"] = it }
|
|
859
|
+
rawItem.getString("category")?.let { itemData["category"] = it }
|
|
860
|
+
if (rawItem.hasKey("quantity")) {
|
|
861
|
+
itemData["quantity"] = rawItem.getDouble("quantity").toInt()
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
result.add(itemData)
|
|
865
|
+
}
|
|
866
|
+
return result
|
|
867
|
+
}
|
|
868
|
+
|
|
662
869
|
private fun convertToStringMap(inputMap: Map<String, Any?>): Map<String, String> {
|
|
663
870
|
val outputMap = mutableMapOf<String, String>()
|
|
664
871
|
for ((key, value) in inputMap) {
|
|
@@ -675,15 +882,38 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
675
882
|
private fun buildItems(rawItems: ReadableArray): List<Item> {
|
|
676
883
|
Log.i(TAG, "buildItems method called with rawItems: $rawItems")
|
|
677
884
|
val items = mutableListOf<Item>()
|
|
885
|
+
|
|
678
886
|
for (i in 0 until rawItems.size()) {
|
|
679
887
|
val rawItem = rawItems.getMap(i) ?: continue
|
|
680
888
|
|
|
681
|
-
//
|
|
682
|
-
val priceValue = rawItem.getString("price")
|
|
683
|
-
val currencyCode = rawItem.getString("currency")
|
|
684
|
-
val
|
|
889
|
+
// Required scalar fields
|
|
890
|
+
val priceValue = rawItem.getString("price") ?: continue
|
|
891
|
+
val currencyCode = rawItem.getString("currency") ?: continue
|
|
892
|
+
val productId = rawItem.getString("productId") ?: continue
|
|
893
|
+
val productVariantId = rawItem.getString("productVariantId") ?: continue
|
|
894
|
+
|
|
895
|
+
// Parse price amount — skip item on malformed value rather than crash
|
|
896
|
+
val priceDecimal = try {
|
|
897
|
+
BigDecimal(priceValue)
|
|
898
|
+
} catch (e: NumberFormatException) {
|
|
899
|
+
Log.w(TAG, "buildItems: invalid price value '$priceValue' at index $i — skipping item")
|
|
900
|
+
continue
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Parse currency — skip item on unrecognised ISO 4217 code rather than crash
|
|
904
|
+
val currency = try {
|
|
905
|
+
Currency.getInstance(currencyCode)
|
|
906
|
+
} catch (e: IllegalArgumentException) {
|
|
907
|
+
Log.w(TAG, "buildItems: invalid currency code '$currencyCode' at index $i — skipping item")
|
|
908
|
+
continue
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
val price = Price.Builder()
|
|
912
|
+
.price(priceDecimal)
|
|
913
|
+
.currency(currency)
|
|
914
|
+
.build()
|
|
685
915
|
|
|
686
|
-
val builder = Item.Builder(
|
|
916
|
+
val builder = Item.Builder(productId, productVariantId, price)
|
|
687
917
|
|
|
688
918
|
if (rawItem.hasKey("productImage")) {
|
|
689
919
|
builder.productImage(rawItem.getString("productImage"))
|
|
@@ -693,16 +923,16 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
693
923
|
builder.name(rawItem.getString("name"))
|
|
694
924
|
}
|
|
695
925
|
|
|
926
|
+
// JS numbers are doubles on the bridge; use getDouble().toInt() to avoid ClassCastException
|
|
696
927
|
if (rawItem.hasKey("quantity")) {
|
|
697
|
-
builder.quantity(rawItem.
|
|
928
|
+
builder.quantity(rawItem.getDouble("quantity").toInt())
|
|
698
929
|
}
|
|
699
930
|
|
|
700
931
|
if (rawItem.hasKey("category")) {
|
|
701
932
|
builder.category(rawItem.getString("category"))
|
|
702
933
|
}
|
|
703
934
|
|
|
704
|
-
|
|
705
|
-
items.add(item)
|
|
935
|
+
items.add(builder.build())
|
|
706
936
|
}
|
|
707
937
|
|
|
708
938
|
return items
|