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 }