Skip to content

Commit 611a3e3

Browse files
authored
Fixed back stack navigation issue in Add Authenticator's SDK (#27)
1 parent c07657b commit 611a3e3

12 files changed

Lines changed: 46 additions & 14 deletions

File tree

Auth0UniversalComponents/Core/Navigation/Router.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ public class Router<Route: Hashable>: ObservableObject {
3535
/// `nil` in standalone mode; set via `useExternalPath(_:)` at view appearance.
3636
private var externalPath: Binding<NavigationPath>?
3737

38+
/// Snapshot of the host path depth at the moment `useExternalPath(_:)` was called.
39+
///
40+
/// Used by `popToSDKRoot()` to determine how many entries the SDK itself has
41+
/// appended, so only those are removed without disturbing host-app entries.
42+
private var initialExternalPathCount = 0
43+
3844
public init() {}
3945

4046
/// Redirects all navigation operations to an external `NavigationPath` binding.
@@ -46,6 +52,7 @@ public class Router<Route: Hashable>: ObservableObject {
4652
/// - Parameter binding: A writable binding to the host stack's `NavigationPath`.
4753
func useExternalPath(_ binding: Binding<NavigationPath>) {
4854
externalPath = binding
55+
initialExternalPathCount = binding.wrappedValue.count
4956
}
5057

5158
/// Pushes `route` onto the navigation stack.
@@ -81,4 +88,20 @@ public class Router<Route: Hashable>: ObservableObject {
8188
path.removeLast(path.count)
8289
}
8390
}
91+
92+
/// Pops only the routes pushed by the SDK, returning to `MyAccountAuthMethodsView`.
93+
///
94+
/// In standalone mode, clears the entire internal path so the root content
95+
/// (`MyAccountAuthMethodsView`) is shown.
96+
/// In embedded mode, only removes entries added since `useExternalPath(_:)` was
97+
/// called, leaving the host-app entries intact so the host stack is undisturbed.
98+
func popToSDKRoot() {
99+
if let ext = externalPath {
100+
let toRemove = ext.wrappedValue.count - initialExternalPathCount
101+
guard toRemove > 0 else { return }
102+
ext.wrappedValue.removeLast(toRemove)
103+
} else {
104+
path.removeLast(path.count)
105+
}
106+
}
84107
}

Auth0UniversalComponents/Core/Utils/Hashable+Extension.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ enum Route: Hashable {
2424
emailEnrollmentChallenge: EmailEnrollmentChallenge? = nil)
2525
/// Route to the filtered authentication method selection screen
2626
case filteredAuthListScreen(type: AuthMethodType,
27-
authMethods: [AuthenticationMethod])
27+
authMethods: [AuthenticationMethod],
28+
isPostEnrollment: Bool = false)
2829
}
2930

3031
/// Makes AuthMethodType conform to Hashable for use in routes.

Auth0UniversalComponents/Core/ViewFactories/ViewFactory.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ struct ViewFactory {
1919
type: type,
2020
emailOrPhoneNumber: emailOrPhoneNumber,
2121
delegate: delegate))
22-
case let .filteredAuthListScreen(type, authMethods):
22+
case let .filteredAuthListScreen(type, authMethods, isPostEnrollment):
2323
SavedAuthenticatorsView(viewModel: SavedAuthenticatorsViewModel(type: type,
2424
authenticationMethods: authMethods,
25+
isPostEnrollment: isPostEnrollment,
2526
delegate: delegate))
2627
case let .emailPhoneEnrollmentScreen(type):
2728
EmailPhoneEnrollmentView(viewModel: EmailPhoneEnrollmentViewModel(type: type))

Auth0UniversalComponents/Passkeys/Presentation/PasskeysEnrollmentViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ final class PasskeysEnrollmentViewModel: NSObject,
107107
)
108108
_ = try await confirmPasskeyEnrollmentUseCase.execute(request: confirmPasskeyEnrollmentRequest)
109109
delegate?.refreshAuthData()
110-
navigationRoute = .filteredAuthListScreen(type: .passkey, authMethods: [])
110+
navigationRoute = .filteredAuthListScreen(type: .passkey, authMethods: [], isPostEnrollment: true)
111111
} catch {
112112
await handle(error: error, scope: "openid create:me:authentication_methods") { [weak self] in
113113
Task {

Auth0UniversalComponents/RecoveryCode/Presentation/RecoveryCodeEnrollmentViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ final class RecoveryCodeEnrollmentViewModel: ObservableObject, ErrorViewModelHan
7676
request: confirmRecoveryCodeEnrollmentRequest
7777
)
7878
apiCallInProgress = false
79-
navigationRoute = .filteredAuthListScreen(type: .recoveryCode, authMethods: [])
79+
navigationRoute = .filteredAuthListScreen(type: .recoveryCode, authMethods: [], isPostEnrollment: true)
8080
delegate?.refreshAuthData()
8181
} catch {
8282
apiCallInProgress = false

Auth0UniversalComponents/SaveAuthenticators/Presentation/SavedAuthenticatorsView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ struct SavedAuthenticatorsView: View {
8282
.renderingMode(.template)
8383
.foregroundStyle(theme.colors.text.bold)
8484
.onTapGesture {
85-
router.pop()
85+
if viewModel.isPostEnrollment {
86+
router.popToSDKRoot()
87+
} else {
88+
router.pop()
89+
}
8690
}
8791
}
8892
}

Auth0UniversalComponents/SaveAuthenticators/Presentation/SavedAuthenticatorsViewModel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ final class SavedAuthenticatorsViewModel: ObservableObject, ErrorViewModelHandle
1414
private let dependencies: Auth0UniversalComponentsSDKInitializer
1515
private let authenticationMethods: [AuthenticationMethod]
1616
let type: AuthMethodType
17+
let isPostEnrollment: Bool
1718
private let getAuthMethodsUseCase: GetAuthMethodsUseCaseable
1819
private let deleteAuthMethodUseCase: DeleteAuthMethodUseCaseable
1920
private weak var delegate: RefreshAuthDataProtocol?
@@ -30,12 +31,14 @@ final class SavedAuthenticatorsViewModel: ObservableObject, ErrorViewModelHandle
3031
deleteAuthMethodsUseCase: DeleteAuthMethodUseCaseable = DeleteAuthMethodUseCase(),
3132
type: AuthMethodType,
3233
authenticationMethods: [AuthenticationMethod],
34+
isPostEnrollment: Bool = false,
3335
delegate: RefreshAuthDataProtocol?) {
3436
self.dependencies = dependencies
3537
self.type = type
3638
self.getAuthMethodsUseCase = getAuthMethodsUseCase
3739
self.deleteAuthMethodUseCase = deleteAuthMethodsUseCase
3840
self.authenticationMethods = authenticationMethods
41+
self.isPostEnrollment = isPostEnrollment
3942
self.delegate = delegate
4043
}
4144

Auth0UniversalComponents/TOTPPushEnrollment/Presentation/OTPViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ final class OTPViewModel: ObservableObject, ErrorMessageHandler {
107107
}
108108
apiCallInProgress = false
109109
delegate?.refreshAuthData()
110-
navigationRoute = .filteredAuthListScreen(type: type, authMethods: [])
110+
navigationRoute = .filteredAuthListScreen(type: type, authMethods: [], isPostEnrollment: true)
111111
} catch {
112112
apiCallInProgress = false
113113
await handle(error: error, scope: "openid create:me:authentication_methods") { [weak self] in

Auth0UniversalComponents/TOTPPushEnrollment/Presentation/TOTPPushQRCodeViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ final class TOTPPushQRCodeViewModel: ObservableObject, ErrorViewModelHandler {
112112
)
113113
delegate?.refreshAuthData()
114114
apiCallInProgress = false
115-
navigationRoute = .filteredAuthListScreen(type: type, authMethods: [])
115+
navigationRoute = .filteredAuthListScreen(type: type, authMethods: [], isPostEnrollment: true)
116116
} catch {
117117
apiCallInProgress = false
118118
await handle(error: error, scope: "openid create:me:authentication_methods") { [weak self] in

Auth0UniversalComponentsTests/RecoveryCode/RecoveryCodeEnrollmentViewModelTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ struct RecoveryCodeEnrollmentViewModelTests {
139139
}
140140

141141
await viewModel.confirmEnrollment()
142-
#expect(viewModel.navigationRoute == Route.filteredAuthListScreen(type: .recoveryCode, authMethods: []))
142+
#expect(viewModel.navigationRoute == Route.filteredAuthListScreen(type: .recoveryCode, authMethods: [], isPostEnrollment: true))
143143
}
144144
}
145145

@@ -212,7 +212,7 @@ struct RecoveryCodeEnrollmentViewModelTests {
212212

213213
// Verify delegate was called
214214
#expect(mockDelegate.refreshCalled == true, "Delegate should be called after successful enrollment")
215-
#expect(viewModel.navigationRoute == Route.filteredAuthListScreen(type: .recoveryCode, authMethods: []))
215+
#expect(viewModel.navigationRoute == Route.filteredAuthListScreen(type: .recoveryCode, authMethods: [], isPostEnrollment: true))
216216
}
217217
}
218218

0 commit comments

Comments
 (0)