Skip to content

Commit cabb7f2

Browse files
authored
Support native flows on iOS and Android (#160)
1 parent 0cd0412 commit cabb7f2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+9558
-786
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 0.9.9
2+
3+
- Introducing the `DescopeFlowView` widget for seamless integration of Descope Flows in Flutter applications. Available for both Android and iOS platforms.
4+
- `DescopeFlow` is now deprecated in favor of `DescopeFlowView`, however, it is still available for backward compatibility, and for non-mobile platforms like web.
5+
16
# 0.9.8
27

38
- Support external token in `AuthenticationResponse`

README.md

Lines changed: 135 additions & 178 deletions
Large diffs are not rendered by default.

android/build.gradle

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,8 @@ android {
5050
dependencies {
5151
implementation "androidx.browser:browser:1.8.0"
5252
implementation "androidx.security:security-crypto:1.1.0"
53-
implementation "androidx.credentials:credentials:1.5.0"
54-
implementation "androidx.credentials:credentials-play-services-auth:1.5.0"
55-
implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
5653
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
54+
implementation "com.descope:descope-kotlin:0.17.1"
5755
}
5856

5957
testOptions {
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.descope.flutter
2+
3+
import android.content.Context
4+
import android.view.View
5+
import androidx.core.net.toUri
6+
import com.descope.android.DescopeFlow
7+
import com.descope.android.DescopeFlowView
8+
import com.descope.types.AuthenticationResponse
9+
import com.descope.types.DescopeException
10+
import com.descope.types.DescopeUser
11+
import com.descope.types.OAuthProvider
12+
import io.flutter.plugin.common.BinaryMessenger
13+
import io.flutter.plugin.common.MethodCall
14+
import io.flutter.plugin.common.MethodChannel
15+
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
16+
import io.flutter.plugin.common.MethodChannel.Result
17+
import io.flutter.plugin.common.StandardMessageCodec
18+
import io.flutter.plugin.platform.PlatformView
19+
import io.flutter.plugin.platform.PlatformViewFactory
20+
import kotlin.collections.toMap
21+
22+
class DescopeFlowViewFactory(private val messenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
23+
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
24+
return DescopeFlowViewWrapper(context, viewId, args, messenger)
25+
}
26+
}
27+
28+
class DescopeFlowViewWrapper(
29+
private val context: Context,
30+
private val id: Int,
31+
private val args: Any?,
32+
private val messenger: BinaryMessenger
33+
) : PlatformView, MethodCallHandler {
34+
35+
private val channel = MethodChannel(messenger, "com.descope.flow/view_$id").apply {
36+
setMethodCallHandler(this@DescopeFlowViewWrapper)
37+
}
38+
39+
private val flowView = DescopeFlowView(context).apply {
40+
// init DescopeFlow from args
41+
val descopeFlow = args.toDescopeFlow()
42+
43+
// pipe listener callbacks through a flutter channel
44+
listener = object : DescopeFlowView.Listener {
45+
override fun onReady() {
46+
channel.invokeMethod("onReady", null)
47+
}
48+
49+
override fun onSuccess(response: AuthenticationResponse) {
50+
try {
51+
channel.invokeMethod("onSuccess", response.toMap())
52+
} catch (e: Exception) {
53+
channel.invokeMethod("onError", DescopeException("flutter_error", "Error in onSuccess callback", e.message).toMap())
54+
}
55+
}
56+
57+
override fun onError(exception: DescopeException) {
58+
channel.invokeMethod("onError", exception.toMap())
59+
}
60+
}
61+
62+
startFlow(descopeFlow)
63+
}
64+
65+
// PlatformView implementation
66+
67+
override fun getView(): View = flowView
68+
69+
override fun dispose() {
70+
flowView.listener = null
71+
channel.setMethodCallHandler(null)
72+
}
73+
74+
// MethodCallHandler implementation
75+
76+
override fun onMethodCall(call: MethodCall, result: Result) {
77+
when (call.method) {
78+
"resumeFromDeepLink" -> resumeFromDeepLink(call, result)
79+
else -> result.notImplemented()
80+
}
81+
}
82+
83+
private fun resumeFromDeepLink(call: MethodCall, result: Result) {
84+
val url = call.argument<String>("url") ?: return result.error("MISSINGARGS", "'url' is required for resumeFromDeepLink", null)
85+
try {
86+
flowView.resumeFromDeepLink(url.toUri())
87+
result.success(url)
88+
} catch (ignored: Exception) {
89+
result.error("INVALIDARGS", "url argument is invalid", null)
90+
}
91+
}
92+
93+
}
94+
95+
fun Any?.toDescopeFlow(): DescopeFlow {
96+
val map = this as? Map<*, *> ?: throw IllegalArgumentException("Flow options are required")
97+
val url = map["url"] as? String ?: throw IllegalArgumentException("Flow URL is required")
98+
return DescopeFlow(url).apply {
99+
(map["androidOAuthNativeProvider"] as? String)?.let { oauthNativeProvider = OAuthProvider(name = it) }
100+
(map["oauthRedirect"] as? String)?.let { oauthRedirect = it }
101+
(map["oauthRedirectCustomScheme"] as? String)?.let { oauthRedirectCustomScheme = it }
102+
(map["ssoRedirect"] as? String)?.let { ssoRedirect = it }
103+
(map["ssoRedirectCustomScheme"] as? String)?.let { ssoRedirectCustomScheme = it }
104+
(map["magicLinkRedirect"] as? String)?.let { magicLinkRedirect = it }
105+
}
106+
}
107+
108+
// Utilities
109+
110+
private fun AuthenticationResponse.toMap(): Map<String, Any> = mutableMapOf(
111+
"sessionJwt" to sessionToken.jwt,
112+
"refreshJwt" to refreshToken.jwt,
113+
"user" to user.toMap(),
114+
"firstSeen" to isFirstAuthentication
115+
)
116+
117+
private fun DescopeUser.toMap() = mutableMapOf<String, Any>().apply {
118+
put("userId", userId)
119+
put("loginIds", loginIds)
120+
name?.let { put("name", it) }
121+
picture?.let { put("picture", it.toString()) }
122+
email?.let { put("email", it) }
123+
put("verifiedEmail", isVerifiedEmail)
124+
phone?.let { put("phone", it) }
125+
put("verifiedPhone", isVerifiedPhone)
126+
put("createdTime", createdAt)
127+
put("customAttributes", customAttributes)
128+
givenName?.let { put("givenName", it) }
129+
middleName?.let { put("middleName", it) }
130+
familyName?.let { put("familyName", it) }
131+
put("password", authentication.password)
132+
put("status", status.serialize())
133+
put("roleNames", authorization.roles.toList())
134+
put("ssoAppIds", authorization.ssoAppIds.toList())
135+
put("OAuth", mutableMapOf<String, Boolean>().apply {
136+
authentication.oauth.forEach { this[it] = true }
137+
})
138+
}
139+
140+
private fun DescopeException.toMap(): Map<String, Any> = mutableMapOf<String, Any>().apply {
141+
put("code", code)
142+
put("desc", desc)
143+
message?.let { put("message", it) }
144+
}

0 commit comments

Comments
 (0)