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 + Screenshot 2025-11-01 at 00 38 41 + + Step 2: From opened dialog > Add local + Screenshot 2025-10-30 at 21 10 28 + + Step 3: Continue Add package + + Screenshot 2025-11-01 at 00 38 51 + + Step 4: Add GoogleSignIn 9.0.0 or the one compatible with your project as below: + Screenshot 2025-11-02 at 20 51 45 + +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" }