Not has address

This commit is contained in:
Daniel Arantes Loverde
2026-02-07 17:16:45 -03:00
parent 92c2a67736
commit 54686397b9
28 changed files with 2115 additions and 498 deletions

View File

@@ -0,0 +1,290 @@
import SwiftUI
struct OtpView: View {
private let resendDelaySeconds = 45
let email: String
let phoneNumber: String
@Binding var root: RootFlow
@Binding var selectedTab: MainTab
let tokenStore: TokenStore
@Binding var appState: AppState
@State var otp = ""
@State var isLoading = false
@State var isResending = false
@State var resendCountdown = 45
@State var canResend = false
@State var errorMessage: String?
@State var countdownTask: Task<Void, Never>?
@FocusState var isOtpFocused: Bool
@Environment(\.dismiss) var dismiss
var body: some View {
ScrollView {
VStack(spacing: 0) {
SwiftUI.Image("pedifoods")
.resizable()
.scaledToFit()
.frame(width: 74, height: 74)
.padding(.top, 140)
Text("Verificação")
.font(AppTypography.heading1)
.foregroundStyle(AppColors.textPrimary)
.padding(.top, 26)
Text("Insira o código de 8 dígitos enviado")
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
.padding(.top, 14)
Text("para \(email)")
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
.lineLimit(1)
.minimumScaleFactor(0.85)
.padding(.top, 2)
.padding(.horizontal, 24)
ZStack {
otpGrid
TextField("", text: $otp)
.appOTPKeyboard()
.foregroundStyle(Color.clear)
.tint(Color.clear)
.focused($isOtpFocused)
.frame(maxWidth: CGFloat.greatestFiniteMagnitude, maxHeight: CGFloat.greatestFiniteMagnitude)
.opacity(0.02)
.onChange(of: otp) { _, newValue in
let digits = newValue.filter { $0.isNumber }
let trimmed = String(digits.prefix(8))
if trimmed != newValue {
otp = trimmed
}
}
}
.frame(height: 204)
.onTapGesture {
isOtpFocused = true
}
.padding(.horizontal, 24)
.padding(.top, 26)
HStack(spacing: 8) {
Text("Não recebeu o código?")
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
Button(resendButtonTitle) {
resendOtp()
}
.font(AppTypography.heading3)
.foregroundStyle(AppColors.primary)
.disabled(isResending || !canResend)
}
.padding(.top, 22)
PrimaryButton(title: "Verificar e Entrar") {
validateOtp()
}
.padding(.horizontal, 24)
.padding(.top, 28)
.disabled(isLoading || otp.count != 8)
.opacity((isLoading || otp.count != 8) ? 0.6 : 1.0)
HStack(spacing: 8) {
Image(systemName: "lock.fill")
.font(.caption)
Text("Conexão segura e criptografada")
.font(AppTypography.body)
}
.foregroundStyle(AppColors.textMuted.opacity(0.8))
.padding(.top, 120)
.padding(.bottom, 18)
}
}
.background(AppColors.backgroundLight)
.ignoresSafeArea()
.onAppear {
isOtpFocused = true
startResendCooldown()
}
.onDisappear {
countdownTask?.cancel()
countdownTask = nil
}
}
private var resendButtonTitle: String {
if !canResend {
return String(format: "Reenviar em 00:%02d", resendCountdown)
}
return "Reenviar código"
}
private var otpGrid: some View {
VStack(spacing: 16) {
HStack(spacing: 14) {
otpCell(index: 0)
otpCell(index: 1)
otpCell(index: 2)
otpCell(index: 3)
}
HStack(spacing: 14) {
otpCell(index: 4)
otpCell(index: 5)
otpCell(index: 6)
otpCell(index: 7)
}
}
}
private func otpCell(index: Int) -> some View {
let char = otpCharacter(at: index)
return ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(Color(hex: "#EEF3FA"), lineWidth: 2)
)
if let char {
Text(String(char))
.font(AppTypography.heading2)
.foregroundStyle(AppColors.textPrimary)
} else {
Circle()
.fill(AppColors.textMuted.opacity(0.8))
.frame(width: 10, height: 10)
}
}
.frame(height: 94)
}
private func otpCharacter(at index: Int) -> Character? {
guard index < otp.count else { return nil }
return Array(otp)[index]
}
private func validateOtp() {
let code = otp.filter(\.isNumber)
guard code.count == 8 else { return }
isLoading = true
errorMessage = nil
Task {
do {
let service = ApiService()
let response = try await service.validateOtp(email: email, phoneNumber: phoneNumber, otp: code)
await MainActor.run {
if response.error {
isLoading = false
let message = response.message ?? "Codigo invalido."
errorMessage = message
SnackbarCenter.shared.show(title: message, style: .error, icon: "xmark.octagon.fill", duration: 4.0)
return
}
isLoading = false
SnackbarCenter.shared.show(title: "Login realizado com sucesso.", style: .success, icon: "checkmark.seal.fill", duration: 2.5)
appState.session.isAuthenticated = true
appState.session.jwt = response.result?.token
appState.profile.email = email
routeAfterLogin()
}
} 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)
}
}
}
}
private func routeAfterLogin() {
if let cached = LocationService.shared.cachedLocation() {
appState.address.latitude = cached.0
appState.address.longitude = cached.1
selectedTab = .home
root = .main
return
}
if hasConfiguredAddress() {
selectedTab = .home
root = .main
return
}
appState.address.onboardingMessage = "Vamos criar um endereço para poder visualizar os estabelecimentos mais próximos de você?"
selectedTab = .profile
root = .main
}
private func hasConfiguredAddress() -> Bool {
if appState.address.selectedId != nil {
return true
}
if appState.address.latitude != nil, appState.address.longitude != nil {
return true
}
let normalized = appState.address.display
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return normalized.isEmpty == false && normalized != "defina seu endereco"
}
private func resendOtp() {
guard canResend, !isResending else { return }
isResending = true
startResendCooldown()
errorMessage = nil
Task {
do {
let service = ApiService()
let response = try await service.requestOtp(email: email, phoneNumber: phoneNumber)
await MainActor.run {
isResending = false
if response.error {
canResend = true
resendCountdown = 0
let message = response.message ?? "Nao foi possivel reenviar o código."
errorMessage = message
SnackbarCenter.shared.show(title: message, style: .warning, icon: "exclamationmark.triangle.fill", duration: 4.0)
} else {
SnackbarCenter.shared.show(title: "Codigo reenviado.", style: .info, icon: "paperplane.fill", duration: 2.5)
}
}
} catch {
await MainActor.run {
isResending = false
canResend = true
resendCountdown = 0
let message = userFacingAuthErrorMessage(error)
errorMessage = message
SnackbarCenter.shared.show(title: message, style: .error, icon: "xmark.octagon.fill", duration: 4.0)
}
}
}
}
private func startResendCooldown() {
countdownTask?.cancel()
canResend = false
resendCountdown = resendDelaySeconds
countdownTask = Task {
var remaining = resendDelaySeconds
while !Task.isCancelled && remaining > 0 {
try? await Task.sleep(nanoseconds: 1_000_000_000)
remaining -= 1
await MainActor.run {
resendCountdown = max(remaining, 0)
canResend = remaining == 0
}
}
}
}
}