Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions ComposeAuth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<dict>
<key>CFBundleIdentifier</key>
<string>io.github.jan.supabase.ios</string>
<key>GIDClientID</key>
<string>YOUR_CLIENT_ID</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>YOUR_REVERSED_CLIENT_ID</string>
</array>
</dict>
</array>
</dict>
```

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
<img width="750" height="509" alt="Screenshot 2025-11-01 at 00 38 41" src="https://github.com/user-attachments/assets/3b0f1b05-8946-43bf-b46b-e21ed70b811b" />

Step 2: From opened dialog > Add local
<img width="750" height="618" alt="Screenshot 2025-10-30 at 21 10 28" src="https://github.com/user-attachments/assets/fa35f129-a3b1-4403-9e92-682738f1ada6" />

Step 3: Continue Add package

<img width="750" height="632" alt="Screenshot 2025-11-01 at 00 38 51" src="https://github.com/user-attachments/assets/92b709fa-57d2-41b5-b066-e391f132231c" />

Step 4: Add GoogleSignIn 9.0.0 or the one compatible with your project as below:
<img width="750" height="452" alt="Screenshot 2025-11-02 at 20 51 45" src="https://github.com/user-attachments/assets/7bdcd805-c3a6-4500-ace9-f5a389cdabdf" />

7. Build your app. It should work now.

34 changes: 34 additions & 0 deletions ComposeAuth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -62,4 +77,23 @@ tasks.withType<LintModelMetadataTask> {
}
tasks.withType<AndroidLintAnalysisTask> {
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")
}
}
}
}
27 changes: 27 additions & 0 deletions ComposeAuth/exportedNativeBridge/Package.swift
Original file line number Diff line number Diff line change
@@ -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"

)

]
)
3 changes: 3 additions & 0 deletions ComposeAuth/exportedNativeBridge/Sources/DummySPMFile.swift
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
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()
}
48 changes: 48 additions & 0 deletions ComposeAuth/src/swift/nativeBridge/GoogleSignInController.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
6 changes: 2 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<String>("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 }
Expand All @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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" }
Expand All @@ -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" }

Expand Down
Loading