Registration via API + client-owned email verification
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:
Trey t
2026-06-03 17:46:43 -05:00
parent 90a1d98322
commit 7c892d2bb6
7 changed files with 244 additions and 51 deletions
@@ -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) {