taler-ios

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

ActionsSheet.swift (10688B)


      1 /*
      2  * This file is part of GNU Taler, ©2022-25 Taler Systems S.A.
      3  * See LICENSE.md
      4  */
      5 /**
      6  * @author Marc Stibane
      7  */
      8 import SwiftUI
      9 import os.log
     10 import taler_swift
     11 
     12 /// This view shows the action sheet
     13 /// optional:   [􀰟Spend KUDOS]
     14 ///    [􁉇Send]  [􁉅Request]
     15 /// [􁾭Withdraw]  [􁾩Deposit]
     16 ///        [􀎻  Scan QR ]
     17 struct ActionsSheet: View {
     18     let stack: CallStack
     19     @Binding var qrButtonTapped: Bool
     20 
     21     @Environment(\.openURL) private var openURL
     22     @EnvironmentObject private var controller: Controller
     23     @AppStorage("minimalistic") var minimalistic: Bool = false
     24     @AppStorage("demoHints") var demoHints: Bool = true
     25 
     26     private var hasKudos: Bool {
     27         for balance in controller.balances {
     28             if balance.scopeInfo.currency == DEMOCURRENCY {
     29                 if !balance.available.isZero {
     30                     return true
     31                 }
     32             }
     33         }
     34         return false
     35     }
     36     private var canSend: Bool {     // canDeposit
     37         for balance in controller.balances {
     38             if !balance.available.isZero {
     39                 return true
     40             }
     41         }
     42         return false
     43     }
     44 
     45     var body: some View {
     46         VStack {
     47             let width = UIScreen.screenWidth / 3
     48             RoundedRectangle(cornerRadius: 8)                                   // dropBar
     49                 .foregroundColor(WalletColors().gray4)
     50                 .frame(width: width + 6.5, height: 5)
     51                 .padding(.top, 5)
     52                 .padding(.bottom, 10)
     53 
     54             if hasKudos && demoHints {
     55                 Text(minimalistic ? "Spend your \(DEMOCURRENCY) in the Demo shop"
     56                                   : "You can spend your \(DEMOCURRENCY) in the Demo shop, or send them to another wallet.")
     57                     .talerFont(.body)
     58                     .multilineTextAlignment(.leading)
     59                     .fixedSize(horizontal: false, vertical: true)           // must set this otherwise fixedInnerHeight won't work
     60 
     61                 let buttonTitle = String(localized: "LinkTitle_DEMOSHOP", defaultValue: "Spend demo money")
     62                 let shopAction = {
     63 //                    demoHints += 1
     64                     UIApplication.shared.open(URL(string:DEMOSHOP)!, options: [:])
     65                     dismissTop(stack.push())
     66                 }
     67                 Button(action: shopAction) {
     68                     HStack {
     69                         ButtonIconBadge(type: .payment, foreColor: .accentColor, done: false)
     70                             .talerFont(.title2)
     71                         Spacer()
     72                         Text(buttonTitle)
     73                         Spacer()
     74                     }
     75                 }
     76                 .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, aligned: .center))
     77                 .accessibilityHint(String(localized: "Will go to the demo shop website.", comment: "a11y"))
     78                 .accessibilityAddTraits(.isLink)
     79                 .padding(.bottom, 20)
     80             }
     81             if !controller.scannedURLs.isEmpty {
     82                 Text("Scanned Taler codes:")
     83                     .talerFont(.body)
     84                     .multilineTextAlignment(.leading)
     85                     .fixedSize(horizontal: false, vertical: true)
     86                     .padding(.bottom, -2)
     87                 VStack {
     88                     ForEach(controller.scannedURLs) { scannedURL in
     89                         let action = {
     90                             dismissTop(stack.push())
     91                             DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
     92                                 // need to wait before opening the next sheet, otherwise SwiftUI doesn't update!
     93                                 openURL(scannedURL.url)
     94                             }
     95                         }
     96                         Button(action: action) {
     97                             HStack {
     98                                 Text(scannedURL.command.localizedCommand)
     99                                 ButtonIconBadge(type: scannedURL.command.transactionType,
    100                                            foreColor: .accentColor, done: false)
    101                                     .talerFont(.title2)
    102 
    103                                 Spacer()
    104                                 if let amount = scannedURL.amount {
    105                                     AmountV(scannedURL.scope, amount, isNegative: nil)
    106                                 } else if let baseURL = scannedURL.baseURL {
    107                                     Text(baseURL.trimURL)
    108                                 }
    109                             }
    110                             .padding(.horizontal, 8)
    111                         }
    112                         .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, aligned: .center))
    113                         .swipeActions(edge: .trailing) {
    114                             Button {
    115 //                                symLog?.log("deleteAction")
    116                                 Task { // runs on MainActor
    117                                     controller.removeURL(scannedURL.url)
    118                                 }
    119                             } label: {
    120                                 Label("Delete", systemImage: "trash")
    121                             }
    122                             .tint(WalletColors().negative)
    123                         }
    124 
    125                     }
    126                 }.padding(.bottom, 20)
    127             }
    128 
    129             let sendDisabled = !canSend
    130             let recvDisabled = controller.balances.count == 0
    131             SendRequestV(stack: stack.push(), sendDisabled: sendDisabled, recvDisabled: recvDisabled)
    132             DepositWithdrawV(stack: stack.push(), sendDisabled: sendDisabled, recvDisabled: recvDisabled)
    133             QRButton(hideTitle: false) {
    134                 qrButtonTapped = true
    135             }
    136                 .lineLimit(5)
    137                 .buttonStyle(TalerButtonStyle(type: .bordered, narrow: true, aligned: .center))
    138         }
    139         .padding()
    140     }
    141 }
    142 // MARK: -
    143 @available(iOS 16.4, *)
    144 struct DualHeightSheet: View {
    145     let stack: CallStack
    146     let selectedBalance: Balance?
    147     @Binding var qrButtonTapped: Bool
    148     let dismissScanner: () -> Void
    149 
    150     let logger = Logger(subsystem: "net.taler.gnu", category: "DualSheet")
    151     @Environment(\.colorScheme) private var colorScheme
    152     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
    153     let scanDetent: PresentationDetent = .fraction(SCANDETENT)
    154 //    @State private var selectedDetent: PresentationDetent = scanDetent        // Cannot use instance member 'scanDetent' within property initializer
    155     @State private var selectedDetent: PresentationDetent = .fraction(0.1)      // workaround - update in .task
    156 //    @State private var detents: Set<PresentationDetent> = [scanDetent]
    157     @State private var detents: Set<PresentationDetent> = [.fraction(0.1)]
    158     @State private var qrButtonTapped2: Bool = false
    159     @State private var innerHeight: CGFloat = .zero
    160     @State private var sheetHeight: CGFloat = .zero
    161 
    162     func updateDetentsWithDelay() {
    163         Task {
    164             //(1 second = 1_000_000_000 nanoseconds)
    165             try? await Task.sleep(nanoseconds: 80_000_000)
    166             guard selectedDetent == scanDetent else { return }
    167             detents = [scanDetent]
    168         }
    169     }
    170 
    171     var body: some View {
    172         let scrollView = ScrollView {
    173             let background = colorScheme == .dark ? WalletColors().gray6
    174                                                   : WalletColors().gray2
    175             ActionsSheet(stack: stack.push(),
    176                 qrButtonTapped: $qrButtonTapped2)
    177             .presentationDragIndicator(.hidden)
    178             .presentationBackground {
    179                 background
    180                 /// overflow the bottom of the screen by a sufficient amount to fill the gap that is seen when the size changes
    181                     .padding(.bottom, -1000)
    182             }
    183             .innerHeight($innerHeight)
    184 
    185             .onChange(of: qrButtonTapped2) { tapped2 in
    186                 if tapped2 {
    187 //                    logger.trace("❗️the user tapped")
    188                     qrButtonTapped = true           // tell our caller
    189                     withAnimation(Animation.easeIn(duration: 0.6)) {
    190                         // animate this sheet to full height
    191                         selectedDetent = scanDetent
    192                     }
    193                 }
    194             }
    195             .onChange(of: qrButtonTapped) { tapped in
    196                 if !tapped {
    197                     logger.trace("❗️dismissed, cleanup")
    198                     sheetHeight = innerHeight
    199                     qrButtonTapped2 = false
    200                 }
    201             }
    202             .onChange(of: innerHeight) { newHeight in
    203                 logger.trace("onChange❗️set sheetHeight: \(sheetHeight) -> \(newHeight)❗️")
    204 //            withAnimation {
    205                 sheetHeight = newHeight
    206                 selectedDetent = .height(sheetHeight)       // will update detents in .onChange(:)
    207 //            }
    208             }
    209             .presentationDetents(detents, selection: $selectedDetent)
    210             .onChange(of: selectedDetent) { newValue in
    211                 if newValue == scanDetent {    // user swiped the sheet up to full height
    212                     logger.trace("onChange❗️selectedDetent = \(SCANDETENT)")
    213                     updateDetentsWithDelay()
    214                     qrButtonTapped = true           // tell our caller
    215                 } else {    // SwiftUI "innerHeight" determined how big the half sheet should be
    216                     logger.trace("onChange❗️selectedDetent = .height(\(sheetHeight))")
    217                     detents = [scanDetent, .height(sheetHeight)]
    218                 }
    219             }
    220             .task {
    221                 logger.trace("task❗️selectedDetent = .height(\(sheetHeight))")
    222                 selectedDetent = .height(sheetHeight)       // will update detents in .onChange(:)
    223             }
    224 //            .animation(.spring(), value: selectedDetent)
    225         }
    226         .edgesIgnoringSafeArea(.all)
    227         .frame(maxHeight: innerHeight)
    228 
    229         if #available(iOS 18.0, *) {
    230             scrollView
    231                 .sheet(isPresented: $qrButtonTapped,
    232                          onDismiss: dismissScanner
    233                 ) {
    234                     let qrSheet = AnyView(QRSheet(stack: stack.push(".sheet"),
    235                                         selectedBalance: selectedBalance))
    236                     let scanDetent: PresentationDetent = .fraction(SCANDETENT)
    237                     Sheet(stack: stack.push(), sheetView: qrSheet)
    238                         .presentationDetents([scanDetent])
    239                         .transition(.opacity)
    240 
    241                 }
    242         } else {
    243             scrollView
    244         }
    245     }
    246 }