taler-android

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

IncomingComposable.kt (9553B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2022 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.peer
     18 
     19 import android.annotation.SuppressLint
     20 import androidx.annotation.StringRes
     21 import androidx.compose.foundation.layout.Column
     22 import androidx.compose.foundation.layout.ColumnScope
     23 import androidx.compose.foundation.layout.Row
     24 import androidx.compose.foundation.layout.Spacer
     25 import androidx.compose.foundation.layout.aspectRatio
     26 import androidx.compose.foundation.layout.fillMaxSize
     27 import androidx.compose.foundation.layout.fillMaxWidth
     28 import androidx.compose.foundation.layout.padding
     29 import androidx.compose.foundation.rememberScrollState
     30 import androidx.compose.foundation.verticalScroll
     31 import androidx.compose.material3.Button
     32 import androidx.compose.material3.Card
     33 import androidx.compose.material3.CircularProgressIndicator
     34 import androidx.compose.material3.MaterialTheme
     35 import androidx.compose.material3.Surface
     36 import androidx.compose.material3.Text
     37 import androidx.compose.runtime.Composable
     38 import androidx.compose.runtime.State
     39 import androidx.compose.runtime.mutableStateOf
     40 import androidx.compose.ui.Alignment.Companion.CenterHorizontally
     41 import androidx.compose.ui.Alignment.Companion.End
     42 import androidx.compose.ui.Modifier
     43 import androidx.compose.ui.res.stringResource
     44 import androidx.compose.ui.text.font.FontWeight
     45 import androidx.compose.ui.tooling.preview.Preview
     46 import androidx.compose.ui.unit.dp
     47 import net.taler.common.Amount
     48 import net.taler.wallet.R
     49 import net.taler.wallet.backend.TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE
     50 import net.taler.wallet.backend.TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE
     51 import net.taler.wallet.backend.TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED
     52 import net.taler.wallet.backend.TalerErrorInfo
     53 import net.taler.wallet.systemBarsPaddingBottom
     54 
     55 data class IncomingData(
     56     val isCredit: Boolean,
     57     @StringRes val intro: Int,
     58     @StringRes val button: Int,
     59 )
     60 
     61 val incomingPush = IncomingData(
     62     isCredit = true,
     63     intro = R.string.receive_peer_payment_intro,
     64     button = R.string.receive_peer_payment_title,
     65 )
     66 
     67 val incomingPull = IncomingData(
     68     isCredit = false,
     69     intro = R.string.pay_peer_intro,
     70     button = R.string.payment_button_confirm,
     71 )
     72 
     73 @Composable
     74 fun IncomingComposable(
     75     state: State<IncomingState>,
     76     data: IncomingData,
     77     modifier: Modifier = Modifier,
     78     onAccept: (IncomingTerms) -> Unit,
     79 ) {
     80     val scrollState = rememberScrollState()
     81     Column(
     82         modifier = modifier
     83             .fillMaxSize()
     84             .verticalScroll(scrollState),
     85     ) {
     86         Text(
     87             modifier = Modifier
     88                 .padding(16.dp)
     89                 .align(CenterHorizontally),
     90             text = stringResource(id = data.intro),
     91         )
     92         when (val s = state.value) {
     93             IncomingChecking -> PeerPullCheckingComposable()
     94             is IncomingAccepting -> PeerPullTermsComposable(s, onAccept, data)
     95             is IncomingTerms -> PeerPullTermsComposable(s, onAccept, data)
     96             is IncomingError -> PeerPullErrorComposable(s)
     97             is IncomingAccepted -> {
     98                 // we navigate away, don't show anything
     99             }
    100         }
    101     }
    102 }
    103 
    104 @Composable
    105 fun ColumnScope.PeerPullCheckingComposable() {
    106     CircularProgressIndicator(
    107         modifier = Modifier
    108             .padding(45.dp)
    109             .fillMaxWidth()
    110             .aspectRatio(1f)
    111             .align(CenterHorizontally),
    112     )
    113 }
    114 
    115 @Composable
    116 fun ColumnScope.PeerPullTermsComposable(
    117     terms: IncomingTerms,
    118     onAccept: (IncomingTerms) -> Unit,
    119     data: IncomingData,
    120 ) {
    121     Text(
    122         modifier = Modifier
    123             .padding(16.dp)
    124             .align(CenterHorizontally),
    125         text = terms.contractTerms.summary,
    126         style = MaterialTheme.typography.headlineSmall,
    127     )
    128     Spacer(modifier = Modifier.weight(1f))
    129     Card(modifier = Modifier.fillMaxWidth()) {
    130         Column(
    131             modifier = Modifier
    132                 .padding(8.dp)
    133                 .fillMaxWidth()
    134                 .systemBarsPaddingBottom(),
    135         ) {
    136             Row(
    137                 modifier = Modifier.align(End),
    138             ) {
    139                 Text(
    140                     text = stringResource(id = R.string.amount_total_label),
    141                     style = MaterialTheme.typography.bodyLarge,
    142                 )
    143                 Text(
    144                     modifier = Modifier.padding(start = 8.dp),
    145                     text = terms.contractTerms.amount.toString(),
    146                     style = MaterialTheme.typography.bodyLarge,
    147                     fontWeight = FontWeight.Bold,
    148                 )
    149             }
    150             // this gets used for credit and debit, so fee calculation differs
    151             val fee = if (data.isCredit && terms.amountRaw > terms.amountEffective) {
    152                 terms.amountRaw - terms.amountEffective
    153             } else if (terms.amountEffective > terms.amountRaw) {
    154                 terms.amountEffective - terms.amountRaw
    155             } else null
    156 
    157             if (fee != null) {
    158                 val feeStr = if (data.isCredit) {
    159                     stringResource(R.string.amount_negative, fee)
    160                 } else {
    161                     stringResource(R.string.amount_positive, fee)
    162                 }
    163                 Text(
    164                     modifier = Modifier.align(End),
    165                     text = feeStr,
    166                     style = MaterialTheme.typography.bodyLarge,
    167                     color = MaterialTheme.colorScheme.error,
    168                 )
    169             }
    170 
    171             if (terms is IncomingAccepting) {
    172                 CircularProgressIndicator(
    173                     modifier = Modifier
    174                         .padding(end = 64.dp)
    175                         .align(End),
    176                 )
    177             } else {
    178                 Button(
    179                     modifier = Modifier
    180                         .align(End)
    181                         .padding(top = 8.dp),
    182                     onClick = { onAccept(terms) },
    183                 ) {
    184                     Text(
    185                         text = if (terms is IncomingTosReview) {
    186                             stringResource(id = R.string.exchange_tos_view)
    187                         } else {
    188                             stringResource(id = data.button)
    189                         }
    190                     )
    191                 }
    192             }
    193         }
    194     }
    195 }
    196 
    197 @Composable
    198 fun ColumnScope.PeerPullErrorComposable(s: IncomingError) {
    199     val message = when (s.info.code) {
    200         WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE -> stringResource(R.string.payment_balance_insufficient)
    201         WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE -> stringResource(R.string.payment_balance_insufficient)
    202         else -> s.info.userFacingMsg
    203     }
    204 
    205     Text(
    206         modifier = Modifier
    207             .align(CenterHorizontally)
    208             .padding(horizontal = 32.dp),
    209         text = message,
    210         style = MaterialTheme.typography.headlineSmall,
    211         color = MaterialTheme.colorScheme.error,
    212     )
    213 }
    214 
    215 @Preview
    216 @Composable
    217 fun PeerPullCheckingPreview() {
    218     Surface {
    219         @SuppressLint("UnrememberedMutableState")
    220         val s = mutableStateOf(IncomingChecking)
    221         IncomingComposable(s, incomingPush) {}
    222     }
    223 }
    224 
    225 @Preview(showSystemUi = true)
    226 @Composable
    227 fun PeerPullTermsPreview() {
    228     Surface {
    229         val terms = IncomingTerms(
    230             amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
    231             amountEffective = Amount.fromString("TESTKUDOS", "42.423"),
    232             contractTerms = PeerContractTerms(
    233                 summary = "This is a long test summary that can be more than one line long for sure",
    234                 amount = Amount.fromString("TESTKUDOS", "23.42"),
    235             ),
    236             id = "ID123",
    237         )
    238 
    239         @SuppressLint("UnrememberedMutableState")
    240         val s = mutableStateOf(terms)
    241         IncomingComposable(s, incomingPush) {}
    242     }
    243 }
    244 
    245 @Preview(showSystemUi = true)
    246 @Composable
    247 fun PeerPullAcceptingPreview() {
    248     Surface {
    249         val terms = IncomingTerms(
    250             amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
    251             amountEffective = Amount.fromString("TESTKUDOS", "42.123"),
    252             contractTerms = PeerContractTerms(
    253                 summary = "This is a long test summary that can be more than one line long for sure",
    254                 amount = Amount.fromString("TESTKUDOS", "23.42"),
    255             ),
    256             id = "ID123",
    257         )
    258 
    259         @SuppressLint("UnrememberedMutableState")
    260         val s = mutableStateOf(IncomingAccepting(terms))
    261         IncomingComposable(s, incomingPush) {}
    262     }
    263 }
    264 
    265 @Preview
    266 @Composable
    267 fun PeerPullPayErrorPreview() {
    268     Surface {
    269         @SuppressLint("UnrememberedMutableState")
    270         val s = mutableStateOf(
    271             IncomingError(
    272                 info = TalerErrorInfo(WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "msg"),
    273             )
    274         )
    275         IncomingComposable(s, incomingPush) {}
    276     }
    277 }