taler-ios

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

OIMbalances.swift (13324B)


      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 taler_swift
     10 
     11 enum OIMbalancesState {
     12     case chestsClosed
     13     case chestClosing
     14     case chestOpenTapped
     15     case chestIsOpen
     16 
     17     case sendTapped
     18     case sending
     19 
     20     case requestTapped
     21     case requesting
     22 
     23     case balanceTapped
     24     case historyShown
     25     case historyTapped
     26 }
     27 
     28 // MARK: -
     29 // called by BalancesListView
     30 // shows one savings box per currency
     31 @available(iOS 16.4, *)
     32 struct OIMbalances: View {
     33     let stack: CallStack
     34 //    let decimal: Int            // 0 for ¥,HUF;   2 for $,€,£;   3 for ﷼,₯ (arabic)
     35     @Binding var selectedBalance: Balance?              // return user's choice
     36     @Binding var historyTapped: Int?
     37     let oimEuro: Bool
     38 
     39     @EnvironmentObject private var controller: Controller
     40     @EnvironmentObject private var wrapper: NamespaceWrapper
     41 
     42     @StateObject private var cash: OIMcash
     43     @State private var availableVal: UInt64 = 0
     44     @State private var tappedVal: UInt64 = 0            // unused, canEdit == false
     45     @State private var available: Amount? = nil
     46     @State private var viewState: OIMbalancesState = .chestsClosed
     47     @State private var closing = false                  // debounce tap (on open chest to close it)
     48     @State private var balanceIndex: Int? = nil
     49 
     50     init(stack: CallStack,
     51           selectedBalance: Binding<Balance?>,
     52             historyTapped: Binding<Int?>,
     53                   oimEuro: Bool
     54     ) {
     55         self.stack = stack
     56         self._selectedBalance = selectedBalance
     57         self._historyTapped = historyTapped
     58         self.oimEuro = oimEuro
     59         let oimCurrency = oimCurrency(selectedBalance.wrappedValue?.scopeInfo, oimEuro: oimEuro)  // might be nil ==> OIMeuros
     60         let oimCash = OIMcash(oimCurrency)
     61         self._cash = StateObject(wrappedValue: { oimCash }())
     62     }
     63 
     64     func requestTapped() {
     65 
     66     }
     67 
     68     func sendTapped() {
     69         withAnimation(.basicFast) {
     70             viewState = .sendTapped
     71         }
     72         cash.flyOneByOne(to: .drawer)
     73         withAnimation(.basic1.delay(0.6)) {
     74             viewState = .sending    // go to edit view, blend in missing denominations in drawer
     75         }
     76         DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
     77             let actionType = ActionType(animationDisabled: true)
     78             let userinfo = [NOTIFICATIONANIMATION: actionType]
     79             // will trigger NavigationLink
     80             NotificationCenter.default.post(name: .SendAction,          // switch to OIMEditView
     81                                           object: nil,
     82                                         userInfo: userinfo)
     83         }
     84     }
     85 
     86     func closeChest() {
     87         if !closing {
     88             closing = true
     89             let chestBeingClosed = selectedBalance   // so a stale timer can't stomp on a chest opened later
     90             viewState = .chestClosing
     91             let delay = cash.flyOneByOne(to: .curve)        // back to chest...
     92             DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
     93                 print("closeChest", delay)
     94                 if selectedBalance == chestBeingClosed {    // still the same chest? otherwise a newer
     95                     withAnimation(.basic1) {                // openChest() already replaced it - leave it alone
     96                         print("🚩OIMbalances.closeChest() reset selectedBalance")
     97                         selectedBalance = nil
     98                         available = nil
     99                         viewState = .chestsClosed
    100                     }
    101                 }
    102                 closing = false
    103             }
    104         }
    105     }
    106 
    107     func openChest(_ oimCurrency: OIMcurrency, _ index: Int, _ balance: Balance) {
    108         cash.clearFunds()
    109         print("❗️openChest❗️")
    110         let duration: TimeInterval
    111         let initial: TimeInterval
    112 #if DEBUG
    113         duration = debugAnimations ? 2.5 :
    114                     fastAnimations ? 0.6 : 1.1
    115         initial = debugAnimations ? 1.0 : 0.1
    116 #else
    117         duration = fastAnimations ? 0.6 : 1.1
    118         initial = 0.1
    119 #endif
    120         viewState = .chestOpenTapped
    121         withAnimation(.basic1) {
    122             print("🚩OIMbalances.openChest() set selectedBalance to", balance.scopeInfo.currency)
    123             selectedBalance = balance
    124             balanceIndex = index
    125             viewState = .chestIsOpen
    126             cash.setCurrency(oimCurrency)
    127             available = balance.available
    128             availableVal = balance.available.centValue                          // TODO: centValue factor
    129             cash.update2(availableVal, state: .chestOpening, duration, initial)  // set cash to available
    130             let maxAvailable = cash.max(available: availableVal)
    131             print("OIMView.openChest availableVal", availableVal, maxAvailable)
    132         }
    133     }
    134 
    135 //    func closeHistory() {
    136 //        withAnimation(.basic1) {
    137 //            viewState = .historyTapped
    138 //        }
    139 //        let delay = cash.flyOneByOne(to: .idle)             // back to center
    140 //        DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
    141 //            print("closeHistory", delay)
    142 //            withAnimation(.basic1) {
    143 //                viewState = .chestIsOpen
    144 //            }
    145 //        }
    146 //    }
    147 
    148     func openHistory() {
    149         viewState = .balanceTapped
    150         let delay = cash.flyOneByOne(to: .history, true)
    151         DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
    152             print("openHistory", delay)
    153             withAnimation(.basic1) {
    154                 viewState = .historyShown
    155                 DispatchQueue.main.asyncAfter(deadline: .now() + Animation.talerDuration2) {
    156                     var transaction = Transaction()
    157                     transaction.disablesAnimations = true
    158                     withTransaction(transaction) {
    159                         historyTapped = balanceIndex          // ==> go to transaction list
    160                     }
    161                     DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    162                         cash.moveBack()
    163                         viewState = .chestIsOpen
    164                     }
    165                 }
    166             }
    167         }
    168     }
    169 
    170     func initView() {
    171         availableVal = 0
    172         cash.update2(availableVal)           // set cash to 0
    173         viewState = .chestsClosed
    174     }
    175 
    176     var body: some View {
    177         var debugTick = 0
    178 //        let _ = Self._printChanges()
    179 
    180         let enabled = if let available {
    181             !available.isZero
    182         } else { false }
    183         let showSend = viewState == .chestIsOpen
    184 #if DEBUG
    185         let showRequest = viewState == .chestIsOpen
    186 #else
    187         let showRequest = false
    188 #endif
    189         let topButtons = HStack(alignment: .top) {
    190             if selectedBalance == nil {
    191                 let showQR = viewState == .chestsClosed
    192                 QRButton(hideTitle: true) {
    193                     NotificationCenter.default.post(name: .QrScanAction, object: nil)   // will trigger NavigationLink
    194                 }
    195                 .opacity(showQR ? 1 : INVISIBLE)
    196                 .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    197                 .matchedGeometryEffect(id: OIMBACK, in: wrapper.namespace, isSource: true)
    198             } else {
    199 
    200             }
    201             Spacer()
    202             VStack {
    203                 OIMactionButton(type: .sendP2P, isFinal: false, action: sendTapped)
    204                     .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    205                     .opacity(showSend ? 1 : INVISIBLE)
    206                 OIMactionButton(type: .requestP2P, isFinal: false, action: requestTapped)
    207                     .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    208                     .opacity(showRequest ? 1 : INVISIBLE)
    209             }
    210         }
    211 
    212         let maxAvailable = cash.max(available: available?.centValue ?? 0)       // TODO: centValue factor
    213 //        let _ = print("maxAvailable", maxAvailable)
    214 
    215         let sidePosition = HStack {
    216             Spacer()
    217             Color.clear
    218                 .frame(width: 80, height: 80)
    219                 .matchedGeometryEffect(id: OIMSIDE, in: wrapper.namespace, isSource: true)
    220         }
    221         OIMbackground() {
    222             ZStack(alignment: .top) {
    223                 topButtons
    224                 VStack {
    225                     // balance, amountToSend
    226                     OIMtitleView(cash: cash,
    227                                amount: available,
    228                               history: viewState == .historyShown,
    229                          secondAmount: nil)             // appears in OIMEditView
    230                     Spacer()
    231                     let isOpen = selectedBalance != nil
    232                     ZStack {
    233                         sidePosition
    234 //                        let scaleMoney = viewState == .chestIsOpen
    235                         OIMlineView(stack: stack.push(),
    236                                      cash: cash,
    237                                 amountVal: $availableVal,
    238                                   canEdit: false)
    239                             .opacity(isOpen ? 1 : INVISIBLE)
    240 //                            .scaleEffect(scaleMoney ? 0.6 : 1)
    241                             .onTapGesture {
    242                                 openHistory()
    243                             }
    244                     }
    245                     Spacer()
    246                 } // title on top, money in the middle
    247 
    248                 VStack {
    249                     // multiple savings chests (Euro, Sierra Leone, Côte d'Ivoire)
    250                     Spacer()
    251                     HStack(spacing: 30) {
    252                         ForEachWithIndex(array: controller.balances) { index, balance in
    253                             let oimCurrency = oimCurrency(balance.scopeInfo, oimEuro: oimEuro)
    254                             let itsMe = selectedBalance == balance
    255                             let isClosed = selectedBalance == nil
    256                             let size = isClosed ? 160.0 : OIMbuttonSize
    257                             ZStack {
    258                                 OIMbalanceButton(isOpen: itsMe, chest: oimCurrency.chest, isFinal: false) {
    259                                     if itsMe {
    260                                         closeChest()
    261                                     } else {
    262                                         openChest(oimCurrency, index, balance)
    263                                     }
    264                                 }
    265                                 .frame(width: size, height: size)
    266                                 .zIndex(itsMe ? 3 : 0)
    267                                 .opacity((isClosed || itsMe) ? 1 : INVISIBLE)
    268                                 .disabled(!(isClosed || itsMe))     // disable invisible
    269                                 .matchedGeometryEffect(id: itsMe ? OIMNUMBER // (sending ? OIMBACK : OIMNUMBER)
    270 //                                                               : String(index),
    271                                                                  : oimCurrency.currencyStr,
    272                                                        in: wrapper.namespace, isSource: false)
    273                                 Color.clear
    274                                     .frame(width: 40, height: 40)
    275 //                                  .matchedGeometryEffect(id: OIMCHEST + String(index), in: wrapper.namespace, isSource: true)
    276                                     .matchedGeometryEffect(id: OIMCHEST + oimCurrency.currencyStr, in: wrapper.namespace, isSource: true)
    277                             }
    278                         }
    279                     }
    280                     Spacer()
    281                 } // three chests
    282 
    283                 VStack {
    284                     Spacer()
    285                     let showDrawer = viewState == .sending
    286                     OIMcurrencyDrawer(stack: stack.push(),
    287                                        cash: cash,
    288                                availableVal: $availableVal,
    289                                   tappedVal: $tappedVal,        // unused, since canEdit == false
    290                              scrollPosition: maxAvailable,
    291                                     canEdit: false)
    292                     .clipped(antialiased: true)
    293                     .padding(.horizontal, 5)
    294                     .ignoresSafeArea(edges: .horizontal)
    295                     .scrollDisabled(true)
    296                     .opacity(showDrawer ? 1 : INVISIBLE)
    297                 } // source for matching positions of money in the drawer
    298             }
    299         }
    300         .onAppear {
    301             if let selectedBalance {
    302                 print("🚩OIMbalances.onAppear() selectedBalance", selectedBalance.scopeInfo.currency)
    303                 available = selectedBalance.available
    304                 availableVal = available?.centValue ?? 0                        // TODO: centValue factor
    305                 cash.update2(availableVal)           // set cash to available
    306                 if viewState == .historyTapped {
    307                     withAnimation(.basic1) {
    308                         viewState = .chestIsOpen
    309                     }
    310                 }
    311             } else {
    312                 print("🚩OIMbalances.onAppear() no selectedBalance")
    313                 initView()
    314             }
    315             debugTick += 1
    316         }
    317         .onDisappear {
    318             if (selectedBalance != nil) {
    319                 cash.moveBack()
    320                 viewState = .chestIsOpen
    321             } else {
    322                 initView()
    323             }
    324         }
    325     }
    326 }