Implement bank transfer app flows
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import SwiftUI
|
||||
|
||||
struct NumpadView: View {
|
||||
let onKey: (String) -> Void
|
||||
|
||||
private let rows: [[String]] = [
|
||||
["1", "2", "3"],
|
||||
["4", "5", "6"],
|
||||
["7", "8", "9"],
|
||||
["⌫", "0", "✓"]
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
ForEach(rows, id: \.self) { row in
|
||||
HStack(spacing: 20) {
|
||||
ForEach(row, id: \.self) { key in
|
||||
NumpadKey(label: key) { onKey(key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NumpadKey: View {
|
||||
let label: String
|
||||
let action: () -> Void
|
||||
|
||||
var isConfirm: Bool { label == "✓" }
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Group {
|
||||
switch label {
|
||||
case "⌫":
|
||||
Image(systemName: "delete.left")
|
||||
.font(.system(size: 22))
|
||||
case "✓":
|
||||
Image(systemName: "checkmark")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
default:
|
||||
Text(label)
|
||||
.font(.system(size: 28, weight: .light))
|
||||
}
|
||||
}
|
||||
.frame(width: 72, height: 72)
|
||||
.background(isConfirm ? Color.accentColor : Color(.systemFill))
|
||||
.foregroundStyle(isConfirm ? .white : .primary)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NumpadView { _ in }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OtpPreviewView: View {
|
||||
let seed: String
|
||||
|
||||
@State private var code = ""
|
||||
@State private var progress = 0.0 // 0.0 – 1.0 within the current 30s window
|
||||
@State private var timer: Timer?
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Current OTP")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(formatted(code))
|
||||
.font(.system(size: 22, weight: .semibold, design: .monospaced))
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color(.systemFill), lineWidth: 3)
|
||||
Circle()
|
||||
.trim(from: 0, to: progress)
|
||||
.stroke(progressColor, style: StrokeStyle(lineWidth: 3, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.linear(duration: 0.8), value: progress)
|
||||
Text("\(Int(progress * 30))")
|
||||
.font(.caption2.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { UIPasteboard.general.string = code }
|
||||
.onAppear { refresh(); startTimer() }
|
||||
.onDisappear { timer?.invalidate(); timer = nil }
|
||||
}
|
||||
|
||||
private var progressColor: Color {
|
||||
progress < 0.33 ? .red : progress < 0.6 ? .orange : .green
|
||||
}
|
||||
|
||||
private func formatted(_ otp: String) -> String {
|
||||
guard otp.count == 6 else { return otp }
|
||||
return "\(otp.prefix(3)) \(otp.suffix(3))"
|
||||
}
|
||||
|
||||
private func refresh() {
|
||||
code = (try? Totp.generate(seed)) ?? Totp.generate(seed)
|
||||
let seconds = Int(Date().timeIntervalSince1970) % 30
|
||||
progress = Double(30 - seconds) / 30.0
|
||||
}
|
||||
|
||||
private func startTimer() {
|
||||
timer?.invalidate()
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in refresh() }
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
OtpPreviewView(seed: "JBSWY3DPEHPK3PXP")
|
||||
.padding()
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
|
||||
struct QRScannerSheet: View {
|
||||
var onResult: (String) -> Void
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
QRCameraView(onResult: { result in
|
||||
dismiss()
|
||||
onResult(result)
|
||||
})
|
||||
.ignoresSafeArea()
|
||||
.navigationTitle("Scan QR Code")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") { dismiss() }
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct QRCameraView: UIViewRepresentable {
|
||||
var onResult: (String) -> Void
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .black
|
||||
|
||||
let session = AVCaptureSession()
|
||||
guard let device = AVCaptureDevice.default(for: .video),
|
||||
let input = try? AVCaptureDeviceInput(device: device) else {
|
||||
return view
|
||||
}
|
||||
if session.canAddInput(input) { session.addInput(input) }
|
||||
|
||||
let output = AVCaptureMetadataOutput()
|
||||
if session.canAddOutput(output) {
|
||||
session.addOutput(output)
|
||||
output.setMetadataObjectsDelegate(context.coordinator, queue: .main)
|
||||
output.metadataObjectTypes = [.qr]
|
||||
}
|
||||
|
||||
let preview = AVCaptureVideoPreviewLayer(session: session)
|
||||
preview.videoGravity = .resizeAspectFill
|
||||
preview.frame = view.bounds
|
||||
view.layer.addSublayer(preview)
|
||||
context.coordinator.previewLayer = preview
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { session.startRunning() }
|
||||
context.coordinator.session = session
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
context.coordinator.previewLayer?.frame = uiView.bounds
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator { Coordinator(onResult: onResult) }
|
||||
|
||||
final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
|
||||
var onResult: (String) -> Void
|
||||
var session: AVCaptureSession?
|
||||
var previewLayer: AVCaptureVideoPreviewLayer?
|
||||
private var didCapture = false
|
||||
|
||||
init(onResult: @escaping (String) -> Void) {
|
||||
self.onResult = onResult
|
||||
}
|
||||
|
||||
func metadataOutput(_ output: AVCaptureMetadataOutput,
|
||||
didOutput objects: [AVMetadataObject],
|
||||
from connection: AVCaptureConnection) {
|
||||
guard !didCapture,
|
||||
let obj = objects.first as? AVMetadataMachineReadableCodeObject,
|
||||
let str = obj.stringValue else { return }
|
||||
didCapture = true
|
||||
session?.stopRunning()
|
||||
DispatchQueue.main.async { self.onResult(str) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user