// // HomeView.swift // OS Vault // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import SwiftUI import BigInt struct HomeView: View { @EnvironmentObject var app: AppState @State private var showSend = false @State private var showReceive = false @State private var showSettings = false @State private var showBitcoin = false @State private var showSolana = false @State private var showTron = false @State private var showXRPL = false @State private var showTON = false var body: some View { VStack(spacing: 0) { header Divider() ScrollView { VStack(spacing: 16) { balanceCard if lowOnGas { gasWarning } actionButtons historySection } .padding(20) } } .sheet(isPresented: $showSend) { SendView() } .sheet(isPresented: $showReceive) { ReceiveView() } .sheet(isPresented: $showSettings) { SettingsView() } .sheet(isPresented: $showBitcoin) { BitcoinView() } .sheet(isPresented: $showSolana) { SolanaView() } .sheet(isPresented: $showTron) { TronView() } .sheet(isPresented: $showXRPL) { XRPLView() } .sheet(isPresented: $showTON) { TONView() } } private var header: some View { HStack { NetworkBadge(network: app.network) Spacer() if let address = app.address { Text(address.shortAddress) .font(.callout.monospaced()) .foregroundStyle(.secondary) CopyButton(text: address) .labelStyle(.iconOnly) .buttonStyle(.borderless) } Menu { Button("Bitcoin") { showBitcoin = true } Button("Solana") { showSolana = true } Button("Tron") { showTron = true } Button("XRP Ledger") { showXRPL = true } Button("TON") { showTON = true } } label: { Image(systemName: "link.circle") } .menuStyle(.borderlessButton) .frame(width: 40) .help("Other chains") Button { showSettings = true } label: { Image(systemName: "gearshape") } .buttonStyle(.borderless) Button { app.lock() } label: { Image(systemName: "lock.fill") } .buttonStyle(.borderless) .help("Lock the vault") } .padding(.horizontal, 20) .padding(.vertical, 12) } private var tokens: [Token] { Token.available(on: app.network) } private var balanceCard: some View { VStack(alignment: .leading, spacing: 12) { if !app.balancesLoaded && app.balanceError == nil { HStack { ProgressView().controlSize(.small) Text("Loading balances…").foregroundStyle(.secondary) } } if let error = app.balanceError, !app.balancesLoaded { Label(error, systemImage: "wifi.exclamationmark") .foregroundStyle(.orange) .font(.callout) } ForEach(tokens) { token in HStack(alignment: .firstTextBaseline) { Text(token.symbol).font(.headline) Text(token.name).font(.caption).foregroundStyle(.secondary) Spacer() VStack(alignment: .trailing, spacing: 2) { Text(TokenAmount.format(app.balances.tokenUnits[token.symbol] ?? 0, decimals: token.decimals, maxFractionDigits: 6)) .font(token.symbol == "USDC" ? .system(size: 28, weight: .bold, design: .rounded) : .title3.weight(.semibold)) .monospacedDigit() if let fiat = fiatLine(units: app.balances.tokenUnits[token.symbol] ?? 0, decimals: token.decimals, symbol: token.symbol) { Text(fiat).font(.caption).foregroundStyle(.secondary) } } } } Divider() HStack { Text("\(app.network.config.nativeSymbol) (gas)").font(.subheadline).foregroundStyle(.secondary) Spacer() VStack(alignment: .trailing, spacing: 2) { Text(TokenAmount.formatWei(app.balances.ethWei, maxFractionDigits: 6) + " " + app.network.config.nativeSymbol) .font(.subheadline.monospaced()) .foregroundStyle(.secondary) if let fiat = fiatLine(units: app.balances.ethWei, decimals: 18, symbol: app.network.config.nativeSymbol) { Text(fiat).font(.caption).foregroundStyle(.secondary) } } } if let error = app.balanceError, app.balancesLoaded { Text("Refresh failed: \(error)").font(.caption).foregroundStyle(.orange) } } .padding(16) .background(.quaternary.opacity(0.4)) .clipShape(RoundedRectangle(cornerRadius: 12)) } private func fiatLine(units: BigUInt, decimals: Int, symbol: String) -> String? { guard app.pricesEnabled, units > 0, let price = app.fiatPrices[symbol] else { return nil } let value = PriceService.fiatValue(units: units, decimals: decimals, price: price) return "≈ " + PriceService.formatFiat(value, currency: app.fiatCurrency) } private var lowOnGas: Bool { // Rough per-chain floor for one ERC-20 transfer; refined at send time. guard app.balancesLoaded else { return false } let floor: BigUInt switch app.network { case .polygon, .bnb, .avalanche, .gnosis: floor = 5_000_000_000_000_000 // 0.005 native case .ethereum: floor = 1_000_000_000_000_000 // 0.001 ETH default: floor = 30_000_000_000_000 // cheap L2s } return app.balances.ethWei < floor } private var gasWarning: some View { Label("\(app.network.config.nativeSymbol) balance is very low. Gas is paid in \(app.network.config.nativeSymbol) — sends may fail until you top up.", systemImage: "fuelpump.slash") .font(.callout) .foregroundStyle(.orange) .frame(maxWidth: .infinity, alignment: .leading) .padding(12) .background(Color.orange.opacity(0.12)) .clipShape(RoundedRectangle(cornerRadius: 8)) } private var actionButtons: some View { HStack(spacing: 12) { Button { showSend = true } label: { Label("Send", systemImage: "arrow.up.circle.fill") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) Button { showReceive = true } label: { Label("Receive", systemImage: "arrow.down.circle") .frame(maxWidth: .infinity) } .controlSize(.large) Button { Task { await app.refreshBalances() } } label: { Image(systemName: "arrow.clockwise") } .controlSize(.large) .help("Refresh balances") } } private var historySection: some View { VStack(alignment: .leading, spacing: 8) { Text("Activity").font(.headline) let visible = app.history.filter { $0.network == app.network } if visible.isEmpty { Text("Sends from this app will appear here. Full history is on the explorer.") .font(.callout) .foregroundStyle(.secondary) } ForEach(visible) { record in HStack { statusIcon(record.status) VStack(alignment: .leading, spacing: 2) { Text("Sent \(record.displayAmount) \(record.tokenSymbol)") .font(.callout.weight(.medium)) Text("to \(record.recipient.shortAddress) · \(record.date.formatted(date: .abbreviated, time: .shortened))") .font(.caption) .foregroundStyle(.secondary) } Spacer() Link(destination: record.explorerURL) { Image(systemName: "arrow.up.right.square") } .help("View on the explorer") } .padding(10) .background(.quaternary.opacity(0.3)) .clipShape(RoundedRectangle(cornerRadius: 8)) } if let address = app.address { Link("View full history on the explorer", destination: app.network.explorerAddressURL(address)) .font(.callout) .padding(.top, 4) } } .frame(maxWidth: .infinity, alignment: .leading) } private func statusIcon(_ status: TransactionRecord.Status) -> some View { switch status { case .pending: return Image(systemName: "clock").foregroundStyle(Color.orange) case .confirmed: return Image(systemName: "checkmark.circle.fill").foregroundStyle(Color.green) case .failed: return Image(systemName: "xmark.circle.fill").foregroundStyle(Color.red) } } }