@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,311 @@
1
+ @testable import ClerkExpo
2
+ import SwiftUI
3
+ import XCTest
4
+
5
+ final class ClerkUserProfileCustomPageStateTests: XCTestCase {
6
+ @MainActor
7
+ func testNavigationPathCanBeRestoredIntoANewLiveHost() {
8
+ let state = ClerkUserProfileCustomPageState()
9
+ let navigationPath = makeNavigationPath("billing")
10
+
11
+ state.navigationPathDidChange(navigationPath)
12
+
13
+ XCTAssertEqual(state.navigationPathForRestoration().count, 1)
14
+ }
15
+
16
+ @MainActor
17
+ func testKeepingTheSameNavigationDepthDoesNotDismissThePage() {
18
+ let state = ClerkUserProfileCustomPageState()
19
+ var events: [String] = []
20
+ state.setPageEventHandler { type, path in
21
+ events.append("\(type):\(path)")
22
+ }
23
+
24
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
25
+ state.navigationDepthDidChange(1)
26
+
27
+ XCTAssertEqual(events, ["presented:billing"])
28
+ }
29
+
30
+ @MainActor
31
+ func testIncreasingTheNavigationDepthDoesNotDismissThePage() {
32
+ let state = ClerkUserProfileCustomPageState()
33
+ var events: [String] = []
34
+ state.setPageEventHandler { type, path in
35
+ events.append("\(type):\(path)")
36
+ }
37
+
38
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
39
+ state.navigationDepthDidChange(2)
40
+
41
+ XCTAssertEqual(events, ["presented:billing"])
42
+ }
43
+
44
+ @MainActor
45
+ func testDecreasingTheNavigationDepthDismissesThePageOnce() {
46
+ let state = ClerkUserProfileCustomPageState()
47
+ var events: [String] = []
48
+ state.setPageEventHandler { type, path in
49
+ events.append("\(type):\(path)")
50
+ }
51
+
52
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
53
+ state.navigationDepthDidChange(0)
54
+ state.navigationDepthDidChange(0)
55
+
56
+ XCTAssertEqual(events, ["presented:billing", "dismissed:billing"])
57
+ }
58
+
59
+ @MainActor
60
+ func testInitialUserObservationKeepsTheRetainedPath() {
61
+ let state = ClerkUserProfileCustomPageState()
62
+ state.navigationPathDidChange(makeNavigationPath("billing"))
63
+
64
+ state.userDidChange(to: "user_1")
65
+
66
+ XCTAssertEqual(state.navigationPathForRestoration().count, 1)
67
+ }
68
+
69
+ @MainActor
70
+ func testUserIdentityChangeInvalidatesNavigationAndDismissesThePageOnce() {
71
+ let state = ClerkUserProfileCustomPageState()
72
+ var events: [String] = []
73
+ state.setPageEventHandler { type, path in
74
+ events.append("\(type):\(path)")
75
+ }
76
+ state.userDidChange(to: "user_1")
77
+ state.navigationPathDidChange(makeNavigationPath("billing"))
78
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
79
+
80
+ state.userDidChange(to: nil)
81
+ state.userDidChange(to: nil)
82
+
83
+ XCTAssertEqual(state.navigationPathForRestoration().count, 0)
84
+ XCTAssertEqual(events, ["presented:billing", "dismissed:billing"])
85
+ }
86
+
87
+ @MainActor
88
+ func testKeepingTheSameUserPreservesNavigation() {
89
+ let state = ClerkUserProfileCustomPageState()
90
+ state.userDidChange(to: "user_1")
91
+ state.navigationPathDidChange(makeNavigationPath("billing"))
92
+
93
+ state.userDidChange(to: "user_1")
94
+
95
+ XCTAssertEqual(state.navigationPathForRestoration().count, 1)
96
+ }
97
+
98
+ @MainActor
99
+ func testUserIdentityChangeInvalidatesTheEntireUserButtonStack() {
100
+ let state = ClerkUserProfileCustomPageState()
101
+ var events: [String] = []
102
+ state.setPageEventHandler { type, path in
103
+ events.append("\(type):\(path)")
104
+ }
105
+ state.userDidChange(to: "user_1")
106
+ state.pageDidPresent(path: "billing")
107
+ state.navigate(action: "push", routeKey: "preferences")
108
+ state.pageDidDismiss(path: "billing")
109
+ state.pageDidPresent(path: "preferences")
110
+ events.removeAll()
111
+
112
+ state.userDidChange(to: "user_2")
113
+ state.userDidChange(to: "user_2")
114
+
115
+ XCTAssertEqual(events, ["dismissed:preferences", "dismissed:billing"])
116
+ }
117
+
118
+ @MainActor
119
+ func testRemovingThePresentedCustomPageInvalidatesNavigation() {
120
+ let state = ClerkUserProfileCustomPageState()
121
+ var events: [String] = []
122
+ state.setPageEventHandler { type, path in
123
+ events.append("\(type):\(path)")
124
+ }
125
+ state.navigationPathDidChange(makeNavigationPath("billing"))
126
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
127
+
128
+ state.reconcileCustomPagePaths(["preferences"])
129
+
130
+ XCTAssertEqual(state.navigationPathForRestoration().count, 0)
131
+ XCTAssertEqual(events, ["presented:billing", "dismissed:billing"])
132
+ }
133
+
134
+ @MainActor
135
+ func testRemovingAnEarlierCustomPageInvalidatesTheRetainedStack() {
136
+ let state = ClerkUserProfileCustomPageState()
137
+ var events: [String] = []
138
+ state.setPageEventHandler { type, path in
139
+ events.append("\(type):\(path)")
140
+ }
141
+ state.navigationPathDidChange(makeNavigationPath("billing", "preferences"))
142
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
143
+ state.pageDidPresent(path: "preferences", navigationDepth: 2)
144
+
145
+ state.reconcileCustomPagePaths(["preferences"])
146
+
147
+ XCTAssertEqual(state.navigationPathForRestoration().count, 0)
148
+ XCTAssertEqual(
149
+ events,
150
+ ["presented:billing", "presented:preferences", "dismissed:preferences", "dismissed:billing"]
151
+ )
152
+ }
153
+
154
+ @MainActor
155
+ func testRemovingAnEarlierUserButtonPageInvalidatesTheRetainedStack() {
156
+ let state = ClerkUserProfileCustomPageState()
157
+ var events: [String] = []
158
+ state.setPageEventHandler { type, path in
159
+ events.append("\(type):\(path)")
160
+ }
161
+ state.pageDidPresent(path: "billing")
162
+ state.navigate(action: "push", routeKey: "preferences")
163
+ state.pageDidDismiss(path: "billing")
164
+ state.pageDidPresent(path: "preferences")
165
+ events.removeAll()
166
+
167
+ state.reconcileCustomPagePaths(["preferences"])
168
+
169
+ XCTAssertEqual(events, ["dismissed:preferences", "dismissed:billing"])
170
+ }
171
+
172
+ @MainActor
173
+ func testClosingUserButtonProfileDismissesCoveredPages() {
174
+ var pendingResets: [ClerkUserProfileCustomPageState.InactiveResetAction] = []
175
+ let state = ClerkUserProfileCustomPageState { pendingResets.append($0) }
176
+ var events: [String] = []
177
+ state.setPageEventHandler { type, path in
178
+ events.append("\(type):\(path)")
179
+ }
180
+ state.pageDidPresent(path: "billing")
181
+ state.navigate(action: "push", routeKey: "preferences")
182
+ state.pageDidDismiss(path: "billing")
183
+ state.pageDidPresent(path: "preferences")
184
+ events.removeAll()
185
+
186
+ state.pageDidDismiss(path: "preferences")
187
+ XCTAssertEqual(pendingResets.count, 1)
188
+ pendingResets.removeFirst()()
189
+
190
+ XCTAssertEqual(events, ["dismissed:preferences", "dismissed:billing"])
191
+ }
192
+
193
+ @MainActor
194
+ func testReturningToCoveredUserButtonPageKeepsItRetained() {
195
+ var pendingResets: [ClerkUserProfileCustomPageState.InactiveResetAction] = []
196
+ let state = ClerkUserProfileCustomPageState { pendingResets.append($0) }
197
+ var events: [String] = []
198
+ state.setPageEventHandler { type, path in
199
+ events.append("\(type):\(path)")
200
+ }
201
+ state.pageDidPresent(path: "billing")
202
+ state.navigate(action: "push", routeKey: "preferences")
203
+ state.pageDidDismiss(path: "billing")
204
+ state.pageDidPresent(path: "preferences")
205
+ events.removeAll()
206
+
207
+ state.pageDidDismiss(path: "preferences")
208
+ state.pageDidPresent(path: "billing")
209
+ XCTAssertEqual(pendingResets.count, 1)
210
+ pendingResets.removeFirst()()
211
+ state.reconcileCustomPagePaths(["billing"])
212
+
213
+ XCTAssertEqual(events, ["dismissed:preferences", "presented:billing"])
214
+ }
215
+
216
+ @MainActor
217
+ func testPushingAUserButtonPathAlreadyInTheStackDoesNotCollapseLaterPaths() {
218
+ let state = ClerkUserProfileCustomPageState()
219
+ var events: [String] = []
220
+ state.setPageEventHandler { type, path in
221
+ events.append("\(type):\(path)")
222
+ }
223
+ state.pageDidPresent(path: "billing")
224
+ state.navigate(action: "push", routeKey: "preferences")
225
+ state.pageDidDismiss(path: "billing")
226
+ state.pageDidPresent(path: "preferences")
227
+ events.removeAll()
228
+
229
+ state.navigate(action: "push", routeKey: "billing")
230
+ state.reconcileCustomPagePaths(["billing", "preferences"])
231
+
232
+ XCTAssertEqual(events, [])
233
+ }
234
+
235
+ @MainActor
236
+ func testPoppingMultipleCustomPagesDismissesEachRetainedPage() {
237
+ let state = ClerkUserProfileCustomPageState()
238
+ var events: [String] = []
239
+ state.setPageEventHandler { type, path in
240
+ events.append("\(type):\(path)")
241
+ }
242
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
243
+ state.pageDidPresent(path: "preferences", navigationDepth: 2)
244
+
245
+ state.navigationDepthDidChange(0)
246
+
247
+ XCTAssertEqual(
248
+ events,
249
+ ["presented:billing", "presented:preferences", "dismissed:preferences", "dismissed:billing"]
250
+ )
251
+ }
252
+
253
+ @MainActor
254
+ func testPoppedCustomPagesAreNotIncludedInLaterReconciliation() {
255
+ let state = ClerkUserProfileCustomPageState()
256
+ state.navigationPathDidChange(makeNavigationPath("billing", "preferences"))
257
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
258
+ state.pageDidPresent(path: "preferences", navigationDepth: 2)
259
+
260
+ state.navigationPathDidChange(makeNavigationPath("billing"))
261
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
262
+ state.reconcileCustomPagePaths(["billing"])
263
+
264
+ XCTAssertEqual(state.navigationPathForRestoration().count, 1)
265
+ }
266
+
267
+ @MainActor
268
+ func testKeepingThePresentedCustomPagePreservesNavigation() {
269
+ let state = ClerkUserProfileCustomPageState()
270
+ state.navigationPathDidChange(makeNavigationPath("billing"))
271
+ state.pageDidPresent(path: "billing", navigationDepth: 1)
272
+
273
+ state.reconcileCustomPagePaths(["billing", "preferences"])
274
+
275
+ XCTAssertEqual(state.navigationPathForRestoration().count, 1)
276
+ }
277
+
278
+ func testCustomPageLabelComesFromTheMatchingRow() {
279
+ let rows = parseUserProfileCustomPages(
280
+ """
281
+ [{"path":"billing","label":"Billing details","icon":"billing","placement":{"type":"sectionEnd","section":"profile"}}]
282
+ """,
283
+ pageCount: 1
284
+ )
285
+
286
+ XCTAssertEqual(userProfileCustomPageLabel(for: "billing", rows: rows), "Billing details")
287
+ }
288
+
289
+ func testPushOnlyDestinationsKeepTheirTitleWithoutCreatingRows() {
290
+ let pages = parseUserProfileCustomPages(
291
+ """
292
+ [
293
+ {"path":"billing","label":"Billing","icon":"billing","placement":{"type":"sectionEnd","section":"profile"}},
294
+ {"path":"invoice-details","label":"Invoice details","icon":"settings","placement":{"type":"sectionEnd","section":"profile"},"showAsRow":false}
295
+ ]
296
+ """,
297
+ pageCount: 2
298
+ )
299
+
300
+ XCTAssertEqual(pages.filter(\.shouldShowAsRow).map(\.path), ["billing"])
301
+ XCTAssertEqual(userProfileCustomPageLabel(for: "invoice-details", rows: pages), "Invoice details")
302
+ }
303
+
304
+ private func makeNavigationPath(_ routes: String...) -> NavigationPath {
305
+ var navigationPath = NavigationPath()
306
+ for route in routes {
307
+ navigationPath.append(route)
308
+ }
309
+ return navigationPath
310
+ }
311
+ }
package/package.json CHANGED
@@ -1,6 +1,82 @@
1
1
  {
2
2
  "name": "@clerk/expo-native-components",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "description": "Native Clerk components and native SDK integration for Expo, powered by clerk-ios and clerk-android",
5
+ "keywords": [
6
+ "react",
7
+ "react-native",
8
+ "expo",
9
+ "clerk",
10
+ "auth",
11
+ "authentication",
12
+ "native"
13
+ ],
14
+ "homepage": "https://clerk.com/",
15
+ "bugs": {
16
+ "url": "https://github.com/clerk/javascript/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/clerk/javascript.git",
21
+ "directory": "packages/expo-native-components"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Clerk",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./app.plugin.js": "./app.plugin.js",
31
+ "./package.json": "./package.json"
32
+ },
33
+ "main": "./dist/index.js",
34
+ "source": "./src/index.ts",
35
+ "types": "./dist/index.d.ts",
36
+ "files": [
37
+ "dist",
38
+ "android",
39
+ "ios",
40
+ "src/specs",
41
+ "expo-module.config.json",
42
+ "react-native.config.js",
43
+ "app.plugin.js",
44
+ "app.plugin.d.ts"
45
+ ],
46
+ "dependencies": {
47
+ "tslib": "2.8.1",
48
+ "@clerk/react": "^6.17.7"
49
+ },
50
+ "devDependencies": {
51
+ "@expo/config-plugins": "^54.0.4",
52
+ "react-native": "^0.86.0"
53
+ },
54
+ "peerDependencies": {
55
+ "expo": ">=54 <58",
56
+ "react": "^18.0.0 || ^19.0.0",
57
+ "react-native": ">=0.75"
58
+ },
59
+ "engines": {
60
+ "node": ">=20.9.0"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ },
65
+ "codegenConfig": {
66
+ "name": "ClerkExpoSpec",
67
+ "type": "all",
68
+ "jsSrcsDir": "src/specs"
69
+ },
70
+ "scripts": {
71
+ "build": "tsdown",
72
+ "build:declarations": "tsc -p tsconfig.declarations.json",
73
+ "clean": "rimraf ./dist",
74
+ "dev": "tsdown --watch",
75
+ "dev:pub": "pnpm dev -- --env.publish",
76
+ "format": "node ../../scripts/format-package.mjs",
77
+ "format:check": "node ../../scripts/format-package.mjs --check",
78
+ "lint": "eslint src",
79
+ "test": "vitest run",
80
+ "test:watch": "vitest watch"
81
+ }
6
82
  }
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ ios: {},
5
+ android: null,
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,15 @@
1
+ import { requireNativeView } from 'expo';
2
+ import type { NativeSyntheticEvent, ViewProps } from 'react-native';
3
+
4
+ type AuthEvent = Readonly<{ type: string }>;
5
+
6
+ interface NativeProps extends ViewProps {
7
+ mode?: string;
8
+ isDismissible?: boolean;
9
+ logoMaxHeight?: number;
10
+ hostBackButton?: boolean;
11
+ onAuthEvent?: (event: NativeSyntheticEvent<AuthEvent>) => void;
12
+ onHostBack?: (event: NativeSyntheticEvent<object>) => void;
13
+ }
14
+
15
+ export default requireNativeView<NativeProps>('ClerkAuthView');
@@ -0,0 +1,19 @@
1
+ import { requireNativeView } from 'expo';
2
+ import type { NativeSyntheticEvent, ViewProps } from 'react-native';
3
+ import { Platform } from 'react-native';
4
+
5
+ type AuthEvent = Readonly<{ type: string }>;
6
+
7
+ interface NativeProps extends ViewProps {
8
+ mode?: string;
9
+ isDismissible?: boolean;
10
+ logoMaxHeight?: number;
11
+ hostBackButton?: boolean;
12
+ onAuthEvent?: (event: NativeSyntheticEvent<AuthEvent>) => void;
13
+ onHostBack?: (event: NativeSyntheticEvent<object>) => void;
14
+ }
15
+
16
+ const NativeClerkAuthView =
17
+ Platform.OS === 'ios' || Platform.OS === 'android' ? requireNativeView<NativeProps>('ClerkAuthView') : null;
18
+
19
+ export default NativeClerkAuthView;
@@ -0,0 +1,6 @@
1
+ import { requireOptionalNativeModule } from 'expo';
2
+
3
+ import type { Spec } from './NativeClerkModule.types';
4
+
5
+ // Optional so it resolves to null in Expo Go instead of throwing at import time.
6
+ export default requireOptionalNativeModule<Spec>('ClerkExpo');
@@ -0,0 +1,9 @@
1
+ export type NativeAuthFlowState = {
2
+ isLoaded: boolean;
3
+ isAuthFlowComplete: boolean;
4
+ };
5
+
6
+ export interface Spec {
7
+ addListener?(eventName: string, listener?: (...args: unknown[]) => void): { remove: () => void };
8
+ getAuthFlowState?(): Promise<NativeAuthFlowState>;
9
+ }
@@ -0,0 +1,3 @@
1
+ import type { Spec } from './NativeClerkModule.types';
2
+
3
+ export default null as Spec | null;
@@ -0,0 +1,9 @@
1
+ import { requireNativeView } from 'expo';
2
+ import type { ViewProps } from 'react-native';
3
+
4
+ // Codegen requires an interface declaration in the iOS spec; keep the Android
5
+ // view prop shape identical for the shared React wrapper.
6
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
7
+ interface NativeProps extends ViewProps {}
8
+
9
+ export default requireNativeView<NativeProps>('ClerkUserButtonView');
@@ -0,0 +1,11 @@
1
+ import { requireNativeView } from 'expo';
2
+ import type { ViewProps } from 'react-native';
3
+ import { Platform } from 'react-native';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
6
+ interface NativeProps extends ViewProps {}
7
+
8
+ const NativeClerkUserButtonView =
9
+ Platform.OS === 'ios' || Platform.OS === 'android' ? requireNativeView<NativeProps>('ClerkUserButtonView') : null;
10
+
11
+ export default NativeClerkUserButtonView;
@@ -0,0 +1,13 @@
1
+ import { requireNativeView } from 'expo';
2
+ import type { NativeSyntheticEvent, ViewProps } from 'react-native';
3
+
4
+ type ProfileEvent = Readonly<{ type: string }>;
5
+
6
+ interface NativeProps extends ViewProps {
7
+ isDismissible?: boolean;
8
+ hostBackButton?: boolean;
9
+ onProfileEvent?: (event: NativeSyntheticEvent<ProfileEvent>) => void;
10
+ onHostBack?: (event: NativeSyntheticEvent<object>) => void;
11
+ }
12
+
13
+ export default requireNativeView<NativeProps>('ClerkUserProfileView');
@@ -0,0 +1,17 @@
1
+ import { requireNativeView } from 'expo';
2
+ import type { NativeSyntheticEvent, ViewProps } from 'react-native';
3
+ import { Platform } from 'react-native';
4
+
5
+ type ProfileEvent = Readonly<{ type: string }>;
6
+
7
+ interface NativeProps extends ViewProps {
8
+ isDismissible?: boolean;
9
+ hostBackButton?: boolean;
10
+ onProfileEvent?: (event: NativeSyntheticEvent<ProfileEvent>) => void;
11
+ onHostBack?: (event: NativeSyntheticEvent<object>) => void;
12
+ }
13
+
14
+ const NativeClerkUserProfileView =
15
+ Platform.OS === 'ios' || Platform.OS === 'android' ? requireNativeView<NativeProps>('ClerkUserProfileView') : null;
16
+
17
+ export default NativeClerkUserProfileView;
@@ -0,0 +1,43 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ requireNativeView: vi.fn(() => {
5
+ throw new Error('requireNativeView should not be called on web');
6
+ }),
7
+ }));
8
+
9
+ vi.mock('expo', () => ({
10
+ requireNativeView: mocks.requireNativeView,
11
+ }));
12
+
13
+ vi.mock('react-native', () => ({
14
+ Platform: {
15
+ OS: 'web',
16
+ },
17
+ }));
18
+
19
+ async function importNativeViews() {
20
+ const [authView, userProfileView, userButtonView] = await Promise.all([
21
+ import('../NativeClerkAuthView'),
22
+ import('../NativeClerkUserProfileView'),
23
+ import('../NativeClerkUserButtonView'),
24
+ ]);
25
+
26
+ return [authView.default, userProfileView.default, userButtonView.default] as const;
27
+ }
28
+
29
+ describe('native view specs on web', () => {
30
+ beforeEach(() => {
31
+ vi.resetModules();
32
+ vi.clearAllMocks();
33
+ });
34
+
35
+ test('do not require native views at import time', async () => {
36
+ const [authView, userProfileView, userButtonView] = await importNativeViews();
37
+
38
+ expect(authView).toBeNull();
39
+ expect(userProfileView).toBeNull();
40
+ expect(userButtonView).toBeNull();
41
+ expect(mocks.requireNativeView).not.toHaveBeenCalled();
42
+ });
43
+ });