@attentive-mobile/attentive-react-native-sdk 2.0.0-beta.3 → 2.0.0-beta.5

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 +145 -10
  2. package/android/build.gradle +4 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentivePushHelper.kt +93 -0
  5. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt +382 -56
  6. package/android/src/main/kotlin/com/attentivereactnativesdk/debug/NetworkingHelper.kt +220 -0
  7. package/ios/AttentiveReactNativeSdk.mm +33 -0
  8. package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  9. package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/xcuserdata/zheref.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  10. package/ios/AttentiveReactNativeSdk.xcodeproj/xcuserdata/zheref.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
  11. package/ios/Bridging/ATTNNativeSDK.swift +47 -6
  12. package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -1
  13. package/lib/commonjs/eventTypes.js.map +1 -1
  14. package/lib/commonjs/index.js +28 -9
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -1
  17. package/lib/module/eventTypes.js.map +1 -1
  18. package/lib/module/index.js +29 -11
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +9 -1
  21. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -1
  22. package/lib/typescript/index.d.ts +24 -9
  23. package/lib/typescript/index.d.ts.map +1 -1
  24. package/package.json +7 -3
  25. package/src/NativeAttentiveReactNativeSdk.ts +11 -2
  26. package/src/index.tsx +29 -10
  27. package/ios/AttentiveReactNativeSdk.xcworkspace/contents.xcworkspacedata +0 -10
package/README.md CHANGED
@@ -169,11 +169,148 @@ 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
+ If your app uses Firebase Cloud Messaging and you have an FCM token, register it with the Attentive backend and then call `handleRegularOpen` in the callback (same pattern as iOS):
274
+
275
+ ```typescript
276
+ import { registerDeviceTokenWithCallback, handleRegularOpen } from 'attentive-react-native-sdk';
277
+
278
+ // When you receive the FCM token (e.g. from Firebase Messaging):
279
+ getPushAuthorizationStatus().then((authStatus) => {
280
+ registerDeviceTokenWithCallback(
281
+ fcmToken,
282
+ authStatus,
283
+ (data, url, response, error) => {
284
+ if (error) {
285
+ console.error('Attentive token registration failed', error);
286
+ }
287
+ handleRegularOpen(authStatus);
288
+ }
289
+ );
290
+ });
291
+ ```
292
+
293
+ #### 4. Optional: Handle notification opens and foreground (Android)
294
+
295
+ 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:
296
+
297
+ - **User opened notification (background/inactive):** `handlePushOpen(payload, authorizationStatus)`
298
+ - **Notification received while app in foreground:** `handleForegroundPush(payload, authorizationStatus)`
299
+
300
+ Get `authorizationStatus` via `getPushAuthorizationStatus()` when handling the event.
301
+
302
+ #### Complete Android flow (reference)
303
+
304
+ 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
+
306
+ 1. **Launch:** `initialize` → `identify` → (Android) `getPushAuthorizationStatus()` → `handleRegularOpen(authStatus)` → `registerForPushNotifications()`.
307
+ 2. **Foreground:** `AppState.addEventListener('change', …)` → when `active` and Android → `getPushAuthorizationStatus()` → `handleRegularOpen(authStatus)`.
308
+ 3. **Optional:** When FCM token is available → `registerDeviceTokenWithCallback(token, authStatus, callback)` → in callback call `handleRegularOpen(authStatus)`.
309
+ 4. **Optional:** When user opens a notification or receives one in foreground → `handlePushOpen` / `handleForegroundPush` with payload and status from `getPushAuthorizationStatus()`.
310
+
311
+ ---
312
+
313
+ #### Request Push Permission (iOS)
177
314
 
178
315
  ```typescript
179
316
  import { registerForPushNotifications } from 'attentive-react-native-sdk';
@@ -183,9 +320,9 @@ import { registerForPushNotifications } from 'attentive-react-native-sdk';
183
320
  registerForPushNotifications();
184
321
  ```
185
322
 
186
- #### Register Device Token
323
+ #### Register Device Token (iOS: APNs / Android: FCM)
187
324
 
188
- When your app receives a device token from APNs, register it with the Attentive backend:
325
+ When your app receives a device token (APNs on iOS, FCM on Android), register it with the Attentive backend:
189
326
 
190
327
  ```typescript
191
328
  import { registerDeviceToken } from 'attentive-react-native-sdk';
@@ -202,7 +339,7 @@ The `authorizationStatus` parameter should be one of:
202
339
  - `'provisional'` - Provisional authorization (quiet notifications)
203
340
  - `'ephemeral'` - App Clip notifications
204
341
 
205
- #### Handle Push Notification Opens
342
+ #### Handle Push Notification Opens (iOS and Android)
206
343
 
207
344
  When a user taps on a push notification, track the event:
208
345
 
@@ -218,7 +355,7 @@ handlePushOpened(
218
355
  );
219
356
  ```
220
357
 
221
- #### Handle Foreground Notifications
358
+ #### Handle Foreground Notifications (iOS and Android)
222
359
 
223
360
  When a notification arrives while the app is in the foreground:
224
361
 
@@ -285,6 +422,4 @@ func application(
285
422
  - [Push Notifications Setup](./PUSH_NOTIFICATIONS_SETUP.md) - General push notification setup
286
423
  - [iOS Native SDK documentation](https://github.com/attentive-mobile/attentive-ios-sdk) - Native SDK reference
287
424
 
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.
425
+ For a full Android implementation (app launch, foreground, permission, and optional FCM), see the **[App Events on Android](#app-events-on-android)** section above.
@@ -85,6 +85,10 @@ dependencies {
85
85
  implementation 'com.attentive:attentive-android-sdk:1.0.1'
86
86
  implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.10"
87
87
  implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.9.10"))
88
+
89
+ // OkHttp for networking debugging (optional, only used when debugging is enabled)
90
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
91
+ implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
88
92
  }
89
93
 
90
94
  if (isNewArchitectureEnabled()) {
@@ -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,93 @@
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 denied or permission not granted
30
+ * - "notDetermined" – not yet requested (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].
40
+ * On API < 33: returns [STATUS_AUTHORIZED] (no runtime permission required).
41
+ *
42
+ * @param context Application or Activity context
43
+ * @return One of [STATUS_AUTHORIZED], [STATUS_DENIED], or [STATUS_NOT_DETERMINED]
44
+ */
45
+ @JvmStatic
46
+ fun getAuthorizationStatus(context: Context): String {
47
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
48
+ // API 32 and below: notification permission not required at runtime
49
+ return STATUS_AUTHORIZED
50
+ }
51
+ return when (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS)) {
52
+ PackageManager.PERMISSION_GRANTED -> STATUS_AUTHORIZED
53
+ else -> {
54
+ // Not granted. Without an Activity we cannot distinguish "denied" vs "not yet asked".
55
+ // Return notDetermined so the app can call registerForPushNotifications to request.
56
+ STATUS_NOT_DETERMINED
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Requests the push notification permission (POST_NOTIFICATIONS) if needed.
63
+ * Must be called from an [Activity] (e.g. [reactApplicationContext.currentActivity]).
64
+ *
65
+ * On API < 33 this is a no-op and returns immediately.
66
+ *
67
+ * @param activity Current activity (required for requestPermissions)
68
+ * @param requestCode Request code for [Activity.onRequestPermissionsResult]
69
+ * @return true if the permission request was started or already granted, false if activity is null or permission not applicable
70
+ */
71
+ @JvmStatic
72
+ fun requestPermissionIfNeeded(activity: Activity?, requestCode: Int): Boolean {
73
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
74
+ Log.d(TAG, "requestPermissionIfNeeded: API < 33, no runtime permission needed")
75
+ return true
76
+ }
77
+ if (activity == null) {
78
+ Log.w(TAG, "requestPermissionIfNeeded: activity is null, cannot request permission")
79
+ return false
80
+ }
81
+ if (ContextCompat.checkSelfPermission(activity, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
82
+ Log.d(TAG, "requestPermissionIfNeeded: POST_NOTIFICATIONS already granted")
83
+ return true
84
+ }
85
+ Log.i(TAG, "requestPermissionIfNeeded: requesting POST_NOTIFICATIONS")
86
+ ActivityCompat.requestPermissions(
87
+ activity,
88
+ arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
89
+ requestCode
90
+ )
91
+ return true
92
+ }
93
+ }