// // Copyright 2021-2024 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. // import SwiftUI struct AuthenticationLoginScreen: View { // MARK: - Properties // MARK: Private private enum CustomText { case username, submit } @Environment(\.theme) private var theme: ThemeSwiftUI /// A boolean that can be toggled to give focus to the password text field. /// This must be manually set back to `false` when the text field finishes editing. @State private var isPasswordFocused = false // BWI #7555 migration part 3 @State private var showMigrationView = BWIBuildSettings.shared.BuMXMigrationInfoLevel == 3 // BWI #7555 END // MARK: Public @ObservedObject var viewModel: AuthenticationLoginViewModel.Context var body: some View { VStack { ScrollView { VStack(spacing: 0) { if BWIBuildSettings.shared.bumLoginFlowLayout || BWIBuildSettings.shared.bwiLoginFlowLayout { ServerIcon(image: nil, size: OnboardingMetrics.iconSize) .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) .padding(.bottom, 16) } else { header .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) .padding(.bottom, 28) } if !BWIBuildSettings.shared.bwiLoginFlowLayout { serverInfo .padding(.leading, 12) .padding(.bottom, 16) Rectangle() .fill(theme.colors.quinaryContent) .frame(height: 1) .padding(.bottom, 22) } else { // bwi: show cutom header authLoginHeaderlineText } if BWIBuildSettings.shared.bumLoginFlowLayout { loginDescription .padding(.bottom, 22) } if viewModel.viewState.homeserver.showLoginForm { loginForm } if viewModel.viewState.homeserver.showQRLogin && BWIBuildSettings.shared.allowLoginWithQR { qrLoginButton } if viewModel.viewState.homeserver.showLoginForm && viewModel.viewState.showSSOButtons && BWIBuildSettings.shared.isOIDCEnabled { Text(VectorL10n.or) .foregroundColor(theme.colors.secondaryContent) .padding(.top, 16) } if viewModel.viewState.showSSOButtons && BWIBuildSettings.shared.isOIDCEnabled { ssoButtons .padding(.top, 16) } VStack(spacing: 14) { if BWIBuildSettings.shared.authScreenShowForgotPassword { forgotPasswordButton } if BWIBuildSettings.shared.bwiEnableRegisterInfo { registerButton } } .padding([.vertical], BWIBuildSettings.shared.bwiLoginFlowLayout ? 36 : 0) if !viewModel.viewState.homeserver.showLoginForm && !viewModel.viewState.showSSOButtons { fallbackButton } } .readableFrame() .padding(.horizontal, 16) } if BWIBuildSettings.shared.bumLoginFlowLayout && BWIBuildSettings.shared.bwiShowAccessibilityDeclaration { accessibilityDeclaration .frame(alignment: .bottom) .padding(.bottom, 10) } if BWIBuildSettings.shared.bumLoginFlowLayout { dataPrivacyForm .frame(alignment: .bottom) .padding(.bottom, 10) } } .background(theme.colors.background.ignoresSafeArea()) .alert(item: $viewModel.alertInfo) { $0.alert } .accentColor(theme.colors.accent) // BWI #7555 migration part 3 .sheet(isPresented: $showMigrationView) { MigrationInfoView(username: "", getUserName: nil) .environmentObject(BWIThemeService.shared) .interactiveDismissDisabled(true) } // BWI #7555 END } /// The header containing a Welcome Back title. var header: some View { Text(VectorL10n.authenticationLoginTitle) .font(theme.fonts.title2B) .multilineTextAlignment(.center) .foregroundColor(theme.colors.primaryContent) } /// The sever information section that includes a button to select a different server. var serverInfo: some View { AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address, flow: .login) { viewModel.send(viewAction: .selectServer) } } /// The form with text fields for username and password, along with a submit button. var loginForm: some View { VStack(spacing: 14) { RoundedBorderTextField(placeHolder: getCustomText(text: .username), text: $viewModel.username, isFirstResponder: false, configuration: UIKitTextInputConfiguration(returnKeyType: .next, autocapitalizationType: .none, autocorrectionType: .no), onEditingChanged: usernameEditingChanged, onCommit: { isPasswordFocused = true }) .accessibilityIdentifier("usernameTextField") .padding(.bottom, 7) RoundedBorderTextField(placeHolder: VectorL10n.authPasswordPlaceholder, text: $viewModel.password, isFirstResponder: isPasswordFocused, configuration: UIKitTextInputConfiguration(returnKeyType: .done, isSecureTextEntry: true), onEditingChanged: passwordEditingChanged, onCommit: submit) .accessibilityIdentifier("passwordTextField") // bwi: hide nv forgot password button if !BWIBuildSettings.shared.bumLoginFlowLayout && !BWIBuildSettings.shared.bwiLoginFlowLayout { Button { viewModel.send(viewAction: .forgotPassword) } label: { Text(VectorL10n.authenticationLoginForgotPassword) .font(theme.fonts.body) } .frame(maxWidth: .infinity, alignment: .trailing) .padding(.bottom, 8) } Button(action: submit) { Text(getCustomText(text: .submit)) } .disabled(BWIBuildSettings.shared.BuMXMigrationInfoLevel > 2) // BWI #7555 migration part 3 .buttonStyle(PrimaryActionButtonStyle()) .disabled(!viewModel.viewState.canSubmit) .accessibilityIdentifier("nextButton") .padding([.top], BWIBuildSettings.shared.bwiLoginFlowLayout ? 36 : 0) } } /// A QR login button that can be used for login. var qrLoginButton: some View { Button(action: qrLogin) { Label { Text(VectorL10n.authenticationLoginWithQr) } icon: { Image(uiImage: Asset.Images.qr.image) .resizable() .frame(width: 24, height: 24) } } .buttonStyle(SecondaryActionButtonStyle(font: theme.fonts.bodySB)) .padding(.vertical) .accessibilityIdentifier("qrLoginButton") } /// A list of SSO buttons that can be used for login. var ssoButtons: some View { VStack(spacing: 16) { ForEach(viewModel.viewState.homeserver.ssoIdentityProviders) { provider in AuthenticationSSOButton(provider: provider) { viewModel.send(viewAction: .continueWithSSO(provider)) } .accessibilityIdentifier("ssoButton") } } } /// A fallback button that can be used for login. var fallbackButton: some View { Button(action: fallback) { Text(VectorL10n.login) } .buttonStyle(PrimaryActionButtonStyle()) .accessibilityIdentifier("fallbackButton") } var dataPrivacyForm: some View { VStack() { if viewModel.viewState.dataPrivacyString != nil { Button(action: { guard let urlString = viewModel.viewState.dataPrivacyString else { return } let tosURL = URL.init(string: urlString)! // add your link here UIApplication.shared.vc_open(tosURL, completionHandler: nil) }, label: { Text(BWIL10n.authenticationDataprivacyText) .font(theme.fonts.footnote) .foregroundColor(theme.colors.primaryContent) + Text(BWIL10n.authenticationDataprivacyLink) .font(theme.fonts.footnote) .foregroundColor(.blue) .underline() }) .padding([.horizontal], 20) } else { EmptyView() } } } var loginDescription: some View { HStack(spacing: 0) { Text(BWIL10n.authenticationLoginDescription) .font(theme.fonts.callout) .foregroundColor(theme.colors.primaryContent) } } /// Parses the username for a homeserver. func usernameEditingChanged(isEditing: Bool) { guard !isEditing, !viewModel.username.isEmpty else { return } viewModel.send(viewAction: .parseUsername) } /// Resets the password field focus. func passwordEditingChanged(isEditing: Bool) { guard !isEditing else { return } isPasswordFocused = false } /// Sends the `next` view action so long as the form is ready to submit. func submit() { guard viewModel.viewState.canSubmit else { return } viewModel.send(viewAction: .next) } /// Sends the `fallback` view action. func fallback() { viewModel.send(viewAction: .fallback) } /// Sends the `qrLogin` view action. func qrLogin() { viewModel.send(viewAction: .qrLogin) } // bwi: custom forgot password button var forgotPasswordButton: some View { Button { viewModel.send(viewAction: .forgotPassword) } label: { Text(BWIL10n.authForgotPassword) .font(theme.fonts.body) } .frame(maxWidth: .infinity, alignment: .center) .padding(.bottom, 8) } // bwi: custom register button var registerButton: some View { Button { viewModel.send(viewAction: .register) } label: { Text(BWIL10n.bwiAuthRegisterButtonTitle) .font(theme.fonts.body) } .frame(maxWidth: .infinity, alignment: .center) .padding(.bottom, 8) } // bwi: custom header var authLoginHeaderlineText: some View { VStack(alignment: .leading) { Text(BWIL10n.authLoginHeadlineText) .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) Text(BWIL10n.authLoginSubheadlineText) .font(theme.fonts.subheadline) .foregroundColor(theme.colors.secondaryContent) } .frame(maxWidth: .infinity, alignment: .leading) .padding([.vertical], 36) } // bwi: get app specific text private func getCustomText(text: CustomText) -> String { switch text { case .submit: return BWIBuildSettings.shared.bwiLoginFlowLayout ? BWIL10n.authenticationServerSelectionSubmitButtonTitle : VectorL10n.next case .username: return BWIBuildSettings.shared.bwiLoginFlowLayout ? BWIL10n.authUserIdPlaceholder : BWIL10n.authenticationLoginUsername } } // bwi: Accessibility declaration var accessibilityDeclaration: some View { Button(action: { viewModel.send(viewAction: .accessibilityDeclaration) }, label: { Text(BWIL10n.bwiAccessibilityDeclarationButtonTitle) .font(theme.fonts.footnote) .foregroundColor(.blue) .underline() }) .padding([.horizontal], 20) } } // MARK: - Previews @available(iOS 15.0, *) struct AuthenticationLogin_Previews: PreviewProvider { static let stateRenderer = MockAuthenticationLoginScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup(addNavigation: true) .navigationViewStyle(.stack) } }