taler-android

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

RefundUriFragment.kt (12288B)


      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.layout.Arrangement
     21 import androidx.compose.foundation.layout.Box
     22 import androidx.compose.foundation.layout.BoxWithConstraints
     23 import androidx.compose.foundation.layout.Column
     24 import androidx.compose.foundation.layout.Row
     25 import androidx.compose.foundation.layout.Spacer
     26 import androidx.compose.foundation.layout.fillMaxSize
     27 import androidx.compose.foundation.layout.fillMaxWidth
     28 import androidx.compose.foundation.layout.height
     29 import androidx.compose.foundation.layout.padding
     30 import androidx.compose.foundation.layout.size
     31 import androidx.compose.foundation.layout.statusBarsPadding
     32 import androidx.compose.material3.Button
     33 import androidx.compose.material3.ButtonDefaults
     34 import androidx.compose.material3.MaterialTheme
     35 import androidx.compose.material3.OutlinedButton
     36 import androidx.compose.material3.Text
     37 import androidx.compose.runtime.Composable
     38 import androidx.compose.ui.Alignment
     39 import androidx.compose.ui.Modifier
     40 import androidx.compose.ui.platform.ComposeView
     41 import androidx.compose.ui.platform.LocalConfiguration
     42 import androidx.compose.ui.platform.ViewCompositionStrategy
     43 import androidx.compose.ui.res.painterResource
     44 import androidx.compose.ui.res.stringResource
     45 import androidx.compose.ui.text.style.TextAlign
     46 import androidx.compose.ui.unit.dp
     47 import androidx.fragment.app.Fragment
     48 import androidx.fragment.app.activityViewModels
     49 import net.taler.merchantpos.MainActivity
     50 import net.taler.lib.android.AnimatedQrCodeComposable
     51 import net.taler.lib.android.TalerNfcService.Companion.hasNfc
     52 import net.taler.merchantpos.MainViewModel
     53 import net.taler.merchantpos.R
     54 import net.taler.merchantpos.compose.PosTheme
     55 import net.taler.merchantpos.showPosError
     56 
     57 class RefundUriFragment : Fragment() {
     58 
     59     private val model: MainViewModel by activityViewModels()
     60     private val refundManager by lazy { model.refundManager }
     61 
     62     override fun onCreateView(
     63         inflater: android.view.LayoutInflater,
     64         container: android.view.ViewGroup?,
     65         savedInstanceState: Bundle?,
     66     ) = ComposeView(requireContext()).apply {
     67         setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
     68         val result = refundManager.refundResult.value as? RefundResult.Success
     69         if (result == null) {
     70             requireActivity().showPosError(R.string.refund_state_missing)
     71             (requireActivity() as MainActivity).navigateBack()
     72             return@apply
     73         }
     74         setContent {
     75             RefundUriScreen(
     76                 result = result,
     77                 deviceHasNfc = hasNfc(requireContext()),
     78                 onAbort = {
     79                     refundManager.abortRefund()
     80                     (requireActivity() as MainActivity).navigateBack()
     81                 },
     82             )
     83         }
     84     }
     85 
     86     override fun onViewCreated(view: android.view.View, savedInstanceState: Bundle?) {
     87         super.onViewCreated(view, savedInstanceState)
     88         refundManager.refundReceived.observe(viewLifecycleOwner) { received ->
     89             if (received == true) {
     90                 refundManager.completeRefund()
     91                 (requireActivity() as MainActivity).apply {
     92                     navigateBack()
     93                     navigateBack()
     94                 }
     95             }
     96         }
     97     }
     98 }
     99 
    100 @Composable
    101 private fun RefundUriScreen(
    102     result: RefundResult.Success,
    103     deviceHasNfc: Boolean,
    104     onAbort: () -> Unit,
    105 ) {
    106     PosTheme {
    107         val introText = if (deviceHasNfc) {
    108             stringResource(R.string.refund_intro_nfc)
    109         } else {
    110             stringResource(R.string.refund_intro)
    111         }
    112         val isTabletLayout = LocalConfiguration.current.smallestScreenWidthDp >= 720
    113 
    114         if (isTabletLayout) {
    115             TabletRefundUriScreen(result, introText, onAbort)
    116         } else {
    117             PhoneRefundUriScreen(result, introText, onAbort)
    118         }
    119     }
    120 }
    121 
    122 @Composable
    123 private fun TabletRefundUriScreen(
    124     result: RefundResult.Success,
    125     introText: String,
    126     onAbort: () -> Unit,
    127 ) {
    128     Row(
    129         modifier = Modifier
    130             .fillMaxSize()
    131             .statusBarsPadding()
    132             .padding(16.dp),
    133         horizontalArrangement = Arrangement.spacedBy(16.dp),
    134     ) {
    135         Column(
    136             modifier = Modifier
    137                 .weight(0.54f)
    138                 .fillMaxSize(),
    139             horizontalAlignment = Alignment.CenterHorizontally,
    140         ) {
    141             Box(
    142                 modifier = Modifier
    143                     .fillMaxWidth()
    144                     .weight(1f),
    145                 contentAlignment = Alignment.Center,
    146             ) {
    147                 BoxWithConstraints(
    148                     modifier = Modifier
    149                         .fillMaxSize()
    150                         .padding(12.dp),
    151                     contentAlignment = Alignment.Center,
    152                 ) {
    153                     val qrSize = minOf(maxWidth, maxHeight)
    154                     Box(
    155                         modifier = Modifier.size(qrSize),
    156                         contentAlignment = Alignment.Center,
    157                     ) {
    158                         AnimatedQrCodeComposable(
    159                             link = result.refundUri,
    160                             logoPainter = painterResource(R.drawable.ic_taler_logo_qr),
    161                             modifier = Modifier.fillMaxSize(),
    162                         )
    163                     }
    164                 }
    165             }
    166         }
    167 
    168         Column(
    169             modifier = Modifier
    170                 .weight(0.46f)
    171                 .fillMaxSize()
    172                 .padding(horizontal = 8.dp),
    173             verticalArrangement = Arrangement.spacedBy(16.dp),
    174             horizontalAlignment = Alignment.CenterHorizontally,
    175         ) {
    176             Text(
    177                 text = introText,
    178                 style = MaterialTheme.typography.headlineSmall,
    179                 textAlign = TextAlign.Center,
    180             )
    181             Text(
    182                 text = result.amount.toString(),
    183                 style = MaterialTheme.typography.headlineMedium,
    184                 textAlign = TextAlign.Center,
    185             )
    186             Text(
    187                 text = stringResource(
    188                     R.string.refund_order_ref,
    189                     result.item.orderId,
    190                     result.reason,
    191                 ),
    192                 style = MaterialTheme.typography.bodyLarge,
    193                 textAlign = TextAlign.Center,
    194             )
    195             Row(
    196                 modifier = Modifier.fillMaxWidth(),
    197                 horizontalArrangement = Arrangement.spacedBy(12.dp),
    198             ) {
    199                 OutlinedButton(
    200                     onClick = onAbort,
    201                     modifier = Modifier.weight(1f),
    202                     colors = ButtonDefaults.outlinedButtonColors(
    203                         containerColor = MaterialTheme.colorScheme.error,
    204                         contentColor = MaterialTheme.colorScheme.onError,
    205                     ),
    206                 ) {
    207                     Text(stringResource(R.string.refund_abort))
    208                 }
    209             }
    210         }
    211     }
    212 }
    213 
    214 @Composable
    215 private fun PhoneRefundUriScreen(
    216     result: RefundResult.Success,
    217     introText: String,
    218     onAbort: () -> Unit,
    219 ) {
    220     Row(
    221         modifier = Modifier
    222             .fillMaxSize()
    223             .statusBarsPadding()
    224             .padding(16.dp),
    225         horizontalArrangement = Arrangement.spacedBy(16.dp),
    226     ) {
    227         Column(
    228             modifier = Modifier
    229                 .weight(0.5f)
    230                 .fillMaxSize(),
    231             verticalArrangement = Arrangement.Center,
    232             horizontalAlignment = Alignment.CenterHorizontally,
    233         ) {
    234             Box(
    235                 modifier = Modifier
    236                     .fillMaxWidth()
    237                     .weight(1f),
    238                 contentAlignment = Alignment.Center,
    239             ) {
    240                 BoxWithConstraints(
    241                     modifier = Modifier
    242                         .fillMaxSize()
    243                         .padding(12.dp),
    244                     contentAlignment = Alignment.Center,
    245                 ) {
    246                     val qrSize = minOf(maxWidth, maxHeight)
    247                     Box(
    248                         modifier = Modifier.size(qrSize),
    249                         contentAlignment = Alignment.Center,
    250                     ) {
    251                         AnimatedQrCodeComposable(
    252                             link = result.refundUri,
    253                             logoPainter = painterResource(R.drawable.ic_taler_logo_qr),
    254                             modifier = Modifier.fillMaxSize(),
    255                         )
    256                     }
    257                 }
    258             }
    259         }
    260 
    261         Column(
    262             modifier = Modifier
    263                 .weight(0.5f)
    264                 .fillMaxSize()
    265                 .padding(horizontal = 8.dp),
    266             verticalArrangement = Arrangement.Center,
    267             horizontalAlignment = Alignment.CenterHorizontally,
    268         ) {
    269             BoxWithConstraints(
    270                 modifier = Modifier.fillMaxSize(),
    271                 contentAlignment = Alignment.Center,
    272             ) {
    273                 val verticalGap = (maxHeight * 0.02f).coerceIn(2.dp, 8.dp)
    274                 val isCompactHeight = maxHeight < 420.dp
    275                 val introStyle = if (isCompactHeight) {
    276                     MaterialTheme.typography.titleMedium
    277                 } else {
    278                     MaterialTheme.typography.headlineSmall
    279                 }
    280                 val amountStyle = if (isCompactHeight) {
    281                     MaterialTheme.typography.titleLarge
    282                 } else {
    283                     MaterialTheme.typography.headlineMedium
    284                 }
    285                 val detailsStyle = if (isCompactHeight) {
    286                     MaterialTheme.typography.bodyMedium
    287                 } else {
    288                     MaterialTheme.typography.bodyLarge
    289                 }
    290                 Column(
    291                     horizontalAlignment = Alignment.CenterHorizontally,
    292                     verticalArrangement = Arrangement.spacedBy(verticalGap, Alignment.CenterVertically),
    293                 ) {
    294                     Text(
    295                         text = introText,
    296                         style = introStyle,
    297                         textAlign = TextAlign.Center,
    298                     )
    299                     Text(
    300                         text = result.amount.toString(),
    301                         style = amountStyle,
    302                         textAlign = TextAlign.Center,
    303                     )
    304                     Text(
    305                         text = stringResource(
    306                             R.string.refund_order_ref,
    307                             result.item.orderId,
    308                             result.reason,
    309                         ),
    310                         style = detailsStyle,
    311                         textAlign = TextAlign.Center,
    312                     )
    313                     OutlinedButton(
    314                         onClick = onAbort,
    315                         modifier = Modifier.fillMaxWidth(),
    316                         colors = ButtonDefaults.outlinedButtonColors(
    317                             containerColor = MaterialTheme.colorScheme.error,
    318                             contentColor = MaterialTheme.colorScheme.onError,
    319                         ),
    320                     ) {
    321                         Text(stringResource(R.string.refund_abort))
    322                     }
    323                 }
    324             }
    325         }
    326     }
    327 }
    328 
    329 @Composable
    330 internal fun RefundQrScreenContent(result: RefundResult.Success) {
    331     RefundUriScreen(
    332         result = result,
    333         deviceHasNfc = false,
    334         onAbort = {},
    335     )
    336 }