catalyst-core 0.0.3-canary.18 โ†’ 0.0.3-canary.19

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 (48) hide show
  1. package/changelog.md +29 -3
  2. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +55 -0
  3. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +53 -5
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +97 -39
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +8 -1
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NetworkUtils.kt +105 -0
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationConstants.kt +2 -1
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationUtils.kt +7 -5
  9. package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +2 -7
  10. package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +39 -18
  11. package/dist/native/bridge/constants/NativeInterfaces.js +5 -4
  12. package/dist/native/bridge/hooks.js +7 -4
  13. package/dist/native/bridge/utils/NativeBridge.js +4 -0
  14. package/dist/native/buildAppAndroid.js +3 -2
  15. package/dist/native/buildAppIos.js +3 -3
  16. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +1 -0
  17. package/dist/native/iosnativeWebView/Sources/Core/Utils/NetworkMonitor.swift +112 -0
  18. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +6 -0
  19. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +31 -0
  20. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +14 -7
  21. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +105 -7
  22. package/package.json +1 -1
  23. package/dist/native/androidProject/app/.editorconfig +0 -17
  24. package/dist/native/androidProject/app/detekt-config.yml +0 -562
  25. package/dist/native/androidProject/app/src/main/assets/webview_config.properties +0 -30
  26. package/dist/native/androidProject/splashscreen_fallback_original.avif +0 -0
  27. package/dist/native/iosnativeWebView/iosnativeWebView/BootTimingUtility.swift +0 -26
  28. package/dist/native/iosnativeWebView/iosnativeWebView/BridgeCommandHandler.swift +0 -312
  29. package/dist/native/iosnativeWebView/iosnativeWebView/BridgeDelegateHandler.swift +0 -166
  30. package/dist/native/iosnativeWebView/iosnativeWebView/BridgeFileHandler.swift +0 -340
  31. package/dist/native/iosnativeWebView/iosnativeWebView/BridgeJavaScriptInterface.swift +0 -141
  32. package/dist/native/iosnativeWebView/iosnativeWebView/BridgeMessageValidator.swift +0 -509
  33. package/dist/native/iosnativeWebView/iosnativeWebView/CacheManager.swift +0 -272
  34. package/dist/native/iosnativeWebView/iosnativeWebView/CameraTransportUtils.swift +0 -363
  35. package/dist/native/iosnativeWebView/iosnativeWebView/CatalystConstants.swift +0 -91
  36. package/dist/native/iosnativeWebView/iosnativeWebView/ConfigConstants.swift +0 -7
  37. package/dist/native/iosnativeWebView/iosnativeWebView/DeviceInfoUtils.swift +0 -75
  38. package/dist/native/iosnativeWebView/iosnativeWebView/FilePickerHandler.swift +0 -938
  39. package/dist/native/iosnativeWebView/iosnativeWebView/FrameworkServerUtils.swift +0 -821
  40. package/dist/native/iosnativeWebView/iosnativeWebView/ImageHandler.swift +0 -461
  41. package/dist/native/iosnativeWebView/iosnativeWebView/NativeBridge.swift +0 -185
  42. package/dist/native/iosnativeWebView/iosnativeWebView/ResourceURLProtocol.swift +0 -146
  43. package/dist/native/iosnativeWebView/iosnativeWebView/SplashView.swift +0 -91
  44. package/dist/native/iosnativeWebView/iosnativeWebView/SplashViewModel.swift +0 -86
  45. package/dist/native/iosnativeWebView/iosnativeWebView/URLWhitelistManager.swift +0 -238
  46. package/dist/native/iosnativeWebView/iosnativeWebView/WebView.swift +0 -151
  47. package/dist/native/iosnativeWebView/iosnativeWebView/WebViewModel.swift +0 -47
  48. package/dist/native/iosnativeWebView/iosnativeWebView/WebViewNavigationDelegate.swift +0 -237
package/changelog.md CHANGED
@@ -1,9 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.3-canary.19] - 2025-12-09
4
+
5
+ - Offline fallback support: packages `public/offline.html` into Android/iOS bundles and shows it automatically when connectivity drops (with retry handling)
6
+ - Notification refactor: streamlined config toggle for local vs push, centralized permission/asset handling, and simpler routing into the web app
7
+ - Access control: combined access control config for ios
8
+
9
+ ## [0.0.3-canary.18] - 2025-11-19
10
+
11
+ - Configuration Files
12
+ - Android Native WebView - MainActivity.kt
13
+ - Android Native WebView - CustomWebview.kt
14
+
15
+ ## [0.0.3-canary.17] - 2025-11-19
16
+
17
+ - โœจ Added complete notification system (local & push)
18
+ - ๐Ÿ”” New useNotification() React hook
19
+ - ๐Ÿ“ฑ Firebase Cloud Messaging conditional build support
20
+ - ๐Ÿค– Android notification handling in MainActivity
21
+ - ๐Ÿ”ง 5 new notification commands + 5 callbacks in NativeBridge
22
+ - ๐Ÿ“ฆ Auto-processing of notification icons & sounds
23
+ - โš™๏ธ Conditional dependencies based on notification config
24
+ - ๐Ÿงน Updated .gitignore & .npmignore
25
+ - ๐Ÿ“‹ Enhanced build process with automatic permission injection
26
+ - ๐Ÿ”— Deep link integration with notifications
27
+
3
28
  ## [0.0.3-canary.16] - 2025-11-19
4
- - FeaturesGallery Support for File Picker: Enhanced file picker to open native gallery for selecting images and videos
5
- - HTTPS Framework Server: Enabled HTTPS support for the framework server with self-signed SSL certificate
6
- - MIME Type Handling: Fixed and improved MIME type detection and handling in the file picker
29
+
30
+ - FeaturesGallery Support for File Picker: Enhanced file picker to open native gallery for selecting images and videos
31
+ - HTTPS Framework Server: Enabled HTTPS support for the framework server with self-signed SSL certificate
32
+ - MIME Type Handling: Fixed and improved MIME type detection and handling in the file picker
7
33
 
8
34
  ## [0.0.3-canary.15] - 2025-11-19
9
35
 
@@ -19,6 +19,7 @@ import io.yourname.androidproject.isUrlAllowed
19
19
  import io.yourname.androidproject.isExternalDomain
20
20
  import io.yourname.androidproject.matchesCachePattern
21
21
  import io.yourname.androidproject.utils.CameraUtils
22
+ import io.yourname.androidproject.utils.NetworkUtils
22
23
  import kotlinx.coroutines.*
23
24
  import java.io.FileNotFoundException
24
25
  import java.util.Properties
@@ -46,6 +47,10 @@ class CustomWebView(
46
47
  private lateinit var assetLoader: WebViewAssetLoader
47
48
  private var allowedUrls: List<String> = emptyList()
48
49
  private var accessControlEnabled: Boolean = false
50
+ private val offlineAssetPath = "offline/offline.html"
51
+ private val offlineAssetUrl = "file:///android_asset/$offlineAssetPath"
52
+ private var offlinePageVisible = false
53
+ private var lastTargetUrl: String? = null
49
54
 
50
55
  // Counters for asset loading statistics
51
56
  private var assetLoadAttempts = 0
@@ -123,9 +128,34 @@ class CustomWebView(
123
128
  }
124
129
 
125
130
  fun loadUrl(url: String) {
131
+ lastTargetUrl = url
132
+ offlinePageVisible = false
126
133
  webView.loadUrl(url)
127
134
  }
128
135
 
136
+ fun updateLastTargetUrl(url: String) {
137
+ lastTargetUrl = url
138
+ }
139
+
140
+ fun showOfflinePage() {
141
+ if (offlinePageVisible) {
142
+ return
143
+ }
144
+
145
+ // Verify offline asset exists to avoid crash
146
+ try {
147
+ context.assets.open(offlineAssetPath).close()
148
+ offlinePageVisible = true
149
+ webView.loadUrl(offlineAssetUrl)
150
+ if (BuildConfig.DEBUG) {
151
+ Log.d(TAG, "๐Ÿ“ด Showing offline page from assets: $offlineAssetPath")
152
+ }
153
+ } catch (e: Exception) {
154
+ Log.e(TAG, "โŒ offline.html not found in assets, cannot show offline page", e)
155
+ }
156
+ }
157
+
158
+
129
159
  fun saveState(outState: Bundle) {
130
160
  webView.saveState(outState)
131
161
  }
@@ -264,6 +294,27 @@ class CustomWebView(
264
294
  val uri = Uri.parse(url)
265
295
  val scheme = uri.scheme?.lowercase() ?: return false
266
296
 
297
+ // Handle offline retry scheme
298
+ if (scheme == "catalyst" && (uri.host?.lowercase() == "retry" || uri.schemeSpecificPart?.lowercase() == "retry")) {
299
+ val status = NetworkUtils.getCurrentStatus(context)
300
+ if (status.isOnline) {
301
+ val target = lastTargetUrl
302
+ if (target != null) {
303
+ if (BuildConfig.DEBUG) {
304
+ Log.d(TAG, "๐Ÿ”„ Retry requested, online. Reloading: $target")
305
+ }
306
+ offlinePageVisible = false
307
+ lastTargetUrl = target
308
+ webView.loadUrl(target)
309
+ } else if (BuildConfig.DEBUG) {
310
+ Log.w(TAG, "๐Ÿ”„ Retry requested but no target URL is known")
311
+ }
312
+ } else if (BuildConfig.DEBUG) {
313
+ Log.d(TAG, "๐Ÿ”„ Retry requested but still offline; staying on offline page")
314
+ }
315
+ return true
316
+ }
317
+
267
318
  // Only handle tel, mailto, sms
268
319
  if (scheme !in listOf("tel", "mailto", "sms")) {
269
320
  return false
@@ -573,6 +624,7 @@ class CustomWebView(
573
624
 
574
625
  // Let WebView handle loading non-API HTTP/HTTPS URLs
575
626
  if (url.scheme in listOf("http", "https")) {
627
+ lastTargetUrl = urlString
576
628
  return false
577
629
  }
578
630
  }
@@ -807,6 +859,9 @@ class CustomWebView(
807
859
 
808
860
  override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
809
861
  super.onPageStarted(view, url, favicon)
862
+ if (url != null && url != offlineAssetUrl) {
863
+ offlinePageVisible = false
864
+ }
810
865
  progressBar.visibility = View.VISIBLE
811
866
  val startTime = System.currentTimeMillis()
812
867
  view?.tag = startTime // Store start time for performance tracking
@@ -7,10 +7,13 @@ import android.view.WindowManager
7
7
  import androidx.activity.OnBackPressedCallback
8
8
  import androidx.appcompat.app.AppCompatActivity
9
9
 
10
+ import org.json.JSONObject
10
11
  import java.util.Properties
11
12
  import io.yourname.androidproject.databinding.ActivityMainBinding
12
13
  import io.yourname.androidproject.NativeBridge
14
+ import io.yourname.androidproject.utils.BridgeUtils
13
15
  import io.yourname.androidproject.utils.KeyboardUtil
16
+ import io.yourname.androidproject.utils.NetworkUtils
14
17
  import io.yourname.androidproject.utils.NotificationConstants
15
18
  import kotlinx.coroutines.CoroutineScope
16
19
  import kotlinx.coroutines.MainScope
@@ -139,14 +142,25 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
139
142
  val useHttps = properties.getProperty("useHttps", "false").toBoolean()
140
143
  val protocol = if (useHttps) "https" else "http"
141
144
  currentUrl = "$protocol://$local_ip:$port/$initial_url"
145
+ customWebView.updateLastTargetUrl(currentUrl)
142
146
 
143
147
 
144
148
  // Check for notification and handle via /notification endpoint
145
149
  if (intent.getBooleanExtra(NotificationConstants.EXTRA_IS_NOTIFICATION, false)) {
146
150
  handleNotificationClick(currentUrl, intent)
147
151
  } else {
148
- Log.d(TAG, "๐Ÿ”— Loading base URL: $currentUrl")
149
- customWebView.loadUrl(currentUrl)
152
+ val isOnline = NetworkUtils.getCurrentStatus(this).isOnline
153
+ if (isOnline) {
154
+ if (BuildConfig.DEBUG) {
155
+ Log.d(TAG, "๐Ÿ”— Loading base URL: $currentUrl")
156
+ }
157
+ customWebView.loadUrl(currentUrl)
158
+ } else {
159
+ if (BuildConfig.DEBUG) {
160
+ Log.w(TAG, "๐Ÿ“ด Device offline on launch, showing offline page")
161
+ }
162
+ customWebView.showOfflinePage()
163
+ }
150
164
  }
151
165
 
152
166
  metricsMonitor.markAppStartComplete()
@@ -230,18 +244,52 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
230
244
  try {
231
245
  val action = intent.getStringExtra(NotificationConstants.EXTRA_ACTION)
232
246
  val notificationData = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_DATA)
247
+ val notificationId = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_ID)
233
248
 
234
249
  Log.d(TAG, "๐Ÿ”” Handling notification click - Action: ${action ?: "none"}")
235
250
 
236
251
  // Dismiss the notification
237
252
  val notificationManager = getSystemService(android.content.Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
238
- notificationManager.cancelAll() // This dismisses all notifications from this app
253
+ if (!notificationId.isNullOrBlank()) {
254
+ notificationManager.cancel(notificationId.hashCode())
255
+ } else {
256
+ notificationManager.cancelAll() // Fallback if we didn't get an ID
257
+ }
239
258
 
240
259
  // Build simple /notification URL
241
260
  val url = buildNotificationUrl(baseUrl, action, notificationData)
242
261
  Log.d(TAG, "๐Ÿ”” Navigating to: $url")
243
262
 
244
- customWebView.loadUrl(url)
263
+ // Notify web layer about the action/tap
264
+ try {
265
+ val payload = org.json.JSONObject().apply {
266
+ put("action", action ?: "tap")
267
+ put("deepLinkURL", url)
268
+ if (!notificationData.isNullOrBlank()) {
269
+ put("data", org.json.JSONObject(notificationData))
270
+ }
271
+ if (!notificationId.isNullOrBlank()) {
272
+ put("notificationId", notificationId)
273
+ }
274
+ }
275
+ val webView = customWebView.getWebView()
276
+ if (action.isNullOrBlank()) {
277
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.NOTIFICATION_TAPPED, payload.toString())
278
+ } else {
279
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.NOTIFICATION_ACTION_PERFORMED, payload.toString())
280
+ }
281
+ } catch (e: Exception) {
282
+ Log.e(TAG, "Failed to notify web of notification action", e)
283
+ }
284
+
285
+ customWebView.updateLastTargetUrl(url)
286
+ val isOnline = NetworkUtils.getCurrentStatus(this).isOnline
287
+ if (isOnline) {
288
+ customWebView.loadUrl(url)
289
+ } else {
290
+ Log.w(TAG, "๐Ÿ“ด Offline during notification click, showing offline page")
291
+ customWebView.showOfflinePage()
292
+ }
245
293
 
246
294
  } catch (e: Exception) {
247
295
  Log.e(TAG, "๐Ÿ”” Error handling notification click: ${e.message}", e)
@@ -278,4 +326,4 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
278
326
  "$baseUrl/notification"
279
327
  }
280
328
  }
281
- }
329
+ }
@@ -104,6 +104,7 @@ class NativeBridge(
104
104
  private lateinit var permissionLauncher: ActivityResultLauncher<String>
105
105
  private lateinit var filePickerLauncher: ActivityResultLauncher<Intent>
106
106
  private var currentFilePickerOptions: FilePickerOptions = FilePickerOptions()
107
+ private var networkMonitor: NetworkMonitor? = null
107
108
 
108
109
  // Unified notification manager
109
110
  private val notificationManager = AppNotificationManager(mainActivity, mainActivity.properties)
@@ -272,6 +273,37 @@ class NativeBridge(
272
273
  }
273
274
  }
274
275
 
276
+ @JavascriptInterface
277
+ fun getNetworkStatus(param: String? = null) {
278
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.NETWORK_STATUS_CHANGED, "get network status") {
279
+ // Lazily start monitor on first request
280
+ if (networkMonitor == null) {
281
+ networkMonitor = NetworkMonitor(mainActivity) { status ->
282
+ mainActivity.runOnUiThread {
283
+ try {
284
+ val payload = JSONObject().apply {
285
+ put("online", status.isOnline)
286
+ status.transport?.let { put("type", it) }
287
+ }
288
+ BridgeUtils.notifyWebJson(webView, BridgeUtils.WebEvents.NETWORK_STATUS_CHANGED, payload)
289
+ } catch (e: Exception) {
290
+ BridgeUtils.logError(TAG, "Failed to send network status to web: ${e.message}")
291
+ }
292
+ }
293
+ }.also { it.start() }
294
+ } else {
295
+ val status = NetworkUtils.getCurrentStatus(mainActivity)
296
+ val payload = JSONObject().apply {
297
+ put("online", status.isOnline)
298
+ status.transport?.let { put("type", it) }
299
+ }
300
+ mainActivity.runOnUiThread {
301
+ BridgeUtils.notifyWebJson(webView, BridgeUtils.WebEvents.NETWORK_STATUS_CHANGED, payload)
302
+ }
303
+ }
304
+ }
305
+ }
306
+
275
307
  @JavascriptInterface
276
308
  fun getDeviceInfo(options: String?) {
277
309
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_DEVICE_INFO_ERROR, "get device info") {
@@ -287,11 +319,11 @@ class NativeBridge(
287
319
  fun scheduleLocalNotification(config: String?) {
288
320
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_SCHEDULED, "schedule local notification") {
289
321
  mainActivity.runOnUiThread {
290
- // Parse config with full NotificationConfig support
291
- val notificationConfig = if (config.isNullOrBlank()) {
292
- NotificationConfig(title = NotificationConstants.DEFAULT_NOTIFICATION_TITLE, body = NotificationConstants.DEFAULT_NOTIFICATION_BODY)
293
- } else {
294
- try {
322
+ try {
323
+ // Parse config with full NotificationConfig support
324
+ val notificationConfig = if (config.isNullOrBlank()) {
325
+ NotificationConfig(title = NotificationConstants.DEFAULT_NOTIFICATION_TITLE, body = NotificationConstants.DEFAULT_NOTIFICATION_BODY)
326
+ } else {
295
327
  val json = org.json.JSONObject(config)
296
328
 
297
329
  // Parse actions array with title and actionId
@@ -343,26 +375,43 @@ class NativeBridge(
343
375
  ongoing = json.optBoolean("ongoing", false),
344
376
  data = data
345
377
  )
346
- } catch (e: Exception) {
347
- BridgeUtils.logError(TAG, "Error parsing notification config", e)
348
- NotificationConfig(title = NotificationConstants.DEFAULT_NOTIFICATION_TITLE, body = NotificationConstants.DEFAULT_NOTIFICATION_BODY)
349
378
  }
379
+
380
+ val notificationId = notificationManager.scheduleLocal(notificationConfig)
381
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_SCHEDULED,
382
+ """{"notificationId": "$notificationId", "scheduled": true, "success": true}""")
383
+
384
+ // Emit received event
385
+ val payload = org.json.JSONObject().apply {
386
+ put("title", notificationConfig.title)
387
+ put("body", notificationConfig.body)
388
+ notificationConfig.data?.let { dataMap ->
389
+ put("data", org.json.JSONObject(dataMap))
390
+ }
391
+ put("foreground", false)
392
+ put("notificationId", notificationId)
393
+ }
394
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED, payload.toString())
395
+ } catch (e: Exception) {
396
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_SCHEDULED,
397
+ """{"notificationId": "", "scheduled": false, "success": false, "error": "Failed to schedule notification: ${e.message}"}""")
350
398
  }
351
-
352
- val notificationId = notificationManager.scheduleLocal(notificationConfig)
353
- BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_SCHEDULED,
354
- """{"notificationId": "$notificationId", "scheduled": true}""")
355
399
  }
356
400
  }
357
401
  }
358
402
 
359
403
  @JavascriptInterface
360
404
  fun cancelLocalNotification(notificationId: String?) {
361
- BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_SCHEDULED, "cancel local notification") {
405
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_CANCELLED, "cancel local notification") {
362
406
  mainActivity.runOnUiThread {
363
- val success = notificationManager.cancelLocal(notificationId)
364
- BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_SCHEDULED,
365
- """{"notificationId": "$notificationId", "cancelled": $success}""")
407
+ try {
408
+ val success = notificationManager.cancelLocal(notificationId)
409
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_CANCELLED,
410
+ """{"notificationId": "${notificationId ?: ""}", "success": $success}""")
411
+ } catch (e: Exception) {
412
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_CANCELLED,
413
+ """{"notificationId": "${notificationId ?: ""}", "success": false, "error": "Failed to cancel notification: ${e.message}"}""")
414
+ }
366
415
  }
367
416
  }
368
417
  }
@@ -386,11 +435,16 @@ class NativeBridge(
386
435
  launch {
387
436
  try {
388
437
  val token = notificationManager.initializePush()
389
- BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.PUSH_NOTIFICATION_TOKEN,
390
- """{"token": "$token", "registered": true}""")
438
+ if (token.isBlank()) {
439
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.PUSH_NOTIFICATION_TOKEN,
440
+ """{"success": false, "error": "Push registration returned empty token"}""")
441
+ } else {
442
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.PUSH_NOTIFICATION_TOKEN,
443
+ """{"token": "$token", "success": true}""")
444
+ }
391
445
  } catch (e: Exception) {
392
- BridgeUtils.notifyWebError(webView, BridgeUtils.WebEvents.PUSH_NOTIFICATION_TOKEN,
393
- "Push registration failed: ${e.message}")
446
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.PUSH_NOTIFICATION_TOKEN,
447
+ """{"success": false, "error": "Push registration failed: ${e.message}"}""")
394
448
  }
395
449
  }
396
450
  }
@@ -400,7 +454,7 @@ class NativeBridge(
400
454
 
401
455
  @JavascriptInterface
402
456
  fun subscribeToTopic(config: String?) {
403
- BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED, "subscribe to topic") {
457
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.TOPIC_SUBSCRIPTION_RESULT, "subscribe to topic") {
404
458
  mainActivity.runOnUiThread {
405
459
  launch {
406
460
  try {
@@ -408,18 +462,18 @@ class NativeBridge(
408
462
  val topic = json.optString("topic", "")
409
463
 
410
464
  if (topic.isBlank()) {
411
- BridgeUtils.notifyWebError(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED,
412
- "Topic name cannot be empty")
465
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.TOPIC_SUBSCRIPTION_RESULT,
466
+ """{"topic": "$topic", "success": false, "error": "Topic name cannot be empty", "action": "subscribe"}""")
413
467
  return@launch
414
468
  }
415
469
 
416
470
  val success = notificationManager.subscribeToTopic(topic)
417
- BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED,
418
- """{"topic": "$topic", "subscribed": $success}""")
471
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.TOPIC_SUBSCRIPTION_RESULT,
472
+ """{"topic": "$topic", "success": $success, "action": "subscribe"}""")
419
473
 
420
474
  } catch (e: Exception) {
421
- BridgeUtils.notifyWebError(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED,
422
- "Topic subscription failed: ${e.message}")
475
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.TOPIC_SUBSCRIPTION_RESULT,
476
+ """{"topic": "", "success": false, "error": "Topic subscription failed: ${e.message}", "action": "subscribe"}""")
423
477
  }
424
478
  }
425
479
  }
@@ -428,7 +482,7 @@ class NativeBridge(
428
482
 
429
483
  @JavascriptInterface
430
484
  fun unsubscribeFromTopic(config: String?) {
431
- BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED, "unsubscribe from topic") {
485
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.TOPIC_SUBSCRIPTION_RESULT, "unsubscribe from topic") {
432
486
  mainActivity.runOnUiThread {
433
487
  launch {
434
488
  try {
@@ -436,18 +490,18 @@ class NativeBridge(
436
490
  val topic = json.optString("topic", "")
437
491
 
438
492
  if (topic.isBlank()) {
439
- BridgeUtils.notifyWebError(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED,
440
- "Topic name cannot be empty")
493
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.TOPIC_SUBSCRIPTION_RESULT,
494
+ """{"topic": "$topic", "success": false, "error": "Topic name cannot be empty", "action": "unsubscribe"}""")
441
495
  return@launch
442
496
  }
443
497
 
444
498
  val success = notificationManager.unsubscribeFromTopic(topic)
445
- BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED,
446
- """{"topic": "$topic", "unsubscribed": $success}""")
499
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.TOPIC_SUBSCRIPTION_RESULT,
500
+ """{"topic": "$topic", "success": $success, "action": "unsubscribe"}""")
447
501
 
448
502
  } catch (e: Exception) {
449
- BridgeUtils.notifyWebError(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED,
450
- "Topic unsubscription failed: ${e.message}")
503
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.TOPIC_SUBSCRIPTION_RESULT,
504
+ """{"topic": "", "success": false, "error": "Topic unsubscription failed: ${e.message}", "action": "unsubscribe"}""")
451
505
  }
452
506
  }
453
507
  }
@@ -456,17 +510,17 @@ class NativeBridge(
456
510
 
457
511
  @JavascriptInterface
458
512
  fun getSubscribedTopics(config: String?) {
459
- BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED, "get subscribed topics") {
513
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.SUBSCRIBED_TOPICS_RESULT, "get subscribed topics") {
460
514
  mainActivity.runOnUiThread {
461
515
  try {
462
516
  val topics = notificationManager.getSubscribedTopics()
463
517
  val topicsArray = org.json.JSONArray(topics.toList())
464
- BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED,
465
- """{"topics": $topicsArray}""")
518
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.SUBSCRIBED_TOPICS_RESULT,
519
+ """{"topics": $topicsArray, "success": true}""")
466
520
 
467
521
  } catch (e: Exception) {
468
- BridgeUtils.notifyWebError(webView, BridgeUtils.WebEvents.NOTIFICATION_RECEIVED,
469
- "Failed to get subscribed topics: ${e.message}")
522
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.SUBSCRIBED_TOPICS_RESULT,
523
+ """{"topics": [], "success": false, "error": "Failed to get subscribed topics: ${e.message}"}""")
470
524
  }
471
525
  }
472
526
  }
@@ -773,6 +827,10 @@ class NativeBridge(
773
827
  // Cleanup NotificationManager
774
828
  notificationManager.cleanup()
775
829
 
830
+ // Stop network monitoring
831
+ networkMonitor?.stop()
832
+ networkMonitor = null
833
+
776
834
  // Stop FrameworkServer
777
835
  FrameworkServerUtils.stopServer()
778
836
 
@@ -57,9 +57,16 @@ object BridgeUtils {
57
57
  // Notification events
58
58
  NOTIFICATION_PERMISSION_STATUS("NOTIFICATION_PERMISSION_STATUS"),
59
59
  LOCAL_NOTIFICATION_SCHEDULED("LOCAL_NOTIFICATION_SCHEDULED"),
60
+ LOCAL_NOTIFICATION_CANCELLED("LOCAL_NOTIFICATION_CANCELLED"),
60
61
  PUSH_NOTIFICATION_TOKEN("PUSH_NOTIFICATION_TOKEN"),
61
62
  NOTIFICATION_RECEIVED("NOTIFICATION_RECEIVED"),
62
63
  NOTIFICATION_ACTION_PERFORMED("NOTIFICATION_ACTION_PERFORMED"),
64
+ NOTIFICATION_TAPPED("NOTIFICATION_TAPPED"),
65
+ TOPIC_SUBSCRIPTION_RESULT("TOPIC_SUBSCRIPTION_RESULT"),
66
+ SUBSCRIBED_TOPICS_RESULT("SUBSCRIBED_TOPICS_RESULT"),
67
+
68
+ // Network events
69
+ NETWORK_STATUS_CHANGED("NETWORK_STATUS_CHANGED"),
63
70
  }
64
71
 
65
72
  /**
@@ -226,4 +233,4 @@ object BridgeUtils {
226
233
  fun logWarning(tag: String, message: String) {
227
234
  Log.w(tag, message)
228
235
  }
229
- }
236
+ }
@@ -0,0 +1,105 @@
1
+ package io.yourname.androidproject.utils
2
+
3
+ import android.content.Context
4
+ import android.net.ConnectivityManager
5
+ import android.net.Network
6
+ import android.net.NetworkCapabilities
7
+ import android.net.NetworkRequest
8
+ import android.os.Build
9
+
10
+ data class NetworkStatus(
11
+ val isOnline: Boolean,
12
+ val transport: String? = null
13
+ )
14
+
15
+ object NetworkUtils {
16
+ private val transportLabels = mapOf(
17
+ NetworkCapabilities.TRANSPORT_WIFI to "wifi",
18
+ NetworkCapabilities.TRANSPORT_CELLULAR to "cellular",
19
+ NetworkCapabilities.TRANSPORT_ETHERNET to "ethernet",
20
+ NetworkCapabilities.TRANSPORT_BLUETOOTH to "bluetooth"
21
+ )
22
+
23
+ private fun resolveTransport(capabilities: NetworkCapabilities?): String? {
24
+ if (capabilities == null) return null
25
+ val transport = transportLabels.keys.firstOrNull { capabilities.hasTransport(it) }
26
+ return transport?.let { transportLabels[it] } ?: "unknown"
27
+ }
28
+
29
+ fun getCurrentStatus(context: Context): NetworkStatus {
30
+ val connectivityManager =
31
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
32
+
33
+ val activeNetwork = connectivityManager.activeNetwork
34
+ val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
35
+
36
+ val isValidated = capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true
37
+ val isConnected = capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
38
+ val transport = resolveTransport(capabilities)
39
+
40
+ // Fallback for older APIs or missing capabilities
41
+ val legacyConnected = connectivityManager.activeNetworkInfo?.isConnectedOrConnecting == true
42
+
43
+ val online = isValidated || isConnected || legacyConnected
44
+ return NetworkStatus(isOnline = online, transport = transport)
45
+ }
46
+ }
47
+
48
+ class NetworkMonitor(
49
+ context: Context,
50
+ private val onStatusChanged: (NetworkStatus) -> Unit
51
+ ) {
52
+ private val appContext = context.applicationContext
53
+ private val connectivityManager =
54
+ appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
55
+ private var callback: ConnectivityManager.NetworkCallback? = null
56
+
57
+ fun start() {
58
+ if (callback != null) return
59
+
60
+ // Emit current state immediately
61
+ onStatusChanged(NetworkUtils.getCurrentStatus(appContext))
62
+
63
+ val request = NetworkRequest.Builder()
64
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
65
+ .build()
66
+
67
+ val networkCallback = object : ConnectivityManager.NetworkCallback() {
68
+ override fun onAvailable(network: Network) {
69
+ onStatusChanged(NetworkUtils.getCurrentStatus(appContext))
70
+ }
71
+
72
+ override fun onLost(network: Network) {
73
+ onStatusChanged(NetworkUtils.getCurrentStatus(appContext))
74
+ }
75
+
76
+ override fun onUnavailable() {
77
+ onStatusChanged(NetworkUtils.getCurrentStatus(appContext))
78
+ }
79
+ }
80
+
81
+ callback = networkCallback
82
+
83
+ try {
84
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
85
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
86
+ } else {
87
+ connectivityManager.registerNetworkCallback(request, networkCallback)
88
+ }
89
+ } catch (_: Exception) {
90
+ // Swallow registration errors to avoid crashes; hook users will fall back to navigator.onLine
91
+ callback = null
92
+ }
93
+ }
94
+
95
+ fun stop() {
96
+ callback?.let {
97
+ try {
98
+ connectivityManager.unregisterNetworkCallback(it)
99
+ } catch (_: Exception) {
100
+ // Ignore unregister errors
101
+ }
102
+ }
103
+ callback = null
104
+ }
105
+ }
@@ -24,4 +24,5 @@ object NotificationConstants {
24
24
  const val EXTRA_IS_NOTIFICATION = "is_notification"
25
25
  const val EXTRA_ACTION = "action"
26
26
  const val EXTRA_NOTIFICATION_DATA = "notification_data"
27
- }
27
+ const val EXTRA_NOTIFICATION_ID = "notification_id"
28
+ }