appambit-push-notifications 0.3.0 → 1.0.0
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/AppAmbitSdkPushNotifications.podspec +14 -4
- package/README.md +184 -105
- package/android/build.gradle +21 -3
- package/android/src/main/AndroidManifest.xml +107 -1
- package/android/src/main/java/com/appambitpushnotifications/AppAmbitContextHolder.kt +22 -0
- package/android/src/main/java/com/appambitpushnotifications/AppAmbitHeadlessService.kt +177 -0
- package/android/src/main/java/com/appambitpushnotifications/AppAmbitInitProvider.kt +73 -0
- package/android/src/main/java/com/appambitpushnotifications/AppAmbitMessagingService.kt +12 -0
- package/android/src/main/java/com/appambitpushnotifications/AppAmbitNotificationSerializer.kt +88 -0
- package/android/src/main/java/com/appambitpushnotifications/AppAmbitPayloadUtils.kt +59 -0
- package/android/src/main/java/com/appambitpushnotifications/AppAmbitPushEventEmitter.kt +100 -0
- package/android/src/main/java/com/appambitpushnotifications/AppAmbitRNServiceExtension.kt +75 -0
- package/android/src/main/java/com/appambitpushnotifications/AppAmbitRemoteMessageStore.kt +26 -0
- package/android/src/main/java/com/appambitpushnotifications/AppambitPushNotificationsModule.kt +377 -76
- package/ios/AppAmbitNotificationSwizzler.m +290 -0
- package/ios/AppAmbitPushWrapper.swift +165 -25
- package/ios/AppAmbitRNNotificationService.swift +46 -0
- package/ios/AppambitPushNotifications.mm +264 -10
- package/lib/module/NativeAppambitPushNotifications.js.map +1 -1
- package/lib/module/index.js +46 -10
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeAppambitPushNotifications.d.ts +2 -1
- package/lib/typescript/src/NativeAppambitPushNotifications.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +32 -6
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeAppambitPushNotifications.ts +7 -1
- package/src/index.tsx +93 -20
package/android/src/main/java/com/appambitpushnotifications/AppambitPushNotificationsModule.kt
CHANGED
|
@@ -1,93 +1,394 @@
|
|
|
1
1
|
package com.appambitpushnotifications
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.net.ConnectivityManager
|
|
8
|
+
import android.net.Network
|
|
9
|
+
import android.net.NetworkCapabilities
|
|
10
|
+
import android.net.NetworkRequest
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.util.Log
|
|
4
13
|
import androidx.activity.ComponentActivity
|
|
5
|
-
import com.
|
|
14
|
+
import com.appambit.sdk.AppAmbit
|
|
15
|
+
import com.appambit.sdk.PushKernel
|
|
16
|
+
import com.appambit.sdk.PushNotifications
|
|
17
|
+
import com.facebook.react.bridge.ActivityEventListener
|
|
18
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
6
19
|
import com.facebook.react.bridge.Promise
|
|
20
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
21
|
import com.facebook.react.module.annotations.ReactModule
|
|
8
|
-
import com.appambit.sdk.PushNotifications
|
|
9
|
-
import com.facebook.react.bridge.Arguments
|
|
10
|
-
import com.facebook.react.bridge.WritableMap
|
|
11
|
-
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
12
22
|
|
|
23
|
+
/**
|
|
24
|
+
* AppambitPushNotificationsModule
|
|
25
|
+
*
|
|
26
|
+
* The Turbo Native Module that exposes AppAmbit Push SDK functionality to JavaScript.
|
|
27
|
+
*/
|
|
13
28
|
@ReactModule(name = AppambitPushNotificationsModule.NAME)
|
|
14
29
|
class AppambitPushNotificationsModule(reactContext: ReactApplicationContext) :
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
30
|
+
NativeAppambitPushNotificationsSpec(reactContext), ActivityEventListener, LifecycleEventListener {
|
|
31
|
+
|
|
32
|
+
companion object {
|
|
33
|
+
const val NAME = "AppambitPushNotifications"
|
|
34
|
+
private const val TAG = "AppAmbitPushModule"
|
|
35
|
+
|
|
36
|
+
// SharedPreferences for UI state persistence across restarts.
|
|
37
|
+
private const val PUSH_PREFS = "appambit_push_prefs"
|
|
38
|
+
private const val KEY_ENABLED_STATE = "appambit_push_enabled_state"
|
|
39
|
+
private const val KEY_HAS_ENABLED_STATE = "appambit_push_has_enabled_state"
|
|
40
|
+
// Pending consumer-sync intent: last toggle not yet confirmed online.
|
|
41
|
+
private const val KEY_PENDING = "appambit_push_pending_sync"
|
|
42
|
+
private const val KEY_PENDING_VALUE = "appambit_push_pending_value"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private var hasRegisteredOpenedListener = false
|
|
46
|
+
private var lastProcessedIntent: Intent? = null
|
|
47
|
+
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
|
48
|
+
|
|
49
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
override fun getName(): String = NAME
|
|
52
|
+
|
|
53
|
+
override fun initialize() {
|
|
54
|
+
super.initialize()
|
|
55
|
+
Log.d(TAG, "Module initialized")
|
|
56
|
+
AppAmbitContextHolder.set(reactApplicationContext)
|
|
57
|
+
AppAmbitPushEventEmitter.attach(reactApplicationContext)
|
|
58
|
+
|
|
59
|
+
reactApplicationContext.addActivityEventListener(this)
|
|
60
|
+
reactApplicationContext.addLifecycleEventListener(this)
|
|
61
|
+
|
|
62
|
+
registerNetworkCallback()
|
|
63
|
+
|
|
64
|
+
// Try to check intent immediately if activity exists
|
|
65
|
+
checkInitialIntent()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override fun invalidate() {
|
|
69
|
+
AppAmbitPushEventEmitter.detach()
|
|
70
|
+
PushKernel.setOpenedNotificationListener(null)
|
|
71
|
+
reactApplicationContext.removeActivityEventListener(this)
|
|
72
|
+
reactApplicationContext.removeLifecycleEventListener(this)
|
|
73
|
+
unregisterNetworkCallback()
|
|
74
|
+
hasRegisteredOpenedListener = false
|
|
75
|
+
super.invalidate()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun checkInitialIntent() {
|
|
79
|
+
val activity = currentActivity
|
|
80
|
+
Log.d(TAG, "checkInitialIntent called. activity=$activity")
|
|
81
|
+
|
|
82
|
+
if (activity != null) {
|
|
83
|
+
var intent = activity.intent
|
|
84
|
+
Log.d(TAG, "checkInitialIntent: intent=$intent, lastProcessedIntent=$lastProcessedIntent")
|
|
85
|
+
|
|
86
|
+
if (intent != null && intent != lastProcessedIntent) {
|
|
87
|
+
lastProcessedIntent = intent
|
|
88
|
+
|
|
89
|
+
Log.d(TAG, "checkInitialIntent: original action=${intent.action}")
|
|
90
|
+
val extras = intent.extras
|
|
91
|
+
|
|
92
|
+
if (extras != null && intent.action != "com.appambit.sdk.NOTIFICATION_OPENED") {
|
|
93
|
+
var isPush = extras.containsKey("google.message_id")
|
|
94
|
+
for (key in extras.keySet()) {
|
|
95
|
+
Log.d(TAG, "checkInitialIntent: extra $key = ${extras.get(key)}")
|
|
96
|
+
if (key.startsWith("gcm.")) {
|
|
97
|
+
isPush = true
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isPush) {
|
|
102
|
+
Log.d(TAG, "checkInitialIntent: Detected FCM System Tray Intent. Mutating format...")
|
|
103
|
+
val pushIntent = Intent(intent)
|
|
104
|
+
pushIntent.action = "com.appambit.sdk.NOTIFICATION_OPENED"
|
|
105
|
+
|
|
106
|
+
val title = extras.getString("gcm.notification.title") ?: extras.getString("title")
|
|
107
|
+
val body = extras.getString("gcm.notification.body") ?: extras.getString("body")
|
|
108
|
+
if (title != null) pushIntent.putExtra("appambit_title", title)
|
|
109
|
+
if (body != null) pushIntent.putExtra("appambit_body", body)
|
|
110
|
+
|
|
111
|
+
// imageUrl: gcm.notification.image is filtered from the data loop below,
|
|
112
|
+
// so we extract it explicitly here.
|
|
113
|
+
val imageUrl = extras.getString("gcm.notification.image")
|
|
114
|
+
?: extras.getString("image_url")
|
|
115
|
+
?: extras.getString("image")
|
|
116
|
+
if (imageUrl != null) pushIntent.putExtra("appambit_image_url", imageUrl)
|
|
117
|
+
|
|
118
|
+
// color and icon
|
|
119
|
+
val color = extras.getString("gcm.notification.color")
|
|
120
|
+
val icon = extras.getString("gcm.notification.icon")
|
|
121
|
+
if (color != null) pushIntent.putExtra("appambit_color", color)
|
|
122
|
+
if (icon != null) pushIntent.putExtra("appambit_icon", icon)
|
|
123
|
+
|
|
124
|
+
val keysList = mutableListOf<String>()
|
|
125
|
+
val valuesList = mutableListOf<String>()
|
|
126
|
+
|
|
127
|
+
// Inject Android notification display fields as _aa_* data keys so the
|
|
128
|
+
// serializer can promote them into the android sub-object and strip them
|
|
129
|
+
// from the custom data map exposed to JS.
|
|
130
|
+
mapOf(
|
|
131
|
+
"_aa_ticker" to (extras.getString("gcm.notification.ticker")),
|
|
132
|
+
"_aa_sticky" to (extras.getString("gcm.notification.sticky")),
|
|
133
|
+
"_aa_visibility" to (extras.getString("gcm.notification.visibility")),
|
|
134
|
+
"_aa_channel_id" to (extras.getString("gcm.notification.channel_id")),
|
|
135
|
+
"_aa_priority" to (extras.getString("gcm.notification.priority")
|
|
136
|
+
?: extras.getString("gcm.notification.notification_priority")),
|
|
137
|
+
"_aa_tag" to (extras.getString("gcm.notification.tag")),
|
|
138
|
+
"_aa_sound" to (extras.getString("gcm.notification.sound")),
|
|
139
|
+
"_aa_click_action" to (extras.getString("gcm.notification.click_action")
|
|
140
|
+
?: extras.getString("gcm.notification.clickAction"))
|
|
141
|
+
).forEach { (key, value) ->
|
|
142
|
+
if (value != null) { keysList.add(key); valuesList.add(value) }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Custom data keys from the FCM data payload
|
|
146
|
+
for (key in extras.keySet()) {
|
|
147
|
+
if (!key.startsWith("google.") && !key.startsWith("gcm.") && !key.startsWith("android.") &&
|
|
148
|
+
key != "from" && key != "collapse_key" && key != "profile") {
|
|
149
|
+
keysList.add(key)
|
|
150
|
+
valuesList.add(extras.get(key).toString())
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (keysList.isNotEmpty()) {
|
|
155
|
+
pushIntent.putExtra("appambit_data_keys", keysList.toTypedArray())
|
|
156
|
+
pushIntent.putExtra("appambit_data_keys_values", valuesList.toTypedArray())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
intent = pushIntent
|
|
160
|
+
}
|
|
161
|
+
} else if (extras == null) {
|
|
162
|
+
Log.d(TAG, "checkInitialIntent: no extras")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
PushNotifications.handleNotificationOpened(reactApplicationContext.applicationContext, intent)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── LifecycleEventListener ────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
override fun onHostResume() {
|
|
173
|
+
checkInitialIntent()
|
|
174
|
+
// Replay any deferred consumer sync (covers "toggle offline → reopen online").
|
|
175
|
+
flushPendingConsumerSync()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override fun onHostPause() {
|
|
179
|
+
// No-op
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
override fun onHostDestroy() {
|
|
183
|
+
// No-op
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── ActivityEventListener ─────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
|
|
189
|
+
// No-op
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
override fun onNewIntent(intent: Intent) {
|
|
193
|
+
if (intent != lastProcessedIntent) {
|
|
194
|
+
lastProcessedIntent = intent
|
|
195
|
+
Log.d(TAG, "onNewIntent: action=${intent.action}")
|
|
196
|
+
PushNotifications.handleNotificationOpened(reactApplicationContext.applicationContext, intent)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── JS API ────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
override fun start() {
|
|
203
|
+
checkInitialIntent()
|
|
204
|
+
PushNotifications.start(reactApplicationContext.applicationContext)
|
|
205
|
+
// start() is invoked from JS right after AppAmbit.start() (Core init), so this
|
|
206
|
+
// is the first reliable point where a deferred consumer sync can succeed.
|
|
207
|
+
// onHostResume / the network callback may fire before Core is initialized.
|
|
208
|
+
flushPendingConsumerSync()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
override fun requestNotificationPermission() {
|
|
212
|
+
val activity: Activity? = currentActivity
|
|
213
|
+
if (activity is ComponentActivity) {
|
|
214
|
+
PushNotifications.requestNotificationPermission(activity)
|
|
215
|
+
} else {
|
|
216
|
+
Log.w(TAG, "requestNotificationPermission: currentActivity is not a ComponentActivity")
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
override fun requestNotificationPermissionWithResult(promise: Promise) {
|
|
221
|
+
val activity: Activity? = currentActivity
|
|
222
|
+
if (activity is ComponentActivity) {
|
|
223
|
+
PushNotifications.requestNotificationPermission(activity) { isGranted ->
|
|
224
|
+
promise.resolve(isGranted)
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
promise.resolve(false)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
override fun setNotificationsEnabled(enabled: Boolean) {
|
|
232
|
+
val context = reactApplicationContext.applicationContext
|
|
233
|
+
val prefs = context.getSharedPreferences(PUSH_PREFS, Context.MODE_PRIVATE)
|
|
234
|
+
// 1. Persist user intent for UI state consistency across restarts and
|
|
235
|
+
// 2. record a pending consumer-sync intent (cleared once delivered).
|
|
236
|
+
prefs.edit()
|
|
237
|
+
.putBoolean(KEY_ENABLED_STATE, enabled)
|
|
238
|
+
.putBoolean(KEY_HAS_ENABLED_STATE, true)
|
|
239
|
+
.putBoolean(KEY_PENDING_VALUE, enabled)
|
|
240
|
+
.putBoolean(KEY_PENDING, true)
|
|
241
|
+
.apply()
|
|
242
|
+
// 3. Update the SDK's local enabled flag (no network) so cold-start token
|
|
243
|
+
// sync knows the user's intent.
|
|
244
|
+
PushKernel.setNotificationsEnabled(context, enabled)
|
|
245
|
+
// 4. Push to the backend when online; otherwise defer until connectivity
|
|
246
|
+
// returns (replayed from the network callback / onHostResume).
|
|
247
|
+
flushPendingConsumerSync()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
override fun isNotificationsEnabled(promise: Promise) {
|
|
251
|
+
val context = reactApplicationContext.applicationContext
|
|
252
|
+
val prefs = context.getSharedPreferences(PUSH_PREFS, Context.MODE_PRIVATE)
|
|
253
|
+
if (prefs.getBoolean(KEY_HAS_ENABLED_STATE, false)) {
|
|
254
|
+
// Return our own persisted state — always the last value the user explicitly set,
|
|
255
|
+
// survives cold restarts and SDK state inconsistencies.
|
|
256
|
+
promise.resolve(prefs.getBoolean(KEY_ENABLED_STATE, false))
|
|
257
|
+
} else {
|
|
258
|
+
// First-ever launch: no stored state yet, ask the SDK.
|
|
259
|
+
promise.resolve(
|
|
260
|
+
PushNotifications.isNotificationsEnabled(context)
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
override fun hasNotificationPermission(promise: Promise) {
|
|
266
|
+
val granted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
267
|
+
reactApplicationContext.checkSelfPermission(
|
|
268
|
+
android.Manifest.permission.POST_NOTIFICATIONS
|
|
269
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
270
|
+
} else {
|
|
271
|
+
true
|
|
272
|
+
}
|
|
273
|
+
promise.resolve(granted)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
override fun backgroundHandlerCompleted() {
|
|
277
|
+
// Android background execution is managed by the headless JS task registered with
|
|
278
|
+
// BACKGROUND_NOTIFICATION_TASK — no native completion signal needed here.
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Required by NativeEventEmitter ───────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
override fun addListener(eventName: String?) {
|
|
284
|
+
if (eventName == AppAmbitPushEventEmitter.EVENT_OPENED && !hasRegisteredOpenedListener) {
|
|
285
|
+
hasRegisteredOpenedListener = true
|
|
286
|
+
registerOpenedListener()
|
|
71
287
|
}
|
|
72
|
-
params.putMap("data", dataMap)
|
|
73
|
-
}
|
|
74
|
-
sendEvent("onNotificationReceived", params)
|
|
75
288
|
}
|
|
76
|
-
}
|
|
77
289
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
reactApplicationContext
|
|
81
|
-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
82
|
-
.emit(eventName, params)
|
|
290
|
+
override fun removeListeners(count: Double) {
|
|
291
|
+
// No-op
|
|
83
292
|
}
|
|
84
|
-
}
|
|
85
293
|
|
|
86
|
-
|
|
294
|
+
// ── Offline-resilient consumer sync ──────────────────────────────────────
|
|
295
|
+
// The SDK's consumer update is fire-and-forget with no offline retry, so we
|
|
296
|
+
// defer it while offline and replay it when connectivity returns.
|
|
87
297
|
|
|
88
|
-
|
|
298
|
+
private fun registerNetworkCallback() {
|
|
299
|
+
if (networkCallback != null) return
|
|
300
|
+
val cm = reactApplicationContext.getSystemService(Context.CONNECTIVITY_SERVICE)
|
|
301
|
+
as? ConnectivityManager ?: return
|
|
302
|
+
val callback = object : ConnectivityManager.NetworkCallback() {
|
|
303
|
+
override fun onAvailable(network: Network) {
|
|
304
|
+
Log.d(TAG, "NetworkCallback.onAvailable")
|
|
305
|
+
flushPendingConsumerSync()
|
|
306
|
+
}
|
|
89
307
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
308
|
+
override fun onCapabilitiesChanged(
|
|
309
|
+
network: Network,
|
|
310
|
+
caps: NetworkCapabilities
|
|
311
|
+
) {
|
|
312
|
+
// onAvailable can fire before the link is actually usable; the
|
|
313
|
+
// capabilities update is a second chance to replay once the
|
|
314
|
+
// network reports INTERNET.
|
|
315
|
+
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
|
|
316
|
+
flushPendingConsumerSync()
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
val request = NetworkRequest.Builder()
|
|
321
|
+
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
322
|
+
.build()
|
|
323
|
+
try {
|
|
324
|
+
cm.registerNetworkCallback(request, callback)
|
|
325
|
+
networkCallback = callback
|
|
326
|
+
} catch (e: Exception) {
|
|
327
|
+
Log.w(TAG, "registerNetworkCallback failed: ${e.message}")
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private fun unregisterNetworkCallback() {
|
|
332
|
+
val cb = networkCallback ?: return
|
|
333
|
+
val cm = reactApplicationContext.getSystemService(Context.CONNECTIVITY_SERVICE)
|
|
334
|
+
as? ConnectivityManager
|
|
335
|
+
try {
|
|
336
|
+
cm?.unregisterNetworkCallback(cb)
|
|
337
|
+
} catch (e: Exception) {
|
|
338
|
+
Log.w(TAG, "unregisterNetworkCallback failed: ${e.message}")
|
|
339
|
+
}
|
|
340
|
+
networkCallback = null
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private fun isNetworkAvailable(context: Context): Boolean {
|
|
344
|
+
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE)
|
|
345
|
+
as? ConnectivityManager ?: return false
|
|
346
|
+
val network = cm.activeNetwork ?: return false
|
|
347
|
+
val caps = cm.getNetworkCapabilities(network) ?: return false
|
|
348
|
+
// Only require INTERNET capability (matches iOS NWPathMonitor's `.satisfied`).
|
|
349
|
+
// We intentionally do NOT require NET_CAPABILITY_VALIDATED: emulators and
|
|
350
|
+
// freshly-reconnected networks often report a usable connection before (or
|
|
351
|
+
// without ever) flipping VALIDATED, which would otherwise make the deferred
|
|
352
|
+
// consumer sync skip forever.
|
|
353
|
+
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@Synchronized
|
|
357
|
+
private fun flushPendingConsumerSync() {
|
|
358
|
+
val context = reactApplicationContext.applicationContext
|
|
359
|
+
val prefs = context.getSharedPreferences(PUSH_PREFS, Context.MODE_PRIVATE)
|
|
360
|
+
if (!prefs.getBoolean(KEY_PENDING, false)) return
|
|
361
|
+
if (!isNetworkAvailable(context)) {
|
|
362
|
+
Log.d(TAG, "flushPendingConsumerSync: offline, keeping pending intent")
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
// The Core SDK rejects (and logs) any consumer update before AppAmbit.start()
|
|
366
|
+
// has run. onHostResume / the network callback can fire during cold start
|
|
367
|
+
// BEFORE the JS layer initializes Core, so we must keep the pending intent
|
|
368
|
+
// until Core is ready — otherwise the replay is silently dropped.
|
|
369
|
+
if (!AppAmbit.isInitialized()) {
|
|
370
|
+
Log.d(TAG, "flushPendingConsumerSync: Core not initialized yet, keeping pending intent")
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
val desired = prefs.getBoolean(KEY_PENDING_VALUE, false)
|
|
374
|
+
Log.d(TAG, "flushPendingConsumerSync: replaying consumer update enabled=$desired")
|
|
375
|
+
// The SDK's consumer update reuses the last stored device token when no
|
|
376
|
+
// live token is available, so this succeeds as long as a token was ever
|
|
377
|
+
// registered (it only skips when nothing has ever been stored).
|
|
378
|
+
PushNotifications.setNotificationsEnabled(context, desired)
|
|
379
|
+
prefs.edit()
|
|
380
|
+
.remove(KEY_PENDING)
|
|
381
|
+
.remove(KEY_PENDING_VALUE)
|
|
382
|
+
.apply()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Internal helpers ──────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
private fun registerOpenedListener() {
|
|
388
|
+
PushNotifications.setOpenedListener { notification ->
|
|
389
|
+
Log.d(TAG, "Notification opened: ${notification.title}")
|
|
390
|
+
val payload = AppAmbitNotificationSerializer.toEventPayload(notification)
|
|
391
|
+
AppAmbitPushEventEmitter.emit(AppAmbitPushEventEmitter.EVENT_OPENED, payload)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
93
394
|
}
|