@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,204 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public class ClerkNativeViewHost: ExpoView {
|
|
5
|
+
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
|
|
6
|
+
private var hasInitialized: Bool = false
|
|
7
|
+
private var configuredObserver: NSObjectProtocol?
|
|
8
|
+
|
|
9
|
+
public required init(appContext: AppContext? = nil) {
|
|
10
|
+
super.init(appContext: appContext)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@available(*, unavailable)
|
|
14
|
+
public required init?(coder: NSCoder) {
|
|
15
|
+
fatalError("init(coder:) has not been implemented")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
deinit {
|
|
19
|
+
removeConfiguredObserver()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override public func didMoveToWindow() {
|
|
23
|
+
super.didMoveToWindow()
|
|
24
|
+
|
|
25
|
+
guard window != nil else {
|
|
26
|
+
if hasInitialized {
|
|
27
|
+
hostedViewDidDetachFromWindow()
|
|
28
|
+
}
|
|
29
|
+
removeConfiguredObserver()
|
|
30
|
+
hostingCoordinator.detach()
|
|
31
|
+
hasInitialized = false
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
guard !hasInitialized else { return }
|
|
36
|
+
hasInitialized = true
|
|
37
|
+
addConfiguredObserver()
|
|
38
|
+
hostedViewDidAttachToWindow()
|
|
39
|
+
updateHostedView()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override public func layoutSubviews() {
|
|
43
|
+
super.layoutSubviews()
|
|
44
|
+
hostingCoordinator.layout()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func setNeedsHostedViewUpdate() {
|
|
48
|
+
guard hasInitialized else { return }
|
|
49
|
+
updateHostedView()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func makeHostedController() -> UIViewController? {
|
|
53
|
+
nil
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Subclasses can observe attach/detach without making this host know about their RN event props.
|
|
57
|
+
func hostedViewDidAttachToWindow() {}
|
|
58
|
+
|
|
59
|
+
func hostedViewDidDetachFromWindow() {}
|
|
60
|
+
|
|
61
|
+
private func addConfiguredObserver() {
|
|
62
|
+
guard configuredObserver == nil else { return }
|
|
63
|
+
|
|
64
|
+
configuredObserver = NotificationCenter.default.addObserver(
|
|
65
|
+
forName: .clerkNativeSDKDidConfigure,
|
|
66
|
+
object: nil,
|
|
67
|
+
queue: .main
|
|
68
|
+
) { [weak self] _ in
|
|
69
|
+
guard let self, !hostingCoordinator.hasAttachedController else { return }
|
|
70
|
+
setNeedsHostedViewUpdate()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private func removeConfiguredObserver() {
|
|
75
|
+
guard let configuredObserver else { return }
|
|
76
|
+
NotificationCenter.default.removeObserver(configuredObserver)
|
|
77
|
+
self.configuredObserver = nil
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private func updateHostedView() {
|
|
81
|
+
guard let controller = makeHostedController() else { return }
|
|
82
|
+
hostingCoordinator.attach(controller)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public class ClerkUserProfileCustomPageHost: ClerkNativeViewHost {
|
|
87
|
+
private var currentCustomPages: String = "[]"
|
|
88
|
+
let customPageState = ClerkUserProfileCustomPageState()
|
|
89
|
+
|
|
90
|
+
// Expo resolves event dispatchers through `Mirror(reflecting:).children`, which skips
|
|
91
|
+
// inherited properties, so subclasses must own the dispatcher and surface it here.
|
|
92
|
+
var customPageEventDispatcher: EventDispatcher? { nil }
|
|
93
|
+
|
|
94
|
+
public required init(appContext: AppContext? = nil) {
|
|
95
|
+
super.init(appContext: appContext)
|
|
96
|
+
customPageState.setPageEventHandler { [weak self] type, path in
|
|
97
|
+
self?.customPageEventDispatcher?(["type": type, "path": path])
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func setCustomPages(_ customPages: String?) {
|
|
102
|
+
let newCustomPages = customPages ?? "[]"
|
|
103
|
+
guard newCustomPages != currentCustomPages else { return }
|
|
104
|
+
let validPaths = Set(decodeUserProfileCustomPages(newCustomPages).map(\.path))
|
|
105
|
+
customPageState.reconcileCustomPagePaths(validPaths)
|
|
106
|
+
currentCustomPages = newCustomPages
|
|
107
|
+
setNeedsHostedViewUpdate()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func navigateCustomPage(action: String, routeKey: String?) {
|
|
111
|
+
customPageState.navigate(action: action, routeKey: routeKey)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func customRows() -> [ClerkUserProfileCustomRowConfig] {
|
|
115
|
+
parseUserProfileCustomPages(currentCustomPages, pageCount: customPageState.views.count)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#if RCT_NEW_ARCH_ENABLED
|
|
119
|
+
override public func mountChildComponentView(_ childComponentView: UIView, index: Int) {
|
|
120
|
+
customPageState.insertView(childComponentView, at: index)
|
|
121
|
+
setNeedsHostedViewUpdate()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override public func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
|
|
125
|
+
customPageState.removeView(childComponentView)
|
|
126
|
+
setNeedsHostedViewUpdate()
|
|
127
|
+
}
|
|
128
|
+
#else
|
|
129
|
+
override public func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
|
|
130
|
+
super.insertReactSubview(subview, at: atIndex)
|
|
131
|
+
customPageState.insertView(subview, at: atIndex)
|
|
132
|
+
setNeedsHostedViewUpdate()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
override public func removeReactSubview(_ subview: UIView!) {
|
|
136
|
+
customPageState.removeView(subview)
|
|
137
|
+
super.removeReactSubview(subview)
|
|
138
|
+
setNeedsHostedViewUpdate()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override public func didUpdateReactSubviews() {}
|
|
142
|
+
#endif
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private final class ClerkNativeHostingCoordinator {
|
|
146
|
+
private weak var containerView: UIView?
|
|
147
|
+
private var hostingController: UIViewController?
|
|
148
|
+
|
|
149
|
+
var hasAttachedController: Bool {
|
|
150
|
+
hostingController != nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
init(containerView: UIView) {
|
|
154
|
+
self.containerView = containerView
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func attach(_ controller: UIViewController) {
|
|
158
|
+
detach()
|
|
159
|
+
|
|
160
|
+
guard let containerView else { return }
|
|
161
|
+
|
|
162
|
+
controller.view.frame = containerView.bounds
|
|
163
|
+
controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
164
|
+
|
|
165
|
+
if let parentVC = findViewController(from: containerView) {
|
|
166
|
+
parentVC.addChild(controller)
|
|
167
|
+
containerView.addSubview(controller.view)
|
|
168
|
+
controller.didMove(toParent: parentVC)
|
|
169
|
+
} else {
|
|
170
|
+
containerView.addSubview(controller.view)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
hostingController = controller
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func detach() {
|
|
177
|
+
guard let controller = hostingController else { return }
|
|
178
|
+
|
|
179
|
+
if controller.parent != nil {
|
|
180
|
+
controller.willMove(toParent: nil)
|
|
181
|
+
}
|
|
182
|
+
controller.view.removeFromSuperview()
|
|
183
|
+
if controller.parent != nil {
|
|
184
|
+
controller.removeFromParent()
|
|
185
|
+
}
|
|
186
|
+
hostingController = nil
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func layout() {
|
|
190
|
+
guard let containerView else { return }
|
|
191
|
+
hostingController?.view.frame = containerView.bounds
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private func findViewController(from view: UIView) -> UIViewController? {
|
|
195
|
+
var responder: UIResponder? = view
|
|
196
|
+
while let nextResponder = responder?.next {
|
|
197
|
+
if let vc = nextResponder as? UIViewController {
|
|
198
|
+
return vc
|
|
199
|
+
}
|
|
200
|
+
responder = nextResponder
|
|
201
|
+
}
|
|
202
|
+
return nil
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public class ClerkUserButtonNativeView: ClerkUserProfileCustomPageHost {
|
|
5
|
+
let onCustomPageEvent = EventDispatcher()
|
|
6
|
+
|
|
7
|
+
override var customPageEventDispatcher: EventDispatcher? { onCustomPageEvent }
|
|
8
|
+
|
|
9
|
+
override func makeHostedController() -> UIViewController? {
|
|
10
|
+
return ClerkNativeBridge.shared.makeUserButtonViewController(
|
|
11
|
+
customRows: customRows(),
|
|
12
|
+
customPageState: customPageState
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public class ClerkUserButtonViewModule: Module {
|
|
18
|
+
public func definition() -> ModuleDefinition {
|
|
19
|
+
Name("ClerkUserButtonView")
|
|
20
|
+
|
|
21
|
+
View(ClerkUserButtonNativeView.self) {
|
|
22
|
+
Events("onCustomPageEvent")
|
|
23
|
+
|
|
24
|
+
Prop("customPages") { (view: ClerkUserButtonNativeView, customPages: String?) in
|
|
25
|
+
view.setCustomPages(customPages)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
AsyncFunction("navigateCustomPage") {
|
|
29
|
+
(view: ClerkUserButtonNativeView, action: String, routeKey: String?) in
|
|
30
|
+
view.navigateCustomPage(action: action, routeKey: routeKey)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public class ClerkUserProfileNativeView: ClerkUserProfileCustomPageHost {
|
|
5
|
+
private var currentDismissible: Bool = true
|
|
6
|
+
private var currentHostBackButton: Bool = false
|
|
7
|
+
private var didSendDismiss = false
|
|
8
|
+
|
|
9
|
+
let onProfileEvent = EventDispatcher()
|
|
10
|
+
let onHostBack = EventDispatcher()
|
|
11
|
+
let onCustomPageEvent = EventDispatcher()
|
|
12
|
+
|
|
13
|
+
override var customPageEventDispatcher: EventDispatcher? { onCustomPageEvent }
|
|
14
|
+
|
|
15
|
+
func setDismissible(_ isDismissible: Bool?) {
|
|
16
|
+
let newDismissible = isDismissible ?? true
|
|
17
|
+
guard newDismissible != currentDismissible else { return }
|
|
18
|
+
currentDismissible = newDismissible
|
|
19
|
+
setNeedsHostedViewUpdate()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func setHostBackButton(_ hostBackButton: Bool?) {
|
|
23
|
+
let newHostBackButton = hostBackButton ?? false
|
|
24
|
+
guard newHostBackButton != currentHostBackButton else { return }
|
|
25
|
+
currentHostBackButton = newHostBackButton
|
|
26
|
+
setNeedsHostedViewUpdate()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private func sendProfileEvent(type: ClerkNativeViewEvent) {
|
|
30
|
+
onProfileEvent(["type": type.rawValue])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private func sendDismissIfNeeded() {
|
|
34
|
+
// SwiftUI dismissals detach the hosted view without calling UIKit dismiss().
|
|
35
|
+
guard currentDismissible, !didSendDismiss else { return }
|
|
36
|
+
didSendDismiss = true
|
|
37
|
+
sendProfileEvent(type: .dismissed)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override func hostedViewDidAttachToWindow() {
|
|
41
|
+
didSendDismiss = false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override func hostedViewDidDetachFromWindow() {
|
|
45
|
+
sendDismissIfNeeded()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override func makeHostedController() -> UIViewController? {
|
|
49
|
+
let hostBackAction: (() -> Void)? = currentHostBackButton
|
|
50
|
+
? { [weak self] in self?.onHostBack([:]) }
|
|
51
|
+
: nil
|
|
52
|
+
|
|
53
|
+
return ClerkNativeBridge.shared.makeUserProfileViewController(
|
|
54
|
+
dismissible: currentDismissible,
|
|
55
|
+
customRows: customRows(),
|
|
56
|
+
customPageState: customPageState,
|
|
57
|
+
hostBackAction: hostBackAction,
|
|
58
|
+
onEvent: { [weak self] event, _ in
|
|
59
|
+
if event == .dismissed {
|
|
60
|
+
self?.sendDismissIfNeeded()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public class ClerkUserProfileViewModule: Module {
|
|
68
|
+
public func definition() -> ModuleDefinition {
|
|
69
|
+
Name("ClerkUserProfileView")
|
|
70
|
+
|
|
71
|
+
View(ClerkUserProfileNativeView.self) {
|
|
72
|
+
Events("onProfileEvent", "onCustomPageEvent", "onHostBack")
|
|
73
|
+
|
|
74
|
+
Prop("isDismissible") { (view: ClerkUserProfileNativeView, isDismissible: Bool?) in
|
|
75
|
+
view.setDismissible(isDismissible)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Prop("hostBackButton") { (view: ClerkUserProfileNativeView, hostBackButton: Bool?) in
|
|
79
|
+
view.setHostBackButton(hostBackButton)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Prop("customPages") { (view: ClerkUserProfileNativeView, customPages: String?) in
|
|
83
|
+
view.setCustomPages(customPages)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
AsyncFunction("navigateCustomPage") {
|
|
87
|
+
(view: ClerkUserProfileNativeView, action: String, routeKey: String?) in
|
|
88
|
+
view.navigateCustomPage(action: action, routeKey: routeKey)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#if !RCT_NEW_ARCH_ENABLED
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
import UIKit
|
|
4
|
+
import XCTest
|
|
5
|
+
import ClerkExpo
|
|
6
|
+
|
|
7
|
+
final class ClerkAuthNativeViewPaperTests: XCTestCase {
|
|
8
|
+
@MainActor
|
|
9
|
+
func testAddingAndRemovingLogoAfterMountKeepsReactChildOutOfHostHierarchy() {
|
|
10
|
+
let window = UIWindow(frame: UIScreen.main.bounds)
|
|
11
|
+
let viewController = UIViewController()
|
|
12
|
+
let authView = ClerkAuthNativeView(appContext: nil)
|
|
13
|
+
window.rootViewController = viewController
|
|
14
|
+
viewController.view.addSubview(authView)
|
|
15
|
+
window.makeKeyAndVisible()
|
|
16
|
+
|
|
17
|
+
let logoView = UIView(frame: CGRect(x: 0, y: 0, width: 120, height: 40))
|
|
18
|
+
authView.insertReactSubview(logoView, at: 0)
|
|
19
|
+
authView.didUpdateReactSubviews()
|
|
20
|
+
|
|
21
|
+
XCTAssertFalse(logoView.superview === authView)
|
|
22
|
+
|
|
23
|
+
authView.removeReactSubview(logoView)
|
|
24
|
+
authView.didUpdateReactSubviews()
|
|
25
|
+
|
|
26
|
+
XCTAssertNil(logoView.superview)
|
|
27
|
+
window.isHidden = true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
#endif
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
import ClerkKit
|
|
3
|
+
@testable import ClerkExpo
|
|
4
|
+
|
|
5
|
+
final class ClerkNativeBridgeTests: XCTestCase {
|
|
6
|
+
@MainActor
|
|
7
|
+
func testBiometricCredentialAvailabilityIsUnavailableBeforeConfiguration() async throws {
|
|
8
|
+
let availability = try await ClerkNativeBridge.shared.getBiometricCredentialAvailability(
|
|
9
|
+
id: nil,
|
|
10
|
+
identifierHint: nil
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
XCTAssertEqual(availability["isAvailable"] as? Bool, false)
|
|
14
|
+
XCTAssertEqual(availability["unavailableReason"] as? String, "environment_unavailable")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@MainActor
|
|
18
|
+
func testBiometricCredentialOperationsRejectBeforeConfiguration() async {
|
|
19
|
+
await assertEnvironmentUnavailable {
|
|
20
|
+
try await ClerkNativeBridge.shared.reverifyWithBiometrics(
|
|
21
|
+
sessionId: "sess_test", level: "multi_factor", reason: nil
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
await assertEnvironmentUnavailable {
|
|
25
|
+
try await ClerkNativeBridge.shared.listBiometricCredentials()
|
|
26
|
+
}
|
|
27
|
+
await assertEnvironmentUnavailable {
|
|
28
|
+
try await ClerkNativeBridge.shared.enrollBiometricCredential(
|
|
29
|
+
deviceName: nil,
|
|
30
|
+
identifierHint: nil,
|
|
31
|
+
reason: nil,
|
|
32
|
+
policy: "biometry_or_device_passcode"
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
await assertEnvironmentUnavailable {
|
|
36
|
+
try await ClerkNativeBridge.shared.revokeBiometricCredential(id: "td_test")
|
|
37
|
+
}
|
|
38
|
+
await assertEnvironmentUnavailable {
|
|
39
|
+
try await ClerkNativeBridge.shared.signInWithBiometrics(
|
|
40
|
+
id: nil,
|
|
41
|
+
identifierHint: nil,
|
|
42
|
+
reason: nil
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func testBiometricReverificationLevels() throws {
|
|
48
|
+
XCTAssertEqual(try ClerkNativeBridge.biometricReverificationLevel("first_factor"), .firstFactor)
|
|
49
|
+
XCTAssertEqual(try ClerkNativeBridge.biometricReverificationLevel("second_factor"), .secondFactor)
|
|
50
|
+
XCTAssertEqual(try ClerkNativeBridge.biometricReverificationLevel("multi_factor"), .multiFactor)
|
|
51
|
+
XCTAssertThrowsError(try ClerkNativeBridge.biometricReverificationLevel("unknown"))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func testBiometricReverificationPayloadWithoutEmbeddedSession() {
|
|
55
|
+
let verification = SessionVerification(id: "stepup_test", status: .complete, level: .multiFactor)
|
|
56
|
+
let payload = ClerkNativeBridge.biometricReverificationPayload(verification, sessionId: "sess_test")
|
|
57
|
+
XCTAssertEqual(payload["id"] as? String, "stepup_test")
|
|
58
|
+
XCTAssertEqual(payload["status"] as? String, "complete")
|
|
59
|
+
XCTAssertEqual(payload["level"] as? String, "multi_factor")
|
|
60
|
+
XCTAssertEqual(payload["sessionId"] as? String, "sess_test")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@MainActor
|
|
64
|
+
func testBiometricReverificationContinuesToSecondFactor() async throws {
|
|
65
|
+
var factors: [Session.BiometricVerificationLevel] = []
|
|
66
|
+
let started = SessionVerification(id: "stepup_test", status: .needsFirstFactor, level: .multiFactor)
|
|
67
|
+
let result = try await ClerkNativeBridge.verifyBiometricReverification(started) { factor in
|
|
68
|
+
factors.append(factor)
|
|
69
|
+
return SessionVerification(id: started.id, status: factor == .firstFactor ? .needsSecondFactor : .complete,
|
|
70
|
+
level: .multiFactor)
|
|
71
|
+
}
|
|
72
|
+
XCTAssertEqual(factors, [.firstFactor, .secondFactor])
|
|
73
|
+
XCTAssertEqual(result.id, started.id)
|
|
74
|
+
XCTAssertEqual(result.status, .complete)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@MainActor
|
|
78
|
+
func testBiometricReverificationPromptsOnlyForRequiredFactors() async throws {
|
|
79
|
+
for status in [SessionVerification.Status.needsFirstFactor, .needsSecondFactor, .complete] {
|
|
80
|
+
var factors: [Session.BiometricVerificationLevel] = []
|
|
81
|
+
let started = SessionVerification(id: "stepup_test", status: status, level: .multiFactor)
|
|
82
|
+
let result = try await ClerkNativeBridge.verifyBiometricReverification(started) { factor in
|
|
83
|
+
factors.append(factor)
|
|
84
|
+
return SessionVerification(id: started.id, status: .complete, level: .multiFactor)
|
|
85
|
+
}
|
|
86
|
+
XCTAssertEqual(factors, status == .complete ? [] : [status == .needsFirstFactor ? .firstFactor : .secondFactor])
|
|
87
|
+
XCTAssertEqual(result.status, .complete)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@MainActor
|
|
92
|
+
func testBiometricReverificationDoesNotRetryAnIncompleteSecondFactor() async throws {
|
|
93
|
+
var factors: [Session.BiometricVerificationLevel] = []
|
|
94
|
+
let started = SessionVerification(status: .needsFirstFactor, level: .multiFactor)
|
|
95
|
+
let result = try await ClerkNativeBridge.verifyBiometricReverification(started) { factor in
|
|
96
|
+
factors.append(factor)
|
|
97
|
+
return SessionVerification(status: .needsSecondFactor, level: .multiFactor)
|
|
98
|
+
}
|
|
99
|
+
XCTAssertEqual(factors, [.firstFactor, .secondFactor])
|
|
100
|
+
XCTAssertEqual(result.status, .needsSecondFactor)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@MainActor
|
|
104
|
+
func testBiometricReverificationPreservesFactorFailures() async {
|
|
105
|
+
for failingFactor in [Session.BiometricVerificationLevel.firstFactor, .secondFactor] {
|
|
106
|
+
var factors: [Session.BiometricVerificationLevel] = []
|
|
107
|
+
let expected = NSError(domain: "biometric_authentication_canceled", code: 1)
|
|
108
|
+
do {
|
|
109
|
+
_ = try await ClerkNativeBridge.verifyBiometricReverification(
|
|
110
|
+
SessionVerification(status: .needsFirstFactor, level: .multiFactor)
|
|
111
|
+
) { factor in
|
|
112
|
+
factors.append(factor)
|
|
113
|
+
if factor == failingFactor { throw expected }
|
|
114
|
+
return SessionVerification(status: .needsSecondFactor, level: .multiFactor)
|
|
115
|
+
}
|
|
116
|
+
XCTFail("Expected the factor failure to propagate.")
|
|
117
|
+
} catch {
|
|
118
|
+
XCTAssertEqual(error as NSError, expected)
|
|
119
|
+
}
|
|
120
|
+
XCTAssertEqual(factors, failingFactor == .firstFactor ? [.firstFactor] : [.firstFactor, .secondFactor])
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@MainActor
|
|
125
|
+
func testCancellationBetweenBiometricFactorsStopsContinuation() async {
|
|
126
|
+
var factors: [Session.BiometricVerificationLevel] = []
|
|
127
|
+
let task = Task { @MainActor in
|
|
128
|
+
try await ClerkNativeBridge.verifyBiometricReverification(
|
|
129
|
+
SessionVerification(status: .needsFirstFactor, level: .multiFactor)
|
|
130
|
+
) { factor in
|
|
131
|
+
factors.append(factor)
|
|
132
|
+
withUnsafeCurrentTask { $0?.cancel() }
|
|
133
|
+
return SessionVerification(status: .needsSecondFactor, level: .multiFactor)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
do {
|
|
137
|
+
_ = try await task.value
|
|
138
|
+
XCTFail("Expected cancellation to stop the second factor.")
|
|
139
|
+
} catch {
|
|
140
|
+
XCTAssertTrue(error is CancellationError)
|
|
141
|
+
}
|
|
142
|
+
XCTAssertEqual(factors, [.firstFactor])
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@MainActor
|
|
146
|
+
func testUnknownBiometricVerificationStatusDoesNotPrompt() async {
|
|
147
|
+
do {
|
|
148
|
+
_ = try await ClerkNativeBridge.verifyBiometricReverification(
|
|
149
|
+
SessionVerification(status: .unknown("future_status"), level: .multiFactor)
|
|
150
|
+
) { _ in
|
|
151
|
+
XCTFail("An unknown status must not prompt.")
|
|
152
|
+
return SessionVerification(status: .complete, level: .multiFactor)
|
|
153
|
+
}
|
|
154
|
+
XCTFail("Expected an unsupported status error.")
|
|
155
|
+
} catch {
|
|
156
|
+
XCTAssertEqual(ClerkNativeBridge.biometricCredentialErrorDescriptor(error, fallbackCode: "unexpected").code,
|
|
157
|
+
"E_BIOMETRIC_REVERIFICATION_FAILED")
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func testBiometricReverificationPreservesNativePolicyError() {
|
|
162
|
+
let error = BiometricCredentialError.policyIncompatible
|
|
163
|
+
let result = ClerkNativeBridge.biometricCredentialErrorDescriptor(error, fallbackCode: "unexpected")
|
|
164
|
+
XCTAssertEqual(result.code, "biometric_credential_policy_incompatible")
|
|
165
|
+
XCTAssertEqual(result.message, error.localizedDescription)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@MainActor
|
|
169
|
+
private func assertEnvironmentUnavailable(
|
|
170
|
+
_ operation: @MainActor () async throws -> Any,
|
|
171
|
+
file: StaticString = #filePath,
|
|
172
|
+
line: UInt = #line
|
|
173
|
+
) async {
|
|
174
|
+
do {
|
|
175
|
+
_ = try await operation()
|
|
176
|
+
XCTFail("Expected biometric-credential operation to reject before configuration.", file: file, line: line)
|
|
177
|
+
} catch {
|
|
178
|
+
let descriptor = ClerkNativeBridge.biometricCredentialErrorDescriptor(
|
|
179
|
+
error,
|
|
180
|
+
fallbackCode: "unexpected_error"
|
|
181
|
+
)
|
|
182
|
+
XCTAssertEqual(descriptor.code, "environment_unavailable", file: file, line: line)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import ClerkExpo
|
|
4
|
+
|
|
5
|
+
final class ClerkNativeViewHostTests: XCTestCase {
|
|
6
|
+
@MainActor
|
|
7
|
+
func testConfigureNotificationDoesNotReplaceAttachedController() {
|
|
8
|
+
let hostView = TestClerkNativeViewHost(appContext: nil)
|
|
9
|
+
let controller = UIViewController()
|
|
10
|
+
hostView.controller = controller
|
|
11
|
+
let window = mountInWindow(hostView)
|
|
12
|
+
let callsBeforeNotification = hostView.makeHostedControllerCallCount
|
|
13
|
+
|
|
14
|
+
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
|
|
15
|
+
|
|
16
|
+
XCTAssertEqual(hostView.makeHostedControllerCallCount, callsBeforeNotification)
|
|
17
|
+
XCTAssertTrue(controller.view.superview === hostView)
|
|
18
|
+
unmount(hostView, from: window)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@MainActor
|
|
22
|
+
func testConfigureNotificationAttachesControllerWhenInitiallyUnavailable() {
|
|
23
|
+
let hostView = TestClerkNativeViewHost(appContext: nil)
|
|
24
|
+
let window = mountInWindow(hostView)
|
|
25
|
+
let callsBeforeConfiguration = hostView.makeHostedControllerCallCount
|
|
26
|
+
let controller = UIViewController()
|
|
27
|
+
hostView.controller = controller
|
|
28
|
+
|
|
29
|
+
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
|
|
30
|
+
|
|
31
|
+
XCTAssertEqual(hostView.makeHostedControllerCallCount, callsBeforeConfiguration + 1)
|
|
32
|
+
XCTAssertTrue(controller.view.superview === hostView)
|
|
33
|
+
|
|
34
|
+
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
|
|
35
|
+
|
|
36
|
+
XCTAssertEqual(hostView.makeHostedControllerCallCount, callsBeforeConfiguration + 1)
|
|
37
|
+
unmount(hostView, from: window)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@MainActor
|
|
41
|
+
private func mountInWindow(_ hostView: UIView) -> UIWindow {
|
|
42
|
+
let window = UIWindow(frame: UIScreen.main.bounds)
|
|
43
|
+
let viewController = UIViewController()
|
|
44
|
+
window.rootViewController = viewController
|
|
45
|
+
viewController.view.addSubview(hostView)
|
|
46
|
+
window.makeKeyAndVisible()
|
|
47
|
+
return window
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@MainActor
|
|
51
|
+
private func unmount(_ hostView: UIView, from window: UIWindow) {
|
|
52
|
+
hostView.removeFromSuperview()
|
|
53
|
+
window.isHidden = true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@MainActor
|
|
58
|
+
private final class TestClerkNativeViewHost: ClerkNativeViewHost {
|
|
59
|
+
var controller: UIViewController?
|
|
60
|
+
private(set) var makeHostedControllerCallCount = 0
|
|
61
|
+
|
|
62
|
+
override func makeHostedController() -> UIViewController? {
|
|
63
|
+
makeHostedControllerCallCount += 1
|
|
64
|
+
return controller
|
|
65
|
+
}
|
|
66
|
+
}
|