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 }