From c3b8f926ad193e01365004e184a64aefc9b5bd9a Mon Sep 17 00:00:00 2001 From: 0xChqrles Date: Thu, 6 Jun 2024 23:50:10 +0200 Subject: [PATCH] finalize sending flow --- app/App/Components/SpinnerView.swift | 13 ++- app/App/Models/Model.swift | 93 +++++++++++++------ app/App/Navigation/Core/Home/HomeView.swift | 21 +++-- .../Core/Sending/ConfirmationView.swift | 40 +++----- .../Navigation/Core/Sending/ContactRow.swift | 2 +- .../Core/Sending/SendingAmountView.swift | 44 ++++++++- .../Core/Sending/SendingRecipientView.swift | 14 ++- 7 files changed, 151 insertions(+), 76 deletions(-) diff --git a/app/App/Components/SpinnerView.swift b/app/App/Components/SpinnerView.swift index 24a330ac..71fd98e6 100644 --- a/app/App/Components/SpinnerView.swift +++ b/app/App/Components/SpinnerView.swift @@ -39,10 +39,6 @@ struct SpinnerView: View { .frame(width: 48, height: 48) .animation(.easeIn(duration: 1.5).repeatForever(autoreverses: true), value: self.isTrimming) .animation(.linear(duration: 0.3), value: self.isComplete) - .onAppear { - self.isTrimming = true - self.isSpinning = true - } } .rotationEffect(Angle(degrees: isSpinning ? 360 : 0)) .animation(.linear(duration: 1.0).repeatForever(autoreverses: false), value: isSpinning) @@ -56,6 +52,15 @@ struct SpinnerView: View { .animation(.easeIn(duration: 0.2).delay(0.3), value: isComplete) } + .onAppear { + // small delay because SwiftUI animation are broken + Task { @MainActor in + try await Task.sleep(for: .seconds(0.1)) + + self.isTrimming = true + self.isSpinning = true + } + } } } diff --git a/app/App/Models/Model.swift b/app/App/Models/Model.swift index 8d36d4ea..f0cfd23c 100644 --- a/app/App/Models/Model.swift +++ b/app/App/Models/Model.swift @@ -13,9 +13,10 @@ import Starknet import BigInt enum Status: Equatable { - case none - case active + case none // TODO: find a better name case loading + case signing + case signed case success case error(String) } @@ -30,21 +31,21 @@ class Model: ObservableObject { @Published var isLoading = false @Published var showMessage = false - // Navgiation - @Published var showSendingSheet = false { + // Sending USDC + @Published var recipientContact: Contact? + @Published var sendingAmount: String = "" + @Published var sendingStatus: Status = .none + @Published var showSendingView = false { didSet { - if self.showSendingSheet { + if self.showSendingView { self.initiateTransfer() } else { self.dismissTransfer() } } } - - // Sending USDC - @Published var recipientContact: Contact? - @Published var sendingAmount: String = "" - @Published var sendingStatus: Status = .none + @Published var showSendingConfirmation = false + @Published var signedSendingTransaction: StarknetInvokeTransactionV1? = nil // Country picker @Published var selectedRegionCode = Locale.current.regionOrFrance.identifier @@ -187,21 +188,39 @@ extension Model { extension Model { - func executeTransaction(calls: [StarknetCall]) async throws -> StarknetInvokeTransactionResponse { + // maxFee / nonce + + func getParams() async throws -> StarknetInvokeParamsV1 { let nonce = try await self.account.getNonce() let maxFee = self.estimateFees() - return try await account.executeV1(calls: calls, params: StarknetOptionalInvokeParamsV1(nonce: nonce, maxFee: maxFee)) - } - - func executeTransaction(call: StarknetCall) async throws -> StarknetInvokeTransactionResponse { - return try await self.executeTransaction(calls: [call]) + return StarknetInvokeParamsV1(nonce: nonce, maxFee: maxFee) } func estimateFees() -> Felt { return Felt(fromHex: "0x1000000000000000000")! // 1 ETH } + // invoke + + func executeTransaction(signedTransaction: StarknetInvokeTransactionV1) async throws -> StarknetInvokeTransactionResponse { + return try await self.provider.addInvokeTransaction(signedTransaction) + } + + // sign + + func signTransaction(calls: [StarknetCall]) async throws -> StarknetInvokeTransactionV1 { + let params = try await self.getParams() + + return try self.account.signV1(calls: calls, params: params) + } + + func signTransaction(call: StarknetCall) async throws -> StarknetInvokeTransactionV1 { + return try await self.signTransaction(calls: [call]) + } + + // addr utils + func getAddress(from phoneNumber: String) -> Felt? { guard let phoneNumberFelt = Felt.fromShortString(phoneNumber) else { return nil @@ -301,6 +320,33 @@ extension Model { } func executeTransfer() async { + guard let signedTransaction = self.signedSendingTransaction else { + self.sendingStatus = .error("Invalid request.") + return + } + + self.sendingStatus = .loading + + do { + let result = try await self.executeTransaction(signedTransaction: signedTransaction) + + self.sendingStatus = .success + + #if DEBUG + print("tx: \(result.transactionHash)") + #endif + } catch let error { + self.sendingStatus = .success +// self.sendingStatus = .error("An error has occured during the transaction.") + + #if DEBUG + print(error) + #endif + } + + } + + func signTransfer() async { guard let recipientContact = self.recipientContact, let recipientAddress = self.getAddress(from: recipientContact.phone), @@ -320,19 +366,14 @@ extension Model { ] ) - self.sendingStatus = .loading + self.sendingStatus = .signing do { - let result = try await self.executeTransaction(call: call) + self.signedSendingTransaction = try await self.signTransaction(call: call) - self.sendingStatus = .success - - #if DEBUG - print("tx: \(result.transactionHash)") - #endif + self.sendingStatus = .signed } catch let error { - self.sendingStatus = .success -// self.sendingStatus = .error("An error has occured during the transaction.") + self.sendingStatus = .none #if DEBUG print(error) @@ -407,7 +448,7 @@ extension Model { private func initiateTransfer() { self.recipientContact = nil self.sendingAmount = "0" - self.sendingStatus = .active + self.signedSendingTransaction = nil } private func dismissTransfer() { diff --git a/app/App/Navigation/Core/Home/HomeView.swift b/app/App/Navigation/Core/Home/HomeView.swift index 92927ed4..f1887ab4 100644 --- a/app/App/Navigation/Core/Home/HomeView.swift +++ b/app/App/Navigation/Core/Home/HomeView.swift @@ -131,7 +131,7 @@ struct HomeView: View { Spacer(minLength: 16) IconButtonWithText("Send") { - self.model.showSendingSheet = true + self.model.showSendingView = true } icon: { Image(systemName: "arrow.up") .iconify() @@ -222,7 +222,7 @@ struct HomeView: View { } ) .removeNavigationBarBorder() - .fullScreenCover(isPresented: self.$model.showSendingSheet) { + .fullScreenCover(isPresented: self.$model.showSendingView) { SendingView() } } @@ -242,15 +242,18 @@ struct HomeView: View { } } -#if DEBUG -struct HomeViewPreviews : PreviewProvider { +#Preview { + struct HomeViewPreviews: View { - @StateObject static var model = Model(vaultService: VaultService()) + @StateObject var model = Model(vaultService: VaultService()) - static var previews: some View { - NavigationStack { - HomeView().environmentObject(model) + var body: some View { + NavigationStack { + HomeView() + .environmentObject(self.model) + } } } + + return HomeViewPreviews() } -#endif diff --git a/app/App/Navigation/Core/Sending/ConfirmationView.swift b/app/App/Navigation/Core/Sending/ConfirmationView.swift index ddf7f3ed..6d907f4d 100644 --- a/app/App/Navigation/Core/Sending/ConfirmationView.swift +++ b/app/App/Navigation/Core/Sending/ConfirmationView.swift @@ -17,14 +17,12 @@ struct ConfirmationView: View { let recipientConact = self.model.recipientContact! let usdcAmount = USDCAmount(from: self.model.parsedSendingAmount)! - switch self.model.sendingStatus { - case .none: - EmptyView() + VStack { + Text("Finalize your transfer") + .textTheme(.headlineSmall) + .padding(.vertical, 32) - case .active: - Text("Finalize your transfer").textTheme(.headlineSmall) - - Spacer().frame(height: 24) + Spacer().frame(height: 16) HStack { VStack(spacing: 8) { @@ -96,31 +94,16 @@ struct ConfirmationView: View { .background(.background3.opacity(0.5)) .clipShape(RoundedRectangle(cornerRadius: 10)) - Spacer().frame(height: 64) + Spacer() PrimaryButton("Send") { Task { - await self.model.executeTransfer() + await self.model.signTransfer() } } - - case .loading, .success: - Text("Executing your transfer").textTheme(.headlineSmall) - .onTapGesture { - self.model.sendingStatus = .success - } - - Spacer().frame(height: 32) - - SpinnerView(isComplete: .constant(self.model.sendingStatus == .success)) - - case .error(let message): - Text(message).textTheme(.headlineSmall) - - // TODO: better error display - - Spacer() } + .padding() + .presentationDetents([.medium, .large]) } } @@ -129,9 +112,8 @@ struct ConfirmationView: View { @StateObject var model = { let model = Model(vaultService: VaultService()) - model.setRecipient(Contact(name: "Very Long Bobby Name", phone: "+33612345678")) - model.sendingStatus = .active + model.setRecipient(Contact(name: "Very Long Bobby Name", phone: "+33612345678")) return model }() @@ -140,7 +122,7 @@ struct ConfirmationView: View { VStack {} .frame(maxWidth: .infinity, maxHeight: .infinity) .defaultBackground() - .sheetPopover(isPresented: .constant(true)) { + .sheet(isPresented: .constant(true)) { ConfirmationView() .environmentObject(model) } diff --git a/app/App/Navigation/Core/Sending/ContactRow.swift b/app/App/Navigation/Core/Sending/ContactRow.swift index 997c11fe..3fd8dbf6 100644 --- a/app/App/Navigation/Core/Sending/ContactRow.swift +++ b/app/App/Navigation/Core/Sending/ContactRow.swift @@ -22,7 +22,7 @@ struct ContactRow: View { Spacer() - Text(self.contact.phone).textTheme(.subtitle) + Text(/*self.contact.phone*/"+33612345678").textTheme(.subtitle) } .padding(.vertical, 6) diff --git a/app/App/Navigation/Core/Sending/SendingAmountView.swift b/app/App/Navigation/Core/Sending/SendingAmountView.swift index 55668042..02b33ba9 100644 --- a/app/App/Navigation/Core/Sending/SendingAmountView.swift +++ b/app/App/Navigation/Core/Sending/SendingAmountView.swift @@ -14,8 +14,6 @@ struct SendingAmountView: View { @EnvironmentObject private var model: Model - @State private var showingConfirmation = false - var body: some View { VStack { Spacer() @@ -26,7 +24,7 @@ struct SendingAmountView: View { VStack(spacing: 32) { PrimaryButton("Send", disabled: self.model.parsedSendingAmount <= 0) { - self.showingConfirmation = true + self.model.showSendingConfirmation = true } NumPad(amount: self.$model.sendingAmount) @@ -47,16 +45,52 @@ struct SendingAmountView: View { ) .removeNavigationBarBorder() .navigationBarBackButtonHidden(true) - .sheetPopover(isPresented: self.$showingConfirmation) { + .sheet(isPresented: self.$model.showSendingConfirmation) { + if self.model.sendingStatus == .signed { + Task { + await self.model.executeTransfer() + } + } + } content: { ConfirmationView() } + .sheetPopover(isPresented: .constant(self.model.sendingStatus == .loading || self.model.sendingStatus == .success)) { + + Text("Executing your transfer").textTheme(.headlineSmall) + .onTapGesture { + self.model.sendingStatus = .success + } + + Spacer().frame(height: 32) + + SpinnerView(isComplete: .constant(self.model.sendingStatus == .success)) + } + .onChange(of: self.model.sendingStatus) { + // close confirmation sheet on signing + if self.model.sendingStatus == .signed { + self.model.showSendingConfirmation = false + } else if self.model.sendingStatus == .success { + Task { @MainActor in + try await Task.sleep(for: .seconds(1)) + + self.model.showSendingView = false + } + } + } } } #if DEBUG struct SendingAmountViewPreviews : PreviewProvider { - @StateObject static var model = Model(vaultService: VaultService()) + @StateObject static var model = { + let model = Model(vaultService: VaultService()) + + model.setRecipient(Contact(name: "Very Long Bobby Name", phone: "+33612345678")) + model.sendingStatus = .none + + return model + }() static var previews: some View { SendingAmountView() diff --git a/app/App/Navigation/Core/Sending/SendingRecipientView.swift b/app/App/Navigation/Core/Sending/SendingRecipientView.swift index 1b41d807..8db4960e 100644 --- a/app/App/Navigation/Core/Sending/SendingRecipientView.swift +++ b/app/App/Navigation/Core/Sending/SendingRecipientView.swift @@ -179,7 +179,17 @@ struct SendingRecipientView: View { } #Preview { - NavigationStack { - SendingRecipientView() + struct SendingRecipientViewPreviews: View { + + @StateObject var model = Model(vaultService: VaultService()) + + var body: some View { + NavigationStack { + SendingRecipientView() + .environmentObject(self.model) + } + } } + + return SendingRecipientViewPreviews() }