Implement bank transfer app flows

This commit is contained in:
2026-06-08 00:03:57 +05:00
parent ef877217ad
commit 9b281d48a7
127 changed files with 8008 additions and 90 deletions
@@ -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) }
}
}
}