import SwiftUI struct RegistrationView: View { @Binding var root: RootFlow @Binding var selectedTab: MainTab let tokenStore: TokenStore @Binding var appState: AppState @State var name = "" @State var email = "" @State var phone = "" @State var acceptedTerms = false @State var isLoading = false @State var errorMessage: String? @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme let navigate: (Route) -> Void private var isFormValid: Bool { !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !phone.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && acceptedTerms } @ViewBuilder private var logoImage: some View { #if os(Android) SwiftUI.Image("pedifoods", bundle: .module) .resizable() #else SwiftUI.Image("pedifoods") .resizable() #endif } var body: some View { ZStack { (colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea() ScrollView { VStack(spacing: 0) { logoImage .scaledToFit() .frame(width: 120, height: 120) Text("Crie sua conta") .font(AppTypography.heading1) .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) Text("Preencha os dados abaixo para começar.") .font(AppTypography.body) .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) .padding(.top, 8) .padding(.bottom, 20) VStack(spacing: 16) { VStack(alignment: .leading, spacing: 16) { LoginField(icon: "person", placeholder: "Ex: Maria Silva", text: $name) LoginField(icon: "envelope", placeholder: "seu@email.com", text: $email) LoginField(icon: "phone", placeholder: "(00) 00000-0000", text: $phone) .onChange(of: phone) { _, newValue in let masked = formatPhoneBR(newValue) if masked != newValue { phone = masked } } } } .padding(.horizontal, 24) HStack(alignment: .top, spacing: 12) { Toggle("", isOn: $acceptedTerms) .labelsHidden() .tint(AppColors.primary) Group { Text("Ao se cadastrar, você concorda com nossos [Termos de Uso](app://terms) e [Política de Privacidade](app://policy)") .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) .tint(AppColors.primary) .environment(\.openURL, OpenURLAction { url in guard url.scheme == "app" else { return .handled } switch url.host { case "terms": navigate(.terms) return .handled case "policy": navigate(.policy) return .handled default: return .handled } }) } .multilineTextAlignment(.leading) } .padding(.horizontal, 24) .padding(.top, 16) .padding(.bottom, 16) PrimaryButton(title: "Cadastrar e Receber Código", image: Image(systemName: "arrow.right")) { registerAndRequestOtp() } .padding(.horizontal, 24) .padding(.top, 6) .disabled(!isFormValid || isLoading) .opacity((!isFormValid || isLoading) ? 0.5 : 1.0) .tint(AppColors.tertiary) HStack(spacing: 6) { Text("Já tem uma conta?") .foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.8) : AppColors.textPrimary) NavigationLink("Entrar") { LoginEmailView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState, navigate: navigate) } .foregroundStyle(AppColors.primary) } .font(AppTypography.body) .padding(.top, 16) Spacer().frame(height: 12) } } .padding(.top, -40) } } private func registerAndRequestOtp() { let sanitizedName = name.trimmingCharacters(in: .whitespacesAndNewlines) let sanitizedEmail = email.trimmingCharacters(in: .whitespacesAndNewlines) let normalizedPhone = normalizePhoneNumberForAPI(phone) guard !sanitizedName.isEmpty, !sanitizedEmail.isEmpty, !normalizedPhone.isEmpty else { return } isLoading = true errorMessage = nil Task { do { let service = ApiService() let registration = try await service.registerCustomer( name: sanitizedName, email: sanitizedEmail, phoneNumber: normalizedPhone ) if registration.error { await MainActor.run { isLoading = false let message = registration.message ?? "Nao foi possivel concluir o cadastro." errorMessage = message SnackbarCenter.shared.show(title: message, style: .error, icon: "xmark.octagon.fill", duration: 4.0) } return } let otp = try await service.requestOtp(email: sanitizedEmail, phoneNumber: normalizedPhone) await MainActor.run { isLoading = false if otp.error { let message = otp.message ?? "Cadastro concluido, mas nao foi possivel enviar o codigo." errorMessage = message SnackbarCenter.shared.show(title: message, style: .warning, icon: "exclamationmark.triangle.fill", duration: 4.0) return } SnackbarCenter.shared.show(title: "Cadastro concluido. Codigo enviado.", style: .success, icon: "checkmark.seal.fill", duration: 3.0) appState.profile.email = sanitizedEmail appState.profile.phone = phone navigate(.otp(email: sanitizedEmail, phoneNumber: normalizedPhone)) } } catch { await MainActor.run { isLoading = false let message = userFacingAuthErrorMessage(error) errorMessage = message SnackbarCenter.shared.show(title: message, style: .error, icon: "xmark.octagon.fill", duration: 4.0) } } } } }