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.
- package/changelog.md +9 -0
- package/dist/native/androidProject/app/build.gradle.kts +23 -1
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +4 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +21 -18
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +19 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +308 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/URLWhitelistManager.kt +231 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/Utils.kt +0 -71
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +12 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +19 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DownloadUtils.kt +4 -3
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +286 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +296 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +594 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +466 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +187 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +304 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +424 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +469 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/URLWhitelistManagerTest.kt +462 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +391 -0
- package/dist/native/androidProject/gradle/libs.versions.toml +5 -1
- package/dist/native/bridge/constants/NativeInterfaces.js +10 -5
- package/dist/native/bridge/errors.js +1 -1
- package/dist/native/bridge/hooks.js +33 -3
- package/dist/native/bridge/useBaseHook.js +3 -5
- package/dist/native/bridge/utils/NativeBridge.js +16 -1
- package/dist/native/buildAppIos.js +11 -8
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -6
- package/dist/native/iosnativeWebView/Package.resolved +0 -126
- package/dist/native/iosnativeWebView/Package.swift +13 -2
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +1 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +12 -2
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +47 -5
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ScreenSecureManager.swift +131 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/URLWhitelistManager.swift +173 -187
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +347 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +18 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -1
- package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +13 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +4 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +10 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +76 -7
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +1 -127
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/xcshareddata/xcschemes/iosnativeWebView.xcscheme +9 -4
- package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +46 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +236 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +418 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +390 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +299 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +383 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +381 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +581 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NotificationHandlerTests.swift +430 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/URLWhitelistManagerTests.swift +367 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +452 -0
- package/dist/scripts/loadEnvironmentVariables.js +3 -1
- 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")
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -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.
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
670
|
-
if (
|
|
671
|
-
|
|
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
|
-
|
|
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
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt
CHANGED
|
@@ -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
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt
CHANGED
|
@@ -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") {
|