@attentive-mobile/attentive-react-native-sdk 2.0.0-beta.4 → 2.0.0-beta.6

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.
Files changed (27) hide show
  1. package/README.md +146 -10
  2. package/android/build.gradle +4 -1
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveNotificationStore.kt +60 -0
  5. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentivePushHelper.kt +101 -0
  6. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt +192 -134
  7. package/android/src/test/kotlin/com/attentivereactnativesdk/AttentiveNotificationStoreTest.kt +103 -0
  8. package/ios/AttentiveReactNativeSdk.mm +49 -2
  9. package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/xcuserdata/zheref.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  10. package/ios/Bridging/ATTNNativeSDK.swift +35 -28
  11. package/lib/commonjs/NativeAttentiveReactNativeSdk.js +1 -1
  12. package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -1
  13. package/lib/commonjs/eventTypes.js.map +1 -1
  14. package/lib/commonjs/index.js +64 -10
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/module/NativeAttentiveReactNativeSdk.js +2 -2
  17. package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -1
  18. package/lib/module/eventTypes.js.map +1 -1
  19. package/lib/module/index.js +64 -12
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +21 -2
  22. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -1
  23. package/lib/typescript/index.d.ts +55 -10
  24. package/lib/typescript/index.d.ts.map +1 -1
  25. package/package.json +9 -4
  26. package/src/NativeAttentiveReactNativeSdk.ts +79 -53
  27. package/src/index.tsx +65 -11
package/README.md CHANGED
@@ -169,11 +169,149 @@ Attentive.identify({phone: '+15556667777'};)
169
169
  // phone: '+15556667777'
170
170
  ```
171
171
 
172
- ### Push Notifications (iOS Only)
172
+ ### Push Notifications (iOS and Android)
173
173
 
174
- The SDK supports push notification integration for iOS. Android support is planned for a future release.
174
+ 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
175
 
176
- #### Request Push Permission
176
+ ---
177
+
178
+ ### App Events on Android
179
+
180
+ This section describes how to implement Attentive app events on Android so they behave like the iOS flow: **regular app opens** (launch and resume from background) and **notification permission** are handled using the SDK’s native Android APIs. You can add FCM token registration and push open handling when your app uses Firebase Cloud Messaging.
181
+
182
+ | SDK method | Purpose on Android |
183
+ |------------|--------------------|
184
+ | `getPushAuthorizationStatus()` | Returns `authorized`, `denied`, or `notDetermined` (uses `POST_NOTIFICATIONS` on API 33+). Use before `handleRegularOpen` so tracking uses the correct status. |
185
+ | `registerForPushNotifications()` | Requests `POST_NOTIFICATIONS` on Android 13+; no-op on older versions. |
186
+ | `handleRegularOpen(authStatus)` | Tracks a regular app open (launch or return to foreground). Call after `identify()` and pass the result of `getPushAuthorizationStatus()`. |
187
+ | `registerDeviceToken` / `registerDeviceTokenWithCallback` | Optional. Register your FCM token when using Firebase Cloud Messaging. |
188
+ | `handlePushOpen` / `handleForegroundPush` | Optional. Call when the user opens a notification or receives one in the foreground. |
189
+
190
+ #### Overview
191
+
192
+ - **Regular app open** – Call `handleRegularOpen(authorizationStatus)` when the app is opened (launch or returning to foreground). The SDK uses this for tracking and the `/mtctrl` endpoint.
193
+ - **Permission status** – On Android 13+ (API 33+), notification permission is `POST_NOTIFICATIONS`. The SDK exposes `getPushAuthorizationStatus()` so you can pass the correct status into `handleRegularOpen`.
194
+ - **Requesting permission** – Call `registerForPushNotifications()` to trigger the system permission dialog on Android 13+; it is a no-op on older versions.
195
+ - **Order of operations** – Always call `identify()` before any `handleRegularOpen()` so the SDK has user context for network requests.
196
+
197
+ #### Prerequisites
198
+
199
+ 1. **AndroidManifest** – Declare the notification permission for Android 13+:
200
+
201
+ ```xml
202
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
203
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
204
+ <!-- other permissions -->
205
+ </manifest>
206
+ ```
207
+
208
+ 2. **Initialize and identify first** – In your app entry (e.g. root component `useEffect`), call `initialize(config)` and `identify(identifiers)` before any push or app-event logic.
209
+
210
+ #### 1. On app launch (Android)
211
+
212
+ Right after `identify()`, do the following for the Android path:
213
+
214
+ 1. Get the current notification authorization status with `getPushAuthorizationStatus()`.
215
+ 2. Call `handleRegularOpen(authStatus)` with that status.
216
+ 3. Optionally call `registerForPushNotifications()` to prompt for permission (Android 13+).
217
+
218
+ ```typescript
219
+ import { Platform } from 'react-native';
220
+ import {
221
+ initialize,
222
+ identify,
223
+ getPushAuthorizationStatus,
224
+ registerForPushNotifications,
225
+ handleRegularOpen,
226
+ type AttentiveSdkConfiguration,
227
+ type PushAuthorizationStatus,
228
+ } from 'attentive-react-native-sdk';
229
+
230
+ // Inside your root component (e.g. App.tsx useEffect):
231
+ initialize(config);
232
+ identify({ email: 'user@example.com', clientUserId: 'id-123' });
233
+
234
+ if (Platform.OS === 'android') {
235
+ getPushAuthorizationStatus()
236
+ .then((authStatus: PushAuthorizationStatus) => {
237
+ handleRegularOpen(authStatus);
238
+ })
239
+ .catch(() => {
240
+ handleRegularOpen('authorized'); // fallback
241
+ });
242
+ registerForPushNotifications(); // Shows permission dialog on Android 13+
243
+ }
244
+ ```
245
+
246
+ #### 2. When app returns to foreground (Android)
247
+
248
+ Subscribe to `AppState` and, when the app becomes `active`, get the current status and call `handleRegularOpen` again:
249
+
250
+ ```typescript
251
+ import { AppState } from 'react-native';
252
+ import { getPushAuthorizationStatus, handleRegularOpen } from 'attentive-react-native-sdk';
253
+ import type { PushAuthorizationStatus } from 'attentive-react-native-sdk';
254
+
255
+ const subscription = AppState.addEventListener('change', (nextAppState) => {
256
+ if (nextAppState === 'active' && Platform.OS === 'android') {
257
+ getPushAuthorizationStatus()
258
+ .then((authStatus: PushAuthorizationStatus) => {
259
+ handleRegularOpen(authStatus);
260
+ })
261
+ .catch(() => {
262
+ handleRegularOpen('authorized');
263
+ });
264
+ }
265
+ });
266
+
267
+ // Cleanup on unmount:
268
+ return () => subscription.remove();
269
+ ```
270
+
271
+ #### 3. Optional: Register FCM token (Android)
272
+
273
+ **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.
274
+
275
+ **Alternative (token from JS):** If you obtain the FCM token elsewhere (e.g. Firebase Messaging), use `registerDeviceTokenWithCallback` and then call `handleRegularOpen` in the callback:
276
+
277
+ ```typescript
278
+ import { registerDeviceTokenWithCallback, handleRegularOpen } from 'attentive-react-native-sdk';
279
+
280
+ getPushAuthorizationStatus().then((authStatus) => {
281
+ registerDeviceTokenWithCallback(
282
+ fcmToken,
283
+ authStatus,
284
+ (data, url, response, error) => {
285
+ if (error) {
286
+ console.error('Attentive token registration failed', error);
287
+ }
288
+ handleRegularOpen(authStatus);
289
+ }
290
+ );
291
+ });
292
+ ```
293
+
294
+ #### 4. Optional: Handle notification opens and foreground (Android)
295
+
296
+ If you handle FCM messages (e.g. with `@react-native-firebase/messaging`), you can report notification opens and foreground receives the same way as on iOS:
297
+
298
+ - **User opened notification (background/inactive):** `handlePushOpen(payload, authorizationStatus)`
299
+ - **Notification received while app in foreground:** `handleForegroundPush(payload, authorizationStatus)`
300
+
301
+ Get `authorizationStatus` via `getPushAuthorizationStatus()` when handling the event.
302
+
303
+ #### Complete Android flow (reference)
304
+
305
+ 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:
306
+
307
+ 1. **Launch:** `initialize` → `identify` → (Android) `getPushAuthorizationStatus()` → `handleRegularOpen(authStatus)` → `registerForPushNotifications()`.
308
+ 2. **Foreground:** `AppState.addEventListener('change', …)` → when `active` and Android → `getPushAuthorizationStatus()` → `handleRegularOpen(authStatus)`.
309
+ 3. **Optional:** When FCM token is available → `registerDeviceTokenWithCallback(token, authStatus, callback)` → in callback call `handleRegularOpen(authStatus)`.
310
+ 4. **Optional:** When user opens a notification or receives one in foreground → `handlePushOpen` / `handleForegroundPush` with payload and status from `getPushAuthorizationStatus()`.
311
+
312
+ ---
313
+
314
+ #### Request Push Permission (iOS)
177
315
 
178
316
  ```typescript
179
317
  import { registerForPushNotifications } from 'attentive-react-native-sdk';
@@ -183,9 +321,9 @@ import { registerForPushNotifications } from 'attentive-react-native-sdk';
183
321
  registerForPushNotifications();
184
322
  ```
185
323
 
186
- #### Register Device Token
324
+ #### Register Device Token (iOS: APNs / Android: FCM)
187
325
 
188
- When your app receives a device token from APNs, register it with the Attentive backend:
326
+ When your app receives a device token (APNs on iOS, FCM on Android), register it with the Attentive backend:
189
327
 
190
328
  ```typescript
191
329
  import { registerDeviceToken } from 'attentive-react-native-sdk';
@@ -202,7 +340,7 @@ The `authorizationStatus` parameter should be one of:
202
340
  - `'provisional'` - Provisional authorization (quiet notifications)
203
341
  - `'ephemeral'` - App Clip notifications
204
342
 
205
- #### Handle Push Notification Opens
343
+ #### Handle Push Notification Opens (iOS and Android)
206
344
 
207
345
  When a user taps on a push notification, track the event:
208
346
 
@@ -218,7 +356,7 @@ handlePushOpened(
218
356
  );
219
357
  ```
220
358
 
221
- #### Handle Foreground Notifications
359
+ #### Handle Foreground Notifications (iOS and Android)
222
360
 
223
361
  When a notification arrives while the app is in the foreground:
224
362
 
@@ -285,6 +423,4 @@ func application(
285
423
  - [Push Notifications Setup](./PUSH_NOTIFICATIONS_SETUP.md) - General push notification setup
286
424
  - [iOS Native SDK documentation](https://github.com/attentive-mobile/attentive-ios-sdk) - Native SDK reference
287
425
 
288
- #### Android Support
289
-
290
- Android push notification support is not yet implemented. The push notification methods will be no-ops on Android. FCM (Firebase Cloud Messaging) integration is planned for a future release.
426
+ For a full Android implementation (app launch, foreground, permission, and optional FCM), see the **[App Events on Android](#app-events-on-android)** section above.
@@ -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
- implementation 'com.attentive:attentive-android-sdk:1.0.1'
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-beta.1'
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
 
@@ -1,4 +1,6 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
2
  package="com.attentivereactnativesdk">
3
3
 
4
+ <!-- Required for push notification permission prompt on Android 13+ (API 33+) -->
5
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
4
6
  </manifest>
@@ -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
+ }
@@ -0,0 +1,101 @@
1
+ package com.attentivereactnativesdk
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.os.Build
7
+ import android.util.Log
8
+ import androidx.core.app.ActivityCompat
9
+ import androidx.core.content.ContextCompat
10
+
11
+ /**
12
+ * Android push notification permission helper for the Attentive SDK.
13
+ *
14
+ * On Android 13+ (API 33+), push notifications require the runtime permission
15
+ * [android.permission.POST_NOTIFICATIONS]. On older versions, notifications
16
+ * are allowed by default (no runtime permission).
17
+ *
18
+ * This helper provides:
19
+ * - [getAuthorizationStatus] – current permission status for parity with iOS
20
+ * - [requestPermission] – request POST_NOTIFICATIONS when needed (used by registerForPushNotifications)
21
+ */
22
+ object AttentivePushHelper {
23
+
24
+ private const val TAG = "AttentivePushHelper"
25
+
26
+ /**
27
+ * Authorization status values aligned with iOS push authorization for use in handleRegularOpen etc.
28
+ * - "authorized" – user has granted notification permission (or API < 33)
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
+ */
32
+ const val STATUS_AUTHORIZED = "authorized"
33
+ const val STATUS_DENIED = "denied"
34
+ const val STATUS_NOT_DETERMINED = "notDetermined"
35
+
36
+ /**
37
+ * Returns the current push notification authorization status.
38
+ *
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).
42
+ * On API < 33: returns [STATUS_AUTHORIZED] (no runtime permission required).
43
+ *
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.
47
+ * @return One of [STATUS_AUTHORIZED], [STATUS_DENIED], or [STATUS_NOT_DETERMINED]
48
+ */
49
+ @JvmStatic
50
+ fun getAuthorizationStatus(context: Context, activity: Activity? = null): String {
51
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
52
+ // API 32 and below: notification permission not required at runtime
53
+ return STATUS_AUTHORIZED
54
+ }
55
+ return when (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS)) {
56
+ PackageManager.PERMISSION_GRANTED -> STATUS_AUTHORIZED
57
+ else -> {
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
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Requests the push notification permission (POST_NOTIFICATIONS) if needed.
71
+ * Must be called from an [Activity] (e.g. [reactApplicationContext.currentActivity]).
72
+ *
73
+ * On API < 33 this is a no-op and returns immediately.
74
+ *
75
+ * @param activity Current activity (required for requestPermissions)
76
+ * @param requestCode Request code for [Activity.onRequestPermissionsResult]
77
+ * @return true if the permission request was started or already granted, false if activity is null or permission not applicable
78
+ */
79
+ @JvmStatic
80
+ fun requestPermissionIfNeeded(activity: Activity?, requestCode: Int): Boolean {
81
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
82
+ Log.d(TAG, "requestPermissionIfNeeded: API < 33, no runtime permission needed")
83
+ return true
84
+ }
85
+ if (activity == null) {
86
+ Log.w(TAG, "requestPermissionIfNeeded: activity is null, cannot request permission")
87
+ return false
88
+ }
89
+ if (ContextCompat.checkSelfPermission(activity, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
90
+ Log.d(TAG, "requestPermissionIfNeeded: POST_NOTIFICATIONS already granted")
91
+ return true
92
+ }
93
+ Log.i(TAG, "requestPermissionIfNeeded: requesting POST_NOTIFICATIONS")
94
+ ActivityCompat.requestPermissions(
95
+ activity,
96
+ arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
97
+ requestCode
98
+ )
99
+ return true
100
+ }
101
+ }