@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -2
  3. package/android/build.gradle +152 -0
  4. package/android/src/main/AndroidManifest.xml +4 -0
  5. package/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt +241 -0
  6. package/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt +148 -0
  7. package/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt +1077 -0
  8. package/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt +156 -0
  9. package/android/src/main/java/expo/modules/clerk/ClerkUserProfileCustomPageState.kt +120 -0
  10. package/android/src/main/java/expo/modules/clerk/ClerkUserProfileViewModule.kt +315 -0
  11. package/android/src/test/java/expo/modules/clerk/BiometricCredentialBridgeTest.kt +326 -0
  12. package/android/src/test/java/expo/modules/clerk/ClerkUserProfileCustomPageStateTest.kt +163 -0
  13. package/app.plugin.d.ts +5 -0
  14. package/app.plugin.js +333 -0
  15. package/dist/AuthView.d.ts +35 -0
  16. package/dist/AuthView.d.ts.map +1 -0
  17. package/dist/AuthView.js +79 -0
  18. package/dist/AuthView.js.map +1 -0
  19. package/dist/AuthView.types.d.ts +62 -0
  20. package/dist/AuthView.types.d.ts.map +1 -0
  21. package/dist/AuthView.types.js +0 -0
  22. package/dist/EmbeddedNavigation.types.d.ts +24 -0
  23. package/dist/EmbeddedNavigation.types.d.ts.map +1 -0
  24. package/dist/EmbeddedNavigation.types.js +0 -0
  25. package/dist/UserButton.d.ts +38 -0
  26. package/dist/UserButton.d.ts.map +1 -0
  27. package/dist/UserButton.js +68 -0
  28. package/dist/UserButton.js.map +1 -0
  29. package/dist/UserProfileCustomPages.d.ts +81 -0
  30. package/dist/UserProfileCustomPages.d.ts.map +1 -0
  31. package/dist/UserProfileCustomPages.js +140 -0
  32. package/dist/UserProfileCustomPages.js.map +1 -0
  33. package/dist/UserProfileView.d.ts +76 -0
  34. package/dist/UserProfileView.d.ts.map +1 -0
  35. package/dist/UserProfileView.js +101 -0
  36. package/dist/UserProfileView.js.map +1 -0
  37. package/dist/_virtual/_rolldown/runtime.js +29 -0
  38. package/dist/index.d.ts +41 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +37 -0
  41. package/dist/specs/NativeClerkAuthView.android.d.ts +15 -0
  42. package/dist/specs/NativeClerkAuthView.android.d.ts.map +1 -0
  43. package/dist/specs/NativeClerkAuthView.android.js +8 -0
  44. package/dist/specs/NativeClerkAuthView.android.js.map +1 -0
  45. package/dist/specs/NativeClerkAuthView.d.ts +15 -0
  46. package/dist/specs/NativeClerkAuthView.d.ts.map +1 -0
  47. package/dist/specs/NativeClerkAuthView.js +9 -0
  48. package/dist/specs/NativeClerkAuthView.js.map +1 -0
  49. package/dist/specs/NativeClerkModule.d.ts +4 -0
  50. package/dist/specs/NativeClerkModule.d.ts.map +1 -0
  51. package/dist/specs/NativeClerkModule.js +8 -0
  52. package/dist/specs/NativeClerkModule.js.map +1 -0
  53. package/dist/specs/NativeClerkModule.types.d.ts +11 -0
  54. package/dist/specs/NativeClerkModule.types.d.ts.map +1 -0
  55. package/dist/specs/NativeClerkModule.types.js +0 -0
  56. package/dist/specs/NativeClerkModule.web.d.ts +4 -0
  57. package/dist/specs/NativeClerkModule.web.d.ts.map +1 -0
  58. package/dist/specs/NativeClerkModule.web.js +7 -0
  59. package/dist/specs/NativeClerkModule.web.js.map +1 -0
  60. package/dist/specs/NativeClerkUserButtonView.android.d.ts +6 -0
  61. package/dist/specs/NativeClerkUserButtonView.android.d.ts.map +1 -0
  62. package/dist/specs/NativeClerkUserButtonView.android.js +8 -0
  63. package/dist/specs/NativeClerkUserButtonView.android.js.map +1 -0
  64. package/dist/specs/NativeClerkUserButtonView.d.ts +6 -0
  65. package/dist/specs/NativeClerkUserButtonView.d.ts.map +1 -0
  66. package/dist/specs/NativeClerkUserButtonView.js +9 -0
  67. package/dist/specs/NativeClerkUserButtonView.js.map +1 -0
  68. package/dist/specs/NativeClerkUserProfileView.android.d.ts +13 -0
  69. package/dist/specs/NativeClerkUserProfileView.android.d.ts.map +1 -0
  70. package/dist/specs/NativeClerkUserProfileView.android.js +8 -0
  71. package/dist/specs/NativeClerkUserProfileView.android.js.map +1 -0
  72. package/dist/specs/NativeClerkUserProfileView.d.ts +13 -0
  73. package/dist/specs/NativeClerkUserProfileView.d.ts.map +1 -0
  74. package/dist/specs/NativeClerkUserProfileView.js +9 -0
  75. package/dist/specs/NativeClerkUserProfileView.js.map +1 -0
  76. package/dist/useAuthViewState.d.ts +12 -0
  77. package/dist/useAuthViewState.d.ts.map +1 -0
  78. package/dist/useAuthViewState.js +76 -0
  79. package/dist/useAuthViewState.js.map +1 -0
  80. package/dist/utils/native-module.d.ts +3 -0
  81. package/dist/utils/native-module.d.ts.map +1 -0
  82. package/dist/utils/native-module.js +14 -0
  83. package/dist/utils/native-module.js.map +1 -0
  84. package/expo-module.config.json +15 -0
  85. package/ios/ClerkAppDelegateSubscriber.swift +16 -0
  86. package/ios/ClerkAuthNativeView.swift +157 -0
  87. package/ios/ClerkExpo.podspec +67 -0
  88. package/ios/ClerkExpoModule.swift +315 -0
  89. package/ios/ClerkNativeBridge.swift +1575 -0
  90. package/ios/ClerkNativeViewHost.swift +204 -0
  91. package/ios/ClerkUserButtonNativeView.swift +34 -0
  92. package/ios/ClerkUserProfileNativeView.swift +92 -0
  93. package/ios/Tests/ClerkAuthNativeViewPaperTests.swift +30 -0
  94. package/ios/Tests/ClerkNativeBridgeTests.swift +185 -0
  95. package/ios/Tests/ClerkNativeViewHostTests.swift +66 -0
  96. package/ios/Tests/ClerkUserProfileCustomPageStateTests.swift +311 -0
  97. package/package.json +79 -3
  98. package/react-native.config.js +8 -0
  99. package/src/specs/NativeClerkAuthView.android.ts +15 -0
  100. package/src/specs/NativeClerkAuthView.ts +19 -0
  101. package/src/specs/NativeClerkModule.ts +6 -0
  102. package/src/specs/NativeClerkModule.types.ts +9 -0
  103. package/src/specs/NativeClerkModule.web.ts +3 -0
  104. package/src/specs/NativeClerkUserButtonView.android.ts +9 -0
  105. package/src/specs/NativeClerkUserButtonView.ts +11 -0
  106. package/src/specs/NativeClerkUserProfileView.android.ts +13 -0
  107. package/src/specs/NativeClerkUserProfileView.ts +17 -0
  108. 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
+ }
@@ -0,0 +1,5 @@
1
+ import type { ConfigPlugin } from '@expo/config-plugins';
2
+
3
+ declare const withClerkExpoNativeComponents: ConfigPlugin<{ keychainService?: string; theme?: string } | void>;
4
+
5
+ export = withClerkExpoNativeComponents;