taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

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 }