diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_reveal_password.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/authentication_reveal_password.imageset/Contents.json new file mode 100644 index 0000000000..0edfbd457e --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_reveal_password.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "authentication_reveal_password.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_reveal_password.imageset/authentication_reveal_password.svg b/Riot/Assets/Images.xcassets/Authentication/authentication_reveal_password.imageset/authentication_reveal_password.svg new file mode 100644 index 0000000000..3db7b6835a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_reveal_password.imageset/authentication_reveal_password.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index a7dd8ed579..11cf533866 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -33,6 +33,7 @@ internal class Asset: NSObject { internal static let authenticationEmailIcon = ImageAsset(name: "authentication_email_icon") internal static let authenticationMsisdnIcon = ImageAsset(name: "authentication_msisdn_icon") internal static let authenticationPasswordIcon = ImageAsset(name: "authentication_password_icon") + internal static let authenticationRevealPassword = ImageAsset(name: "authentication_reveal_password") internal static let authenticationServerSelectionIcon = ImageAsset(name: "authentication_server_selection_icon") internal static let authenticationSsoIconApple = ImageAsset(name: "authentication_sso_icon_apple") internal static let authenticationSsoIconFacebook = ImageAsset(name: "authentication_sso_icon_facebook") diff --git a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift index dcd7d919cd..b8bdec32f5 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift @@ -22,10 +22,9 @@ struct AuthenticationTermsToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) -> some View { Button { configuration.isOn.toggle() } label: { - Image(systemName: configuration.isOn ? "checkmark.square.fill" : "square") + Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle") .font(.title3.weight(.regular)) - .imageScale(.large) - .foregroundColor(configuration.isOn ? theme.colors.accent : theme.colors.tertiaryContent) + .foregroundColor(theme.colors.accent) } .buttonStyle(.plain) } diff --git a/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift b/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift index bce16b4ff1..7eb67d39c6 100644 --- a/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift +++ b/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift @@ -16,24 +16,6 @@ import SwiftUI -@available(iOS 14.0, *) -extension ThemableTextField { - /// Adds a clear button to the text field - /// - Parameters: - /// - show: A boolean that can be used to dynamically show/hide the button. Defaults to `true`. - /// - text: The text for the clear button to clear. - /// - alignment: The vertical alignment of the button in the text field. Default to `center` - @ViewBuilder - func showClearButton(_ show: Bool = true, text: Binding, alignment: VerticalAlignment = .center) -> some View { - if show { - modifier(ClearViewModifier(alignment: alignment, text: text)) - } else { - self - } - } -} - -@available(iOS 14.0, *) extension ThemableTextEditor { func showClearButton(text: Binding, alignment: VerticalAlignment = .top) -> some View { return modifier(ClearViewModifier(alignment: alignment, text: text)) @@ -41,9 +23,7 @@ extension ThemableTextEditor { } /// `ClearViewModifier` aims to add a clear button (e.g. `x` button) on the right side of any text editing view -@available(iOS 14.0, *) -struct ClearViewModifier: ViewModifier -{ +struct ClearViewModifier: ViewModifier { // MARK: - Properties let alignment: VerticalAlignment @@ -58,8 +38,7 @@ struct ClearViewModifier: ViewModifier // MARK: - Public - public func body(content: Content) -> some View - { + public func body(content: Content) -> some View { HStack(alignment: alignment) { content if !text.isEmpty { @@ -70,7 +49,9 @@ struct ClearViewModifier: ViewModifier .renderingMode(.template) .foregroundColor(theme.colors.quarterlyContent) } - .padding(EdgeInsets(top: alignment == .top ? 8 : 0, leading: 0, bottom: alignment == .bottom ? 8 : 0, trailing: 8)) + .padding(.top, alignment == .top ? 8 : 0) + .padding(.bottom, alignment == .bottom ? 8 : 0) + .padding(.trailing, 12) } } } diff --git a/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift b/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift new file mode 100644 index 0000000000..94fd8525a4 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift @@ -0,0 +1,54 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// Adds a reveal password button (e.g. an eye button) on the +/// right side of the view. For use with `ThemableTextField`. +struct PasswordButtonModifier: ViewModifier { + + // MARK: - Properties + + let text: String + @Binding var isSecureTextVisible: Bool + let alignment: VerticalAlignment + + // MARK: - Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + @ScaledMetric private var iconSize = 16 + + // MARK: - Public + + public func body(content: Content) -> some View { + HStack(alignment: .center) { + content + + if !text.isEmpty { + Button { isSecureTextVisible.toggle() } label: { + Image(Asset.Images.authenticationRevealPassword.name) + .renderingMode(.template) + .resizable() + .frame(width: iconSize, height: iconSize) + .foregroundColor(theme.colors.secondaryContent) + } + .padding(.top, alignment == .top ? 8 : 0) + .padding(.bottom, alignment == .bottom ? 8 : 0) + .padding(.trailing, 12) + } + } + } +} diff --git a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift index f3ea7c89fe..c0c69a2091 100644 --- a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift +++ b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift @@ -16,8 +16,6 @@ import SwiftUI - -@available(iOS 14.0, *) struct RoundedBorderTextField: View { // MARK: - Properties @@ -30,6 +28,7 @@ struct RoundedBorderTextField: View { var isFirstResponder = false var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration() + @State var isSecureTextVisible = false var onTextChanged: ((String) -> Void)? = nil var onEditingChanged: ((Bool) -> Void)? = nil @@ -37,7 +36,7 @@ struct RoundedBorderTextField: View { // MARK: Private - @State private var editing = false + @State private var isEditing = false @Environment(\.theme) private var theme: ThemeSwiftUI @Environment(\.isEnabled) private var isEnabled @@ -51,7 +50,7 @@ struct RoundedBorderTextField: View { .foregroundColor(theme.colors.primaryContent) .font(theme.fonts.subheadline) .multilineTextAlignment(.leading) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 0)) + .padding(.bottom, 8) } ZStack(alignment: .leading) { @@ -63,14 +62,17 @@ struct RoundedBorderTextField: View { .accessibilityHidden(true) } - ThemableTextField(placeholder: "", text: $text, configuration: configuration, onEditingChanged: { edit in - self.editing = edit - onEditingChanged?(edit) - }, onCommit: { + ThemableTextField(placeholder: "", + text: $text, + configuration: configuration, + isSecureTextVisible: $isSecureTextVisible) { isEditing in + self.isEditing = isEditing + onEditingChanged?(isEditing) + } onCommit: { onCommit?() - }) + } .makeFirstResponder(isFirstResponder) - .showClearButton(isEnabled, text: $text) + .addButton(isEnabled) .onChange(of: text) { newText in onTextChanged?(newText) } @@ -81,25 +83,39 @@ struct RoundedBorderTextField: View { } .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: text.isEmpty ? 8 : 0)) .background(RoundedRectangle(cornerRadius: 8).fill(theme.colors.background)) - .overlay(RoundedRectangle(cornerRadius: 8) - .stroke(editing ? theme.colors.accent : (footerText != nil && isError ? theme.colors.alert : theme.colors.quinaryContent), lineWidth: editing || (footerText != nil && isError) ? 2 : 1)) + .overlay(RoundedRectangle(cornerRadius: 8).stroke(borderColor, lineWidth: borderWidth)) if let footerText = self.footerText { Text(footerText) .foregroundColor(isError ? theme.colors.alert : theme.colors.tertiaryContent) .font(theme.fonts.footnote) .multilineTextAlignment(.leading) - .padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)) + .padding(.top, 8) .transition(.opacity) } } .animation(.easeOut(duration: 0.2)) } + + /// The text field's border color. + private var borderColor: Color { + if isEditing { + return theme.colors.accent + } else if footerText != nil && isError { + return theme.colors.alert + } else { + return theme.colors.quinaryContent + } + } + + /// The text field's border width. + private var borderWidth: CGFloat { + isEditing || (footerText != nil && isError) ? 2 : 1 + } } // MARK: - Previews -@available(iOS 14.0, *) struct TextFieldWithError_Previews: PreviewProvider { static var previews: some View { @@ -118,6 +134,11 @@ struct TextFieldWithError_Previews: PreviewProvider { RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: "Some normal text", isError: false) RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: "Some normal text", isError: false) .disabled(true) + + Spacer().frame(height: 0) + + RoundedBorderTextField(title: "Password", placeHolder: "Enter your password", text: .constant(""), configuration: UIKitTextInputConfiguration(isSecureTextEntry: true)) + RoundedBorderTextField(title: "Password", placeHolder: "Enter your password", text: .constant("password"), configuration: UIKitTextInputConfiguration(isSecureTextEntry: true)) } } } diff --git a/RiotSwiftUI/Modules/Common/Util/ThemableTextField.swift b/RiotSwiftUI/Modules/Common/Util/ThemableTextField.swift index ef469a43e0..fa31d1f022 100644 --- a/RiotSwiftUI/Modules/Common/Util/ThemableTextField.swift +++ b/RiotSwiftUI/Modules/Common/Util/ThemableTextField.swift @@ -24,7 +24,6 @@ struct UIKitTextInputConfiguration { var autocorrectionType: UITextAutocorrectionType = .default } -@available(iOS 14.0, *) struct ThemableTextField: UIViewRepresentable { // MARK: Properties @@ -32,6 +31,7 @@ struct ThemableTextField: UIViewRepresentable { @State var placeholder: String? @Binding var text: String @State var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration() + @Binding var isSecureTextVisible: Bool var onEditingChanged: ((_ edit: Bool) -> Void)? var onCommit: (() -> Void)? @@ -47,11 +47,13 @@ struct ThemableTextField: UIViewRepresentable { init(placeholder: String? = nil, text: Binding, configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration(), + isSecureTextVisible: Binding = .constant(false), onEditingChanged: ((_ edit: Bool) -> Void)? = nil, onCommit: (() -> Void)? = nil) { self._text = text self._placeholder = State(initialValue: placeholder) self._configuration = State(initialValue: configuration) + self._isSecureTextVisible = isSecureTextVisible self.onEditingChanged = onEditingChanged self.onCommit = onCommit @@ -89,7 +91,7 @@ struct ThemableTextField: UIViewRepresentable { uiView.keyboardType = configuration.keyboardType uiView.returnKeyType = configuration.returnKeyType - uiView.isSecureTextEntry = configuration.isSecureTextEntry + uiView.isSecureTextEntry = configuration.isSecureTextEntry ? !isSecureTextVisible : false uiView.autocapitalizationType = configuration.autocapitalizationType uiView.autocorrectionType = configuration.autocorrectionType } @@ -149,7 +151,6 @@ struct ThemableTextField: UIViewRepresentable { // MARK: - modifiers -@available(iOS 14.0, *) extension ThemableTextField { func makeFirstResponder() -> ThemableTextField { return makeFirstResponder(true) @@ -159,4 +160,22 @@ extension ThemableTextField { internalParams.isFirstResponder = isFirstResponder return self } + + /// Adds a button button to the text field + /// - Parameters: + /// - show: A boolean that can be used to dynamically show/hide the button. Defaults to `true`. + /// - alignment: The vertical alignment of the button in the text field. Default to `center` + @ViewBuilder + func addButton(_ show: Bool, alignment: VerticalAlignment = .center) -> some View { + if show && configuration.isSecureTextEntry { + modifier(PasswordButtonModifier(text: text, + isSecureTextVisible: $isSecureTextVisible, + alignment: alignment)) + } else if show { + modifier(ClearViewModifier(alignment: alignment, + text: $text)) + } else { + self + } + } } diff --git a/changelog.d/pr-6268.wip b/changelog.d/pr-6268.wip new file mode 100644 index 0000000000..6d0b282b48 --- /dev/null +++ b/changelog.d/pr-6268.wip @@ -0,0 +1 @@ +Authentication: Add reveal password button and use a rounded checkbox