diff --git a/ComposeAuth/README.md b/ComposeAuth/README.md
index 9277e70..a44d374 100644
--- a/ComposeAuth/README.md
+++ b/ComposeAuth/README.md
@@ -97,3 +97,50 @@ Here is a small guide on how to use Native Google Auth on Android:
4. Create OAuth credentials for an Android app, and put in your package name and SHA-1 certificate (which you can get by using `gradlew signingReport`)
5. Put the Android OAuth client id to the authorized client ids in the Supabase Dashboard
6. Use the **Web** OAuth client id in the Compose Auth plugin
+
+# Native Google Auth on iOS
+Before start, make sure your iOS app works well first, it would be easier to isolate the issue from this step to resolve
+1. Create a project in your [Google Cloud Developer Console](https://console.cloud.google.com/)
+2. Create OAuth credentials for a Web application, iOS version
+3. Put in Client ID of Web OAuth, Apple OAuth in your Supabase Auth Settings for Google in the Dashboard
+4. Set up XCode 26
+ Add Client ID and Reversed Client ID (Retrieved from iOS OAuth details on Google Console). Your `Info.plist` will look like this:
+```swift
+
+ CFBundleIdentifier
+ io.github.jan.supabase.ios
+ GIDClientID
+ YOUR_CLIENT_ID
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLSchemes
+
+ YOUR_REVERSED_CLIENT_ID
+
+
+
+
+```
+
+5. Download `exportedNativeBridge` at `/ComposeAuth/exportedNativeBridge` in this repository
+ In XCode, add it as dependency
+ Step 1: Right click on the left tool bar > Select Add dependencies
+
+
+ Step 2: From opened dialog > Add local
+
+
+ Step 3: Continue Add package
+
+
+
+ Step 4: Add GoogleSignIn 9.0.0 or the one compatible with your project as below:
+
+
+7. Build your app. It should work now.
+
diff --git a/ComposeAuth/build.gradle.kts b/ComposeAuth/build.gradle.kts
index 3dfd79e..fa98cc7 100644
--- a/ComposeAuth/build.gradle.kts
+++ b/ComposeAuth/build.gradle.kts
@@ -8,6 +8,7 @@ plugins {
id(libs.plugins.android.library.get().pluginId)
id(libs.plugins.compose.plugin.get().pluginId)
alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.spm)
}
description = "Extends gotrue-kt with Native Auth composables"
@@ -32,6 +33,20 @@ kotlin {
}
jvmToolchain(11)
composeTargets(JvmTarget.JVM_11)
+ iosTargets()
+ targets.forEach {
+ if (it is org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget) {
+ it.binaries.framework {
+ baseName = "common"
+ isStatic = true
+ }
+ it.compilations {
+ val main by getting {
+ cinterops.create("nativeBridge")
+ }
+ }
+ }
+ }
sourceSets {
commonMain {
dependencies {
@@ -62,4 +77,23 @@ tasks.withType {
}
tasks.withType {
dependsOn("generateResourceAccessorsForAndroidUnitTest")
+}
+
+swiftPackageConfig {
+ create("nativeBridge") {
+ dependency {
+ linkerOpts =
+ listOf("-ObjC", "-fObjC")
+ remotePackageVersion(
+ url = uri("https://github.com/google/GoogleSignIn-iOS.git"),
+ products = {
+ add("GoogleSignIn", exportToKotlin = true)
+ },
+ version = "9.0.0",
+ )
+ exportedPackageSettings {
+ includeProduct = listOf("GoogleSignIn")
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/ComposeAuth/exportedNativeBridge/Package.swift b/ComposeAuth/exportedNativeBridge/Package.swift
new file mode 100644
index 0000000..7a16e3a
--- /dev/null
+++ b/ComposeAuth/exportedNativeBridge/Package.swift
@@ -0,0 +1,27 @@
+// swift-tools-version: 5.9
+import PackageDescription
+
+let package = Package(
+ name: "exportedNativeBridge",
+ platforms: [.iOS("12.0"), .macOS("10.13"), .tvOS("12.0"), .watchOS("4.0")],
+ products: [
+ .library(
+ name: "exportedNativeBridge",
+ type: .static,
+ targets: ["exportedNativeBridge"])
+ ],
+ dependencies: [
+ .package(url: "https://github.com/google/GoogleSignIn-iOS.git", exact: "9.0.0")
+ ],
+ targets: [
+ .target(
+ name: "exportedNativeBridge",
+ dependencies: [
+ .product(name: "GoogleSignIn", package: "GoogleSignIn-iOS")
+ ],
+ path: "Sources"
+
+ )
+
+ ]
+)
diff --git a/ComposeAuth/exportedNativeBridge/Sources/DummySPMFile.swift b/ComposeAuth/exportedNativeBridge/Sources/DummySPMFile.swift
new file mode 100644
index 0000000..24a531d
--- /dev/null
+++ b/ComposeAuth/exportedNativeBridge/Sources/DummySPMFile.swift
@@ -0,0 +1,3 @@
+// This file has been generated by Spm4Kmp plugin
+// DO NO EDIT THIS FILE AS IT WILL BE OVERWRITTEN ON EACH BUILD
+import Foundation
\ No newline at end of file
diff --git a/ComposeAuth/src/appleMain/kotlin/io/github/jan/supabase/compose/auth/composable/GoogleAuth.kt b/ComposeAuth/src/appleMain/kotlin/io/github/jan/supabase/compose/auth/composable/GoogleAuth.kt
index 061db34..cffa798 100644
--- a/ComposeAuth/src/appleMain/kotlin/io/github/jan/supabase/compose/auth/composable/GoogleAuth.kt
+++ b/ComposeAuth/src/appleMain/kotlin/io/github/jan/supabase/compose/auth/composable/GoogleAuth.kt
@@ -3,7 +3,17 @@ package io.github.jan.supabase.compose.auth.composable
import androidx.compose.runtime.Composable
import io.github.jan.supabase.compose.auth.ComposeAuth
import io.github.jan.supabase.compose.auth.IdTokenCallback
-import io.github.jan.supabase.compose.auth.defaultLoginBehavior
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import io.github.jan.supabase.auth.providers.Google
+import io.github.jan.supabase.compose.auth.hash
+import io.github.jan.supabase.logging.d
+import io.github.jan.supabase.logging.e
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+import nativeBridge.GoogleSignInController
/**
* Composable function that implements Native Google Auth.
@@ -15,12 +25,73 @@ import io.github.jan.supabase.compose.auth.defaultLoginBehavior
* @param fallback Fallback function for unsupported platforms
* @return [NativeSignInState]
*/
+@OptIn(ExperimentalForeignApi::class)
@Composable
actual fun ComposeAuth.rememberSignInWithGoogle(
onResult: (NativeSignInResult) -> Unit,
onIdToken: IdTokenCallback,
type: GoogleDialogType,
fallback: suspend () -> Unit
-): NativeSignInState = defaultLoginBehavior(fallback)
+): NativeSignInState {
+ val state = remember { NativeSignInState(this.serializer) }
+ val scope = rememberCoroutineScope()
-internal actual suspend fun handleGoogleSignOut() = Unit
\ No newline at end of file
+ val googleSignInController = remember {
+ GoogleSignInController()
+ }
+
+ LaunchedEffect(key1 = state.status) {
+ if (state.status is NativeSignInStatus.Started) {
+ val startedStatus = state.status as NativeSignInStatus.Started
+ ComposeAuth.logger.d { "Starting Native Google Sign In flow on iOS" }
+ try {
+ if (config.googleLoginConfig != null) {
+ ComposeAuth.logger.d { "Google login config found" }
+ val hashedNonce = startedStatus.nonce?.hash()
+ ComposeAuth.logger.d { "Native Google Sign In Flow${if (hashedNonce != null) " with hashed nonce: $hashedNonce" else ""}" }
+ googleSignInController.signInCompletion(
+ completion = { idToken, errorMessage, isCancelled ->
+ scope.launch {
+ if (isCancelled) {
+ ComposeAuth.logger.d { "Native Google Sign In Flow was closed by user" }
+ onResult.invoke(NativeSignInResult.ClosedByUser)
+ } else if (idToken != null) {
+ ComposeAuth.logger.d { "Id token available" }
+ onIdToken.invoke(
+ composeAuth = this@rememberSignInWithGoogle,
+ result = IdTokenCallback.Result(
+ idToken = idToken,
+ provider = Google,
+ nonce = startedStatus.nonce,
+ extraData = startedStatus.extraData
+ )
+ )
+ onResult.invoke(NativeSignInResult.Success)
+ } else if (errorMessage != null) {
+ ComposeAuth.logger.d { "Error happens due to: $errorMessage" }
+ onResult.invoke(NativeSignInResult.Error(errorMessage))
+ } else {
+ ComposeAuth.logger.e { "Error while logging into Supabase with Google ID Token Credential" }
+ }
+ }
+ },
+ nonce = startedStatus.nonce?.hash()
+ )
+ } else {
+ fallback.invoke()
+ }
+ } catch (e: Exception) {
+ coroutineContext.ensureActive()
+ onResult.invoke(NativeSignInResult.Error(e.message ?: "error"))
+ } finally {
+ state.reset()
+ }
+ }
+ }
+ return state
+}
+
+@OptIn(ExperimentalForeignApi::class)
+internal actual suspend fun handleGoogleSignOut() {
+ GoogleSignInController.signOutGoogle()
+}
\ No newline at end of file
diff --git a/ComposeAuth/src/swift/nativeBridge/GoogleSignInController.swift b/ComposeAuth/src/swift/nativeBridge/GoogleSignInController.swift
new file mode 100644
index 0000000..2d6f423
--- /dev/null
+++ b/ComposeAuth/src/swift/nativeBridge/GoogleSignInController.swift
@@ -0,0 +1,48 @@
+import Foundation
+import GoogleSignIn
+import UIKit // Needed for UIViewController
+
+// Define a public typealias for the completion handler closure
+public typealias GoogleSignInCompletionHandler = (String?, String?, Bool) -> Void
+
+@objcMembers public class GoogleSignInController: NSObject {
+
+ public override init() {
+ super.init()
+ }
+
+ public func signIn(
+ completion: @escaping GoogleSignInCompletionHandler,
+ nonce: String? = nil
+ ) {
+ guard let presentingViewController = UIApplication.shared.keyWindow?.rootViewController else {
+ completion(nil, "No root view controller found", false)
+ return
+ }
+ GoogleSignIn.GIDSignIn.sharedInstance.signIn(
+ withPresenting: presentingViewController,
+ hint: nil,
+ additionalScopes: nil,
+ nonce: nonce
+ ) { result, error in
+ if let error = error {
+ if let nsError = error as NSError?, nsError.code == -5 {
+ completion(nil, nil, true)
+ } else {
+ completion(nil, error.localizedDescription, false)
+ }
+ return
+ }
+
+ guard let idToken = result?.user.idToken?.tokenString else {
+ completion(nil, "No ID token returned", false)
+ return
+ }
+ completion(idToken, nil, false)
+ }
+ }
+
+ @objc public static func signOutGoogle() {
+ GoogleSignIn.GIDSignIn.sharedInstance.signOut()
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 70f2fa6..1e08c68 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,10 +1,7 @@
-@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
-
-import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
-val excludedModules = listOf("test-common")
+val excludedModules = listOf("test-common")
private val libraryFilter = { withFilter: Boolean ->
allprojects.filter { it.name !in excludedModules && !it.path.contains("sample") && if(withFilter) true else it.name != "bom" && it.name != it.rootProject.name }
@@ -21,6 +18,7 @@ plugins {
id(libs.plugins.detekt.get().pluginId) apply false
id(libs.plugins.dokka.get().pluginId)
alias(libs.plugins.kotlinx.plugin.serialization) apply false
+ alias(libs.plugins.spm) apply false
id(libs.plugins.maven.publish.get().pluginId) apply false
id(libs.plugins.power.assert.get().pluginId) apply false
}
diff --git a/gradle.properties b/gradle.properties
index 6bd36b3..41811a4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,6 +5,7 @@ kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.native.ignoreDisabledTargets=true
org.gradle.parallel=true
kotlin.suppressGradlePluginWarnings=IncorrectCompileOnlyDependencyWarning
+kotlin.mpp.enableCInteropCommonization=true
org.jetbrains.compose.experimental.uikit.enabled=true
org.jetbrains.compose.experimental.jscanvas.enabled=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index eb744ef..0c4eea6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,7 +5,7 @@ dokka = "2.1.0"
coroutines = "1.10.2"
androidx-activity-compose = "1.11.0"
multiplatform-settings = "1.3.0"
-agp = "8.9.2"
+agp = "8.11.1"
maven-publish = "0.34.0"
apollo-kotlin = "4.3.3"
detekt = "1.23.8"
@@ -18,7 +18,7 @@ coil3 = "3.3.0"
okio = "3.16.2"
credentials = "1.5.0"
sketch = "4.3.1"
-
+spm = "1.0.1"
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
@@ -29,6 +29,7 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
compose-plugin = { id = "org.jetbrains.compose", version.ref = "compose" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+spm = { id = "io.github.frankois944.spmForKmp", version.ref = "spm" }
power-assert = { id = "org.jetbrains.kotlin.plugin.power-assert", version.ref = "kotlin" }