Skip to content

Ali Bank of Pakistan #10

@anwarali777laghari-eng

Description

@anwarali777laghari-eng

// Anwar Bank – SwiftUI Starter App (iOS 17+) // Unique features for Pakistan market: Zakat calculator, savings jars (Goals), // Urdu/English localization-ready strings, offline QR receive (mock), CNIC mask. // This is a single-file scaffold to paste into a fresh SwiftUI project and iterate. // Create a new Xcode iOS App project named "AnwarBank" and replace ContentView.swift // with this file's contents. Add proper signing and capabilities before shipping.

import SwiftUI import LocalAuthentication import Combine

// MARK: - App Entry @main struct AnwarBankApp: App { @StateObject private var auth = AuthViewModel() @StateObject private var router = Router()

var body: some Scene {
WindowGroup {
RootView()
.environmentObject(auth)
.environmentObject(router)
}
}

}

// MARK: - Router final class Router: ObservableObject { @published var path = NavigationPath() func reset() { path = NavigationPath() } }

// MARK: - Root struct RootView: View { @EnvironmentObject var auth: AuthViewModel

var body: some View {
Group {
if auth.phase == .authenticated {
MainTabView()
} else if auth.phase == .otpPending {
OTPView()
} else {
LoginView()
}
}
.tint(.accentColor)
}

}

// MARK: - Design Tokens extension Color { static let abPrimary = Color("ABPrimary") // Add in Assets: a rich emerald static let abBg = Color(UIColor.systemGroupedBackground) }

// MARK: - Models struct User: Codable, Identifiable { let id: String var fullName: String var phone: String // E.164 (e.g., +923001234567) var cnic: String // 13 digits, no dashes stored; UI masks }

enum AccountType: String, Codable, CaseIterable { case current, savings, islamicSavings }

struct Account: Codable, Identifiable, Hashable { let id: String let type: AccountType var iban: String // IBAN-like; mock e.g. PK98ANWR0000... var balancePKR: Decimal }

struct Txn: Codable, Identifiable, Hashable { enum Kind: String, Codable { case debit, credit } let id: String let accountId: String let date: Date let description: String let amountPKR: Decimal let kind: Kind }

struct Payee: Codable, Identifiable, Hashable { let id: String var name: String var bank: String var accountNumber: String }

struct GoalJar: Codable, Identifiable, Hashable { let id: String var title: String var target: Decimal var saved: Decimal }

// MARK: - Services (Mock) actor KeychainService { private var storage: [String: Data] = [:] // Replace with Keychain in production func set(_ data: Data, for key: String) { storage[key] = data } func get(_ key: String) -> Data? { storage[key] } func delete(_ key: String) { storage[key] = nil } }

actor ApiService { // Simulate network with delay func requestOTP(phone: String) async throws { try await Task.sleep(nanoseconds: 400_000_000) } func verifyOTP(code: String) async throws -> User { try await Task.sleep(nanoseconds: 400_000_000) return MockData.user } func fetchAccounts() async throws -> [Account] { try await Task.sleep(nanoseconds: 250_000_000) return MockData.accounts } func fetchTransactions(accountId: String) async throws -> [Txn] { try await Task.sleep(nanoseconds: 250_000_000) return MockData.transactions.filter{ $0.accountId == accountId } } func transfer(to payee: Payee, amount: Decimal) async throws -> String { try await Task.sleep(nanoseconds: 500_000_000) return "TRX-(Int.random(in: 100000...999999))" } }

// MARK: - Auth @mainactor final class AuthViewModel: ObservableObject { enum Phase { case loggedOut, otpPending, authenticated } @published var phone = "" @published var phase: Phase = .loggedOut @published var isLoading = false @published var error: String? @published var user: User?

private let api = ApiService()
private let keychain = KeychainService()

func requestOTP() {
Task { await _requestOTP() }
}
private func _requestOTP() async {
guard phone.starts(with: "+92") else { error = "Enter phone in +92 format"; return }
error = nil; isLoading = true
do { try await api.requestOTP(phone: phone); phase = .otpPending }
catch { self.error = "Failed to send OTP" }
isLoading = false
}

func verifyOTP(_ code: String) {
Task { await _verifyOTP(code) }
}
private func verifyOTP( code: String) async {
isLoading = true; error = nil
do {
let u = try await api.verifyOTP(code: code)
user = u; phase = .authenticated
try? await keychain.set(Data("session".utf8), for: "AB.session")
} catch {
self.error = "Invalid code"
}
isLoading = false
}

func logout() {
Task { await keychain.delete("AB.session") }
phase = .loggedOut; user = nil; phone = ""
}

func biometricLoginIfPossible() {
let context = LAContext()
var err: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &err) {
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Login to Anwar Bank") { success, _ in
DispatchQueue.main.async {
if success { self.phase = .authenticated; self.user = MockData.user }
}
}
}
}

}

// MARK: - Dashboard VM @mainactor final class DashboardViewModel: ObservableObject { @published var accounts: [Account] = [] @published var selected: Account? @published var txns: [Txn] = [] @published var isLoading = false

private let api = ApiService()

func load() {
Task { await _load() }
}
private func _load() async {
isLoading = true
do {
accounts = try await api.fetchAccounts()
if selected == nil { selected = accounts.first }
if let id = selected?.id { txns = try await api.fetchTransactions(accountId: id) }
} catch {
// handle error UI as needed
}
isLoading = false
}

func refreshTxns() {
Task { @mainactor in
if let id = selected?.id { txns = (try? await api.fetchTransactions(accountId: id)) ?? [] }
}
}

}

// MARK: - Transfer VM @mainactor final class TransferViewModel: ObservableObject { @published var payees: [Payee] = MockData.payees @published var selected: Payee? = nil @published var amount: String = "" @published var note: String = "" @published var lastRef: String? = nil @published var isLoading = false @published var error: String? = nil

private let api = ApiService()

func send() {
guard let payee = selected, let amt = Decimal(string: amount), amt > 0 else {
error = "Select payee and enter valid amount"; return
}
error = nil; isLoading = true
Task {
do { let ref = try await api.transfer(to: payee, amount: Decimal(string: amount) ?? 0); lastRef = ref }
catch { error = "Transfer failed" }
isLoading = false
}
}

}

// MARK: - Goals VM (Savings Jars) @mainactor final class GoalsViewModel: ObservableObject { @published var jars: [GoalJar] = MockData.jars func add(title: String, target: Decimal) { jars.append(.init(id: UUID().uuidString, title: title, target: target, saved: 0)) } func deposit(id: String, amount: Decimal) { if let i = jars.firstIndex(where: { $0.id == id }) { jars[i].saved += amount } } }

// MARK: - Views struct LoginView: View { @EnvironmentObject var auth: AuthViewModel @State private var useBiometric = false

var body: some View {
NavigationStack {
VStack(spacing: 24) {
VStack(spacing: 8) {
Image(systemName: "creditcard")
.font(.system(size: 48, weight: .semibold))
.padding(12)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 24))
Text("Anwar Bank")
.font(.largeTitle).fontWeight(.bold)
Text("Pakistan • Secure • Simple")
.foregroundStyle(.secondary)
}
.padding(.top, 40)

        VStack(alignment: .leading, spacing: 8) {
            Text("Phone (+92)").font(.caption).foregroundStyle(.secondary)
            TextField("+92XXXXXXXXXX", text: $auth.phone)
                .keyboardType(.phonePad)
                .textContentType(.telephoneNumber)
                .padding().background(Color.abBg, in: RoundedRectangle(cornerRadius: 16))
        }

        Toggle("Enable Face ID / Touch ID", isOn: $useBiometric)
            .toggleStyle(.switch)

        Button(action: auth.requestOTP) {
            HStack { if auth.isLoading { ProgressView() } ; Text("Get OTP") }
        }
        .disabled(auth.isLoading)
        .buttonStyle(.borderedProminent)
        .padding(.top, 8)

        if let e = auth.error { Text(e).foregroundStyle(.red) }

        Spacer()

        Button("Try Biometric Login") { auth.biometricLoginIfPossible() }
            .buttonStyle(.bordered)
    }
    .padding()
}

}

}

struct OTPView: View { @EnvironmentObject var auth: AuthViewModel @State private var code = "" var body: some View { VStack(spacing: 20) { Text("Enter 6-digit OTP").font(.title3).bold() TextField("123456", text: $code) .keyboardType(.numberPad) .multilineTextAlignment(.center) .padding().background(Color.abBg, in: RoundedRectangle(cornerRadius: 16)) Button(action: { auth.verifyOTP(code) }) { HStack { if auth.isLoading { ProgressView() } ; Text("Verify & Continue") } } .buttonStyle(.borderedProminent) if let e = auth.error { Text(e).foregroundStyle(.red) } } .padding() } }

struct MainTabView: View { var body: some View { TabView { DashboardView() .tabItem { Label("Home", systemImage: "house.fill") } TransferView() .tabItem { Label("Transfer", systemImage: "arrow.left.arrow.right") } GoalsView() .tabItem { Label("Goals", systemImage: "target") } QRPayView() .tabItem { Label("QR", systemImage: "qrcode") } MoreView() .tabItem { Label("More", systemImage: "ellipsis.circle") } } .accentColor(.abPrimary) } }

struct DashboardView: View { @StateObject private var vm = DashboardViewModel()

var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
if vm.isLoading { ProgressView().frame(maxWidth: .infinity) }

            // Accounts horizontal cards
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 12) {
                    ForEach(vm.accounts) { acct in
                        AccountCard(account: acct, selected: vm.selected?.id == acct.id)
                            .onTapGesture { vm.selected = acct; vm.refreshTxns() }
                    }
                }.padding(.horizontal)
            }

            // Recent transactions
            VStack(alignment: .leading, spacing: 8) {
                HStack { Text("Recent").font(.headline); Spacer(); Button("See all"){} }
                if let acct = vm.selected {
                    ForEach(vm.txns.prefix(8)) { t in TransactionRow(txn: t) }
                    if vm.txns.isEmpty { Text("No transactions yet").foregroundStyle(.secondary) }
                }
            }.padding(.horizontal)
        }
        .padding(.vertical)
    }
    .navigationTitle("Anwar Bank")
    .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button(action: vm.load) { Image(systemName: "arrow.clockwise") } } }
}
.task { vm.load() }

}

}

struct AccountCard: View { let account: Account let selected: Bool var body: some View { VStack(alignment: .leading, spacing: 8) { Text(account.type == .current ? "Current" : account.type == .savings ? "Savings" : "Islamic Savings") .font(.caption).foregroundStyle(.secondary) Text(formatCurrency(account.balancePKR)) .font(.title2).bold() Text(maskIBAN(account.iban)).font(.caption2).foregroundStyle(.secondary) } .padding(16) .frame(width: 220, alignment: .leading) .background(selected ? .blue.opacity(0.15) : .thinMaterial, in: RoundedRectangle(cornerRadius: 24)) .overlay(alignment: .topTrailing) { Image(systemName: "banknote") .padding(8) } .shadow(radius: selected ? 4 : 0) } }

struct TransactionRow: View { let txn: Txn var body: some View { HStack { Image(systemName: txn.kind == .debit ? "arrow.up.right.circle" : "arrow.down.left.circle") .font(.title3) VStack(alignment: .leading) { Text(txn.description).font(.subheadline) Text(txn.date, style: .date).font(.caption).foregroundStyle(.secondary) } Spacer() Text(formatCurrency(txn.amountPKR * (txn.kind == .debit ? -1 : 1))) .font(.subheadline).bold() } .padding(12) .background(Color.abBg, in: RoundedRectangle(cornerRadius: 16)) } }

struct TransferView: View { @StateObject private var vm = TransferViewModel()

var body: some View {
NavigationStack {
Form {
Section("Payee") {
Picker("Select", selection: $vm.selected) {
Text("Choose...").tag(Payee?.none)
ForEach(vm.payees) { p in Text("(p.name) – (p.bank)").tag(Payee?.some(p)) }
}
}
Section("Amount") {
TextField("PKR", text: $vm.amount).keyboardType(.decimalPad)
TextField("Note (optional)", text: $vm.note)
}
if let ref = vm.lastRef { Section { Label("Sent: (ref)", systemImage: "checkmark.seal") } }
if let e = vm.error { Section { Text(e).foregroundStyle(.red) } }
}
.navigationTitle("Transfer")
.toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Send") { vm.send() } .disabled(vm.isLoading) } }
}
}

}

struct GoalsView: View { @StateObject private var vm = GoalsViewModel() @State private var showAdd = false @State private var newTitle = "" @State private var newTarget = ""

var body: some View {
NavigationStack {
List {
ForEach(vm.jars) { jar in
VStack(alignment: .leading, spacing: 8) {
HStack { Text(jar.title).bold(); Spacer(); Text(progressLabel(jar)) }
ProgressView(value: min(1, (NSDecimalNumber(decimal: jar.saved).doubleValue / max(1, NSDecimalNumber(decimal: jar.target).doubleValue))))
HStack {
Button("+1,000") { vm.deposit(id: jar.id, amount: 1000) }.buttonStyle(.bordered)
Button("+5,000") { vm.deposit(id: jar.id, amount: 5000) }.buttonStyle(.bordered)
}
}
.padding(.vertical, 6)
}
}
.navigationTitle("Savings Jars")
.toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { showAdd = true }) { Image(systemName: "plus.circle.fill") } } }
.sheet(isPresented: $showAdd) {
NavigationStack {
Form {
TextField("Title (e.g., Umrah)", text: $newTitle)
TextField("Target (PKR)", text: $newTarget).keyboardType(.decimalPad)
}.navigationTitle("New Jar").toolbar { ToolbarItem(placement: .confirmationAction) { Button("Add") { if let t = Decimal(string: newTarget), !newTitle.isEmpty { vm.add(title: newTitle, target: t) ; showAdd = false; newTitle = ""; newTarget = "" } } } }
}
}
}
}
private func progressLabel(_ jar: GoalJar) -> String { "(formatCurrency(jar.saved)) / (formatCurrency(jar.target))" }

}

struct QRPayView: View { @State private var amount = "" @State private var reference = UUID().uuidString.prefix(8) @State private var isReceiving = false var body: some View { NavigationStack { VStack(spacing: 20) { Text("Receive via QR (Offline Mock)").font(.headline) TextField("Amount (PKR)", text: $amount).keyboardType(.decimalPad) .padding().background(Color.abBg, in: RoundedRectangle(cornerRadius: 16)) Image(systemName: "qrcode").resizable().scaledToFit().frame(height: 160) Text("Ref: (reference)").foregroundStyle(.secondary) Toggle("I am ready to receive", isOn: $isReceiving) Spacer() } .padding() .navigationTitle("QR Pay") } } }

struct MoreView: View { var body: some View { NavigationStack { List { NavigationLink(destination: BillsView()) { Label("Pay Bills (Stub)", systemImage: "doc.text") } NavigationLink(destination: CardManagementView()) { Label("Cards", systemImage: "creditcard") } NavigationLink(destination: SupportChatView()) { Label("Support Chat (Mock)", systemImage: "bubble.left.and.bubble.right") } NavigationLink(destination: ProfileView()) { Label("Profile", systemImage: "person.circle") } NavigationLink(destination: SettingsView()) { Label("Settings", systemImage: "gear") } } .navigationTitle("More") } } }

struct BillsView: View { var body: some View { Text("Billers and utilities integration TBD").padding() } }

struct CardManagementView: View { @State private var isLocked = false var body: some View { Form { Section("Virtual Card") { HStack { Text("**** **** **** 4242"); Spacer(); Text("PKR"); } Toggle("Lock card", isOn: $isLocked) Button("Set spending limit") {} } } .navigationTitle("Cards") } }

struct SupportChatView: View { @State private var messages: [String] = ["Assalam o Alaikum! How can we help?" ] @State private var input = "" var body: some View { VStack { List(messages, id: .self) { Text($0) } HStack { TextField("Type message", text: $input) Button("Send") { if !input.isEmpty { messages.append(input); input = "" } } }.padding().background(.thinMaterial) } .navigationTitle("Support Chat") } }

struct ProfileView: View { @EnvironmentObject var auth: AuthViewModel var body: some View { Form { if let u = auth.user { Section("Account") { LabeledContent("Name", value: u.fullName) LabeledContent("Phone", value: u.phone) LabeledContent("CNIC", value: maskCNIC(u.cnic)) } } Section { Button("Logout", role: .destructive) { auth.logout() } } } .navigationTitle("Profile") } }

struct SettingsView: View { @State private var lang = 0 // 0=en, 1=ur (placeholder) @State private var themeDark = false @State private var zakatAmount = "" var body: some View { Form { Picker("Language", selection: $lang) { Text("English").tag(0); Text("اردو").tag(1) } Toggle("Dark Mode", isOn: $themeDark) Section("Zakat Calculator (2.5%)") { TextField("Wealth (PKR)", text: $zakatAmount).keyboardType(.decimalPad) if let n = Decimal(string: zakatAmount), n > 0 { Text("Zakat due: (formatCurrency(n * 0.025))") } } } .navigationTitle("Settings") } }

// MARK: - Utilities & Mock func formatCurrency(_ amount: Decimal) -> String { let ns = NSDecimalNumber(decimal: amount) let f = NumberFormatter(); f.numberStyle = .currency; f.currencyCode = "PKR"; f.maximumFractionDigits = 0 return f.string(from: ns) ?? "PKR 0" }

func maskIBAN(_ iban: String) -> String { guard iban.count > 6 else { return iban }; return String(iban.prefix(6)) + String(repeating: "*", count: max(0, iban.count - 10)) + String(iban.suffix(4)) } func maskCNIC(_ c: String) -> String { // 13 digits -> 12345-1234567-1 let digits = c.filter(.$0.isNumber) guard digits.count == 13 else { return c } let a = digits.prefix(5) let b = digits.dropFirst(5).prefix(7) let d = digits.suffix(1) return "(a)-(b)-(d)" }

enum MockData { static let user = User(id: "u1", fullName: "Anwar Ali", phone: "+923001400025", cnic: "4210112345671") static let accounts: [Account] = [ .init(id: "a1", type: .current, iban: "PK98ANWR0000123456789012", balancePKR: 125000), .init(id: "a2", type: .islamicSavings, iban: "PK12ANWR0000098765432101", balancePKR: 830000) ] static let transactions: [Txn] = { var ds: [Txn] = [] let descs = ["Grocery Store", "Fuel", "Salary", "Electric Bill", "Mobile Top-up", "School Fee", "Zakat", "Remittance"] for i in 0..<18 { let acct = i % 2 == 0 ? "a1" : "a2" let kind: Txn.Kind = i % 3 == 0 ? .credit : .debit let amt: Decimal = kind == .credit ? Decimal(Int.random(in: 20000...150000)) : Decimal(Int.random(in: 500...15000)) ds.append(Txn(id: UUID().uuidString, accountId: acct, date: Date().addingTimeInterval(Double(-i*86400)), description: descs[i % descs.count], amountPKR: amt, kind: kind)) } return ds }() static let payees: [Payee] = [ .init(id:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions