diff --git a/iosApp/iosApp/Login/LoginView.swift b/iosApp/iosApp/Login/LoginView.swift index dda1206..0d527ca 100644 --- a/iosApp/iosApp/Login/LoginView.swift +++ b/iosApp/iosApp/Login/LoginView.swift @@ -97,28 +97,17 @@ struct LoginView: View { VStack(alignment: .leading, spacing: 8) { FieldLabel(text: L10n.Auth.loginPasswordLabel) - HStack(spacing: 12) { - IconTextField( - icon: "lock.fill", - placeholder: L10n.Auth.enterPassword, - text: $viewModel.password, - isSecure: !isPasswordVisible, - textContentType: .password, - onSubmit: { viewModel.login() } - ) - .onChange(of: viewModel.password) { _, _ in - viewModel.clearError() - } - .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordField) - - Button(action: { - isPasswordVisible.toggle() - }) { - Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill") - .font(.system(size: 16, weight: .medium)) - .foregroundColor(Color.appTextSecondary) - } - .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordVisibilityToggle) + SecureIconTextField( + icon: "lock.fill", + placeholder: L10n.Auth.enterPassword, + text: $viewModel.password, + isVisible: $isPasswordVisible, + textContentType: .password, + onSubmit: { viewModel.login() }, + accessibilityId: AccessibilityIdentifiers.Authentication.passwordField + ) + .onChange(of: viewModel.password) { _, _ in + viewModel.clearError() } } diff --git a/iosApp/iosApp/Shared/Components/FormComponents.swift b/iosApp/iosApp/Shared/Components/FormComponents.swift index 2ff77ed..2165030 100644 --- a/iosApp/iosApp/Shared/Components/FormComponents.swift +++ b/iosApp/iosApp/Shared/Components/FormComponents.swift @@ -288,6 +288,62 @@ struct IconTextField: View { } } +// MARK: - Secure TextField with Icon and Visibility Toggle + +struct SecureIconTextField: View { + let icon: String + let placeholder: String + @Binding var text: String + @Binding var isVisible: Bool + var textContentType: UITextContentType? = nil + var onSubmit: (() -> Void)? = nil + var accessibilityId: String? = nil + + @FocusState private var isFocused: Bool + + var body: some View { + HStack(spacing: 12) { + ZStack { + Circle() + .fill(Color.appPrimary.opacity(0.1)) + .frame(width: 32, height: 32) + Image(systemName: icon) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(Color.appPrimary) + } + + Group { + if isVisible { + TextField(placeholder, text: $text) + .accessibilityIdentifier(accessibilityId ?? "") + } else { + SecureField(placeholder, text: $text) + .accessibilityIdentifier(accessibilityId ?? "") + } + } + .font(.system(size: 16, weight: .medium)) + .textContentType(textContentType) + .focused($isFocused) + .submitLabel(.go) + .onSubmit { onSubmit?() } + + Button(action: { isVisible.toggle() }) { + Image(systemName: isVisible ? "eye.slash.fill" : "eye.fill") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(Color.appTextSecondary) + } + } + .padding(16) + .background(Color.appBackgroundPrimary.opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(isFocused ? Color.appPrimary : Color.appTextSecondary.opacity(0.15), lineWidth: 1.5) + ) + .animation(.easeInOut(duration: 0.2), value: isFocused) + } +} + // MARK: - Field Label struct FieldLabel: View {