From 190834e71bc99ac85405b54448db2174cce6b4a9 Mon Sep 17 00:00:00 2001 From: geigerzaehler242 Date: Mon, 8 Apr 2024 17:28:08 -0400 Subject: [PATCH] UI: Add Faucet view UI: Add Faucet view UI: Add Transaction Details view (copied from BDKSwiftExampleWallet) Task: Bump minimum iOS and Xcode versions to latest --- iosApp/iosApp/ContentView.swift | 4 + iosApp/iosApp/WalletView.swift | 272 +++++++++++++++++++++++----- iosApp/iosApp/WalletViewModel.swift | 35 +++- 3 files changed, 263 insertions(+), 48 deletions(-) diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 97ef6710..38840e5a 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -13,6 +13,7 @@ struct ContentView: View { // MARK: PROPERTIES + @EnvironmentObject var viewModel: WalletViewModel @State var selectedTab: Int = 0 enum Tab: Int { @@ -53,6 +54,9 @@ struct ContentView: View { // WelcomeView() // }) } + .onAppear{ + viewModel.load() + } } } diff --git a/iosApp/iosApp/WalletView.swift b/iosApp/iosApp/WalletView.swift index 65ba8444..86367e85 100644 --- a/iosApp/iosApp/WalletView.swift +++ b/iosApp/iosApp/WalletView.swift @@ -23,7 +23,7 @@ struct WalletView: View { NavigationStack(path: $navigationPath) { - VStack(spacing: 40) { + VStack(spacing: 20) { RoundedRectangle(cornerRadius: 20) .frame(height: 200) @@ -33,21 +33,20 @@ struct WalletView: View { Spacer() HStack { Text("Bitcoin Testnet") - //VStack { - Picker("Display in BTC or sats?", selection: $satsBTC) { - ForEach(amountDisplayOptions, id: \.self) { - Text($0) - } + + Picker("Display in BTC or sats?", selection: $satsBTC) { + ForEach(amountDisplayOptions, id: \.self) { + Text($0) } - .pickerStyle(.segmented) - .onChange(of: satsBTC) { - if satsBTC == "BTC" { - viewModel.toggleBTCDisplay(displayOption: "BTC") - } else { - viewModel.toggleBTCDisplay(displayOption: "sats") - } + } + .pickerStyle(.segmented) + .onChange(of: satsBTC) { + if satsBTC == "BTC" { + viewModel.toggleBTCDisplay(displayOption: "BTC") + } else { + viewModel.toggleBTCDisplay(displayOption: "sats") } - //} + } } Spacer() @@ -136,18 +135,17 @@ struct WalletView: View { HStack() { Text("Transactions") - .font(.headline) + .font(.title) Spacer() } - List { + if viewModel.transactions.isEmpty { - if viewModel.transactions.isEmpty { - Text("No Transactions") - .font(.caption) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - } else { + FaucetView() + + } else { + List { + ForEach( viewModel.transactions.sorted( by: { @@ -156,32 +154,39 @@ struct WalletView: View { } ), id: \.txid - ) { transaction in - -// NavigationLink( -// destination: TransactionDetailsView( -// transaction: transaction, -// amount: -// transaction.sent > 0 -// ? transaction.sent - transaction.received -// : transaction.received - transaction.sent -// ) -// ) { -// - WalletTransactionsListItemView(transaction: transaction) -// } - } - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - } + ) + { transaction in + + NavigationLink( + destination: TransactionDetailsView( + transaction: transaction, + amount: + transaction.sent > transaction.received + ? transaction.sent - transaction.received + : transaction.received - transaction.sent + ) + ) + { + + WalletTransactionsListItemView(transaction: transaction) + // .refreshable { + // viewModel.sync() + // //viewModel.getBalance() + // //viewModel.getTransactions() + // //await viewModel.getPrices() + // } + } + } + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + }.listStyle(.plain) - } - .listStyle(.plain) + }//viewModel.transactions.isEmpty - //Spacer(minLength: 0) - } - .padding(30) - + }//VStack + .padding(30) + } //navigation stack .onAppear{ viewModel.load() @@ -189,6 +194,51 @@ struct WalletView: View { } //body } + +struct FaucetView: View { + + @EnvironmentObject var viewModel: WalletViewModel + @State private var faucetFailed = false + + var body: some View { + + VStack { + ZStack { + RoundedRectangle(cornerRadius: 25, style: .continuous) + .stroke(Color.black , lineWidth: 1) + .frame(width: 350, height: 150, alignment: Alignment.top ) + .layoutPriority(1) // default is 0, now higher priority than Text() + Text("Hey! It looks like your transaction list is empty. Take a look around, and come back to get some coins so you can start playing with the wallet!").padding(20) + } + + if viewModel.syncState == .synced { //only show button once synced! + Button(action: { + //TODO add nnetwork call to get testnet coins!! + faucetFailed = true + }, label: { + Text("Get coins \(Image(systemName: "bitcoinsign")) \(Image(systemName: "arrow.down.left"))") + .font(.headline) + .foregroundColor(.white) + .frame(height: 55) + .frame(maxWidth: .infinity) + .background(Color.orange) + .cornerRadius(20) + }) + .padding(20) + .alert("Faucet Not Implemented Yet!", + isPresented: $faucetFailed) { + Button("Ok", role: .destructive) { + // TODO + } + } message: { + Text("Try to recover a wallet instead") + } + } + + } + } +} + struct WalletTransactionsListItemView: View { let transaction: TransactionDetails let isRedacted: Bool @@ -214,7 +264,7 @@ struct WalletTransactionsListItemView: View { } else { Image( systemName: - transaction.sent > 0 + transaction.sent > transaction.received ? "arrow.up.circle.fill" : "arrow.down.circle.fill" ) .font(.largeTitle) @@ -262,6 +312,134 @@ struct WalletTransactionsListItemView: View { }//body } +struct TransactionDetailsView: View { +// @ObservedObject var viewModel: TransactionDetailsViewModel + @EnvironmentObject var viewModel: WalletViewModel + + let transaction: TransactionDetails + let amount: UInt64 + @State private var isCopied = false + @State private var showCheckmark = false + + var body: some View { + + VStack { + + VStack(spacing: 8) { + Image(systemName: "bitcoinsign.circle.fill") + .resizable() + .foregroundColor(.orange) + .fontWeight(.bold) + .frame(width: 100, height: 100, alignment: .center) + HStack(spacing: 3) { + Text( + transaction.sent > transaction.received ? "Send" : "Receive" + ) + if transaction.confirmationTime == nil { + Text("Unconfirmed") + } else { + Text("Confirmed") + } + } + .fontWeight(.semibold) + if let height = transaction.confirmationTime?.height { + Text("Block \(height.delimiter)") + .foregroundColor(.secondary) + } + } + .font(.caption) + + Spacer() + + VStack(spacing: 8) { + HStack { + Text(amount.delimiter) + Text("sats") + } + .lineLimit(1) + .minimumScaleFactor(0.5) + .font(.largeTitle) + .foregroundColor(.primary) + .fontWeight(.bold) + .fontDesign(.rounded) + VStack(spacing: 4) { + if transaction.confirmationTime == nil { + Text("Unconfirmed") + } else { + VStack { + if let timestamp = transaction.confirmationTime?.timestamp { + Text( + timestamp.toDate().formatted( + date: .abbreviated, + time: Date.FormatStyle.TimeStyle.shortened + ) + ) + } + } + } + if let fee = transaction.fee { + Text("\(fee) sats fee") + } + } + .foregroundColor(.secondary) + .font(.callout) + } + + Spacer() + + HStack { +// if viewModel.network != Network.regtest.description { +// Button { +// if let esploraURL = viewModel.esploraURL { +// let urlString = "\(esploraURL)/tx/\(transaction.txid)" +// .replacingOccurrences(of: "/api", with: "") +// if let url = URL(string: urlString) { +// UIApplication.shared.open(url) +// } +// } +// } label: { +// Image(systemName: "safari") +// .fontWeight(.semibold) +// .foregroundColor(.bitcoinOrange) +// } +// Spacer() +// } + Text(transaction.txid) + .lineLimit(1) + .truncationMode(.middle) + Spacer() + Button { + UIPasteboard.general.string = transaction.txid + isCopied = true + showCheckmark = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + isCopied = false + showCheckmark = false + } + } label: { + HStack { + withAnimation { + Image(systemName: showCheckmark ? "checkmark" : "doc.on.doc") + } + } + .fontWeight(.semibold) + .foregroundColor(.orange) + } + } + .fontDesign(.monospaced) + .font(.caption) + .padding() + .onAppear { + // viewModel.getNetwork() + // viewModel.getEsploraUrl() + } + + } + .padding() + + } +} + #Preview { WalletView(selectedTab: .constant (0)) .environmentObject(WalletViewModel()) diff --git a/iosApp/iosApp/WalletViewModel.swift b/iosApp/iosApp/WalletViewModel.swift index 6d5b87a8..0f567b18 100644 --- a/iosApp/iosApp/WalletViewModel.swift +++ b/iosApp/iosApp/WalletViewModel.swift @@ -28,7 +28,32 @@ extension TransactionDetails: Comparable { } } +extension UInt32 { + private static var numberFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + + return numberFormatter + }() + + var delimiter: String { + return UInt32.numberFormatter.string(from: NSNumber(value: self)) ?? "" + } +} + extension UInt64 { + + private static var numberFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + + return numberFormatter + }() + + var delimiter: String { + return UInt64.numberFormatter.string(from: NSNumber(value: self)) ?? "" + } + func toDate() -> Date { return Date(timeIntervalSince1970: TimeInterval(self)) } @@ -123,7 +148,15 @@ class WalletViewModel: ObservableObject { case loaded(Wallet, Blockchain) } - enum SyncState { + enum SyncState : Equatable { + + static func == (lhs: WalletViewModel.SyncState, rhs: WalletViewModel.SyncState) -> Bool { + switch (lhs, rhs) { + case (.empty, .empty),(.syncing, .syncing), (.synced, .synced), (.failed, .failed): return true + default: return false + } + } + case empty case syncing case synced