Skip to content

Commit 94f4042

Browse files
committed
Add OAuth2 handling
1 parent a98cca2 commit 94f4042

23 files changed

+553
-772
lines changed

src/SDK/Language/KMP.php

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public function getFilters(): array
2525
});
2626

2727
$filters[] = new TwigFilter('webAuthServices', function (array $spec) {
28-
return $this->findWebAuthServices($spec);
28+
return $this->getWebAuthServices($spec);
2929
});
3030

3131
$filters[] = new TwigFilter('propertySerializerName', function (array $property) {
@@ -43,7 +43,7 @@ protected function getReturnType(array $method, array $spec, string $namespace,
4343
return parent::getReturnType($method, $spec, $namespace, $generic, $withGeneric);
4444
}
4545

46-
protected function findWebAuthServices(array $spec): array
46+
protected function getWebAuthServices(array $spec): array
4747
{
4848
$webAuthServices = [];
4949
foreach ($spec['services'] as $service) {
@@ -53,7 +53,9 @@ protected function findWebAuthServices(array $spec): array
5353
if ($method['type'] === 'webAuth') {
5454
$webAuthMethods[] = [
5555
'methodName' => $method['name'],
56-
'parameters' => $method['parameters']
56+
'parameters' => $method['parameters'],
57+
'path' => $method['path'],
58+
'auth' => $method['auth']
5759
];
5860
$hasWebAuth = true;
5961
}
@@ -239,6 +241,7 @@ public function getFiles(): array
239241
'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/WebAuthComponent.kt',
240242
'template' => '/kmp/shared/src/commonMain/kotlin/io/package/WebAuthComponent.kt.twig',
241243
],
244+
242245

243246
// Coroutines
244247
[
@@ -375,28 +378,18 @@ public function getFiles(): array
375378
// Cookies
376379
[
377380
'scope' => 'default',
378-
'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/AndroidCookieStorage.kt',
379-
'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/AndroidCookieStorage.kt.twig',
380-
],
381-
[
382-
'scope' => 'default',
383-
'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/Extensions.kt',
384-
'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/Extensions.kt.twig',
385-
],
386-
[
387-
'scope' => 'default',
388-
'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/InternalCookie.kt',
389-
'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/InternalCookie.kt.twig',
381+
'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/SerializableCookie.kt',
382+
'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/SerializableCookie.kt.twig',
390383
],
391384
[
392385
'scope' => 'default',
393-
'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/stores/InMemoryCookieStore.kt',
394-
'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/stores/InMemoryCookieStore.kt.twig',
386+
'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/stores/DataStoreManager.kt',
387+
'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/stores/DataStoreManager.kt.twig',
395388
],
396389
[
397390
'scope' => 'default',
398-
'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/stores/SharedPreferencesCookieStore.kt',
399-
'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/stores/SharedPreferencesCookieStore.kt.twig',
391+
'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/stores/DataStoreCookieStorage.kt',
392+
'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/stores/DataStoreCookieStorage.kt.twig',
400393
],
401394

402395
// Extensions

templates/kmp/gradle/libs.versions.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
agp = "8.7.3"
33
appcompat = "1.7.0"
44
coreKtx = "1.15.0"
5+
datastore = "1.1.2"
56
espressoCore = "3.6.1"
67
gson = "2.10.1"
78
junit = "4.13.2"
@@ -15,8 +16,8 @@ kotlinxDatetime = "0.6.1"
1516
kotlinxSerializationJson = "1.7.3"
1617
napier = "2.7.1"
1718
lifecycleLivedataKtx = "2.8.7"
18-
navigationUiKtx = "2.8.5"
19-
firebaseCrashlyticsBuildtools = "3.0.2"
19+
navigationUiKtx = "2.8.7"
20+
firebaseCrashlyticsBuildtools = "3.0.3"
2021
okio = "3.10.2"
2122
robolectric = "4.14.1"
2223
androidx-test = "1.6.1"
@@ -25,6 +26,8 @@ test-runner = "1.6.2"
2526

2627

2728
[libraries]
29+
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
30+
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
2831
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
2932
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
3033
androidx-test-core = { module = "androidx.test:core-ktx", version.ref = "androidx-test" }

templates/kmp/shared/build.gradle.kts.twig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ kotlin {
118118
implementation(libs.firebase.messaging)
119119
implementation(libs.ktor.client.okhttp)
120120
implementation(libs.ktor.client.logging)
121+
implementation(libs.androidx.datastore)
122+
implementation(libs.androidx.datastore.preferences)
121123
implementation(libs.gson)
122124
implementation(project.dependencies.platform("com.google.firebase:firebase-bom:33.6.0"))
123125
}

templates/kmp/shared/src/androidMain/kotlin/io/package/Client.android.kt.twig

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ package {{ sdk.namespace | caseDot }}
22

33
import android.content.Context
44
import android.content.pm.PackageManager
5+
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
6+
import {{ sdk.namespace | caseDot }}.cookies.stores.DataStoreCookieStorage
7+
import {{ sdk.namespace | caseDot }}.cookies.stores.DataStoreManager
58
import kotlinx.coroutines.Dispatchers
69
import kotlinx.coroutines.Job
10+
import okio.Path.Companion.toPath
711

812
actual class Client constructor(
913
private val context: Context,
@@ -13,6 +17,12 @@ actual class Client constructor(
1317
) : BaseClient<Client>(endpoint, endpointRealtime) {
1418
actual override val coroutineContext = Job() + Dispatchers.Default
1519

20+
private val dataStoreManager = DataStoreManager(
21+
PreferenceDataStoreFactory.createWithPath (
22+
produceFile = { context.filesDir.resolve("appwriteCookies.preferences_pb").absolutePath.toPath() }
23+
))
24+
val dataStoreCookieStorage = DataStoreCookieStorage(dataStoreManager)
25+
1626
private val appVersion by lazy {
1727
try {
1828
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
@@ -24,7 +34,8 @@ actual class Client constructor(
2434
}
2535

2636
init {
27-
httpClient = createHttpClient(context, selfSigned)
37+
38+
httpClient = createHttpClient(selfSigned, dataStoreCookieStorage)
2839
headers = mutableMapOf(
2940
"content-type" to "application/json",
3041
"origin" to "{{ spec.title | caseLower }}-android://${context.packageName}",
@@ -42,7 +53,7 @@ actual class Client constructor(
4253
}
4354

4455
actual fun setSelfSigned(value: Boolean): Client {
45-
httpClient = createHttpClient(context, value)
56+
httpClient = createHttpClient(value, dataStoreCookieStorage)
4657
return this
4758
}
48-
}
59+
}

templates/kmp/shared/src/androidMain/kotlin/io/package/HttpClientConfig.kt.twig

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package {{ sdk.namespace | caseDot }}
22

3-
import android.content.Context
4-
import {{ sdk.namespace | caseDot }}.cookies.AndroidCookieStorage
5-
import io.github.aakira.napier.Napier
3+
import {{ sdk.namespace | caseDot }}.cookies.stores.DataStoreCookieStorage
64
import io.ktor.client.HttpClient
75
import io.ktor.client.engine.okhttp.OkHttp
86
import io.ktor.client.plugins.HttpTimeout
@@ -18,9 +16,9 @@ import javax.net.ssl.TrustManagerFactory
1816
import javax.net.ssl.X509TrustManager
1917
import kotlin.time.Duration.Companion.seconds
2018

21-
fun createHttpClient(context: Context, selfSigned: Boolean) = HttpClient(OkHttp) {
19+
fun createHttpClient(selfSigned: Boolean, dataStoreCookieStorage: DataStoreCookieStorage) = HttpClient(OkHttp) {
2220
install(HttpCookies) {
23-
storage = AndroidCookieStorage(context)
21+
storage = dataStoreCookieStorage
2422
}
2523
install(WebSockets) {
2624
pingInterval = 30.seconds
@@ -32,7 +30,7 @@ fun createHttpClient(context: Context, selfSigned: Boolean) = HttpClient(OkHttp)
3230
socketTimeoutMillis = 30000
3331
}
3432
install(ContentNegotiation) {
35-
json({{ sdk.namespace | caseDot }}.extensions.json)
33+
json(io.appwrite.extensions.json)
3634
}
3735

3836
if (selfSigned) {
Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package {{ sdk.namespace | caseDot }}
22

3-
import android.content.Context
43
import android.content.Intent
54
import android.net.Uri
5+
import androidx.activity.ComponentActivity
66
import androidx.browser.customtabs.CustomTabsIntent
77
import androidx.lifecycle.DefaultLifecycleObserver
88
import androidx.lifecycle.LifecycleOwner
@@ -17,72 +17,75 @@ import kotlin.collections.set
1717
* Used to authenticate with external OAuth2 providers. Launches browser windows and handles
1818
* suspension until the user completes the process or otherwise returns to the app.
1919
*/
20-
actual class WebAuthComponent(private val context: Context) {
21-
22-
companion object : DefaultLifecycleObserver {
20+
actual class WebAuthComponent {
21+
actual companion object : DefaultLifecycleObserver {
2322
private var suspended = false
24-
private val callbacks = mutableMapOf<String, (((Result<String>) -> Unit)?)>()
23+
private val callbacks = mutableMapOf<String, ((Result<String>) -> Unit)?>()
2524

2625
override fun onResume(owner: LifecycleOwner) {
26+
// When the activity resumes, end the suspension so that the caller can continue.
2727
suspended = false
2828
}
2929

30-
suspend fun authenticate(
31-
context: Context,
30+
/**
31+
* Authenticate session using OAuth2.
32+
*
33+
* Launches a Chrome Custom Tab from the provided activity to open the given URL.
34+
* Once the user returns to the app (resuming the activity), the provided callback is invoked.
35+
*
36+
* @param activity The activity used to launch the browser and observe lifecycle events.
37+
* @param url The URL to open.
38+
* @param callbackUrlScheme The URL scheme to match for the authentication callback.
39+
* @param onComplete The callback to run with the authentication result.
40+
*/
41+
internal suspend fun authenticate(
42+
activity: ComponentActivity,
3243
url: String,
3344
callbackUrlScheme: String,
3445
onComplete: ((Result<String>) -> Unit)?
3546
) {
3647
val intent = CustomTabsIntent.Builder().build()
37-
val keepAliveIntent = Intent(context, KeepAliveService::class.java)
48+
val keepAliveIntent = Intent(activity, KeepAliveService::class.java)
3849

3950
callbacks[callbackUrlScheme] = onComplete
4051

4152
intent.intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
4253
intent.intent.putExtra("android.support.customtabs.extra.KEEP_ALIVE", keepAliveIntent)
43-
intent.launchUrl(context, Uri.parse(url))
54+
intent.launchUrl(activity, Uri.parse(url))
55+
56+
// Add this as a lifecycle observer so that we know when the user returns to the app.
57+
activity.runOnUiThread {
58+
activity.lifecycle.addObserver(this)
59+
}
4460

45-
// Dirty poll block so execution doesn't continue at the callsite of this function
61+
// Poll until the authentication has been resumed.
4662
suspended = true
4763
while (suspended) {
4864
delay(200)
4965
}
5066
cleanUp()
5167
}
5268

53-
54-
fun onCallback(scheme: String, url: String) {
55-
callbacks.remove(scheme)?.invoke(
56-
Result.success(url)
57-
)
69+
/**
70+
* Invoke the callback for the provided scheme.
71+
*
72+
* This method ends the suspension, allowing any waiting coroutines to resume.
73+
*
74+
* @param scheme The callback scheme key.
75+
* @param url The URL provided in the callback.
76+
*/
77+
actual fun onCallback(scheme: String, url: String) {
78+
callbacks.remove(scheme)?.invoke(Result.success(url))
5879
suspended = false
5980
}
6081

6182
private fun cleanUp() {
62-
callbacks.forEach { (_, danglingResultCallback) ->
63-
danglingResultCallback?.invoke(
64-
Result.failure(IllegalStateException("User cancelled login"))
65-
)
83+
callbacks.forEach { (_, callback) ->
84+
callback?.invoke(Result.failure(IllegalStateException("User cancelled login")))
6685
}
6786
callbacks.clear()
6887
}
69-
}
70-
71-
@Throws(Throwable::class)
72-
actual suspend fun authenticate(
73-
url: String,
74-
callbackUrlScheme: String,
75-
onComplete: ((Result<String>) -> Unit)?
76-
) {
77-
authenticate(
78-
context,
79-
url,
80-
callbackUrlScheme,
81-
onComplete,
82-
)
83-
}
8488

85-
actual fun onCallback(scheme: String, url: String) {
86-
WebAuthComponent.onCallback(scheme, url)
89+
actual fun handleIncomingCookie(url: String) {}
8790
}
8891
}

templates/kmp/shared/src/androidMain/kotlin/io/package/cookies/AndroidCookieStorage.kt.twig

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)