react-native-oauth-essentials 1.0.11 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +384 -362
- package/android/src/main/java/com/oauthessentials/CredentialError.kt +4 -1
- package/android/src/main/java/com/oauthessentials/CredentialFactory.kt +24 -2
- package/android/src/main/java/com/oauthessentials/CustomTabsActivity.kt +14 -1
- package/android/src/main/java/com/oauthessentials/OauthEssentialsModule.kt +448 -192
- package/app.plugin.js +1 -0
- package/ios/AppleIdRetriever.swift +25 -26
- package/ios/CredentialError.swift +3 -0
- package/ios/CredentialFactory.swift +38 -20
- package/ios/CredentialsType.swift +1 -0
- package/ios/GoogleIdRetriever.swift +132 -56
- package/ios/HybridRetriever.swift +12 -18
- package/ios/OauthEssentials.h +0 -3
- package/ios/OauthEssentials.mm +45 -2
- package/ios/OauthEssentialsModule.swift +266 -57
- package/ios/PasskeyRetriever.swift +31 -49
- package/ios/PasswordRetriever.swift +10 -30
- package/ios/RCTPromiseSettler.swift +125 -0
- package/jest/index.js +149 -0
- package/lib/module/NativeOauthEssentials.js +10 -1
- package/lib/module/NativeOauthEssentials.js.map +1 -1
- package/lib/module/errors.js +38 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +432 -58
- package/lib/module/index.js.map +1 -1
- package/lib/module/oauth.js +97 -0
- package/lib/module/oauth.js.map +1 -0
- package/lib/module/platform.js +108 -0
- package/lib/module/platform.js.map +1 -0
- package/lib/module/types.js +46 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/NativeOauthEssentials.d.ts +70 -16
- package/lib/typescript/src/NativeOauthEssentials.d.ts.map +1 -1
- package/lib/typescript/src/errors.d.ts +18 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +235 -40
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/oauth.d.ts +32 -0
- package/lib/typescript/src/oauth.d.ts.map +1 -0
- package/lib/typescript/src/platform.d.ts +39 -0
- package/lib/typescript/src/platform.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +190 -3
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +8 -4
- package/plugin/withOauthEssentials.js +291 -0
- package/src/NativeOauthEssentials.ts +91 -17
- package/src/errors.ts +49 -0
- package/src/index.tsx +587 -80
- package/src/oauth.ts +140 -0
- package/src/platform.ts +121 -0
- package/src/types.ts +200 -2
|
@@ -1,90 +1,122 @@
|
|
|
1
1
|
package com.oauthessentials
|
|
2
2
|
|
|
3
|
+
import android.accounts.Account
|
|
3
4
|
import android.annotation.SuppressLint
|
|
5
|
+
import android.app.Activity
|
|
4
6
|
import android.content.BroadcastReceiver
|
|
5
7
|
import android.content.Context
|
|
6
8
|
import android.content.Intent
|
|
7
9
|
import android.content.IntentFilter
|
|
10
|
+
import android.content.pm.PackageManager
|
|
8
11
|
import android.net.Uri
|
|
9
12
|
import android.os.Build
|
|
13
|
+
import android.util.Base64
|
|
10
14
|
import android.util.Log
|
|
11
15
|
import android.util.Patterns
|
|
12
16
|
import androidx.annotation.RequiresApi
|
|
13
17
|
import androidx.core.content.ContextCompat
|
|
14
18
|
import androidx.core.net.toUri
|
|
19
|
+
import androidx.credentials.ClearCredentialStateRequest
|
|
15
20
|
import androidx.credentials.CreatePasswordRequest
|
|
16
21
|
import androidx.credentials.CreatePublicKeyCredentialRequest
|
|
17
22
|
import androidx.credentials.CredentialManager
|
|
23
|
+
import androidx.credentials.CredentialOption
|
|
18
24
|
import androidx.credentials.GetCredentialRequest
|
|
25
|
+
import androidx.credentials.GetCredentialResponse
|
|
19
26
|
import androidx.credentials.GetPasswordOption
|
|
20
27
|
import androidx.credentials.GetPublicKeyCredentialOption
|
|
21
28
|
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
|
22
29
|
import androidx.credentials.exceptions.GetCredentialCancellationException
|
|
30
|
+
import androidx.credentials.exceptions.NoCredentialException
|
|
31
|
+
import com.facebook.react.bridge.ActivityEventListener
|
|
23
32
|
import com.facebook.react.bridge.Arguments
|
|
24
33
|
import com.facebook.react.bridge.Promise
|
|
25
34
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
26
35
|
import com.facebook.react.bridge.ReadableMap
|
|
27
36
|
import com.facebook.react.bridge.WritableMap
|
|
37
|
+
import com.google.android.gms.auth.api.identity.AuthorizationRequest
|
|
38
|
+
import com.google.android.gms.auth.api.identity.AuthorizationResult
|
|
39
|
+
import com.google.android.gms.auth.api.identity.Identity
|
|
28
40
|
import com.google.android.gms.common.ConnectionResult
|
|
29
41
|
import com.google.android.gms.common.GoogleApiAvailability
|
|
42
|
+
import com.google.android.gms.common.api.Scope
|
|
30
43
|
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
|
|
44
|
+
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
|
|
45
|
+
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
|
|
46
|
+
import kotlinx.coroutines.CancellableContinuation
|
|
31
47
|
import kotlinx.coroutines.CoroutineScope
|
|
32
48
|
import kotlinx.coroutines.Dispatchers
|
|
33
|
-
import kotlinx.coroutines.Job
|
|
34
49
|
import kotlinx.coroutines.SupervisorJob
|
|
35
50
|
import kotlinx.coroutines.cancel
|
|
36
51
|
import kotlinx.coroutines.launch
|
|
52
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
37
53
|
import kotlinx.coroutines.withContext
|
|
54
|
+
import java.security.MessageDigest
|
|
38
55
|
import java.security.SecureRandom
|
|
39
|
-
import
|
|
40
|
-
import
|
|
56
|
+
import kotlin.coroutines.resume
|
|
57
|
+
import kotlin.coroutines.resumeWithException
|
|
41
58
|
|
|
42
59
|
class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
43
60
|
NativeOauthEssentialsSpec(reactContext) {
|
|
44
61
|
|
|
45
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Browser flows (web Apple sign in and generic OAuth) share CustomTabsActivity,
|
|
64
|
+
* so only one can be pending at a time.
|
|
65
|
+
*/
|
|
66
|
+
private enum class BrowserSessionKind { APPLE, OAUTH }
|
|
67
|
+
|
|
68
|
+
private var browserSessionSettler: RCTPromiseSettler? = null
|
|
69
|
+
private var browserSessionKind: BrowserSessionKind? = null
|
|
70
|
+
private var authorizationContinuation: CancellableContinuation<Pair<Int, Intent?>>? = null
|
|
46
71
|
private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
47
72
|
|
|
48
73
|
private val receiver = object : BroadcastReceiver() {
|
|
49
74
|
override fun onReceive(context: Context?, intent: Intent?) {
|
|
50
75
|
intent?.action?.let { action ->
|
|
51
76
|
Log.d(LOG_TAG, "Handling intent action ${action} received")
|
|
77
|
+
val settler = browserSessionSettler
|
|
78
|
+
if (settler == null || settler.settled()) {
|
|
79
|
+
Log.d(LOG_TAG, "No pending browser session for ${action}")
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
val kind = browserSessionKind
|
|
52
83
|
when (action) {
|
|
53
84
|
WEB_APPLE_DEEP_LINK_RECEIVED_INTENT -> {
|
|
54
|
-
val url = intent.getStringExtra("url")
|
|
85
|
+
val url = intent.getStringExtra("url") ?: return
|
|
55
86
|
Log.d(LOG_TAG, "Receiving deep link ${url}")
|
|
56
|
-
|
|
87
|
+
if (kind == BrowserSessionKind.OAUTH) {
|
|
88
|
+
settler.resolve(CredentialFactory.fromRedirect(url))
|
|
89
|
+
} else {
|
|
57
90
|
val uri = url.toUri()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
putString("path", uri.path)
|
|
68
|
-
putString("query", uri.query)
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
resolveAndEmit(result, it)
|
|
72
|
-
} else {
|
|
73
|
-
Log.d(LOG_TAG, "Cannot call deep link callback")
|
|
74
|
-
}
|
|
91
|
+
val result = Arguments.createMap().apply {
|
|
92
|
+
putString("type", CredentialFactory.CredentialsType.WEB_APPLE_ID.code)
|
|
93
|
+
putMap("data", Arguments.createMap().apply {
|
|
94
|
+
putString("url", uri.toString())
|
|
95
|
+
putString("scheme", uri.scheme)
|
|
96
|
+
putString("host", uri.host)
|
|
97
|
+
putString("path", uri.path)
|
|
98
|
+
putString("query", uri.query)
|
|
99
|
+
})
|
|
75
100
|
}
|
|
101
|
+
resolveAndEmit(result, settler)
|
|
76
102
|
}
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
WEB_APPLE_CANCELLED_INTENT -> {
|
|
80
106
|
Log.d(LOG_TAG, "Receiving cancellation from CustomTabsActivity")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
if (kind == BrowserSessionKind.OAUTH) {
|
|
108
|
+
settler.resolve(CredentialFactory.fromCancelled())
|
|
109
|
+
} else {
|
|
110
|
+
resolveAndEmit(CredentialFactory.fromCancelled(), settler)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
WEB_BROWSER_UNAVAILABLE_INTENT -> {
|
|
115
|
+
val code = CredentialError.BROWSER_UNAVAILABLE.code
|
|
116
|
+
val message = "No browser available to open the url"
|
|
117
|
+
settler.reject(code, message, null, null)
|
|
118
|
+
if (kind == BrowserSessionKind.APPLE) {
|
|
119
|
+
emitFailure(code, message)
|
|
88
120
|
}
|
|
89
121
|
}
|
|
90
122
|
|
|
@@ -96,23 +128,37 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
96
128
|
}
|
|
97
129
|
}
|
|
98
130
|
|
|
131
|
+
private val activityEventListener = object : ActivityEventListener {
|
|
132
|
+
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
|
|
133
|
+
if (requestCode == AUTHORIZATION_REQUEST_CODE) {
|
|
134
|
+
authorizationContinuation?.let {
|
|
135
|
+
authorizationContinuation = null
|
|
136
|
+
it.resume(resultCode to data)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override fun onNewIntent(intent: Intent) = Unit
|
|
142
|
+
}
|
|
143
|
+
|
|
99
144
|
@SuppressLint("ObsoleteSdkInt")
|
|
100
|
-
override fun getTypedExportedConstants(): MutableMap<String,
|
|
101
|
-
|
|
102
|
-
val hasCredentialManager = getCredentialManagerSupported()
|
|
103
|
-
|
|
145
|
+
override fun getTypedExportedConstants(): MutableMap<String, Any> {
|
|
146
|
+
// Passwords and hybrid sign in go through Credential Manager, which needs Play Services.
|
|
147
|
+
val hasCredentialManager = getCredentialManagerSupported() && hasGooglePlayServices()
|
|
148
|
+
return mutableMapOf(
|
|
104
149
|
"GOOGLE_PLAY_SERVICES_SUPPORTED" to hasGooglePlayServices(),
|
|
105
150
|
"PASSWORD_SUPPORTED" to hasCredentialManager,
|
|
106
151
|
"GOOGLE_ID_SUPPORTED" to googleIdSupported(),
|
|
107
152
|
"APPLE_ID_SUPPORTED" to getAppleIdSupported(),
|
|
108
153
|
"HYBRID_SUPPORTED" to hasCredentialManager,
|
|
109
|
-
"PASSKEYS_SUPPORTED" to getPasskeysSupported()
|
|
154
|
+
"PASSKEYS_SUPPORTED" to getPasskeysSupported(),
|
|
155
|
+
"OAUTH_REDIRECT_URI" to "${reactApplicationContext.packageName}://oauth/callback"
|
|
110
156
|
)
|
|
111
|
-
return constants
|
|
112
157
|
}
|
|
113
158
|
|
|
114
|
-
|
|
115
|
-
|
|
159
|
+
override fun hybridSignIn(jsClientId: String, options: ReadableMap, promise: Promise) {
|
|
160
|
+
val clientId = resolveWebClientId(jsClientId, promise) ?: return
|
|
161
|
+
|
|
116
162
|
if (!hasGooglePlayServices()) {
|
|
117
163
|
rejectAndEmitEvent(
|
|
118
164
|
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
@@ -121,14 +167,6 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
121
167
|
)
|
|
122
168
|
return
|
|
123
169
|
}
|
|
124
|
-
if (!getCredentialManagerSupported()) {
|
|
125
|
-
rejectAndEmitEvent(
|
|
126
|
-
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
127
|
-
"Device does not support Credential Manager, only android >=12 allowed.",
|
|
128
|
-
promise
|
|
129
|
-
)
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
170
|
|
|
133
171
|
val activity = reactApplicationContext.currentActivity
|
|
134
172
|
if (activity == null) {
|
|
@@ -143,46 +181,21 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
143
181
|
moduleScope.launch {
|
|
144
182
|
try {
|
|
145
183
|
val manager = CredentialManager.create(activity)
|
|
146
|
-
val
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
|
|
153
|
-
.setNonce(randomNonce)
|
|
154
|
-
.setServerClientId(clientId)
|
|
155
|
-
.setFilterByAuthorizedAccounts(options.getBoolean("authorizedAccounts"))
|
|
156
|
-
.setAutoSelectEnabled(options.getBoolean("autoSelectEnabled"))
|
|
157
|
-
.build()
|
|
158
|
-
|
|
159
|
-
val request: GetCredentialRequest = GetCredentialRequest.Builder()
|
|
160
|
-
.addCredentialOption(googleIdOption)
|
|
161
|
-
.addCredentialOption(GetPasswordOption())
|
|
162
|
-
.build()
|
|
163
|
-
|
|
164
|
-
// getCredential precisa lançar a Activity de UI (chooser), então deve
|
|
165
|
-
// rodar na Main thread — igual ao googleSignIn. Chamar em Dispatchers.IO
|
|
166
|
-
// fazia o chooser abrir sem se vincular à UI e travar (ANR na abertura).
|
|
167
|
-
val result = withContext(Dispatchers.Main) {
|
|
168
|
-
manager.getCredential(
|
|
169
|
-
request = request,
|
|
170
|
-
context = activity
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
resolveAndEmit(
|
|
174
|
-
CredentialFactory.fromCredentialResponse(result), promise
|
|
184
|
+
val result = getCredential(
|
|
185
|
+
activity,
|
|
186
|
+
manager,
|
|
187
|
+
googleIdOption(clientId, options, silent = false),
|
|
188
|
+
GetPasswordOption()
|
|
175
189
|
)
|
|
190
|
+
resolveAndEmit(CredentialFactory.fromCredentialResponse(result), promise)
|
|
176
191
|
} catch (e: GetCredentialCancellationException) {
|
|
177
192
|
Log.d(LOG_TAG, e.toString())
|
|
178
193
|
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
194
|
+
} catch (e: NoCredentialException) {
|
|
195
|
+
rejectAndEmitEvent(CredentialError.NO_SIGN_IN_AVAILABLE.code, e, promise)
|
|
179
196
|
} catch (e: Throwable) {
|
|
180
197
|
Log.d(LOG_TAG, e.toString())
|
|
181
|
-
rejectAndEmitEvent(
|
|
182
|
-
CredentialError.INVALID_RESULT_ERROR.code,
|
|
183
|
-
e,
|
|
184
|
-
promise
|
|
185
|
-
)
|
|
198
|
+
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e, promise)
|
|
186
199
|
}
|
|
187
200
|
}
|
|
188
201
|
}
|
|
@@ -191,33 +204,58 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
191
204
|
* Performs the apple sign in.
|
|
192
205
|
* The signing in is redirecting to the device default browser to handle it.
|
|
193
206
|
*/
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
appleSignInPromiseSettler?.let {
|
|
197
|
-
if (!it.settled()) {
|
|
198
|
-
it.resolve(CredentialFactory.fromCancelled())
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
appleSignInPromiseSettler = null
|
|
202
|
-
|
|
203
|
-
val activity = this.reactApplicationContext.currentActivity
|
|
204
|
-
if (activity === null) {
|
|
205
|
-
rejectAndEmitEvent(
|
|
206
|
-
CredentialError.NO_ACTIVITY_ERROR.code,
|
|
207
|
-
"Could not find app activity.",
|
|
208
|
-
promise
|
|
209
|
-
)
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
var uri: Uri
|
|
207
|
+
override fun appleSignIn(androidWebUrl: String?, options: ReadableMap, promise: Promise) {
|
|
208
|
+
val uri: Uri
|
|
213
209
|
try {
|
|
214
210
|
uri = parseWebUrl(androidWebUrl)
|
|
215
211
|
} catch (e: Throwable) {
|
|
216
212
|
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e, promise)
|
|
217
213
|
return
|
|
218
214
|
}
|
|
215
|
+
openBrowserSession(uri, BrowserSessionKind.APPLE, promise)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
override fun getAppleIdCredentialState(userId: String, promise: Promise) {
|
|
219
|
+
promise.reject(
|
|
220
|
+
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
221
|
+
"Apple ID credential state is only available on iOS"
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Opens an authorization url in a Custom Tab and resolves with the redirect url.
|
|
227
|
+
* The redirect must reach CustomTabsActivity (OAUTH_REDIRECT_URI or an app intent filter).
|
|
228
|
+
* It does not emit events: oauthSignIn finishes the flow in JS.
|
|
229
|
+
*/
|
|
230
|
+
override fun openAuthSession(url: String, options: ReadableMap, promise: Promise) {
|
|
231
|
+
val uri = url.toUri()
|
|
232
|
+
if (uri.scheme != "https" && uri.scheme != "http") {
|
|
233
|
+
promise.reject(CredentialError.INVALID_RESULT_ERROR.code, "Invalid authorization url: $url")
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
openBrowserSession(uri, BrowserSessionKind.OAUTH, promise)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private fun openBrowserSession(uri: Uri, kind: BrowserSessionKind, promise: Promise) {
|
|
240
|
+
val activity = reactApplicationContext.currentActivity
|
|
241
|
+
if (activity == null) {
|
|
242
|
+
val code = CredentialError.NO_ACTIVITY_ERROR.code
|
|
243
|
+
promise.reject(code, "Could not find app activity.")
|
|
244
|
+
if (kind == BrowserSessionKind.APPLE) {
|
|
245
|
+
emitFailure(code, "Could not find app activity.")
|
|
246
|
+
}
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// A new browser session replaces a pending one, which resolves as cancelled.
|
|
251
|
+
browserSessionSettler?.let {
|
|
252
|
+
if (!it.settled()) {
|
|
253
|
+
it.resolve(CredentialFactory.fromCancelled())
|
|
254
|
+
}
|
|
255
|
+
}
|
|
219
256
|
|
|
220
|
-
|
|
257
|
+
browserSessionSettler = RCTPromiseSettler(promise)
|
|
258
|
+
browserSessionKind = kind
|
|
221
259
|
activity.runOnUiThread {
|
|
222
260
|
val intent = Intent(activity, CustomTabsActivity::class.java).apply {
|
|
223
261
|
data = uri
|
|
@@ -230,10 +268,10 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
230
268
|
/**
|
|
231
269
|
* Performs the google sign in.
|
|
232
270
|
*/
|
|
233
|
-
|
|
234
|
-
@RequiresApi(Build.VERSION_CODES.O)
|
|
235
|
-
override fun googleSignIn(clientId: String, options: ReadableMap, promise: Promise) {
|
|
271
|
+
override fun googleSignIn(jsClientId: String, options: ReadableMap, promise: Promise) {
|
|
236
272
|
Log.d(LOG_TAG, "Calling googleSignIn")
|
|
273
|
+
val clientId = resolveWebClientId(jsClientId, promise) ?: return
|
|
274
|
+
|
|
237
275
|
if (!googleIdSupported() || !getCredentialManagerSupported()) {
|
|
238
276
|
rejectAndEmitEvent(
|
|
239
277
|
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
@@ -252,27 +290,86 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
252
290
|
moduleScope.launch {
|
|
253
291
|
try {
|
|
254
292
|
val manager = CredentialManager.create(activity)
|
|
293
|
+
val response = if (options.optString("mode") == "button") {
|
|
294
|
+
getCredential(activity, manager, signInWithGoogleOption(clientId, options))
|
|
295
|
+
} else {
|
|
296
|
+
try {
|
|
297
|
+
getCredential(activity, manager, googleIdOption(clientId, options, silent = false))
|
|
298
|
+
} catch (e: NoCredentialException) {
|
|
299
|
+
// No account on the device or the bottom sheet is in cooldown after being dismissed.
|
|
300
|
+
// The button flow has no cooldown and lets the user add an account.
|
|
301
|
+
if (options.optBoolean("authorizedAccounts")) throw e
|
|
302
|
+
Log.d(LOG_TAG, "No credential for the bottom sheet, falling back to the button flow")
|
|
303
|
+
getCredential(activity, manager, signInWithGoogleOption(clientId, options))
|
|
304
|
+
}
|
|
305
|
+
}
|
|
255
306
|
|
|
256
|
-
val
|
|
257
|
-
|
|
258
|
-
.
|
|
259
|
-
|
|
260
|
-
.setAutoSelectEnabled(options.getBoolean("autoSelectEnabled"))
|
|
261
|
-
.build()
|
|
262
|
-
|
|
263
|
-
val request: GetCredentialRequest = GetCredentialRequest.Builder()
|
|
264
|
-
.addCredentialOption(googleIdOption)
|
|
265
|
-
.build()
|
|
266
|
-
|
|
267
|
-
val result = withContext(Dispatchers.Main) {
|
|
268
|
-
manager.getCredential(activity, request)
|
|
307
|
+
val authorization = authorizeIfNeeded(activity, clientId, response, options, interactive = true)
|
|
308
|
+
if (authorization == null && needsAuthorization(options)) {
|
|
309
|
+
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
310
|
+
return@launch
|
|
269
311
|
}
|
|
270
|
-
resolveAndEmit(CredentialFactory.fromCredentialResponse(
|
|
312
|
+
resolveAndEmit(CredentialFactory.fromCredentialResponse(response, authorization), promise)
|
|
271
313
|
} catch (e: GetCredentialCancellationException) {
|
|
272
314
|
Log.d(LOG_TAG, e.toString())
|
|
273
315
|
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
316
|
+
} catch (e: NoCredentialException) {
|
|
317
|
+
rejectAndEmitEvent(CredentialError.NO_SIGN_IN_AVAILABLE.code, e, promise)
|
|
274
318
|
} catch (e: Throwable) {
|
|
275
|
-
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e
|
|
319
|
+
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e, promise)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Signs in with an account that already signed in to the app, auto selecting it when possible.
|
|
326
|
+
*/
|
|
327
|
+
override fun googleSilentSignIn(jsClientId: String, options: ReadableMap, promise: Promise) {
|
|
328
|
+
val clientId = resolveWebClientId(jsClientId, promise) ?: return
|
|
329
|
+
|
|
330
|
+
if (!googleIdSupported() || !getCredentialManagerSupported()) {
|
|
331
|
+
rejectAndEmitEvent(
|
|
332
|
+
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
333
|
+
"Device does not support googleSilentSignIn method.",
|
|
334
|
+
promise
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
val activity = reactApplicationContext.currentActivity
|
|
340
|
+
if (activity == null) {
|
|
341
|
+
rejectAndEmitEvent(CredentialError.NO_ACTIVITY_ERROR.code, "Activity not available", promise)
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
moduleScope.launch {
|
|
346
|
+
try {
|
|
347
|
+
val manager = CredentialManager.create(activity)
|
|
348
|
+
val response = getCredential(activity, manager, googleIdOption(clientId, options, silent = true))
|
|
349
|
+
val authorization = authorizeIfNeeded(activity, clientId, response, options, interactive = false)
|
|
350
|
+
resolveAndEmit(CredentialFactory.fromCredentialResponse(response, authorization), promise)
|
|
351
|
+
} catch (e: GetCredentialCancellationException) {
|
|
352
|
+
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
353
|
+
} catch (e: NoCredentialException) {
|
|
354
|
+
rejectAndEmitEvent(CredentialError.NO_SIGN_IN_AVAILABLE.code, e, promise)
|
|
355
|
+
} catch (e: Throwable) {
|
|
356
|
+
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e, promise)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Clears the Credential Manager state, so auto select does not sign the user back in.
|
|
363
|
+
*/
|
|
364
|
+
override fun signOut(promise: Promise) {
|
|
365
|
+
moduleScope.launch {
|
|
366
|
+
try {
|
|
367
|
+
CredentialManager.create(reactApplicationContext)
|
|
368
|
+
.clearCredentialState(ClearCredentialStateRequest())
|
|
369
|
+
promise.resolve(true)
|
|
370
|
+
} catch (e: Throwable) {
|
|
371
|
+
Log.e(LOG_TAG, e.toString())
|
|
372
|
+
promise.reject(CredentialError.INVALID_RESULT_ERROR.code, e)
|
|
276
373
|
}
|
|
277
374
|
}
|
|
278
375
|
}
|
|
@@ -284,7 +381,7 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
284
381
|
if (!googleIdSupported()) {
|
|
285
382
|
rejectAndEmitEvent(
|
|
286
383
|
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
287
|
-
"Device does not support
|
|
384
|
+
"Device does not support getPassword method.",
|
|
288
385
|
promise
|
|
289
386
|
)
|
|
290
387
|
return
|
|
@@ -303,23 +400,18 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
303
400
|
moduleScope.launch {
|
|
304
401
|
try {
|
|
305
402
|
val manager = CredentialManager.create(activity)
|
|
306
|
-
val
|
|
307
|
-
listOf(GetPasswordOption())
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
// getCredential lança UI: deve rodar na Main thread (ver hybridSignIn).
|
|
311
|
-
val result = withContext(Dispatchers.Main) {
|
|
312
|
-
manager.getCredential(
|
|
313
|
-
request = request,
|
|
314
|
-
context = activity
|
|
315
|
-
)
|
|
316
|
-
}
|
|
403
|
+
val result = getCredential(activity, manager, GetPasswordOption())
|
|
317
404
|
resolveAndEmit(
|
|
318
405
|
CredentialFactory.fromCredentialResponse(result), promise
|
|
319
406
|
)
|
|
407
|
+
} catch (e: GetCredentialCancellationException) {
|
|
408
|
+
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
409
|
+
} catch (e: NoCredentialException) {
|
|
410
|
+
// Same as iOS: no saved password is reported as cancelled.
|
|
411
|
+
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
320
412
|
} catch (e: Throwable) {
|
|
321
413
|
Log.d(LOG_TAG, e.toString())
|
|
322
|
-
promise
|
|
414
|
+
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e, promise)
|
|
323
415
|
}
|
|
324
416
|
}
|
|
325
417
|
}
|
|
@@ -331,7 +423,7 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
331
423
|
if (!googleIdSupported()) {
|
|
332
424
|
rejectAndEmitEvent(
|
|
333
425
|
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
334
|
-
"Device does not support
|
|
426
|
+
"Device does not support passwordSignIn method.",
|
|
335
427
|
promise
|
|
336
428
|
)
|
|
337
429
|
return
|
|
@@ -351,7 +443,7 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
351
443
|
password = password
|
|
352
444
|
)
|
|
353
445
|
|
|
354
|
-
// createCredential lança UI: deve rodar na Main thread
|
|
446
|
+
// createCredential lança UI: deve rodar na Main thread.
|
|
355
447
|
withContext(Dispatchers.Main) {
|
|
356
448
|
manager.createCredential(
|
|
357
449
|
request = request,
|
|
@@ -369,12 +461,11 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
369
461
|
/**
|
|
370
462
|
* Creates the passkey.
|
|
371
463
|
*/
|
|
372
|
-
@RequiresApi(Build.VERSION_CODES.P)
|
|
373
464
|
override fun createPassKey(options: ReadableMap, promise: Promise) {
|
|
374
465
|
if (!getPasskeysSupported()) {
|
|
375
466
|
rejectAndEmitEvent(
|
|
376
467
|
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
377
|
-
"Device does not support
|
|
468
|
+
"Device does not support passkeys.",
|
|
378
469
|
promise
|
|
379
470
|
)
|
|
380
471
|
return
|
|
@@ -390,23 +481,16 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
390
481
|
try {
|
|
391
482
|
val requestJson = options.getString("requestJson")
|
|
392
483
|
?: throw IllegalArgumentException("\"requestJson\" field should be set on android.")
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
options.getBoolean("preferImmediatelyAvailableCredentials")
|
|
397
|
-
}
|
|
398
|
-
var isConditional = false
|
|
399
|
-
if (options.hasKey("isConditional")) {
|
|
400
|
-
isConditional = options.getBoolean("isConditional")
|
|
401
|
-
}
|
|
484
|
+
// `isConditional` (conditional create) só existe em androidx.credentials
|
|
485
|
+
// >= 1.6.0. Estamos fixados em 1.5.0 estável (ver build.gradle), então o
|
|
486
|
+
// parâmetro não é suportado aqui.
|
|
402
487
|
|
|
403
488
|
val manager = CredentialManager.create(activity)
|
|
404
489
|
val request = CreatePublicKeyCredentialRequest(
|
|
405
490
|
requestJson = requestJson,
|
|
406
|
-
preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
|
|
407
|
-
isConditional = isConditional
|
|
491
|
+
preferImmediatelyAvailableCredentials = options.optBoolean("preferImmediatelyAvailableCredentials")
|
|
408
492
|
)
|
|
409
|
-
// createCredential lança UI: deve rodar na Main thread
|
|
493
|
+
// createCredential lança UI: deve rodar na Main thread.
|
|
410
494
|
val result = withContext(Dispatchers.Main) {
|
|
411
495
|
manager.createCredential(
|
|
412
496
|
request = request,
|
|
@@ -414,17 +498,11 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
414
498
|
)
|
|
415
499
|
}
|
|
416
500
|
resolveAndEmit(CredentialFactory.fromPublicKeyCredential(result), promise)
|
|
501
|
+
} catch (e: CreateCredentialCancellationException) {
|
|
502
|
+
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
417
503
|
} catch (e: Throwable) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
else -> {
|
|
424
|
-
Log.e(LOG_TAG, e.toString())
|
|
425
|
-
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e.toString(), promise)
|
|
426
|
-
}
|
|
427
|
-
}
|
|
504
|
+
Log.e(LOG_TAG, e.toString())
|
|
505
|
+
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e, promise)
|
|
428
506
|
}
|
|
429
507
|
}
|
|
430
508
|
}
|
|
@@ -432,12 +510,11 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
432
510
|
/**
|
|
433
511
|
* Gets the passkey.
|
|
434
512
|
*/
|
|
435
|
-
@RequiresApi(Build.VERSION_CODES.P)
|
|
436
513
|
override fun getPassKey(options: ReadableMap, promise: Promise) {
|
|
437
514
|
if (!getPasskeysSupported()) {
|
|
438
515
|
rejectAndEmitEvent(
|
|
439
516
|
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
440
|
-
"Device does not support
|
|
517
|
+
"Device does not support passkeys.",
|
|
441
518
|
promise
|
|
442
519
|
)
|
|
443
520
|
return
|
|
@@ -453,11 +530,6 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
453
530
|
try {
|
|
454
531
|
val requestJson = options.getString("requestJson")
|
|
455
532
|
?: throw IllegalArgumentException("\"requestJson\" field should be set on android.")
|
|
456
|
-
var preferImmediatelyAvailableCredentials = false
|
|
457
|
-
if (options.hasKey("preferImmediatelyAvailableCredentials")) {
|
|
458
|
-
preferImmediatelyAvailableCredentials =
|
|
459
|
-
options.getBoolean("preferImmediatelyAvailableCredentials")
|
|
460
|
-
}
|
|
461
533
|
|
|
462
534
|
val manager = CredentialManager.create(activity)
|
|
463
535
|
val request = GetCredentialRequest(
|
|
@@ -466,9 +538,9 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
466
538
|
requestJson = requestJson,
|
|
467
539
|
),
|
|
468
540
|
),
|
|
469
|
-
preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
|
|
541
|
+
preferImmediatelyAvailableCredentials = options.optBoolean("preferImmediatelyAvailableCredentials")
|
|
470
542
|
)
|
|
471
|
-
// getCredential lança UI: deve rodar na Main thread
|
|
543
|
+
// getCredential lança UI: deve rodar na Main thread.
|
|
472
544
|
val result = withContext(Dispatchers.Main) {
|
|
473
545
|
manager.getCredential(
|
|
474
546
|
request = request,
|
|
@@ -476,19 +548,142 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
476
548
|
)
|
|
477
549
|
}
|
|
478
550
|
resolveAndEmit(CredentialFactory.fromPublicKeyCredential(result), promise)
|
|
551
|
+
} catch (e: GetCredentialCancellationException) {
|
|
552
|
+
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
553
|
+
} catch (e: NoCredentialException) {
|
|
554
|
+
rejectAndEmitEvent(CredentialError.NO_SIGN_IN_AVAILABLE.code, e, promise)
|
|
479
555
|
} catch (e: Throwable) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
556
|
+
Log.e(LOG_TAG, e.toString())
|
|
557
|
+
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e, promise)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
484
561
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Passkey AutoFill is iOS only.
|
|
564
|
+
*/
|
|
565
|
+
override fun cancelPassKeyAutoFill(promise: Promise) {
|
|
566
|
+
promise.resolve(false)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Generates a PKCE code verifier, its S256 challenge and a random state.
|
|
571
|
+
*/
|
|
572
|
+
override fun generatePkce(promise: Promise) {
|
|
573
|
+
try {
|
|
574
|
+
val codeVerifier = randomBase64Url(32)
|
|
575
|
+
val challenge = MessageDigest.getInstance("SHA-256")
|
|
576
|
+
.digest(codeVerifier.toByteArray(Charsets.US_ASCII))
|
|
577
|
+
promise.resolve(Arguments.createMap().apply {
|
|
578
|
+
putString("codeVerifier", codeVerifier)
|
|
579
|
+
putString("codeChallenge", base64Url(challenge))
|
|
580
|
+
putString("state", randomBase64Url(16))
|
|
581
|
+
})
|
|
582
|
+
} catch (e: Throwable) {
|
|
583
|
+
promise.reject(CredentialError.INVALID_RESULT_ERROR.code, e)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private fun randomBase64Url(byteCount: Int): String {
|
|
588
|
+
val bytes = ByteArray(byteCount)
|
|
589
|
+
// SecureRandom() não bloqueia, ao contrário de getInstanceStrong().
|
|
590
|
+
SecureRandom().nextBytes(bytes)
|
|
591
|
+
return base64Url(bytes)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private fun base64Url(bytes: ByteArray): String =
|
|
595
|
+
Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Bottom sheet option: lists the device Google accounts.
|
|
599
|
+
*/
|
|
600
|
+
private fun googleIdOption(clientId: String, options: ReadableMap, silent: Boolean): GetGoogleIdOption {
|
|
601
|
+
val builder = GetGoogleIdOption.Builder()
|
|
602
|
+
.setServerClientId(clientId)
|
|
603
|
+
.setFilterByAuthorizedAccounts(silent || options.optBoolean("authorizedAccounts"))
|
|
604
|
+
.setAutoSelectEnabled(silent || options.optBoolean("autoSelectEnabled"))
|
|
605
|
+
options.optString("nonce")?.let { builder.setNonce(it) }
|
|
606
|
+
return builder.build()
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* "Sign in with Google" button option: always shows the sign in flow.
|
|
611
|
+
*/
|
|
612
|
+
private fun signInWithGoogleOption(clientId: String, options: ReadableMap): GetSignInWithGoogleOption {
|
|
613
|
+
val builder = GetSignInWithGoogleOption.Builder(clientId)
|
|
614
|
+
options.optString("nonce")?.let { builder.setNonce(it) }
|
|
615
|
+
options.optString("hostedDomain")?.let { builder.setHostedDomainFilter(it) }
|
|
616
|
+
return builder.build()
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* getCredential launches UI, so it must run on the main thread.
|
|
621
|
+
*/
|
|
622
|
+
private suspend fun getCredential(
|
|
623
|
+
activity: Activity,
|
|
624
|
+
manager: CredentialManager,
|
|
625
|
+
vararg credentialOptions: CredentialOption
|
|
626
|
+
): GetCredentialResponse {
|
|
627
|
+
val request = GetCredentialRequest.Builder().apply {
|
|
628
|
+
credentialOptions.forEach { addCredentialOption(it) }
|
|
629
|
+
}.build()
|
|
630
|
+
return withContext(Dispatchers.Main) {
|
|
631
|
+
manager.getCredential(activity, request)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private fun needsAuthorization(options: ReadableMap): Boolean =
|
|
636
|
+
options.optStringList("scopes").isNotEmpty() || options.optBoolean("offlineAccess")
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Requests the access token, granted scopes and server auth code when scopes or offline access
|
|
640
|
+
* were requested. Returns null when not needed, when the user declined the consent screen or,
|
|
641
|
+
* on silent sign in, when consent would be required.
|
|
642
|
+
*/
|
|
643
|
+
private suspend fun authorizeIfNeeded(
|
|
644
|
+
activity: Activity,
|
|
645
|
+
clientId: String,
|
|
646
|
+
response: GetCredentialResponse,
|
|
647
|
+
options: ReadableMap,
|
|
648
|
+
interactive: Boolean
|
|
649
|
+
): AuthorizationResult? {
|
|
650
|
+
if (!needsAuthorization(options)) return null
|
|
651
|
+
|
|
652
|
+
val scopes = options.optStringList("scopes").ifEmpty { listOf("openid", "email", "profile") }
|
|
653
|
+
val builder = AuthorizationRequest.Builder()
|
|
654
|
+
.setRequestedScopes(scopes.map { Scope(it) })
|
|
655
|
+
if (options.optBoolean("offlineAccess")) {
|
|
656
|
+
builder.requestOfflineAccess(options.optString("serverClientId") ?: clientId)
|
|
657
|
+
}
|
|
658
|
+
options.optString("hostedDomain")?.let { builder.filterByHostedDomain(it) }
|
|
659
|
+
val credential = response.credential
|
|
660
|
+
if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
|
|
661
|
+
val googleCredential = GoogleIdTokenCredential.createFrom(credential.data)
|
|
662
|
+
builder.setAccount(Account(googleCredential.id, "com.google"))
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
val client = Identity.getAuthorizationClient(activity)
|
|
666
|
+
val result = suspendCancellableCoroutine { continuation ->
|
|
667
|
+
client.authorize(builder.build())
|
|
668
|
+
.addOnSuccessListener { continuation.resume(it) }
|
|
669
|
+
.addOnFailureListener { continuation.resumeWithException(it) }
|
|
670
|
+
}
|
|
671
|
+
if (!result.hasResolution()) return result
|
|
672
|
+
if (!interactive) return null
|
|
673
|
+
|
|
674
|
+
val pendingIntent = result.pendingIntent ?: return null
|
|
675
|
+
val (resultCode, data) = withContext(Dispatchers.Main) {
|
|
676
|
+
suspendCancellableCoroutine { continuation ->
|
|
677
|
+
authorizationContinuation?.cancel()
|
|
678
|
+
authorizationContinuation = continuation
|
|
679
|
+
continuation.invokeOnCancellation { authorizationContinuation = null }
|
|
680
|
+
activity.startIntentSenderForResult(
|
|
681
|
+
pendingIntent.intentSender, AUTHORIZATION_REQUEST_CODE, null, 0, 0, 0
|
|
682
|
+
)
|
|
490
683
|
}
|
|
491
684
|
}
|
|
685
|
+
if (resultCode != Activity.RESULT_OK) return null
|
|
686
|
+
return client.getAuthorizationResultFromIntent(data)
|
|
492
687
|
}
|
|
493
688
|
|
|
494
689
|
/**
|
|
@@ -500,7 +695,6 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
500
695
|
return result == ConnectionResult.SUCCESS
|
|
501
696
|
}
|
|
502
697
|
|
|
503
|
-
@RequiresApi(Build.VERSION_CODES.O)
|
|
504
698
|
override fun initialize() {
|
|
505
699
|
super.initialize()
|
|
506
700
|
|
|
@@ -512,9 +706,11 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
512
706
|
IntentFilter().apply {
|
|
513
707
|
addAction(WEB_APPLE_DEEP_LINK_RECEIVED_INTENT)
|
|
514
708
|
addAction(WEB_APPLE_CANCELLED_INTENT)
|
|
709
|
+
addAction(WEB_BROWSER_UNAVAILABLE_INTENT)
|
|
515
710
|
},
|
|
516
711
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
517
712
|
)
|
|
713
|
+
reactApplicationContext.addActivityEventListener(activityEventListener)
|
|
518
714
|
}
|
|
519
715
|
|
|
520
716
|
override fun invalidate() {
|
|
@@ -523,10 +719,11 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
523
719
|
|
|
524
720
|
Log.d(LOG_TAG, "Cancelling all coroutines and credential operations")
|
|
525
721
|
moduleScope.cancel()
|
|
722
|
+
reactApplicationContext.removeActivityEventListener(activityEventListener)
|
|
526
723
|
|
|
527
|
-
|
|
724
|
+
browserSessionSettler?.let {
|
|
528
725
|
if (!it.settled()) {
|
|
529
|
-
Log.d(LOG_TAG, "Rejecting pending
|
|
726
|
+
Log.d(LOG_TAG, "Rejecting pending browser session promise without event")
|
|
530
727
|
it.reject(
|
|
531
728
|
"CANCELLED",
|
|
532
729
|
"Module is being invalidated",
|
|
@@ -535,10 +732,10 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
535
732
|
)
|
|
536
733
|
}
|
|
537
734
|
}
|
|
538
|
-
|
|
735
|
+
browserSessionSettler = null
|
|
539
736
|
|
|
540
737
|
Log.d(LOG_TAG, "Unregistering deep link broadcast receiver")
|
|
541
|
-
reactApplicationContext.unregisterReceiver(receiver)
|
|
738
|
+
reactApplicationContext.applicationContext.unregisterReceiver(receiver)
|
|
542
739
|
}
|
|
543
740
|
|
|
544
741
|
/**
|
|
@@ -565,15 +762,71 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
565
762
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && getCredentialManagerSupported()
|
|
566
763
|
}
|
|
567
764
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
private fun
|
|
572
|
-
|
|
765
|
+
private fun ReadableMap.optBoolean(key: String): Boolean =
|
|
766
|
+
hasKey(key) && !isNull(key) && getBoolean(key)
|
|
767
|
+
|
|
768
|
+
private fun ReadableMap.optString(key: String): String? =
|
|
769
|
+
if (hasKey(key) && !isNull(key)) getString(key)?.takeIf { it.isNotEmpty() } else null
|
|
770
|
+
|
|
771
|
+
private fun ReadableMap.optStringList(key: String): List<String> {
|
|
772
|
+
if (!hasKey(key) || isNull(key)) return emptyList()
|
|
773
|
+
val array = getArray(key) ?: return emptyList()
|
|
774
|
+
return (0 until array.size()).mapNotNull { array.getString(it) }
|
|
775
|
+
}
|
|
573
776
|
|
|
777
|
+
private fun emitFailure(code: String, message: String) {
|
|
574
778
|
val error = Arguments.createMap()
|
|
575
779
|
error.putString("code", code)
|
|
576
|
-
error.putString("message",
|
|
780
|
+
error.putString("message", message)
|
|
781
|
+
emitOnCredentialFailure(error)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Credential Manager needs the web client id. JS passes '' when it is not given, then it is read
|
|
786
|
+
* from the GOOGLE_WEB_CLIENT_ID meta-data (written by the Expo plugin) or the
|
|
787
|
+
* `default_web_client_id` string (generated by the Firebase google-services Gradle plugin).
|
|
788
|
+
* Rejects CONFIGURATION_ERROR and returns null when none is set.
|
|
789
|
+
*/
|
|
790
|
+
private fun resolveWebClientId(jsClientId: String, promise: Promise): String? {
|
|
791
|
+
val clientId = jsClientId.trim().ifEmpty { null }
|
|
792
|
+
?: webClientIdFromMetaData()
|
|
793
|
+
?: webClientIdFromFirebase()
|
|
794
|
+
if (clientId != null) return clientId
|
|
795
|
+
rejectAndEmitEvent(
|
|
796
|
+
CredentialError.CONFIGURATION_ERROR.code,
|
|
797
|
+
"Missing Google web client id (the web client id, not the Android one): pass { webClientId }, " +
|
|
798
|
+
"add <meta-data android:name=\"$GOOGLE_WEB_CLIENT_ID_META_DATA\" android:value=\"...\" /> " +
|
|
799
|
+
"to <application> in AndroidManifest.xml (the Expo plugin does it), or use the Firebase " +
|
|
800
|
+
"google-services.json with the google-services Gradle plugin (default_web_client_id).",
|
|
801
|
+
promise
|
|
802
|
+
)
|
|
803
|
+
return null
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
@Suppress("DEPRECATION")
|
|
807
|
+
private fun webClientIdFromMetaData(): String? {
|
|
808
|
+
return try {
|
|
809
|
+
val info = reactApplicationContext.packageManager.getApplicationInfo(
|
|
810
|
+
reactApplicationContext.packageName,
|
|
811
|
+
PackageManager.GET_META_DATA
|
|
812
|
+
)
|
|
813
|
+
info.metaData?.getString(GOOGLE_WEB_CLIENT_ID_META_DATA)?.trim()?.ifEmpty { null }
|
|
814
|
+
} catch (e: PackageManager.NameNotFoundException) {
|
|
815
|
+
null
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
@SuppressLint("DiscouragedApi")
|
|
820
|
+
private fun webClientIdFromFirebase(): String? {
|
|
821
|
+
val resources = reactApplicationContext.resources
|
|
822
|
+
val id = resources.getIdentifier("default_web_client_id", "string", reactApplicationContext.packageName)
|
|
823
|
+
if (id == 0) return null
|
|
824
|
+
return resources.getString(id).trim().ifEmpty { null }
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private fun rejectAndEmitEvent(code: String, e: Throwable, promise: Promise) {
|
|
828
|
+
promise.reject(code, e.message ?: e.toString(), e)
|
|
829
|
+
emitFailure(code, e.message ?: e.toString())
|
|
577
830
|
}
|
|
578
831
|
|
|
579
832
|
/**
|
|
@@ -581,11 +834,7 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
581
834
|
*/
|
|
582
835
|
private fun rejectAndEmitEvent(code: String, e: String, promise: Promise) {
|
|
583
836
|
promise.reject(code, e)
|
|
584
|
-
|
|
585
|
-
val error = Arguments.createMap()
|
|
586
|
-
error.putString("code", code)
|
|
587
|
-
error.putString("message", e)
|
|
588
|
-
emitOnCredentialFailure(error)
|
|
837
|
+
emitFailure(code, e)
|
|
589
838
|
}
|
|
590
839
|
|
|
591
840
|
/**
|
|
@@ -610,11 +859,18 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
610
859
|
|
|
611
860
|
const val LOG_TAG = "NativeOauthEssentials"
|
|
612
861
|
|
|
613
|
-
//
|
|
862
|
+
// <application> meta-data with the Google web client id, used when JS passes no client id.
|
|
863
|
+
const val GOOGLE_WEB_CLIENT_ID_META_DATA = "com.oauthessentials.GOOGLE_WEB_CLIENT_ID"
|
|
864
|
+
|
|
865
|
+
// Browser session intents (web Apple sign in and generic OAuth)
|
|
614
866
|
const val WEB_APPLE_DEEP_LINK_RECEIVED_INTENT =
|
|
615
867
|
"com.oauthessentials.WEB_APPLE_DEEP_LINK_RECEIVED_INTENT"
|
|
616
868
|
const val WEB_APPLE_CANCELLED_INTENT =
|
|
617
869
|
"com.oauthessentials.WEB_APPLE_CANCELLED_INTENT"
|
|
870
|
+
const val WEB_BROWSER_UNAVAILABLE_INTENT =
|
|
871
|
+
"com.oauthessentials.WEB_BROWSER_UNAVAILABLE_INTENT"
|
|
618
872
|
const val DISPLAY_CUSTOM_TABS_INTENT = "com.oauthessentials.DISPLAY_CUSTOM_TABS_INTENT"
|
|
873
|
+
|
|
874
|
+
private const val AUTHORIZATION_REQUEST_CODE = 0x0A07
|
|
619
875
|
}
|
|
620
876
|
}
|