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.
- package/changelog.md +29 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +55 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +53 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +97 -39
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +8 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NetworkUtils.kt +105 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationConstants.kt +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationUtils.kt +7 -5
- package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +2 -7
- package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +39 -18
- package/dist/native/bridge/constants/NativeInterfaces.js +5 -4
- package/dist/native/bridge/hooks.js +7 -4
- package/dist/native/bridge/utils/NativeBridge.js +4 -0
- package/dist/native/buildAppAndroid.js +3 -2
- package/dist/native/buildAppIos.js +3 -3
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/NetworkMonitor.swift +112 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +6 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +31 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +14 -7
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +105 -7
- package/package.json +1 -1
- package/dist/native/androidProject/app/.editorconfig +0 -17
- package/dist/native/androidProject/app/detekt-config.yml +0 -562
- package/dist/native/androidProject/app/src/main/assets/webview_config.properties +0 -30
- package/dist/native/androidProject/splashscreen_fallback_original.avif +0 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/BootTimingUtility.swift +0 -26
- package/dist/native/iosnativeWebView/iosnativeWebView/BridgeCommandHandler.swift +0 -312
- package/dist/native/iosnativeWebView/iosnativeWebView/BridgeDelegateHandler.swift +0 -166
- package/dist/native/iosnativeWebView/iosnativeWebView/BridgeFileHandler.swift +0 -340
- package/dist/native/iosnativeWebView/iosnativeWebView/BridgeJavaScriptInterface.swift +0 -141
- package/dist/native/iosnativeWebView/iosnativeWebView/BridgeMessageValidator.swift +0 -509
- package/dist/native/iosnativeWebView/iosnativeWebView/CacheManager.swift +0 -272
- package/dist/native/iosnativeWebView/iosnativeWebView/CameraTransportUtils.swift +0 -363
- package/dist/native/iosnativeWebView/iosnativeWebView/CatalystConstants.swift +0 -91
- package/dist/native/iosnativeWebView/iosnativeWebView/ConfigConstants.swift +0 -7
- package/dist/native/iosnativeWebView/iosnativeWebView/DeviceInfoUtils.swift +0 -75
- package/dist/native/iosnativeWebView/iosnativeWebView/FilePickerHandler.swift +0 -938
- package/dist/native/iosnativeWebView/iosnativeWebView/FrameworkServerUtils.swift +0 -821
- package/dist/native/iosnativeWebView/iosnativeWebView/ImageHandler.swift +0 -461
- package/dist/native/iosnativeWebView/iosnativeWebView/NativeBridge.swift +0 -185
- package/dist/native/iosnativeWebView/iosnativeWebView/ResourceURLProtocol.swift +0 -146
- package/dist/native/iosnativeWebView/iosnativeWebView/SplashView.swift +0 -91
- package/dist/native/iosnativeWebView/iosnativeWebView/SplashViewModel.swift +0 -86
- package/dist/native/iosnativeWebView/iosnativeWebView/URLWhitelistManager.swift +0 -238
- package/dist/native/iosnativeWebView/iosnativeWebView/WebView.swift +0 -151
- package/dist/native/iosnativeWebView/iosnativeWebView/WebViewModel.swift +0 -47
- 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
|
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
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
|
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -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
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt
CHANGED
|
@@ -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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt
CHANGED
|
@@ -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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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.
|
|
405
|
+
BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.LOCAL_NOTIFICATION_CANCELLED, "cancel local notification") {
|
|
362
406
|
mainActivity.runOnUiThread {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
418
|
-
"""{"topic": "$topic", "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
446
|
-
"""{"topic": "$topic", "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt
CHANGED
|
@@ -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
|
+
}
|