Registration via API + client-owned email verification
Android UI Tests / ui-tests (push) Has been cancelled
Android UI Tests / ui-tests (push) Has been cancelled
register() now calls POST /auth/register (admin-create) then logs in for a session, replacing Kratos self-service registration — which never returns the verification flow id, so the emailed code could never be matched. The verify screen now starts its own verification flow and sends the single code on appear; verifyEmail submits the code to that exact stored flow. - AuthApi: register -> our API + immediate login; startEmailVerification; verifyEmail targets DataManager.pendingVerificationFlowId (no codeless fallback) - DataManager.pendingVerificationFlowId; KratosLoginSuccess.continue_with - iOS verify screens (standalone + onboarding) send the code on appear + Resend Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -243,6 +243,10 @@ struct OnboardingVerifyEmailContent: View {
|
||||
.onAppear {
|
||||
print("🏠 ONBOARDING: OnboardingVerifyEmailContent appeared")
|
||||
isAnimating = true
|
||||
// Establish the client-owned verification flow + send the code.
|
||||
// Kratos binds the code to this flow; verifyEmail submits it back
|
||||
// here. Without this the screen has no flow to verify against.
|
||||
viewModel.sendCode(silent: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
isCodeFieldFocused = true
|
||||
}
|
||||
|
||||
@@ -165,6 +165,17 @@ struct VerifyEmailView: View {
|
||||
.disabled(viewModel.code.count != 6 || viewModel.isLoading)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.verifyButton)
|
||||
|
||||
// Resend code
|
||||
Button(action: {
|
||||
viewModel.code = ""
|
||||
viewModel.sendCode()
|
||||
}) {
|
||||
Text("Resend code")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
.disabled(viewModel.isLoading)
|
||||
|
||||
// Help Text
|
||||
Text(L10n.Auth.verifyHelpText)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
@@ -202,6 +213,11 @@ struct VerifyEmailView: View {
|
||||
}
|
||||
.onAppear {
|
||||
isFocused = true
|
||||
// Establish the client-owned verification flow and send the
|
||||
// code. Kratos binds the code to this flow; verifyEmail submits
|
||||
// it back here. Without this the screen has no flow to verify
|
||||
// against. Re-running on every appear also covers expired codes.
|
||||
viewModel.sendCode(silent: true)
|
||||
}
|
||||
.onChange(of: viewModel.isVerified) { _, isVerified in
|
||||
if isVerified {
|
||||
|
||||
@@ -26,6 +26,30 @@ class VerifyEmailViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Start a client-owned verification flow and have Kratos send a code.
|
||||
///
|
||||
/// MUST run before the user can verify: Kratos binds each emailed code to a
|
||||
/// specific flow id, and the registration response never exposes the
|
||||
/// auto-created flow's id (see AuthApi.startEmailVerification). The screen
|
||||
/// establishes its own flow here; verifyEmail() submits the code back to it.
|
||||
/// Called on screen appear and on "Resend code".
|
||||
func sendCode(silent: Bool = false) {
|
||||
guard let email = dataManager.currentUser?.email, !email.isEmpty else {
|
||||
errorMessage = "We couldn't determine your email. Please sign in again."
|
||||
return
|
||||
}
|
||||
if !silent { isLoading = true }
|
||||
errorMessage = nil
|
||||
Task {
|
||||
let result = try? await APILayer.shared.startEmailVerification(email: email)
|
||||
if let result = result, let error = ApiResultBridge.error(from: result) {
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
}
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmail() {
|
||||
// Validation using ValidationRules
|
||||
if let error = ValidationRules.validateCode(code, expectedLength: 6) {
|
||||
|
||||
Reference in New Issue
Block a user