react-native-oauth-essentials 1.0.12 → 1.2.0
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/OauthEssentials.podspec +17 -2
- package/README.md +385 -361
- package/android/build.gradle +15 -10
- 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 +445 -187
- 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 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +9 -5
- 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 -1
|
@@ -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
|
+
}
|
|
219
217
|
|
|
220
|
-
|
|
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
|
+
}
|
|
256
|
+
|
|
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)
|
|
318
|
+
} catch (e: Throwable) {
|
|
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)
|
|
274
370
|
} catch (e: Throwable) {
|
|
275
|
-
|
|
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,11 +481,6 @@ 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
|
-
var preferImmediatelyAvailableCredentials = false
|
|
394
|
-
if (options.hasKey("preferImmediatelyAvailableCredentials")) {
|
|
395
|
-
preferImmediatelyAvailableCredentials =
|
|
396
|
-
options.getBoolean("preferImmediatelyAvailableCredentials")
|
|
397
|
-
}
|
|
398
484
|
// `isConditional` (conditional create) só existe em androidx.credentials
|
|
399
485
|
// >= 1.6.0. Estamos fixados em 1.5.0 estável (ver build.gradle), então o
|
|
400
486
|
// parâmetro não é suportado aqui.
|
|
@@ -402,9 +488,9 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
402
488
|
val manager = CredentialManager.create(activity)
|
|
403
489
|
val request = CreatePublicKeyCredentialRequest(
|
|
404
490
|
requestJson = requestJson,
|
|
405
|
-
preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
|
|
491
|
+
preferImmediatelyAvailableCredentials = options.optBoolean("preferImmediatelyAvailableCredentials")
|
|
406
492
|
)
|
|
407
|
-
// createCredential lança UI: deve rodar na Main thread
|
|
493
|
+
// createCredential lança UI: deve rodar na Main thread.
|
|
408
494
|
val result = withContext(Dispatchers.Main) {
|
|
409
495
|
manager.createCredential(
|
|
410
496
|
request = request,
|
|
@@ -412,17 +498,11 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
412
498
|
)
|
|
413
499
|
}
|
|
414
500
|
resolveAndEmit(CredentialFactory.fromPublicKeyCredential(result), promise)
|
|
501
|
+
} catch (e: CreateCredentialCancellationException) {
|
|
502
|
+
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
415
503
|
} catch (e: Throwable) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
resolveAndEmit(CredentialFactory.fromCancelled(), promise)
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
else -> {
|
|
422
|
-
Log.e(LOG_TAG, e.toString())
|
|
423
|
-
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e.toString(), promise)
|
|
424
|
-
}
|
|
425
|
-
}
|
|
504
|
+
Log.e(LOG_TAG, e.toString())
|
|
505
|
+
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e, promise)
|
|
426
506
|
}
|
|
427
507
|
}
|
|
428
508
|
}
|
|
@@ -430,12 +510,11 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
430
510
|
/**
|
|
431
511
|
* Gets the passkey.
|
|
432
512
|
*/
|
|
433
|
-
@RequiresApi(Build.VERSION_CODES.P)
|
|
434
513
|
override fun getPassKey(options: ReadableMap, promise: Promise) {
|
|
435
514
|
if (!getPasskeysSupported()) {
|
|
436
515
|
rejectAndEmitEvent(
|
|
437
516
|
CredentialError.NOT_SUPPORTED_ERROR.code,
|
|
438
|
-
"Device does not support
|
|
517
|
+
"Device does not support passkeys.",
|
|
439
518
|
promise
|
|
440
519
|
)
|
|
441
520
|
return
|
|
@@ -451,11 +530,6 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
451
530
|
try {
|
|
452
531
|
val requestJson = options.getString("requestJson")
|
|
453
532
|
?: throw IllegalArgumentException("\"requestJson\" field should be set on android.")
|
|
454
|
-
var preferImmediatelyAvailableCredentials = false
|
|
455
|
-
if (options.hasKey("preferImmediatelyAvailableCredentials")) {
|
|
456
|
-
preferImmediatelyAvailableCredentials =
|
|
457
|
-
options.getBoolean("preferImmediatelyAvailableCredentials")
|
|
458
|
-
}
|
|
459
533
|
|
|
460
534
|
val manager = CredentialManager.create(activity)
|
|
461
535
|
val request = GetCredentialRequest(
|
|
@@ -464,9 +538,9 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
464
538
|
requestJson = requestJson,
|
|
465
539
|
),
|
|
466
540
|
),
|
|
467
|
-
preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
|
|
541
|
+
preferImmediatelyAvailableCredentials = options.optBoolean("preferImmediatelyAvailableCredentials")
|
|
468
542
|
)
|
|
469
|
-
// getCredential lança UI: deve rodar na Main thread
|
|
543
|
+
// getCredential lança UI: deve rodar na Main thread.
|
|
470
544
|
val result = withContext(Dispatchers.Main) {
|
|
471
545
|
manager.getCredential(
|
|
472
546
|
request = request,
|
|
@@ -474,19 +548,142 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
474
548
|
)
|
|
475
549
|
}
|
|
476
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)
|
|
477
555
|
} catch (e: Throwable) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
556
|
+
Log.e(LOG_TAG, e.toString())
|
|
557
|
+
rejectAndEmitEvent(CredentialError.INVALID_RESULT_ERROR.code, e, promise)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
482
561
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
)
|
|
488
683
|
}
|
|
489
684
|
}
|
|
685
|
+
if (resultCode != Activity.RESULT_OK) return null
|
|
686
|
+
return client.getAuthorizationResultFromIntent(data)
|
|
490
687
|
}
|
|
491
688
|
|
|
492
689
|
/**
|
|
@@ -498,7 +695,6 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
498
695
|
return result == ConnectionResult.SUCCESS
|
|
499
696
|
}
|
|
500
697
|
|
|
501
|
-
@RequiresApi(Build.VERSION_CODES.O)
|
|
502
698
|
override fun initialize() {
|
|
503
699
|
super.initialize()
|
|
504
700
|
|
|
@@ -510,9 +706,11 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
510
706
|
IntentFilter().apply {
|
|
511
707
|
addAction(WEB_APPLE_DEEP_LINK_RECEIVED_INTENT)
|
|
512
708
|
addAction(WEB_APPLE_CANCELLED_INTENT)
|
|
709
|
+
addAction(WEB_BROWSER_UNAVAILABLE_INTENT)
|
|
513
710
|
},
|
|
514
711
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
515
712
|
)
|
|
713
|
+
reactApplicationContext.addActivityEventListener(activityEventListener)
|
|
516
714
|
}
|
|
517
715
|
|
|
518
716
|
override fun invalidate() {
|
|
@@ -521,10 +719,11 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
521
719
|
|
|
522
720
|
Log.d(LOG_TAG, "Cancelling all coroutines and credential operations")
|
|
523
721
|
moduleScope.cancel()
|
|
722
|
+
reactApplicationContext.removeActivityEventListener(activityEventListener)
|
|
524
723
|
|
|
525
|
-
|
|
724
|
+
browserSessionSettler?.let {
|
|
526
725
|
if (!it.settled()) {
|
|
527
|
-
Log.d(LOG_TAG, "Rejecting pending
|
|
726
|
+
Log.d(LOG_TAG, "Rejecting pending browser session promise without event")
|
|
528
727
|
it.reject(
|
|
529
728
|
"CANCELLED",
|
|
530
729
|
"Module is being invalidated",
|
|
@@ -533,10 +732,10 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
533
732
|
)
|
|
534
733
|
}
|
|
535
734
|
}
|
|
536
|
-
|
|
735
|
+
browserSessionSettler = null
|
|
537
736
|
|
|
538
737
|
Log.d(LOG_TAG, "Unregistering deep link broadcast receiver")
|
|
539
|
-
reactApplicationContext.unregisterReceiver(receiver)
|
|
738
|
+
reactApplicationContext.applicationContext.unregisterReceiver(receiver)
|
|
540
739
|
}
|
|
541
740
|
|
|
542
741
|
/**
|
|
@@ -563,15 +762,71 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
563
762
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && getCredentialManagerSupported()
|
|
564
763
|
}
|
|
565
764
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
private fun
|
|
570
|
-
|
|
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
|
+
}
|
|
571
776
|
|
|
777
|
+
private fun emitFailure(code: String, message: String) {
|
|
572
778
|
val error = Arguments.createMap()
|
|
573
779
|
error.putString("code", code)
|
|
574
|
-
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())
|
|
575
830
|
}
|
|
576
831
|
|
|
577
832
|
/**
|
|
@@ -579,11 +834,7 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
579
834
|
*/
|
|
580
835
|
private fun rejectAndEmitEvent(code: String, e: String, promise: Promise) {
|
|
581
836
|
promise.reject(code, e)
|
|
582
|
-
|
|
583
|
-
val error = Arguments.createMap()
|
|
584
|
-
error.putString("code", code)
|
|
585
|
-
error.putString("message", e)
|
|
586
|
-
emitOnCredentialFailure(error)
|
|
837
|
+
emitFailure(code, e)
|
|
587
838
|
}
|
|
588
839
|
|
|
589
840
|
/**
|
|
@@ -608,11 +859,18 @@ class OauthEssentialsModule(reactContext: ReactApplicationContext) :
|
|
|
608
859
|
|
|
609
860
|
const val LOG_TAG = "NativeOauthEssentials"
|
|
610
861
|
|
|
611
|
-
//
|
|
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)
|
|
612
866
|
const val WEB_APPLE_DEEP_LINK_RECEIVED_INTENT =
|
|
613
867
|
"com.oauthessentials.WEB_APPLE_DEEP_LINK_RECEIVED_INTENT"
|
|
614
868
|
const val WEB_APPLE_CANCELLED_INTENT =
|
|
615
869
|
"com.oauthessentials.WEB_APPLE_CANCELLED_INTENT"
|
|
870
|
+
const val WEB_BROWSER_UNAVAILABLE_INTENT =
|
|
871
|
+
"com.oauthessentials.WEB_BROWSER_UNAVAILABLE_INTENT"
|
|
616
872
|
const val DISPLAY_CUSTOM_TABS_INTENT = "com.oauthessentials.DISPLAY_CUSTOM_TABS_INTENT"
|
|
873
|
+
|
|
874
|
+
private const val AUTHORIZATION_REQUEST_CODE = 0x0A07
|
|
617
875
|
}
|
|
618
876
|
}
|