catalyst-core 0.1.0-canary.3 → 0.1.0-canary.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 (59) hide show
  1. package/changelog.md +9 -0
  2. package/dist/native/androidProject/app/build.gradle.kts +23 -1
  3. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +4 -1
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +21 -18
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +19 -0
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +308 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/URLWhitelistManager.kt +231 -0
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/Utils.kt +0 -71
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +12 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +19 -1
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DownloadUtils.kt +4 -3
  13. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +286 -0
  14. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +296 -0
  15. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +594 -0
  16. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +466 -0
  17. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +187 -0
  18. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +304 -0
  19. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +424 -0
  20. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +469 -0
  21. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/URLWhitelistManagerTest.kt +462 -0
  22. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +391 -0
  23. package/dist/native/androidProject/gradle/libs.versions.toml +5 -1
  24. package/dist/native/bridge/constants/NativeInterfaces.js +10 -5
  25. package/dist/native/bridge/errors.js +1 -1
  26. package/dist/native/bridge/hooks.js +33 -3
  27. package/dist/native/bridge/useBaseHook.js +3 -5
  28. package/dist/native/bridge/utils/NativeBridge.js +16 -1
  29. package/dist/native/buildAppIos.js +11 -8
  30. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -6
  31. package/dist/native/iosnativeWebView/Package.resolved +0 -126
  32. package/dist/native/iosnativeWebView/Package.swift +13 -2
  33. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +1 -1
  34. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +12 -2
  35. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +47 -5
  36. package/dist/native/iosnativeWebView/Sources/Core/Utils/ScreenSecureManager.swift +131 -0
  37. package/dist/native/iosnativeWebView/Sources/Core/Utils/URLWhitelistManager.swift +173 -187
  38. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +347 -0
  39. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +18 -0
  40. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -1
  41. package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +13 -0
  42. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +4 -0
  43. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +10 -0
  44. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +76 -7
  45. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +1 -127
  46. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/xcshareddata/xcschemes/iosnativeWebView.xcscheme +9 -4
  47. package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +46 -0
  48. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +236 -0
  49. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +418 -0
  50. package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +390 -0
  51. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +299 -0
  52. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +383 -0
  53. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +381 -0
  54. package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +581 -0
  55. package/dist/native/iosnativeWebView/iosnativeWebViewTests/NotificationHandlerTests.swift +430 -0
  56. package/dist/native/iosnativeWebView/iosnativeWebViewTests/URLWhitelistManagerTests.swift +367 -0
  57. package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +452 -0
  58. package/dist/scripts/loadEnvironmentVariables.js +3 -1
  59. package/package.json +1 -1
package/changelog.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0-canary.4] - 2026-02-12
4
+
5
+ - Added Google Sign-In support for both Android and iOS in Catalyst, enabling a unified native authentication experience for apps built on the framework.
6
+ - Improved release/build reliability with related bridge and CI updates, making integration smoother and more consistent across platforms.
7
+
8
+ ## [0.1.0-canary.3] - 2026-02-04
9
+
10
+ - Android device security: added root, emulator, and Frida detection with launch-time checks and a Material Design 3 bottom sheet threat alert UI
11
+
3
12
  ## [0.1.0-canary.2] - 2026-01-28
4
13
 
5
14
  - Adds mono-repository support for catalyst
@@ -15,6 +15,18 @@ val keystorePassword: String? by project.properties // Changed from keyStorePas
15
15
  val keyAlias: String? by project.properties
16
16
  val keyPassword: String? by project.properties
17
17
 
18
+ fun isAllowBackupEnabled(): Boolean {
19
+ return try {
20
+ val propsFile = File("${project.projectDir}/src/main/assets/webview_config.properties")
21
+ if (!propsFile.exists()) return false
22
+ val props = Properties()
23
+ props.load(propsFile.inputStream())
24
+ props.getProperty("android.security.allowBackup", "false").trim().lowercase() == "true"
25
+ } catch (e: Exception) {
26
+ false
27
+ }
28
+ }
29
+
18
30
  fun isNotificationsEnabled(): Boolean {
19
31
  return try {
20
32
  if (configPath == null) return false
@@ -56,6 +68,7 @@ android {
56
68
  versionName = "1.1"
57
69
  testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
58
70
  buildConfigField("String", "LOCAL_IP", "\"${getLocalIpAddress()}\"")
71
+ manifestPlaceholders["allowBackup"] = isAllowBackupEnabled()
59
72
  }
60
73
 
61
74
  // Add signing configuration for app bundle
@@ -148,6 +161,13 @@ android {
148
161
  abortOnError = true
149
162
  }
150
163
 
164
+ // Configure test options for unit tests
165
+ testOptions {
166
+ unitTests {
167
+ isReturnDefaultValues = true
168
+ }
169
+ }
170
+
151
171
  // Conditional source sets based on notifications config
152
172
  sourceSets {
153
173
  getByName("main") {
@@ -171,8 +191,10 @@ dependencies {
171
191
  androidTestImplementation(libs.androidx.junit)
172
192
  androidTestImplementation(libs.androidx.espresso.core)
173
193
  implementation(libs.androidx.webkit)
194
+ implementation(libs.androidx.credentials)
195
+ implementation(libs.androidx.credentials.play.services.auth)
196
+ implementation(libs.googleid)
174
197
  implementation("org.json:json:20231013")
175
-
176
198
  // Ktor Server dependencies for FrameworkServer (~200KB total)
177
199
  implementation("io.ktor:ktor-server-core:3.0.3")
178
200
  implementation("io.ktor:ktor-server-netty:3.0.3")
@@ -12,7 +12,7 @@
12
12
  </intent>
13
13
  </queries>
14
14
  <application
15
- android:allowBackup="true"
15
+ android:allowBackup="${allowBackup}"
16
16
  android:dataExtractionRules="@xml/data_extraction_rules"
17
17
  android:fullBackupContent="@xml/backup_rules"
18
18
  android:hardwareAccelerated="true"
@@ -70,7 +70,10 @@ object CatalystConstants {
70
70
  "logger",
71
71
  "pickFile",
72
72
  "openFileWithIntent",
73
- "requestHapticFeedback"
73
+ "requestHapticFeedback",
74
+ "setScreenSecure",
75
+ "getScreenSecure",
76
+ "clearWebData"
74
77
  )
75
78
  }
76
79
 
@@ -15,8 +15,7 @@ import androidx.webkit.WebResourceRequestCompat
15
15
  import android.widget.ProgressBar
16
16
  import androidx.webkit.WebViewAssetLoader
17
17
  import io.yourname.androidproject.WebCacheManager
18
- import io.yourname.androidproject.isUrlAllowed
19
- import io.yourname.androidproject.isExternalDomain
18
+ import io.yourname.androidproject.URLWhitelistManager
20
19
  import io.yourname.androidproject.matchesCachePattern
21
20
  import io.yourname.androidproject.utils.CameraUtils
22
21
  import io.yourname.androidproject.utils.NetworkUtils
@@ -100,6 +99,9 @@ class CustomWebView(
100
99
  .getProperty("accessControl.enabled", "true")
101
100
  .equals("true", ignoreCase = true)
102
101
 
102
+ // Initialize URLWhitelistManager with access control configuration
103
+ URLWhitelistManager.initialize(accessControlEnabled, allowedUrls)
104
+
103
105
  // Set initial flags based on buildOptimisation
104
106
  if (buildOptimisation) {
105
107
  isInitialApiCalled = false
@@ -187,6 +189,10 @@ class CustomWebView(
187
189
  webView.clearHistory()
188
190
  }
189
191
 
192
+ fun clearAllCache() {
193
+ cacheManager.clearAll()
194
+ }
195
+
190
196
  fun canGoBack(): Boolean = webView.canGoBack()
191
197
 
192
198
  fun goBack() {
@@ -480,7 +486,7 @@ class CustomWebView(
480
486
  }
481
487
 
482
488
  // Check if URL is allowed
483
- if (accessControlEnabled && !isUrlAllowed(url, allowedUrls)) {
489
+ if (!URLWhitelistManager.isUrlAllowed(url)) {
484
490
  if (BuildConfig.DEBUG) {
485
491
  Log.w(TAG, "🙫 SERVICE_WORKER: Request blocked by access control: $url")
486
492
  }
@@ -624,7 +630,7 @@ class CustomWebView(
624
630
  }
625
631
 
626
632
  // Check if URL is an external domain
627
- if (url.scheme in listOf("http", "https") && isExternalDomain(urlString, allowedUrls)) {
633
+ if (url.scheme in listOf("http", "https") && URLWhitelistManager.isExternalDomain(urlString)) {
628
634
  if (BuildConfig.DEBUG) {
629
635
  Log.d(TAG, "🌍 External domain detected, opening in in-app browser: $urlString")
630
636
  }
@@ -633,7 +639,7 @@ class CustomWebView(
633
639
  }
634
640
 
635
641
  // Check if URL is allowed for internal navigation
636
- if (accessControlEnabled && !isUrlAllowed(urlString, allowedUrls)) {
642
+ if (!URLWhitelistManager.isUrlAllowed(urlString)) {
637
643
  if (BuildConfig.DEBUG) {
638
644
  Log.w(TAG, "🚫 URL blocked by access control: $urlString")
639
645
  }
@@ -666,21 +672,18 @@ class CustomWebView(
666
672
  Log.d(TAG, "🔄 INTERCEPT: isInitialPageLoaded: $isInitialPageLoaded")
667
673
  }
668
674
 
669
- if (accessControlEnabled) {
670
- if (!isUrlAllowed(url, allowedUrls)) {
671
- if (BuildConfig.DEBUG) {
672
- Log.w(TAG, "🚫 STEP_1_FAIL: Network request blocked by access control: $url")
673
- Log.w(TAG, "🚫 STEP_1_FAIL: Allowed URLs: $allowedUrls")
674
- }
675
- // Return an empty response to block the request
676
- return WebResourceResponse("text/plain", "utf-8", null)
677
- } else {
678
- if (BuildConfig.DEBUG) {
679
- Log.d(TAG, "✅ STEP_1_PASS: URL allowed by access control: $url")
680
- }
675
+ if (!URLWhitelistManager.isUrlAllowed(url)) {
676
+ if (BuildConfig.DEBUG) {
677
+ Log.w(TAG, "🚫 STEP_1_FAIL: Network request blocked by access control: $url")
681
678
  }
679
+ // Return an empty response to block the request
680
+ return WebResourceResponse("text/plain", "utf-8", null)
682
681
  } else if (BuildConfig.DEBUG) {
683
- Log.d(TAG, "⚙️ STEP_1_SKIP: Access control disabled, allowing URL: $url")
682
+ if (URLWhitelistManager.isAccessControlEnabled()) {
683
+ Log.d(TAG, "✅ STEP_1_PASS: URL allowed by access control: $url")
684
+ } else {
685
+ Log.d(TAG, "⚙️ STEP_1_SKIP: Access control disabled, allowing URL: $url")
686
+ }
684
687
  }
685
688
 
686
689
  // TODO: Add HTML file workflow - index.html not available yet
@@ -141,6 +141,25 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
141
141
  // Public method for NativeBridge to get current safe area insets
142
142
  fun getCurrentSafeAreaInsets(): SafeAreaInsets = latestSafeAreaInsets
143
143
 
144
+ fun clearWebCache() {
145
+ customWebView.clearAllCache()
146
+ }
147
+
148
+ fun setScreenSecure(enable: Boolean) {
149
+ if (enable) {
150
+ window.setFlags(
151
+ WindowManager.LayoutParams.FLAG_SECURE,
152
+ WindowManager.LayoutParams.FLAG_SECURE
153
+ )
154
+ } else {
155
+ window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
156
+ }
157
+ }
158
+
159
+ fun isScreenSecure(): Boolean {
160
+ return (window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0
161
+ }
162
+
144
163
  /**
145
164
  * Notify WebView when safe area insets are updated
146
165
  * Called when deferred inset calculation completes
@@ -10,12 +10,19 @@ import android.webkit.WebView
10
10
  import androidx.activity.result.ActivityResultLauncher
11
11
  import androidx.activity.result.contract.ActivityResultContracts
12
12
  import androidx.core.app.NotificationCompat
13
+ import androidx.credentials.CredentialManager
14
+ import androidx.credentials.GetCredentialRequest
15
+ import androidx.credentials.exceptions.GetCredentialCancellationException
16
+ import androidx.credentials.exceptions.GetCredentialException
17
+ import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
18
+ import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
13
19
  import io.yourname.androidproject.MainActivity
14
20
  import io.yourname.androidproject.utils.*
15
21
  import kotlinx.coroutines.*
16
22
  import org.json.JSONArray
17
23
  import org.json.JSONObject
18
24
  import java.util.Properties
25
+ import java.util.concurrent.atomic.AtomicBoolean
19
26
 
20
27
  private data class FilePickerOptions(
21
28
  val mimeType: String = "*/*",
@@ -86,6 +93,38 @@ private fun JSONObject.optIntNullable(key: String): Int? =
86
93
  private fun JSONObject.optLongNullable(key: String): Long? =
87
94
  if (has(key) && !isNull(key)) getLong(key) else null
88
95
 
96
+ private data class GoogleSignInOptions(
97
+ val clientId: String,
98
+ val nonce: String? = null,
99
+ val autoSelect: Boolean = false,
100
+ val filterByAuthorizedAccounts: Boolean = false
101
+ ) {
102
+ companion object {
103
+ fun fromRaw(raw: String?): GoogleSignInOptions {
104
+ if (raw.isNullOrBlank()) {
105
+ // Allow fallback to config-supplied clientId
106
+ return GoogleSignInOptions(clientId = "")
107
+ }
108
+
109
+ return try {
110
+ val json = JSONObject(raw)
111
+ val resolvedClientId = json.optString("clientId")
112
+ .ifBlank { json.optString("webClientId") }
113
+
114
+ GoogleSignInOptions(
115
+ clientId = resolvedClientId,
116
+ nonce = json.optString("nonce").takeIf { it.isNotBlank() },
117
+ autoSelect = json.optBoolean("autoSelect", false),
118
+ filterByAuthorizedAccounts = json.optBoolean("filterByAuthorizedAccounts", false)
119
+ )
120
+ } catch (e: Exception) {
121
+ BridgeUtils.logDebug("NativeBridge", "Failed to parse options JSON: ${e.message}")
122
+ GoogleSignInOptions(clientId = raw.trim())
123
+ }
124
+ }
125
+ }
126
+ }
127
+
89
128
  class NativeBridge(
90
129
  private val mainActivity: MainActivity,
91
130
  private val webView: WebView,
@@ -104,6 +143,12 @@ class NativeBridge(
104
143
  private lateinit var permissionLauncher: ActivityResultLauncher<String>
105
144
  private lateinit var filePickerLauncher: ActivityResultLauncher<Intent>
106
145
  private var currentFilePickerOptions: FilePickerOptions = FilePickerOptions()
146
+ private val isGoogleSignInInProgress = AtomicBoolean(false)
147
+ private val credentialManager: CredentialManager by lazy { CredentialManager.create(mainActivity) }
148
+ private val isGoogleSignInEnabled: Boolean by lazy {
149
+ properties.getProperty("googleSignIn.enabled", "false").toBoolean()
150
+ }
151
+
107
152
  private var networkMonitor: NetworkMonitor? = null
108
153
 
109
154
  // Unified notification manager
@@ -111,6 +156,7 @@ class NativeBridge(
111
156
 
112
157
  companion object {
113
158
  private const val TAG = "NativeBridge"
159
+ private const val GOOGLE_SIGN_IN_SAFE_EXECUTE_ACTION = "start Google Sign-In"
114
160
 
115
161
  /**
116
162
  * Parse and validate bridge message
@@ -237,6 +283,55 @@ class NativeBridge(
237
283
  }
238
284
  }
239
285
 
286
+ @JavascriptInterface
287
+ fun googleSignIn(optionsRaw: String?) {
288
+ BridgeUtils.safeExecute(
289
+ webView,
290
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
291
+ GOOGLE_SIGN_IN_SAFE_EXECUTE_ACTION
292
+ ) {
293
+ if (!isGoogleSignInEnabled) {
294
+ BridgeUtils.notifyWebError(
295
+ webView,
296
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
297
+ "Google Sign-In is disabled in config"
298
+ )
299
+ return@safeExecute
300
+ }
301
+
302
+ val options = GoogleSignInOptions.fromRaw(optionsRaw)
303
+
304
+ val resolvedClientId = when {
305
+ options.clientId.isNotBlank() -> options.clientId
306
+ !properties.getProperty("googleSignIn.clientId", "").isNullOrBlank() ->
307
+ properties.getProperty("googleSignIn.clientId", "")
308
+ else -> ""
309
+ }
310
+
311
+ if (resolvedClientId.isBlank()) {
312
+ BridgeUtils.notifyWebError(
313
+ webView,
314
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
315
+ "clientId is required for Google Sign-In"
316
+ )
317
+ return@safeExecute
318
+ }
319
+
320
+ if (!isGoogleSignInInProgress.compareAndSet(false, true)) {
321
+ BridgeUtils.notifyWebError(
322
+ webView,
323
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
324
+ "A Google Sign-In request is already in progress"
325
+ )
326
+ return@safeExecute
327
+ }
328
+
329
+ BridgeUtils.logDebug(TAG, "Google Sign-In using clientId prefix: ${resolvedClientId.take(12)}...")
330
+
331
+ launchGoogleSignIn(options.copy(clientId = resolvedClientId))
332
+ }
333
+ }
334
+
240
335
  @JavascriptInterface
241
336
  fun openFileWithIntent(params: String?) {
242
337
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_INTENT_ERROR, "open file with intent") {
@@ -626,6 +721,115 @@ class NativeBridge(
626
721
  }
627
722
  }
628
723
  }
724
+
725
+ private fun launchGoogleSignIn(options: GoogleSignInOptions) {
726
+ launch {
727
+ try {
728
+ val googleOptionBuilder = GetSignInWithGoogleOption.Builder(options.clientId)
729
+ options.nonce?.let { googleOptionBuilder.setNonce(it) }
730
+ val googleOption = googleOptionBuilder.build()
731
+
732
+ val request = GetCredentialRequest.Builder()
733
+ .addCredentialOption(googleOption)
734
+ .build()
735
+
736
+ BridgeUtils.logDebug(
737
+ TAG,
738
+ "Starting Google Sign-In via CredentialManager (hasNonce=${options.nonce != null})"
739
+ )
740
+
741
+ val response = credentialManager.getCredential(mainActivity, request)
742
+ handleCredentialResponse(response)
743
+ } catch (e: GetCredentialCancellationException) {
744
+ isGoogleSignInInProgress.set(false)
745
+ BridgeUtils.logDebug(
746
+ TAG,
747
+ "Google Sign-In cancelled: ${e.message} :: ${e.javaClass.simpleName}"
748
+ )
749
+ BridgeUtils.notifyWeb(
750
+ webView,
751
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_CANCELLED,
752
+ "User cancelled Google sign-in"
753
+ )
754
+ } catch (e: GetCredentialException) {
755
+ isGoogleSignInInProgress.set(false)
756
+ val message = e.errorMessage?.toString() ?: e.message ?: "Credential request failed"
757
+ BridgeUtils.logError(
758
+ TAG,
759
+ "Google Sign-In credential error: $message :: ${e.javaClass.simpleName}",
760
+ e
761
+ )
762
+ BridgeUtils.notifyWebError(
763
+ webView,
764
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
765
+ message
766
+ )
767
+ } catch (e: Exception) {
768
+ isGoogleSignInInProgress.set(false)
769
+ BridgeUtils.logError(
770
+ TAG,
771
+ "Google Sign-In unexpected error: ${e.message} :: ${e.javaClass.simpleName}",
772
+ e
773
+ )
774
+ BridgeUtils.notifyWebError(
775
+ webView,
776
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
777
+ "Error preparing Google sign-in: ${e.message}"
778
+ )
779
+ }
780
+ }
781
+ }
782
+
783
+ private fun handleCredentialResponse(response: androidx.credentials.GetCredentialResponse) {
784
+ try {
785
+ val credential = response.credential
786
+ if (credential.type != GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
787
+ BridgeUtils.logError(TAG, "Unexpected credential type: ${credential.type}")
788
+ isGoogleSignInInProgress.set(false)
789
+ BridgeUtils.notifyWebError(
790
+ webView,
791
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
792
+ "Unsupported credential type returned"
793
+ )
794
+ return
795
+ }
796
+
797
+ val googleCredential = GoogleIdTokenCredential.createFrom(credential.data)
798
+ val idToken = googleCredential.idToken
799
+
800
+ if (idToken.isNullOrBlank()) {
801
+ isGoogleSignInInProgress.set(false)
802
+ BridgeUtils.notifyWebError(
803
+ webView,
804
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
805
+ "No ID token returned from Google sign-in"
806
+ )
807
+ return
808
+ }
809
+
810
+ val payload = JSONObject().apply {
811
+ put("idToken", idToken)
812
+ googleCredential.id?.let { put("email", it) }
813
+ googleCredential.displayName?.let { put("name", it) }
814
+ googleCredential.profilePictureUri?.toString()?.let { put("photoUrl", it) }
815
+ put("provider", "google")
816
+ }
817
+
818
+ isGoogleSignInInProgress.set(false)
819
+ BridgeUtils.notifyWebJson(
820
+ webView,
821
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_SUCCESS,
822
+ payload
823
+ )
824
+ } catch (e: Exception) {
825
+ isGoogleSignInInProgress.set(false)
826
+ BridgeUtils.notifyWebError(
827
+ webView,
828
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
829
+ "Error handling Google credential: ${e.message}"
830
+ )
831
+ }
832
+ }
629
833
 
630
834
  private fun processSelectedFiles(uris: List<Uri>, options: FilePickerOptions) {
631
835
  launch {
@@ -825,6 +1029,110 @@ class NativeBridge(
825
1029
  }
826
1030
  }
827
1031
 
1032
+ @JavascriptInterface
1033
+ fun setScreenSecure(params: String?) {
1034
+ mainActivity.runOnUiThread {
1035
+ try {
1036
+ val enable = try {
1037
+ org.json.JSONObject(params ?: "{}").optBoolean("enable", true)
1038
+ } catch (e: Exception) {
1039
+ true
1040
+ }
1041
+ mainActivity.setScreenSecure(enable)
1042
+ BridgeUtils.notifyWeb(
1043
+ webView,
1044
+ BridgeUtils.WebEvents.ON_SCREEN_SECURE_SET,
1045
+ """{"secure": $enable, "success": true}"""
1046
+ )
1047
+ } catch (e: Exception) {
1048
+ // Use notifyWebJson to prevent JSON injection from e.message
1049
+ BridgeUtils.notifyWebJson(
1050
+ webView,
1051
+ BridgeUtils.WebEvents.ON_SCREEN_SECURE_ERROR,
1052
+ org.json.JSONObject().apply {
1053
+ put("success", false)
1054
+ put("error", e.message ?: "setScreenSecure failed")
1055
+ }
1056
+ )
1057
+ }
1058
+ }
1059
+ }
1060
+
1061
+ @JavascriptInterface
1062
+ fun getScreenSecure(params: String?) {
1063
+ mainActivity.runOnUiThread {
1064
+ try {
1065
+ val isSecure = mainActivity.isScreenSecure()
1066
+ BridgeUtils.notifyWebJson(
1067
+ webView,
1068
+ BridgeUtils.WebEvents.ON_SCREEN_SECURE_STATUS,
1069
+ org.json.JSONObject().apply {
1070
+ put("secure", isSecure)
1071
+ put("success", true)
1072
+ }
1073
+ )
1074
+ } catch (e: Exception) {
1075
+ BridgeUtils.notifyWebJson(
1076
+ webView,
1077
+ BridgeUtils.WebEvents.ON_SCREEN_SECURE_ERROR,
1078
+ org.json.JSONObject().apply {
1079
+ put("success", false)
1080
+ put("error", e.message ?: "getScreenSecure failed")
1081
+ }
1082
+ )
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ @JavascriptInterface
1088
+ fun clearWebData(params: String?) {
1089
+ mainActivity.runOnUiThread {
1090
+ try {
1091
+ webView.clearCache(true)
1092
+ webView.clearHistory()
1093
+ android.webkit.WebStorage.getInstance().deleteAllData()
1094
+ mainActivity.clearWebCache()
1095
+ // CookieManager.removeAllCookies is async — fire success only from its callback
1096
+ android.webkit.CookieManager.getInstance().removeAllCookies { cookiesRemoved ->
1097
+ android.webkit.CookieManager.getInstance().flush()
1098
+ mainActivity.runOnUiThread {
1099
+ try {
1100
+ // Use notifyWebJson to prevent JSON injection; cookiesRemoved is a
1101
+ // Boolean so string interpolation is safe, but keep consistent pattern
1102
+ BridgeUtils.notifyWebJson(
1103
+ webView,
1104
+ BridgeUtils.WebEvents.ON_WEB_DATA_CLEARED,
1105
+ org.json.JSONObject().apply {
1106
+ put("success", true)
1107
+ put("cookiesRemoved", cookiesRemoved)
1108
+ }
1109
+ )
1110
+ } catch (e: Exception) {
1111
+ BridgeUtils.notifyWebJson(
1112
+ webView,
1113
+ BridgeUtils.WebEvents.ON_WEB_DATA_CLEAR_ERROR,
1114
+ org.json.JSONObject().apply {
1115
+ put("success", false)
1116
+ put("error", e.message ?: "clearWebData cookie flush failed")
1117
+ }
1118
+ )
1119
+ }
1120
+ }
1121
+ }
1122
+ } catch (e: Exception) {
1123
+ // Use notifyWebJson to prevent JSON injection from e.message
1124
+ BridgeUtils.notifyWebJson(
1125
+ webView,
1126
+ BridgeUtils.WebEvents.ON_WEB_DATA_CLEAR_ERROR,
1127
+ org.json.JSONObject().apply {
1128
+ put("success", false)
1129
+ put("error", e.message ?: "clearWebData failed")
1130
+ }
1131
+ )
1132
+ }
1133
+ }
1134
+ }
1135
+
828
1136
  @JavascriptInterface
829
1137
  fun getSafeArea(data: String? = null) {
830
1138
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_SAFE_AREA_INSETS_UPDATED, "get safe area") {