taler-ios

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

OIMbalances.swift (12856B)


      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 qrButtonTapped: Bool
     37     @Binding var historyTapped: Int?
     38     let oimEuro: Bool
     39 
     40     @EnvironmentObject private var controller: Controller
     41     @EnvironmentObject private var wrapper: NamespaceWrapper
     42 
     43     @StateObject private var cash: OIMcash
     44     @State private var availableVal: UInt64 = 0
     45     @State private var tappedVal: UInt64 = 0            // unused, canEdit == false
     46     @State private var available: Amount? = nil
     47     @State private var viewState: OIMbalancesState = .chestsClosed
     48     @State private var closing = false                  // debounce tap (on open chest to close it)
     49     @State private var balanceIndex: Int? = nil
     50 
     51     init(stack: CallStack,
     52           selectedBalance: Binding<Balance?>,
     53            qrButtonTapped: Binding<Bool>,
     54             historyTapped: Binding<Int?>,
     55                   oimEuro: Bool
     56     ) {
     57         self.stack = stack
     58         self._selectedBalance = selectedBalance
     59         self._qrButtonTapped = qrButtonTapped
     60         self._historyTapped = historyTapped
     61         self.oimEuro = oimEuro
     62         let oimCurrency = oimCurrency(selectedBalance.wrappedValue?.scopeInfo, oimEuro: oimEuro)  // might be nil ==> OIMeuros
     63         let oimCash = OIMcash(oimCurrency)
     64         self._cash = StateObject(wrappedValue: { oimCash }())
     65     }
     66 
     67     func requestTapped() {
     68 
     69     }
     70 
     71     func sendTapped() {
     72         withAnimation(.basicFast) {
     73             viewState = .sendTapped
     74         }
     75         cash.flyOneByOne(to: .drawer)
     76         withAnimation(.basic1.delay(0.6)) {
     77             viewState = .sending    // go to edit view, blend in missing denominations in drawer
     78         }
     79         DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
     80             let actionType = ActionType(animationDisabled: true)
     81             let userinfo = [NOTIFICATIONANIMATION: actionType]
     82             // will trigger NavigationLink
     83             NotificationCenter.default.post(name: .SendAction,          // switch to OIMEditView
     84                                           object: nil,
     85                                         userInfo: userinfo)
     86         }
     87     }
     88 
     89     func closeChest() {
     90         if !closing {
     91             closing = true
     92             viewState = .chestClosing
     93             let delay = cash.flyOneByOne(to: .curve)        // back to chest...
     94             DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
     95                 print("closeChest", delay)
     96                 withAnimation(.basic1) {
     97                     print("🚩OIMbalances.closeChest() reset selectedBalance")
     98                     selectedBalance = nil
     99                     available = nil
    100                     viewState = .chestsClosed
    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 topButtons = HStack(alignment: .top) {
    184             if selectedBalance == nil {
    185                 let showQR = viewState == .chestsClosed
    186                 QRButton(hideTitle: true) {
    187                     qrButtonTapped = true
    188                 }
    189                 .opacity(showQR ? 1 : INVISIBLE)
    190                 .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    191                 .matchedGeometryEffect(id: OIMBACK, in: wrapper.namespace, isSource: true)
    192             } else {
    193                 let showRequest = viewState == .chestIsOpen
    194                 OIMactionButton(type: .requestP2P, isFinal: false, action: requestTapped)
    195                     .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    196                     .opacity(showRequest ? 1 : INVISIBLE)
    197             }
    198             Spacer()
    199             let showSend = viewState == .chestIsOpen
    200             OIMactionButton(type: .sendP2P, isFinal: false, action: sendTapped)
    201                 .frame(width: OIMbuttonSize, height: OIMbuttonSize)
    202                 .opacity(showSend ? 1 : INVISIBLE)
    203         }
    204 
    205         let maxAvailable = cash.max(available: available?.centValue ?? 0)       // TODO: centValue factor
    206 //        let _ = print("maxAvailable", maxAvailable)
    207 
    208         let sidePosition = HStack {
    209             Spacer()
    210             Color.clear
    211                 .frame(width: 80, height: 80)
    212                 .matchedGeometryEffect(id: OIMSIDE, in: wrapper.namespace, isSource: true)
    213         }
    214         OIMbackground() {
    215             ZStack(alignment: .top) {
    216                 topButtons
    217                 VStack {
    218                     // balance, amountToSend
    219                     OIMtitleView(cash: cash,
    220                                amount: available,
    221                               history: viewState == .historyShown,
    222                          secondAmount: nil)             // appears in OIMEditView
    223                     Spacer()
    224                     let isOpen = selectedBalance != nil
    225                     ZStack {
    226                         sidePosition
    227 //                        let scaleMoney = viewState == .chestIsOpen
    228                         OIMlineView(stack: stack.push(),
    229                                      cash: cash,
    230                                 amountVal: $availableVal,
    231                                   canEdit: false)
    232                             .opacity(isOpen ? 1 : INVISIBLE)
    233 //                            .scaleEffect(scaleMoney ? 0.6 : 1)
    234                             .onTapGesture {
    235                                 openHistory()
    236                             }
    237                     }
    238                     Spacer()
    239                 } // title on top, money in the middle
    240 
    241                 VStack {
    242                     // multiple savings chests (Euro, Sierra Leone, Côte d'Ivoire)
    243                     Spacer()
    244                     HStack(spacing: 30) {
    245                         ForEach(Array(controller.balances.enumerated()), id: \.element) { index, balance in
    246                             let oimCurrency = oimCurrency(balance.scopeInfo, oimEuro: oimEuro)
    247                             let itsMe = selectedBalance == balance
    248                             let isClosed = selectedBalance == nil
    249                             let size = isClosed ? 160.0 : OIMbuttonSize
    250                             ZStack {
    251                                 OIMbalanceButton(isOpen: itsMe, chest: oimCurrency.chest, isFinal: false) {
    252                                     if itsMe {
    253                                         closeChest()
    254                                     } else {
    255                                         openChest(oimCurrency, index, balance)
    256                                     }
    257                                 }
    258                                 .frame(width: size, height: size)
    259                                 .zIndex(itsMe ? 3 : 0)
    260                                 .opacity((isClosed || itsMe) ? 1 : INVISIBLE)
    261                                 .matchedGeometryEffect(id: itsMe ? OIMNUMBER
    262 //                                                               : String(index),
    263                                                                  : oimCurrency.currencyStr,
    264                                                        in: wrapper.namespace, isSource: false)
    265                                 Color.clear
    266                                     .frame(width: 40, height: 40)
    267 //                                  .matchedGeometryEffect(id: OIMCHEST + String(index), in: wrapper.namespace, isSource: true)
    268                                     .matchedGeometryEffect(id: OIMCHEST + oimCurrency.currencyStr, in: wrapper.namespace, isSource: true)
    269                             }
    270                         }
    271                     }
    272                     Spacer()
    273                 } // three chests
    274 
    275                 VStack {
    276                     Spacer()
    277                     let showDrawer = viewState == .sending
    278                     OIMcurrencyDrawer(stack: stack.push(),
    279                                        cash: cash,
    280                                availableVal: $availableVal,
    281                                   tappedVal: $tappedVal,        // unused, since canEdit == false
    282                              scrollPosition: maxAvailable,
    283                                     canEdit: false)
    284                     .clipped(antialiased: true)
    285                     .padding(.horizontal, 5)
    286                     .ignoresSafeArea(edges: .horizontal)
    287                     .scrollDisabled(true)
    288                     .opacity(showDrawer ? 1 : INVISIBLE)
    289                 } // source for matching positions of money in the drawer
    290             }
    291         }
    292         .onAppear {
    293             if let selectedBalance {
    294                 print("🚩OIMbalances.onAppear() selectedBalance", selectedBalance.scopeInfo.currency)
    295                 available = selectedBalance.available
    296                 availableVal = available?.centValue ?? 0                        // TODO: centValue factor
    297                 cash.update2(availableVal)           // set cash to available
    298                 if viewState == .historyTapped {
    299                     withAnimation(.basic1) {
    300                         viewState = .chestIsOpen
    301                     }
    302                 }
    303             } else {
    304                 print("🚩OIMbalances.onAppear() no selectedBalance")
    305                 initView()
    306             }
    307             debugTick += 1
    308         }
    309         .onDisappear {
    310             if (selectedBalance != nil) {
    311                 cash.moveBack()
    312                 viewState = .chestIsOpen
    313             } else {
    314                 initView()
    315             }
    316         }
    317     }
    318 }