import SwiftUI // Mirrors fragment_accounts.xml (RecyclerView) + item_account.xml layout: // [40dp circle logo] [Name / Number(mono) / Type] [Balance + blocked + send button] // Divider between items, grouped by bank with section headers. struct AccountsView: View { @Environment(HomeViewModel.self) private var vm var body: some View { NavigationStack { Group { if vm.accounts.isEmpty && !vm.isRefreshing { emptyState } else { accountList } } .navigationTitle("Accounts") .toolbar { if vm.isRefreshing { ToolbarItem(placement: .topBarTrailing) { ProgressView() } } ToolbarItem(placement: .topBarTrailing) { Button { withAnimation { vm.hideAmounts.toggle() } } label: { Image(systemName: vm.hideAmounts ? "eye.slash" : "eye") } } } } } // MARK: - Account list private var accountList: some View { List { bankSection(title: "MIB Faisanet", accounts: vm.mibAccounts, color: bankColor("MIB")) bankSection(title: "Bank of Maldives", accounts: vm.bmlAccounts, color: bankColor("BML")) bankSection(title: "Fahipay", accounts: vm.fahipayAccounts, color: bankColor("FAHIPAY")) } .listStyle(.plain) .refreshable { await vm.refresh() } } @ViewBuilder private func bankSection(title: String, accounts: [BankAccount], color: Color) -> some View { if !accounts.isEmpty { Section { ForEach(accounts) { account in AccountRow(account: account, hideAmounts: vm.hideAmounts) .listRowInsets(EdgeInsets()) // remove default insets — row provides its own .listRowSeparator(.hidden) } } header: { HStack(spacing: 6) { Circle().fill(color).frame(width: 8, height: 8) Text(title) .font(.caption.weight(.semibold)) .foregroundStyle(.secondary) .textCase(nil) } .padding(.horizontal, 16) .padding(.top, 8) } } } // MARK: - Empty state private var emptyState: some View { VStack(spacing: 16) { Image(systemName: "building.columns") .font(.system(size: 52)) .foregroundStyle(.tertiary) Text("No accounts yet") .font(.headline) Text("Your accounts will appear here after logging in to a bank.") .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } // MARK: - Account row (item_account.xml) // Layout: [40dp circle] [Name / Number(mono) / Type] → [Balance / Blocked / SendBtn(40dp)] // Padding: 16dp horizontal, 14dp vertical. Divider below. private struct AccountRow: View { let account: BankAccount let hideAmounts: Bool var body: some View { VStack(spacing: 0) { HStack(alignment: .center, spacing: 0) { // ── Bank logo circle (ShapeableImageView 40dp) ────────────────── bankLogo .padding(.trailing, 12) // ── Left: name / number / type ────────────────────────────────── VStack(alignment: .leading, spacing: 2) { Text(displayName) .font(.subheadline.weight(.medium)) // textAppearanceTitleMedium .foregroundStyle(.primary) .lineLimit(1) Text(account.accountNumber) .font(.callout) // textAppearanceTitleSmall .foregroundStyle(.secondary) .fontDesign(.monospaced) // android:fontFamily="monospace" .lineLimit(1) if !account.accountTypeName.isEmpty { Text(account.accountTypeName) .font(.caption) // textAppearanceBodySmall .foregroundStyle(.secondary) .lineLimit(1) } } Spacer(minLength: 16) // ── Right: balance / blocked / send button ────────────────────── VStack(alignment: .trailing, spacing: 2) { Text(hideAmounts ? "••••••" : account.formattedAvailableBalance) .font(.callout.weight(.medium)) // textAppearanceTitleSmall .foregroundStyle(.primary) .monospacedDigit() .lineLimit(1) if account.blockedAmount > 0 && !hideAmounts { Text("Blocked: \(account.currencyName) \(String(format: "%,.2f", account.blockedAmount))") .font(.caption2) .foregroundStyle(.red) .lineLimit(1) } // Transfer button (40x40dp, borderless, tinted primary) Button { /* Transfer — Phase 7 */ } label: { Image(systemName: "arrow.up.right") .font(.system(size: 16, weight: .medium)) .foregroundStyle(.tint) .frame(width: 40, height: 40) .contentShape(Rectangle()) } .buttonStyle(.plain) .padding(.top, 4) } } .padding(.horizontal, 16) .padding(.vertical, 14) // Divider (1dp, 16dp horizontal margin, colorOutlineVariant) Divider() .padding(.horizontal, 16) } } // MARK: - Helpers private var displayName: String { account.accountBriefName.isEmpty ? account.accountTypeName : account.accountBriefName } private var bankLogo: some View { Circle() .fill(color.opacity(0.12)) .frame(width: 40, height: 40) .overlay { Text(String(account.bank.prefix(1))) .font(.headline) .foregroundStyle(color) } } private var color: Color { bankColor(account.bank) } } // MARK: - Shared bank color private func bankColor(_ bank: String) -> Color { switch bank { case "MIB": return Color(red: 0.247, green: 0.396, blue: 0.678) // #3F65AD primary case "BML": return Color(red: 0.0, green: 0.47, blue: 0.80) case "FAHIPAY": return .purple default: return .gray } } #Preview { AccountsView() .environment(HomeViewModel()) }