Require Face ID to open the app and to send
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Full-screen biometric lock, overlaid on the app whenever `model.locked`.
|
||||
struct LockScreen: View {
|
||||
var unlock: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Theme.background.ignoresSafeArea()
|
||||
VStack(spacing: 16) {
|
||||
Spacer()
|
||||
Image(systemName: "lock.shield")
|
||||
.font(.system(size: 44, weight: .light))
|
||||
.foregroundStyle(Theme.accent)
|
||||
Text("Koins is locked")
|
||||
.font(.system(size: 22, weight: .bold))
|
||||
.foregroundStyle(Theme.text)
|
||||
Text("Authenticate to continue")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(Theme.muted)
|
||||
Spacer()
|
||||
PrimaryButton(title: "Unlock with \(Biometrics.label)", action: unlock)
|
||||
Spacer()
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
.task { unlock() }
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,13 @@ struct RootView: View {
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
.overlay {
|
||||
if model.locked, model.faceIDEnabled {
|
||||
LockScreen {
|
||||
Task { await model.authenticate() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.task { await model.bootstrap() }
|
||||
.sheet(item: $seedBox) { box in
|
||||
SeedRevealView(seed: box.seed, symbol: box.symbol) { seedBox = nil }
|
||||
@@ -67,6 +74,9 @@ struct RootView: View {
|
||||
if route == .dashboard { route = .home }
|
||||
}
|
||||
.onChange(of: scenePhase) { phase in
|
||||
if phase == .background, model.faceIDEnabled, Biometrics.isAvailable {
|
||||
model.locked = true
|
||||
}
|
||||
if phase != .active {
|
||||
Task { await model.persistWallet() }
|
||||
}
|
||||
@@ -79,6 +89,7 @@ struct HomeView: View {
|
||||
@Binding var route: Route
|
||||
@State private var showDeleteConfirm = false
|
||||
@State private var selectedTransaction: TransactionRecord?
|
||||
@State private var showSettings = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 18) {
|
||||
@@ -104,6 +115,10 @@ struct HomeView: View {
|
||||
symbol: model.symbol,
|
||||
chain: model.chain) { selectedTransaction = nil }
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView()
|
||||
.environmentObject(model)
|
||||
}
|
||||
.confirmationDialog("Delete this wallet?",
|
||||
isPresented: $showDeleteConfirm,
|
||||
titleVisibility: .visible) {
|
||||
@@ -120,13 +135,26 @@ struct HomeView: View {
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Koins")
|
||||
.font(.system(size: 30, weight: .heavy))
|
||||
.foregroundStyle(Theme.text)
|
||||
Text("Monero and XRP wallet")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(Theme.muted)
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Koins")
|
||||
.font(.system(size: 30, weight: .heavy))
|
||||
.foregroundStyle(Theme.text)
|
||||
Text("Monero and XRP wallet")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(Theme.muted)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
showSettings = true
|
||||
} label: {
|
||||
Image(systemName: "gearshape")
|
||||
.font(.system(size: 17, weight: .medium))
|
||||
.foregroundStyle(Theme.muted)
|
||||
.frame(width: 36, height: 36)
|
||||
.background(Theme.surface)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject private var model: AppModel
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Theme.background.ignoresSafeArea()
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
ScreenHeader(title: "Settings", subtitle: "Security and device options", onBack: { dismiss() })
|
||||
|
||||
Card {
|
||||
Toggle(isOn: Binding(get: { model.faceIDEnabled },
|
||||
set: { model.faceIDEnabled = $0 })) {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("Unlock with \(Biometrics.label)")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(Theme.text)
|
||||
Text("Require \(Biometrics.label) to open the app, reveal the recovery phrase, and send payments.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(Theme.muted)
|
||||
}
|
||||
}
|
||||
.tint(Theme.accent)
|
||||
.disabled(!Biometrics.isAvailable)
|
||||
|
||||
if !Biometrics.isAvailable {
|
||||
Text("This device has no biometric sensor configured.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(Theme.muted)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user