@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/README.md
CHANGED
|
@@ -68,12 +68,59 @@ See [DEBUGGING.md](./DEBUGGING.md) for detailed information about debugging feat
|
|
|
68
68
|
|
|
69
69
|
### Initialize the SDK
|
|
70
70
|
|
|
71
|
+
> **Platform difference:** iOS and Android have different initialization requirements.
|
|
72
|
+
|
|
73
|
+
#### iOS — Initialize from TypeScript
|
|
74
|
+
|
|
75
|
+
On iOS, call `initialize` from TypeScript as early as possible (e.g. the root `App` component's `useEffect`):
|
|
76
|
+
|
|
71
77
|
```typescript
|
|
72
|
-
//
|
|
73
|
-
// Note: 'initialize' should only be called once per app session - if you call it multiple times it will throw an exception
|
|
78
|
+
// Called once per app session, before any other SDK operations.
|
|
74
79
|
Attentive.initialize(config);
|
|
75
80
|
```
|
|
76
81
|
|
|
82
|
+
#### Android — Initialize from Native Code
|
|
83
|
+
|
|
84
|
+
On Android, `AttentiveSdk.initialize()` **must** be called from your `Application.onCreate()` in native Kotlin/Java code. This is required so that lifecycle observers (e.g. `AppLaunchTracker`) are registered before the React Native bridge is ready. Calling `initialize()` from TypeScript on Android is a **no-op** — the SDK will not be started and all subsequent event calls will fail.
|
|
85
|
+
|
|
86
|
+
Add the following to your `MainApplication.kt` (or `MainApplication.java`):
|
|
87
|
+
|
|
88
|
+
```kotlin
|
|
89
|
+
import android.app.Application
|
|
90
|
+
import com.attentive.androidsdk.AttentiveConfig
|
|
91
|
+
import com.attentive.androidsdk.AttentiveSdk
|
|
92
|
+
import com.attentive.androidsdk.AttentiveLogLevel
|
|
93
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
94
|
+
|
|
95
|
+
class MainApplication : Application(), ReactApplication {
|
|
96
|
+
|
|
97
|
+
override fun onCreate() {
|
|
98
|
+
super.onCreate()
|
|
99
|
+
// ... your existing setup ...
|
|
100
|
+
initAttentiveSDK()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun initAttentiveSDK() {
|
|
104
|
+
val config = AttentiveConfig.Builder()
|
|
105
|
+
.applicationContext(this)
|
|
106
|
+
.domain("YOUR_ATTENTIVE_DOMAIN")
|
|
107
|
+
.mode(AttentiveConfig.Mode.PRODUCTION) // or Mode.DEBUG for testing
|
|
108
|
+
.skipFatigueOnCreatives(false)
|
|
109
|
+
.logLevel(AttentiveLogLevel.VERBOSE)
|
|
110
|
+
.build()
|
|
111
|
+
|
|
112
|
+
// AttentiveSdk.initialize registers lifecycle observers and must run on the main thread.
|
|
113
|
+
UiThreadUtil.runOnUiThread {
|
|
114
|
+
AttentiveSdk.initialize(config)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
After the native initialization, all other SDK operations (`identify`, `recordAddToCartEvent`, `recordPurchaseEvent`, etc.) are called from TypeScript as normal on both platforms. The TypeScript `initialize()` call is still required on iOS but is safely ignored on Android.
|
|
121
|
+
|
|
122
|
+
> **Tip:** If you see `[AttentiveSDK] recordAddToCartEvent failed — SDK may not be initialized` in your Android logcat, it means `AttentiveSdk.initialize()` was not called from native code before the event was recorded. Check your `Application.onCreate()` setup.
|
|
123
|
+
|
|
77
124
|
### Destroy the creative
|
|
78
125
|
|
|
79
126
|
```typescript
|
|
@@ -173,6 +220,25 @@ Attentive.identify({phone: '+15556667777'};)
|
|
|
173
220
|
|
|
174
221
|
The SDK supports push notification integration on both iOS (APNs) and Android (runtime permission + optional FCM). The following sections cover iOS-specific flows and a full **App events on Android** implementation that mirrors the behavior of the [Bonni](https://github.com/attentive-mobile/attentive-react-native-sdk/tree/main/Bonni) example app.
|
|
175
222
|
|
|
223
|
+
> **iOS — required setup:** Your AppDelegate **must** forward notification
|
|
224
|
+
> responses to the SDK for push tracking to work. Add this single line to your
|
|
225
|
+
> `userNotificationCenter(_:didReceive:withCompletionHandler:)`:
|
|
226
|
+
>
|
|
227
|
+
> ```swift
|
|
228
|
+
> AttentiveSDKManager.shared.handleNotificationResponse(response)
|
|
229
|
+
> ```
|
|
230
|
+
>
|
|
231
|
+
> Without this, push open and foreground push events **will not be tracked** on
|
|
232
|
+
> iOS. See [iOS AppDelegate Integration](#ios-appdelegate-integration) for full
|
|
233
|
+
> details.
|
|
234
|
+
>
|
|
235
|
+
> **Migrating from an earlier version?** If you previously called
|
|
236
|
+
> `AttentiveSDKManager.shared.handleForegroundPush(response:authorizationStatus:)`
|
|
237
|
+
> or `AttentiveSDKManager.shared.handlePushOpen(response:authorizationStatus:)`
|
|
238
|
+
> directly from your AppDelegate, **replace** that code with the single
|
|
239
|
+
> `handleNotificationResponse` call above. Using both will result in
|
|
240
|
+
> double-tracked events. The old methods are now deprecated.
|
|
241
|
+
|
|
176
242
|
---
|
|
177
243
|
|
|
178
244
|
### App Events on Android
|
|
@@ -205,7 +271,7 @@ This section describes how to implement Attentive app events on Android so they
|
|
|
205
271
|
</manifest>
|
|
206
272
|
```
|
|
207
273
|
|
|
208
|
-
2. **Initialize and identify first** –
|
|
274
|
+
2. **Initialize and identify first** – The SDK must be initialized natively from `Application.onCreate()` on Android (see [Android Native Initialization](#android--initialize-from-native-code) above). Then, in your app entry (e.g. root component `useEffect`), call `identify(identifiers)` before any push or app-event logic.
|
|
209
275
|
|
|
210
276
|
#### 1. On app launch (Android)
|
|
211
277
|
|
|
@@ -228,7 +294,13 @@ import {
|
|
|
228
294
|
} from 'attentive-react-native-sdk';
|
|
229
295
|
|
|
230
296
|
// Inside your root component (e.g. App.tsx useEffect):
|
|
231
|
-
|
|
297
|
+
|
|
298
|
+
// iOS only: initialize from TypeScript.
|
|
299
|
+
// Android: initialization must be done natively from Application.onCreate() — see README.
|
|
300
|
+
if (Platform.OS === 'ios') {
|
|
301
|
+
initialize(config);
|
|
302
|
+
}
|
|
303
|
+
|
|
232
304
|
identify({ email: 'user@example.com', clientUserId: 'id-123' });
|
|
233
305
|
|
|
234
306
|
if (Platform.OS === 'android') {
|
|
@@ -270,12 +342,13 @@ return () => subscription.remove();
|
|
|
270
342
|
|
|
271
343
|
#### 3. Optional: Register FCM token (Android)
|
|
272
344
|
|
|
273
|
-
|
|
345
|
+
**Recommended:** This React Native SDK’s Android native module depends on Attentive Android SDK **2.1.1**, which exposes `AttentiveSdk.getPushTokenWithCallback`. Calling `registerForPushNotifications()` from JS triggers that API: the SDK requests permission (when needed), fetches the FCM token, and registers it with Attentive. No separate native code is required.
|
|
346
|
+
|
|
347
|
+
**Alternative (token from JS):** If you obtain the FCM token elsewhere (e.g. Firebase Messaging), use `registerDeviceTokenWithCallback` and then call `handleRegularOpen` in the callback:
|
|
274
348
|
|
|
275
349
|
```typescript
|
|
276
350
|
import { registerDeviceTokenWithCallback, handleRegularOpen } from 'attentive-react-native-sdk';
|
|
277
351
|
|
|
278
|
-
// When you receive the FCM token (e.g. from Firebase Messaging):
|
|
279
352
|
getPushAuthorizationStatus().then((authStatus) => {
|
|
280
353
|
registerDeviceTokenWithCallback(
|
|
281
354
|
fcmToken,
|
|
@@ -303,7 +376,7 @@ Get `authorizationStatus` via `getPushAuthorizationStatus()` when handling the e
|
|
|
303
376
|
|
|
304
377
|
The [Bonni](https://github.com/attentive-mobile/attentive-react-native-sdk/tree/main/Bonni) example app ([App.tsx](https://github.com/attentive-mobile/attentive-react-native-sdk/blob/main/Bonni/App.tsx)) implements the full flow:
|
|
305
378
|
|
|
306
|
-
1. **Launch:** `initialize` → `identify` →
|
|
379
|
+
1. **Launch:** Native `AttentiveSdk.initialize(config)` (from `Application.onCreate()`) → TypeScript `identify` → `getPushAuthorizationStatus()` → `handleRegularOpen(authStatus)` → `registerForPushNotifications()`.
|
|
307
380
|
2. **Foreground:** `AppState.addEventListener('change', …)` → when `active` and Android → `getPushAuthorizationStatus()` → `handleRegularOpen(authStatus)`.
|
|
308
381
|
3. **Optional:** When FCM token is available → `registerDeviceTokenWithCallback(token, authStatus, callback)` → in callback call `handleRegularOpen(authStatus)`.
|
|
309
382
|
4. **Optional:** When user opens a notification or receives one in foreground → `handlePushOpen` / `handleForegroundPush` with payload and status from `getPushAuthorizationStatus()`.
|
|
@@ -372,7 +445,40 @@ For proper push notification integration, your iOS AppDelegate needs to:
|
|
|
372
445
|
|
|
373
446
|
1. Request notification permissions via the SDK
|
|
374
447
|
2. Implement `application:didRegisterForRemoteNotificationsWithDeviceToken:` to register the token
|
|
375
|
-
3.
|
|
448
|
+
3. **Forward notification responses to the SDK for push-open tracking**
|
|
449
|
+
|
|
450
|
+
##### Push Open Tracking (Required)
|
|
451
|
+
|
|
452
|
+
Add **one line** to your AppDelegate's `didReceive` handler so the SDK can track
|
|
453
|
+
push opens and foreground push events. Without this, `handlePushOpen()` and
|
|
454
|
+
`handleForegroundPush()` called from JavaScript will not be able to track events
|
|
455
|
+
on iOS (the native SDK requires a `UNNotificationResponse` which cannot cross the
|
|
456
|
+
React Native bridge).
|
|
457
|
+
|
|
458
|
+
```swift
|
|
459
|
+
// In AppDelegate.swift — UNUserNotificationCenterDelegate
|
|
460
|
+
func userNotificationCenter(
|
|
461
|
+
_ center: UNUserNotificationCenter,
|
|
462
|
+
didReceive response: UNNotificationResponse,
|
|
463
|
+
withCompletionHandler completionHandler: @escaping () -> Void
|
|
464
|
+
) {
|
|
465
|
+
// Attentive push tracking (handles app-state + auth status automatically)
|
|
466
|
+
AttentiveSDKManager.shared.handleNotificationResponse(response)
|
|
467
|
+
|
|
468
|
+
// Forward to your push library (e.g. RNCPushNotificationIOS) for JS events
|
|
469
|
+
RNCPushNotificationIOS.didReceive(response)
|
|
470
|
+
completionHandler()
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
`handleNotificationResponse` automatically:
|
|
475
|
+
- Detects whether the app is in the foreground or background
|
|
476
|
+
- Fetches the current authorization status
|
|
477
|
+
- Calls the correct native SDK method (`handlePushOpen` or `handleForegroundPush`)
|
|
478
|
+
- Caches the response so the JS-side `handlePushOpen()` / `handleForegroundPush()` calls
|
|
479
|
+
are fulfilled without double-tracking
|
|
480
|
+
- **Cold-launch safe:** If the user taps a push while the app is killed, the
|
|
481
|
+
response is cached and automatically tracked once the SDK initializes
|
|
376
482
|
|
|
377
483
|
##### Callback-Based Registration (Recommended)
|
|
378
484
|
|
|
@@ -389,13 +495,13 @@ func application(
|
|
|
389
495
|
) {
|
|
390
496
|
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
|
391
497
|
let authStatus = settings.authorizationStatus
|
|
392
|
-
|
|
498
|
+
|
|
393
499
|
// Get SDK instance with proper type
|
|
394
500
|
guard let attentiveSdk = AttentiveSDKManager.shared.sdk as? ATTNNativeSDK else {
|
|
395
501
|
print("[Attentive] SDK not initialized")
|
|
396
502
|
return
|
|
397
503
|
}
|
|
398
|
-
|
|
504
|
+
|
|
399
505
|
// Register device token with callback
|
|
400
506
|
attentiveSdk.registerDeviceToken(
|
|
401
507
|
deviceToken,
|
|
@@ -406,7 +512,7 @@ func application(
|
|
|
406
512
|
if let error = error {
|
|
407
513
|
print("[Attentive] Registration failed: \(error.localizedDescription)")
|
|
408
514
|
}
|
|
409
|
-
|
|
515
|
+
|
|
410
516
|
// Trigger regular open event after registration
|
|
411
517
|
attentiveSdk.handleRegularOpen(authorizationStatus: authStatus)
|
|
412
518
|
}
|
package/android/build.gradle
CHANGED
|
@@ -82,7 +82,10 @@ dependencies {
|
|
|
82
82
|
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
|
|
83
83
|
//noinspection GradleDynamicVersion
|
|
84
84
|
implementation "com.facebook.react:react-native:+"
|
|
85
|
-
|
|
85
|
+
// Use `api` so that the Attentive Android SDK types (AttentiveSdk, CustomEvent, etc.)
|
|
86
|
+
// are visible to app-level code (e.g. Bonni's AttentiveFirebaseMessagingService) that
|
|
87
|
+
// depends on this library and needs to call the SDK directly from native components.
|
|
88
|
+
api 'com.attentive:attentive-android-sdk:2.1.3'
|
|
86
89
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.10"
|
|
87
90
|
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.9.10"))
|
|
88
91
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package com.attentivereactnativesdk
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-process store for a pending initial push notification payload.
|
|
5
|
+
*
|
|
6
|
+
* When the user taps an FCM notification while the app is in the killed state,
|
|
7
|
+
* Android launches the app's main activity before the React Native bridge is
|
|
8
|
+
* initialised. The notification payload is written here by the host app's
|
|
9
|
+
* `MainActivity` and consumed exactly once by [AttentiveReactNativeSdkModule.getInitialPushNotification]
|
|
10
|
+
* after the JS layer is ready.
|
|
11
|
+
*
|
|
12
|
+
* ## Usage pattern
|
|
13
|
+
*
|
|
14
|
+
* ```kotlin
|
|
15
|
+
* // In host app's MainActivity.onCreate:
|
|
16
|
+
* intent?.extras?.let { extras ->
|
|
17
|
+
* val payload = // … extract FCM data …
|
|
18
|
+
* AttentiveNotificationStore.setPendingInitialNotification(payload)
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // In JS (App.tsx), after initialize():
|
|
24
|
+
* const initial = await getInitialPushNotification()
|
|
25
|
+
* if (initial) handlePushOpen(initial, authStatus)
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Thread-safety: the two public methods are `@Synchronized` so concurrent access
|
|
29
|
+
* from the main UI thread (writer) and the JS bridge thread (reader) is safe.
|
|
30
|
+
*/
|
|
31
|
+
object AttentiveNotificationStore {
|
|
32
|
+
|
|
33
|
+
@Volatile
|
|
34
|
+
private var pendingInitialNotification: Map<String, String>? = null
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stores [payload] as the pending initial push notification.
|
|
38
|
+
*
|
|
39
|
+
* Replaces any previously stored value (only one initial notification is tracked at a time).
|
|
40
|
+
*
|
|
41
|
+
* @param payload A map of string key-value pairs representing the notification data.
|
|
42
|
+
*/
|
|
43
|
+
@Synchronized
|
|
44
|
+
fun setPendingInitialNotification(payload: Map<String, String>) {
|
|
45
|
+
pendingInitialNotification = payload
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the stored initial push notification payload and clears it atomically,
|
|
50
|
+
* ensuring the payload is delivered to the JS layer exactly once.
|
|
51
|
+
*
|
|
52
|
+
* @return The stored payload map, or `null` if no initial notification is pending.
|
|
53
|
+
*/
|
|
54
|
+
@Synchronized
|
|
55
|
+
fun getAndClear(): Map<String, String>? {
|
|
56
|
+
val pending = pendingInitialNotification
|
|
57
|
+
pendingInitialNotification = null
|
|
58
|
+
return pending
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -26,8 +26,8 @@ object AttentivePushHelper {
|
|
|
26
26
|
/**
|
|
27
27
|
* Authorization status values aligned with iOS push authorization for use in handleRegularOpen etc.
|
|
28
28
|
* - "authorized" – user has granted notification permission (or API < 33)
|
|
29
|
-
* - "denied" – user denied
|
|
30
|
-
* - "notDetermined" – not yet requested (API 33+ only)
|
|
29
|
+
* - "denied" – user was asked and denied (API 33+, when determinable via activity)
|
|
30
|
+
* - "notDetermined" – not yet requested, or unable to distinguish (API 33+ only)
|
|
31
31
|
*/
|
|
32
32
|
const val STATUS_AUTHORIZED = "authorized"
|
|
33
33
|
const val STATUS_DENIED = "denied"
|
|
@@ -36,14 +36,18 @@ object AttentivePushHelper {
|
|
|
36
36
|
/**
|
|
37
37
|
* Returns the current push notification authorization status.
|
|
38
38
|
*
|
|
39
|
-
* On API 33+: uses [android.permission.POST_NOTIFICATIONS].
|
|
39
|
+
* On API 33+: uses [android.permission.POST_NOTIFICATIONS]. When permission is not granted,
|
|
40
|
+
* uses [activity] (when provided) and [ActivityCompat.shouldShowRequestPermissionRationale]
|
|
41
|
+
* to distinguish "denied" (user was asked and declined) from "notDetermined" (not yet asked).
|
|
40
42
|
* On API < 33: returns [STATUS_AUTHORIZED] (no runtime permission required).
|
|
41
43
|
*
|
|
42
44
|
* @param context Application or Activity context
|
|
45
|
+
* @param activity Current activity, or null. When non-null on API 33+, used to detect
|
|
46
|
+
* "denied" vs "notDetermined" so downstream logic and analytics are correct for denied users.
|
|
43
47
|
* @return One of [STATUS_AUTHORIZED], [STATUS_DENIED], or [STATUS_NOT_DETERMINED]
|
|
44
48
|
*/
|
|
45
49
|
@JvmStatic
|
|
46
|
-
fun getAuthorizationStatus(context: Context): String {
|
|
50
|
+
fun getAuthorizationStatus(context: Context, activity: Activity? = null): String {
|
|
47
51
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
48
52
|
// API 32 and below: notification permission not required at runtime
|
|
49
53
|
return STATUS_AUTHORIZED
|
|
@@ -51,9 +55,13 @@ object AttentivePushHelper {
|
|
|
51
55
|
return when (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS)) {
|
|
52
56
|
PackageManager.PERMISSION_GRANTED -> STATUS_AUTHORIZED
|
|
53
57
|
else -> {
|
|
54
|
-
// Not granted.
|
|
55
|
-
//
|
|
56
|
-
|
|
58
|
+
// Not granted. Use shouldShowRequestPermissionRationale when we have an Activity
|
|
59
|
+
// so we do not report "denied" users as "notDetermined" (fixes prompt-gating and analytics).
|
|
60
|
+
if (activity != null && ActivityCompat.shouldShowRequestPermissionRationale(activity, android.Manifest.permission.POST_NOTIFICATIONS)) {
|
|
61
|
+
STATUS_DENIED
|
|
62
|
+
} else {
|
|
63
|
+
STATUS_NOT_DETERMINED
|
|
64
|
+
}
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
}
|