taler-ios

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

OIMbalances.swift (12787B)


      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             viewState = .chestClosing
     90             let delay = cash.flyOneByOne(to: .curve)        // back to chest...
     91             DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
     92                 print("closeChest", delay)
     93                 withAnimation(.basic1) {
     94                     print("🚩OIMbalances.closeChest() reset selectedBalance")
     95                     selectedBalance = nil
     96                     available = nil
     97                     viewState = .chestsClosed
     98                 }
     99                 closing = false
    100             }
    101         }
    102     }
    103 
    104     func openChest(_ oimCurrency: OIMcurrency, _ index: Int, _ balance: Balance) {
    105         cash.clearFunds()
    106         print("❗️openChest❗️")
    107         let duration: TimeInterval
    108         let initial: TimeInterval
    109 #if DEBUG
    110         duration = debugAnimations ? 2.5 :
    111                     fastAnimations ? 0.6 : 1.1
    112         initial = debugAnimations ? 1.0 : 0.1
    113 #else
    114         duration = fastAnimations ? 0.6 : 1.1
    115         initial = 0.1
    116 #endif
    117         viewState = .chestOpenTapped
    118         withAnimation(.basic1) {
    119             print("🚩OIMbalances.openChest() set selectedBalance to", balance.scopeInfo.currency)
    120             selectedBalance = balance
    121             balanceIndex = index
    122             viewState = .chestIsOpen
    123             cash.setCurrency(oimCurrency)
    124             available = balance.available
    125             availableVal = balance.available.centValue                          // TODO: centValue factor
    126             cash.update2(availableVal, state: .chestOpening, duration, initial)  // set cash to available
    127             let maxAvailable = cash.max(available: availableVal)
    128             print("OIMView.openChest availableVal", availableVal, maxAvailable)
    129         }
    130     }
    131 
    132 //    func closeHistory() {
    133 //        withAnimation(.basic1) {
    134 //            viewState = .historyTapped
    135 //        }
    136 //        let delay = cash.flyOneByOne(to: .idle)             // back to center
    137 //        DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
    138 //            print("closeHistory", delay)
    139 //            withAnimation(.basic1) {
    140 //                viewState = .chestIsOpen
    141 //            }
    142 //        }
    143 //    }
    144 
    145     func openHistory() {
    146         viewState = .balanceTapped
    147         let delay = cash.flyOneByOne(to: .history, true)
    148         DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
    149             print("openHistory", delay)
    150             withAnimation(.basic1) {
    151                 viewState = .historyShown
    152                 DispatchQueue.main.asyncAfter(deadline: .now() + Animation.talerDuration2) {
    153                     var transaction = Transaction()
    154                     transaction.disablesAnimations = true
    155                     withTransaction(transaction) {
    156                         historyTapped = balanceIndex          // ==> go to transaction list
    157                     }
    158                     DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    159                         cash.moveBack()
    160                         viewState = .chestIsOpen
    161                     }
    162                 }
    163             }
    164         }
    165     }
    166 
    167     func initView() {
    168         availableVal = 0
    169         cash.update2(availableVal)           // set cash to 0
    170         viewState = .chestsClosed
    171     }
    172 
    173     var body: some View {
    174         var debugTick = 0
    175 //        let _ = Self._printChanges()
    176 
    177         let enabled = if let available {
    178             !available.isZero
    179         } else { false }
    180         let topButtons = HStack(alignment: .top) {
    181             if selectedBalance == nil {
    182                 let showQR = viewState == .chestsClosed
    183                 QRButton(hideTitle: true) {
    184                     NotificationCenter.default.post(name: .QrScanAction, object: nil)   // will trigger NavigationLink
    185                 }
    186                 .opacity(showQR ? 1 : INVISIBLE)
    187                 .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    188                 .matchedGeometryEffect(id: OIMBACK, in: wrapper.namespace, isSource: true)
    189             } else {
    190                 let showRequest = viewState == .chestIsOpen
    191                 OIMactionButton(type: .requestP2P, isFinal: false, action: requestTapped)
    192                     .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    193                     .opacity(showRequest ? 1 : INVISIBLE)
    194             }
    195             Spacer()
    196             let showSend = viewState == .chestIsOpen
    197             OIMactionButton(type: .sendP2P, isFinal: false, action: sendTapped)
    198                 .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    199                 .opacity(showSend ? 1 : INVISIBLE)
    200         }
    201 
    202         let maxAvailable = cash.max(available: available?.centValue ?? 0)       // TODO: centValue factor
    203 //        let _ = print("maxAvailable", maxAvailable)
    204 
    205         let sidePosition = HStack {
    206             Spacer()
    207             Color.clear
    208                 .frame(width: 80, height: 80)
    209                 .matchedGeometryEffect(id: OIMSIDE, in: wrapper.namespace, isSource: true)
    210         }
    211         OIMbackground() {
    212             ZStack(alignment: .top) {
    213                 topButtons
    214                 VStack {
    215                     // balance, amountToSend
    216                     OIMtitleView(cash: cash,
    217                                amount: available,
    218                               history: viewState == .historyShown,
    219                          secondAmount: nil)             // appears in OIMEditView
    220                     Spacer()
    221                     let isOpen = selectedBalance != nil
    222                     ZStack {
    223                         sidePosition
    224 //                        let scaleMoney = viewState == .chestIsOpen
    225                         OIMlineView(stack: stack.push(),
    226                                      cash: cash,
    227                                 amountVal: $availableVal,
    228                                   canEdit: false)
    229                             .opacity(isOpen ? 1 : INVISIBLE)
    230 //                            .scaleEffect(scaleMoney ? 0.6 : 1)
    231                             .onTapGesture {
    232                                 openHistory()
    233                             }
    234                     }
    235                     Spacer()
    236                 } // title on top, money in the middle
    237 
    238                 VStack {
    239                     // multiple savings chests (Euro, Sierra Leone, Côte d'Ivoire)
    240                     Spacer()
    241                     HStack(spacing: 30) {
    242                         ForEachWithIndex(data: controller.balances) { index, balance in
    243                             let oimCurrency = oimCurrency(balance.scopeInfo, oimEuro: oimEuro)
    244                             let itsMe = selectedBalance == balance
    245                             let isClosed = selectedBalance == nil
    246                             let size = isClosed ? 160.0 : OIMbuttonSize
    247                             ZStack {
    248                                 OIMbalanceButton(isOpen: itsMe, chest: oimCurrency.chest, isFinal: false) {
    249                                     if itsMe {
    250                                         closeChest()
    251                                     } else {
    252                                         openChest(oimCurrency, index, balance)
    253                                     }
    254                                 }
    255                                 .frame(width: size, height: size)
    256                                 .zIndex(itsMe ? 3 : 0)
    257                                 .opacity((isClosed || itsMe) ? 1 : INVISIBLE)
    258                                 .matchedGeometryEffect(id: itsMe ? OIMNUMBER
    259 //                                                               : String(index),
    260                                                                  : oimCurrency.currencyStr,
    261                                                        in: wrapper.namespace, isSource: false)
    262                                 Color.clear
    263                                     .frame(width: 40, height: 40)
    264 //                                  .matchedGeometryEffect(id: OIMCHEST + String(index), in: wrapper.namespace, isSource: true)
    265                                     .matchedGeometryEffect(id: OIMCHEST + oimCurrency.currencyStr, in: wrapper.namespace, isSource: true)
    266                             }
    267                         }
    268                     }
    269                     Spacer()
    270                 } // three chests
    271 
    272                 VStack {
    273                     Spacer()
    274                     let showDrawer = viewState == .sending
    275                     OIMcurrencyDrawer(stack: stack.push(),
    276                                        cash: cash,
    277                                availableVal: $availableVal,
    278                                   tappedVal: $tappedVal,        // unused, since canEdit == false
    279                              scrollPosition: maxAvailable,
    280                                     canEdit: false)
    281                     .clipped(antialiased: true)
    282                     .padding(.horizontal, 5)
    283                     .ignoresSafeArea(edges: .horizontal)
    284                     .scrollDisabled(true)
    285                     .opacity(showDrawer ? 1 : INVISIBLE)
    286                 } // source for matching positions of money in the drawer
    287             }
    288         }
    289         .onAppear {
    290             if let selectedBalance {
    291                 print("🚩OIMbalances.onAppear() selectedBalance", selectedBalance.scopeInfo.currency)
    292                 available = selectedBalance.available
    293                 availableVal = available?.centValue ?? 0                        // TODO: centValue factor
    294                 cash.update2(availableVal)           // set cash to available
    295                 if viewState == .historyTapped {
    296                     withAnimation(.basic1) {
    297                         viewState = .chestIsOpen
    298                     }
    299                 }
    300             } else {
    301                 print("🚩OIMbalances.onAppear() no selectedBalance")
    302                 initView()
    303             }
    304             debugTick += 1
    305         }
    306         .onDisappear {
    307             if (selectedBalance != nil) {
    308                 cash.moveBack()
    309                 viewState = .chestIsOpen
    310             } else {
    311                 initView()
    312             }
    313         }
    314     }
    315 }