@clerk/expo-native-components 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +106 -2
- package/android/build.gradle +152 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt +241 -0
- package/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt +148 -0
- package/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt +1077 -0
- package/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt +156 -0
- package/android/src/main/java/expo/modules/clerk/ClerkUserProfileCustomPageState.kt +120 -0
- package/android/src/main/java/expo/modules/clerk/ClerkUserProfileViewModule.kt +315 -0
- package/android/src/test/java/expo/modules/clerk/BiometricCredentialBridgeTest.kt +326 -0
- package/android/src/test/java/expo/modules/clerk/ClerkUserProfileCustomPageStateTest.kt +163 -0
- package/app.plugin.d.ts +5 -0
- package/app.plugin.js +333 -0
- package/dist/AuthView.d.ts +35 -0
- package/dist/AuthView.d.ts.map +1 -0
- package/dist/AuthView.js +79 -0
- package/dist/AuthView.js.map +1 -0
- package/dist/AuthView.types.d.ts +62 -0
- package/dist/AuthView.types.d.ts.map +1 -0
- package/dist/AuthView.types.js +0 -0
- package/dist/EmbeddedNavigation.types.d.ts +24 -0
- package/dist/EmbeddedNavigation.types.d.ts.map +1 -0
- package/dist/EmbeddedNavigation.types.js +0 -0
- package/dist/UserButton.d.ts +38 -0
- package/dist/UserButton.d.ts.map +1 -0
- package/dist/UserButton.js +68 -0
- package/dist/UserButton.js.map +1 -0
- package/dist/UserProfileCustomPages.d.ts +81 -0
- package/dist/UserProfileCustomPages.d.ts.map +1 -0
- package/dist/UserProfileCustomPages.js +140 -0
- package/dist/UserProfileCustomPages.js.map +1 -0
- package/dist/UserProfileView.d.ts +76 -0
- package/dist/UserProfileView.d.ts.map +1 -0
- package/dist/UserProfileView.js +101 -0
- package/dist/UserProfileView.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +29 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/specs/NativeClerkAuthView.android.d.ts +15 -0
- package/dist/specs/NativeClerkAuthView.android.d.ts.map +1 -0
- package/dist/specs/NativeClerkAuthView.android.js +8 -0
- package/dist/specs/NativeClerkAuthView.android.js.map +1 -0
- package/dist/specs/NativeClerkAuthView.d.ts +15 -0
- package/dist/specs/NativeClerkAuthView.d.ts.map +1 -0
- package/dist/specs/NativeClerkAuthView.js +9 -0
- package/dist/specs/NativeClerkAuthView.js.map +1 -0
- package/dist/specs/NativeClerkModule.d.ts +4 -0
- package/dist/specs/NativeClerkModule.d.ts.map +1 -0
- package/dist/specs/NativeClerkModule.js +8 -0
- package/dist/specs/NativeClerkModule.js.map +1 -0
- package/dist/specs/NativeClerkModule.types.d.ts +11 -0
- package/dist/specs/NativeClerkModule.types.d.ts.map +1 -0
- package/dist/specs/NativeClerkModule.types.js +0 -0
- package/dist/specs/NativeClerkModule.web.d.ts +4 -0
- package/dist/specs/NativeClerkModule.web.d.ts.map +1 -0
- package/dist/specs/NativeClerkModule.web.js +7 -0
- package/dist/specs/NativeClerkModule.web.js.map +1 -0
- package/dist/specs/NativeClerkUserButtonView.android.d.ts +6 -0
- package/dist/specs/NativeClerkUserButtonView.android.d.ts.map +1 -0
- package/dist/specs/NativeClerkUserButtonView.android.js +8 -0
- package/dist/specs/NativeClerkUserButtonView.android.js.map +1 -0
- package/dist/specs/NativeClerkUserButtonView.d.ts +6 -0
- package/dist/specs/NativeClerkUserButtonView.d.ts.map +1 -0
- package/dist/specs/NativeClerkUserButtonView.js +9 -0
- package/dist/specs/NativeClerkUserButtonView.js.map +1 -0
- package/dist/specs/NativeClerkUserProfileView.android.d.ts +13 -0
- package/dist/specs/NativeClerkUserProfileView.android.d.ts.map +1 -0
- package/dist/specs/NativeClerkUserProfileView.android.js +8 -0
- package/dist/specs/NativeClerkUserProfileView.android.js.map +1 -0
- package/dist/specs/NativeClerkUserProfileView.d.ts +13 -0
- package/dist/specs/NativeClerkUserProfileView.d.ts.map +1 -0
- package/dist/specs/NativeClerkUserProfileView.js +9 -0
- package/dist/specs/NativeClerkUserProfileView.js.map +1 -0
- package/dist/useAuthViewState.d.ts +12 -0
- package/dist/useAuthViewState.d.ts.map +1 -0
- package/dist/useAuthViewState.js +76 -0
- package/dist/useAuthViewState.js.map +1 -0
- package/dist/utils/native-module.d.ts +3 -0
- package/dist/utils/native-module.d.ts.map +1 -0
- package/dist/utils/native-module.js +14 -0
- package/dist/utils/native-module.js.map +1 -0
- package/expo-module.config.json +15 -0
- package/ios/ClerkAppDelegateSubscriber.swift +16 -0
- package/ios/ClerkAuthNativeView.swift +157 -0
- package/ios/ClerkExpo.podspec +67 -0
- package/ios/ClerkExpoModule.swift +315 -0
- package/ios/ClerkNativeBridge.swift +1575 -0
- package/ios/ClerkNativeViewHost.swift +204 -0
- package/ios/ClerkUserButtonNativeView.swift +34 -0
- package/ios/ClerkUserProfileNativeView.swift +92 -0
- package/ios/Tests/ClerkAuthNativeViewPaperTests.swift +30 -0
- package/ios/Tests/ClerkNativeBridgeTests.swift +185 -0
- package/ios/Tests/ClerkNativeViewHostTests.swift +66 -0
- package/ios/Tests/ClerkUserProfileCustomPageStateTests.swift +311 -0
- package/package.json +79 -3
- package/react-native.config.js +8 -0
- package/src/specs/NativeClerkAuthView.android.ts +15 -0
- package/src/specs/NativeClerkAuthView.ts +19 -0
- package/src/specs/NativeClerkModule.ts +6 -0
- package/src/specs/NativeClerkModule.types.ts +9 -0
- package/src/specs/NativeClerkModule.web.ts +3 -0
- package/src/specs/NativeClerkUserButtonView.android.ts +9 -0
- package/src/specs/NativeClerkUserButtonView.ts +11 -0
- package/src/specs/NativeClerkUserProfileView.android.ts +13 -0
- package/src/specs/NativeClerkUserProfileView.ts +17 -0
- package/src/specs/__tests__/native-view.web.test.ts +43 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
package expo.modules.clerk
|
|
2
|
+
|
|
3
|
+
import com.clerk.api.network.model.error.ClerkErrorResponse
|
|
4
|
+
import com.clerk.api.network.model.error.Error as ClerkAPIError
|
|
5
|
+
import com.clerk.api.network.serialization.ClerkResult
|
|
6
|
+
import com.clerk.api.signin.SignIn
|
|
7
|
+
import com.clerk.api.biometriccredential.BiometricCredential
|
|
8
|
+
import com.clerk.api.biometriccredential.BiometricCredentialAvailability
|
|
9
|
+
import com.clerk.api.biometriccredential.BiometricCredentialKeyManagerException
|
|
10
|
+
import com.clerk.api.biometriccredential.BiometricCredentialPolicy
|
|
11
|
+
import com.clerk.api.session.SessionVerification
|
|
12
|
+
import kotlinx.coroutines.cancel
|
|
13
|
+
import kotlinx.coroutines.currentCoroutineContext
|
|
14
|
+
import kotlinx.coroutines.launch
|
|
15
|
+
import kotlinx.coroutines.runBlocking
|
|
16
|
+
import org.junit.Assert.assertEquals
|
|
17
|
+
import org.junit.Assert.assertNull
|
|
18
|
+
import org.junit.Assert.assertSame
|
|
19
|
+
import org.junit.Assert.assertTrue
|
|
20
|
+
import org.junit.Test
|
|
21
|
+
|
|
22
|
+
class BiometricCredentialBridgeTest {
|
|
23
|
+
@Test
|
|
24
|
+
fun `continues the same attempt when the first factor requires a second factor`() = runBlocking {
|
|
25
|
+
val factors = mutableListOf<SessionVerification.Level>()
|
|
26
|
+
val started = SessionVerification(id = "stepup_test", status = SessionVerification.Status.NEEDS_FIRST_FACTOR,
|
|
27
|
+
level = SessionVerification.Level.MULTI_FACTOR)
|
|
28
|
+
val result = verifyBiometricReverification(started) { factor ->
|
|
29
|
+
factors.add(factor)
|
|
30
|
+
ClerkResult.success(started.copy(status = if (factor == SessionVerification.Level.FIRST_FACTOR)
|
|
31
|
+
SessionVerification.Status.NEEDS_SECOND_FACTOR else SessionVerification.Status.COMPLETE))
|
|
32
|
+
}
|
|
33
|
+
assertEquals(listOf(SessionVerification.Level.FIRST_FACTOR, SessionVerification.Level.SECOND_FACTOR), factors)
|
|
34
|
+
assertTrue(result is ClerkResult.Success)
|
|
35
|
+
assertEquals(started.id, (result as ClerkResult.Success).value.id)
|
|
36
|
+
assertEquals(SessionVerification.Status.COMPLETE, result.value.status)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
fun `prompts only for the factors still required`() = runBlocking {
|
|
41
|
+
for (status in listOf(SessionVerification.Status.NEEDS_FIRST_FACTOR,
|
|
42
|
+
SessionVerification.Status.NEEDS_SECOND_FACTOR, SessionVerification.Status.COMPLETE)) {
|
|
43
|
+
val factors = mutableListOf<SessionVerification.Level>()
|
|
44
|
+
val started = SessionVerification(status = status, level = SessionVerification.Level.MULTI_FACTOR)
|
|
45
|
+
val result = verifyBiometricReverification(started) { factor ->
|
|
46
|
+
factors.add(factor)
|
|
47
|
+
ClerkResult.success(started.copy(status = SessionVerification.Status.COMPLETE))
|
|
48
|
+
}
|
|
49
|
+
val expected = when (status) {
|
|
50
|
+
SessionVerification.Status.NEEDS_FIRST_FACTOR -> listOf(SessionVerification.Level.FIRST_FACTOR)
|
|
51
|
+
SessionVerification.Status.NEEDS_SECOND_FACTOR -> listOf(SessionVerification.Level.SECOND_FACTOR)
|
|
52
|
+
else -> emptyList()
|
|
53
|
+
}
|
|
54
|
+
assertEquals(expected, factors)
|
|
55
|
+
assertEquals(SessionVerification.Status.COMPLETE, (result as ClerkResult.Success).value.status)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
fun `does not retry an incomplete second factor`() = runBlocking {
|
|
61
|
+
val factors = mutableListOf<SessionVerification.Level>()
|
|
62
|
+
val started = SessionVerification(status = SessionVerification.Status.NEEDS_FIRST_FACTOR,
|
|
63
|
+
level = SessionVerification.Level.MULTI_FACTOR)
|
|
64
|
+
val result = verifyBiometricReverification(started) { factor ->
|
|
65
|
+
factors.add(factor)
|
|
66
|
+
ClerkResult.success(started.copy(status = SessionVerification.Status.NEEDS_SECOND_FACTOR))
|
|
67
|
+
}
|
|
68
|
+
assertEquals(listOf(SessionVerification.Level.FIRST_FACTOR, SessionVerification.Level.SECOND_FACTOR), factors)
|
|
69
|
+
assertEquals(SessionVerification.Status.NEEDS_SECOND_FACTOR, (result as ClerkResult.Success).value.status)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test
|
|
73
|
+
fun `preserves first and second factor failures`() = runBlocking {
|
|
74
|
+
for (failingFactor in listOf(SessionVerification.Level.FIRST_FACTOR, SessionVerification.Level.SECOND_FACTOR)) {
|
|
75
|
+
val factors = mutableListOf<SessionVerification.Level>()
|
|
76
|
+
val failure = ClerkResult.apiFailure(ClerkErrorResponse(errors = listOf(
|
|
77
|
+
ClerkAPIError(code = "biometric_authentication_canceled", message = "Canceled")
|
|
78
|
+
)))
|
|
79
|
+
val started = SessionVerification(status = SessionVerification.Status.NEEDS_FIRST_FACTOR,
|
|
80
|
+
level = SessionVerification.Level.MULTI_FACTOR)
|
|
81
|
+
val result = verifyBiometricReverification(started) { factor ->
|
|
82
|
+
factors.add(factor)
|
|
83
|
+
if (factor == failingFactor) failure else
|
|
84
|
+
ClerkResult.success(started.copy(status = SessionVerification.Status.NEEDS_SECOND_FACTOR))
|
|
85
|
+
}
|
|
86
|
+
assertSame(failure, result)
|
|
87
|
+
assertEquals(if (failingFactor == SessionVerification.Level.FIRST_FACTOR)
|
|
88
|
+
listOf(SessionVerification.Level.FIRST_FACTOR) else
|
|
89
|
+
listOf(SessionVerification.Level.FIRST_FACTOR, SessionVerification.Level.SECOND_FACTOR), factors)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
fun `cancellation between factors stops continuation`() = runBlocking {
|
|
95
|
+
val factors = mutableListOf<SessionVerification.Level>()
|
|
96
|
+
val job = launch {
|
|
97
|
+
verifyBiometricReverification(SessionVerification(status = SessionVerification.Status.NEEDS_FIRST_FACTOR,
|
|
98
|
+
level = SessionVerification.Level.MULTI_FACTOR)) { factor ->
|
|
99
|
+
factors.add(factor)
|
|
100
|
+
currentCoroutineContext().cancel()
|
|
101
|
+
ClerkResult.success(SessionVerification(status = SessionVerification.Status.NEEDS_SECOND_FACTOR,
|
|
102
|
+
level = SessionVerification.Level.MULTI_FACTOR))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
job.join()
|
|
106
|
+
assertTrue(job.isCancelled)
|
|
107
|
+
assertEquals(listOf(SessionVerification.Level.FIRST_FACTOR), factors)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Test
|
|
111
|
+
fun `unknown verification status does not prompt`() = runBlocking {
|
|
112
|
+
val result = verifyBiometricReverification(SessionVerification(status = SessionVerification.Status.UNKNOWN,
|
|
113
|
+
level = SessionVerification.Level.MULTI_FACTOR)) {
|
|
114
|
+
throw AssertionError("Unknown verification status must not prompt")
|
|
115
|
+
}
|
|
116
|
+
assertTrue(result is ClerkResult.Failure)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Test
|
|
120
|
+
fun `maps biometric reverification requirements`() {
|
|
121
|
+
assertEquals(SessionVerification.Level.FIRST_FACTOR, biometricReverificationLevel("first_factor"))
|
|
122
|
+
assertEquals(SessionVerification.Level.SECOND_FACTOR, biometricReverificationLevel("second_factor"))
|
|
123
|
+
assertEquals(SessionVerification.Level.MULTI_FACTOR, biometricReverificationLevel("multi_factor"))
|
|
124
|
+
assertNull(biometricReverificationLevel("unknown"))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun `maps reverification results without creating a session`() {
|
|
129
|
+
val result = SessionVerification(
|
|
130
|
+
id = "stepup_test",
|
|
131
|
+
status = SessionVerification.Status.COMPLETE,
|
|
132
|
+
level = SessionVerification.Level.MULTI_FACTOR
|
|
133
|
+
)
|
|
134
|
+
assertEquals(mapOf(
|
|
135
|
+
"id" to "stepup_test",
|
|
136
|
+
"status" to "complete",
|
|
137
|
+
"level" to "multi_factor",
|
|
138
|
+
"sessionId" to "sess_test"
|
|
139
|
+
), biometricReverificationPayload(result, "sess_test"))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private fun keyManagerException(
|
|
143
|
+
code: BiometricCredentialKeyManagerException.Code,
|
|
144
|
+
message: String
|
|
145
|
+
): BiometricCredentialKeyManagerException {
|
|
146
|
+
val constructor = BiometricCredentialKeyManagerException::class.java.getDeclaredConstructor(
|
|
147
|
+
BiometricCredentialKeyManagerException.Code::class.java,
|
|
148
|
+
String::class.java,
|
|
149
|
+
Throwable::class.java
|
|
150
|
+
)
|
|
151
|
+
constructor.isAccessible = true
|
|
152
|
+
return constructor.newInstance(code, message, null)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
fun `requires Clerk initialization before biometric-credential operations`() {
|
|
157
|
+
assertEquals(
|
|
158
|
+
BiometricCredentialBridgeError(
|
|
159
|
+
code = "environment_unavailable",
|
|
160
|
+
message = "Biometric credential operations are unavailable until Clerk finishes configuring."
|
|
161
|
+
),
|
|
162
|
+
biometricCredentialEnvironmentError(isInitialized = false)
|
|
163
|
+
)
|
|
164
|
+
assertNull(biometricCredentialEnvironmentError(isInitialized = true))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@Test
|
|
168
|
+
fun `reports unavailable biometric credentials before Clerk initialization`() {
|
|
169
|
+
assertEquals(
|
|
170
|
+
mapOf(
|
|
171
|
+
"isAvailable" to false,
|
|
172
|
+
"unavailableReason" to "environment_unavailable"
|
|
173
|
+
),
|
|
174
|
+
biometricCredentialEnvironmentAvailabilityPayload(isInitialized = false)
|
|
175
|
+
)
|
|
176
|
+
assertNull(biometricCredentialEnvironmentAvailabilityPayload(isInitialized = true))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@Test
|
|
180
|
+
fun `maps biometric-credential availability to the JavaScript contract`() {
|
|
181
|
+
assertEquals(
|
|
182
|
+
mapOf("isAvailable" to true, "unavailableReason" to null),
|
|
183
|
+
biometricCredentialAvailabilityPayload(BiometricCredentialAvailability.Available)
|
|
184
|
+
)
|
|
185
|
+
assertEquals(
|
|
186
|
+
mapOf(
|
|
187
|
+
"isAvailable" to false,
|
|
188
|
+
"unavailableReason" to "biometric_authentication_unavailable"
|
|
189
|
+
),
|
|
190
|
+
biometricCredentialAvailabilityPayload(
|
|
191
|
+
BiometricCredentialAvailability.Unavailable(
|
|
192
|
+
BiometricCredentialAvailability.UnavailableReason.BIOMETRIC_AUTHENTICATION_UNAVAILABLE
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@Test
|
|
199
|
+
fun `maps biometric-credential resources to the JavaScript contract`() {
|
|
200
|
+
val payload = biometricCredentialPayload(
|
|
201
|
+
BiometricCredential(
|
|
202
|
+
id = "td_123",
|
|
203
|
+
platform = BiometricCredential.Platform.ANDROID,
|
|
204
|
+
appIdentifier = "com.example.app",
|
|
205
|
+
name = "Pixel",
|
|
206
|
+
status = BiometricCredential.Status.ACTIVE,
|
|
207
|
+
createdAt = 1_700_000_000_000,
|
|
208
|
+
updatedAt = 1_700_000_100_000,
|
|
209
|
+
lastUsedAt = 1_700_000_200_000
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
assertEquals("trusted_device", payload["object"])
|
|
214
|
+
assertEquals("android", payload["platform"])
|
|
215
|
+
assertEquals("active", payload["status"])
|
|
216
|
+
assertEquals("ES256", payload["algorithm"])
|
|
217
|
+
assertEquals(1_700_000_200_000, payload["lastUsedAt"])
|
|
218
|
+
assertNull(payload["revokedAt"])
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Test
|
|
222
|
+
fun `maps every supported authentication policy`() {
|
|
223
|
+
assertEquals(
|
|
224
|
+
BiometricCredentialPolicy.BIOMETRY_CURRENT_SET,
|
|
225
|
+
biometricCredentialPolicy("biometry_current_set")
|
|
226
|
+
)
|
|
227
|
+
assertEquals(BiometricCredentialPolicy.BIOMETRY_ANY, biometricCredentialPolicy("biometry_any"))
|
|
228
|
+
assertEquals(
|
|
229
|
+
BiometricCredentialPolicy.BIOMETRY_OR_DEVICE_PASSCODE,
|
|
230
|
+
biometricCredentialPolicy("biometry_or_device_passcode")
|
|
231
|
+
)
|
|
232
|
+
assertNull(biometricCredentialPolicy("unsupported"))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@Test
|
|
236
|
+
fun `maps biometric sign-in results`() {
|
|
237
|
+
assertEquals(
|
|
238
|
+
mapOf(
|
|
239
|
+
"id" to "sia_123",
|
|
240
|
+
"status" to "complete",
|
|
241
|
+
"createdSessionId" to "sess_123"
|
|
242
|
+
),
|
|
243
|
+
biometricSignInPayload(
|
|
244
|
+
SignIn(
|
|
245
|
+
id = "sia_123",
|
|
246
|
+
status = SignIn.Status.COMPLETE,
|
|
247
|
+
createdSessionId = "sess_123"
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@Test
|
|
254
|
+
fun `preserves Clerk API error codes and detailed messages`() {
|
|
255
|
+
val failure = ClerkResult.apiFailure(
|
|
256
|
+
ClerkErrorResponse(
|
|
257
|
+
errors = listOf(
|
|
258
|
+
ClerkAPIError(
|
|
259
|
+
code = "trusted_device_not_registered",
|
|
260
|
+
message = "Biometric credential not found.",
|
|
261
|
+
longMessage = "This device is no longer registered as trusted."
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
assertEquals(
|
|
268
|
+
BiometricCredentialBridgeError(
|
|
269
|
+
code = "trusted_device_not_registered",
|
|
270
|
+
message = "This device is no longer registered as trusted."
|
|
271
|
+
),
|
|
272
|
+
biometricCredentialBridgeError(
|
|
273
|
+
failure = failure,
|
|
274
|
+
fallbackCode = "E_TRUSTED_DEVICE_SIGN_IN_FAILED",
|
|
275
|
+
fallbackMessage = "Unable to sign in with biometric credential"
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@Test
|
|
281
|
+
fun `normalizes key-manager exceptions from bridge operations`() {
|
|
282
|
+
assertEquals(
|
|
283
|
+
BiometricCredentialBridgeError(
|
|
284
|
+
code = "key_invalidated",
|
|
285
|
+
message = "The biometric credential key was invalidated."
|
|
286
|
+
),
|
|
287
|
+
biometricCredentialBridgeError(
|
|
288
|
+
throwable = keyManagerException(
|
|
289
|
+
BiometricCredentialKeyManagerException.Code.KEY_INVALIDATED,
|
|
290
|
+
"The biometric credential key was invalidated."
|
|
291
|
+
),
|
|
292
|
+
fallbackCode = "E_TRUSTED_DEVICE_SIGN_IN_FAILED",
|
|
293
|
+
fallbackMessage = "Unable to sign in with biometric credential"
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@Test
|
|
299
|
+
fun `uses fallback details for plain bridge exceptions`() {
|
|
300
|
+
assertEquals(
|
|
301
|
+
BiometricCredentialBridgeError(
|
|
302
|
+
code = "E_TRUSTED_DEVICE_SIGN_IN_FAILED",
|
|
303
|
+
message = "Unable to sign in with biometric credential"
|
|
304
|
+
),
|
|
305
|
+
biometricCredentialBridgeError(
|
|
306
|
+
throwable = Exception(),
|
|
307
|
+
fallbackCode = "E_TRUSTED_DEVICE_SIGN_IN_FAILED",
|
|
308
|
+
fallbackMessage = "Unable to sign in with biometric credential"
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@Test
|
|
314
|
+
fun `normalizes native key-manager error codes`() {
|
|
315
|
+
assertEquals(
|
|
316
|
+
"biometric_authentication_canceled",
|
|
317
|
+
biometricCredentialKeyManagerErrorCode(
|
|
318
|
+
BiometricCredentialKeyManagerException.Code.BIOMETRIC_AUTHENTICATION_CANCELED
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
assertEquals(
|
|
322
|
+
"key_invalidated",
|
|
323
|
+
biometricCredentialKeyManagerErrorCode(BiometricCredentialKeyManagerException.Code.KEY_INVALIDATED)
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
package expo.modules.clerk
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class ClerkUserProfileCustomPageStateTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun coveredPageRemainsRetainedUntilItIsActuallyPopped() {
|
|
9
|
+
val events = mutableListOf<String>()
|
|
10
|
+
val pushedPaths = mutableListOf<String>()
|
|
11
|
+
val state = state(events, push = pushedPaths::add)
|
|
12
|
+
|
|
13
|
+
state.pageDidPresent("billing")
|
|
14
|
+
state.navigate("push", "invoice-details")
|
|
15
|
+
state.pageDidDismiss("billing")
|
|
16
|
+
state.pageDidPresent("invoice-details")
|
|
17
|
+
|
|
18
|
+
assertEquals(listOf("billing", "invoice-details"), state.retainedPathsForTesting())
|
|
19
|
+
assertEquals(listOf("invoice-details"), pushedPaths)
|
|
20
|
+
assertEquals(
|
|
21
|
+
listOf("presented:billing", "dismissed:billing", "presented:invoice-details"),
|
|
22
|
+
events,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
state.pageDidDismiss("invoice-details")
|
|
26
|
+
state.pageDidPresent("billing")
|
|
27
|
+
|
|
28
|
+
assertEquals(listOf("billing"), state.retainedPathsForTesting())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun closingUserButtonProfileDismissesCoveredPages() {
|
|
33
|
+
val events = mutableListOf<String>()
|
|
34
|
+
val pendingResets = mutableListOf<() -> Unit>()
|
|
35
|
+
val state =
|
|
36
|
+
state(
|
|
37
|
+
events,
|
|
38
|
+
resetCoveredPathsWhenInactive = true,
|
|
39
|
+
postInactiveReset = pendingResets::add,
|
|
40
|
+
)
|
|
41
|
+
state.pageDidPresent("billing")
|
|
42
|
+
state.navigate("push", "invoice-details")
|
|
43
|
+
state.pageDidDismiss("billing")
|
|
44
|
+
state.pageDidPresent("invoice-details")
|
|
45
|
+
events.clear()
|
|
46
|
+
|
|
47
|
+
state.pageDidDismiss("invoice-details")
|
|
48
|
+
pendingResets.single().invoke()
|
|
49
|
+
|
|
50
|
+
assertEquals(emptyList<String>(), state.retainedPathsForTesting())
|
|
51
|
+
assertEquals(listOf("dismissed:invoice-details", "dismissed:billing"), events)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
fun returningToCoveredUserButtonPageCancelsInactiveReset() {
|
|
56
|
+
val events = mutableListOf<String>()
|
|
57
|
+
val pendingResets = mutableListOf<() -> Unit>()
|
|
58
|
+
val state =
|
|
59
|
+
state(
|
|
60
|
+
events,
|
|
61
|
+
resetCoveredPathsWhenInactive = true,
|
|
62
|
+
postInactiveReset = pendingResets::add,
|
|
63
|
+
)
|
|
64
|
+
state.pageDidPresent("billing")
|
|
65
|
+
state.navigate("push", "invoice-details")
|
|
66
|
+
state.pageDidDismiss("billing")
|
|
67
|
+
state.pageDidPresent("invoice-details")
|
|
68
|
+
events.clear()
|
|
69
|
+
|
|
70
|
+
state.pageDidDismiss("invoice-details")
|
|
71
|
+
state.pageDidPresent("billing")
|
|
72
|
+
pendingResets.single().invoke()
|
|
73
|
+
|
|
74
|
+
assertEquals(listOf("billing"), state.retainedPathsForTesting())
|
|
75
|
+
assertEquals(listOf("dismissed:invoice-details", "presented:billing"), events)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Test
|
|
79
|
+
fun pushingAPathAlreadyInTheStackDoesNotPushOrCollapseIt() {
|
|
80
|
+
val events = mutableListOf<String>()
|
|
81
|
+
val pushedPaths = mutableListOf<String>()
|
|
82
|
+
val state = state(events, push = pushedPaths::add)
|
|
83
|
+
state.pageDidPresent("billing")
|
|
84
|
+
state.navigate("push", "invoice-details")
|
|
85
|
+
state.pageDidDismiss("billing")
|
|
86
|
+
state.pageDidPresent("invoice-details")
|
|
87
|
+
events.clear()
|
|
88
|
+
|
|
89
|
+
state.navigate("push", "billing")
|
|
90
|
+
|
|
91
|
+
assertEquals(listOf("billing", "invoice-details"), state.retainedPathsForTesting())
|
|
92
|
+
assertEquals(listOf("invoice-details"), pushedPaths)
|
|
93
|
+
assertEquals(emptyList<String>(), events)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun popToRootDismissesEveryRetainedPageDeepestFirst() {
|
|
98
|
+
val events = mutableListOf<String>()
|
|
99
|
+
var popToRootCount = 0
|
|
100
|
+
val state = state(events, popToRoot = { popToRootCount += 1 })
|
|
101
|
+
state.pageDidPresent("billing")
|
|
102
|
+
state.navigate("push", "invoice-details")
|
|
103
|
+
state.pageDidDismiss("billing")
|
|
104
|
+
state.pageDidPresent("invoice-details")
|
|
105
|
+
events.clear()
|
|
106
|
+
|
|
107
|
+
state.navigate("popToRoot", null)
|
|
108
|
+
|
|
109
|
+
assertEquals(emptyList<String>(), state.retainedPathsForTesting())
|
|
110
|
+
assertEquals(1, popToRootCount)
|
|
111
|
+
assertEquals(listOf("dismissed:invoice-details", "dismissed:billing"), events)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Test
|
|
115
|
+
fun removingAnEarlierRouteInvalidatesTheWholeStack() {
|
|
116
|
+
val events = mutableListOf<String>()
|
|
117
|
+
var popToRootCount = 0
|
|
118
|
+
val state = state(events, popToRoot = { popToRootCount += 1 })
|
|
119
|
+
state.pageDidPresent("billing")
|
|
120
|
+
state.navigate("push", "invoice-details")
|
|
121
|
+
state.pageDidDismiss("billing")
|
|
122
|
+
state.pageDidPresent("invoice-details")
|
|
123
|
+
events.clear()
|
|
124
|
+
|
|
125
|
+
state.reconcileCustomPagePaths(setOf("invoice-details"))
|
|
126
|
+
|
|
127
|
+
assertEquals(emptyList<String>(), state.retainedPathsForTesting())
|
|
128
|
+
assertEquals(1, popToRootCount)
|
|
129
|
+
assertEquals(listOf("dismissed:invoice-details", "dismissed:billing"), events)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test
|
|
133
|
+
fun changingUsersInvalidatesRetainedNavigationOnce() {
|
|
134
|
+
val events = mutableListOf<String>()
|
|
135
|
+
var popToRootCount = 0
|
|
136
|
+
val state = state(events, popToRoot = { popToRootCount += 1 })
|
|
137
|
+
state.userDidChange("user_1")
|
|
138
|
+
state.pageDidPresent("billing")
|
|
139
|
+
events.clear()
|
|
140
|
+
|
|
141
|
+
state.userDidChange(null)
|
|
142
|
+
state.userDidChange(null)
|
|
143
|
+
|
|
144
|
+
assertEquals(emptyList<String>(), state.retainedPathsForTesting())
|
|
145
|
+
assertEquals(1, popToRootCount)
|
|
146
|
+
assertEquals(listOf("dismissed:billing"), events)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun state(
|
|
150
|
+
events: MutableList<String>,
|
|
151
|
+
popToRoot: () -> Unit = {},
|
|
152
|
+
push: (String) -> Unit = {},
|
|
153
|
+
resetCoveredPathsWhenInactive: Boolean = false,
|
|
154
|
+
postInactiveReset: ((() -> Unit) -> Unit) = { it() },
|
|
155
|
+
): ClerkUserProfileCustomPageState {
|
|
156
|
+
return ClerkUserProfileCustomPageState(
|
|
157
|
+
resetCoveredPathsWhenInactive = resetCoveredPathsWhenInactive,
|
|
158
|
+
postInactiveReset = postInactiveReset,
|
|
159
|
+
) { type, path -> events.add("$type:$path") }.also {
|
|
160
|
+
it.configureNavigation(navigateBack = {}, popToRoot = popToRoot, push = push)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|