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 }