catalyst-core 0.1.0-canary.2 → 0.1.0-canary.4
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 +14 -4
- package/dist/native/androidProject/app/proguard-rules.pro +25 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +10 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +204 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/design/DesignTokens.kt +307 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/EmulatorDetector.kt +203 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +187 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertHandler.kt +170 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertUI.kt +366 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityBottomSheet.kt +138 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckManager.kt +511 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckScheduler.kt +68 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +12 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DeviceInfoUtils.kt +12 -0
- package/dist/native/androidProject/app/src/main/res/values/themes.xml +12 -0
- package/dist/native/androidProject/gradle/libs.versions.toml +5 -1
- package/dist/native/bridge/constants/NativeInterfaces.js +6 -3
- package/dist/native/bridge/errors.js +1 -1
- package/dist/native/bridge/hooks.js +6 -2
- package/dist/native/bridge/utils/NativeBridge.js +5 -1
- package/dist/native/buildAppIos.js +11 -8
- package/dist/native/iosnativeWebView/Package.swift +5 -2
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +1 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +3 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +7 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +264 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +18 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +2 -0
- 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/package.json +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/LegacyGoogleSignInPlugin.kt +0 -176
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/README.md +0 -97
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
|
|
@@ -114,12 +114,12 @@ android {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
compileOptions {
|
|
117
|
-
sourceCompatibility = JavaVersion.
|
|
118
|
-
targetCompatibility = JavaVersion.
|
|
117
|
+
sourceCompatibility = JavaVersion.VERSION_11
|
|
118
|
+
targetCompatibility = JavaVersion.VERSION_11
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
kotlinOptions {
|
|
122
|
-
jvmTarget = "
|
|
122
|
+
jvmTarget = "11"
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
buildFeatures {
|
|
@@ -136,6 +136,10 @@ android {
|
|
|
136
136
|
excludes.add("META-INF/INDEX.LIST")
|
|
137
137
|
excludes.add("META-INF/io.netty.versions.properties")
|
|
138
138
|
}
|
|
139
|
+
jniLibs {
|
|
140
|
+
// Enable 16KB page size alignment for all native libraries
|
|
141
|
+
useLegacyPackaging = false
|
|
142
|
+
}
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
// Configure lint options
|
|
@@ -167,14 +171,20 @@ dependencies {
|
|
|
167
171
|
androidTestImplementation(libs.androidx.junit)
|
|
168
172
|
androidTestImplementation(libs.androidx.espresso.core)
|
|
169
173
|
implementation(libs.androidx.webkit)
|
|
174
|
+
implementation(libs.androidx.credentials)
|
|
175
|
+
implementation(libs.androidx.credentials.play.services.auth)
|
|
176
|
+
implementation(libs.googleid)
|
|
170
177
|
implementation("org.json:json:20231013")
|
|
171
|
-
|
|
172
178
|
// Ktor Server dependencies for FrameworkServer (~200KB total)
|
|
173
179
|
implementation("io.ktor:ktor-server-core:3.0.3")
|
|
174
180
|
implementation("io.ktor:ktor-server-netty:3.0.3")
|
|
175
181
|
implementation("io.ktor:ktor-server-content-negotiation:3.0.3")
|
|
176
182
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
|
177
183
|
|
|
184
|
+
// Security detection dependencies
|
|
185
|
+
implementation("com.scottyab:rootbeer-lib:0.1.1") // Root detection
|
|
186
|
+
implementation("com.google.android.play:integrity:1.3.0") // Play Integrity API
|
|
187
|
+
|
|
178
188
|
// SLF4J simple logger for Ktor (optional, can be excluded if needed)
|
|
179
189
|
implementation("org.slf4j:slf4j-simple:2.0.9")
|
|
180
190
|
|
|
@@ -59,6 +59,31 @@
|
|
|
59
59
|
-keep class io.yourname.androidproject.WebCacheManager$CacheEntry { *; }
|
|
60
60
|
-keep class io.yourname.androidproject.WebCacheManager$CacheMetadata { *; }
|
|
61
61
|
|
|
62
|
+
# Keep RootBeer library for root detection
|
|
63
|
+
-keep class com.scottyab.rootbeer.** { *; }
|
|
64
|
+
-dontwarn com.scottyab.rootbeer.**
|
|
65
|
+
|
|
66
|
+
# Security: keep only the public API surface that MainActivity wires up.
|
|
67
|
+
# Detection internals (EmulatorDetector, FridaDetector) and UI classes
|
|
68
|
+
# (SecurityBottomSheet, SecurityAlertUI) are intentionally obfuscated.
|
|
69
|
+
-keep class io.yourname.androidproject.security.SecurityCheckScheduler {
|
|
70
|
+
public static void initialize(android.content.Context, kotlinx.coroutines.CoroutineScope, io.yourname.androidproject.security.SecurityCheckScheduler$SecurityCheckCallback);
|
|
71
|
+
}
|
|
72
|
+
-keep interface io.yourname.androidproject.security.SecurityCheckScheduler$SecurityCheckCallback {
|
|
73
|
+
public void onSecurityCheckComplete(org.json.JSONObject);
|
|
74
|
+
}
|
|
75
|
+
-keep class io.yourname.androidproject.security.SecurityCheckManager {
|
|
76
|
+
public static org.json.JSONObject getLatestSecurityResults(android.content.Context);
|
|
77
|
+
public static java.lang.String getSecurityMode(android.content.Context);
|
|
78
|
+
}
|
|
79
|
+
-keep class io.yourname.androidproject.security.SecurityAlertHandler {
|
|
80
|
+
public static void handleSecurityCheckResults(android.app.Activity, org.json.JSONObject);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Keep Play Integrity API
|
|
84
|
+
-keep class com.google.android.play.core.integrity.** { *; }
|
|
85
|
+
-dontwarn com.google.android.play.core.integrity.**
|
|
86
|
+
|
|
62
87
|
# Remove debug logging in release builds
|
|
63
88
|
-assumenosideeffects class android.util.Log {
|
|
64
89
|
public static *** d(...);
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt
CHANGED
|
@@ -20,6 +20,7 @@ import io.yourname.androidproject.utils.NetworkUtils
|
|
|
20
20
|
import io.yourname.androidproject.utils.NotificationConstants
|
|
21
21
|
import io.yourname.androidproject.utils.SafeAreaInsets
|
|
22
22
|
import io.yourname.androidproject.utils.SafeAreaUtils
|
|
23
|
+
import io.yourname.androidproject.security.SecurityCheckScheduler
|
|
23
24
|
import kotlinx.coroutines.CoroutineScope
|
|
24
25
|
import kotlinx.coroutines.MainScope
|
|
25
26
|
import kotlinx.coroutines.cancelChildren
|
|
@@ -253,6 +254,15 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
253
254
|
|
|
254
255
|
configureEdgeToEdge()
|
|
255
256
|
|
|
257
|
+
// Initialize security checks (release builds only)
|
|
258
|
+
if (!BuildConfig.DEBUG) {
|
|
259
|
+
SecurityCheckScheduler.initialize(this, this, object : SecurityCheckScheduler.SecurityCheckCallback {
|
|
260
|
+
override fun onSecurityCheckComplete(results: JSONObject) {
|
|
261
|
+
io.yourname.androidproject.security.SecurityAlertHandler.handleSecurityCheckResults(this@MainActivity, results)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
256
266
|
// Initialize MetricsMonitor
|
|
257
267
|
metricsMonitor = MetricsMonitor.getInstance(this)
|
|
258
268
|
|
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 {
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
package io.yourname.androidproject.design
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import androidx.annotation.ColorInt
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Global design tokens for the application.
|
|
9
|
+
* Follows Material Design 3 principles with support for light/dark themes.
|
|
10
|
+
*
|
|
11
|
+
* All spacing follows 8dp grid system.
|
|
12
|
+
* Color system supports dynamic theming based on system configuration.
|
|
13
|
+
*/
|
|
14
|
+
object DesignTokens {
|
|
15
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// COLORS - Light Theme
|
|
18
|
+
// ============================================================
|
|
19
|
+
object LightColors {
|
|
20
|
+
// Surface colors
|
|
21
|
+
@ColorInt const val SURFACE = 0xFFFFFFFF.toInt() // Pure white
|
|
22
|
+
@ColorInt const val SURFACE_VARIANT = 0xFFF5F5F5.toInt() // Light gray background
|
|
23
|
+
|
|
24
|
+
// Error colors (for alerts, warnings)
|
|
25
|
+
@ColorInt const val ERROR = 0xFFB3261E.toInt() // Material error red
|
|
26
|
+
@ColorInt const val ERROR_CONTAINER = 0xFFF9DEDC.toInt() // Light red background
|
|
27
|
+
@ColorInt const val ON_ERROR = 0xFFFFFFFF.toInt() // White text on error
|
|
28
|
+
@ColorInt const val ON_ERROR_CONTAINER = 0xFF410E0B.toInt() // Dark red text
|
|
29
|
+
|
|
30
|
+
// Text colors
|
|
31
|
+
@ColorInt const val ON_SURFACE = 0xFF1C1B1F.toInt() // Primary text (near black)
|
|
32
|
+
@ColorInt const val ON_SURFACE_VARIANT = 0xFF49454F.toInt() // Secondary text (gray)
|
|
33
|
+
@ColorInt const val OUTLINE = 0xFF79747E.toInt() // Dividers, borders
|
|
34
|
+
|
|
35
|
+
// Accent colors
|
|
36
|
+
@ColorInt const val PRIMARY = 0xFF6750A4.toInt() // Material purple
|
|
37
|
+
@ColorInt const val ON_PRIMARY = 0xFFFFFFFF.toInt() // White text on primary
|
|
38
|
+
@ColorInt const val PRIMARY_CONTAINER = 0xFFEADDFF.toInt() // Light purple background
|
|
39
|
+
@ColorInt const val ON_PRIMARY_CONTAINER = 0xFF21005D.toInt() // Dark purple text
|
|
40
|
+
|
|
41
|
+
// Secondary colors
|
|
42
|
+
@ColorInt const val SECONDARY = 0xFF625B71.toInt() // Material gray-purple
|
|
43
|
+
@ColorInt const val ON_SECONDARY = 0xFFFFFFFF.toInt() // White text on secondary
|
|
44
|
+
@ColorInt const val SECONDARY_CONTAINER = 0xFFE8DEF8.toInt() // Light secondary background
|
|
45
|
+
@ColorInt const val ON_SECONDARY_CONTAINER = 0xFF1D192B.toInt() // Dark text
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================================
|
|
49
|
+
// COLORS - Dark Theme
|
|
50
|
+
// ============================================================
|
|
51
|
+
object DarkColors {
|
|
52
|
+
// Surface colors
|
|
53
|
+
@ColorInt const val SURFACE = 0xFF1C1B1F.toInt() // Dark surface
|
|
54
|
+
@ColorInt const val SURFACE_VARIANT = 0xFF2B2930.toInt() // Darker variant
|
|
55
|
+
|
|
56
|
+
// Error colors (for alerts, warnings)
|
|
57
|
+
@ColorInt const val ERROR = 0xFFF2B8B5.toInt() // Light red for dark mode
|
|
58
|
+
@ColorInt const val ERROR_CONTAINER = 0xFF8C1D18.toInt() // Dark red background
|
|
59
|
+
@ColorInt const val ON_ERROR = 0xFF601410.toInt() // Dark text on light error
|
|
60
|
+
@ColorInt const val ON_ERROR_CONTAINER = 0xFFF2B8B5.toInt() // Light text on dark error
|
|
61
|
+
|
|
62
|
+
// Text colors
|
|
63
|
+
@ColorInt const val ON_SURFACE = 0xFFE6E1E5.toInt() // Light text
|
|
64
|
+
@ColorInt const val ON_SURFACE_VARIANT = 0xFFCAC4D0.toInt() // Secondary light text
|
|
65
|
+
@ColorInt const val OUTLINE = 0xFF938F99.toInt() // Light dividers, borders
|
|
66
|
+
|
|
67
|
+
// Accent colors
|
|
68
|
+
@ColorInt const val PRIMARY = 0xFFD0BCFF.toInt() // Light purple for dark mode
|
|
69
|
+
@ColorInt const val ON_PRIMARY = 0xFF381E72.toInt() // Dark text on light primary
|
|
70
|
+
@ColorInt const val PRIMARY_CONTAINER = 0xFF4F378B.toInt() // Dark purple background
|
|
71
|
+
@ColorInt const val ON_PRIMARY_CONTAINER = 0xFFEADDFF.toInt() // Light text
|
|
72
|
+
|
|
73
|
+
// Secondary colors
|
|
74
|
+
@ColorInt const val SECONDARY = 0xFFCCC2DC.toInt() // Light gray-purple for dark
|
|
75
|
+
@ColorInt const val ON_SECONDARY = 0xFF332D41.toInt() // Dark text on light secondary
|
|
76
|
+
@ColorInt const val SECONDARY_CONTAINER = 0xFF4A4458.toInt() // Dark secondary background
|
|
77
|
+
@ColorInt const val ON_SECONDARY_CONTAINER = 0xFFE8DEF8.toInt() // Light text
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================
|
|
81
|
+
// SPACING - Following 8dp Grid System
|
|
82
|
+
// ============================================================
|
|
83
|
+
object Spacing {
|
|
84
|
+
const val SPACE_XXS = 4 // 4dp - Minimal spacing
|
|
85
|
+
const val SPACE_XS = 8 // 8dp - Tight spacing
|
|
86
|
+
const val SPACE_SM = 12 // 12dp - Small spacing
|
|
87
|
+
const val SPACE_MD = 16 // 16dp - Medium spacing (default)
|
|
88
|
+
const val SPACE_LG = 24 // 24dp - Large spacing
|
|
89
|
+
const val SPACE_XL = 32 // 32dp - Extra large spacing
|
|
90
|
+
const val SPACE_XXL = 48 // 48dp - Maximum spacing
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================
|
|
94
|
+
// DIMENSIONS
|
|
95
|
+
// ============================================================
|
|
96
|
+
object Dimensions {
|
|
97
|
+
// Corner radius
|
|
98
|
+
const val CORNER_RADIUS_SM = 8 // Small corners (buttons, chips)
|
|
99
|
+
const val CORNER_RADIUS_MD = 16 // Medium corners (cards)
|
|
100
|
+
const val CORNER_RADIUS_LG = 24 // Large corners (bottom sheet top)
|
|
101
|
+
const val CORNER_RADIUS_XL = 28 // Extra large (full rounded)
|
|
102
|
+
|
|
103
|
+
// Icon sizes
|
|
104
|
+
const val ICON_SIZE_SM = 24 // Small icons
|
|
105
|
+
const val ICON_SIZE_MD = 48 // Medium icons
|
|
106
|
+
const val ICON_SIZE_LG = 64 // Large icons (hero icon)
|
|
107
|
+
const val ICON_SIZE_XL = 96 // Extra large icons
|
|
108
|
+
|
|
109
|
+
// Minimum touch target (Material Design)
|
|
110
|
+
const val MIN_TOUCH_TARGET = 48 // 48dp minimum for accessibility
|
|
111
|
+
|
|
112
|
+
// Elevation (for shadows/depth)
|
|
113
|
+
const val ELEVATION_NONE = 0f
|
|
114
|
+
const val ELEVATION_SM = 2f // Small elevation
|
|
115
|
+
const val ELEVATION_MD = 4f // Medium elevation
|
|
116
|
+
const val ELEVATION_LG = 8f // Large elevation
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================
|
|
120
|
+
// TYPOGRAPHY
|
|
121
|
+
// ============================================================
|
|
122
|
+
object Typography {
|
|
123
|
+
// Text sizes (SP - scalable pixels)
|
|
124
|
+
const val TEXT_SIZE_DISPLAY = 32f // Large headlines
|
|
125
|
+
const val TEXT_SIZE_HEADLINE = 24f // Section headlines
|
|
126
|
+
const val TEXT_SIZE_TITLE = 20f // Card titles
|
|
127
|
+
const val TEXT_SIZE_BODY_LG = 16f // Large body text
|
|
128
|
+
const val TEXT_SIZE_BODY = 14f // Default body text
|
|
129
|
+
const val TEXT_SIZE_LABEL = 12f // Small labels, captions
|
|
130
|
+
|
|
131
|
+
// Line height multipliers
|
|
132
|
+
const val LINE_HEIGHT_TIGHT = 1.2f // Tight line height
|
|
133
|
+
const val LINE_HEIGHT_NORMAL = 1.5f // Normal line height
|
|
134
|
+
const val LINE_HEIGHT_RELAXED = 1.7f // Relaxed line height
|
|
135
|
+
|
|
136
|
+
// Font weights (Android Typeface)
|
|
137
|
+
const val FONT_WEIGHT_NORMAL = android.graphics.Typeface.NORMAL
|
|
138
|
+
const val FONT_WEIGHT_BOLD = android.graphics.Typeface.BOLD
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================
|
|
142
|
+
// ANIMATION DURATIONS (milliseconds)
|
|
143
|
+
// ============================================================
|
|
144
|
+
object Animation {
|
|
145
|
+
const val DURATION_SHORT = 200L // Quick transitions
|
|
146
|
+
const val DURATION_MEDIUM = 300L // Standard transitions
|
|
147
|
+
const val DURATION_LONG = 500L // Emphasized transitions
|
|
148
|
+
const val DURATION_EXTRA_LONG = 700L // Hero transitions
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================
|
|
152
|
+
// OPACITY/ALPHA VALUES
|
|
153
|
+
// ============================================================
|
|
154
|
+
object Opacity {
|
|
155
|
+
const val DISABLED = 0.38f // Disabled state
|
|
156
|
+
const val MEDIUM = 0.60f // Medium emphasis
|
|
157
|
+
const val HIGH = 0.87f // High emphasis
|
|
158
|
+
const val FULL = 1.0f // Full opacity
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================
|
|
162
|
+
// HELPER FUNCTIONS
|
|
163
|
+
// ============================================================
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if the device is in dark mode.
|
|
167
|
+
*
|
|
168
|
+
* @param context Android context
|
|
169
|
+
* @return true if dark mode is enabled
|
|
170
|
+
*/
|
|
171
|
+
fun isDarkMode(context: Context): Boolean {
|
|
172
|
+
val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
|
173
|
+
return nightModeFlags == Configuration.UI_MODE_NIGHT_YES
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get surface color based on current theme.
|
|
178
|
+
*/
|
|
179
|
+
@ColorInt
|
|
180
|
+
fun getSurfaceColor(context: Context): Int {
|
|
181
|
+
return if (isDarkMode(context)) DarkColors.SURFACE else LightColors.SURFACE
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get surface variant color based on current theme.
|
|
186
|
+
*/
|
|
187
|
+
@ColorInt
|
|
188
|
+
fun getSurfaceVariantColor(context: Context): Int {
|
|
189
|
+
return if (isDarkMode(context)) DarkColors.SURFACE_VARIANT else LightColors.SURFACE_VARIANT
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get error color based on current theme.
|
|
194
|
+
*/
|
|
195
|
+
@ColorInt
|
|
196
|
+
fun getErrorColor(context: Context): Int {
|
|
197
|
+
return if (isDarkMode(context)) DarkColors.ERROR else LightColors.ERROR
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get error container color based on current theme.
|
|
202
|
+
*/
|
|
203
|
+
@ColorInt
|
|
204
|
+
fun getErrorContainerColor(context: Context): Int {
|
|
205
|
+
return if (isDarkMode(context)) DarkColors.ERROR_CONTAINER else LightColors.ERROR_CONTAINER
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get on-error color (text on error background) based on current theme.
|
|
210
|
+
*/
|
|
211
|
+
@ColorInt
|
|
212
|
+
fun getOnErrorColor(context: Context): Int {
|
|
213
|
+
return if (isDarkMode(context)) DarkColors.ON_ERROR else LightColors.ON_ERROR
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get on-error-container color based on current theme.
|
|
218
|
+
*/
|
|
219
|
+
@ColorInt
|
|
220
|
+
fun getOnErrorContainerColor(context: Context): Int {
|
|
221
|
+
return if (isDarkMode(context)) DarkColors.ON_ERROR_CONTAINER else LightColors.ON_ERROR_CONTAINER
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get primary text color based on current theme.
|
|
226
|
+
*/
|
|
227
|
+
@ColorInt
|
|
228
|
+
fun getOnSurfaceColor(context: Context): Int {
|
|
229
|
+
return if (isDarkMode(context)) DarkColors.ON_SURFACE else LightColors.ON_SURFACE
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get secondary text color based on current theme.
|
|
234
|
+
*/
|
|
235
|
+
@ColorInt
|
|
236
|
+
fun getOnSurfaceVariantColor(context: Context): Int {
|
|
237
|
+
return if (isDarkMode(context)) DarkColors.ON_SURFACE_VARIANT else LightColors.ON_SURFACE_VARIANT
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get outline color (dividers, borders) based on current theme.
|
|
242
|
+
*/
|
|
243
|
+
@ColorInt
|
|
244
|
+
fun getOutlineColor(context: Context): Int {
|
|
245
|
+
return if (isDarkMode(context)) DarkColors.OUTLINE else LightColors.OUTLINE
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get primary color based on current theme.
|
|
250
|
+
*/
|
|
251
|
+
@ColorInt
|
|
252
|
+
fun getPrimaryColor(context: Context): Int {
|
|
253
|
+
return if (isDarkMode(context)) DarkColors.PRIMARY else LightColors.PRIMARY
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get on-primary color (text on primary background) based on current theme.
|
|
258
|
+
*/
|
|
259
|
+
@ColorInt
|
|
260
|
+
fun getOnPrimaryColor(context: Context): Int {
|
|
261
|
+
return if (isDarkMode(context)) DarkColors.ON_PRIMARY else LightColors.ON_PRIMARY
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get primary container color based on current theme.
|
|
266
|
+
*/
|
|
267
|
+
@ColorInt
|
|
268
|
+
fun getPrimaryContainerColor(context: Context): Int {
|
|
269
|
+
return if (isDarkMode(context)) DarkColors.PRIMARY_CONTAINER else LightColors.PRIMARY_CONTAINER
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get on-primary-container color based on current theme.
|
|
274
|
+
*/
|
|
275
|
+
@ColorInt
|
|
276
|
+
fun getOnPrimaryContainerColor(context: Context): Int {
|
|
277
|
+
return if (isDarkMode(context)) DarkColors.ON_PRIMARY_CONTAINER else LightColors.ON_PRIMARY_CONTAINER
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get secondary color based on current theme.
|
|
282
|
+
*/
|
|
283
|
+
@ColorInt
|
|
284
|
+
fun getSecondaryColor(context: Context): Int {
|
|
285
|
+
return if (isDarkMode(context)) DarkColors.SECONDARY else LightColors.SECONDARY
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get on-secondary color based on current theme.
|
|
290
|
+
*/
|
|
291
|
+
@ColorInt
|
|
292
|
+
fun getOnSecondaryColor(context: Context): Int {
|
|
293
|
+
return if (isDarkMode(context)) DarkColors.ON_SECONDARY else LightColors.ON_SECONDARY
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Convert DP to pixels.
|
|
298
|
+
*
|
|
299
|
+
* @param context Android context
|
|
300
|
+
* @param dp Value in density-independent pixels
|
|
301
|
+
* @return Value in pixels
|
|
302
|
+
*/
|
|
303
|
+
fun dpToPx(context: Context, dp: Int): Int {
|
|
304
|
+
return (dp * context.resources.displayMetrics.density).toInt()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
}
|