diff --git a/Sources/BraveWallet/Crypto/CryptoTabsView.swift b/Sources/BraveWallet/Crypto/CryptoTabsView.swift index f0f5b3f643c..986017516ec 100644 --- a/Sources/BraveWallet/Crypto/CryptoTabsView.swift +++ b/Sources/BraveWallet/Crypto/CryptoTabsView.swift @@ -58,33 +58,7 @@ struct CryptoTabsView: View { ) .navigationTitle(Strings.Wallet.wallet) .navigationBarTitleDisplayMode(.inline) - .introspectViewController(customize: { vc in - vc.navigationItem.do { - // no shadow when content is at top. - let noShadowAppearance: UINavigationBarAppearance = { - let appearance = UINavigationBarAppearance() - appearance.configureWithOpaqueBackground() - appearance.titleTextAttributes = [.foregroundColor: UIColor.braveLabel] - appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.braveLabel] - appearance.backgroundColor = UIColor(braveSystemName: .pageBackground) - appearance.shadowColor = .clear - return appearance - }() - $0.scrollEdgeAppearance = noShadowAppearance - $0.compactScrollEdgeAppearance = noShadowAppearance - // shadow when content is scrolled behind navigation bar. - let shadowAppearance: UINavigationBarAppearance = { - let appearance = UINavigationBarAppearance() - appearance.configureWithOpaqueBackground() - appearance.titleTextAttributes = [.foregroundColor: UIColor.braveLabel] - appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.braveLabel] - appearance.backgroundColor = UIColor(braveSystemName: .pageBackground) - return appearance - }() - $0.standardAppearance = shadowAppearance - $0.compactAppearance = shadowAppearance - } - }) + .transparentUnlessScrolledNavigationAppearance() .toolbar { sharedToolbarItems } .background(settingsNavigationLink(for: .portfolio)) } @@ -243,23 +217,3 @@ struct CryptoTabsView: View { .hidden() } } - -private extension View { - func applyRegularNavigationAppearance() -> some View { - introspectViewController(customize: { vc in - vc.navigationItem.do { - let appearance: UINavigationBarAppearance = { - let appearance = UINavigationBarAppearance() - appearance.configureWithOpaqueBackground() - appearance.titleTextAttributes = [.foregroundColor: UIColor.braveLabel] - appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.braveLabel] - appearance.backgroundColor = .braveBackground - return appearance - }() - $0.standardAppearance = appearance - $0.compactAppearance = appearance - $0.scrollEdgeAppearance = appearance - } - }) - } -} diff --git a/Sources/BraveWallet/Crypto/Onboarding/RestoreWalletView.swift b/Sources/BraveWallet/Crypto/Onboarding/RestoreWalletView.swift index 7f20c43f580..23450f99f78 100644 --- a/Sources/BraveWallet/Crypto/Onboarding/RestoreWalletView.swift +++ b/Sources/BraveWallet/Crypto/Onboarding/RestoreWalletView.swift @@ -19,7 +19,7 @@ struct RestoreWalletContainerView: View { .background(Color(.braveBackground)) } .background(Color(.braveBackground).edgesIgnoringSafeArea(.all)) - .transparentNavigationBar(backButtonTitle: Strings.Wallet.restoreWalletBackButtonTitle, backButtonDisplayMode: .generic) + .transparentUnlessScrolledNavigationAppearance() } } diff --git a/Sources/BraveWallet/Crypto/Portfolio/PortfolioView.swift b/Sources/BraveWallet/Crypto/Portfolio/PortfolioView.swift index 645428a7bf2..55a803fc155 100644 --- a/Sources/BraveWallet/Crypto/Portfolio/PortfolioView.swift +++ b/Sources/BraveWallet/Crypto/Portfolio/PortfolioView.swift @@ -46,7 +46,7 @@ struct PortfolioView: View { VStack(spacing: 0) { Color(braveSystemName: .pageBackground) // top scroll rubberband area Color(braveSystemName: .containerBackground) // bottom drawer scroll rubberband area - }.edgesIgnoringSafeArea(.bottom) + }.edgesIgnoringSafeArea(.all) ) } diff --git a/Sources/BraveWallet/Crypto/UnlockWalletView.swift b/Sources/BraveWallet/Crypto/UnlockWalletView.swift index 784e768d05f..80cc2ad6bf8 100644 --- a/Sources/BraveWallet/Crypto/UnlockWalletView.swift +++ b/Sources/BraveWallet/Crypto/UnlockWalletView.swift @@ -12,6 +12,7 @@ struct UnlockWalletView: View { @ObservedObject var keyringStore: KeyringStore @State private var password: String = "" + @FocusState private var isPasswordFieldFocused: Bool @State private var unlockError: UnlockError? @State private var attemptedBiometricsUnlock: Bool = false @@ -25,11 +26,18 @@ struct UnlockWalletView: View { } } } - + private var isPasswordValid: Bool { !password.isEmpty } - + + private func fillPasswordFromKeychain() { + if let password = keyringStore.retrievePasswordFromKeychain() { + self.password = password + unlock() + } + } + private func unlock() { // Conflict with the keyboard submit/dismissal that causes a bug // with SwiftUI animating the screen away... @@ -42,85 +50,77 @@ struct UnlockWalletView: View { } } } - - private func fillPasswordFromKeychain() { - if let password = keyringStore.retrievePasswordFromKeychain() { - self.password = password - unlock() - } - } - - private var biometricsIcon: Image? { - let context = LAContext() - if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { - switch context.biometryType { - case .faceID: - return Image(systemName: "faceid") - case .touchID: - return Image(systemName: "touchid") - case .none: - return nil - @unknown default: - return nil - } - } - return nil - } - + var body: some View { - ScrollView(.vertical) { - VStack(spacing: 46) { - Image("graphic-lock", bundle: .module) - .padding(.bottom) - .accessibilityHidden(true) - VStack { - Text(Strings.Wallet.unlockWalletTitle) - .font(.headline) - .padding(.bottom) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - HStack { - SecureField(Strings.Wallet.passwordPlaceholder, text: $password, onCommit: unlock) - .textContentType(.password) - .font(.subheadline) - .introspectTextField(customize: { tf in - tf.becomeFirstResponder() - }) - .textFieldStyle(BraveValidatedTextFieldStyle(error: unlockError)) - if keyringStore.isKeychainPasswordStored, let icon = biometricsIcon { - Button(action: fillPasswordFromKeychain) { - icon - .imageScale(.large) - .font(.headline) - } + ScrollView { + VStack(spacing: 40) { + VStack(spacing: 4) { + Text(Strings.Wallet.unlockWallet) + .font(.title) + .fontWeight(.medium) + .foregroundColor(Color(braveSystemName: .textPrimary)) + Text(Strings.Wallet.unlockWalletDescription) + .font(.subheadline) + .foregroundColor(Color(braveSystemName: .textSecondary)) + } + .padding(.top, 44) + + VStack(spacing: 32) { + SecureField(Strings.Wallet.passwordPlaceholder, text: $password, onCommit: unlock) + .textContentType(.password) + .modifier(WalletUnlockStyleModifier(isFocused: isPasswordFieldFocused, error: unlockError)) + .focused($isPasswordFieldFocused) + + VStack(spacing: 16) { + Button(action: unlock) { + Text(Strings.Wallet.unlockWalletButtonTitle) + .frame(maxWidth: .infinity) + } + .buttonStyle(BraveFilledButtonStyle(size: .large)) + .disabled(!isPasswordValid) + + NavigationLink( + destination: RestoreWalletContainerView( + keyringStore: keyringStore + ) + ) { + Text(Strings.Wallet.restoreWalletButtonTitle) + .fontWeight(.semibold) + .foregroundColor(Color(braveSystemName: .textInteractive)) + .padding(.vertical, 10) + .padding(.horizontal, 20) + .frame(maxWidth: .infinity) } } - .padding(.horizontal, 48) } - VStack(spacing: 30) { - Button(action: unlock) { - Text(Strings.Wallet.unlockWalletButtonTitle) - } - .buttonStyle(BraveFilledButtonStyle(size: .normal)) - .disabled(!isPasswordValid) - NavigationLink(destination: RestoreWalletContainerView(keyringStore: keyringStore)) { - Text(Strings.Wallet.restoreWalletButtonTitle) - .font(.subheadline.weight(.medium)) + + if keyringStore.isKeychainPasswordStored, let icon = biometricsIcon { + Button(action: fillPasswordFromKeychain) { + icon + .imageScale(.large) + .font(.headline) + .foregroundColor(Color(braveSystemName: .iconInteractive)) + .padding() + .background(Circle() + .strokeBorder(Color(braveSystemName: .dividerInteractive), lineWidth: 1)) } - .foregroundColor(Color(.braveLabel)) } } - .frame(maxHeight: .infinity, alignment: .top) - .padding() - .padding(.vertical) + .padding(.horizontal, 34) } - .navigationTitle(Strings.Wallet.cryptoTitle) - .navigationBarTitleDisplayMode(.inline) - .background(Color(.braveBackground).edgesIgnoringSafeArea(.all)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background( + Image("wallet-background", bundle: .module) + .resizable() + .aspectRatio(contentMode: .fill) + .edgesIgnoringSafeArea(.all) + ) .onChange(of: password) { _ in unlockError = nil } .onAppear { + self.isPasswordFieldFocused = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in if !keyringStore.lockedManually && !attemptedBiometricsUnlock && keyringStore.isWalletLocked && UIApplication.shared.isProtectedDataAvailable { attemptedBiometricsUnlock = true @@ -128,15 +128,91 @@ struct UnlockWalletView: View { } } } + .navigationTitle(Strings.Wallet.cryptoTitle) + .navigationBarTitleDisplayMode(.inline) + .transparentUnlessScrolledNavigationAppearance() + .ignoresSafeArea(.keyboard, edges: .bottom) + } + + private var biometricsIcon: Image? { + let context = LAContext() + if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { + switch context.biometryType { + case .faceID: + return Image(systemName: "faceid") + case .touchID: + return Image(systemName: "touchid") +#if swift(>=5.9) + case .opticID: + return Image(systemName: "opticid") +#endif + case .none: + return nil + @unknown default: + return nil + } + } + return nil } } #if DEBUG -struct CryptoUnlockView_Previews: PreviewProvider { +struct UnlockWalletView_Previews: PreviewProvider { static var previews: some View { - UnlockWalletView(keyringStore: .previewStore) - .previewLayout(.sizeThatFits) - .previewColorSchemes() + NavigationView { + UnlockWalletView( + keyringStore: .previewStoreWithWalletCreated + ) + } + .previewColorSchemes() } } #endif + +private struct WalletUnlockStyleModifier: ViewModifier { + + var isFocused: Bool + var error: Failure? + + private var borderColor: Color { + if error != nil { + return Color.red + } else if isFocused { + return Color(braveSystemName: .iconInteractive) + } + return Color.clear + } + + func body(content: Content) -> some View { + VStack(spacing: 6) { + content + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .strokeBorder(borderColor, lineWidth: 1) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color(braveSystemName: .containerBackground)) + ) + ) + HStack(alignment: .firstTextBaseline, spacing: 4) { + Image(braveSystemName: "leo.warning.triangle-outline") + Text(error?.localizedDescription ?? " ") // maintain space when not showing an error, `hidden()` below + .fixedSize(horizontal: false, vertical: true) + .animation(nil, value: error?.localizedDescription) // Dont animate the text change, just alpha + } + .frame(maxWidth: .infinity, alignment: .leading) + .transition( + .asymmetric( + insertion: .opacity.animation(.default), + removal: .identity + ) + ) + .font(.footnote) + .foregroundColor(Color(.braveErrorLabel)) + .padding(.leading, 8) + .hidden(isHidden: error == nil) + } + } +} diff --git a/Sources/BraveWallet/Extensions/ViewExtensions.swift b/Sources/BraveWallet/Extensions/ViewExtensions.swift index 908a84739d3..d7d48ad4207 100644 --- a/Sources/BraveWallet/Extensions/ViewExtensions.swift +++ b/Sources/BraveWallet/Extensions/ViewExtensions.swift @@ -16,6 +16,7 @@ extension View { self } } + /// This function will use the help from `introspectViewController` to find the /// containing `UIViewController` of the current SwiftUI view and configure its navigation /// bar appearance to be transparent. @@ -31,6 +32,53 @@ extension View { vc.navigationItem.backButtonDisplayMode = backButtonDisplayMode } } + + func applyRegularNavigationAppearance() -> some View { + introspectViewController(customize: { vc in + vc.navigationItem.do { + let appearance: UINavigationBarAppearance = { + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.titleTextAttributes = [.foregroundColor: UIColor.braveLabel] + appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.braveLabel] + appearance.backgroundColor = .braveBackground + return appearance + }() + $0.standardAppearance = appearance + $0.compactAppearance = appearance + $0.scrollEdgeAppearance = appearance + } + }) + } + + func transparentUnlessScrolledNavigationAppearance() -> some View { + introspectViewController(customize: { vc in + vc.navigationItem.do { + // no shadow when content is at top. + let noShadowAppearance: UINavigationBarAppearance = { + let appearance = UINavigationBarAppearance() + appearance.configureWithTransparentBackground() + appearance.titleTextAttributes = [.foregroundColor: UIColor.braveLabel] + appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.braveLabel] + appearance.backgroundColor = .clear + appearance.shadowColor = .clear + return appearance + }() + $0.scrollEdgeAppearance = noShadowAppearance + $0.compactScrollEdgeAppearance = noShadowAppearance + // shadow when content is scrolled behind navigation bar. + let shadowAppearance: UINavigationBarAppearance = { + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.titleTextAttributes = [.foregroundColor: UIColor.braveLabel] + appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.braveLabel] + return appearance + }() + $0.standardAppearance = shadowAppearance + $0.compactAppearance = shadowAppearance + } + }) + } func addAccount( keyringStore: KeyringStore, diff --git a/Sources/BraveWallet/Panels/WalletPanelView.swift b/Sources/BraveWallet/Panels/WalletPanelView.swift index 09707428bde..7ea7335f1c9 100644 --- a/Sources/BraveWallet/Panels/WalletPanelView.swift +++ b/Sources/BraveWallet/Panels/WalletPanelView.swift @@ -67,7 +67,7 @@ public struct WalletPanelContainerView: View { } label: { HStack(spacing: 4) { Image(braveSystemName: "leo.lock.open") - Text(Strings.Wallet.walletPanelUnlockWallet) + Text(Strings.Wallet.unlockWallet) } } .buttonStyle(BraveFilledButtonStyle(size: .normal)) diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift index f7ca3a6759a..4b5ac5a737b 100644 --- a/Sources/BraveWallet/WalletStrings.swift +++ b/Sources/BraveWallet/WalletStrings.swift @@ -414,8 +414,8 @@ extension Strings { value: "Incorrect password", comment: "The error message displayed when the user enters the wrong password while unlocking the wallet" ) - public static let unlockWalletTitle = NSLocalizedString( - "wallet.unlockWalletTitle", + public static let unlockWalletDescription = NSLocalizedString( + "wallet.unlockWalletDescription", tableName: "BraveWallet", bundle: .module, value: "Enter password to unlock wallet", @@ -2802,11 +2802,11 @@ extension Strings { value: "None", comment: "The value shown when selecting the default wallet as none / no wallet in wallet settings, or when grouping Portfolio assets." ) - public static let walletPanelUnlockWallet = NSLocalizedString( - "wallet.walletPanelUnlockWallet", + public static let unlockWallet = NSLocalizedString( + "wallet.unlockWallet", tableName: "BraveWallet", bundle: .module, - value: "Unlock wallet", + value: "Unlock Wallet", comment: "The title of the button in wallet panel when wallet is locked. Users can click it to open full screen unlock wallet screen." ) public static let walletPanelSetupWalletDescription = NSLocalizedString(