taler-android

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

RefundFragment.kt (10093B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2020 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.merchantpos.refund
     18 
     19 import android.os.Bundle
     20 import androidx.compose.foundation.gestures.detectTapGestures
     21 import androidx.compose.foundation.layout.Arrangement
     22 import androidx.compose.foundation.layout.Column
     23 import androidx.compose.foundation.layout.Spacer
     24 import androidx.compose.foundation.layout.fillMaxSize
     25 import androidx.compose.foundation.layout.fillMaxWidth
     26 import androidx.compose.foundation.layout.imePadding
     27 import androidx.compose.foundation.layout.padding
     28 import androidx.compose.foundation.text.KeyboardActions
     29 import androidx.compose.foundation.text.KeyboardOptions
     30 import androidx.compose.material3.Button
     31 import androidx.compose.material3.MaterialTheme
     32 import androidx.compose.material3.OutlinedButton
     33 import androidx.compose.material3.OutlinedTextField
     34 import androidx.compose.material3.Text
     35 import androidx.compose.runtime.LaunchedEffect
     36 import androidx.compose.runtime.Composable
     37 import androidx.compose.runtime.getValue
     38 import androidx.compose.runtime.mutableStateOf
     39 import androidx.compose.runtime.remember
     40 import androidx.compose.runtime.setValue
     41 import androidx.compose.ui.focus.FocusRequester
     42 import androidx.compose.ui.focus.focusRequester
     43 import androidx.compose.ui.Modifier
     44 import androidx.compose.ui.input.pointer.pointerInput
     45 import androidx.compose.ui.platform.ComposeView
     46 import androidx.compose.ui.platform.LocalFocusManager
     47 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
     48 import androidx.compose.ui.platform.ViewCompositionStrategy
     49 import androidx.compose.ui.res.stringResource
     50 import androidx.compose.ui.text.input.KeyboardType
     51 import androidx.compose.ui.text.input.ImeAction
     52 import androidx.compose.ui.unit.dp
     53 import androidx.fragment.app.Fragment
     54 import androidx.fragment.app.activityViewModels
     55 import androidx.compose.runtime.livedata.observeAsState
     56 import net.taler.merchantpos.MainActivity
     57 import net.taler.common.Amount
     58 import net.taler.common.AmountParserException
     59 import net.taler.merchantlib.OrderHistoryEntry
     60 import net.taler.merchantpos.MainViewModel
     61 import net.taler.merchantpos.PosDestination
     62 import net.taler.merchantpos.R
     63 import net.taler.merchantpos.compose.PosTheme
     64 import net.taler.merchantpos.refund.RefundResult.AlreadyRefunded
     65 import net.taler.merchantpos.refund.RefundResult.Error
     66 import net.taler.merchantpos.refund.RefundResult.PastDeadline
     67 import net.taler.merchantpos.refund.RefundResult.Success
     68 import net.taler.merchantpos.showPosError
     69 
     70 class RefundFragment : Fragment() {
     71 
     72     private val model: MainViewModel by activityViewModels()
     73     private val refundManager by lazy { model.refundManager }
     74 
     75     override fun onCreateView(
     76         inflater: android.view.LayoutInflater,
     77         container: android.view.ViewGroup?,
     78         savedInstanceState: Bundle?,
     79     ) = ComposeView(requireContext()).apply {
     80         setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
     81         setContent {
     82             val item = refundManager.toBeRefunded
     83             if (item == null) {
     84                 requireActivity().showPosError(R.string.refund_state_missing)
     85                 (requireActivity() as MainActivity).navigateBack()
     86                 return@setContent
     87             }
     88             RefundScreen(
     89                 item = item,
     90                 currencySpec = model.configManager.currencySpec,
     91                 onAbort = { (requireActivity() as MainActivity).navigateBack() },
     92                 onRefund = ::onRefundButtonClicked,
     93             )
     94         }
     95     }
     96 
     97     override fun onViewCreated(view: android.view.View, savedInstanceState: Bundle?) {
     98         refundManager.refundResult.observe(viewLifecycleOwner) { result ->
     99             when (result) {
    100                 is Error -> onError(R.string.refund_error_backend, result.msg)
    101                 PastDeadline -> onError(R.string.refund_error_deadline)
    102                 AlreadyRefunded -> onError(R.string.refund_error_already_refunded)
    103                 is Success -> (requireActivity() as MainActivity).navigateTo(PosDestination.RefundUri)
    104                 null -> Unit
    105             }
    106         }
    107     }
    108 
    109     private fun onRefundButtonClicked(item: OrderHistoryEntry, amount: Amount, reason: String) {
    110         refundManager.refund(item, amount, reason)
    111     }
    112 
    113     private fun onError(mainResId: Int, details: String = "") {
    114         requireActivity().showPosError(mainResId, details)
    115     }
    116 }
    117 
    118 @Composable
    119 private fun RefundScreen(
    120     item: OrderHistoryEntry,
    121     currencySpec: net.taler.common.CurrencySpecification?,
    122     initialReason: String? = null,
    123     onAbort: () -> Unit,
    124     onRefund: (OrderHistoryEntry, Amount, String) -> Unit,
    125 ) {
    126     var amountText by remember {
    127         mutableStateOf(item.amount.withSpec(currencySpec).amountStr)
    128     }
    129     var reason by remember { mutableStateOf(initialReason ?: "") }
    130     var errorText by remember { mutableStateOf<String?>(null) }
    131     val amountFocusRequester = remember { FocusRequester() }
    132     val reasonFocusRequester = remember { FocusRequester() }
    133     val focusManager = LocalFocusManager.current
    134     val keyboardController = LocalSoftwareKeyboardController.current
    135     val invalidAmountText = stringResource(R.string.refund_error_invalid_amount)
    136     val zeroAmountText = stringResource(R.string.refund_error_zero)
    137     val maxAmountTemplate = stringResource(R.string.refund_error_max_amount, "%s")
    138     val submitRefund = submit@{
    139         val maxAmount = item.amount.withSpec(currencySpec)
    140         val normalizedAmountText = amountText.trim()
    141         val inputAmount = try {
    142             if (normalizedAmountText.isEmpty()) {
    143                 maxAmount
    144             } else {
    145                 Amount.fromString(item.amount.currency, normalizedAmountText).withSpec(currencySpec)
    146             }
    147         } catch (_: AmountParserException) {
    148             errorText = invalidAmountText
    149             return@submit
    150         }
    151         if (inputAmount > maxAmount) {
    152             errorText = maxAmountTemplate.replace("%s", maxAmount.toString(showSymbol = false))
    153             return@submit
    154         }
    155         if (inputAmount.isZero()) {
    156             errorText = zeroAmountText
    157             return@submit
    158         }
    159         focusManager.clearFocus(force = true)
    160         keyboardController?.hide()
    161         onRefund(item, inputAmount, reason)
    162     }
    163 
    164     PosTheme {
    165         LaunchedEffect(Unit) {
    166             amountFocusRequester.requestFocus()
    167         }
    168         Column(
    169             modifier = Modifier
    170                 .fillMaxSize()
    171                 .imePadding()
    172                 .padding(20.dp),
    173             verticalArrangement = Arrangement.spacedBy(16.dp),
    174         ) {
    175             Text(item.summary, style = MaterialTheme.typography.bodyLarge)
    176             OutlinedTextField(
    177                 value = amountText,
    178                 onValueChange = {
    179                     amountText = it
    180                     errorText = null
    181                 },
    182                 modifier = Modifier
    183                     .fillMaxWidth()
    184                     .focusRequester(amountFocusRequester),
    185                 label = { Text(stringResource(R.string.refund_amount)) },
    186                 supportingText = errorText?.let { { Text(it) } },
    187                 keyboardOptions = KeyboardOptions(
    188                     keyboardType = KeyboardType.Decimal,
    189                     imeAction = ImeAction.Next,
    190                 ),
    191                 keyboardActions = KeyboardActions(
    192                     onNext = { reasonFocusRequester.requestFocus() },
    193                 ),
    194                 singleLine = true,
    195             )
    196             OutlinedTextField(
    197                 value = reason,
    198                 onValueChange = { reason = it },
    199                 modifier = Modifier
    200                     .fillMaxWidth()
    201                     .focusRequester(reasonFocusRequester),
    202                 label = { Text(stringResource(R.string.refund_reason)) },
    203                 keyboardOptions = KeyboardOptions(
    204                     keyboardType = KeyboardType.Text,
    205                     imeAction = ImeAction.Done,
    206                 ),
    207                 keyboardActions = KeyboardActions(
    208                     onDone = { submitRefund() },
    209                 ),
    210                 singleLine = true,
    211             )
    212             Button(
    213                 onClick = { submitRefund() },
    214                 modifier = Modifier.fillMaxWidth(),
    215             ) {
    216                 Text(stringResource(R.string.refund_confirm))
    217             }
    218             OutlinedButton(
    219                 onClick = onAbort,
    220                 modifier = Modifier.fillMaxWidth(),
    221             ) {
    222                 Text(stringResource(R.string.refund_abort))
    223             }
    224             Spacer(
    225                 modifier = Modifier
    226                     .weight(1f)
    227                     .fillMaxWidth()
    228                     .pointerInput(Unit) {
    229                         detectTapGestures(
    230                             onTap = {
    231                                 focusManager.clearFocus(force = true)
    232                                 keyboardController?.hide()
    233                             },
    234                         )
    235                     },
    236             )
    237         }
    238     }
    239 }
    240 
    241 @Composable
    242 internal fun RefundScreenContent(
    243     item: OrderHistoryEntry,
    244     currencySpec: net.taler.common.CurrencySpecification?,
    245     initialReason: String? = null,
    246 ) {
    247     RefundScreen(
    248         item = item,
    249         currencySpec = currencySpec,
    250         initialReason = initialReason,
    251         onAbort = {},
    252         onRefund = { _, _, _ -> },
    253     )
    254 }