TransactionPaymentComposable.kt (10728B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2023 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.wallet.payment 18 19 import android.graphics.Bitmap 20 import androidx.compose.foundation.Image 21 import androidx.compose.foundation.clickable 22 import androidx.compose.foundation.layout.Column 23 import androidx.compose.foundation.layout.aspectRatio 24 import androidx.compose.foundation.layout.fillMaxWidth 25 import androidx.compose.foundation.layout.padding 26 import androidx.compose.foundation.rememberScrollState 27 import androidx.compose.foundation.verticalScroll 28 import androidx.compose.material3.Card 29 import androidx.compose.material3.CircularProgressIndicator 30 import androidx.compose.material3.MaterialTheme 31 import androidx.compose.material3.Text 32 import androidx.compose.runtime.Composable 33 import androidx.compose.runtime.LaunchedEffect 34 import androidx.compose.runtime.getValue 35 import androidx.compose.runtime.mutableStateOf 36 import androidx.compose.runtime.remember 37 import androidx.compose.runtime.setValue 38 import androidx.compose.ui.Alignment.Companion.CenterHorizontally 39 import androidx.compose.ui.Modifier 40 import androidx.compose.ui.graphics.asImageBitmap 41 import androidx.compose.ui.platform.LocalContext 42 import androidx.compose.ui.res.stringResource 43 import androidx.compose.ui.tooling.preview.Preview 44 import androidx.compose.ui.unit.dp 45 import androidx.compose.ui.window.Dialog 46 import net.taler.common.Amount 47 import net.taler.common.CurrencySpecification 48 import net.taler.common.Merchant 49 import net.taler.common.Timestamp 50 import net.taler.lib.android.toAbsoluteTime 51 import net.taler.wallet.BottomInsetsSpacer 52 import net.taler.wallet.R 53 import net.taler.wallet.backend.TalerErrorCode 54 import net.taler.wallet.backend.TalerErrorInfo 55 import net.taler.wallet.balances.ScopeInfo 56 import net.taler.wallet.compose.LoadingScreen 57 import net.taler.wallet.compose.TalerSurface 58 import net.taler.wallet.transactions.AmountType 59 import net.taler.wallet.transactions.ErrorTransactionButton 60 import net.taler.wallet.transactions.TransactionAction 61 import net.taler.wallet.transactions.TransactionAction.Abort 62 import net.taler.wallet.transactions.TransactionAction.Retry 63 import net.taler.wallet.transactions.TransactionAction.Suspend 64 import net.taler.wallet.transactions.TransactionAmountComposable 65 import net.taler.wallet.transactions.TransactionInfo 66 import net.taler.wallet.transactions.TransactionInfoComposable 67 import net.taler.wallet.transactions.TransactionLinkComposable 68 import net.taler.wallet.transactions.TransactionMajorState 69 import net.taler.wallet.transactions.TransactionMajorState.Pending 70 import net.taler.wallet.transactions.TransactionMinorState 71 import net.taler.wallet.transactions.TransactionPayment 72 import net.taler.wallet.transactions.TransactionState 73 import net.taler.wallet.transactions.TransactionStateComposable 74 import net.taler.wallet.transactions.TransitionsComposable 75 76 @Composable 77 fun TransactionPaymentComposable( 78 t: TransactionPayment, 79 payStatus: PayStatus, 80 devMode: Boolean, 81 spec: CurrencySpecification?, 82 modifier: Modifier = Modifier, 83 onFulfill: (url: String) -> Unit, 84 onTransition: (t: TransactionAction) -> Unit, 85 onConfirmPay: (Int?, useDonau: Boolean) -> Unit, 86 onSetupDonau: (donauBaseUrl: String) -> Unit, 87 checkDonauForChoice: suspend (PayChoiceDetails) -> DonauStatus?, 88 ) { 89 if (t.txState.major == TransactionMajorState.Dialog) { 90 return TransactionPaymentPrompt( 91 payStatus = payStatus, 92 onConfirmPay = onConfirmPay, 93 onAbortPay = { onTransition(Abort) }, 94 onSetupDonau = onSetupDonau, 95 checkDonauForChoice = checkDonauForChoice, 96 ) 97 } 98 99 val scrollState = rememberScrollState() 100 Column( 101 modifier = modifier 102 .fillMaxWidth() 103 .verticalScroll(scrollState), 104 horizontalAlignment = CenterHorizontally, 105 ) { 106 TransactionStateComposable(state = t.txState) 107 108 Text( 109 modifier = Modifier.padding(16.dp), 110 text = t.timestamp.ms.toAbsoluteTime(LocalContext.current).toString(), 111 style = MaterialTheme.typography.bodyLarge, 112 ) 113 114 if (t.txState.minor == TransactionMinorState.ClaimProposal) { 115 CircularProgressIndicator( 116 modifier = Modifier 117 .padding(45.dp) 118 .fillMaxWidth() 119 .aspectRatio(1f) 120 .align(CenterHorizontally) 121 ) 122 } else { 123 TransactionAmountComposable( 124 label = stringResource(id = R.string.transaction_order_total), 125 amount = t.amountRaw.withSpec(spec), 126 amountType = AmountType.Neutral, 127 ) 128 129 if (t.amountEffective > t.amountRaw) { 130 val fee = t.amountEffective - t.amountRaw 131 TransactionAmountComposable( 132 label = stringResource(id = R.string.amount_fee), 133 amount = fee.withSpec(spec), 134 amountType = AmountType.Negative, 135 ) 136 } 137 138 TransactionAmountComposable( 139 label = stringResource(id = R.string.transaction_paid), 140 amount = t.amountEffective.withSpec(spec), 141 amountType = AmountType.Negative, 142 ) 143 } 144 145 if (t.posConfirmation != null) { 146 TransactionInfoComposable( 147 label = stringResource(id = R.string.payment_confirmation_code), 148 info = t.posConfirmation, 149 ) 150 } 151 152 if (t.info != null) PurchaseDetails(info = t.info) { 153 onFulfill(t.info.fulfillmentUrl ?: "") 154 } 155 156 TransitionsComposable(t, devMode, onTransition) 157 if (devMode && t.error != null) { 158 ErrorTransactionButton(error = t.error) 159 } 160 161 BottomInsetsSpacer() 162 } 163 } 164 165 @Composable 166 fun TransactionPaymentPrompt( 167 payStatus: PayStatus, 168 onConfirmPay: (Int?, useDonau: Boolean) -> Unit, 169 onAbortPay: () -> Unit, 170 onSetupDonau: (donauBaseUrl: String) -> Unit, 171 checkDonauForChoice: suspend (PayChoiceDetails) -> DonauStatus?, 172 ) { 173 var showImage by remember { mutableStateOf<Bitmap?>(null) } 174 when (val status = payStatus) { 175 is PayStatus.None, 176 is PayStatus.Loading, 177 is PayStatus.Prepared -> LoadingScreen() 178 is PayStatus.Choices -> PromptPaymentComposable( 179 status = status, 180 onConfirm = { index, useDonau -> 181 onConfirmPay(index, useDonau) 182 }, 183 onCancel = { 184 onAbortPay() 185 }, 186 onClickImage = { bitmap -> 187 showImage = bitmap 188 }, 189 checkDonauStatus = { index -> 190 status.choices.find { it.choiceIndex == index }?.let { choice -> 191 checkDonauForChoice(choice) 192 } ?: DonauStatus.Unavailable 193 }, 194 onSetupDonau = { donauBaseUrl -> 195 onSetupDonau(donauBaseUrl) 196 }, 197 ) 198 else -> {} 199 } 200 201 if (showImage != null) { 202 ProductImageDialog(showImage!!) { showImage = null } 203 } 204 } 205 206 @Composable 207 fun PurchaseDetails( 208 info: TransactionInfo, 209 onClick: () -> Unit, 210 ) { 211 Column( 212 horizontalAlignment = CenterHorizontally, 213 ) { 214 // Summary and fulfillment message 215 val text = if (info.fulfillmentMessage == null) { 216 info.summary 217 } else { 218 "${info.summary}\n\n${info.fulfillmentMessage}" 219 } 220 if (info.fulfillmentUrl != null) { 221 TransactionLinkComposable( 222 label = stringResource(id = R.string.transaction_order), 223 info = text, 224 ) { onClick() } 225 } else { 226 TransactionInfoComposable( 227 label = stringResource(id = R.string.transaction_order), 228 info = text, 229 ) 230 } 231 // Order ID 232 Text( 233 stringResource(id = R.string.transaction_order_id, info.orderId), 234 style = MaterialTheme.typography.bodyMedium, 235 ) 236 } 237 } 238 239 @Composable 240 fun ProductImageDialog(bitmap: Bitmap, onDismiss: () -> Unit) { 241 Dialog(onDismissRequest = onDismiss) { 242 Card( 243 shape = MaterialTheme.shapes.medium, 244 modifier = Modifier 245 .padding(16.dp) 246 .fillMaxWidth(), 247 ) { 248 Image( 249 bitmap = bitmap.asImageBitmap(), 250 contentDescription = null, 251 modifier = Modifier 252 .fillMaxWidth() 253 .clickable { onDismiss() } 254 ) 255 } 256 } 257 } 258 259 @Preview 260 @Composable 261 fun TransactionPaymentComposablePreview() { 262 val t = TransactionPayment( 263 transactionId = "transactionId", 264 timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000), 265 txState = TransactionState(Pending), 266 txActions = listOf(Retry, Suspend, Abort), 267 info = TransactionInfo( 268 orderId = "123", 269 merchant = Merchant(name = "Taler"), 270 summary = "Some Product that was bought and can have quite a long label", 271 fulfillmentMessage = "This is some fulfillment message", 272 fulfillmentUrl = "https://bank.demo.taler.net/", 273 products = listOf(), 274 ), 275 amountRaw = Amount.fromString("TESTKUDOS", "42.1337"), 276 amountEffective = Amount.fromString("TESTKUDOS", "42.23"), 277 error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED), 278 scopes = listOf(ScopeInfo.Exchange( 279 currency = "TESTKUDOS", 280 url = "exchange.test.taler.net", 281 )) 282 ) 283 TalerSurface { 284 TransactionPaymentComposable( 285 t = t, 286 payStatus = PayStatus.None, 287 devMode = true, 288 spec = null, 289 onFulfill = {}, 290 onTransition = {}, 291 onConfirmPay = { _, _ ->}, 292 onSetupDonau = {}, 293 checkDonauForChoice = { _ -> DonauStatus.Available }, 294 ) 295 } 296 }