taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

commit 6ce5c6445242a3e8fed7d39d2ecdcd776445a6e2
parent 7ed1053d809cbfaefaa1bb744631b3e5ac144b99
Author: Marc Stibane <marc@taler.net>
Date:   Sat, 12 Oct 2024 20:56:42 +0200

DualHeightSheet

Diffstat:
MTalerWallet1/Views/Actions/ActionsSheet.swift | 120++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
MTalerWallet1/Views/Actions/DepositWithdrawV.swift | 64++++++++++++----------------------------------------------------
MTalerWallet1/Views/Actions/SendRequestV.swift | 91+++++++++++--------------------------------------------------------------------
MTalerWallet1/Views/HelperViews/View+fixedInnerHeight.swift | 9+++------
MTalerWallet1/Views/Main/MainView.swift | 100+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
5 files changed, 188 insertions(+), 196 deletions(-)

diff --git a/TalerWallet1/Views/Actions/ActionsSheet.swift b/TalerWallet1/Views/Actions/ActionsSheet.swift @@ -13,12 +13,11 @@ import taler_swift /// optional: [􀰟Spend KUDOS] /// [􁉇Send] [􁉅Request] /// [􁾭Withdraw] [􁾩Deposit] +/// [􀎻 Scan QR ] struct ActionsSheet: View { let stack: CallStack @Binding var balances: [Balance] - @Binding var showSpendingHint: Bool - @Binding var amountToTransfer: Amount // does still have the wrong currency - @Binding var summary: String + @Binding var qrButtonTapped: Bool @AppStorage("minimalistic") var minimalistic: Bool = false @AppStorage("hideSpendingHint") var hideSpendingHint: Bool = false @@ -33,20 +32,22 @@ struct ActionsSheet: View { } return false } - - @MainActor - func qrButtonTapped() { - dismissTop(stack.push()) - NotificationCenter.default.post(name: .QrScanAction, - object: nil) // will trigger NavigationLink + private var canSend: Bool { // canDeposit + for balance in balances { + if !balance.available.isZero { + return true + } + } + return false } var body: some View { VStack { let width = UIScreen.screenWidth / 3 RoundedRectangle(cornerRadius: 8) // dropBar - .foregroundColor(.primary) + .foregroundColor(WalletColors().gray4) .frame(width: width + 6.5, height: 5) + .padding(.top, 5) .padding(.bottom, 10) if hasKudos && !hideSpendingHint { @@ -57,12 +58,12 @@ struct ActionsSheet: View { .fixedSize(horizontal: false, vertical: true) // must set this otherwise fixedInnerHeight won't work let title = String(localized: "LinkTitle_DEMOSHOP", defaultValue: "Spend demo money") - let action = { + let shopAction = { hideSpendingHint = true UIApplication.shared.open(URL(string:DEMOSHOP)!, options: [:]) dismissTop(stack.push()) } - Button(action: action) { + Button(action: shopAction) { HStack { ButtonIconBadge(type: .payment, foreColor: .accentColor, done: false) Spacer() @@ -75,19 +76,100 @@ struct ActionsSheet: View { .padding(.bottom, 20) } - QRButton(isNavBarItem: false, action: qrButtonTapped) + let sendDisabled = !canSend + SendRequestV(stack: stack.push(), sendDisabled: sendDisabled) + DepositWithdrawV(stack: stack.push(), sendDisabled: sendDisabled) + QRButton(isNavBarItem: false) { + qrButtonTapped = true + } .lineLimit(5) .buttonStyle(TalerButtonStyle(type: .bordered, narrow: true, aligned: .center)) + } + .padding() + } +} +// MARK: - +@available(iOS 16.4, *) +struct DualHeightSheet: View { + let stack: CallStack + @Binding var balances: [Balance] + @Binding var qrButtonTapped: Bool + + let logger = Logger(subsystem: "net.taler.gnu", category: "DualSheet") +// @State private var sheetHeight: CGFloat = .zero + @State private var selectedDetent: PresentationDetent = .fraction(0.1) + @State private var detents: Set<PresentationDetent> = [.fraction(0.1)] +// @State private var selectedDetent: PresentationDetent = .large +// @State private var detents: Set<PresentationDetent> = [.large] + @State private var qrButtonTapped2: Bool = false + @State private var innerHeight: CGFloat = .zero + @State private var sheetHeight: CGFloat = .zero - SendRequestV(stack: stack.push(), + func updateDetentsWithDelay() { + Task { + //(1 second = 1_000_000_000 nanoseconds) + try? await Task.sleep(nanoseconds: 80_000_000) + guard selectedDetent == .large else { return } + detents = [.large] + } + } + + var body: some View { + ScrollView { + ActionsSheet(stack: stack.push(), balances: $balances, - amountToTransfer: $amountToTransfer, // does still have the wrong currency - summary: $summary) + qrButtonTapped: $qrButtonTapped2) + .presentationDragIndicator(.hidden) + .presentationBackground { + WalletColors().gray2 + /// overflow the bottom of the screen by a sufficient amount to fill the gap that is seen when the size changes + .padding(.bottom, -1000) + } + .innerHeight($innerHeight) - DepositWithdrawV(stack: stack.push(), - balances: $balances, - amountToTransfer: $amountToTransfer) // does still have the wrong currency + .onChange(of: qrButtonTapped2) { tapped2 in + if tapped2 { + logger.log("❗️the user tapped") + qrButtonTapped = true // tell our caller + withAnimation(Animation.easeIn(duration: 0.6)) { + // animate this sheet to full height + selectedDetent = .large + } + } + } + .onChange(of: qrButtonTapped) { tapped in + if !tapped { + logger.log("❗️dismissed, cleanup") + sheetHeight = innerHeight + qrButtonTapped2 = false + } + } + .onChange(of: innerHeight) { newHeight in + logger.log("onChange❗️set sheetHeight: \(sheetHeight) -> \(newHeight)❗️") +// withAnimation { + sheetHeight = newHeight + selectedDetent = .height(sheetHeight) // will update detents in .onChange(:) +// } + } + .presentationDetents(detents, selection: $selectedDetent) + .onChange(of: selectedDetent) { newValue in + if newValue == .large { + logger.log("onChange❗️selectedDetent = .large)") + updateDetentsWithDelay() + qrButtonTapped = true // tell our caller + } else { + logger.log("onChange❗️selectedDetent = .height(\(sheetHeight))") + detents = [.large, .height(sheetHeight)] + } + } + .task { + logger.log("task❗️selectedDetent = .height(\(sheetHeight))") + selectedDetent = .height(sheetHeight) // will update detents in .onChange(:) + } +// .animation(.spring(), value: selectedDetent) } + .edgesIgnoringSafeArea(.all) + .frame(maxHeight: innerHeight) } } // MARK: - diff --git a/TalerWallet1/Views/Actions/DepositWithdrawV.swift b/TalerWallet1/Views/Actions/DepositWithdrawV.swift @@ -12,72 +12,32 @@ import SymLog struct DepositWithdrawV: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var balances: [Balance] -// @Binding var currencyInfo: CurrencyInfo -// let amountAvailable: Amount? -// let currency: String // this is the currency to be used - @Binding var amountToTransfer: Amount // does still have the wrong currency + let sendDisabled: Bool @EnvironmentObject private var model: WalletModel @AppStorage("minimalistic") var minimalistic: Bool = false - @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) - @State private var currencyName: String = UNKNOWN - @State private var currencySymbol: String = UNKNOWN - @State private var amountAvailable = Amount.zero(currency: EMPTYSTRING) // Update currency when used func selectAndUpdate(_ button: Int) { - let scope = currencyInfo.scope - let currency = scope.currency - amountToTransfer.setCurrency(currency) dismissTop(stack.push()) NotificationCenter.default.post(name: button == 3 ? .DepositAction : .WithdrawAction, object: nil) // will trigger NavigationLink - // after user tapped a button, while navigation animation runs, contact Exchange to update Fees - if let url = scope.url { - Task { // runs on MainActor - do { - // TODO: try await model.updateExchange(scopeInfo: scope) - try await model.updateExchange(exchangeBaseUrl: url) - } catch { // TODO: error handling - couldn't updateExchange - symLog.log("error: \(error)") - } - } - } - } - - private var canDeposit: Bool { - for balance in balances { - if !balance.available.isZero { - return true - } - } - return false } var body: some View { - let scope = currencyInfo.scope - let currencyName = currencyInfo.specs.name let depositTitle = String(localized: "DepositButton_Short", defaultValue: "Deposit", comment: "Abbreviation of button `Deposit (currency)´") let withdrawTitle = String(localized: "WithdrawButton_Short", defaultValue: "Withdraw", comment: "Abbreviation of button `Withdraw (currency)´") - let twoRowButtons = TwoRowButtons(stack: stack.push(), - sendTitle: depositTitle, - sendType: .deposit, - sendA11y: depositTitle,//1.tabbed(oneLine: true), - recvTitle: withdrawTitle, - recvType: .withdrawal, - recvA11y: withdrawTitle,//1.tabbed(oneLine: true), - lineLimit: 5, - sendDisabled: !canDeposit, - sendAction: { selectAndUpdate(3) }, - recvAction: { selectAndUpdate(4) }) - Group { - if #available(iOS 16.0, *) { - twoRowButtons - } else { // view for iOS 15 - twoRowButtons - } - } + TwoRowButtons(stack: stack.push(), + sendTitle: depositTitle, + sendType: .deposit, + sendA11y: depositTitle,//1.tabbed(oneLine: true), + recvTitle: withdrawTitle, + recvType: .withdrawal, + recvA11y: withdrawTitle,//1.tabbed(oneLine: true), + lineLimit: 5, + sendDisabled: sendDisabled, + sendAction: { selectAndUpdate(3) }, + recvAction: { selectAndUpdate(4) }) } } diff --git a/TalerWallet1/Views/Actions/SendRequestV.swift b/TalerWallet1/Views/Actions/SendRequestV.swift @@ -12,100 +12,33 @@ import SymLog struct SendRequestV: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var balances: [Balance] -// @Binding var currencyInfo: CurrencyInfo -// let amountAvailable: Amount - // let currency: String // this is the currency to be used - @Binding var amountToTransfer: Amount // does still have the wrong currency - @Binding var summary: String + let sendDisabled: Bool @EnvironmentObject private var model: WalletModel @EnvironmentObject private var controller: Controller @AppStorage("minimalistic") var minimalistic: Bool = false - @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) - @State private var currencyName: String = UNKNOWN - @State private var currencySymbol: String = UNKNOWN - @State private var amountAvailable = Amount.zero(currency: EMPTYSTRING) // Update currency when used func selectAndUpdate(_ button: Int) { - let scope = currencyInfo.scope - let currency = scope.currency - amountToTransfer.setCurrency(currency) dismissTop(stack.push()) NotificationCenter.default.post(name: button == 1 ? .SendAction : .RequestAction, object: nil) // will trigger NavigationLink - // after user tapped a button, while navigation animation runs, contact Exchange to update Fees - if let url = scope.url { - Task { // runs on MainActor - do { - // TODO: try await model.updateExchange(scopeInfo: scope) - try await model.updateExchange(exchangeBaseUrl: url) - } catch { // TODO: error handling - couldn't updateExchange - symLog.log("error: \(error)") - } - } - } - } - - private var canSend: Bool { - for balance in balances { - if !balance.available.isZero { - return true - } - } - return false } var body: some View { - let scope = currencyInfo.scope - let currencyName = currencyInfo.specs.name let sendTitle = String(localized: "SendButton_Short", defaultValue: "Send", comment: "Abbreviation of button `Send (currency)´") let requTitle = String(localized: "RequestButton_Short", defaultValue: "Request", comment: "Abbreviation of button `Request (currency)´") - let twoRowButtons = TwoRowButtons(stack: stack.push(), - sendTitle: sendTitle, - sendType: .peerPushDebit, - sendA11y: sendTitle,//1.tabbed(oneLine: true), - recvTitle: requTitle, - recvType: .peerPullCredit, - recvA11y: requTitle,//1.tabbed(oneLine: true), - lineLimit: 5, - sendDisabled: !canSend, - sendAction: { controller.frontendState = -1; selectAndUpdate(1) }, - recvAction: { controller.frontendState = 1; selectAndUpdate(2) }) - Group { - if #available(iOS 16.0, *) { - twoRowButtons - } else { // view for iOS 15 - twoRowButtons - } - } - } -} -// MARK: - -#if false -struct SendRequestV_Previews: PreviewProvider { - @MainActor - struct StateContainer: View { - var body: some View { - let test = Amount(currency: TESTCURRENCY, cent: 123) - let demo = Amount(currency: DEMOCURRENCY, cent: 123456) - - List { - Section { - SendRequestV(stack: CallStack("Preview"), currencyName: DEMOCURRENCY, amount: demo, - sendAction: {}, recvAction: {}, rowAction: {}, balanceDest: nil) - } - SendRequestV(stack: CallStack("Preview"), currencyName: TESTCURRENCY, amount: test, - sendAction: {}, recvAction: {}, rowAction: {}, balanceDest: nil) - } - } - } - - static var previews: some View { - StateContainer() -// .environment(\.sizeCategory, .extraExtraLarge) Canvas Device Settings + TwoRowButtons(stack: stack.push(), + sendTitle: sendTitle, + sendType: .peerPushDebit, + sendA11y: sendTitle,//1.tabbed(oneLine: true), + recvTitle: requTitle, + recvType: .peerPullCredit, + recvA11y: requTitle,//1.tabbed(oneLine: true), + lineLimit: 5, + sendDisabled: sendDisabled, + sendAction: { controller.frontendState = -1; selectAndUpdate(1) }, + recvAction: { controller.frontendState = 1; selectAndUpdate(2) }) } } -#endif diff --git a/TalerWallet1/Views/HelperViews/View+fixedInnerHeight.swift b/TalerWallet1/Views/HelperViews/View+fixedInnerHeight.swift @@ -31,18 +31,15 @@ struct InnerHeightPreferenceKey: PreferenceKey { } extension View { - @available(iOS 16.0, *) - func fixedInnerHeight(_ sheetHeight: Binding<CGFloat>) -> some View { - padding() - .overlay { // .background doesn't work + func innerHeight(_ height: Binding<CGFloat>) -> some View { + overlay { // background doesn't work for sheets GeometryReader { proxy in Color.clear.preference(key: InnerHeightPreferenceKey.self, value: proxy.size.height) } } .onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in // print("InnerHeightPreferenceKey \(newHeight)") - sheetHeight.wrappedValue = newHeight + height.wrappedValue = newHeight } - .presentationDetents([.height(sheetHeight.wrappedValue)]) } } diff --git a/TalerWallet1/Views/Main/MainView.swift b/TalerWallet1/Views/Main/MainView.swift @@ -40,12 +40,13 @@ struct MainView: View { @State private var balances: [Balance] = [] @State private var selectedBalance: Balance? = nil - @State private var sheetPresented = false @State private var urlToOpen: URL? = nil - @State private var showQRScanner: Bool = false - @State private var showCameraAlert: Bool = false + @State private var sheetPresented = false @State private var showActionSheet = false @State private var showScanner = false +// @State private var showCameraAlert: Bool = false + @State private var qrButtonTapped = false + @State private var innerHeight: CGFloat = .zero func sheetDismissed() -> Void { logger.info("sheet dismiss") @@ -73,18 +74,12 @@ struct MainView: View { showScanner: $showScanner) .onAppear() { #if DEBUG - if playSoundsI != 0 && playSoundsB && !soundPlayed { - controller.playSound(1008) // Startup chime - } -#endif - soundPlayed = true + if playSoundsI != 0 && playSoundsB && !soundPlayed { + controller.playSound(1008) // Startup chime } - .sheet(isPresented: $showQRScanner, onDismiss: dismissingSheet) { - let sheet = AnyView(QRSheet(stack: stack.push(".sheet"), - balances: $balances, - selectedBalance: $selectedBalance)) - Sheet(stack: stack.push(), sheetView: sheet) - } // sheet +#endif + soundPlayed = true + } } else if controller.backendState == .error { ErrorView(errortext: nil) // TODO: show Error View } else { @@ -120,6 +115,51 @@ struct MainView: View { }.interactiveDismissDisabled() } } + .onChange(of: qrButtonTapped) { tapped in + if tapped { + let delay = if #available(iOS 16.4, *) { 0.5 } else { 0.01 } + withAnimation(Animation.easeOut(duration: 0.5).delay(delay)) { + showScanner = true // switch to qrSheet => camera on + } } } + .sheet(isPresented: $showActionSheet, + onDismiss: { showScanner = false; qrButtonTapped = false } + ) { + let qrSheet = AnyView(QRSheet(stack: stack.push(".sheet"), + balances: $balances, + selectedBalance: $selectedBalance)) + if #available(iOS 16.4, *) { + if showScanner { + let _ = logger.log("❗️showScanner: .large❗️") + Sheet(stack: stack.push(), sheetView: qrSheet) + .presentationDetents([.large]) + .transition(.opacity) + } else { + let _ = logger.log("❗️actionsSheet: small❗️") + DualHeightSheet(stack: stack.push(), + balances: $balances, + qrButtonTapped: $qrButtonTapped) + } + } else { + if showScanner { + Sheet(stack: stack.push(), sheetView: qrSheet) + .transition(.opacity) + } else { + Group { + Spacer() + ScrollView { + ActionsSheet(stack: stack.push(), + balances: $balances, + qrButtonTapped: $qrButtonTapped) + .innerHeight($innerHeight) +// .padding() + } + .frame(maxHeight: innerHeight) + .edgesIgnoringSafeArea(.all) + } + .background(WalletColors().gray2) + } + } + } } // body } // MARK: - TabBar @@ -142,7 +182,6 @@ enum Tab: String, Hashable, CaseIterable { switch self { case .balances: return String(localized: "TitleBalances", defaultValue: "Balances") case .actions: return String(localized: "TitleActions", defaultValue: "Actions") -// case .overview: return String(localized: "TitleOverview", defaultValue: "Overview") case .settings: return String(localized: "TitleSettings", defaultValue: "Settings") } } @@ -178,6 +217,7 @@ extension MainView { } } } + struct Content: View { let logger: Logger let stack: CallStack @@ -206,11 +246,9 @@ extension MainView { @State private var showKycAlert: Bool = false @State private var kycURI: URL? - @State private var sheetHeight: CGFloat = .zero @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used @State private var amountLastUsed = Amount.zero(currency: EMPTYSTRING) // Update currency when used @State private var summary: String = EMPTYSTRING - @State private var showSpendingHint = true @State private var myExchange: Exchange? = nil private var openKycButton: some View { @@ -281,8 +319,8 @@ extension MainView { // ViewState.shared.popToRootView(nil) either do this after any of the buttons was operated, or don't do it at all showActionSheet = true } onActionDrag: { -// showScanner = true -// showActionSheet = true + showScanner = true + showActionSheet = true } let scope = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange, @@ -361,7 +399,7 @@ extension MainView { // .padding(.bottom, 10) tabBarView .ignoresSafeArea(.keyboard, edges: .bottom) - } + } // ZStack .frame(maxWidth: .infinity, maxHeight: .infinity) .onNotification(.KYCrequired) { notification in @@ -383,30 +421,12 @@ extension MainView { actions: { openKycButton dismissAlertButton }, message: { Text("Tap the button to go to the KYC website.") }) -// .sheet(isPresented: $showActionSheet, onDismiss: { tabBarView.show() }) { - .sheet(isPresented: $showActionSheet) { - let content = VStack { - ActionsSheet(stack: stack.push(), - balances: $balances, - showSpendingHint: $showSpendingHint, - amountToTransfer: $amountToTransfer, - summary: $summary) - .padding(.bottom, 32) - } - if #available(iOS 16, *) { - content.fixedInnerHeight($sheetHeight) - } else { - content - } - } .onNotification(.BalanceChange) { notification in - // reload balances on receiving BalanceChange notification ... - logger.info(".onNotification(.BalanceChange) ==> reload") + logger.info(".onNotification(.BalanceChange) ==> reload balances") shouldReloadBalances += 1 } .onNotification(.TransactionExpired) { notification in - // reload balances on receiving TransactionExpired notification ... - logger.info(".onNotification(.TransactionExpired) ==> reload") + logger.info(".onNotification(.TransactionExpired) ==> reload balances") shouldReloadBalances += 1 shouldReloadPending += 1 }