|
| 1 | +package com.flipcash.app.session.internal |
| 2 | + |
| 3 | +import com.flipcash.app.analytics.Analytics |
| 4 | +import com.flipcash.app.analytics.FlipcashAnalyticsService |
| 5 | +import com.flipcash.app.core.bill.Bill |
| 6 | +import com.flipcash.app.core.bill.BillState |
| 7 | +import com.flipcash.app.core.bill.PaymentValuation |
| 8 | +import com.flipcash.app.core.internal.bill.BillController |
| 9 | +import com.flipcash.app.core.internal.errors.showNetworkError |
| 10 | +import com.flipcash.app.session.BillDeterminationResult |
| 11 | +import com.flipcash.app.session.BillOperations |
| 12 | +import com.flipcash.app.session.Grabbed |
| 13 | +import com.flipcash.app.session.PutInWallet |
| 14 | +import com.flipcash.app.session.internal.toast.SessionToastController |
| 15 | +import com.flipcash.app.tokens.TokenCoordinator |
| 16 | +import com.flipcash.core.R |
| 17 | +import com.flipcash.libs.coroutines.DispatcherProvider |
| 18 | +import com.flipcash.services.user.UserManager |
| 19 | +import com.getcode.manager.BottomBarManager |
| 20 | +import com.getcode.opencode.model.accounts.AccountCluster |
| 21 | +import com.getcode.util.resources.ResourceHelper |
| 22 | +import com.getcode.util.vibration.Vibrator |
| 23 | +import com.getcode.utils.ErrorUtils |
| 24 | +import com.getcode.utils.TraceType |
| 25 | +import com.getcode.utils.network.NetworkConnectivityListener |
| 26 | +import com.getcode.utils.trace |
| 27 | +import kotlinx.coroutines.CoroutineScope |
| 28 | +import kotlinx.coroutines.SupervisorJob |
| 29 | +import kotlinx.coroutines.channels.BufferOverflow |
| 30 | +import kotlinx.coroutines.flow.MutableSharedFlow |
| 31 | +import kotlinx.coroutines.flow.SharedFlow |
| 32 | +import kotlinx.coroutines.flow.StateFlow |
| 33 | +import kotlinx.coroutines.flow.asSharedFlow |
| 34 | +import kotlinx.coroutines.launch |
| 35 | +import javax.inject.Inject |
| 36 | +import javax.inject.Singleton |
| 37 | + |
| 38 | +/** |
| 39 | + * Implements [BillOperations] — creating, presenting, and dismissing cash bills. |
| 40 | + * |
| 41 | + * This delegate handles the full lifecycle of a bill that the user "gives": |
| 42 | + * 1. Validates the amount and network connectivity. |
| 43 | + * 2. Configures bill actions (Send-as-Link, Cancel). |
| 44 | + * 3. Starts the "await grab" flow that renders the Kik Code and waits for a scan. |
| 45 | + * 4. On grab: subtracts the token balance, enqueues a toast, and emits [Event.RefreshFeed]. |
| 46 | + * 5. On Send-as-Link tap: cancels the grab and emits [Event.SendAsLinkRequested] so |
| 47 | + * the shell can route to [GiftCardSharingDelegate]. |
| 48 | + * |
| 49 | + * Cross-delegate communication is handled via [events]; the [RealSessionController] |
| 50 | + * shell collects these and dispatches to the appropriate delegate. |
| 51 | + * |
| 52 | + * @see RealSessionController |
| 53 | + */ |
| 54 | +@Singleton |
| 55 | +class BillPresentationDelegate @Inject constructor( |
| 56 | + private val billController: BillController, |
| 57 | + private val stateHolder: SessionStateHolder, |
| 58 | + private val toastController: SessionToastController, |
| 59 | + private val tokenCoordinator: TokenCoordinator, |
| 60 | + private val analytics: FlipcashAnalyticsService, |
| 61 | + private val vibrator: Vibrator, |
| 62 | + private val resources: ResourceHelper, |
| 63 | + private val networkObserver: NetworkConnectivityListener, |
| 64 | + private val userManager: UserManager, |
| 65 | + dispatchers: DispatcherProvider, |
| 66 | +) : BillOperations { |
| 67 | + |
| 68 | + sealed interface Event { |
| 69 | + data class SendAsLinkRequested(val bill: Bill.Cash, val owner: AccountCluster) : Event |
| 70 | + data object RefreshFeed : Event |
| 71 | + } |
| 72 | + |
| 73 | + private val scope = CoroutineScope(dispatchers.IO + SupervisorJob()) |
| 74 | + |
| 75 | + private val _events = MutableSharedFlow<Event>( |
| 76 | + extraBufferCapacity = 1, |
| 77 | + onBufferOverflow = BufferOverflow.DROP_OLDEST, |
| 78 | + ) |
| 79 | + val events: SharedFlow<Event> = _events.asSharedFlow() |
| 80 | + |
| 81 | + override val billState: StateFlow<BillState> get() = billController.state |
| 82 | + |
| 83 | + override fun showBill(bill: Bill) { |
| 84 | + if (bill.amount.nativeAmount.decimalValue == 0.0) return |
| 85 | + val owner = userManager.accountCluster ?: return |
| 86 | + |
| 87 | + if (!networkObserver.isConnected) { |
| 88 | + return ErrorUtils.showNetworkError(resources) |
| 89 | + } |
| 90 | + |
| 91 | + when (bill) { |
| 92 | + is Bill.Cash -> { |
| 93 | + when (bill.kind) { |
| 94 | + Bill.Kind.airdrop -> { |
| 95 | + billController.update { |
| 96 | + it.copy( |
| 97 | + primaryAction = null, |
| 98 | + secondaryAction = null, |
| 99 | + ) |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + Bill.Kind.cash -> { |
| 104 | + if (bill.didReceive) { |
| 105 | + billController.update { |
| 106 | + it.copy( |
| 107 | + primaryAction = null, |
| 108 | + secondaryAction = null, |
| 109 | + ) |
| 110 | + } |
| 111 | + } else { |
| 112 | + billController.update { |
| 113 | + it.copy( |
| 114 | + primaryAction = BillState.Action.SendAsLink( |
| 115 | + action = { |
| 116 | + billController.cancelAwaitForGrab() |
| 117 | + _events.tryEmit(Event.SendAsLinkRequested(bill, owner)) |
| 118 | + } |
| 119 | + ), |
| 120 | + secondaryAction = BillState.Action.Cancel( |
| 121 | + action = { dismissBill(PutInWallet) } |
| 122 | + ), |
| 123 | + ) |
| 124 | + } |
| 125 | + } |
| 126 | + awaitBillGrab(bill, owner) |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + override fun dismissBill(action: BillDeterminationResult) { |
| 134 | + scope.launch { |
| 135 | + stateHolder.update { it.copy(billResult = action) } |
| 136 | + billController.reset() |
| 137 | + toastController.consumeQueue() |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + internal fun awaitBillGrab(bill: Bill, owner: AccountCluster) { |
| 142 | + analytics.transferStart(Analytics.Transfer.Initiate.GiveBillStart) |
| 143 | + billController.awaitGrab( |
| 144 | + amount = bill.amount, |
| 145 | + token = bill.token, |
| 146 | + verifiedState = (bill as? Bill.Cash)?.verifiedState, |
| 147 | + nonce = (bill as? Bill.Cash)?.nonce?.takeIf { it.isNotEmpty() }, |
| 148 | + owner = owner, |
| 149 | + onGrabbed = { amount -> |
| 150 | + tokenCoordinator.subtract(bill.token, amount) |
| 151 | + analytics.transfer(Analytics.Transfer.GiveBill, bill.amount) |
| 152 | + toastController.enqueue(bill.amount, isDeposit = false) |
| 153 | + dismissBill(Grabbed) |
| 154 | + vibrator.vibrate() |
| 155 | + _events.tryEmit(Event.RefreshFeed) |
| 156 | + }, |
| 157 | + onTimeout = { |
| 158 | + dismissBill(action = PutInWallet) |
| 159 | + }, |
| 160 | + onError = { |
| 161 | + analytics.transfer( |
| 162 | + event = Analytics.Transfer.GiveBill, |
| 163 | + amount = bill.amount, |
| 164 | + successful = false, |
| 165 | + error = it |
| 166 | + ) |
| 167 | + dismissBill(action = PutInWallet) |
| 168 | + BottomBarManager.showError( |
| 169 | + title = resources.getString(R.string.error_title_CashReturnedToWallet), |
| 170 | + message = resources.getString(R.string.error_description_CashReturnedToWallet) |
| 171 | + ) |
| 172 | + }, |
| 173 | + present = { (data, nonce) -> |
| 174 | + if (!bill.didReceive) { |
| 175 | + trace( |
| 176 | + tag = "Session", |
| 177 | + message = "Pull out cash", |
| 178 | + metadata = { |
| 179 | + "underlying quarks" to bill.amount.underlyingTokenAmount.quarks |
| 180 | + "native amount" to bill.amount.nativeAmount.formatted() |
| 181 | + "fx" to bill.amount.rate.fx |
| 182 | + "currency" to bill.amount.rate.currency.name |
| 183 | + "token mint" to bill.amount.mint |
| 184 | + }, |
| 185 | + type = TraceType.User, |
| 186 | + ) |
| 187 | + } |
| 188 | + presentBillToUser(data, nonce, bill) |
| 189 | + }, |
| 190 | + ) |
| 191 | + } |
| 192 | + |
| 193 | + private fun presentBillToUser(data: List<Byte>, nonce: List<Byte>, bill: Bill) { |
| 194 | + if (billController.state.value.bill != null) return |
| 195 | + |
| 196 | + val presentedBill = when (bill) { |
| 197 | + is Bill.Cash -> bill.copy( |
| 198 | + data = data, |
| 199 | + nonce = nonce, |
| 200 | + ) |
| 201 | + } |
| 202 | + |
| 203 | + billController.update { |
| 204 | + it.copy( |
| 205 | + bill = presentedBill, |
| 206 | + valuation = PaymentValuation(bill.amount.nativeAmount), |
| 207 | + ) |
| 208 | + } |
| 209 | + |
| 210 | + val style: BillDeterminationResult = |
| 211 | + if (bill.didReceive) Grabbed else PutInWallet |
| 212 | + |
| 213 | + stateHolder.update { it.copy(billResult = style) } |
| 214 | + |
| 215 | + if (bill.didReceive) { |
| 216 | + toastController.enqueue(bill.amount, isDeposit = true) |
| 217 | + vibrator.vibrate(duration = 50) |
| 218 | + } |
| 219 | + } |
| 220 | +} |
0 commit comments