Skip to content

Commit fd8d872

Browse files
authored
Merge pull request #45 from hieuwu/main
Implement Native Google OAuth for Apple targets
2 parents eb2818c + 320115b commit fd8d872

File tree

9 files changed

+239
-9
lines changed

9 files changed

+239
-9
lines changed

ComposeAuth/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,50 @@ Here is a small guide on how to use Native Google Auth on Android:
9797
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`)
9898
5. Put the Android OAuth client id to the authorized client ids in the Supabase Dashboard
9999
6. Use the **Web** OAuth client id in the Compose Auth plugin
100+
101+
# Native Google Auth on iOS
102+
Before start, make sure your iOS app works well first, it would be easier to isolate the issue from this step to resolve
103+
1. Create a project in your [Google Cloud Developer Console](https://console.cloud.google.com/)
104+
2. Create OAuth credentials for a Web application, iOS version
105+
3. Put in Client ID of Web OAuth, Apple OAuth in your Supabase Auth Settings for Google in the Dashboard
106+
4. Set up XCode 26
107+
Add Client ID and Reversed Client ID (Retrieved from iOS OAuth details on Google Console). Your `Info.plist` will look like this:
108+
```swift
109+
<dict>
110+
<key>CFBundleIdentifier</key>
111+
<string>io.github.jan.supabase.ios</string>
112+
<key>GIDClientID</key>
113+
<string>YOUR_CLIENT_ID</string>
114+
<key>CADisableMinimumFrameDurationOnPhone</key>
115+
<true/>
116+
<key>CFBundleURLTypes</key>
117+
<array>
118+
<dict>
119+
<key>CFBundleTypeRole</key>
120+
<string>Editor</string>
121+
<key>CFBundleURLSchemes</key>
122+
<array>
123+
<string>YOUR_REVERSED_CLIENT_ID</string>
124+
</array>
125+
</dict>
126+
</array>
127+
</dict>
128+
```
129+
130+
5. Download `exportedNativeBridge` at `/ComposeAuth/exportedNativeBridge` in this repository
131+
In XCode, add it as dependency
132+
Step 1: Right click on the left tool bar > Select Add dependencies
133+
<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" />
134+
135+
Step 2: From opened dialog > Add local
136+
<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" />
137+
138+
Step 3: Continue Add package
139+
140+
<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" />
141+
142+
Step 4: Add GoogleSignIn 9.0.0 or the one compatible with your project as below:
143+
<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" />
144+
145+
7. Build your app. It should work now.
146+

ComposeAuth/build.gradle.kts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ plugins {
88
id(libs.plugins.android.library.get().pluginId)
99
id(libs.plugins.compose.plugin.get().pluginId)
1010
alias(libs.plugins.compose.compiler)
11+
alias(libs.plugins.spm)
1112
}
1213

1314
description = "Extends gotrue-kt with Native Auth composables"
@@ -32,6 +33,20 @@ kotlin {
3233
}
3334
jvmToolchain(11)
3435
composeTargets(JvmTarget.JVM_11)
36+
iosTargets()
37+
targets.forEach {
38+
if (it is org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget) {
39+
it.binaries.framework {
40+
baseName = "common"
41+
isStatic = true
42+
}
43+
it.compilations {
44+
val main by getting {
45+
cinterops.create("nativeBridge")
46+
}
47+
}
48+
}
49+
}
3550
sourceSets {
3651
commonMain {
3752
dependencies {
@@ -62,4 +77,23 @@ tasks.withType<LintModelMetadataTask> {
6277
}
6378
tasks.withType<AndroidLintAnalysisTask> {
6479
dependsOn("generateResourceAccessorsForAndroidUnitTest")
80+
}
81+
82+
swiftPackageConfig {
83+
create("nativeBridge") {
84+
dependency {
85+
linkerOpts =
86+
listOf("-ObjC", "-fObjC")
87+
remotePackageVersion(
88+
url = uri("https://github.com/google/GoogleSignIn-iOS.git"),
89+
products = {
90+
add("GoogleSignIn", exportToKotlin = true)
91+
},
92+
version = "9.0.0",
93+
)
94+
exportedPackageSettings {
95+
includeProduct = listOf("GoogleSignIn")
96+
}
97+
}
98+
}
6599
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// swift-tools-version: 5.9
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "exportedNativeBridge",
6+
platforms: [.iOS("12.0"), .macOS("10.13"), .tvOS("12.0"), .watchOS("4.0")],
7+
products: [
8+
.library(
9+
name: "exportedNativeBridge",
10+
type: .static,
11+
targets: ["exportedNativeBridge"])
12+
],
13+
dependencies: [
14+
.package(url: "https://github.com/google/GoogleSignIn-iOS.git", exact: "9.0.0")
15+
],
16+
targets: [
17+
.target(
18+
name: "exportedNativeBridge",
19+
dependencies: [
20+
.product(name: "GoogleSignIn", package: "GoogleSignIn-iOS")
21+
],
22+
path: "Sources"
23+
24+
)
25+
26+
]
27+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This file has been generated by Spm4Kmp plugin
2+
// DO NO EDIT THIS FILE AS IT WILL BE OVERWRITTEN ON EACH BUILD
3+
import Foundation

ComposeAuth/src/appleMain/kotlin/io/github/jan/supabase/compose/auth/composable/GoogleAuth.kt

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@ package io.github.jan.supabase.compose.auth.composable
33
import androidx.compose.runtime.Composable
44
import io.github.jan.supabase.compose.auth.ComposeAuth
55
import io.github.jan.supabase.compose.auth.IdTokenCallback
6-
import io.github.jan.supabase.compose.auth.defaultLoginBehavior
6+
import androidx.compose.runtime.LaunchedEffect
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.rememberCoroutineScope
9+
import io.github.jan.supabase.auth.providers.Google
10+
import io.github.jan.supabase.compose.auth.hash
11+
import io.github.jan.supabase.logging.d
12+
import io.github.jan.supabase.logging.e
13+
import kotlinx.cinterop.ExperimentalForeignApi
14+
import kotlinx.coroutines.ensureActive
15+
import kotlinx.coroutines.launch
16+
import nativeBridge.GoogleSignInController
717

818
/**
919
* Composable function that implements Native Google Auth.
@@ -15,12 +25,73 @@ import io.github.jan.supabase.compose.auth.defaultLoginBehavior
1525
* @param fallback Fallback function for unsupported platforms
1626
* @return [NativeSignInState]
1727
*/
28+
@OptIn(ExperimentalForeignApi::class)
1829
@Composable
1930
actual fun ComposeAuth.rememberSignInWithGoogle(
2031
onResult: (NativeSignInResult) -> Unit,
2132
onIdToken: IdTokenCallback,
2233
type: GoogleDialogType,
2334
fallback: suspend () -> Unit
24-
): NativeSignInState = defaultLoginBehavior(fallback)
35+
): NativeSignInState {
36+
val state = remember { NativeSignInState(this.serializer) }
37+
val scope = rememberCoroutineScope()
2538

26-
internal actual suspend fun handleGoogleSignOut() = Unit
39+
val googleSignInController = remember {
40+
GoogleSignInController()
41+
}
42+
43+
LaunchedEffect(key1 = state.status) {
44+
if (state.status is NativeSignInStatus.Started) {
45+
val startedStatus = state.status as NativeSignInStatus.Started
46+
ComposeAuth.logger.d { "Starting Native Google Sign In flow on iOS" }
47+
try {
48+
if (config.googleLoginConfig != null) {
49+
ComposeAuth.logger.d { "Google login config found" }
50+
val hashedNonce = startedStatus.nonce?.hash()
51+
ComposeAuth.logger.d { "Native Google Sign In Flow${if (hashedNonce != null) " with hashed nonce: $hashedNonce" else ""}" }
52+
googleSignInController.signInCompletion(
53+
completion = { idToken, errorMessage, isCancelled ->
54+
scope.launch {
55+
if (isCancelled) {
56+
ComposeAuth.logger.d { "Native Google Sign In Flow was closed by user" }
57+
onResult.invoke(NativeSignInResult.ClosedByUser)
58+
} else if (idToken != null) {
59+
ComposeAuth.logger.d { "Id token available" }
60+
onIdToken.invoke(
61+
composeAuth = this@rememberSignInWithGoogle,
62+
result = IdTokenCallback.Result(
63+
idToken = idToken,
64+
provider = Google,
65+
nonce = startedStatus.nonce,
66+
extraData = startedStatus.extraData
67+
)
68+
)
69+
onResult.invoke(NativeSignInResult.Success)
70+
} else if (errorMessage != null) {
71+
ComposeAuth.logger.d { "Error happens due to: $errorMessage" }
72+
onResult.invoke(NativeSignInResult.Error(errorMessage))
73+
} else {
74+
ComposeAuth.logger.e { "Error while logging into Supabase with Google ID Token Credential" }
75+
}
76+
}
77+
},
78+
nonce = startedStatus.nonce?.hash()
79+
)
80+
} else {
81+
fallback.invoke()
82+
}
83+
} catch (e: Exception) {
84+
coroutineContext.ensureActive()
85+
onResult.invoke(NativeSignInResult.Error(e.message ?: "error"))
86+
} finally {
87+
state.reset()
88+
}
89+
}
90+
}
91+
return state
92+
}
93+
94+
@OptIn(ExperimentalForeignApi::class)
95+
internal actual suspend fun handleGoogleSignOut() {
96+
GoogleSignInController.signOutGoogle()
97+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Foundation
2+
import GoogleSignIn
3+
import UIKit // Needed for UIViewController
4+
5+
// Define a public typealias for the completion handler closure
6+
public typealias GoogleSignInCompletionHandler = (String?, String?, Bool) -> Void
7+
8+
@objcMembers public class GoogleSignInController: NSObject {
9+
10+
public override init() {
11+
super.init()
12+
}
13+
14+
public func signIn(
15+
completion: @escaping GoogleSignInCompletionHandler,
16+
nonce: String? = nil
17+
) {
18+
guard let presentingViewController = UIApplication.shared.keyWindow?.rootViewController else {
19+
completion(nil, "No root view controller found", false)
20+
return
21+
}
22+
GoogleSignIn.GIDSignIn.sharedInstance.signIn(
23+
withPresenting: presentingViewController,
24+
hint: nil,
25+
additionalScopes: nil,
26+
nonce: nonce
27+
) { result, error in
28+
if let error = error {
29+
if let nsError = error as NSError?, nsError.code == -5 {
30+
completion(nil, nil, true)
31+
} else {
32+
completion(nil, error.localizedDescription, false)
33+
}
34+
return
35+
}
36+
37+
guard let idToken = result?.user.idToken?.tokenString else {
38+
completion(nil, "No ID token returned", false)
39+
return
40+
}
41+
completion(idToken, nil, false)
42+
}
43+
}
44+
45+
@objc public static func signOutGoogle() {
46+
GoogleSignIn.GIDSignIn.sharedInstance.signOut()
47+
}
48+
}

build.gradle.kts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
2-
3-
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
41
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport
52
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
63

7-
val excludedModules = listOf<String>("test-common")
4+
val excludedModules = listOf("test-common")
85

96
private val libraryFilter = { withFilter: Boolean ->
107
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 {
2118
id(libs.plugins.detekt.get().pluginId) apply false
2219
id(libs.plugins.dokka.get().pluginId)
2320
alias(libs.plugins.kotlinx.plugin.serialization) apply false
21+
alias(libs.plugins.spm) apply false
2422
id(libs.plugins.maven.publish.get().pluginId) apply false
2523
id(libs.plugins.power.assert.get().pluginId) apply false
2624
}

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ kotlin.mpp.androidSourceSetLayoutVersion=2
55
kotlin.native.ignoreDisabledTargets=true
66
org.gradle.parallel=true
77
kotlin.suppressGradlePluginWarnings=IncorrectCompileOnlyDependencyWarning
8+
kotlin.mpp.enableCInteropCommonization=true
89

910
org.jetbrains.compose.experimental.uikit.enabled=true
1011
org.jetbrains.compose.experimental.jscanvas.enabled=true

gradle/libs.versions.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ dokka = "2.1.0"
55
coroutines = "1.10.2"
66
androidx-activity-compose = "1.11.0"
77
multiplatform-settings = "1.3.0"
8-
agp = "8.9.2"
8+
agp = "8.11.1"
99
maven-publish = "0.34.0"
1010
apollo-kotlin = "4.3.3"
1111
detekt = "1.23.8"
@@ -18,7 +18,7 @@ coil3 = "3.3.0"
1818
okio = "3.16.2"
1919
credentials = "1.5.0"
2020
sketch = "4.3.1"
21-
21+
spm = "1.0.1"
2222
[plugins]
2323
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2424
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
@@ -29,6 +29,7 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
2929

3030
compose-plugin = { id = "org.jetbrains.compose", version.ref = "compose" }
3131
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
32+
spm = { id = "io.github.frankois944.spmForKmp", version.ref = "spm" }
3233

3334
power-assert = { id = "org.jetbrains.kotlin.plugin.power-assert", version.ref = "kotlin" }
3435

0 commit comments

Comments
 (0)