Skip to content

Commit

Permalink
Fix brave/brave-ios#8328: Wallet unlock v2 (brave/brave-ios#8340)
Browse files Browse the repository at this point in the history
* Update UnlockWalletView to v2 designs

* Navigation styling

* Address PR comments. Add `opticID` biometry type. Maintain error space.
  • Loading branch information
StephenHeaps authored Nov 14, 2023
1 parent c1e474d commit e94973e
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 128 deletions.
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:
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

0 comments on commit e94973e

Please sign in to comment.