Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Fix #8328: Wallet unlock v2 #8340

Merged
merged 3 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 1 addition & 47 deletions Sources/BraveWallet/Crypto/CryptoTabsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,33 +58,7 @@ struct CryptoTabsView<DismissContent: ToolbarContent>: 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))
}
Expand Down Expand Up @@ -243,23 +217,3 @@ struct CryptoTabsView<DismissContent: ToolbarContent>: 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
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct RestoreWalletContainerView: View {
.background(Color(.braveBackground))
}
.background(Color(.braveBackground).edgesIgnoringSafeArea(.all))
.transparentNavigationBar(backButtonTitle: Strings.Wallet.restoreWalletBackButtonTitle, backButtonDisplayMode: .generic)
.transparentUnlessScrolledNavigationAppearance()
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/BraveWallet/Crypto/Portfolio/PortfolioView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
}

Expand Down
222 changes: 149 additions & 73 deletions Sources/BraveWallet/Crypto/UnlockWalletView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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...
Expand All @@ -42,101 +50,169 @@ 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
fillPasswordFromKeychain()
}
}
}
.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:
StephenHeaps marked this conversation as resolved.
Show resolved Hide resolved
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<Failure: LocalizedError & Equatable>: 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)
}
}
}
Loading