taler-ios

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

OIMbalances.swift (12913B)


      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 showSend = viewState == .chestIsOpen
    181 #if DEBUG
    182         let showRequest = viewState == .chestIsOpen
    183 #else
    184         let showRequest = false
    185 #endif
    186         let topButtons = HStack(alignment: .top) {
    187             if selectedBalance == nil {
    188                 let showQR = viewState == .chestsClosed
    189                 QRButton(hideTitle: true) {
    190                     NotificationCenter.default.post(name: .QrScanAction, object: nil)   // will trigger NavigationLink
    191                 }
    192                 .opacity(showQR ? 1 : INVISIBLE)
    193                 .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    194                 .matchedGeometryEffect(id: OIMBACK, in: wrapper.namespace, isSource: true)
    195             } else {
    196 
    197             }
    198             Spacer()
    199             VStack {
    200                 OIMactionButton(type: .sendP2P, isFinal: false, action: sendTapped)
    201                     .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    202                     .opacity(showSend ? 1 : INVISIBLE)
    203                 OIMactionButton(type: .requestP2P, isFinal: false, action: requestTapped)
    204                     .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    205                     .opacity(showRequest ? 1 : INVISIBLE)
    206             }
    207         }
    208 
    209         let maxAvailable = cash.max(available: available?.centValue ?? 0)       // TODO: centValue factor
    210 //        let _ = print("maxAvailable", maxAvailable)
    211 
    212         let sidePosition = HStack {
    213             Spacer()
    214             Color.clear
    215                 .frame(width: 80, height: 80)
    216                 .matchedGeometryEffect(id: OIMSIDE, in: wrapper.namespace, isSource: true)
    217         }
    218         OIMbackground() {
    219             ZStack(alignment: .top) {
    220                 topButtons
    221                 VStack {
    222                     // balance, amountToSend
    223                     OIMtitleView(cash: cash,
    224                                amount: available,
    225                               history: viewState == .historyShown,
    226                          secondAmount: nil)             // appears in OIMEditView
    227                     Spacer()
    228                     let isOpen = selectedBalance != nil
    229                     ZStack {
    230                         sidePosition
    231 //                        let scaleMoney = viewState == .chestIsOpen
    232                         OIMlineView(stack: stack.push(),
    233                                      cash: cash,
    234                                 amountVal: $availableVal,
    235                                   canEdit: false)
    236                             .opacity(isOpen ? 1 : INVISIBLE)
    237 //                            .scaleEffect(scaleMoney ? 0.6 : 1)
    238                             .onTapGesture {
    239                                 openHistory()
    240                             }
    241                     }
    242                     Spacer()
    243                 } // title on top, money in the middle
    244 
    245                 VStack {
    246                     // multiple savings chests (Euro, Sierra Leone, Côte d'Ivoire)
    247                     Spacer()
    248                     HStack(spacing: 30) {
    249                         ForEachWithIndex(data: controller.balances) { index, balance in
    250                             let oimCurrency = oimCurrency(balance.scopeInfo, oimEuro: oimEuro)
    251                             let itsMe = selectedBalance == balance
    252                             let isClosed = selectedBalance == nil
    253                             let size = isClosed ? 160.0 : OIMbuttonSize
    254                             ZStack {
    255                                 OIMbalanceButton(isOpen: itsMe, chest: oimCurrency.chest, isFinal: false) {
    256                                     if itsMe {
    257                                         closeChest()
    258                                     } else {
    259                                         openChest(oimCurrency, index, balance)
    260                                     }
    261                                 }
    262                                 .frame(width: size, height: size)
    263                                 .zIndex(itsMe ? 3 : 0)
    264                                 .opacity((isClosed || itsMe) ? 1 : INVISIBLE)
    265                                 .matchedGeometryEffect(id: itsMe ? OIMNUMBER // (sending ? OIMBACK : OIMNUMBER)
    266 //                                                               : String(index),
    267                                                                  : oimCurrency.currencyStr,
    268                                                        in: wrapper.namespace, isSource: false)
    269                                 Color.clear
    270                                     .frame(width: 40, height: 40)
    271 //                                  .matchedGeometryEffect(id: OIMCHEST + String(index), in: wrapper.namespace, isSource: true)
    272                                     .matchedGeometryEffect(id: OIMCHEST + oimCurrency.currencyStr, in: wrapper.namespace, isSource: true)
    273                             }
    274                         }
    275                     }
    276                     Spacer()
    277                 } // three chests
    278 
    279                 VStack {
    280                     Spacer()
    281                     let showDrawer = viewState == .sending
    282                     OIMcurrencyDrawer(stack: stack.push(),
    283                                        cash: cash,
    284                                availableVal: $availableVal,
    285                                   tappedVal: $tappedVal,        // unused, since canEdit == false
    286                              scrollPosition: maxAvailable,
    287                                     canEdit: false)
    288                     .clipped(antialiased: true)
    289                     .padding(.horizontal, 5)
    290                     .ignoresSafeArea(edges: .horizontal)
    291                     .scrollDisabled(true)
    292                     .opacity(showDrawer ? 1 : INVISIBLE)
    293                 } // source for matching positions of money in the drawer
    294             }
    295         }
    296         .onAppear {
    297             if let selectedBalance {
    298                 print("🚩OIMbalances.onAppear() selectedBalance", selectedBalance.scopeInfo.currency)
    299                 available = selectedBalance.available
    300                 availableVal = available?.centValue ?? 0                        // TODO: centValue factor
    301                 cash.update2(availableVal)           // set cash to available
    302                 if viewState == .historyTapped {
    303                     withAnimation(.basic1) {
    304                         viewState = .chestIsOpen
    305                     }
    306                 }
    307             } else {
    308                 print("🚩OIMbalances.onAppear() no selectedBalance")
    309                 initView()
    310             }
    311             debugTick += 1
    312         }
    313         .onDisappear {
    314             if (selectedBalance != nil) {
    315                 cash.moveBack()
    316                 viewState = .chestIsOpen
    317             } else {
    318                 initView()
    319             }
    320         }
    321     }
    322 }