catalyst-core 0.1.0-canary.3 → 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 +3 -1
- 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/utils/BridgeUtils.kt +12 -1
- 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/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
|
|
@@ -171,8 +171,10 @@ dependencies {
|
|
|
171
171
|
androidTestImplementation(libs.androidx.junit)
|
|
172
172
|
androidTestImplementation(libs.androidx.espresso.core)
|
|
173
173
|
implementation(libs.androidx.webkit)
|
|
174
|
+
implementation(libs.androidx.credentials)
|
|
175
|
+
implementation(libs.androidx.credentials.play.services.auth)
|
|
176
|
+
implementation(libs.googleid)
|
|
174
177
|
implementation("org.json:json:20231013")
|
|
175
|
-
|
|
176
178
|
// Ktor Server dependencies for FrameworkServer (~200KB total)
|
|
177
179
|
implementation("io.ktor:ktor-server-core:3.0.3")
|
|
178
180
|
implementation("io.ktor:ktor-server-netty:3.0.3")
|
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 {
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt
CHANGED
|
@@ -4,6 +4,7 @@ import android.util.Log
|
|
|
4
4
|
import android.webkit.WebView
|
|
5
5
|
import io.yourname.androidproject.CatalystConstants
|
|
6
6
|
import org.json.JSONObject
|
|
7
|
+
import android.os.Looper
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Common utilities for native bridge operations
|
|
@@ -65,9 +66,13 @@ object BridgeUtils {
|
|
|
65
66
|
TOPIC_SUBSCRIPTION_RESULT("TOPIC_SUBSCRIPTION_RESULT"),
|
|
66
67
|
SUBSCRIBED_TOPICS_RESULT("SUBSCRIBED_TOPICS_RESULT"),
|
|
67
68
|
|
|
69
|
+
// Auth events
|
|
70
|
+
ON_GOOGLE_SIGN_IN_SUCCESS("ON_GOOGLE_SIGN_IN_SUCCESS"),
|
|
71
|
+
ON_GOOGLE_SIGN_IN_ERROR("ON_GOOGLE_SIGN_IN_ERROR"),
|
|
72
|
+
ON_GOOGLE_SIGN_IN_CANCELLED("ON_GOOGLE_SIGN_IN_CANCELLED"),
|
|
73
|
+
|
|
68
74
|
// Network events
|
|
69
75
|
NETWORK_STATUS_CHANGED("NETWORK_STATUS_CHANGED"),
|
|
70
|
-
|
|
71
76
|
// Safe area events
|
|
72
77
|
ON_SAFE_AREA_INSETS_UPDATED("ON_SAFE_AREA_INSETS_UPDATED"),
|
|
73
78
|
}
|
|
@@ -80,6 +85,12 @@ object BridgeUtils {
|
|
|
80
85
|
* @param data The data to send (can be null)
|
|
81
86
|
*/
|
|
82
87
|
fun notifyWeb(webView: WebView, event: WebEvents, data: String? = null) {
|
|
88
|
+
// Ensure WebView interactions happen on the main thread
|
|
89
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
90
|
+
webView.post { notifyWeb(webView, event, data) }
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
try {
|
|
84
95
|
val safeData = data?.replace("'", "\\'") ?: ""
|
|
85
96
|
val jsCode = if (data != null) {
|
|
@@ -10,6 +10,8 @@ material = "1.12.0"
|
|
|
10
10
|
constraintlayout = "2.1.4"
|
|
11
11
|
webkit = "1.13.0"
|
|
12
12
|
googleServices = "4.4.0"
|
|
13
|
+
androidxCredentials = "1.3.0"
|
|
14
|
+
googleId = "1.1.1"
|
|
13
15
|
|
|
14
16
|
[libraries]
|
|
15
17
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
|
@@ -20,9 +22,11 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
|
|
|
20
22
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
|
21
23
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
|
22
24
|
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
|
|
25
|
+
androidx-credentials = { group = "androidx.credentials", name = "credentials", version.ref = "androidxCredentials" }
|
|
26
|
+
androidx-credentials-play-services-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "androidxCredentials" }
|
|
27
|
+
googleid = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "googleId" }
|
|
23
28
|
|
|
24
29
|
[plugins]
|
|
25
30
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
|
26
31
|
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
|
27
32
|
google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }
|
|
28
|
-
|
|
@@ -6,7 +6,9 @@ OPEN_CAMERA:"openCamera",REQUEST_CAMERA_PERMISSION:"requestCameraPermission",//
|
|
|
6
6
|
PICK_FILE:"pickFile",OPEN_FILE_WITH_INTENT:"openFileWithIntent",// Haptic feedback commands
|
|
7
7
|
REQUEST_HAPTIC_FEEDBACK:"requestHapticFeedback",// Notification commands
|
|
8
8
|
SCHEDULE_LOCAL_NOTIFICATION:"scheduleLocalNotification",CANCEL_LOCAL_NOTIFICATION:"cancelLocalNotification",REQUEST_NOTIFICATION_PERMISSION:"requestNotificationPermission",REGISTER_FOR_PUSH_NOTIFICATIONS:"registerForPushNotifications",UPDATE_BADGE_COUNT:"updateBadgeCount",SUBSCRIBE_TO_TOPIC:"subscribeToTopic",UNSUBSCRIBE_FROM_TOPIC:"unsubscribeFromTopic",GET_SUBSCRIBED_TOPICS:"getSubscribedTopics",// Device info commands
|
|
9
|
-
GET_DEVICE_INFO:"getDeviceInfo"
|
|
9
|
+
GET_DEVICE_INFO:"getDeviceInfo",// Network commands
|
|
10
|
+
GET_NETWORK_STATUS:"getNetworkStatus",// Auth commands
|
|
11
|
+
GOOGLE_SIGN_IN:"googleSignIn",// Safe area commands
|
|
10
12
|
GET_SAFE_AREA:"getSafeArea"};// Callback Interfaces: Native → Web (incoming events/callbacks)
|
|
11
13
|
const NATIVE_CALLBACKS=exports.NATIVE_CALLBACKS={// Camera callbacks
|
|
12
14
|
CAMERA_PERMISSION_STATUS:"CAMERA_PERMISSION_STATUS",ON_CAMERA_CAPTURE:"ON_CAMERA_CAPTURE",ON_CAMERA_ERROR:"ON_CAMERA_ERROR",// File picker callbacks
|
|
@@ -14,10 +16,11 @@ ON_FILE_PICKED:"ON_FILE_PICKED",ON_FILE_PICK_ERROR:"ON_FILE_PICK_ERROR",ON_FILE_
|
|
|
14
16
|
ON_INTENT_SUCCESS:"ON_INTENT_SUCCESS",ON_INTENT_ERROR:"ON_INTENT_ERROR",ON_INTENT_CANCELLED:"ON_INTENT_CANCELLED",// Haptic feedback callbacks
|
|
15
17
|
HAPTIC_FEEDBACK:"HAPTIC_FEEDBACK",// Notification callbacks
|
|
16
18
|
NOTIFICATION_PERMISSION_STATUS:"NOTIFICATION_PERMISSION_STATUS",LOCAL_NOTIFICATION_SCHEDULED:"LOCAL_NOTIFICATION_SCHEDULED",LOCAL_NOTIFICATION_CANCELLED:"LOCAL_NOTIFICATION_CANCELLED",PUSH_NOTIFICATION_TOKEN:"PUSH_NOTIFICATION_TOKEN",NOTIFICATION_RECEIVED:"NOTIFICATION_RECEIVED",NOTIFICATION_ACTION_PERFORMED:"NOTIFICATION_ACTION_PERFORMED",NOTIFICATION_TAPPED:"NOTIFICATION_TAPPED",TOPIC_SUBSCRIPTION_RESULT:"TOPIC_SUBSCRIPTION_RESULT",SUBSCRIBED_TOPICS_RESULT:"SUBSCRIBED_TOPICS_RESULT",// Device info callbacks
|
|
17
|
-
ON_DEVICE_INFO_SUCCESS:"ON_DEVICE_INFO_SUCCESS",ON_DEVICE_INFO_ERROR:"ON_DEVICE_INFO_ERROR",//
|
|
19
|
+
ON_DEVICE_INFO_SUCCESS:"ON_DEVICE_INFO_SUCCESS",ON_DEVICE_INFO_ERROR:"ON_DEVICE_INFO_ERROR",// Auth callbacks
|
|
20
|
+
ON_GOOGLE_SIGN_IN_SUCCESS:"ON_GOOGLE_SIGN_IN_SUCCESS",ON_GOOGLE_SIGN_IN_ERROR:"ON_GOOGLE_SIGN_IN_ERROR",ON_GOOGLE_SIGN_IN_CANCELLED:"ON_GOOGLE_SIGN_IN_CANCELLED",// Network callbacks
|
|
18
21
|
NETWORK_STATUS_CHANGED:"NETWORK_STATUS_CHANGED",// Safe area callbacks
|
|
19
22
|
ON_SAFE_AREA_INSETS_UPDATED:"ON_SAFE_AREA_INSETS_UPDATED"};// Interface Categories for easier management
|
|
20
|
-
const INTERFACE_CATEGORIES=exports.INTERFACE_CATEGORIES={CAMERA:{commands:[NATIVE_COMMANDS.OPEN_CAMERA,NATIVE_COMMANDS.REQUEST_CAMERA_PERMISSION],callbacks:[NATIVE_CALLBACKS.CAMERA_PERMISSION_STATUS,NATIVE_CALLBACKS.ON_CAMERA_CAPTURE,NATIVE_CALLBACKS.ON_CAMERA_ERROR]},FILE:{commands:[NATIVE_COMMANDS.PICK_FILE,NATIVE_COMMANDS.OPEN_FILE_WITH_INTENT],callbacks:[NATIVE_CALLBACKS.ON_FILE_PICKED,NATIVE_CALLBACKS.ON_FILE_PICK_ERROR,NATIVE_CALLBACKS.ON_FILE_PICK_CANCELLED,NATIVE_CALLBACKS.ON_FILE_PICK_STATE_UPDATE]},INTENT:{commands:[NATIVE_COMMANDS.OPEN_FILE_WITH_INTENT],callbacks:[NATIVE_CALLBACKS.ON_INTENT_SUCCESS,NATIVE_CALLBACKS.ON_INTENT_ERROR,NATIVE_CALLBACKS.ON_INTENT_CANCELLED]},HAPTIC:{commands:[NATIVE_COMMANDS.REQUEST_HAPTIC_FEEDBACK],callbacks:[NATIVE_CALLBACKS.HAPTIC_FEEDBACK]},NOTIFICATION:{commands:[NATIVE_COMMANDS.SCHEDULE_LOCAL_NOTIFICATION,NATIVE_COMMANDS.CANCEL_LOCAL_NOTIFICATION,NATIVE_COMMANDS.REQUEST_NOTIFICATION_PERMISSION,NATIVE_COMMANDS.REGISTER_FOR_PUSH_NOTIFICATIONS,NATIVE_COMMANDS.UPDATE_BADGE_COUNT,NATIVE_COMMANDS.SUBSCRIBE_TO_TOPIC,NATIVE_COMMANDS.UNSUBSCRIBE_FROM_TOPIC,NATIVE_COMMANDS.GET_SUBSCRIBED_TOPICS],callbacks:[NATIVE_CALLBACKS.NOTIFICATION_PERMISSION_STATUS,NATIVE_CALLBACKS.LOCAL_NOTIFICATION_SCHEDULED,NATIVE_CALLBACKS.LOCAL_NOTIFICATION_CANCELLED,NATIVE_CALLBACKS.PUSH_NOTIFICATION_TOKEN,NATIVE_CALLBACKS.NOTIFICATION_RECEIVED,NATIVE_CALLBACKS.NOTIFICATION_ACTION_PERFORMED,NATIVE_CALLBACKS.NOTIFICATION_TAPPED,NATIVE_CALLBACKS.TOPIC_SUBSCRIPTION_RESULT,NATIVE_CALLBACKS.SUBSCRIBED_TOPICS_RESULT]},DEVICE_INFO:{commands:[NATIVE_COMMANDS.GET_DEVICE_INFO],callbacks:[NATIVE_CALLBACKS.ON_DEVICE_INFO_SUCCESS,NATIVE_CALLBACKS.ON_DEVICE_INFO_ERROR]},NETWORK:{commands:[NATIVE_COMMANDS.GET_NETWORK_STATUS],callbacks:[NATIVE_CALLBACKS.NETWORK_STATUS_CHANGED]}};// All available interfaces (for validation)
|
|
23
|
+
const INTERFACE_CATEGORIES=exports.INTERFACE_CATEGORIES={CAMERA:{commands:[NATIVE_COMMANDS.OPEN_CAMERA,NATIVE_COMMANDS.REQUEST_CAMERA_PERMISSION],callbacks:[NATIVE_CALLBACKS.CAMERA_PERMISSION_STATUS,NATIVE_CALLBACKS.ON_CAMERA_CAPTURE,NATIVE_CALLBACKS.ON_CAMERA_ERROR]},FILE:{commands:[NATIVE_COMMANDS.PICK_FILE,NATIVE_COMMANDS.OPEN_FILE_WITH_INTENT],callbacks:[NATIVE_CALLBACKS.ON_FILE_PICKED,NATIVE_CALLBACKS.ON_FILE_PICK_ERROR,NATIVE_CALLBACKS.ON_FILE_PICK_CANCELLED,NATIVE_CALLBACKS.ON_FILE_PICK_STATE_UPDATE]},INTENT:{commands:[NATIVE_COMMANDS.OPEN_FILE_WITH_INTENT],callbacks:[NATIVE_CALLBACKS.ON_INTENT_SUCCESS,NATIVE_CALLBACKS.ON_INTENT_ERROR,NATIVE_CALLBACKS.ON_INTENT_CANCELLED]},HAPTIC:{commands:[NATIVE_COMMANDS.REQUEST_HAPTIC_FEEDBACK],callbacks:[NATIVE_CALLBACKS.HAPTIC_FEEDBACK]},NOTIFICATION:{commands:[NATIVE_COMMANDS.SCHEDULE_LOCAL_NOTIFICATION,NATIVE_COMMANDS.CANCEL_LOCAL_NOTIFICATION,NATIVE_COMMANDS.REQUEST_NOTIFICATION_PERMISSION,NATIVE_COMMANDS.REGISTER_FOR_PUSH_NOTIFICATIONS,NATIVE_COMMANDS.UPDATE_BADGE_COUNT,NATIVE_COMMANDS.SUBSCRIBE_TO_TOPIC,NATIVE_COMMANDS.UNSUBSCRIBE_FROM_TOPIC,NATIVE_COMMANDS.GET_SUBSCRIBED_TOPICS],callbacks:[NATIVE_CALLBACKS.NOTIFICATION_PERMISSION_STATUS,NATIVE_CALLBACKS.LOCAL_NOTIFICATION_SCHEDULED,NATIVE_CALLBACKS.LOCAL_NOTIFICATION_CANCELLED,NATIVE_CALLBACKS.PUSH_NOTIFICATION_TOKEN,NATIVE_CALLBACKS.NOTIFICATION_RECEIVED,NATIVE_CALLBACKS.NOTIFICATION_ACTION_PERFORMED,NATIVE_CALLBACKS.NOTIFICATION_TAPPED,NATIVE_CALLBACKS.TOPIC_SUBSCRIPTION_RESULT,NATIVE_CALLBACKS.SUBSCRIBED_TOPICS_RESULT]},DEVICE_INFO:{commands:[NATIVE_COMMANDS.GET_DEVICE_INFO],callbacks:[NATIVE_CALLBACKS.ON_DEVICE_INFO_SUCCESS,NATIVE_CALLBACKS.ON_DEVICE_INFO_ERROR]},AUTH:{commands:[NATIVE_COMMANDS.GOOGLE_SIGN_IN],callbacks:[NATIVE_CALLBACKS.ON_GOOGLE_SIGN_IN_SUCCESS,NATIVE_CALLBACKS.ON_GOOGLE_SIGN_IN_ERROR,NATIVE_CALLBACKS.ON_GOOGLE_SIGN_IN_CANCELLED]},NETWORK:{commands:[NATIVE_COMMANDS.GET_NETWORK_STATUS],callbacks:[NATIVE_CALLBACKS.NETWORK_STATUS_CHANGED]}};// All available interfaces (for validation)
|
|
21
24
|
const ALL_COMMANDS=exports.ALL_COMMANDS=Object.values(NATIVE_COMMANDS);const ALL_CALLBACKS=exports.ALL_CALLBACKS=Object.values(NATIVE_CALLBACKS);const ALL_INTERFACES=exports.ALL_INTERFACES=[...ALL_COMMANDS,...ALL_CALLBACKS];// Permission status constants
|
|
22
25
|
const PERMISSION_STATUS=exports.PERMISSION_STATUS={GRANTED:"GRANTED",DENIED:"DENIED",NOT_DETERMINED:"NOT_DETERMINED",RESTRICTED:"RESTRICTED"};// Haptic feedback types (matching Android implementation)
|
|
23
26
|
const HAPTIC_FEEDBACK_TYPES=exports.HAPTIC_FEEDBACK_TYPES={VIRTUAL_KEY:"VIRTUAL_KEY",LONG_PRESS:"LONG_PRESS",DEFAULT:"DEFAULT",// Legacy types for backward compatibility
|
|
@@ -22,7 +22,7 @@ const ERROR_DEFINITIONS=exports.ERROR_DEFINITIONS={[ERROR_CODES.PERMISSION_DENIE
|
|
|
22
22
|
* Translate native error to standardized error
|
|
23
23
|
* @param {any} nativeError - Native error from Android/iOS
|
|
24
24
|
* @returns {Object} Standardized error object
|
|
25
|
-
*/exports.createStandardError=createStandardError;const translateError=nativeError=>{if(!nativeError){return createStandardError(ERROR_CODES.INTERNAL_ERROR,null,nativeError);}const errorString=nativeError.toString().toLowerCase()
|
|
25
|
+
*/exports.createStandardError=createStandardError;const translateError=nativeError=>{if(!nativeError){return createStandardError(ERROR_CODES.INTERNAL_ERROR,null,nativeError);}const errorString=nativeError.toString().toLowerCase();if(errorString.includes("disabled")&&errorString.includes("config")){return createStandardError(ERROR_CODES.FEATURE_UNSUPPORTED,nativeError.toString(),nativeError);}// Permission errors
|
|
26
26
|
if(errorString.includes("permission")&&errorString.includes("denied")){if(errorString.includes("camera")){return createStandardError(ERROR_CODES.PERMISSION_DENIED,"Camera permission denied",nativeError);}if(errorString.includes("storage")||errorString.includes("external_storage")){return createStandardError(ERROR_CODES.PERMISSION_DENIED,"Storage permission denied",nativeError);}return createStandardError(ERROR_CODES.PERMISSION_DENIED,null,nativeError);}// File system errors
|
|
27
27
|
if(errorString.includes("filenotfound")||errorString.includes("file not found")){return createStandardError(ERROR_CODES.FILE_NOT_FOUND,null,nativeError);}if(errorString.includes("storage")&&errorString.includes("full")){return createStandardError(ERROR_CODES.STORAGE_FULL,null,nativeError);}// Network errors
|
|
28
28
|
if(errorString.includes("network")||errorString.includes("connection")||errorString.includes("internet")){return createStandardError(ERROR_CODES.NETWORK_UNAVAILABLE,null,nativeError);}// User cancellation
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.useSafeArea=exports.useNotificationPermission=exports.useNotification=exports.useNetworkStatus=exports.useIntent=exports.useHapticFeedback=exports.useFilePicker=exports.useCameraPermission=exports.useCamera=exports.requestNotificationPermission=exports.requestHapticFeedback=exports.requestCameraPermission=void 0;var _react=require("react");var _NativeBridge=_interopRequireDefault(require("./utils/NativeBridge.js"));var _NativeInterfaces=require("./constants/NativeInterfaces.js");var _useBaseHook=require("./useBaseHook.js");var _errors=require("./errors.js");var _FileObjectConverter=require("./utils/FileObjectConverter.js");var _safeArea=require("./safeArea.js");function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}/* eslint-disable react-compiler/react-compiler, react-hooks/exhaustive-deps */// Shared callback system for notification permission status
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.useSafeArea=exports.useNotificationPermission=exports.useNotification=exports.useNetworkStatus=exports.useIntent=exports.useHapticFeedback=exports.useGoogleSignIn=exports.useFilePicker=exports.useCameraPermission=exports.useCamera=exports.requestNotificationPermission=exports.requestHapticFeedback=exports.requestCameraPermission=void 0;var _react=require("react");var _NativeBridge=_interopRequireDefault(require("./utils/NativeBridge.js"));var _NativeInterfaces=require("./constants/NativeInterfaces.js");var _useBaseHook=require("./useBaseHook.js");var _errors=require("./errors.js");var _FileObjectConverter=require("./utils/FileObjectConverter.js");var _safeArea=require("./safeArea.js");function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}/* eslint-disable react-compiler/react-compiler, react-hooks/exhaustive-deps */// Shared callback system for notification permission status
|
|
2
2
|
// This allows multiple listeners (hook + requestNotificationPermission) to receive updates
|
|
3
3
|
const permissionStatusListeners=new Set();let isPermissionCallbackRegistered=false;const registerPermissionStatusListener=callback=>{permissionStatusListeners.add(callback);// Register the shared handler with WebBridge if this is the first listener
|
|
4
4
|
if(!isPermissionCallbackRegistered&&typeof window!=="undefined"&&window.WebBridge){isPermissionCallbackRegistered=true;window.WebBridge.register(_NativeInterfaces.NATIVE_CALLBACKS.NOTIFICATION_PERMISSION_STATUS,data=>{// Notify all listeners
|
|
5
|
-
permissionStatusListeners.forEach(listener=>{try{listener(data);}catch(error){console.error("Error in permission status listener:",error);}});});}return()=>{permissionStatusListeners.delete(callback);};};const unregisterPermissionStatusListener=callback=>{permissionStatusListeners.delete(callback);};const noop=()=>{};const createSSRUnavailable=methodName=>async()=>{throw new Error(`${methodName} is not available in SSR environment`);};const SSR_FILE_PICKER_STUB={data:null,loading:false,progress:null,error:null,isWeb:true,isNative:false,execute:noop,clear:noop,clearError:noop,getFileObject:createSSRUnavailable("getFileObject"),getAllFileObjects:createSSRUnavailable("getAllFileObjects"),canCreateFileObject:false,canCreateFileObjects:[],selectedFile:null,selectedFiles:[],pickFile:noop,isLoading:false,processingState:null,clearFile:noop};const parseNativePayload=payload=>{if(payload==null)return null;if(typeof payload!=="string")return payload;return JSON.parse(payload);};const extractFiles=value=>{if(!value)return[];if(Array.isArray(value))return value.filter(Boolean);if(Array.isArray(value.files))return value.files.filter(Boolean);if(value.files)return[value.files];return[value].filter(Boolean);};const normalizeFilePickResult=payload=>{const parsed=parseNativePayload(payload);if(!parsed)return null;const files=extractFiles(parsed);if(!files.length)return null;const first=files[0];const rawOptions=!Array.isArray(parsed)&&parsed&&typeof parsed.options==="object"?parsed.options:null;let normalizedOptions=rawOptions||null;if(rawOptions){try{normalizedOptions=sanitizeFilePickerOptions(rawOptions);}catch(_error){normalizedOptions=rawOptions;}}const totalSize=parsed.totalSize??files.reduce((sum,file)=>{const size=typeof file?.size==="number"?file.size:0;return sum+size;},0);const base=Array.isArray(parsed)?{}:parsed;return{...base,files,multiple:parsed.multiple??files.length>1,count:parsed.count??files.length,totalSize,fileName:parsed.fileName??first?.fileName??first?.name??null,fileSrc:parsed.fileSrc??first?.fileSrc??first?.src??null,filePath:parsed.filePath??first?.filePath??null,size:parsed.size??first?.size??null,mimeType:parsed.mimeType??first?.mimeType??first?.type??null,transport:parsed.transport??first?.transport??null,options:normalizedOptions};}
|
|
5
|
+
permissionStatusListeners.forEach(listener=>{try{listener(data);}catch(error){console.error("Error in permission status listener:",error);}});});}return()=>{permissionStatusListeners.delete(callback);};};const unregisterPermissionStatusListener=callback=>{permissionStatusListeners.delete(callback);};const noop=()=>{};const createSSRUnavailable=methodName=>async()=>{throw new Error(`${methodName} is not available in SSR environment`);};const SSR_FILE_PICKER_STUB={data:null,loading:false,progress:null,error:null,isWeb:true,isNative:false,execute:noop,clear:noop,clearError:noop,getFileObject:createSSRUnavailable("getFileObject"),getAllFileObjects:createSSRUnavailable("getAllFileObjects"),canCreateFileObject:false,canCreateFileObjects:[],selectedFile:null,selectedFiles:[],pickFile:noop,isLoading:false,processingState:null,clearFile:noop};const parseNativePayload=payload=>{if(payload==null)return null;if(typeof payload!=="string")return payload;return JSON.parse(payload);};const extractFiles=value=>{if(!value)return[];if(Array.isArray(value))return value.filter(Boolean);if(Array.isArray(value.files))return value.files.filter(Boolean);if(value.files)return[value.files];return[value].filter(Boolean);};const normalizeFilePickResult=payload=>{const parsed=parseNativePayload(payload);if(!parsed)return null;const files=extractFiles(parsed);if(!files.length)return null;const first=files[0];const rawOptions=!Array.isArray(parsed)&&parsed&&typeof parsed.options==="object"?parsed.options:null;let normalizedOptions=rawOptions||null;if(rawOptions){try{normalizedOptions=sanitizeFilePickerOptions(rawOptions);}catch(_error){normalizedOptions=rawOptions;}}const totalSize=parsed.totalSize??files.reduce((sum,file)=>{const size=typeof file?.size==="number"?file.size:0;return sum+size;},0);const base=Array.isArray(parsed)?{}:parsed;return{...base,files,multiple:parsed.multiple??files.length>1,count:parsed.count??files.length,totalSize,fileName:parsed.fileName??first?.fileName??first?.name??null,fileSrc:parsed.fileSrc??first?.fileSrc??first?.src??null,filePath:parsed.filePath??first?.filePath??null,size:parsed.size??first?.size??null,mimeType:parsed.mimeType??first?.mimeType??first?.type??null,transport:parsed.transport??first?.transport??null,options:normalizedOptions};};/**
|
|
6
|
+
* Google Sign-In hook (Android-first, Credential Manager)
|
|
7
|
+
* Provides a promise-like sign-in call and standardized callbacks
|
|
8
|
+
*/const useGoogleSignIn=(defaultOptions={})=>{const base=(0,_useBaseHook.useBaseHook)("useGoogleSignIn");// Server-side rendering safety
|
|
9
|
+
if(typeof window==="undefined"){return{data:null,loading:false,error:null,progress:null,isWeb:true,isNative:false,signIn:()=>{},execute:()=>{},clear:()=>{},clearError:()=>{}};}if(!window.WebBridge){throw new Error("WebBridge is not initialized. Call WebBridge.init() first.");}(0,_react.useEffect)(()=>{const handleSuccess=payload=>{try{const parsed=typeof payload==="string"?JSON.parse(payload):payload;base.setDataAndComplete(parsed);}catch(err){base.handleNativeError(err);}};const handleError=nativeError=>{base.handleNativeError(nativeError);};const handleCancelled=()=>{const error=(0,_errors.createStandardError)(_errors.ERROR_CODES.OPERATION_CANCELLED,"Google sign-in cancelled",null,"User dismissed Google sign-in");base.handleNativeError(error);};window.WebBridge.register(_NativeInterfaces.NATIVE_CALLBACKS.ON_GOOGLE_SIGN_IN_SUCCESS,handleSuccess);window.WebBridge.register(_NativeInterfaces.NATIVE_CALLBACKS.ON_GOOGLE_SIGN_IN_ERROR,handleError);window.WebBridge.register(_NativeInterfaces.NATIVE_CALLBACKS.ON_GOOGLE_SIGN_IN_CANCELLED,handleCancelled);return()=>{window.WebBridge.unregister(_NativeInterfaces.NATIVE_CALLBACKS.ON_GOOGLE_SIGN_IN_SUCCESS);window.WebBridge.unregister(_NativeInterfaces.NATIVE_CALLBACKS.ON_GOOGLE_SIGN_IN_ERROR);window.WebBridge.unregister(_NativeInterfaces.NATIVE_CALLBACKS.ON_GOOGLE_SIGN_IN_CANCELLED);};},[base]);const signIn=(0,_react.useCallback)((options={})=>{base.executeOperation(()=>{const payload={...defaultOptions,...options};_NativeBridge.default.auth.googleSignIn(payload);},"google sign-in");},[base,defaultOptions]);return{data:base.data,loading:base.loading,progress:base.progress,error:base.error,isWeb:base.isWeb,isNative:base.isNative,signIn,execute:signIn,clear:base.clear,clearError:base.clearError};};exports.useGoogleSignIn=useGoogleSignIn;const updateProgressFromResult=(updateProgress,result)=>{if(!result)return;const first=result.files?.[0];const transport=result.transport??first?.transport??null;const bytesTotal=result.totalSize??result.size??(typeof first?.size==="number"?first.size:null);if(transport||bytesTotal!=null){updateProgress({transport:transport??null,bytesTotal});}};const mapStateToProgress=state=>{if(state==null){return{state:"starting",phase:null,message:"File picker: starting..."};}const rawState=String(state);const normalized=rawState.toLowerCase();const allowedStates=new Set(["opening","processing","routing"]);const progressState=allowedStates.has(normalized)?normalized:"starting";return{state:progressState,phase:rawState,message:`File picker: ${rawState}...`};};const sanitizeFilePickerOptions=input=>{const options={...input};const mime=typeof options.mimeType==="string"?options.mimeType.trim():"";options.mimeType=mime.length>0?mime:"*/*";const coerceInteger=(value,min)=>{if(value==null)return undefined;const numeric=typeof value==="string"?Number(value.trim()):Number(value);if(!Number.isFinite(numeric)){throw new Error("File picker numeric options must be valid numbers");}if(!Number.isInteger(numeric)){throw new Error("File picker numeric options must be integers");}if(numeric<min){throw new Error(`File picker numeric options must be ≥ ${min}`);}return numeric;};const minFiles=coerceInteger(options.minFiles,1);const maxFiles=coerceInteger(options.maxFiles,1);const minFileSize=coerceInteger(options.minFileSize,0);const maxFileSize=coerceInteger(options.maxFileSize,0);if(minFiles!==undefined)options.minFiles=minFiles;if(maxFiles!==undefined)options.maxFiles=maxFiles;if(minFileSize!==undefined)options.minFileSize=minFileSize;if(maxFileSize!==undefined)options.maxFileSize=maxFileSize;if(minFiles!==undefined&&maxFiles!==undefined&&minFiles>maxFiles){throw new Error("minFiles cannot be greater than maxFiles");}if(minFileSize!==undefined&&maxFileSize!==undefined&&minFileSize>maxFileSize){throw new Error("minFileSize cannot be greater than maxFileSize");}const multiple=typeof options.multiple==="boolean"?options.multiple:Boolean(options.multiple);options.multiple=multiple||minFiles&&minFiles>1||maxFiles&&maxFiles>1;return options;};const resolvePickPayload=input=>{if(input==null)return"*/*";if(typeof input==="string")return input.trim()||"*/*";return sanitizeFilePickerOptions(input);};const registerNativeHandlers=handlers=>{handlers.forEach(([event,handler])=>window.WebBridge.register(event,handler));return()=>{handlers.forEach(([event])=>window.WebBridge.unregister(event));};};/**
|
|
6
10
|
* React hook for camera functionality using standardized interface
|
|
7
11
|
* Handles camera permissions, photo capture, and error states
|
|
8
12
|
*/const useCamera=()=>{// Use standardized base hook
|
|
@@ -80,7 +80,11 @@ if(process.env.NODE_ENV==="development"){console.warn(`🌉 Development mode: Na
|
|
|
80
80
|
* Get device information
|
|
81
81
|
* @returns {Promise} - Returns device info through callback
|
|
82
82
|
*/getDeviceInfo:()=>this.call(_NativeInterfaces.NATIVE_COMMANDS.GET_DEVICE_INFO)};/**
|
|
83
|
-
*
|
|
83
|
+
* Auth methods
|
|
84
|
+
*/auth={/**
|
|
85
|
+
* Trigger Google Sign-In (Credential Manager)
|
|
86
|
+
* @param {Object} options - config (clientId, nonce, autoSelect, etc.)
|
|
87
|
+
*/googleSignIn:(options={})=>{const payload=options&&typeof options==="object"?JSON.stringify(options):options?.toString?.();return this.call(_NativeInterfaces.NATIVE_COMMANDS.GOOGLE_SIGN_IN,payload);}};/** Safe area methods
|
|
84
88
|
*/safeArea={/**
|
|
85
89
|
* Get current safe area insets
|
|
86
90
|
* Returns insets through ON_SAFE_AREA_INSETS_UPDATED callback
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";const{exec,execSync}=require("child_process");const fs=require("fs");const path=require("path");const TerminalProgress=require("./TerminalProgress.js").default;const crypto=require("crypto");const catalystCorePath=path.dirname(require.resolve("catalyst-core/package.json"));const pwd=path.join(catalystCorePath,"dist/native");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=require(`${process.env.PWD}/config/config.json`);// Configuration constants
|
|
2
|
-
const iosConfig=WEBVIEW_CONFIG.ios;const protocol=WEBVIEW_CONFIG.useHttps?"https":"http";const ip=WEBVIEW_CONFIG.LOCAL_IP||"localhost";const port=WEBVIEW_CONFIG.port?WEBVIEW_CONFIG.useHttps?443:WEBVIEW_CONFIG.port:null;let url=port?`${protocol}://${ip}:${port}`:`${protocol}://${ip}`;const PUBLIC_PATH=`${process.env.PWD}/public`;const PROJECT_DIR=`${pwd}/iosnativeWebView`;const SCHEME_NAME="iosnativeWebView";const APP_BUNDLE_ID=iosConfig.appBundleId||"com.debug.webview";const PROJECT_NAME=path.basename(PROJECT_DIR);const IPHONE_MODEL=iosConfig.simulatorName;const GOOGLE_SERVICES_FILENAME="GoogleService-Info.plist";function generateProjectObjectId(identifier,suffix=""){return crypto.createHash("md5").update(`${identifier}:${suffix}`).digest("hex").substring(0,24).toUpperCase();}function getXcodeProjectFilePath(){return path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");}// Define build steps for progress tracking
|
|
2
|
+
const iosConfig=WEBVIEW_CONFIG.ios;const isGoogleSignInEnabled=WEBVIEW_CONFIG.googleSignIn?.enabled??false;const protocol=WEBVIEW_CONFIG.useHttps?"https":"http";const ip=WEBVIEW_CONFIG.LOCAL_IP||"localhost";const port=WEBVIEW_CONFIG.port?WEBVIEW_CONFIG.useHttps?443:WEBVIEW_CONFIG.port:null;let url=port?`${protocol}://${ip}:${port}`:`${protocol}://${ip}`;const PUBLIC_PATH=`${process.env.PWD}/public`;const PROJECT_DIR=`${pwd}/iosnativeWebView`;const SCHEME_NAME="iosnativeWebView";const APP_BUNDLE_ID=iosConfig.appBundleId||"com.debug.webview";const PROJECT_NAME=path.basename(PROJECT_DIR);const IPHONE_MODEL=iosConfig.simulatorName;const GOOGLE_SERVICES_FILENAME="GoogleService-Info.plist";function generateProjectObjectId(identifier,suffix=""){return crypto.createHash("md5").update(`${identifier}:${suffix}`).digest("hex").substring(0,24).toUpperCase();}function getXcodeProjectFilePath(){return path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");}// Define build steps for progress tracking
|
|
3
3
|
const steps={config:"Generating Required Configuration for build",deviceDetection:"Detecting Physical Device",launchSimulator:"Launch iOS Simulator",clean:"Clean Build Artifacts",assets:"Process Notification Assets",build:"Build IOS Project",findApp:"Locate Built Application",install:"Install Application",launch:"Launch Application"};// Configure progress display
|
|
4
4
|
const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new TerminalProgress(steps,"Catalyst iOS Build",progressConfig);// Utility function to run shell commands
|
|
5
5
|
function runCommand(command,options={}){return new Promise((resolve,reject)=>{// eslint-disable-next-line security/detect-child-process
|
|
6
6
|
exec(command,{maxBuffer:1024*1024*10,...options},(error,stdout,stderr)=>{if(error){console.error(`Command failed: ${command}`);console.error(`Error: ${error.message}`);console.error(`stderr: ${stderr}`);reject(error);return;}if(stderr){console.warn(`Warning: ${stderr}`);}resolve(stdout.trim());});});}async function getBootedSimulatorUUID(modelName){try{// First try to find a booted simulator of the specified model
|
|
7
7
|
let command=`xcrun simctl list devices | grep "${modelName}" | grep "Booted" | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})" | head -n 1`;let uuid=execSync(command).toString().trim();if(uuid){console.log(`Found booted simulator of model ${modelName}`);return uuid;}// If no booted simulator of the specified model is found, check any booted simulator
|
|
8
8
|
console.log(`No booted simulator of model ${modelName} found, checking for any booted simulator...`);command=`xcrun simctl list devices | grep "Booted" | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})" | head -n 1`;uuid=execSync(command).toString().trim();if(uuid){console.log("Found another booted simulator, will use it instead");return uuid;}return null;}catch(error){console.log("No booted simulators found");return null;}}async function getBootedSimulatorInfo(){try{const listCommand="xcrun simctl list devices --json";const simulatorList=JSON.parse(execSync(listCommand).toString());for(const runtime in simulatorList.devices){const devices=simulatorList.devices[runtime];for(const device of devices){if(device.state==="Booted"){return{udid:device.udid,name:device.name,runtime:runtime};}}}return null;}catch(error){console.log("Failed to get booted simulator info:",error.message);return null;}}async function generatePackageSwift(){try{const isNotificationsEnabled=WEBVIEW_CONFIG.notifications?.enabled??false;progress.log(`🔧 Generating Package.swift (notifications: ${isNotificationsEnabled})`,"info");// Create hash of current config to detect changes
|
|
9
|
-
const configHash=crypto.createHash("md5").update(JSON.stringify({notifications:isNotificationsEnabled})).digest("hex");const hashFilePath=path.join(PROJECT_DIR,".package-config-hash");const targetPath=path.join(PROJECT_DIR,"Package.swift");let shouldUpdate=true;// Check if we need to update based on config change
|
|
9
|
+
const configHash=crypto.createHash("md5").update(JSON.stringify({notifications:isNotificationsEnabled,googleSignIn:isGoogleSignInEnabled})).digest("hex");const hashFilePath=path.join(PROJECT_DIR,".package-config-hash");const targetPath=path.join(PROJECT_DIR,"Package.swift");let shouldUpdate=true;// Check if we need to update based on config change
|
|
10
10
|
if(fs.existsSync(hashFilePath)){const previousHash=fs.readFileSync(hashFilePath,"utf8");if(previousHash===configHash){shouldUpdate=false;progress.log("Package.swift already up to date","info");}}// Ensure Package.swift exists even if hash matches
|
|
11
11
|
if(!fs.existsSync(targetPath)){shouldUpdate=true;progress.log("Package.swift missing, will generate it now","info");}if(shouldUpdate){progress.log("Generating Package.swift dynamically","info");// Build the Package.swift content dynamically
|
|
12
12
|
let packageContent=`// swift-tools-version: 5.9
|
|
@@ -23,7 +23,8 @@ if(isNotificationsEnabled){packageContent+=`,
|
|
|
23
23
|
.library(name: "CatalystNotifications", targets: ["CatalystNotifications"])`;}packageContent+=`
|
|
24
24
|
],
|
|
25
25
|
dependencies: [
|
|
26
|
-
.package(url: "https://github.com/kylef/JSONSchema.swift", from: "0.6.0")
|
|
26
|
+
.package(url: "https://github.com/kylef/JSONSchema.swift", from: "0.6.0"),
|
|
27
|
+
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "7.0.0")`;// Add Firebase dependency only if notifications enabled
|
|
27
28
|
if(isNotificationsEnabled){packageContent+=`,
|
|
28
29
|
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "12.3.0")`;}packageContent+=`
|
|
29
30
|
],
|
|
@@ -33,7 +34,8 @@ if(isNotificationsEnabled){packageContent+=`,
|
|
|
33
34
|
.target(
|
|
34
35
|
name: "CatalystCore",
|
|
35
36
|
dependencies: [
|
|
36
|
-
.product(name: "JSONSchema", package: "JSONSchema.swift")
|
|
37
|
+
.product(name: "JSONSchema", package: "JSONSchema.swift"),
|
|
38
|
+
.product(name: "GoogleSignIn", package: "GoogleSignIn-iOS")
|
|
37
39
|
],
|
|
38
40
|
path: "Sources/Core"
|
|
39
41
|
)`;// Add CatalystNotifications target only if enabled
|
|
@@ -63,9 +65,9 @@ projectContent=projectContent.replace(/(C99974342E97D56900C25611 \/\* CatalystCo
|
|
|
63
65
|
projectContent=projectContent.replace(/(C99974342E97D56900C25611 \/\* CatalystCore in Frameworks \*\/,)/,`$1\n\t\t\t\t${NOTIF_BUILD_FILE_ID} /* CatalystNotifications in Frameworks */,`);// 3. Add to packageProductDependencies array
|
|
64
66
|
projectContent=projectContent.replace(/(packageProductDependencies = \(\s*C99974332E97D56900C25611 \/\* CatalystCore \*\/,)/,`$1\n\t\t\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */,`);// 4. Add to XCSwiftPackageProductDependency section
|
|
65
67
|
projectContent=projectContent.replace(/(\/\* End XCSwiftPackageProductDependency section \*\/)/,`\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = C99974322E97D56900C25611 /* XCLocalSwiftPackageReference "." */;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n$1`);fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("✅ CatalystNotifications added to Xcode project","success");}else if(!isNotificationsEnabled&&hasNotifications){progress.log("Removing CatalystNotifications from Xcode project","info");// Remove all CatalystNotifications entries
|
|
66
|
-
projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/ = {isa = PBXBuildFile; productRef = [A-F0-9]+ \/\* CatalystNotifications \*\/; };\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = [A-F0-9]+ \/\* XCLocalSwiftPackageReference "." \*\/;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n/g,"");fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("✅ CatalystNotifications removed from Xcode project","success");}else{progress.log("Package dependencies already correct","info");}}catch(error){progress.log(`❌ Failed to update package dependencies: ${error.message}`,"error");throw error;}}async function updateInfoPlist(){try{const infoPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info.plist");
|
|
67
|
-
if(!plistContent.includes("CFBundleDisplayName")){const insertPoint=plistContent.lastIndexOf("</dict>");const newEntry=`\t<key>CFBundleDisplayName</key>\n\t<string>${iosConfig.appName||"Catalyst Application"}</string>\n`;plistContent=plistContent.slice(0,insertPoint)+newEntry+plistContent.slice(insertPoint);fs.writeFileSync(
|
|
68
|
-
const displayNameRegex=/(<key>CFBundleDisplayName<\/key>\s*<string>)([^<]*)(<\/string>)/;if(displayNameRegex.test(plistContent)){plistContent=plistContent.replace(displayNameRegex,`$1${iosConfig.appName||"Catalyst Application"}$3`);fs.writeFileSync(
|
|
68
|
+
projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/ = {isa = PBXBuildFile; productRef = [A-F0-9]+ \/\* CatalystNotifications \*\/; };\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = [A-F0-9]+ \/\* XCLocalSwiftPackageReference "." \*\/;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n/g,"");fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("✅ CatalystNotifications removed from Xcode project","success");}else{progress.log("Package dependencies already correct","info");}}catch(error){progress.log(`❌ Failed to update package dependencies: ${error.message}`,"error");throw error;}}async function updateInfoPlist(){try{const infoPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info.plist");const infoReleasePlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info-Release.plist");const googleServicesPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,GOOGLE_SERVICES_FILENAME);const googleClientId=WEBVIEW_CONFIG.googleSignIn?.clientId||WEBVIEW_CONFIG.googleSignIn?.webClientId||"";const iosClientId=WEBVIEW_CONFIG.googleSignIn?.iosClientId||"";const googleServicesContent=fs.existsSync(googleServicesPlistPath)?fs.readFileSync(googleServicesPlistPath,"utf8"):null;const reversedClientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>REVERSED_CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const clientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const computeReversed=value=>{if(!value)return"";return value.split(".").reverse().join(".");};const resolvedClientIdForScheme=iosClientId||googleClientId||clientIdFromServices||"";const resolvedReversedClientId=reversedClientIdFromServices||computeReversed(resolvedClientIdForScheme);if(isGoogleSignInEnabled&&!resolvedReversedClientId){progress.fail("config","Google Sign-In enabled but no valid clientId found");process.exit(1);}const plistTargets=[infoPlistPath,infoReleasePlistPath];const findMatchingArrayCloseTag=(content,arrayStartIndex)=>{if(arrayStartIndex<0)return-1;const tokenRegex=/<array>|<\/array>/g;tokenRegex.lastIndex=arrayStartIndex;let depth=0;let match;while((match=tokenRegex.exec(content))!==null){if(match[0]==="<array>"){depth+=1;}else{depth-=1;if(depth===0){return match.index;}}}return-1;};const ensureGoogleUrlScheme=plistPath=>{if(!isGoogleSignInEnabled||!resolvedReversedClientId){return;}if(!fs.existsSync(plistPath)){return;}let plistContent=fs.readFileSync(plistPath,"utf8");if(plistContent.includes(resolvedReversedClientId)){return;}const urlTypeEntry=`\n\t<key>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleURLName</key>\n\t\t\t<string>googleSignIn</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>${resolvedReversedClientId}</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n`;const urlTypeDictEntry=`\n\t\t<dict>\n\t\t\t<key>CFBundleURLName</key>\n\t\t\t<string>googleSignIn</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>${resolvedReversedClientId}</string>\n\t\t\t</array>\n\t\t</dict>\n`;if(plistContent.includes("<key>CFBundleURLTypes</key>")){const urlTypesKeyIndex=plistContent.indexOf("<key>CFBundleURLTypes</key>");const urlTypesArrayStart=plistContent.indexOf("<array>",urlTypesKeyIndex);const urlTypesArrayClose=findMatchingArrayCloseTag(plistContent,urlTypesArrayStart);if(urlTypesArrayStart>=0&&urlTypesArrayClose>=0){plistContent=plistContent.slice(0,urlTypesArrayClose)+urlTypeDictEntry+plistContent.slice(urlTypesArrayClose);}else{const insertPoint=plistContent.lastIndexOf("</dict>");plistContent=plistContent.slice(0,insertPoint)+urlTypeEntry+plistContent.slice(insertPoint);progress.log(`CFBundleURLTypes block could not be parsed in ${path.basename(plistPath)}; appended block instead`,"warning");}}else{const insertPoint=plistContent.lastIndexOf("</dict>");plistContent=plistContent.slice(0,insertPoint)+urlTypeEntry+plistContent.slice(insertPoint);}fs.writeFileSync(plistPath,plistContent,"utf8");progress.log(`Added Google Sign-In URL scheme to ${path.basename(plistPath)}`,"info");};const ensureLSApplicationQueriesSchemes=plistPath=>{if(!isGoogleSignInEnabled||!resolvedReversedClientId){return;}if(!fs.existsSync(plistPath)){return;}let plistContent=fs.readFileSync(plistPath,"utf8");const matches=plistContent.match(/<key>LSApplicationQueriesSchemes<\/key>\s*<array>([\s\S]*?)<\/array>/);const existingSchemes=new Set();if(matches&&matches[1]){const schemeRegex=/<string>([^<]+)<\/string>/g;let match;while((match=schemeRegex.exec(matches[1]))!==null){existingSchemes.add(match[1]);}}const targetSchemes=["google",resolvedReversedClientId];targetSchemes.forEach(scheme=>{if(scheme&&!existingSchemes.has(scheme)){existingSchemes.add(scheme);}});const schemesArrayContent=Array.from(existingSchemes).map(s=>`\n\t\t<string>${s}</string>`).join("");const newLSBlock=`\n\t<key>LSApplicationQueriesSchemes</key>\n\t<array>${schemesArrayContent}\n\t</array>\n`;if(matches){plistContent=plistContent.replace(/<key>LSApplicationQueriesSchemes<\/key>\s*<array>[\s\S]*?<\/array>/,newLSBlock.trimEnd());}else{const insertPoint=plistContent.lastIndexOf("</dict>");plistContent=plistContent.slice(0,insertPoint)+newLSBlock+plistContent.slice(insertPoint);}fs.writeFileSync(plistPath,plistContent,"utf8");progress.log(`Ensured LSApplicationQueriesSchemes for Google in ${path.basename(plistPath)}`,"info");};const ensureDisplayName=plistPath=>{if(!fs.existsSync(plistPath))return;let plistContent=fs.readFileSync(plistPath,"utf8");// Add CFBundleDisplayName if it doesn't exist
|
|
69
|
+
if(!plistContent.includes("CFBundleDisplayName")){const insertPoint=plistContent.lastIndexOf("</dict>");const newEntry=`\t<key>CFBundleDisplayName</key>\n\t<string>${iosConfig.appName||"Catalyst Application"}</string>\n`;plistContent=plistContent.slice(0,insertPoint)+newEntry+plistContent.slice(insertPoint);fs.writeFileSync(plistPath,plistContent,"utf8");}else{// Update existing CFBundleDisplayName with new appName
|
|
70
|
+
const displayNameRegex=/(<key>CFBundleDisplayName<\/key>\s*<string>)([^<]*)(<\/string>)/;if(displayNameRegex.test(plistContent)){plistContent=plistContent.replace(displayNameRegex,`$1${iosConfig.appName||"Catalyst Application"}$3`);fs.writeFileSync(plistPath,plistContent,"utf8");}}};plistTargets.forEach(plistPath=>{ensureDisplayName(plistPath);ensureGoogleUrlScheme(plistPath);ensureLSApplicationQueriesSchemes(plistPath);});}catch(err){progress.fail("config",err);process.exit(1);}}// Function to convert JSON value to Swift property
|
|
69
71
|
function generateSwiftProperty(key,value,indent=" "){if(value===null||value===undefined){return`${indent}public static let ${key}: String? = nil`;}// Special handling for cachePattern - always convert to array
|
|
70
72
|
if(key==="cachePattern"){if(typeof value==="string"){// Convert single string to array
|
|
71
73
|
return`${indent}public static let ${key}: [String] = ["${value}"]`;}else if(Array.isArray(value)){const arrayValues=value.map(v=>`"${v}"`).join(", ");return`${indent}public static let ${key}: [String] = [${arrayValues}]`;}}if(typeof value==="string"){return`${indent}public static let ${key} = "${value}"`;}if(typeof value==="number"){return Number.isInteger(value)?`${indent}public static let ${key} = ${value}`:`${indent}public static let ${key} = ${value}`;}if(typeof value==="boolean"){return`${indent}public static let ${key} = ${value}`;}if(Array.isArray(value)){if(value.length===0){return`${indent}public static let ${key}: [String] = []`;}// Determine array type from first element
|
|
@@ -112,7 +114,8 @@ const imageWidth=splashConfig.imageWidth||120;const imageHeight=splashConfig.ima
|
|
|
112
114
|
public static let splashScreenImageWidth: CGFloat = 120
|
|
113
115
|
public static let splashScreenImageHeight: CGFloat = 120
|
|
114
116
|
public static let splashScreenCornerRadius: CGFloat = 20`;}// Ensure Notifications.enabled always exists (default to false if not configured)
|
|
115
|
-
if(!addedKeys.has("notifications")){progress.log("Notifications not found in config, adding default (false)","info");configContent+="\n public enum Notifications {\n public static let enabled = false\n }";addedKeys.add("notifications");}else{progress.log("Notifications config was processed from WEBVIEW_CONFIG","info");}// Ensure
|
|
117
|
+
if(!addedKeys.has("notifications")){progress.log("Notifications not found in config, adding default (false)","info");configContent+="\n public enum Notifications {\n public static let enabled = false\n }";addedKeys.add("notifications");}else{progress.log("Notifications config was processed from WEBVIEW_CONFIG","info");}// Ensure Google Sign-In defaults exist to keep compile-time references stable
|
|
118
|
+
if(!addedKeys.has("googleSignIn")){progress.log("Google Sign-In not found in config, adding defaults (disabled)","info");configContent+='\n public enum GoogleSignIn {\n public static let enabled = false\n public static let clientId = ""\n public static let iosClientId = ""\n }';addedKeys.add("googleSignIn");}// Ensure EdgeToEdge.enabled always exists (default to false if not configured)
|
|
116
119
|
if(!addedKeys.has("edgeToEdge")){progress.log("EdgeToEdge not found in config, adding default (false)","info");configContent+="\n public enum EdgeToEdge {\n public static let enabled = false\n }";addedKeys.add("edgeToEdge");}else{progress.log("EdgeToEdge config was processed from WEBVIEW_CONFIG","info");}// Close the enum
|
|
117
120
|
configContent+=`
|
|
118
121
|
}`;fs.writeFileSync(appConfigPath,configContent,"utf8");progress.log("Configuration constants generated successfully (SPM Package)","success");progress.complete("config");}catch(error){progress.fail("config",error.message);process.exit(1);}}// MARK: - Notification Asset Processing
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// This minimal version allows Xcode to open the project without resolving Firebase
|
|
4
4
|
import PackageDescription
|
|
5
5
|
|
|
6
|
+
// TODO: haven't made this conditional yet
|
|
6
7
|
let package = Package(
|
|
7
8
|
name: "iosnativeWebView",
|
|
8
9
|
platforms: [.iOS(.v17)],
|
|
@@ -10,7 +11,8 @@ let package = Package(
|
|
|
10
11
|
.library(name: "CatalystCore", targets: ["CatalystCore"])
|
|
11
12
|
],
|
|
12
13
|
dependencies: [
|
|
13
|
-
.package(url: "https://github.com/kylef/JSONSchema.swift", from: "0.6.0")
|
|
14
|
+
.package(url: "https://github.com/kylef/JSONSchema.swift", from: "0.6.0"),
|
|
15
|
+
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "7.0.0")
|
|
14
16
|
],
|
|
15
17
|
targets: [
|
|
16
18
|
// Core functionality (WebView, bridge, utils, constants)
|
|
@@ -18,7 +20,8 @@ let package = Package(
|
|
|
18
20
|
.target(
|
|
19
21
|
name: "CatalystCore",
|
|
20
22
|
dependencies: [
|
|
21
|
-
.product(name: "JSONSchema", package: "JSONSchema.swift")
|
|
23
|
+
.product(name: "JSONSchema", package: "JSONSchema.swift"),
|
|
24
|
+
.product(name: "GoogleSignIn", package: "GoogleSignIn-iOS")
|
|
22
25
|
],
|
|
23
26
|
path: "Sources/Core"
|
|
24
27
|
)
|
|
@@ -65,6 +65,7 @@ public enum CatalystConstants {
|
|
|
65
65
|
"pickFile",
|
|
66
66
|
"openFileWithIntent",
|
|
67
67
|
"requestHapticFeedback",
|
|
68
|
+
"googleSignIn",
|
|
68
69
|
"getSafeArea"
|
|
69
70
|
]
|
|
70
71
|
|
|
@@ -85,6 +86,8 @@ public enum CatalystConstants {
|
|
|
85
86
|
print("❌ DEBUG: Notifications disabled in config - notification commands not available")
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
print("🔐 DEBUG: Google Sign-In enabled in config? \(ConfigConstants.GoogleSignIn.enabled)")
|
|
90
|
+
|
|
88
91
|
print("🔧 DEBUG: Valid commands available: \(commands.sorted().joined(separator: ", "))")
|
|
89
92
|
return commands
|
|
90
93
|
}
|
|
@@ -12,6 +12,13 @@ public enum ConfigConstants {
|
|
|
12
12
|
public enum Notifications {
|
|
13
13
|
public static let enabled = false
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
// Google Sign-In configuration (defaults to disabled, populated during build)
|
|
17
|
+
public enum GoogleSignIn {
|
|
18
|
+
public static let enabled = false
|
|
19
|
+
public static let clientId = ""
|
|
20
|
+
public static let iosClientId = ""
|
|
21
|
+
}
|
|
15
22
|
|
|
16
23
|
// Access control configuration
|
|
17
24
|
public static let accessControlEnabled = false
|
|
@@ -11,6 +11,9 @@ import UIKit
|
|
|
11
11
|
import AVFoundation
|
|
12
12
|
import os
|
|
13
13
|
import UserNotifications
|
|
14
|
+
#if canImport(GoogleSignIn)
|
|
15
|
+
import GoogleSignIn
|
|
16
|
+
#endif
|
|
14
17
|
|
|
15
18
|
private let commandLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: CatalystConstants.Logging.Categories.commandHandler)
|
|
16
19
|
|
|
@@ -33,6 +36,81 @@ class BridgeCommandHandler {
|
|
|
33
36
|
|
|
34
37
|
// Notification handler - injected, defaults to null implementation
|
|
35
38
|
private var notificationHandler: NotificationHandlerProtocol = NullNotificationHandler.shared
|
|
39
|
+
private var isGoogleSignInInProgress = false
|
|
40
|
+
private var hasAttemptedGoogleRestore = false
|
|
41
|
+
private var lastConfiguredGoogleClientId: String?
|
|
42
|
+
|
|
43
|
+
private struct GoogleSignInOptions {
|
|
44
|
+
let clientId: String?
|
|
45
|
+
let nonce: String?
|
|
46
|
+
let hint: String?
|
|
47
|
+
let additionalScopes: [String]?
|
|
48
|
+
let autoSelect: Bool
|
|
49
|
+
let filterByAuthorizedAccounts: Bool
|
|
50
|
+
|
|
51
|
+
static func from(params: Any?) -> GoogleSignInOptions {
|
|
52
|
+
if let stringParam = params as? String, !stringParam.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
53
|
+
if let data = stringParam.data(using: .utf8),
|
|
54
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
55
|
+
return GoogleSignInOptions.fromDictionary(json)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let trimmed = stringParam.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
59
|
+
return GoogleSignInOptions(
|
|
60
|
+
clientId: trimmed,
|
|
61
|
+
nonce: nil,
|
|
62
|
+
hint: nil,
|
|
63
|
+
additionalScopes: nil,
|
|
64
|
+
autoSelect: false,
|
|
65
|
+
filterByAuthorizedAccounts: false
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if let dict = params as? [String: Any] {
|
|
70
|
+
return GoogleSignInOptions.fromDictionary(dict)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return GoogleSignInOptions(
|
|
74
|
+
clientId: nil,
|
|
75
|
+
nonce: nil,
|
|
76
|
+
hint: nil,
|
|
77
|
+
additionalScopes: nil,
|
|
78
|
+
autoSelect: false,
|
|
79
|
+
filterByAuthorizedAccounts: false
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static func fromDictionary(_ dict: [String: Any]) -> GoogleSignInOptions {
|
|
84
|
+
func clean(_ value: String?) -> String? {
|
|
85
|
+
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return nil }
|
|
86
|
+
return value
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let resolvedClientId = clean(dict["clientId"] as? String) ??
|
|
90
|
+
clean(dict["webClientId"] as? String)
|
|
91
|
+
|
|
92
|
+
let nonce = clean(dict["nonce"] as? String)
|
|
93
|
+
let hint = clean(dict["hint"] as? String ?? dict["email"] as? String)
|
|
94
|
+
var scopes: [String]?
|
|
95
|
+
|
|
96
|
+
if let scopesArray = dict["additionalScopes"] as? [String] {
|
|
97
|
+
let filtered = scopesArray.compactMap { clean($0) }
|
|
98
|
+
scopes = filtered.isEmpty ? nil : filtered
|
|
99
|
+
} else if let scopesString = clean(dict["additionalScopes"] as? String) {
|
|
100
|
+
let splitScopes = scopesString.split(separator: ",").map { clean(String($0)) }.compactMap { $0 }
|
|
101
|
+
scopes = splitScopes.isEmpty ? nil : splitScopes
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return GoogleSignInOptions(
|
|
105
|
+
clientId: resolvedClientId,
|
|
106
|
+
nonce: nonce,
|
|
107
|
+
hint: hint,
|
|
108
|
+
additionalScopes: scopes,
|
|
109
|
+
autoSelect: dict["autoSelect"] as? Bool ?? false,
|
|
110
|
+
filterByAuthorizedAccounts: dict["filterByAuthorizedAccounts"] as? Bool ?? false
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
36
114
|
|
|
37
115
|
init(viewController: UIViewController, imageHandler: ImageHandler, filePickerHandler: FilePickerHandler) {
|
|
38
116
|
self.viewController = viewController
|
|
@@ -170,6 +248,139 @@ class BridgeCommandHandler {
|
|
|
170
248
|
}
|
|
171
249
|
}
|
|
172
250
|
|
|
251
|
+
func googleSignIn(params: Any?) {
|
|
252
|
+
#if canImport(GoogleSignIn)
|
|
253
|
+
guard ConfigConstants.GoogleSignIn.enabled else {
|
|
254
|
+
delegate?.sendErrorCallback(
|
|
255
|
+
eventName: "ON_GOOGLE_SIGN_IN_ERROR",
|
|
256
|
+
error: "Google Sign-In is disabled in config",
|
|
257
|
+
code: "GOOGLE_SIGN_IN_ERROR"
|
|
258
|
+
)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let options = GoogleSignInOptions.from(params: params)
|
|
263
|
+
|
|
264
|
+
let resolvedClientId = (options.clientId ?? ConfigConstants.GoogleSignIn.clientId)
|
|
265
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
266
|
+
|
|
267
|
+
guard !resolvedClientId.isEmpty else {
|
|
268
|
+
delegate?.sendErrorCallback(
|
|
269
|
+
eventName: "ON_GOOGLE_SIGN_IN_ERROR",
|
|
270
|
+
error: "clientId is required for Google Sign-In",
|
|
271
|
+
code: "GOOGLE_SIGN_IN_ERROR"
|
|
272
|
+
)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
guard !isGoogleSignInInProgress else {
|
|
277
|
+
delegate?.sendErrorCallback(
|
|
278
|
+
eventName: "ON_GOOGLE_SIGN_IN_ERROR",
|
|
279
|
+
error: "A Google Sign-In request is already in progress",
|
|
280
|
+
code: "GOOGLE_SIGN_IN_ERROR"
|
|
281
|
+
)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
guard let presentingVC = resolvePresentingViewController() else {
|
|
286
|
+
delegate?.sendErrorCallback(
|
|
287
|
+
eventName: "ON_GOOGLE_SIGN_IN_ERROR",
|
|
288
|
+
error: "No valid view controller available for Google Sign-In",
|
|
289
|
+
code: "GOOGLE_SIGN_IN_ERROR"
|
|
290
|
+
)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
isGoogleSignInInProgress = true
|
|
295
|
+
commandLogger.debug("Starting Google Sign-In with clientId prefix: \(resolvedClientId.prefix(12))...")
|
|
296
|
+
|
|
297
|
+
configureGoogleSignIn(clientId: resolvedClientId)
|
|
298
|
+
|
|
299
|
+
if options.autoSelect || options.filterByAuthorizedAccounts {
|
|
300
|
+
commandLogger.debug("Google Sign-In options autoSelect/filterByAuthorizedAccounts are not supported on iOS SDK and will be ignored")
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
DispatchQueue.main.async { [weak self] in
|
|
304
|
+
guard let self = self else { return }
|
|
305
|
+
GIDSignIn.sharedInstance.signIn(
|
|
306
|
+
withPresenting: presentingVC,
|
|
307
|
+
hint: options.hint,
|
|
308
|
+
additionalScopes: options.additionalScopes
|
|
309
|
+
) { [weak self] result, error in
|
|
310
|
+
guard let self = self else { return }
|
|
311
|
+
self.isGoogleSignInInProgress = false
|
|
312
|
+
|
|
313
|
+
let hasValidUserData = result?.user != nil && result?.user.idToken?.tokenString != nil
|
|
314
|
+
|
|
315
|
+
if let error = error as NSError? {
|
|
316
|
+
if self.isCancelledGoogleError(error) {
|
|
317
|
+
self.delegate?.sendErrorCallback(
|
|
318
|
+
eventName: "ON_GOOGLE_SIGN_IN_CANCELLED",
|
|
319
|
+
error: "User cancelled Google sign-in",
|
|
320
|
+
code: "OPERATION_CANCELLED"
|
|
321
|
+
)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let errorDescription = error.localizedDescription.lowercased()
|
|
326
|
+
let isKeychainError = errorDescription.contains("keychain")
|
|
327
|
+
|
|
328
|
+
if isKeychainError && hasValidUserData {
|
|
329
|
+
commandLogger.warning("Google Sign-In keychain error (common on simulators), but user data is valid: \(error.localizedDescription)")
|
|
330
|
+
} else {
|
|
331
|
+
commandLogger.error("Google Sign-In error: \(error.localizedDescription)")
|
|
332
|
+
self.delegate?.sendErrorCallback(
|
|
333
|
+
eventName: "ON_GOOGLE_SIGN_IN_ERROR",
|
|
334
|
+
error: error.localizedDescription,
|
|
335
|
+
code: "GOOGLE_SIGN_IN_ERROR"
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
guard let user = result?.user, let idToken = user.idToken?.tokenString else {
|
|
342
|
+
self.delegate?.sendErrorCallback(
|
|
343
|
+
eventName: "ON_GOOGLE_SIGN_IN_ERROR",
|
|
344
|
+
error: "No ID token returned from Google sign-in",
|
|
345
|
+
code: "GOOGLE_SIGN_IN_ERROR"
|
|
346
|
+
)
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
var payload: [String: Any] = [
|
|
351
|
+
"idToken": idToken,
|
|
352
|
+
"provider": "google"
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
if let email = user.profile?.email, !email.isEmpty {
|
|
356
|
+
payload["email"] = email
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if let name = user.profile?.name, !name.isEmpty {
|
|
360
|
+
payload["name"] = name
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if let photoUrl = user.profile?.imageURL(withDimension: 160)?.absoluteString, !photoUrl.isEmpty {
|
|
364
|
+
payload["photoUrl"] = photoUrl
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if let serverAuthCode = result?.serverAuthCode, !serverAuthCode.isEmpty {
|
|
368
|
+
payload["serverAuthCode"] = serverAuthCode
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
commandLogger.debug("Google Sign-In success (idToken length: \(idToken.count))")
|
|
372
|
+
self.delegate?.sendJSONCallback(eventName: "ON_GOOGLE_SIGN_IN_SUCCESS", data: payload)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
#else
|
|
376
|
+
delegate?.sendErrorCallback(
|
|
377
|
+
eventName: "ON_GOOGLE_SIGN_IN_ERROR",
|
|
378
|
+
error: "Google Sign-In SDK not available on this build",
|
|
379
|
+
code: "GOOGLE_SIGN_IN_UNAVAILABLE"
|
|
380
|
+
)
|
|
381
|
+
#endif
|
|
382
|
+
}
|
|
383
|
+
|
|
173
384
|
// Get device information
|
|
174
385
|
func getDeviceInfo() {
|
|
175
386
|
commandLogger.debug("getDeviceInfo called")
|
|
@@ -641,4 +852,57 @@ class BridgeCommandHandler {
|
|
|
641
852
|
}
|
|
642
853
|
}
|
|
643
854
|
|
|
855
|
+
// MARK: - Helpers
|
|
856
|
+
|
|
857
|
+
private func resolvePresentingViewController() -> UIViewController? {
|
|
858
|
+
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
|
859
|
+
if let window = scene.windows.first(where: { $0.isKeyWindow }),
|
|
860
|
+
let rootVC = window.rootViewController {
|
|
861
|
+
return rootVC.presentedViewController ?? rootVC
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return viewController
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
private func configureGoogleSignIn(clientId: String) {
|
|
869
|
+
guard lastConfiguredGoogleClientId != clientId else { return }
|
|
870
|
+
lastConfiguredGoogleClientId = clientId
|
|
871
|
+
|
|
872
|
+
let iosClientId = ConfigConstants.GoogleSignIn.iosClientId
|
|
873
|
+
let serverClientId = clientId // This is the Web Client ID
|
|
874
|
+
let configClientId = !iosClientId.isEmpty ? iosClientId : serverClientId
|
|
875
|
+
|
|
876
|
+
GIDSignIn.sharedInstance.configuration = GIDConfiguration(
|
|
877
|
+
clientID: configClientId,
|
|
878
|
+
serverClientID: serverClientId
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
if !hasAttemptedGoogleRestore {
|
|
882
|
+
hasAttemptedGoogleRestore = true
|
|
883
|
+
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
|
|
884
|
+
if let error = error {
|
|
885
|
+
commandLogger.debug("Google Sign-In restore failed: \(error.localizedDescription)")
|
|
886
|
+
} else if user != nil {
|
|
887
|
+
commandLogger.debug("Google Sign-In restored previous session")
|
|
888
|
+
} else {
|
|
889
|
+
commandLogger.debug("Google Sign-In restore found no previous user")
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private func isCancelledGoogleError(_ error: NSError) -> Bool {
|
|
896
|
+
let description = error.localizedDescription.lowercased()
|
|
897
|
+
if description.contains("cancel") {
|
|
898
|
+
return true
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// GIDSignIn cancellation historically uses code -5; keep the check resilient
|
|
902
|
+
if error.code == -5 {
|
|
903
|
+
return true
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return false
|
|
907
|
+
}
|
|
644
908
|
}
|
|
@@ -134,6 +134,23 @@ class BridgeMessageValidator {
|
|
|
134
134
|
"properties": [:],
|
|
135
135
|
"additionalProperties": false
|
|
136
136
|
],
|
|
137
|
+
"googleSignIn": [
|
|
138
|
+
"type": "object",
|
|
139
|
+
"properties": [
|
|
140
|
+
"clientId": ["type": "string"],
|
|
141
|
+
"webClientId": ["type": "string"],
|
|
142
|
+
"nonce": ["type": "string"],
|
|
143
|
+
"hint": ["type": "string"],
|
|
144
|
+
"email": ["type": "string"],
|
|
145
|
+
"autoSelect": ["type": "boolean"],
|
|
146
|
+
"filterByAuthorizedAccounts": ["type": "boolean"],
|
|
147
|
+
"additionalScopes": [
|
|
148
|
+
"type": "array",
|
|
149
|
+
"items": ["type": "string"]
|
|
150
|
+
]
|
|
151
|
+
],
|
|
152
|
+
"additionalProperties": false
|
|
153
|
+
],
|
|
137
154
|
// Notification commands
|
|
138
155
|
"requestNotificationPermission": [
|
|
139
156
|
"type": "object",
|
|
@@ -345,6 +362,7 @@ class BridgeMessageValidator {
|
|
|
345
362
|
"getDeviceInfo",
|
|
346
363
|
"getNetworkStatus",
|
|
347
364
|
"logger",
|
|
365
|
+
"googleSignIn",
|
|
348
366
|
"getSubscribedTopics",
|
|
349
367
|
"requestNotificationPermission",
|
|
350
368
|
"registerForPushNotifications",
|
|
@@ -258,6 +258,8 @@ extension NativeBridge: WKScriptMessageHandler {
|
|
|
258
258
|
case "requestHapticFeedback":
|
|
259
259
|
let feedbackType = delegateHandler.extractFeedbackType(from: params)
|
|
260
260
|
commandHandler.requestHapticFeedback(feedbackType: feedbackType)
|
|
261
|
+
case "googleSignIn":
|
|
262
|
+
commandHandler.googleSignIn(params: params)
|
|
261
263
|
|
|
262
264
|
// Notification commands (handled via protocol)
|
|
263
265
|
case "requestNotificationPermission":
|
|
@@ -9,6 +9,9 @@ import UIKit
|
|
|
9
9
|
import WebKit
|
|
10
10
|
import os
|
|
11
11
|
import CatalystCore
|
|
12
|
+
#if canImport(GoogleSignIn)
|
|
13
|
+
import GoogleSignIn
|
|
14
|
+
#endif
|
|
12
15
|
|
|
13
16
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "AppDelegate")
|
|
14
17
|
|
|
@@ -68,4 +71,14 @@ public class AppDelegate: NSObject, UIApplicationDelegate {
|
|
|
68
71
|
userInfo: ["notification": userInfo, "completion": completionHandler]
|
|
69
72
|
)
|
|
70
73
|
}
|
|
74
|
+
|
|
75
|
+
public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
|
76
|
+
#if canImport(GoogleSignIn)
|
|
77
|
+
if GIDSignIn.sharedInstance.handle(url) {
|
|
78
|
+
logWithTimestamp("🔐 Google Sign-In handled openURL callback")
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
#endif
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
71
84
|
}
|
|
@@ -9,6 +9,9 @@ import SwiftUI
|
|
|
9
9
|
import UIKit
|
|
10
10
|
import os
|
|
11
11
|
import CatalystCore
|
|
12
|
+
#if canImport(GoogleSignIn)
|
|
13
|
+
import GoogleSignIn
|
|
14
|
+
#endif
|
|
12
15
|
|
|
13
16
|
// IMPORTANT: This is the App target, so it CAN import CatalystNotifications safely
|
|
14
17
|
// This does not create a compile-time dependency in CatalystCore
|
|
@@ -82,6 +85,13 @@ struct iosnativeWebViewApp: App {
|
|
|
82
85
|
.onAppear {
|
|
83
86
|
logWithTimestamp("🎨 WindowGroup ContentView appeared")
|
|
84
87
|
}
|
|
88
|
+
.onOpenURL { url in
|
|
89
|
+
#if canImport(GoogleSignIn)
|
|
90
|
+
if GIDSignIn.sharedInstance.handle(url) {
|
|
91
|
+
logWithTimestamp("🔐 Google Sign-In handled via onOpenURL")
|
|
92
|
+
}
|
|
93
|
+
#endif
|
|
94
|
+
}
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
}
|