import SwiftUI import ComposableArchitecture import Combine struct EMailLoginView: View { let store: StoreOf let onBack: () -> Void @Binding var showEmailLogin: Bool @State private var email: String = "" @State private var verificationCode: String = "" @State private var codeCountdown: Int = 0 @State private var timerCancellable: AnyCancellable? @FocusState private var focusedField: Field? enum Field { case email case verificationCode } private var isLoginButtonEnabled: Bool { return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty } private var getCodeButtonText: String { if store.isCodeLoading { return "" } else if codeCountdown > 0 { return "\(codeCountdown)S" } else { return NSLocalizedString("email_login.get_code", comment: "") } } private var isCodeButtonEnabled: Bool { return !store.isCodeLoading && codeCountdown == 0 } var body: some View { WithViewStore(store, observe: { $0.loginStep }) { viewStore in LoginContentView( store: store, onBack: onBack, email: $email, verificationCode: $verificationCode, codeCountdown: $codeCountdown, focusedField: $focusedField, isLoginButtonEnabled: isLoginButtonEnabled, getCodeButtonText: getCodeButtonText, isCodeButtonEnabled: isCodeButtonEnabled ) .onChange(of: viewStore.state) { newStep in debugInfoSync("πŸ”„ EMailLoginView: loginStep ε˜εŒ–δΈΊ \(newStep)") if newStep == .completed { debugInfoSync("βœ… EMailLoginView: η™»ε½•ζˆεŠŸοΌŒε‡†ε€‡ε…³ι—­θ‡ͺθΊ«") showEmailLogin = false } } } .onAppear { let _ = WithPerceptionTracking { store.send(.resetState) email = "" verificationCode = "" codeCountdown = 0 stopCountdown() #if DEBUG email = "exzero@126.com" store.send(.emailChanged(email)) #endif } } .onDisappear { let _ = WithPerceptionTracking { stopCountdown() } } .onChange(of: email) { newEmail in let _ = WithPerceptionTracking { store.send(.emailChanged(newEmail)) } } .onChange(of: verificationCode) { newCode in let _ = WithPerceptionTracking { store.send(.verificationCodeChanged(newCode)) } } .onChange(of: store.isCodeLoading) { isCodeLoading in let _ = WithPerceptionTracking { if !isCodeLoading && store.errorMessage == nil { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { focusedField = .verificationCode } } } } } private func startCountdown() { stopCountdown() codeCountdown = 60 timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() .sink { _ in if codeCountdown > 0 { codeCountdown -= 1 } else { stopCountdown() } } } private func stopCountdown() { timerCancellable?.cancel() timerCancellable = nil } } private struct LoginContentView: View { let store: StoreOf let onBack: () -> Void @Binding var email: String @Binding var verificationCode: String @Binding var codeCountdown: Int @FocusState.Binding var focusedField: EMailLoginView.Field? let isLoginButtonEnabled: Bool let getCodeButtonText: String let isCodeButtonEnabled: Bool var body: some View { GeometryReader { geometry in WithPerceptionTracking { ZStack { Image("bg") .resizable() .aspectRatio(contentMode: .fill) .ignoresSafeArea(.all) VStack(spacing: 0) { HStack { Button(action: { onBack() }) { Image(systemName: "chevron.left") .font(.system(size: 24, weight: .medium)) .foregroundColor(.white) .frame(width: 44, height: 44) } Spacer() } .padding(.horizontal, 16) .padding(.top, 8) Spacer().frame(height: 60) Text(NSLocalizedString("email_login.title", comment: "")) .font(.system(size: 28, weight: .medium)) .foregroundColor(.white) .padding(.bottom, 80) VStack(spacing: 24) { ZStack { RoundedRectangle(cornerRadius: 25) .fill(Color.white.opacity(0.1)) .overlay( RoundedRectangle(cornerRadius: 25) .stroke(Color.white.opacity(0.3), lineWidth: 1) ) .frame(height: 56) TextField("", text: $email) .placeholder(when: email.isEmpty) { Text(NSLocalizedString("placeholder.enter_email", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) .font(.system(size: 16)) .padding(.horizontal, 24) .keyboardType(.emailAddress) .focused($focusedField, equals: .email) } ZStack { RoundedRectangle(cornerRadius: 25) .fill(Color.white.opacity(0.1)) .overlay( RoundedRectangle(cornerRadius: 25) .stroke(Color.white.opacity(0.3), lineWidth: 1) ) .frame(height: 56) HStack { TextField("", text: $verificationCode) .placeholder(when: verificationCode.isEmpty) { Text(NSLocalizedString("placeholder.enter_code", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) .font(.system(size: 16)) .keyboardType(.numberPad) .focused($focusedField, equals: .verificationCode) Button(action: { store.send(.getVerificationCodeTapped) }) { ZStack { if store.isCodeLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.7) } else { Text(getCodeButtonText) .font(.system(size: 14, weight: .medium)) .foregroundColor(.white) } } .frame(width: 60, height: 36) .background( RoundedRectangle(cornerRadius: 18) .fill(Color.white.opacity(isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1)) ) } .disabled(!isCodeButtonEnabled || email.isEmpty || store.isCodeLoading) } .padding(.horizontal, 24) } } .padding(.horizontal, 32) Spacer().frame(height: 60) Button(action: { store.send(.loginButtonTapped(email: email, verificationCode: verificationCode)) }) { ZStack { LinearGradient( colors: [ Color(red: 0.85, green: 0.37, blue: 1.0), Color(red: 0.54, green: 0.31, blue: 1.0) ], startPoint: .leading, endPoint: .trailing ) .clipShape(RoundedRectangle(cornerRadius: 28)) HStack { if store.isLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.8) } Text(store.isLoading ? NSLocalizedString("email_login.logging_in", comment: "") : NSLocalizedString("email_login.login_button", comment: "")) .font(.system(size: 18, weight: .semibold)) .foregroundColor(.white) } } .frame(height: 56) } .disabled(store.isLoading || email.isEmpty || verificationCode.isEmpty) .opacity(isLoginButtonEnabled ? 1.0 : 0.5) .padding(.horizontal, 32) if let errorMessage = store.errorMessage { Text(errorMessage) .font(.system(size: 14)) .foregroundColor(.red) .padding(.top, 16) .padding(.horizontal, 32) } Spacer() } } } } } } //#Preview { // EMailLoginView( // store: Store( // initialState: EMailLoginFeature.State() // ) { // EMailLoginFeature() // }, // onBack: {}, // showEmailLogin: .constant(true) // ) //}