summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml6
-rw-r--r--.gitmodules0
-rw-r--r--.idea/compiler.xml2
-rw-r--r--.idea/gradle.xml2
-rw-r--r--README.md11
-rwxr-xr-xbootstrap14
-rw-r--r--build.gradle15
-rw-r--r--cashier/build.gradle21
-rw-r--r--cashier/proguard-rules.pro2
-rw-r--r--cashier/src/main/java/net/taler/cashier/HttpHelper.kt12
-rw-r--r--cashier/src/main/java/net/taler/cashier/MainViewModel.kt2
-rw-r--r--cashier/src/main/java/net/taler/cashier/SignedAmount.kt8
-rw-r--r--cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt2
-rw-r--r--cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt7
-rw-r--r--cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt28
-rw-r--r--cashier/src/main/res/mipmap-anydpi-v26/ic_launcher.xml1
-rw-r--r--cashier/src/main/res/values-de/strings.xml2
-rw-r--r--cashier/src/main/res/values-es/strings.xml5
-rw-r--r--cashier/src/main/res/values-fi/strings.xml43
-rw-r--r--cashier/src/main/res/values-fr/strings.xml3
-rw-r--r--cashier/src/main/res/values-ru/strings.xml43
-rw-r--r--cashier/src/main/res/values-sv/strings.xml3
-rw-r--r--cashier/src/main/res/values-tr/strings.xml3
-rw-r--r--gradle.properties3
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--gsoc-23-ivan-avalos-report.md188
-rwxr-xr-ximport-svg-assets.sh51
-rw-r--r--merchant-lib/build.gradle19
-rw-r--r--merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt2
-rw-r--r--merchant-terminal/build.gradle25
-rw-r--r--merchant-terminal/fastlane/Fastfile1
-rw-r--r--merchant-terminal/proguard-rules.pro2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt5
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt4
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt10
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt5
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt4
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt4
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/CustomDialogFragment.kt78
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt4
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt28
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt6
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt8
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt4
-rw-r--r--merchant-terminal/src/main/res/drawable/ic_dialpad.xml25
-rw-r--r--merchant-terminal/src/main/res/layout/fragment_custom_dialog.xml117
-rw-r--r--merchant-terminal/src/main/res/layout/fragment_order.xml131
-rw-r--r--merchant-terminal/src/main/res/values-es/strings.xml4
-rw-r--r--merchant-terminal/src/main/res/values-fi/strings.xml72
-rw-r--r--merchant-terminal/src/main/res/values-fr/strings.xml2
-rw-r--r--merchant-terminal/src/main/res/values-tr/strings.xml4
-rw-r--r--merchant-terminal/src/main/res/values/strings.xml4
-rw-r--r--taler-kotlin-android/build.gradle16
-rw-r--r--taler-kotlin-android/src/main/AndroidManifest.xml3
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/Amount.kt64
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt18
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt10
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/CurrencySpecification.kt36
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/Event.kt4
-rw-r--r--taler-kotlin-android/src/test/java/net/taler/common/AmountTest.kt157
-rw-r--r--wallet/.gitignore2
-rw-r--r--wallet/build.gradle114
-rw-r--r--wallet/fastlane/Fastfile3
-rw-r--r--wallet/proguard-rules.pro4
-rw-r--r--wallet/src/main/AndroidManifest.xml18
-rw-r--r--wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt272
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainActivity.kt193
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainFragment.kt33
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainViewModel.kt213
-rw-r--r--wallet/src/main/java/net/taler/wallet/NetworkManager.kt64
-rw-r--r--wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt70
-rw-r--r--wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt77
-rw-r--r--wallet/src/main/java/net/taler/wallet/UriInputFragment.kt7
-rw-r--r--wallet/src/main/java/net/taler/wallet/Utils.kt45
-rw-r--r--wallet/src/main/java/net/taler/wallet/WalletApp.kt (renamed from wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt)17
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt63
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt95
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt84
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt44
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt201
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt254
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt82
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt40
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt137
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/Balances.kt57
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt46
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt226
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt (renamed from wallet/src/main/java/net/taler/wallet/settings/BackupSettingsFragment.kt)27
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt70
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt42
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt46
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt32
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/Utils.kt6
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt22
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt41
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt4
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/MakeBitcoinDepositComposable.kt23
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt131
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt83
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt62
-rw-r--r--wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt163
-rw-r--r--wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt66
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt3
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt28
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt4
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt75
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt118
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt38
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt111
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt48
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt49
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt184
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt115
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt177
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt84
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt19
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt26
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt47
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt174
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt128
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt87
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt10
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt10
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt19
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt280
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt58
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt141
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.kt149
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt218
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt69
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt123
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.kt149
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt34
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt112
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt73
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt39
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt41
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt104
-rw-r--r--wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt187
-rw-r--r--wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt57
-rw-r--r--wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt17
-rw-r--r--wallet/src/main/java/net/taler/wallet/refund/RefundPaymentInfo.kt39
-rw-r--r--wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt (renamed from wallet/src/main/java/net/taler/wallet/transactions/TransactionTipFragment.kt)98
-rw-r--r--wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt86
-rw-r--r--wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt107
-rw-r--r--wallet/src/main/java/net/taler/wallet/tip/AlreadyAcceptedFragment.kt50
-rw-r--r--wallet/src/main/java/net/taler/wallet/tip/PromptTipFragment.kt158
-rw-r--r--wallet/src/main/java/net/taler/wallet/tip/TipManager.kt100
-rw-r--r--wallet/src/main/java/net/taler/wallet/tip/TipResponses.kt59
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt116
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/DeleteTransactionComposable.kt55
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/ErrorTransactionComposable.kt116
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt129
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt11
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt182
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionDummyFragment.kt64
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionLinkComposable.kt86
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt163
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt174
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt46
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt44
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt105
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt46
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt99
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt120
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt332
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt56
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt113
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt111
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt155
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt245
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt8
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt69
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt171
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt161
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt326
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt112
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt93
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt93
-rw-r--r--wallet/src/main/res/drawable/ic_archive.xml21
-rw-r--r--wallet/src/main/res/drawable/ic_baseline_account_tree.xml10
-rw-r--r--wallet/src/main/res/drawable/ic_baseline_backup.xml10
-rw-r--r--wallet/src/main/res/drawable/ic_baseline_vpn_key.xml10
-rw-r--r--wallet/src/main/res/drawable/ic_fail.xml5
-rw-r--r--wallet/src/main/res/drawable/ic_resume.xml5
-rw-r--r--wallet/src/main/res/drawable/ic_retry.xml5
-rw-r--r--wallet/src/main/res/drawable/ic_suspend.xml5
-rw-r--r--wallet/src/main/res/drawable/ic_unarchive.xml4
-rw-r--r--wallet/src/main/res/drawable/transaction_loss.xml26
-rw-r--r--wallet/src/main/res/layout/app_content_main.xml28
-rw-r--r--wallet/src/main/res/layout/dialog_exchange_add.xml2
-rw-r--r--wallet/src/main/res/layout/fragment_already_accepted.xml52
-rw-r--r--wallet/src/main/res/layout/fragment_already_paid.xml52
-rw-r--r--wallet/src/main/res/layout/fragment_error.xml10
-rw-r--r--wallet/src/main/res/layout/fragment_exchange_fees.xml4
-rw-r--r--wallet/src/main/res/layout/fragment_main.xml5
-rw-r--r--wallet/src/main/res/layout/fragment_manual_withdraw.xml179
-rw-r--r--wallet/src/main/res/layout/fragment_pending_operations.xml34
-rw-r--r--wallet/src/main/res/layout/fragment_prompt_tip.xml257
-rw-r--r--wallet/src/main/res/layout/fragment_prompt_withdraw.xml66
-rw-r--r--wallet/src/main/res/layout/fragment_review_exchange_tos.xml2
-rw-r--r--wallet/src/main/res/layout/fragment_transaction_payment.xml128
-rw-r--r--wallet/src/main/res/layout/fragment_transaction_withdrawal.xml153
-rw-r--r--wallet/src/main/res/layout/fragment_transactions.xml9
-rw-r--r--wallet/src/main/res/layout/fragment_uri_input.xml10
-rw-r--r--wallet/src/main/res/layout/list_item_balance.xml29
-rw-r--r--wallet/src/main/res/layout/list_item_pending_operation.xml47
-rw-r--r--wallet/src/main/res/layout/list_item_transaction.xml23
-rw-r--r--wallet/src/main/res/layout/nav_header_main.xml2
-rw-r--r--wallet/src/main/res/menu/activity_main_drawer.xml5
-rw-r--r--wallet/src/main/res/menu/exchange.xml6
-rw-r--r--wallet/src/main/res/menu/exchange_list.xml (renamed from wallet/src/main/res/menu/pending_operations.xml)9
-rw-r--r--wallet/src/main/res/menu/global_dev.xml24
-rw-r--r--wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml1
-rw-r--r--wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml1
-rw-r--r--wallet/src/main/res/navigation/nav_graph.xml205
-rw-r--r--wallet/src/main/res/values-ca/strings.xml265
-rw-r--r--wallet/src/main/res/values-de/strings.xml191
-rw-r--r--wallet/src/main/res/values-es/strings.xml187
-rw-r--r--wallet/src/main/res/values-fi/strings.xml288
-rw-r--r--wallet/src/main/res/values-fr/strings.xml109
-rw-r--r--wallet/src/main/res/values-it/strings.xml109
-rw-r--r--wallet/src/main/res/values-night/styles.xml28
-rw-r--r--wallet/src/main/res/values-nl/strings.xml (renamed from wallet/src/main/res/menu/transactions_detail.xml)9
-rw-r--r--wallet/src/main/res/values-tr/strings.xml13
-rw-r--r--wallet/src/main/res/values-v27/styles.xml26
-rw-r--r--wallet/src/main/res/values/colors.xml63
-rw-r--r--wallet/src/main/res/values/defaults.xml2
-rw-r--r--wallet/src/main/res/values/strings.xml102
-rw-r--r--wallet/src/main/res/values/styles.xml99
-rw-r--r--wallet/src/main/res/xml/network_security_config.xml6
-rw-r--r--wallet/src/main/res/xml/settings_backup.xml51
-rw-r--r--wallet/src/main/res/xml/settings_main.xml24
-rw-r--r--wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt5
-rw-r--r--wallet/src/test/java/net/taler/wallet/tip/TipResponsesTest.kt83
237 files changed, 10252 insertions, 5196 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9b4c4c0..227859a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,8 @@
image: briar/ci-image-android:latest
+variables:
+ JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
+
cache:
paths:
- .gradle/wrapper
@@ -9,9 +12,6 @@ stages:
- test
- deploy
-before_script:
- - ./bootstrap
-
include:
- local: 'cashier/.gitlab-ci.yml'
- local: 'merchant-lib/.gitlab-ci.yml'
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index e69de29..0000000
--- a/.gitmodules
+++ /dev/null
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 0bb9609..634c739 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -11,7 +11,7 @@
<entry name="!?*.kt" />
<entry name="!?*.clj" />
</wildcardResourcePatterns>
- <bytecodeTargetLevel target="11">
+ <bytecodeTargetLevel target="17">
<module name="common_commonMain" target="1.6" />
<module name="common_commonTest" target="1.6" />
<module name="common_jvmMain" target="1.6" />
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index aa4060f..9b17a13 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -7,7 +7,7 @@
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
- <option name="gradleJvm" value="Embedded JDK" />
+ <option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
diff --git a/README.md b/README.md
index cdc9574..16278ea 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,6 @@ The official location is:
## Structure
-* [**anastasis-ui**](/anastasis-ui) - an Android library for sync and backup UI.
- See [Anastasis](https://docs.taler.net/anastasis.html).
* [**cashier**](/cashier) - an Android app that enables you to take cash and give out electronic cash
* [**merchant-lib**](/merchant-lib) - a library providing communication with a merchant backend
to be used by the point of sale app below.
@@ -19,18 +17,11 @@ The official location is:
and let the customer make a Taler payment via QR code or NFC.
* [**taler-kotlin-android**](/taler-kotlin-android) - an Android library containing common code
needed by more than one Taler Android app.
-* [**multiplatform**](/multiplatform) - multi-platform Taler libraries
- included as a [git submodule](https://git-scm.com/docs/git-submodule)
- from [`https://git.taler.net/wallet-kotlin.git`](https://git.taler.net/wallet-kotlin.git/)
* [**wallet**](/wallet) - a GNU Taler wallet Android app
## Building
-Before building anything, you should initialize and update the submodules by running
-
- $ ./bootstrap
-
-Then, you can get a list of possible build tasks like this:
+You can get a list of possible build tasks like this:
$ ./gradlew tasks
diff --git a/bootstrap b/bootstrap
deleted file mode 100755
index 8efb636..0000000
--- a/bootstrap
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/sh
-
-# Bootstrap the repository. Used when the repository is checked out from git.
-# When using the source tarball, running this script is not necessary.
-
-set -eu
-
-if ! git --version >/dev/null; then
- echo "git not installed"
- exit 1
-fi
-
-git submodule sync --recursive
-git submodule update --init --recursive
diff --git a/build.gradle b/build.gradle
index 93d8a9b..9fc27f6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,15 +1,12 @@
buildscript {
ext {
- kotlin_version = '1.7.20' // observe compatibility with compose compiler
- ktor_version = '2.1.3'
- nav_version = '2.5.3'
- material_version = '1.7.0'
- lifecycle_version = '2.5.1'
+ kotlin_version = '1.9.21' // observe compatibility with compose compiler
+ ktor_version = '2.3.5'
+ nav_version = '2.7.6'
+ material_version = '1.11.0'
+ lifecycle_version = '2.6.2'
constraintlayout_version = '2.1.4'
- compose_version = '1.3.2'
junit_version = "4.13.2"
- // check https://android-rebuilds.beuc.net/ for availability of free build tools
- build_tools_version = "33.0.1"
// should debug build types be minified with D8 as well? good for catching issues early
minify_debug = true
}
@@ -18,7 +15,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.3.1'
+ classpath 'com.android.tools.build:gradle:8.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
diff --git a/cashier/build.gradle b/cashier/build.gradle
index c238513..074124f 100644
--- a/cashier/build.gradle
+++ b/cashier/build.gradle
@@ -23,17 +23,17 @@ plugins {
android {
namespace 'net.taler.cashier'
- compileSdkVersion 33
- //noinspection GradleDependency
- buildToolsVersion "$build_tools_version"
+ compileSdk 34
defaultConfig {
applicationId "net.taler.cashier"
minSdkVersion 23
- targetSdkVersion 32
- versionCode 2
- versionName "0.2"
+ targetSdkVersion 33
+ versionCode 3
+ versionName "0.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ buildConfigField("String", "BACKEND_API_VERSION", "\"4:0:0\"")
}
buildTypes {
@@ -48,15 +48,16 @@ android {
}
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17"
}
buildFeatures {
+ buildConfig = true
viewBinding = true
}
packagingOptions {
@@ -67,7 +68,7 @@ android {
lint {
abortOnError true
- ignore 'WrongConstant'
+ warning 'WrongConstant'
ignoreWarnings false
}
}
diff --git a/cashier/proguard-rules.pro b/cashier/proguard-rules.pro
index ced7b5c..9928f24 100644
--- a/cashier/proguard-rules.pro
+++ b/cashier/proguard-rules.pro
@@ -26,3 +26,5 @@
# androidx.security:security-crypto
# https://github.com/google/tink/issues/361
-keep class * extends com.google.crypto.tink.shaded.protobuf.GeneratedMessageLite { *; }
+
+-dontwarn org.slf4j.impl.StaticLoggerBinder
diff --git a/cashier/src/main/java/net/taler/cashier/HttpHelper.kt b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
index fd48b2d..69cc46f 100644
--- a/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
+++ b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
@@ -50,11 +50,13 @@ object HttpHelper {
Log.e(TAG, "Error retrieving $url", e)
return HttpJsonResult.Error(0)
}
- return if (response.code == 200 && response.body != null) {
+ return if (response.code == 204) {
+ HttpJsonResult.Success(JSONObject())
+ } else if (response.code in 200..299 && response.body != null) {
val jsonObject = JSONObject(response.body!!.string())
HttpJsonResult.Success(jsonObject)
} else {
- Log.e(TAG, "Received status ${response.code} from $url expected 200")
+ Log.e(TAG, "Received status ${response.code} from $url expected 2xx")
HttpJsonResult.Error(response.code, getErrorBody(response))
}
}
@@ -76,11 +78,13 @@ object HttpHelper {
Log.e(TAG, "Error retrieving $url", e)
return HttpJsonResult.Error(0)
}
- return if (response.code == 200 && response.body != null) {
+ return if (response.code == 204) {
+ HttpJsonResult.Success(JSONObject())
+ } else if (response.code in 200..299 && response.body != null) {
val jsonObject = JSONObject(response.body!!.string())
HttpJsonResult.Success(jsonObject)
} else {
- Log.e(TAG, "Received status ${response.code} from $url expected 200")
+ Log.e(TAG, "Received status ${response.code} from $url expected 2xx")
HttpJsonResult.Error(response.code, getErrorBody(response))
}
}
diff --git a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
index 21d0209..2196e78 100644
--- a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
+++ b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
@@ -64,7 +64,7 @@ class MainViewModel(private val app: Application) : AndroidViewModel(app) {
fun getBalance() = viewModelScope.launch(Dispatchers.IO) {
check(configManager.hasConfig()) { "No config to get balance" }
val config = configManager.config
- val url = "${config.bankUrl}/access-api/accounts/${config.username}"
+ val url = "${config.bankUrl}/accounts/${config.username}"
Log.d(TAG, "Checking balance at $url")
val result = when (val response = makeJsonGetRequest(url, config)) {
is HttpJsonResult.Success -> {
diff --git a/cashier/src/main/java/net/taler/cashier/SignedAmount.kt b/cashier/src/main/java/net/taler/cashier/SignedAmount.kt
index 4f624ae..45bc3af 100644
--- a/cashier/src/main/java/net/taler/cashier/SignedAmount.kt
+++ b/cashier/src/main/java/net/taler/cashier/SignedAmount.kt
@@ -23,8 +23,10 @@ data class SignedAmount(
val amount: Amount
) {
- override fun toString(): String {
- return if (positive) "$amount" else "-$amount"
- }
+ override fun toString() = toString(showSymbol = true)
+ fun toString(showSymbol: Boolean) = amount.toString(
+ showSymbol = showSymbol,
+ negative = !positive,
+ )
}
diff --git a/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt b/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
index 6e8a3db..3085bef 100644
--- a/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
+++ b/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
@@ -39,7 +39,7 @@ import net.taler.cashier.databinding.FragmentConfigBinding
import net.taler.common.exhaustive
import net.taler.common.showError
-private const val URL_BANK_TEST = "https://bank.demo.taler.net/demobanks/default"
+private const val URL_BANK_TEST = "https://bank.demo.taler.net"
private const val URL_BANK_TEST_REGISTER = "https://bank.demo.taler.net/webui/#/register"
class ConfigFragment : Fragment() {
diff --git a/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt
index c79fd12..50b1faf 100644
--- a/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt
+++ b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt
@@ -38,12 +38,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import net.taler.cashier.BuildConfig
import net.taler.cashier.Response
import net.taler.cashier.Response.Companion.response
import net.taler.common.Version
import net.taler.common.getIncompatibleStringOrNull
-val VERSION_BANK = Version(0, 0, 0)
+val VERSION_BANK = Version.parse(BuildConfig.BACKEND_API_VERSION)!!
private const val PREF_NAME = "net.taler.cashier.prefs"
private const val PREF_KEY_BANK_URL = "bankUrl"
private const val PREF_KEY_USERNAME = "username"
@@ -114,7 +115,7 @@ class ConfigManager(
}
private suspend fun checkConfig(config: Config) = withContext(Dispatchers.IO) {
- val url = "${config.bankUrl}/integration-api/config"
+ val url = "${config.bankUrl}/config"
Log.d(TAG, "Checking config: $url")
val configResponse = response {
httpClient.get(url).body<ConfigResponse>()
@@ -125,7 +126,7 @@ class ConfigManager(
// we need to check an endpoint that requires authentication as well
// to see if the credentials are valid
val balanceResponse = response {
- val authUrl = "${config.bankUrl}/access-api/accounts/${config.username}"
+ val authUrl = "${config.bankUrl}/accounts/${config.username}"
Log.d(TAG, "Checking auth: $authUrl")
httpClient.get(authUrl) {
header(Authorization, config.basicAuth)
diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
index c6c39d0..487475d 100644
--- a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
@@ -94,7 +94,7 @@ class WithdrawManager(
mWithdrawResult.value = null
mWithdrawAmount.value = amount
scope.launch(Dispatchers.IO) {
- val url = "${config.bankUrl}/access-api/accounts/${config.username}/withdrawals"
+ val url = "${config.bankUrl}/accounts/${config.username}/withdrawals"
Log.d(TAG, "Starting withdrawal at $url")
val map = mapOf("amount" to amount.toJSONString())
val body = JSONObject(map)
@@ -154,31 +154,31 @@ class WithdrawManager(
private fun checkWithdrawStatus(withdrawalId: String) = scope.launch(Dispatchers.IO) {
val url =
- "${config.bankUrl}/access-api/accounts/${config.username}/withdrawals/${withdrawalId}"
+ "${config.bankUrl}/withdrawals/${withdrawalId}"
Log.d(TAG, "Checking withdraw status at $url")
val response = makeJsonGetRequest(url, config)
if (response !is Success) return@launch // ignore errors and continue trying
val oldStatus = mWithdrawStatus.value
try {
- when {
- response.json.getBoolean("aborted") -> {
+ when(response.json.getString("status")) {
+ "selected" -> {
+ // only update status, if there's none, yet
+ // so we don't re-notify or overwrite newer status info
+ if (oldStatus == null) {
+ mWithdrawStatus.postValue(WithdrawStatus.SelectionDone(withdrawalId))
+ }
+ }
+ "aborted" -> {
cancelWithdrawStatusCheck()
mWithdrawStatus.postValue(WithdrawStatus.Aborted)
}
- response.json.getBoolean("confirmation_done") -> {
+ "confirmed" -> {
if (oldStatus !is WithdrawStatus.Success) {
cancelWithdrawStatusCheck()
mWithdrawStatus.postValue(WithdrawStatus.Success)
viewModel.getBalance()
}
}
- response.json.getBoolean("selection_done") -> {
- // only update status, if there's none, yet
- // so we don't re-notify or overwrite newer status info
- if (oldStatus == null) {
- mWithdrawStatus.postValue(WithdrawStatus.SelectionDone(withdrawalId))
- }
- }
}
} catch (e: Exception) {
mWithdrawStatus.postValue(WithdrawStatus.Error(e.toString()))
@@ -206,7 +206,7 @@ class WithdrawManager(
private fun abort(withdrawalId: String) = scope.launch(Dispatchers.IO) {
val url =
- "${config.bankUrl}/access-api/accounts/${config.username}/withdrawals/${withdrawalId}/abort"
+ "${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/abort"
Log.d(TAG, "Aborting withdrawal at $url")
makeJsonPostRequest(url, JSONObject(), config)
}
@@ -216,7 +216,7 @@ class WithdrawManager(
mWithdrawStatus.value = WithdrawStatus.Confirming
scope.launch(Dispatchers.IO) {
val url =
- "${config.bankUrl}/access-api/accounts/${config.username}/withdrawals/${withdrawalId}/confirm"
+ "${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/confirm"
Log.d(TAG, "Confirming withdrawal at $url")
when (val response = makeJsonPostRequest(url, JSONObject(), config)) {
is Success -> {
diff --git a/cashier/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/cashier/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index ac94b34..0648fb4 100644
--- a/cashier/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/cashier/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> \ No newline at end of file
diff --git a/cashier/src/main/res/values-de/strings.xml b/cashier/src/main/res/values-de/strings.xml
index 4b254d4..626d909 100644
--- a/cashier/src/main/res/values-de/strings.xml
+++ b/cashier/src/main/res/values-de/strings.xml
@@ -33,7 +33,7 @@
<string name="config_username">Benutzername</string>
<string name="withdraw_error_timeout">Bitte lassen Sie ein Wallet die Abhebung auslösen.</string>
<string name="action_about">Info</string>
- <string name="ok">OK</string>
+ <string name="ok">Bestätigen</string>
<string name="about_title">GNU Taler Kassierer</string>
<string name="about_license">Lizenz: %s</string>
<string name="about_version">Version: %s</string>
diff --git a/cashier/src/main/res/values-es/strings.xml b/cashier/src/main/res/values-es/strings.xml
index cfbfda6..c9ecc6b 100644
--- a/cashier/src/main/res/values-es/strings.xml
+++ b/cashier/src/main/res/values-es/strings.xml
@@ -3,11 +3,11 @@
<string name="config_username">Nombre de usuario</string>
<string name="config_password">Contraseña</string>
<string name="app_name">Cajero Taler</string>
- <string name="config_bank_url">Banco dirección API</string>
+ <string name="config_bank_url">Dirección API del banco</string>
<string name="config_button_save">Guardar</string>
<string name="config_bank_url_error">La dirección es inválida.</string>
<string name="config_username_error">Por favor inserta tu nombre de usuario!</string>
- <string name="config_error">Error recuperando la configuración : %s</string>
+ <string name="config_error">Error recuperando la configuración</string>
<string name="config_error_auth">Usuario o contraseña inválido!</string>
<string name="config_demo_hint">Para realizar pruebas, puedes &lt;a href=%s&gt;crear una cuenta de test en el banco demo&lt;/a&gt;.</string>
<string name="balance_current_label">Balance actual</string>
@@ -39,4 +39,5 @@
<string name="about_license">Licencia: %s</string>
<string name="about_copyright">Copyright: %s</string>
<string name="about_supported_bank_api">Banco Versión API: %s</string>
+ <string name="withdraw_error_currency_mismatch">Error: El banco reportó una divisa diferente</string>
</resources> \ No newline at end of file
diff --git a/cashier/src/main/res/values-fi/strings.xml b/cashier/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000..030008f
--- /dev/null
+++ b/cashier/src/main/res/values-fi/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="config_bank_url">Pankin API-osoite</string>
+ <string name="config_username">Käyttäjänimi</string>
+ <string name="config_password">Salasana</string>
+ <string name="config_bank_url_error">Osoite on virheellinen.</string>
+ <string name="config_username_error">Anna käyttäjätunnuksesi!</string>
+ <string name="app_name">Taler kassa</string>
+ <string name="config_button_save">Tallenna</string>
+ <string name="config_error">Virhe noudettaessa määritystä</string>
+ <string name="config_error_auth">Virheellinen käyttäjätunnus tai salasana!</string>
+ <string name="balance_current_label">Nykyinen saldo</string>
+ <string name="balance_error">ERROR: %s</string>
+ <string name="balance_offline">Offline-tilassa. Muodosta yhteys Internetiin.</string>
+ <string name="ok">OK</string>
+ <string name="action_reconfigure">Määritä uudelleen</string>
+ <string name="action_lock">Lukitse</string>
+ <string name="action_about">Noin</string>
+ <string name="withdraw_input_amount">Summa</string>
+ <string name="withdraw_into">Kuinka paljon sähköistä käteistä pitäisi nostaa?</string>
+ <string name="withdraw_error_zero">Anna positiivinen summa!</string>
+ <string name="withdraw_error_currency_mismatch">Virhe: Pankki ilmoitti toisen valuutan</string>
+ <string name="withdraw_error_fetch">Virhe kommunikoitaessa pankin kanssa: %s</string>
+ <string name="withdraw_error_timeout">Mikään lompakko ei yrittänyt nostaa. Yritä uudelleen.</string>
+ <string name="withdraw_button_confirm">Nosta</string>
+ <string name="transaction_intro">Skannaa koodi Taler-lompakkosovelluksella nostaaksesi:</string>
+ <string name="transaction_intro_nfc">Skannaa koodi tai käytä NFC:tä Taler-lompakkosovelluksen kanssa nostaaksesi:</string>
+ <string name="transaction_intro_scanned">Vahvista tapahtuma!</string>
+ <string name="transaction_confirm">Vahvista</string>
+ <string name="transaction_abort">Keskeytä</string>
+ <string name="transaction_aborted">Toiminta keskeytetty</string>
+ <string name="transaction_button_back">Takaisin</string>
+ <string name="transaction_last_success">Viimeisin tapahtuma: %s nostettu</string>
+ <string name="transaction_last_aborted">Viimeisin tapahtuma: Keskeytetty</string>
+ <string name="transaction_last_error">Viimeisin tapahtuma: epäonnistui</string>
+ <string name="about_title">GNU Taler Kassa</string>
+ <string name="about_version">Versio: %s</string>
+ <string name="about_license">Lisenssi: %s</string>
+ <string name="about_copyright">Tekijänoikeus: %s</string>
+ <string name="about_supported_bank_api">Pankin API-versio: %s</string>
+ <string name="config_demo_hint">Testausta varten voit &lt;a href=%s&gt;luoda testitilin demopankkiin&lt;/a&gt;.</string>
+ <string name="withdraw_error_insufficient_balance">Riittämätön saldo</string>
+</resources> \ No newline at end of file
diff --git a/cashier/src/main/res/values-fr/strings.xml b/cashier/src/main/res/values-fr/strings.xml
index 0d2fbd2..10ba6ea 100644
--- a/cashier/src/main/res/values-fr/strings.xml
+++ b/cashier/src/main/res/values-fr/strings.xml
@@ -7,7 +7,7 @@
<string name="config_button_save">Enregistrer</string>
<string name="config_bank_url_error">L\'adresse est invalide.</string>
<string name="config_username_error">Veuillez saisir votre identifiant !</string>
- <string name="config_error">Erreur lors de la récupération de la configuration : %s</string>
+ <string name="config_error">Erreur lors de la récupération de la configuration</string>
<string name="config_error_auth">Identifiant ou mot de passe invalide !</string>
<string name="config_demo_hint" tools:ignore="StringFormatInvalid">Pour les tests, vous pouvez &lt;a href=%s&gt;créer un compte de test dans la banque de démonstration&lt;/a&gt; .</string>
<string name="balance_current_label">Solde actuel</string>
@@ -39,4 +39,5 @@
<string name="about_copyright">Droits d\'auteur : %s</string>
<string name="about_supported_bank_api">Version Bank API : %s</string>
<string name="about_title">GNU Taler Cashier</string>
+ <string name="withdraw_error_currency_mismatch">Erreur : La banque a déclaré une devise différente</string>
</resources> \ No newline at end of file
diff --git a/cashier/src/main/res/values-ru/strings.xml b/cashier/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000..377f3f7
--- /dev/null
+++ b/cashier/src/main/res/values-ru/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">Taler Кассир</string>
+ <string name="config_bank_url">Адресс API банка</string>
+ <string name="config_username">Имя пользователя</string>
+ <string name="config_password">Пароль</string>
+ <string name="config_button_save">Сохранить</string>
+ <string name="config_error_auth">Неправильное имя пользователя или пароль!</string>
+ <string name="config_demo_hint">Для тестирования, вы можете &lt;a href=%s&gt;создать тестовую учётную запист в демонстрационном банке&lt;/a&gt;.</string>
+ <string name="balance_current_label">Теущий баланс</string>
+ <string name="balance_error">ОШИБКА: %s</string>
+ <string name="balance_offline">Нет связи. Пожалуйста подключитесь к интернету.</string>
+ <string name="action_reconfigure">Переконфигурировать</string>
+ <string name="action_lock">Заблокировать</string>
+ <string name="action_about">О программе</string>
+ <string name="withdraw_input_amount">Количество</string>
+ <string name="ok">OK</string>
+ <string name="config_bank_url_error">Адрес неправильный.</string>
+ <string name="config_username_error">Пожалуйста введите ваше имя пользователя!</string>
+ <string name="config_error">Ошибка при получении конфигурации</string>
+ <string name="withdraw_into">Сколько e-cash должно быть списано?</string>
+ <string name="withdraw_error_zero">Введите позитивное количество!</string>
+ <string name="withdraw_error_insufficient_balance">Недостаточный баланс</string>
+ <string name="withdraw_error_currency_mismatch">Ошибка: Банк использует другую валюту</string>
+ <string name="withdraw_error_fetch">Ошибка при связи с банком: %s</string>
+ <string name="withdraw_error_timeout">Никакой кошелёк не попробоавл списать. Пожалуйста попробуйте ещё раз.</string>
+ <string name="withdraw_button_confirm">Списание</string>
+ <string name="transaction_intro">Отсканируйте код приложением Кошелька Taler чтобы списать:</string>
+ <string name="transaction_intro_nfc">Отсканируйте код или используйте NFC в приложении Кошелька Taler чтобы списать:</string>
+ <string name="transaction_intro_scanned">Пожалуйста подтвердите транзакцию!</string>
+ <string name="transaction_confirm">Подтвердить</string>
+ <string name="transaction_abort">Подробности</string>
+ <string name="transaction_aborted">Транзакция прервана</string>
+ <string name="transaction_button_back">Перейти назад</string>
+ <string name="transaction_last_success">Последняя Транзакция: %s списано</string>
+ <string name="transaction_last_aborted">Последняя Транзакция: Прервана</string>
+ <string name="transaction_last_error">Последняя Транзакция: Неуспешна</string>
+ <string name="about_title">Кассир GNU Taler</string>
+ <string name="about_version">Версия: %s</string>
+ <string name="about_license">Лицензия: %s</string>
+ <string name="about_copyright">Авторские права: %s</string>
+ <string name="about_supported_bank_api">Версия API банка: %s</string>
+</resources> \ No newline at end of file
diff --git a/cashier/src/main/res/values-sv/strings.xml b/cashier/src/main/res/values-sv/strings.xml
index 1794aa2..967cedb 100644
--- a/cashier/src/main/res/values-sv/strings.xml
+++ b/cashier/src/main/res/values-sv/strings.xml
@@ -5,7 +5,7 @@
<string name="config_password">Lösenord</string>
<string name="config_button_save">Spara</string>
<string name="config_username_error">Vänligen ange ditt användarnamn!</string>
- <string name="config_error">Fel vid hämtning av konfiguration: %s</string>
+ <string name="config_error">Fel vid hämtning av konfiguration</string>
<string name="config_error_auth">Ogiltigt användarnamn eller lösenord!</string>
<string name="balance_current_label">Nuvarande saldo</string>
<string name="balance_error">FEL: %s</string>
@@ -39,4 +39,5 @@
<string name="config_bank_url_error">Adressen är ogiltig.</string>
<string name="config_demo_hint">För testning kan du &lt;a href=%s&gt;skapa ett testkonto i demobanken&lt;/a&gt; .</string>
<string name="withdraw_error_fetch">Fel vid kommunikation med bank: %s</string>
+ <string name="withdraw_error_currency_mismatch">Fel: Banken har rapporterat en annan valuta</string>
</resources> \ No newline at end of file
diff --git a/cashier/src/main/res/values-tr/strings.xml b/cashier/src/main/res/values-tr/strings.xml
index b1afb2e..09db26f 100644
--- a/cashier/src/main/res/values-tr/strings.xml
+++ b/cashier/src/main/res/values-tr/strings.xml
@@ -7,7 +7,7 @@
<string name="config_bank_url_error">Bu adres geçersiz.</string>
<string name="config_username_error">Lütfen kullanıcı adınızı giriniz!</string>
<string name="app_name">Taler Kasiyer</string>
- <string name="config_error">Konfigürasyon alınırken hata oluştu: %s</string>
+ <string name="config_error">Konfigürasyon alınırken hata oluştu</string>
<string name="config_error_auth">Geçersiz kullanıcı adı yada şifre!</string>
<string name="config_demo_hint">Test için &lt;a href=%s&gt;demo bankasında bir test hesabı oluşturabilirsiniz&lt;/a&gt;.</string>
<string name="balance_current_label">Mevcut bakiye</string>
@@ -39,4 +39,5 @@
<string name="about_copyright">Telif Hakkı: %s</string>
<string name="about_supported_bank_api">Banka API Sürümü: %s</string>
<string name="about_version">Sürüm: %s</string>
+ <string name="withdraw_error_currency_mismatch">Hata: Banka farklı bir para birimi bildirdi</string>
</resources> \ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 8a57460..a1c40bf 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
+org.gradle.jvmargs=-Xmx4096m
org.gradle.configureondemand=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
@@ -18,6 +18,7 @@ org.gradle.configureondemand=true
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=false
+android.nonTransitiveRClass=false
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
kotlin.mpp.stability.nowarn=true
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 17f9e71..8111dbf 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Fri Aug 27 15:56:47 CEST 2021
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/gsoc-23-ivan-avalos-report.md b/gsoc-23-ivan-avalos-report.md
new file mode 100644
index 0000000..bbe7945
--- /dev/null
+++ b/gsoc-23-ivan-avalos-report.md
@@ -0,0 +1,188 @@
+[Link to the Android source code](https://git.taler.net/taler-android.git/tree/anastasis?h=dev/ivan-avalos/anastasis)
+
+# Goals of the project
+
+Anastasis is a key recovery system that allows users to create
+encrypted backups of their keys or secrets, split across multiple
+service providers, that can be recovered without needing a
+password. The keys are protected using a key derived from the identity
+attributes of the user, and can only be recovered by answering a
+series of verification steps defined by the user.
+
+The main goal of this Google Summer of Code project was to integrate
+Anastasis into the Taler Android wallet, in order to allow users to
+create encrypted and distributed backups of their wallet keys. This
+could only be achieved by either implementing Anastasis functionality
+within the wallet app, or creating a separate Anastasis app and
+enabling integration between the two apps.
+
+# What I did
+
+I went for the second option and created a separate Anastasis app,
+which has the advantage of allowing Anastasis to be easily used for
+other use cases that don't involve the wallet. Besides, it gave me the
+freedom to build the app with a more suitable architecture.
+
+The core functionality for both the wallet and the Anastasis client is
+built into the same component: “wallet-core”, written in
+TypeScript. The Taler mobile apps and the web extension communicate
+with wallet-core using a consistent and well-documented JSON API.
+
+* [wallet-core.git](https://git.taler.net/wallet-core.git)
+* [wallet-core API docs](https://docs.taler.net/wallet/wallet-core.html)
+
+In order to run wallet-core in the mobile apps, a TypeScript runtime
+needs to be embedded. There are many embedable runtimes, but some of
+them are too big, and not all of them are optimal for the purposes of
+Taler. QuickJS was chosen because of its small size and footprint,
+while having good standards compatibility (ES2020).
+
+* [QuickJS](https://bellard.org/quickjs/)
+
+The Taler developers then created a cross-platform library built on
+top of QuickJS, tailor-made for running wallet-core in mobile, called
+“quickjs-tart” (nicknamed “qtart,” which stands for “**Q**uickJS
+**TA**ler **R**un**T**ime”). This library implements native QuickJS
+modules for cryptographic functions used by wallet-core, pre-compiles
+wallet-core to C, and provides a simple callback-based API that can be
+easily used from the mobile apps using native bridges (e.g. JNI).
+
+* [quickjs-tart.git](https://git.taler.net/quickjs-tart.git)
+
+The repository also includes qtart bindings for Android and iOS, which
+handle the bridging so that the apps don't have to. The Android
+bindings are available as a Maven library in Maven Central.
+
+The main challenge for the Anastasis app was that Anastasis uses a
+different API within wallet-core, that was originally not exposed to
+qtart. This API is designed as a stateless reducer, that takes a state
+and a transition and returns another state.
+
+* [Reducer API docs](https://docs.anastasis.lu/reducer.html)
+
+This is where things start to get interesting. In order to add
+Anastasis support to qtart, I had to expose the Anastasis API in
+wallet-core, implement a native QuickJS module for the Argon2 hash
+function required by Anastasis, and ensure that the “anastasis-core”
+subpackage compiles for QuickJS.
+
+Once I had initial Anastasis support in qtart, the next step was to
+add support for the API in the new Android app. I reused from the
+wallet the code that manages the runtime and abstracts the API
+operations as requests, but modified it to work with the stateless
+model of the reducer. I also wrote Kotlin data structures and
+serialization logic for all the JSON objects in the API.
+
+At this point, I already had the starting point for the rest of the
+app. I wrote the app using Jetpack Compose, a technology that I wasn't
+very familiar with, so I had to learn it on the go, using the code of
+other free-software apps I've used before as examples.
+
+After some time, I managed to successfully create an abstraction for
+the reducer on top of Compose, using a global view model injected to
+the composables with Hilt. This view model contains the reducer state
+as a StateFlow, and a ReducerManager object that allows the views to
+safely interact with the API and update the state accordingly.
+
+The routing part was easy, as the reducer state contains a property
+that indicates the current step in the backup/recovery flow. The root
+composable listens to this property and shows the screen that
+corresponds to the backup/recovery step.
+
+At some point when implementing the backup flow, I realized that I had
+to use another (undocumented) API, separate to the reducer API, to
+contact the backup providers and fetch the list of supported
+verification methods. Again, I had to expose this API from
+wallet-core, build quickjs-tart with this modification, and implement
+this API on the app.
+
+And, at some point when implementing the recovery flow, I learned
+about YET another undocumented API, also separate to the reducer API,
+that was needed to contact the backup providers and fetch the list of
+secrets and recovery policies that match the identity attributes of
+the user.
+
+This resulted in another adventure, as this time, there was an issue
+with data encoding and decoding in qtart related to some cryptographic
+function that resulted in this API returning garbage. Florian helped
+me fix this issue, and soon afterwards, the API was working just fine!
+
+After some more work, I managed to have fully working backup and
+recovery, with many missing features, of course, but with the basics
+working just fine. The deadline was two days away, and I still had to
+implement the Taler integration! Oh, no!
+
+Well, unfortunately I couldn't get Taler integration working before
+the deadline, but at least I got the backup fees showing in the UI,
+and also added support for adding and removing backup providers, so
+that users can choose where to backup their keys and learn about the
+fees each provider demands beforehand. Isn't it cool?
+
+# The current state
+
+The Anastasis app, as it stands now, provides basic support for backup
+and recovery of plain text keys (no files!). It allows users to manage
+the backup providers and learn about the fees they demand, as well as
+the fees per verification challenge (e.g. e-mail, SMS, question).
+
+# What's left to do
+
+There are many missing features:
+
+* Some challenge types are not yet supported, such as IBAN, physical
+ mail, and TOTP.
+* It is only possible to backup plain-text secrets, as there's no
+ functionality in the app to upload and download files.
+* There's no support for paying backup providers with Taler.
+* There's no mechanism to trigger a key backup from the wallet.
+* The reducer state can't be saved or restored from a JSON file, nor
+ is it stored in shared preferences.
+
+I already have a working Anastasis instance that requires payments
+with Taler, which I've been using to test Taler integration and
+implement it in the app. This shouldn't take too long to fully
+implement, but afterwards, better integration with the wallet would
+still be needed to make the UX simpler.
+
+Of course, a mechanism to trigger a backup or recovery from the wallet
+is also missing. This will be implemented with an intent to the
+Anastasis app with the data to backup or recover, and another intent
+to the wallet app to hand-in the recovered data. Not too difficult!
+
+# Merged code
+
+All the required code in wallet-core and qtart has already been
+merged. However, the latest wallet-core release (v0.9.3-dev.19, as of
+now) still doesn't include them. The code for the Anastasis app,
+present in the branch `dev/ivan-avalos/anastasis` of the taler-android
+mono-repo, hasn't been merged yet, and it's still pending review from
+the Taler developers.
+
+* [taler-android.git](https://git.taler.net/taler-android.git)
+
+# What I learned / challenges
+
+One of the hardest parts, and the one that took me the longest, was
+getting familiar with Jetpack Compose, especially creating an entire
+app using only Compose. How would the architecture look like? How was
+I supposed to translate the reducer model, not only to a screen, but
+to an entire app?
+
+I had to look in a lot of places for inspiration, as I wanted
+something clean, easy to work with, and above all, something that
+actually made sense. I ended up modelling the app after the
+architecture of the existing Anastasis web UI, written in React. As it
+turned out, Compose is not too different from React!
+
+Once I solved the initial challenges, I had a burst of creativity,
+which helped me work fast on implementing most of the functionality of
+the app. I refactored the app multiple times, and it always resulted
+in a cleaner codebase and a better architecture. I did my best to have
+a solid and sleek initial version, and so far it has paid off: less
+headaches when working with my own code, and a modern and good-looking
+app that I hope that will be a pleasure to use in the future.
+
+During this Google Summer of Code, I learned to not be afraid of new
+things, as in the end, there's always a way to figure them out. With
+enough help from other people, and enough exploration, it's possible
+for this process to be much shorter.
diff --git a/import-svg-assets.sh b/import-svg-assets.sh
new file mode 100755
index 0000000..19f0923
--- /dev/null
+++ b/import-svg-assets.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# This file is part of GNU Taler
+# (C) 2024 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+
+# To get the s2v command, do this:
+#
+# $ npm install svg2vectordrawable -g
+#
+
+ASSETS_DIR="../taler-assets/svg"
+TMP_DIR="../taler-assets/s2v-tmp"
+OUTPUT_DIR="./wallet/src/main/res/drawable/"
+
+set -ex
+
+mkdir -p "$TMP_DIR"
+
+for d in "$ASSETS_DIR"/*; do
+ s2v --folder="$d" --output="$TMP_DIR" --tint="?attr/colorControlNormal"
+done
+
+# remove unneeded icons
+rm "$TMP_DIR/taler_logo_2021_plain.xml"
+rm "$TMP_DIR/taler_logo_2022.xml"
+rm "$TMP_DIR/logo_2021.xml"
+
+# add tint
+sed -i 's@android:viewportWidth@android:tint="?attr/colorControlNormal"\n android:viewportWidth@g' "$TMP_DIR"/*.xml
+# reduce size
+sed -i 's@"1200dp"@"24dp"@g' "$TMP_DIR"/*.xml
+# add path fillColor
+sed -i 's@<path@<path\n android:fillColor="#FF000000"@g' "$TMP_DIR"/*.xml
+
+# move final files
+mv "$TMP_DIR"/*.xml "$OUTPUT_DIR"
+
+# remove tmp dir
+rm -rf "$TMP_DIR"
diff --git a/merchant-lib/build.gradle b/merchant-lib/build.gradle
index ae7489b..5d4bc48 100644
--- a/merchant-lib/build.gradle
+++ b/merchant-lib/build.gradle
@@ -21,13 +21,11 @@ plugins {
}
android {
- compileSdkVersion 32
- //noinspection GradleDependency
- buildToolsVersion "$build_tools_version"
+ namespace 'net.taler.merchantlib'
+ compileSdk 34
defaultConfig {
minSdkVersion 21
- targetSdkVersion 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -43,12 +41,13 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
-
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
}
- namespace 'net.taler.merchantlib'
}
dependencies {
@@ -63,11 +62,11 @@ dependencies {
testImplementation "junit:junit:$junit_version"
testImplementation "io.ktor:ktor-client-mock-jvm:$ktor_version"
- testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
-} \ No newline at end of file
+}
diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
index 9d6defd..cf627c6 100644
--- a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
+++ b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
@@ -80,7 +80,7 @@ class MerchantApiTest {
{
"product_id": "${product.productId}",
"description": "${product.description}",
- "price": "${product.price.toJSONString()}",
+ "price": "${product.price!!.toJSONString()}",
"quantity": ${product.quantity}
}
]
diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle
index 6d9dac1..68747c2 100644
--- a/merchant-terminal/build.gradle
+++ b/merchant-terminal/build.gradle
@@ -7,17 +7,17 @@ plugins {
android {
namespace 'net.taler.merchantpos'
- compileSdkVersion 33
- //noinspection GradleDependency
- buildToolsVersion "$build_tools_version"
+ compileSdk 34
defaultConfig {
applicationId "net.taler.merchantpos"
minSdkVersion 21
- targetSdkVersion 32
- versionCode 4
- versionName "0.2.2"
+ targetSdkVersion 33
+ versionCode 6
+ versionName "0.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ buildConfigField("String", "BACKEND_API_VERSION", "\"5:0:1\"")
}
buildTypes {
@@ -32,15 +32,16 @@ android {
}
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17"
}
buildFeatures {
+ buildConfig = true
viewBinding = true
}
@@ -67,7 +68,7 @@ dependencies {
implementation "com.google.android.material:material:$material_version"
implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version"
- implementation "androidx.recyclerview:recyclerview:1.2.1"
+ implementation "androidx.recyclerview:recyclerview:1.3.1"
implementation "androidx.recyclerview:recyclerview-selection:1.1.0"
// Navigation
@@ -76,6 +77,6 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
- testImplementation 'androidx.test.ext:junit:1.1.4'
- testImplementation 'org.robolectric:robolectric:4.8.1'
+ testImplementation 'androidx.test.ext:junit:1.1.5'
+ testImplementation 'org.robolectric:robolectric:4.10.3'
}
diff --git a/merchant-terminal/fastlane/Fastfile b/merchant-terminal/fastlane/Fastfile
index 9c09d50..9aa16f0 100644
--- a/merchant-terminal/fastlane/Fastfile
+++ b/merchant-terminal/fastlane/Fastfile
@@ -32,7 +32,6 @@ platform :android do
}
)
upload_to_play_store(
- track: 'beta',
skip_upload_images: 'true',
skip_upload_screenshots: 'true',
skip_upload_apk: 'true', # This is an app bundle, so APK is not possible
diff --git a/merchant-terminal/proguard-rules.pro b/merchant-terminal/proguard-rules.pro
index f417733..6acde1a 100644
--- a/merchant-terminal/proguard-rules.pro
+++ b/merchant-terminal/proguard-rules.pro
@@ -22,3 +22,5 @@
#noinspection ShrinkerUnresolvedReference
-keep class net.taler.merchantpos.** {*;}
+
+-dontwarn org.slf4j.impl.StaticLoggerBinder
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt
index 29f5da4..4a46b27 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt
@@ -75,8 +75,8 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener {
override fun onStart() {
super.onStart()
- if (!model.configManager.config.isValid() && nav.currentDestination?.id != R.id.nav_settings) {
- nav.navigate(R.id.action_global_merchantSettings)
+ if (!model.configManager.config.isValid()) {
+ if (nav.currentDestination?.id != R.id.nav_settings) nav.navigate(R.id.action_global_merchantSettings)
} else if (model.configManager.merchantConfig == null && nav.currentDestination?.id != R.id.configFetcher) {
nav.navigate(R.id.action_global_configFetcher)
}
@@ -103,6 +103,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener {
return true
}
+ @Deprecated("Deprecated in Java")
override fun onBackPressed() {
val currentDestination = nav.currentDestination?.id
if (ui.drawerLayout.isDrawerOpen(START)) {
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
index 5564447..9e82a5a 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
@@ -45,9 +45,7 @@ class ConfigFetcherFragment : Fragment() {
return ui.root
}
- @Deprecated("Deprecated in Java")
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
configManager.fetchConfig(configManager.config, false)
configManager.configUpdateResult.observe(viewLifecycleOwner) { result ->
when (result) {
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt
index 3ee5148..9a5b7a7 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt
@@ -49,7 +49,7 @@ class ConfigFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View? {
+ ): View {
ui = FragmentMerchantConfigBinding.inflate(inflater, container, false)
return ui.root
}
@@ -74,11 +74,11 @@ class ConfigFragment : Fragment() {
password = ui.passwordView.editText!!.text.toString()
)
configManager.fetchConfig(config, true, ui.savePasswordCheckBox.isChecked)
- configManager.configUpdateResult.observe(viewLifecycleOwner, { result ->
+ configManager.configUpdateResult.observe(viewLifecycleOwner) { result ->
if (onConfigUpdate(result)) {
configManager.configUpdateResult.removeObservers(viewLifecycleOwner)
}
- })
+ }
}
ui.forgetPasswordButton.setOnClickListener {
configManager.forgetPassword()
@@ -93,8 +93,8 @@ class ConfigFragment : Fragment() {
super.onStart()
// focus password if this is the only empty field
if (ui.passwordView.editText!!.text.isBlank()
- && !ui.configUrlView.editText!!.text.isBlank()
- && !ui.usernameView.editText!!.text.isBlank()
+ && ui.configUrlView.editText!!.text.isNotBlank()
+ && ui.usernameView.editText!!.text.isNotBlank()
) {
ui.passwordView.requestFocus()
}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
index 8986fc0..70c7b18 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
@@ -40,6 +40,7 @@ import net.taler.common.getIncompatibleStringOrNull
import net.taler.merchantlib.ConfigResponse
import net.taler.merchantlib.MerchantApi
import net.taler.merchantlib.MerchantConfig
+import net.taler.merchantpos.BuildConfig
import net.taler.merchantpos.R
private const val SETTINGS_NAME = "taler-merchant-terminal"
@@ -52,7 +53,7 @@ internal const val CONFIG_URL_DEMO = "https://docs.taler.net/_static/sample-pos-
internal const val CONFIG_USERNAME_DEMO = ""
internal const val CONFIG_PASSWORD_DEMO = ""
-private val VERSION = Version(3, 0, 1)
+private val VERSION = Version.parse(BuildConfig.BACKEND_API_VERSION)!!
private val TAG = ConfigManager::class.java.simpleName
@@ -74,7 +75,7 @@ class ConfigManager(
private val configurationReceivers = ArrayList<ConfigurationReceiver>()
var config = Config(
- configUrl = prefs.getString(SETTINGS_CONFIG_URL, CONFIG_URL_DEMO)!!,
+ configUrl = prefs.getString(SETTINGS_CONFIG_URL, "")!!,
username = prefs.getString(SETTINGS_USERNAME, CONFIG_USERNAME_DEMO)!!,
password = prefs.getString(SETTINGS_PASSWORD, CONFIG_PASSWORD_DEMO)!!
)
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt
index 1f1ab74..556f05f 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt
@@ -31,8 +31,8 @@ data class Config(
val username: String,
val password: String
) {
- fun isValid() = !configUrl.isBlank()
- fun hasPassword() = !password.isBlank()
+ fun isValid() = configUrl.isNotBlank()
+ fun hasPassword() = password.isNotBlank()
}
@Serializable
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
index 69e74ce..8c8b1d5 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
@@ -55,10 +55,10 @@ class CategoriesFragment : Fragment(), CategorySelectionListener {
layoutManager = LinearLayoutManager(requireContext())
}
- orderManager.categories.observe(viewLifecycleOwner, { categories ->
+ orderManager.categories.observe(viewLifecycleOwner) { categories ->
adapter.setItems(categories)
ui.progressBar.visibility = INVISIBLE
- })
+ }
}
override fun onCategorySelected(category: Category) {
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CustomDialogFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CustomDialogFragment.kt
new file mode 100644
index 0000000..45aaf06
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CustomDialogFragment.kt
@@ -0,0 +1,78 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.order
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import android.widget.Toast.LENGTH_LONG
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import net.taler.common.Amount
+import net.taler.common.AmountParserException
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.config.ConfigProduct
+import net.taler.merchantpos.databinding.FragmentCustomDialogBinding
+
+class CustomDialogFragment : DialogFragment() {
+
+ companion object {
+ const val TAG = "CustomDialogFragment"
+ }
+
+ private val viewModel: MainViewModel by activityViewModels()
+
+ private lateinit var ui: FragmentCustomDialogBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ ui = FragmentCustomDialogBinding.inflate(inflater, container, false)
+ return ui.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val currency = viewModel.configManager.currency ?: error("No currency")
+ ui.currencyView.text = currency
+ ui.addButton.setOnClickListener {
+ val currentOrderId =
+ viewModel.orderManager.currentOrderId.value ?: return@setOnClickListener
+ val amount = try {
+ Amount.fromString(currency, ui.amountLayout.editText!!.text.toString())
+ } catch (e: AmountParserException) {
+ Toast.makeText(requireContext(), R.string.refund_error_invalid_amount, LENGTH_LONG)
+ .show()
+ return@setOnClickListener
+ }
+ val product = ConfigProduct(
+ description = ui.productNameLayout.editText!!.text.toString(),
+ price = amount,
+ categories = listOf(Int.MIN_VALUE),
+ )
+ viewModel.orderManager.addProduct(currentOrderId, product)
+ dismiss()
+ }
+ ui.cancelButton.setOnClickListener {
+ dismiss()
+ }
+ }
+}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
index ad9af74..c11b5c7 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
@@ -19,7 +19,7 @@ package net.taler.merchantpos.order
import androidx.annotation.UiThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Transformations
+import androidx.lifecycle.map
import net.taler.common.Amount
import net.taler.common.CombinedLiveData
import net.taler.merchantpos.config.Category
@@ -52,7 +52,7 @@ internal class MutableLiveOrder(
get() = productsByCategory.keys.map { it.id to it }.toMap()
override val order: MutableLiveData<Order?> =
MutableLiveData(Order(id, currency, availableCategories))
- override val orderTotal: LiveData<Amount> = Transformations.map(order) { it?.total }
+ override val orderTotal: LiveData<Amount> = order.map { it?.total ?: Amount.zero(currency) }
override val restartState = MutableLiveData(DISABLED)
private val selectedOrderLine = MutableLiveData<ConfigProduct?>()
override val selectedProductKey: String?
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt
index 2d4d32c..a22ab0a 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt
@@ -73,7 +73,7 @@ data class Order(val id: Int, val currency: String, val availableCategories: Map
val categories = HashMap<Category, Int>()
products.forEach { product ->
val categoryId = product.categories[0]
- val category = availableCategories.getValue(categoryId)
+ val category = availableCategories[categoryId] ?: return@forEach // custom products
val oldQuantity = categories[category] ?: 0
categories[category] = oldQuantity + product.quantity
}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
index 0de5939..cdd2b67 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
@@ -44,15 +44,14 @@ class OrderFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
+ savedInstanceState: Bundle?,
+ ): View {
ui = FragmentOrderBinding.inflate(inflater, container, false)
return ui.root
}
- @Deprecated("Deprecated in Java")
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
orderManager.currentOrderId.observe(viewLifecycleOwner) { orderId ->
val liveOrder = orderManager.getOrder(orderId)
onOrderSwitched(orderId, liveOrder)
@@ -62,6 +61,9 @@ class OrderFragment : Fragment() {
.replace(R.id.fragment1, OrderStateFragment())
.commit()
}
+ ui.customButton.setOnClickListener {
+ CustomDialogFragment().show(childFragmentManager, CustomDialogFragment.TAG)
+ }
}
override fun onStart() {
@@ -75,13 +77,13 @@ class OrderFragment : Fragment() {
private fun onOrderSwitched(orderId: Int, liveOrder: LiveOrder) {
// order title
- liveOrder.order.observe(viewLifecycleOwner, { order ->
+ liveOrder.order.observe(viewLifecycleOwner) { order ->
if (order == null) return@observe
activity?.title = getString(R.string.order_label_title, order.title)
- })
+ }
// restart button
ui.restartButton.setOnClickListener { liveOrder.restartOrUndo() }
- liveOrder.restartState.observe(viewLifecycleOwner, { state ->
+ liveOrder.restartState.observe(viewLifecycleOwner) { state ->
beginDelayedTransition(view as ViewGroup)
if (state == UNDO) {
ui.restartButton.setText(R.string.order_undo)
@@ -92,19 +94,19 @@ class OrderFragment : Fragment() {
ui.restartButton.isEnabled = state == ENABLED
ui.completeButton.isEnabled = state == ENABLED
}
- })
+ }
// -1 and +1 buttons
- liveOrder.modifyOrderAllowed.observe(viewLifecycleOwner, { allowed ->
+ liveOrder.modifyOrderAllowed.observe(viewLifecycleOwner) { allowed ->
ui.minusButton.isEnabled = allowed
ui.plusButton.isEnabled = allowed
- })
+ }
ui.minusButton.setOnClickListener { liveOrder.decreaseSelectedOrderLine() }
ui.plusButton.setOnClickListener { liveOrder.increaseSelectedOrderLine() }
// previous and next button
ui.prevButton.isEnabled = orderManager.hasPreviousOrder(orderId)
- orderManager.hasNextOrder(orderId).observe(viewLifecycleOwner, { hasNextOrder ->
+ orderManager.hasNextOrder(orderId).observe(viewLifecycleOwner) { hasNextOrder ->
ui.nextButton.isEnabled = hasNextOrder
- })
+ }
ui.prevButton.setOnClickListener { orderManager.previousOrder() }
ui.nextButton.setOnClickListener { orderManager.nextOrder() }
// complete button
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
index 6c5ecdf..2efdf4c 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
@@ -21,7 +21,7 @@ import android.util.Log
import androidx.annotation.UiThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Transformations.map
+import androidx.lifecycle.map
import net.taler.merchantpos.R
import net.taler.merchantpos.config.Category
import net.taler.merchantpos.config.ConfigProduct
@@ -32,7 +32,7 @@ import net.taler.merchantpos.order.RestartState.ENABLED
class OrderManager(private val context: Context) : ConfigurationReceiver {
companion object {
- val TAG = OrderManager::class.java.simpleName
+ val TAG: String = OrderManager::class.java.simpleName
}
private lateinit var currency: String
@@ -150,7 +150,7 @@ class OrderManager(private val context: Context) : ConfigurationReceiver {
return currentOrderId != orders.keys.first()
}
- fun hasNextOrder(currentOrderId: Int) = map(order(currentOrderId).restartState) { state ->
+ fun hasNextOrder(currentOrderId: Int) = order(currentOrderId).restartState.map { state ->
state == ENABLED || currentOrderId != orders.keys.last()
}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
index 02f66fa..efcb158 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
@@ -36,10 +36,9 @@ import net.taler.merchantpos.R
import net.taler.merchantpos.config.ConfigManager
import net.taler.merchantpos.order.Order
import java.util.concurrent.TimeUnit.HOURS
-import java.util.concurrent.TimeUnit.MINUTES
import java.util.concurrent.TimeUnit.SECONDS
-private val TIMEOUT = MINUTES.toMillis(2)
+private const val TIMEOUT = Long.MAX_VALUE
private val CHECK_INTERVAL = SECONDS.toMillis(1)
class PaymentManager(
@@ -85,7 +84,10 @@ class PaymentManager(
private fun checkPayment(orderId: String) = scope.launch {
val merchantConfig = configManager.merchantConfig!!
- api.checkOrder(merchantConfig, orderId).handle(::onNetworkError) { response ->
+ api.checkOrder(merchantConfig, orderId).handle({ error ->
+ // don't call onNetworkError() to not cancel payment, just keep trying
+ Log.d(TAG, "Network error: $error")
+ }) { response ->
assertUiThread()
if (!isActive) return@handle // don't continue if job was cancelled
val currentValue = requireNotNull(mPayment.value)
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
index 201c9cf..443ca91 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
@@ -55,9 +55,9 @@ class ProcessPaymentFragment : Fragment() {
val introRes =
if (hasNfc(requireContext())) R.string.payment_intro_nfc else R.string.payment_intro
ui.payIntroView.setText(introRes)
- paymentManager.payment.observe(viewLifecycleOwner, { payment ->
+ paymentManager.payment.observe(viewLifecycleOwner) { payment ->
onPaymentStateChanged(payment)
- })
+ }
ui.cancelPaymentButton.setOnClickListener {
onPaymentCancel()
}
diff --git a/merchant-terminal/src/main/res/drawable/ic_dialpad.xml b/merchant-terminal/src/main/res/drawable/ic_dialpad.xml
new file mode 100644
index 0000000..33ba1d9
--- /dev/null
+++ b/merchant-terminal/src/main/res/drawable/ic_dialpad.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2023 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,19c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM6,1c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM6,7c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM6,13c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,5c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,13c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,13c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,7c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,7c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,1c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
+</vector>
diff --git a/merchant-terminal/src/main/res/layout/fragment_custom_dialog.xml b/merchant-terminal/src/main/res/layout/fragment_custom_dialog.xml
new file mode 100644
index 0000000..8da50ad
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_custom_dialog.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2023 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/titleView"
+ style="@style/TextAppearance.Material3.TitleMedium"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:text="@string/order_custom"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/productNameLayout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/titleView">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/order_custom_product"
+ android:inputType="textShortMessage"
+ android:singleLine="true"
+ android:text="@string/order_custom_product_default" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/amountLayout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="8dp"
+ android:minEms="5"
+ app:layout_constraintEnd_toStartOf="@+id/currencyView"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/productNameLayout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/refund_amount"
+ android:inputType="numberDecimal">
+
+ <requestFocus />
+ </com.google.android.material.textfield.TextInputEditText>
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/currencyView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ app:layout_constraintBottom_toBottomOf="@+id/amountLayout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/amountLayout"
+ app:layout_constraintTop_toTopOf="@+id/amountLayout"
+ tools:text="TESTKUDOS" />
+
+ <Button
+ android:id="@+id/addButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/order_custom_add_button"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/amountLayout"
+ app:layout_constraintVertical_bias="0.0" />
+
+ <Button
+ android:id="@+id/cancelButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:text="@android:string/cancel"
+ app:layout_constraintEnd_toStartOf="@+id/addButton"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/amountLayout" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_order.xml b/merchant-terminal/src/main/res/layout/fragment_order.xml
index dc49db1..dd2edc5 100644
--- a/merchant-terminal/src/main/res/layout/fragment_order.xml
+++ b/merchant-terminal/src/main/res/layout/fragment_order.xml
@@ -25,7 +25,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
- app:layout_constraintBottom_toTopOf="@+id/restartButton"
+ app:layout_constraintBottom_toTopOf="@+id/buttonBar"
app:layout_constraintEnd_toStartOf="@+id/guideline1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@@ -44,7 +44,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
- app:layout_constraintBottom_toTopOf="@+id/restartButton"
+ app:layout_constraintBottom_toTopOf="@+id/buttonBar"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline1"
app:layout_constraintTop_toTopOf="parent"
@@ -63,75 +63,102 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
- app:layout_constraintBottom_toTopOf="@+id/restartButton"
+ app:layout_constraintBottom_toTopOf="@+id/buttonBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent"
tools:layout="@layout/fragment_categories" />
- <Button
- android:id="@+id/restartButton"
- android:layout_width="wrap_content"
+ <HorizontalScrollView
+ android:id="@+id/buttonBar"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
- android:backgroundTint="@color/button_bottom"
- android:text="@string/order_restart"
+ android:scrollbars="horizontal"
+ android:fadeScrollbars="false"
+ app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent" />
+ app:layout_constraintEnd_toStartOf="@id/completeButton">
- <Button
- android:id="@+id/plusButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:minWidth="48dp"
- android:text="+1"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/minusButton"
- tools:ignore="HardcodedText" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
- <Button
- android:id="@+id/minusButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="32dp"
- android:minWidth="48dp"
- android:text="-1"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/restartButton"
- tools:ignore="HardcodedText" />
+ <Button
+ android:id="@+id/restartButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:backgroundTint="@color/button_bottom"
+ android:text="@string/order_restart"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
- <Button
- android:id="@+id/prevButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="32dp"
- android:backgroundTint="@color/button_bottom"
- android:text="@string/order_previous"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/plusButton" />
+ <Button
+ android:id="@+id/plusButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:minWidth="48dp"
+ android:text="+1"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/minusButton"
+ tools:ignore="HardcodedText" />
- <Button
- android:id="@+id/nextButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:backgroundTint="@color/button_bottom"
- android:text="@string/order_next"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/prevButton" />
+ <Button
+ android:id="@+id/minusButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:minWidth="48dp"
+ android:text="-1"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/restartButton"
+ tools:ignore="HardcodedText" />
+
+ <Button
+ android:id="@+id/prevButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:backgroundTint="@color/button_bottom"
+ android:text="@string/order_previous"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/plusButton" />
+
+ <Button
+ android:id="@+id/nextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:backgroundTint="@color/button_bottom"
+ android:text="@string/order_next"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/prevButton" />
+
+ <ImageButton
+ android:id="@+id/customButton"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="8dp"
+ android:backgroundTint="?colorPrimary"
+ app:srcCompat="@drawable/ic_dialpad"
+ android:contentDescription="@string/order_custom" />
+
+ </LinearLayout>
+
+ </HorizontalScrollView>
<Button
android:id="@+id/completeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginStart="32dp"
android:layout_marginEnd="8dp"
- android:backgroundTint="@color/button_bottom"
+ android:backgroundTint="@color/green"
android:text="@string/order_complete"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="1.0"
- app:layout_constraintStart_toEndOf="@+id/nextButton" />
+ app:layout_constraintHorizontal_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/values-es/strings.xml b/merchant-terminal/src/main/res/values-es/strings.xml
index 1ab7d92..4beb9fe 100644
--- a/merchant-terminal/src/main/res/values-es/strings.xml
+++ b/merchant-terminal/src/main/res/values-es/strings.xml
@@ -65,4 +65,8 @@
<string name="refund_error_max_amount">Mayor que la cantidad del pedido de %s</string>
<string name="refund_intro_nfc">Por favor permite al cliente escanear el código QR o usar NFC para ofrecer reembolso</string>
<string name="payment_claimed">A la espera de que el cliente confirme el pago…</string>
+ <string name="order_custom_product">Nombre de producto personalizado</string>
+ <string name="order_custom_product_default">Consejo</string>
+ <string name="order_custom_add_button">Añadir</string>
+ <string name="order_custom">Añadir producto personalizado</string>
</resources> \ No newline at end of file
diff --git a/merchant-terminal/src/main/res/values-fi/strings.xml b/merchant-terminal/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000..ff7c02e
--- /dev/null
+++ b/merchant-terminal/src/main/res/values-fi/strings.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">GNU Taler -myyntipiste</string>
+ <string name="app_name_short">Kauppiasterminaali</string>
+ <string name="project_name">GNU Taler</string>
+ <string name="menu_order">Tilaukset</string>
+ <string name="menu_history">Historia</string>
+ <string name="order_label_title">Tilaus #%s</string>
+ <string name="order_total">Yhteensä: #%s</string>
+ <string name="order_undo">Kumoa</string>
+ <string name="order_previous">Edellinen</string>
+ <string name="order_custom">Lisää mukautettu tuote</string>
+ <string name="order_complete">Valmis</string>
+ <string name="order_custom_product">Mukautettu tuotteen nimi</string>
+ <string name="order_custom_product_default">Tip</string>
+ <string name="config_label">Kauppiaan asetukset</string>
+ <string name="config_url">Määrityksen URL -osoite</string>
+ <string name="config_username">Käyttäjänimi</string>
+ <string name="config_password">Salasana</string>
+ <string name="config_ok">Nouda määritykset</string>
+ <string name="config_error_network">Virhe: Ei voitu muodostaa yhteyttä määrityspalvelimeen</string>
+ <string name="config_error_category">Virhe: kelvollista tuoteluokkaa ei löytynyt</string>
+ <string name="config_error_malformed">Virhe: Määrityksen JSON on virheellinen</string>
+ <string name="config_error_currency">Virhe: Tuotteen %1$s valuutta on %2$s, mutta %3$s odotetaan</string>
+ <string name="config_error_product_category_id">Virhe: Tuote %1$s viittaa tuntemattomaan luokkatunnukseen %2$d</string>
+ <string name="config_error_product_zero">Virhe: kelvollisia tuotteita ei löytynyt</string>
+ <string name="config_error_unknown">Virhe: Virheellinen määritys</string>
+ <string name="config_fetching">Haetaan määritystä…</string>
+ <string name="config_save_password">Muista salasana</string>
+ <string name="config_forget_password">Unohda</string>
+ <string name="config_changed">Vaihdettu uudeksi kauppiaaksi käyttämällä %s</string>
+ <string name="payment_intro_nfc">Anna asiakkaan skannata QR-koodi tai käyttää NFC:tä maksamiseen.</string>
+ <string name="payment_intro">Anna asiakkaan skannata QR-koodi maksaaksesi.</string>
+ <string name="payment_claimed">Odotetaan asiakkaan maksun vahvistamista…</string>
+ <string name="payment_cancel">Peruuta maksu</string>
+ <string name="payment_received">Maksu vastaanotettu</string>
+ <string name="payment_back_button">Jatka</string>
+ <string name="payment_order_id">Kuitti #%s</string>
+ <string name="payment_process_label">Maksua vaaditaan</string>
+ <string name="payment_canceled">Maksu peruutettu</string>
+ <string name="history_label">Maksu historia</string>
+ <string name="history_refund">Palautus</string>
+ <string name="history_unpaid">Maksamaton</string>
+ <string name="refund_amount">Summa</string>
+ <string name="refund_reason">Palautuksen syy</string>
+ <string name="refund_abort">Keskeytä</string>
+ <string name="refund_complete">Vastaanotettu</string>
+ <string name="refund_confirm">Hyväksy hyvitys</string>
+ <string name="refund_error_max_amount">Suurempi kuin tilausmäärä %s</string>
+ <string name="refund_error_invalid_amount">Virheellinen summa</string>
+ <string name="refund_error_zero">On oltava positiivinen määrä</string>
+ <string name="refund_error_backend">Hyvityksen käsittelyssä tapahtui virhe</string>
+ <string name="refund_error_already_refunded">Palautettu jo</string>
+ <string name="refund_intro_nfc">Anna asiakkaan skannata QR-koodi tai käyttää NFC:tä hyvityksen tarjoamiseksi</string>
+ <string name="refund_intro">Anna asiakkaan skannata QR-koodi hyvityksen tarjoamiseksi</string>
+ <string name="refund_order_ref">Ostoviite: %1$s
+\n
+\n%2$s</string>
+ <string name="error_payment">Virhe: Maksua ei vastaanotettu</string>
+ <string name="error_timeout">Maksua ei ole suoritettu maksuajan kuluessa, yritä uudelleen!</string>
+ <string name="error_cancelled">Maksu peruutettu</string>
+ <string name="error_history">Virhe noudettaessa tilaushistoriaa</string>
+ <string name="toast_back_to_exit">Napsauta «takaisin» uudelleen poistuaksesi</string>
+ <string name="menu_settings">Asetukset</string>
+ <string name="order_restart">Uudelleenkäynnistys</string>
+ <string name="order_next">Seuraava</string>
+ <string name="order_custom_add_button">Lisää</string>
+ <string name="config_auth_error">Virhe: Virheellinen käyttäjätunnus tai salasana</string>
+ <string name="config_fetching_label">Haetaan määritystä</string>
+ <string name="config_docs">Katso määritysmuodon <a href="https://docs.taler.net/taler-merchant-pos-terminal.html#apis-and-data-formats">dokumentaatiosta</a>.</string>
+ <string name="refund_error_deadline">Palautusaika on umpeutunut</string>
+</resources> \ No newline at end of file
diff --git a/merchant-terminal/src/main/res/values-fr/strings.xml b/merchant-terminal/src/main/res/values-fr/strings.xml
index 44756ce..4d9636d 100644
--- a/merchant-terminal/src/main/res/values-fr/strings.xml
+++ b/merchant-terminal/src/main/res/values-fr/strings.xml
@@ -10,7 +10,7 @@
<string name="config_docs">Veuillez vous référer à <a href="https://docs.taler.net/taler-merchant-pos-terminal.html#apis-and-data-formats">la documentation</a> pour le format de configuration.</string>
<string name="refund_confirm">Approuver le remboursement</string>
<string name="refund_error_backend">Erreur lors du traitement du remboursement</string>
- <string name="menu_settings">Réglages</string>
+ <string name="menu_settings">Paramètres</string>
<string name="app_name">GNU Taler Point de vente</string>
<string name="app_name_short">Terminal marchand</string>
<string name="menu_order">Commandes</string>
diff --git a/merchant-terminal/src/main/res/values-tr/strings.xml b/merchant-terminal/src/main/res/values-tr/strings.xml
index a6362ca..bdd4d40 100644
--- a/merchant-terminal/src/main/res/values-tr/strings.xml
+++ b/merchant-terminal/src/main/res/values-tr/strings.xml
@@ -65,4 +65,8 @@
<string name="config_fetching">Kurulum getiriliyor…</string>
<string name="config_save_password">Şifreyi hatırla</string>
<string name="menu_history">Geçmiş</string>
+ <string name="order_custom">Özel ürün ekle</string>
+ <string name="order_custom_product">Özel ürünün ismi</string>
+ <string name="order_custom_product_default">Bahşiş</string>
+ <string name="order_custom_add_button">Ekle</string>
</resources> \ No newline at end of file
diff --git a/merchant-terminal/src/main/res/values/strings.xml b/merchant-terminal/src/main/res/values/strings.xml
index f06866d..3cd2043 100644
--- a/merchant-terminal/src/main/res/values/strings.xml
+++ b/merchant-terminal/src/main/res/values/strings.xml
@@ -14,7 +14,11 @@
<string name="order_undo">Undo</string>
<string name="order_previous">Prev</string>
<string name="order_next">Next</string>
+ <string name="order_custom">Add custom product</string>
<string name="order_complete">Complete</string>
+ <string name="order_custom_product">Custom product name</string>
+ <string name="order_custom_product_default">Tip</string>
+ <string name="order_custom_add_button">Add</string>
<string name="config_label">Merchant settings</string>
<string name="config_url">Configuration URL</string>
diff --git a/taler-kotlin-android/build.gradle b/taler-kotlin-android/build.gradle
index 69b4f4d..06d1474 100644
--- a/taler-kotlin-android/build.gradle
+++ b/taler-kotlin-android/build.gradle
@@ -22,8 +22,7 @@ plugins {
android {
namespace 'net.taler.common'
- compileSdkVersion 32
- buildToolsVersion "$build_tools_version"
+ compileSdk 34
defaultConfig {
minSdkVersion 21
@@ -45,6 +44,13 @@ android {
buildFeatures {
viewBinding = true
}
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
+ }
packagingOptions {
resources {
excludes += ['META-INF/*.kotlin_module']
@@ -53,8 +59,8 @@ android {
}
dependencies {
- implementation 'androidx.appcompat:appcompat:1.5.1'
- implementation 'androidx.core:core-ktx:1.9.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version"
// Navigation
@@ -68,7 +74,7 @@ dependencies {
implementation 'com.google.zxing:core:3.5.0' // needs minSdkVersion 24+ or desugar
// JSON parsing and serialization
- api 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1'
+ api 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
testImplementation "junit:junit:$junit_version"
testImplementation 'org.json:json:20220320'
}
diff --git a/taler-kotlin-android/src/main/AndroidManifest.xml b/taler-kotlin-android/src/main/AndroidManifest.xml
index 5533913..bc489b1 100644
--- a/taler-kotlin-android/src/main/AndroidManifest.xml
+++ b/taler-kotlin-android/src/main/AndroidManifest.xml
@@ -14,8 +14,7 @@
~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="net.taler.common">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
index 750a1de..3e3bd0a 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
@@ -16,11 +16,15 @@
package net.taler.common
+import android.os.Build
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
+import java.text.NumberFormat
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.roundToInt
@@ -54,6 +58,11 @@ public data class Amount(
* of 50_000_000 would correspond to 50 cents.
*/
val fraction: Int,
+
+ /**
+ * Currency specification for amount
+ */
+ val spec: CurrencySpecification? = null,
) : Comparable<Amount> {
public companion object {
@@ -65,12 +74,6 @@ public data class Amount(
private const val MAX_FRACTION_LENGTH = 8
public const val MAX_FRACTION: Int = 99_999_999
- public fun fromDouble(currency: String, value: Double): Amount {
- val intPart = floor(value).toLong()
- val fraPart = floor((value - intPart) * FRACTIONAL_BASE).toInt()
- return Amount(currency, intPart, fraPart)
- }
-
public fun zero(currency: String): Amount {
return Amount(checkCurrency(currency), 0, 0)
}
@@ -96,6 +99,7 @@ public data class Amount(
}
public fun isValidAmountStr(str: String): Boolean {
+ if (str.count { it == '.' } > 1) return false
val split = str.split(".")
try {
checkValue(split[0].toLongOrNull())
@@ -175,6 +179,8 @@ public data class Amount(
return Amount(checkCurrency(currency), this.value, this.fraction)
}
+ fun withSpec(spec: CurrencySpecification?) = copy(spec = spec)
+
public operator fun minus(other: Amount): Amount {
check(currency == other.currency) { "Can only subtract from same currency" }
var resultValue = value
@@ -201,8 +207,50 @@ public data class Amount(
return "$currency:$amountStr"
}
- override fun toString(): String {
- return "$amountStr $currency"
+ override fun toString() = toString(
+ showSymbol = true,
+ negative = false,
+ )
+
+ fun toString(
+ showSymbol: Boolean = true,
+ negative: Boolean = false,
+ symbols: DecimalFormatSymbols = DecimalFormat().decimalFormatSymbols,
+ ): String {
+ // We clone the object to safely/cleanly modify it
+ val s = symbols.clone() as DecimalFormatSymbols
+ val amount = (if (negative) "-$amountStr" else amountStr).toBigDecimal()
+
+ // No currency spec, so we render normally
+ if (spec == null) {
+ val format = NumberFormat.getInstance()
+ format.maximumFractionDigits = MAX_FRACTION_LENGTH
+ format.minimumFractionDigits = 0
+ if (Build.VERSION.SDK_INT >= 34) {
+ s.groupingSeparator = s.monetaryGroupingSeparator
+ }
+ s.decimalSeparator = s.monetaryDecimalSeparator
+ (format as DecimalFormat).decimalFormatSymbols = s
+
+ val fmt = format.format(amount)
+ return if (showSymbol) "$fmt $currency" else fmt
+ }
+
+ // There is currency spec, so we can do things right
+ val format = NumberFormat.getCurrencyInstance()
+ format.maximumFractionDigits = spec.numFractionalNormalDigits
+ format.minimumFractionDigits = spec.numFractionalTrailingZeroDigits
+ s.currencySymbol = spec.symbol ?: ""
+ (format as DecimalFormat).decimalFormatSymbols = s
+
+ val fmt = format.format(amount)
+ return if (showSymbol) {
+ // If no symbol, then we use the currency string
+ if (spec.symbol != null) fmt else "$fmt $currency"
+ } else {
+ // We should do better than manually removing the symbol here
+ fmt.replace(s.currencySymbol, "").trim()
+ }
}
override fun compareTo(other: Amount): Int {
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt
index 066184c..8f3e5d5 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt
@@ -23,6 +23,7 @@ import android.content.Context.CONNECTIVITY_SERVICE
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Looper
import android.text.format.DateUtils.DAY_IN_MILLIS
@@ -120,6 +121,23 @@ fun Context.startActivitySafe(intent: Intent) {
}
}
+fun Context.openUri(uri: String, title: String) {
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ data = Uri.parse(uri)
+ }
+
+ startActivitySafe(Intent.createChooser(intent, title))
+}
+
+fun Context.shareText(text: String) {
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ putExtra(Intent.EXTRA_TEXT, text)
+ type = "text/plain"
+ }
+
+ startActivitySafe(Intent.createChooser(intent, null))
+}
+
fun Fragment.navigate(directions: NavDirections) = findNavController().navigate(directions)
fun Long.toRelativeTime(context: Context): CharSequence {
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt
index 910cc36..5b614fe 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt
@@ -42,7 +42,7 @@ abstract class Product {
abstract val productId: String?
abstract val description: String
abstract val descriptionI18n: Map<String, String>?
- abstract val price: Amount
+ abstract val price: Amount?
abstract val location: String?
abstract val image: String?
val localizedDescription: String
@@ -60,14 +60,14 @@ data class ContractProduct(
override val description: String,
@SerialName("description_i18n")
override val descriptionI18n: Map<String, String>? = null,
- override val price: Amount,
+ override val price: Amount? = null,
@SerialName("delivery_location")
override val location: String? = null,
override val image: String? = null,
- val quantity: Int
+ val quantity: Int = 1,
) : Product() {
- val totalPrice: Amount by lazy {
- price * quantity
+ val totalPrice: Amount? by lazy {
+ price?.let { price * quantity }
}
}
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/CurrencySpecification.kt b/taler-kotlin-android/src/main/java/net/taler/common/CurrencySpecification.kt
new file mode 100644
index 0000000..02113f4
--- /dev/null
+++ b/taler-kotlin-android/src/main/java/net/taler/common/CurrencySpecification.kt
@@ -0,0 +1,36 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CurrencySpecification(
+ val name: String,
+ @SerialName("num_fractional_input_digits")
+ val numFractionalInputDigits: Int,
+ @SerialName("num_fractional_normal_digits")
+ val numFractionalNormalDigits: Int,
+ @SerialName("num_fractional_trailing_zero_digits")
+ val numFractionalTrailingZeroDigits: Int,
+ @SerialName("alt_unit_names")
+ val altUnitNames: Map<Int, String>,
+) {
+ // TODO: add support for alt units
+ val symbol: String? get() = altUnitNames[0]
+} \ No newline at end of file
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Event.kt b/taler-kotlin-android/src/main/java/net/taler/common/Event.kt
index 752e20e..868063c 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/Event.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Event.kt
@@ -49,7 +49,7 @@ fun <T> T.toEvent() = Event(this)
* [onEvent] is *only* called if the [Event]'s contents has not been consumed.
*/
class EventObserver<T>(private val onEvent: (T) -> Unit) : Observer<Event<T>> {
- override fun onChanged(event: Event<T>?) {
- event?.getIfNotConsumed()?.let { onEvent(it) }
+ override fun onChanged(value: Event<T>) {
+ value.getIfNotConsumed()?.let { onEvent(it) }
}
}
diff --git a/taler-kotlin-android/src/test/java/net/taler/common/AmountTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/AmountTest.kt
index 7072426..1ea4e70 100644
--- a/taler-kotlin-android/src/test/java/net/taler/common/AmountTest.kt
+++ b/taler-kotlin-android/src/test/java/net/taler/common/AmountTest.kt
@@ -16,10 +16,12 @@
package net.taler.common
+import android.os.Build
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
+import java.text.DecimalFormatSymbols
import kotlin.random.Random
class AmountTest {
@@ -41,7 +43,6 @@ class AmountTest {
assertEquals("TESTKUDOS", amount.currency)
assertEquals(23, amount.value)
assertEquals((0.42 * 1e8).toInt(), amount.fraction)
- assertEquals("23.42 TESTKUDOS", amount.toString())
str = "EUR:500000000.00000001"
amount = Amount.fromJSONString(str)
@@ -49,7 +50,6 @@ class AmountTest {
assertEquals("EUR", amount.currency)
assertEquals(500000000, amount.value)
assertEquals(1, amount.fraction)
- assertEquals("500000000.00000001 EUR", amount.toString())
str = "EUR:1500000000.00000003"
amount = Amount.fromJSONString(str)
@@ -57,14 +57,158 @@ class AmountTest {
assertEquals("EUR", amount.currency)
assertEquals(1500000000, amount.value)
assertEquals(3, amount.fraction)
- assertEquals("1500000000.00000003 EUR", amount.toString())
}
@Test
fun testToString() {
- Amount.fromString("BITCOINBTC", "0.00000001").let { amount ->
- assertEquals("0.00000001 BITCOINBTC", amount.toString())
- assertEquals("0.00000001", amount.amountStr)
+ amountToString(
+ amount = Amount.fromString("KUDOS", "13.71"),
+ spec = CurrencySpecification(
+ name = "Test (Taler Demostrator)",
+ numFractionalInputDigits = 2,
+ numFractionalNormalDigits = 2,
+ numFractionalTrailingZeroDigits = 2,
+ altUnitNames = mapOf(0 to "ク"),
+ ),
+ rawStr = "13.71",
+ fraction = 71000000,
+ specAmount = "13.71",
+ noSpecAmount = "13.71",
+ currency = "KUDOS",
+ symbol = "ク",
+ )
+
+ amountToString(
+ amount = Amount.fromString("TESTKUDOS", "23.42"),
+ spec = CurrencySpecification(
+ name = "Test (Taler Unstable Demostrator)",
+ numFractionalInputDigits = 0,
+ numFractionalNormalDigits = 0,
+ numFractionalTrailingZeroDigits = 0,
+ altUnitNames = mapOf(0 to "テ"),
+ ),
+ rawStr = "23.42",
+ fraction = 42000000,
+ specAmount = "23",
+ noSpecAmount = "23.42",
+ currency = "TESTKUDOS",
+ symbol = "テ",
+ )
+
+ amountToString(
+ amount = Amount.fromString("BITCOINBTC", "0.00000001"),
+ spec = CurrencySpecification(
+ name = "Bitcoin",
+ numFractionalInputDigits = 8,
+ numFractionalNormalDigits = 8,
+ numFractionalTrailingZeroDigits = 0,
+ altUnitNames = mapOf(
+ 0 to "₿",
+ // TODO: uncomment when units get implemented
+ // and then write tests for units, please
+// -1 to "d₿",
+// -2 to "c₿",
+// -3 to "m₿",
+// -6 to "µ₿",
+// -8 to "sat",
+ ),
+ ),
+ rawStr = "0.00000001",
+ fraction = 1,
+ specAmount = "0.00000001",
+ noSpecAmount = "0.00000001",
+ currency = "BITCOINBTC",
+ symbol = "₿",
+ )
+
+ val specEUR = CurrencySpecification(
+ name = "EUR",
+ numFractionalInputDigits = 2,
+ numFractionalNormalDigits = 2,
+ numFractionalTrailingZeroDigits = 2,
+ altUnitNames = mapOf(0 to "€"),
+ )
+
+ amountToString(
+ amount = Amount.fromString("EUR", "1500000000.00000003"),
+ spec = specEUR,
+ rawStr = "1500000000.00000003",
+ fraction = 3,
+ specAmount = "1,500,000,000.00",
+ noSpecAmount = "1,500,000,000.00000003",
+ currency = "EUR",
+ symbol = "€",
+ )
+
+ amountToString(
+ amount = Amount.fromString("EUR", "500000000.126"),
+ spec = specEUR,
+ rawStr = "500000000.126",
+ fraction = 12600000,
+ specAmount = "500,000,000.13",
+ noSpecAmount = "500,000,000.126",
+ currency = "EUR",
+ symbol = "€",
+ )
+
+ amountToString(
+ amount = Amount.fromString("NOSYMBOL", "13.24"),
+ spec = CurrencySpecification(
+ name = "No symbol!",
+ numFractionalInputDigits = 2,
+ numFractionalNormalDigits = 2,
+ numFractionalTrailingZeroDigits = 2,
+ altUnitNames = mapOf(),
+ ),
+ rawStr = "13.24",
+ fraction = 24000000,
+ specAmount = "13.24",
+ noSpecAmount = "13.24",
+ currency = "NOSYMBOL",
+ symbol = "NOSYMBOL",
+ )
+ }
+
+ private fun amountToString(
+ amount: Amount,
+ spec: CurrencySpecification,
+ rawStr: String,
+ fraction: Int,
+ specAmount: String,
+ noSpecAmount: String,
+ currency: String,
+ symbol: String,
+ ) {
+ val symbols = DecimalFormatSymbols.getInstance()
+ symbols.decimalSeparator = '.'
+ symbols.groupingSeparator = ','
+ symbols.monetaryDecimalSeparator = '.'
+ if (Build.VERSION.SDK_INT >= 34) {
+ symbols.monetaryGroupingSeparator = ','
+ }
+
+ // Only the raw amount
+ assertEquals(rawStr, amount.amountStr)
+ assertEquals(fraction, amount.fraction)
+
+ // The amount without currency spec
+ assertEquals("$noSpecAmount $currency", amount.toString(symbols = symbols))
+ assertEquals(noSpecAmount, amount.toString(symbols = symbols, showSymbol = false))
+ assertEquals("-$noSpecAmount $currency", amount.toString(symbols = symbols, negative = true))
+ assertEquals("-$noSpecAmount", amount.toString(symbols = symbols, showSymbol = false, negative = true))
+
+ // The amount with currency spec
+ val withSpec = amount.withSpec(spec)
+ assertEquals(specAmount, withSpec.toString(symbols = symbols, showSymbol = false))
+ assertEquals(specAmount, withSpec.toString(symbols = symbols, showSymbol = false))
+ assertEquals("-$specAmount", withSpec.toString(symbols = symbols, showSymbol = false, negative = true))
+ assertEquals("-$specAmount", withSpec.toString(symbols = symbols, showSymbol = false, negative = true))
+ if (spec.symbol != null) {
+ assertEquals("${symbol}$specAmount", withSpec.toString(symbols = symbols))
+ assertEquals("-${symbol}$specAmount", withSpec.toString(symbols = symbols, negative = true))
+ } else {
+ assertEquals("$specAmount $currency", withSpec.toString(symbols = symbols))
+ assertEquals("-$specAmount $currency", withSpec.toString(symbols = symbols, negative = true))
}
}
@@ -76,7 +220,6 @@ class AmountTest {
assertEquals(str, amount.toJSONString())
assertEquals("TESTKUDOS123", amount.currency)
assertEquals(maxValue, amount.value)
- assertEquals("$maxValue.99999999 TESTKUDOS123", amount.toString())
// longer currency not accepted
assertThrows<AmountParserException>("longer currency was accepted") {
diff --git a/wallet/.gitignore b/wallet/.gitignore
index 44ca846..5ddaed3 100644
--- a/wallet/.gitignore
+++ b/wallet/.gitignore
@@ -1,3 +1,3 @@
/build
/.bundle
-/src/main/assets/taler-wallet-*-*.js
+/src/main/jniLibs \ No newline at end of file
diff --git a/wallet/build.gradle b/wallet/build.gradle
index e17d1ab..93f4a49 100644
--- a/wallet/build.gradle
+++ b/wallet/build.gradle
@@ -13,18 +13,13 @@
* You should have received a copy of the GNU General Public License along with
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-
-import com.android.build.gradle.tasks.MergeResources
-
plugins {
id "com.android.application"
id "kotlin-android"
id "kotlinx-serialization"
- id "de.undercouch.download" version "5.3.0"
}
-def walletCoreVersion = "v0.9.1"
-def walletCoreSha256 = "99fd96fb897c6954710728820369949090bff84d06728f04b29c6cb067c8f9d4"
+def qtart_version = "0.10.6"
static def versionCodeEpoch() {
return (new Date().getTime() / 1000).toInteger()
@@ -40,17 +35,15 @@ def gitCommit = { ->
android {
namespace 'net.taler.wallet'
- compileSdkVersion 33
- buildToolsVersion "$build_tools_version"
+ compileSdk 34
defaultConfig {
applicationId "net.taler.wallet"
- minSdkVersion 21
- targetSdkVersion 32
- versionCode 20
- versionName walletCoreVersion
+ minSdkVersion 24
+ targetSdkVersion 33
+ versionCode 41
+ versionName "0.10.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- buildConfigField "String", "WALLET_CORE_VERSION", "\"$walletCoreVersion\""
}
buildTypes {
release {
@@ -62,11 +55,13 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
- flavorDimensions "distributionChannel"
+ flavorDimensions = ["distributionChannel"]
productFlavors {
fdroid {
dimension "distributionChannel"
applicationIdSuffix ".fdroid"
+ // version codes get multiplied by 10 and an ABI suffix gets added to the code
+ // if 'splitApk' property is set
}
google {
dimension "distributionChannel"
@@ -80,22 +75,23 @@ android {
}
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
// Flag to enable support for the new language APIs for zxing
coreLibraryDesugaringEnabled true
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17"
}
composeOptions {
- kotlinCompilerExtensionVersion "1.3.2"
+ kotlinCompilerExtensionVersion "1.5.7"
}
buildFeatures {
+ buildConfig true
viewBinding true
compose true
}
@@ -107,6 +103,15 @@ android {
excludes += ['META-INF/*.kotlin_module']
}
}
+ splits {
+ abi {
+ // can not be defined per flavor, so we use a property to turn this on for F-Droid
+ enable project.hasProperty('splitApk')
+ reset() // Resets the list of ABIs to remove all included by default
+ include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
+ universalApk false
+ }
+ }
lint {
abortOnError true
@@ -116,26 +121,28 @@ android {
dependencies {
implementation project(":taler-kotlin-android")
- implementation 'net.taler:akono:0.2'
+ implementation "net.taler:qtart:$qtart_version@aar"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
- implementation 'androidx.preference:preference-ktx:1.2.0'
+ implementation 'androidx.preference:preference-ktx:1.2.1'
implementation "com.google.android.material:material:$material_version"
implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version"
- implementation "androidx.browser:browser:1.4.0"
+ implementation "androidx.browser:browser:1.7.0"
// Compose
- implementation 'androidx.activity:activity-compose:1.6.1'
- implementation "androidx.compose.animation:animation:$compose_version"
- implementation "androidx.compose.ui:ui-tooling:$compose_version"
- implementation "androidx.compose.material:material:1.3.1"
- implementation "androidx.compose.material:material-icons-extended:1.3.1"
- implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
- implementation "com.google.accompanist:accompanist-themeadapter-material:0.28.0"
+ implementation platform('androidx.compose:compose-bom:2023.10.01')
+ implementation 'androidx.compose.material3:material3'
+ implementation 'androidx.compose.material:material-icons-extended'
+ implementation "androidx.compose.runtime:runtime-livedata"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose"
+ implementation "com.google.accompanist:accompanist-themeadapter-material3:0.28.0"
+ implementation 'androidx.activity:activity-compose:1.8.2'
+ implementation 'androidx.compose.ui:ui-tooling-preview'
+ debugImplementation 'androidx.compose.ui:ui-tooling'
// Lists and Selection
- implementation "androidx.recyclerview:recyclerview:1.2.1"
+ implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.recyclerview:recyclerview-selection:1.1.0"
// Navigation Library
@@ -148,7 +155,7 @@ dependencies {
// QR codes
implementation 'com.journeyapps:zxing-android-embedded:4.3.0@aar'
// needed to support zxing library in taler-kotlin-android on API < 24
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
// Nicer ProgressBar
implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1'
@@ -159,39 +166,32 @@ dependencies {
implementation "io.noties.markwon:ext-tables:$markwon_version"
implementation "io.noties.markwon:recycler:$markwon_version"
+ // Java Native access for qtart
+ implementation "net.java.dev.jna:jna:5.13.0@aar"
+
testImplementation "junit:junit:$junit_version"
testImplementation 'org.json:json:20220320'
}
-def walletLibraryDir = "src/main/assets"
-def walletDestFile = "${walletLibraryDir}/taler-wallet-embedded-${walletCoreVersion}.js"
-task downloadWalletLibrary(type: Download, dependsOn: preBuild) {
- src "https://git.taler.net/wallet-core.git/plain/${walletCoreVersion}/taler-wallet-embedded.js?h=prebuilt"
- dest walletDestFile
- onlyIfModified true
- overwrite false
- doFirst {
- new File(walletLibraryDir).mkdirs()
- if (!file(dest).exists()) { // delete old versions before fetching new one
- delete fileTree(walletLibraryDir) {
- include 'taler-wallet-embedded-*.js'
- }
- }
- }
-}
-task verifyWalletLibrary(type: Verify, dependsOn: downloadWalletLibrary) {
- src walletDestFile
- algorithm 'SHA-256'
- checksum walletCoreSha256
-}
-
-tasks.withType(MergeResources) {
- inputs.dir walletLibraryDir
- dependsOn verifyWalletLibrary
-}
-
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
}
+
+// Map for the version code that gives each ABI a value.
+ext.abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, x86: 3, x86_64: 4]
+// For each APK output variant, override versionCode with a combination of ext.abiCodes + variant.versionCode.
+android.applicationVariants.configureEach { variant ->
+ // Assigns a different version code for each output APK
+ variant.outputs.each { output ->
+ // Stores the value of ext.abiCodes that is associated with the ABI for this variant.
+ def baseAbiVersionCode =
+ // Determines the ABI for this variant and returns the mapped value.
+ project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
+ if (baseAbiVersionCode != null) {
+ output.versionCodeOverride = 10 * variant.versionCode + baseAbiVersionCode
+ }
+ // leaves version code alone of there's no baseAbiVersionCode
+ }
+}
diff --git a/wallet/fastlane/Fastfile b/wallet/fastlane/Fastfile
index 695540c..70b10b8 100644
--- a/wallet/fastlane/Fastfile
+++ b/wallet/fastlane/Fastfile
@@ -30,6 +30,8 @@ platform :android do
build_type: "Release",
flavor: "google",
gradle_path: '../gradlew',
+ print_command: false,
+ flags: "--no-build-cache --no-configuration-cache",
properties: {
"android.injected.signing.store.file" => ENV["TALER_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["TALER_KEYSTORE_PASS"],
@@ -38,7 +40,6 @@ platform :android do
}
)
upload_to_play_store(
- track: 'beta',
skip_upload_images: 'true',
skip_upload_screenshots: 'true',
skip_upload_apk: 'true', # This is an app bundle, so APK is not possible
diff --git a/wallet/proguard-rules.pro b/wallet/proguard-rules.pro
index 04cb4da..f06a5aa 100644
--- a/wallet/proguard-rules.pro
+++ b/wallet/proguard-rules.pro
@@ -20,7 +20,7 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
--keep class net.taler.akono.** {*;}
-
#noinspection ShrinkerUnresolvedReference
+
+-dontobfuscate
-keep class net.taler.wallet.** {*;}
diff --git a/wallet/src/main/AndroidManifest.xml b/wallet/src/main/AndroidManifest.xml
index 23746cd..b8c05c6 100644
--- a/wallet/src/main/AndroidManifest.xml
+++ b/wallet/src/main/AndroidManifest.xml
@@ -34,22 +34,24 @@
android:required="false" />
<application
+ android:name=".WalletApp"
android:allowBackup="true"
android:extractNativeLibs="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
- tools:ignore="GoogleAppIndexingWarning"
- tools:targetApi="m">
+ tools:ignore="GoogleAppIndexingWarning">
<activity
android:name=".MainActivity"
android:exported="true"
- android:launchMode="singleTop"
- android:theme="@style/AppTheme.NoActionBar">
+ android:launchMode="singleInstance"
+ android:theme="@style/AppTheme.NoActionBar"
+ android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -64,6 +66,10 @@
<data
android:scheme="TALER"
tools:ignore="AppLinkUrlError" />
+ <data android:scheme="ext+taler" />
+ <data
+ android:scheme="EXT+TALER"
+ tools:ignore="AppLinkUrlError" />
<data android:scheme="payto" />
</intent-filter>
</activity>
@@ -85,10 +91,6 @@
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice" />
</service>
-
- <service
- android:name=".backend.WalletBackendService"
- android:process=":WalletBackendService" />
</application>
<queries>
diff --git a/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt b/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt
new file mode 100644
index 0000000..6b8db78
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt
@@ -0,0 +1,272 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet
+
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast.LENGTH_LONG
+import androidx.compose.ui.platform.ComposeView
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import net.taler.common.isOnline
+import net.taler.common.showError
+import net.taler.wallet.compose.LoadingScreen
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.refund.RefundStatus
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URL
+import java.util.Locale
+
+class HandleUriFragment: Fragment() {
+ private val model: MainViewModel by activityViewModels()
+
+ lateinit var uri: String
+ lateinit var from: String
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ uri = arguments?.getString("uri") ?: error("no uri passed")
+ from = arguments?.getString("from") ?: error("no from passed")
+
+ return ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ LoadingScreen()
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val uri = Uri.parse(uri)
+ if (uri.fragment != null && !requireContext().isOnline()) {
+ connectToWifi(requireContext(), uri.fragment!!)
+ }
+
+ // TODO: fix this bad async programming, make it only async when needed.
+ getTalerAction(uri, 3, MutableLiveData<String>()).observe(viewLifecycleOwner) { u ->
+ Log.v(TAG, "found action $u")
+
+ if (u.startsWith("payto://", ignoreCase = true)) {
+ Log.v(TAG, "navigating with paytoUri!")
+ val bundle = bundleOf("uri" to u)
+ findNavController().navigate(R.id.action_handleUri_to_nav_payto_uri, bundle)
+ return@observe
+ }
+
+ val normalizedURL = u.lowercase(Locale.ROOT)
+ var ext = false
+ val action = normalizedURL.substring(
+ if (normalizedURL.startsWith("taler://", ignoreCase = true)) {
+ "taler://".length
+ } else if (normalizedURL.startsWith("ext+taler://", ignoreCase = true)) {
+ ext = true
+ "ext+taler://".length
+ } else if (normalizedURL.startsWith("taler+http://", ignoreCase = true) &&
+ model.devMode.value == true
+ ) {
+ "taler+http://".length
+ } else {
+ normalizedURL.length
+ }
+ )
+
+ // Remove ext+ scheme prefix if present
+ val u2 = if (ext) {
+ "taler://" + u.substring("ext+taler://".length)
+ } else u
+
+ when {
+ action.startsWith("pay/", ignoreCase = true) -> {
+ Log.v(TAG, "navigating!")
+ findNavController().navigate(R.id.action_handleUri_to_promptPayment)
+ model.paymentManager.preparePay(u2)
+ }
+ action.startsWith("withdraw/", ignoreCase = true) -> {
+ Log.v(TAG, "navigating!")
+ // there's more than one entry point, so use global action
+ findNavController().navigate(R.id.action_handleUri_to_promptWithdraw)
+ model.withdrawManager.getWithdrawalDetails(u2)
+ }
+
+ action.startsWith("withdraw-exchange/", ignoreCase = true) -> {
+ prepareManualWithdrawal(u2)
+ }
+
+ action.startsWith("refund/", ignoreCase = true) -> {
+ model.showProgressBar.value = true
+ model.refundManager.refund(u2).observe(viewLifecycleOwner, Observer(::onRefundResponse))
+ }
+ action.startsWith("pay-pull/", ignoreCase = true) -> {
+ findNavController().navigate(R.id.action_handleUri_to_promptPullPayment)
+ model.peerManager.preparePeerPullDebit(u2)
+ }
+ action.startsWith("pay-push/", ignoreCase = true) -> {
+ findNavController().navigate(R.id.action_handleUri_to_promptPushPayment)
+ model.peerManager.preparePeerPushCredit(u2)
+ }
+ action.startsWith("pay-template/", ignoreCase = true) -> {
+ val bundle = bundleOf("uri" to u2)
+ findNavController().navigate(R.id.action_handleUri_to_promptPayTemplate, bundle)
+ }
+ action.startsWith("dev-experiment/", ignoreCase = true) -> {
+ model.applyDevExperiment(u2) { error ->
+ showError(error)
+ }
+ findNavController().navigate(R.id.nav_main)
+ }
+ else -> {
+ showError(R.string.error_unsupported_uri, "From: $from\nURI: $u2")
+ findNavController().popBackStack()
+ }
+ }
+ }
+ }
+
+ private fun getTalerAction(
+ uri: Uri,
+ maxRedirects: Int,
+ actionFound: MutableLiveData<String>,
+ ): MutableLiveData<String> {
+ val scheme = uri.scheme ?: return actionFound
+
+ if (scheme == "http" || scheme == "https") {
+ model.viewModelScope.launch(Dispatchers.IO) {
+ val conn = URL(uri.toString()).openConnection() as HttpURLConnection
+ Log.v(TAG, "prepare query: $uri")
+ conn.setRequestProperty("Accept", "text/html")
+ conn.connectTimeout = 5000
+ conn.requestMethod = "HEAD"
+ try {
+ conn.connect()
+ } catch (e: IOException) {
+ Log.e(TAG, "Error connecting to $uri ", e)
+ showError(R.string.error_broken_uri, "$uri")
+ return@launch
+ }
+ val status = conn.responseCode
+
+ if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_PAYMENT_REQUIRED) {
+ val talerHeader = conn.headerFields["Taler"]
+ if (talerHeader != null && talerHeader[0] != null) {
+ Log.v(TAG, "taler header: ${talerHeader[0]}")
+ val talerHeaderUri = Uri.parse(talerHeader[0])
+ getTalerAction(talerHeaderUri, 0, actionFound)
+ }
+ } else if (status == HttpURLConnection.HTTP_MOVED_TEMP
+ || status == HttpURLConnection.HTTP_MOVED_PERM
+ || status == HttpURLConnection.HTTP_SEE_OTHER
+ ) {
+ val location = conn.headerFields["Location"]
+ if (location != null && location[0] != null) {
+ Log.v(TAG, "location redirect: ${location[0]}")
+ val locUri = Uri.parse(location[0])
+ getTalerAction(locUri, maxRedirects - 1, actionFound)
+ }
+ } else {
+ showError(R.string.error_broken_uri, "$uri")
+ findNavController().popBackStack()
+ }
+ }
+ } else {
+ actionFound.postValue(uri.toString())
+ }
+
+ return actionFound
+ }
+
+ private fun prepareManualWithdrawal(uri: String) {
+ model.showProgressBar.value = true
+ lifecycleScope.launch(Dispatchers.IO) {
+ val response = model.withdrawManager.prepareManualWithdrawal(uri)
+ if (response == null) withContext(Dispatchers.Main) {
+ model.showProgressBar.value = false
+ findNavController().navigate(R.id.errorFragment)
+ } else {
+ val exchange =
+ model.exchangeManager.findExchangeByUrl(response.exchangeBaseUrl)
+ if (exchange == null) withContext(Dispatchers.Main) {
+ model.showProgressBar.value = false
+ showError(R.string.exchange_add_error)
+ findNavController().navigateUp()
+ } else {
+ model.exchangeManager.withdrawalExchange = exchange
+ withContext(Dispatchers.Main) {
+ model.showProgressBar.value = false
+ val args = Bundle().apply {
+ if (response.amount != null) {
+ putString("amount", response.amount.toJSONString())
+ }
+ }
+
+ findNavController().navigate(R.id.action_handleUri_to_manualWithdrawal, args)
+ }
+ }
+ }
+ }
+ }
+
+ private fun onRefundResponse(status: RefundStatus) {
+ model.showProgressBar.value = false
+ when (status) {
+ is RefundStatus.Error -> {
+ if (model.devMode.value == true) {
+ showError(status.error)
+ } else {
+ showError(R.string.refund_error, status.error.userFacingMsg)
+ }
+
+ findNavController().navigateUp()
+ }
+ is RefundStatus.Success -> {
+ lifecycleScope.launch {
+ val transactionId = status.response.transactionId
+ val transaction = model.transactionManager.getTransactionById(transactionId)
+ if (transaction != null) {
+ // TODO: currency what? scopes are the cool thing now
+ // val currency = transaction.amountRaw.currency
+ // model.showTransactions(currency)
+ Snackbar.make(requireView(), getString(R.string.refund_success), LENGTH_LONG).show()
+ }
+
+ findNavController().navigateUp()
+ }
+ }
+
+ }
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
index 13fd394..00fd2d3 100644
--- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
@@ -22,9 +22,9 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.content.IntentFilter
-import android.net.Uri
import android.os.Bundle
import android.util.Log
+import android.view.Menu
import android.view.MenuItem
import android.view.View.GONE
import android.view.View.INVISIBLE
@@ -34,9 +34,6 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat.START
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Observer
-import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
@@ -45,18 +42,12 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener
-import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
-import com.google.android.material.snackbar.Snackbar
import com.google.zxing.client.android.Intents.Scan.MIXED_SCAN
import com.google.zxing.client.android.Intents.Scan.SCAN_TYPE
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.journeyapps.barcodescanner.ScanOptions.QR_CODE
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import net.taler.common.EventObserver
-import net.taler.common.isOnline
-import net.taler.common.showError
import net.taler.wallet.BuildConfig.VERSION_CODE
import net.taler.wallet.BuildConfig.VERSION_NAME
import net.taler.wallet.HostCardEmulatorService.Companion.HTTP_TUNNEL_RESPONSE
@@ -64,12 +55,7 @@ import net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_CONNECTED
import net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_DISCONNECTED
import net.taler.wallet.HostCardEmulatorService.Companion.TRIGGER_PAYMENT_ACTION
import net.taler.wallet.databinding.ActivityMainBinding
-import net.taler.wallet.refund.RefundStatus
-import java.net.HttpURLConnection
-import java.net.URL
-import java.util.Locale.ROOT
-import javax.net.ssl.HttpsURLConnection
-
+import net.taler.wallet.events.ObservabilityDialog
class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
OnPreferenceStartFragmentCallback {
@@ -100,7 +86,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
setSupportActionBar(ui.content.toolbar)
val appBarConfiguration = AppBarConfiguration(
- setOf(R.id.nav_main, R.id.nav_settings, R.id.nav_pending_operations),
+ setOf(R.id.nav_main, R.id.nav_settings),
ui.drawerLayout
)
ui.content.toolbar.setupWithNavController(nav, appBarConfiguration)
@@ -110,14 +96,13 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
}
val versionView: TextView = ui.navView.getHeaderView(0).findViewById(R.id.versionView)
- model.devMode.observe(this) { enabled ->
- ui.navView.menu.findItem(R.id.nav_dev).isVisible = enabled
- if (enabled) {
- @SuppressLint("SetTextI18n")
- versionView.text = "$VERSION_NAME ($VERSION_CODE)"
- versionView.visibility = VISIBLE
- } else versionView.visibility = GONE
- }
+ @SuppressLint("SetTextI18n")
+ versionView.text = "$VERSION_NAME ($VERSION_CODE)"
+
+ // Uncomment if any dev options are added in the future
+ // model.devMode.observe(this) { enabled ->
+ // ui.navView.menu.findItem(R.id.nav_dev).isVisible = enabled
+ // }
if (intent.action == ACTION_VIEW) intent.dataString?.let { uri ->
handleTalerUri(uri, "intent")
@@ -140,8 +125,17 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
}
if (it) barcodeLauncher.launch(scanOptions)
})
+
+ model.networkManager.networkStatus.observe(this) { online ->
+ ui.content.offlineBanner.visibility = if (online) GONE else VISIBLE
+ }
+
+ model.devMode.observe(this) {
+ invalidateMenu()
+ }
}
+ @Deprecated("Deprecated in Java")
override fun onBackPressed() {
if (ui.drawerLayout.isDrawerOpen(START)) ui.drawerLayout.closeDrawer(START)
else super.onBackPressed()
@@ -154,147 +148,43 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
}
}
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ if (model.devMode.value == true) {
+ menuInflater.inflate(R.menu.global_dev, menu)
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_home -> nav.navigate(R.id.nav_main)
R.id.nav_settings -> nav.navigate(R.id.nav_settings)
- R.id.nav_pending_operations -> nav.navigate(R.id.nav_pending_operations)
}
ui.drawerLayout.closeDrawer(START)
return true
}
- override fun onDestroy() {
- unregisterReceiver(triggerPaymentReceiver)
- unregisterReceiver(nfcConnectedReceiver)
- unregisterReceiver(nfcDisconnectedReceiver)
- unregisterReceiver(tunnelResponseReceiver)
- super.onDestroy()
- }
-
- private fun getTalerAction(
- uri: Uri,
- maxRedirects: Int,
- actionFound: MutableLiveData<String>,
- ): MutableLiveData<String> {
- val scheme = uri.scheme ?: return actionFound
-
- if (scheme == "http" || scheme == "https") {
- model.viewModelScope.launch(Dispatchers.IO) {
- val conn: HttpsURLConnection =
- URL(uri.toString()).openConnection() as HttpsURLConnection
- Log.v(TAG, "prepare query: $uri")
- conn.setRequestProperty("Accept", "text/html")
- conn.connectTimeout = 5000
- conn.requestMethod = "HEAD"
- conn.connect()
- val status = conn.responseCode
-
- if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_PAYMENT_REQUIRED) {
- val talerHeader = conn.headerFields["Taler"]
- if (talerHeader != null && talerHeader[0] != null) {
- Log.v(TAG, "taler header: ${talerHeader[0]}")
- val talerHeaderUri = Uri.parse(talerHeader[0])
- getTalerAction(talerHeaderUri, 0, actionFound)
- }
- }
- if (status == HttpURLConnection.HTTP_MOVED_TEMP
- || status == HttpURLConnection.HTTP_MOVED_PERM
- || status == HttpURLConnection.HTTP_SEE_OTHER
- ) {
- val location = conn.headerFields["Location"]
- if (location != null && location[0] != null) {
- Log.v(TAG, "location redirect: ${location[0]}")
- val locUri = Uri.parse(location[0])
- getTalerAction(locUri, maxRedirects - 1, actionFound)
- }
- }
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.action_show_logs -> {
+ ObservabilityDialog().show(supportFragmentManager, "OBSERVABILITY")
}
- } else {
- actionFound.postValue(uri.toString())
}
-
- return actionFound
+ return super.onOptionsItemSelected(item)
}
- private fun handleTalerUri(url: String, from: String) {
- val uri = Uri.parse(url)
- if (uri.fragment != null && !isOnline()) {
- connectToWifi(this, uri.fragment!!)
- }
-
- getTalerAction(uri, 3, MutableLiveData<String>()).observe(this) { u ->
- Log.v(TAG, "found action $u")
-
- if (u.startsWith("payto://", ignoreCase = true)) {
- Log.v(TAG, "navigating with paytoUri!")
- val bundle = bundleOf("uri" to u)
- nav.navigate(R.id.action_nav_payto_uri, bundle)
- return@observe
- }
-
- val normalizedURL = u.lowercase(ROOT)
- val action = normalizedURL.substring(
- if (normalizedURL.startsWith("taler://", ignoreCase = true)) {
- "taler://".length
- } else if (normalizedURL.startsWith("taler+http://", ignoreCase = true) &&
- model.devMode.value == true
- ) {
- "taler+http://".length
- } else {
- normalizedURL.length
- }
- )
-
- when {
- action.startsWith("pay/", ignoreCase = true) -> {
- Log.v(TAG, "navigating!")
- nav.navigate(R.id.action_global_promptPayment)
- model.paymentManager.preparePay(u)
- }
- action.startsWith("tip/", ignoreCase = true) -> {
- Log.v(TAG, "navigating!")
- nav.navigate(R.id.action_global_promptTip)
- model.tipManager.prepareTip(u)
- }
- action.startsWith("withdraw/", ignoreCase = true) -> {
- Log.v(TAG, "navigating!")
- // there's more than one entry point, so use global action
- nav.navigate(R.id.action_global_promptWithdraw)
- model.withdrawManager.getWithdrawalDetails(u)
- }
- action.startsWith("refund/", ignoreCase = true) -> {
- model.showProgressBar.value = true
- model.refundManager.refund(u).observe(this, Observer(::onRefundResponse))
- }
- action.startsWith("pay-pull/", ignoreCase = true) -> {
- nav.navigate(R.id.action_global_prompt_pull_payment)
- model.peerManager.checkPeerPullPayment(u)
- }
- action.startsWith("pay-push/", ignoreCase = true) -> {
- nav.navigate(R.id.action_global_prompt_push_payment)
- model.peerManager.checkPeerPushPayment(u)
- }
- else -> {
- showError(R.string.error_unsupported_uri, "From: $from\nURI: $u")
- }
- }
- }
+ private fun handleTalerUri(uri: String, from: String) {
+ val args = bundleOf("uri" to uri, "from" to from)
+ nav.navigate(R.id.action_global_handle_uri, args)
}
- private fun onRefundResponse(status: RefundStatus) {
- model.showProgressBar.value = false
- when (status) {
- is RefundStatus.Error -> {
- showError(R.string.refund_error, status.msg)
- }
- is RefundStatus.Success -> {
- val amount = status.response.amountRefundGranted
- model.showTransactions(amount.currency)
- val str = getString(R.string.refund_success, amount.amountStr)
- Snackbar.make(ui.navView, str, LENGTH_LONG).show()
- }
- }
+ override fun onDestroy() {
+ unregisterReceiver(triggerPaymentReceiver)
+ unregisterReceiver(nfcConnectedReceiver)
+ unregisterReceiver(nfcDisconnectedReceiver)
+ unregisterReceiver(tunnelResponseReceiver)
+ super.onDestroy()
}
private val triggerPaymentReceiver = object : BroadcastReceiver() {
@@ -335,7 +225,6 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
pref: Preference,
): Boolean {
when (pref.key) {
- "pref_backup" -> nav.navigate(R.id.action_nav_settings_to_nav_settings_backup)
"pref_exchanges" -> nav.navigate(R.id.action_nav_settings_to_nav_settings_exchanges)
}
return true
diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt
index 2521e29..9fa9838 100644
--- a/wallet/src/main/java/net/taler/wallet/MainFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainFragment.kt
@@ -24,19 +24,20 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import net.taler.common.EventObserver
-import net.taler.wallet.CurrencyMode.MULTI
-import net.taler.wallet.CurrencyMode.SINGLE
-import net.taler.wallet.balances.BalanceItem
+import net.taler.wallet.ScopeMode.MULTI
+import net.taler.wallet.ScopeMode.SINGLE
+import net.taler.wallet.balances.BalanceState
+import net.taler.wallet.balances.BalanceState.Success
import net.taler.wallet.balances.BalancesFragment
import net.taler.wallet.databinding.FragmentMainBinding
import net.taler.wallet.transactions.TransactionsFragment
-enum class CurrencyMode { SINGLE, MULTI }
+enum class ScopeMode { SINGLE, MULTI }
class MainFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
- private var currencyMode: CurrencyMode? = null
+ private var scopeMode: ScopeMode? = null
private lateinit var ui: FragmentMainBinding
@@ -50,13 +51,13 @@ class MainFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- model.balances.observe(viewLifecycleOwner) {
+ model.balanceManager.state.observe(viewLifecycleOwner) {
onBalancesChanged(it)
}
- model.transactionsEvent.observe(viewLifecycleOwner, EventObserver { currency ->
- // we only need to navigate to a dedicated list, when in multi-currency mode
- if (currencyMode == MULTI) {
- model.transactionManager.selectedCurrency = currency
+ model.transactionsEvent.observe(viewLifecycleOwner, EventObserver { scopeInfo ->
+ // we only need to navigate to a dedicated list, when in multi-scope mode
+ if (scopeMode == MULTI) {
+ model.transactionManager.selectedScope = scopeInfo
findNavController().navigate(R.id.action_nav_main_to_nav_transactions)
}
})
@@ -72,19 +73,21 @@ class MainFragment : Fragment() {
override fun onStart() {
super.onStart()
- model.loadBalances()
+ model.balanceManager.loadBalances()
}
- private fun onBalancesChanged(balances: List<BalanceItem>) {
+ private fun onBalancesChanged(state: BalanceState) {
+ if (state !is Success) return
+ val balances = state.balances
val mode = if (balances.size == 1) SINGLE else MULTI
- if (currencyMode != mode) {
+ if (scopeMode != mode) {
val f = if (mode == SINGLE) {
- model.transactionManager.selectedCurrency = balances[0].available.currency
+ model.transactionManager.selectedScope = balances[0].scopeInfo
TransactionsFragment()
} else {
BalancesFragment()
}
- currencyMode = mode
+ scopeMode = mode
childFragmentManager.beginTransaction()
.replace(R.id.mainFragmentContainer, f, mode.name)
.commitNow()
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
index 255c28b..82eb8d7 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -22,134 +22,146 @@ import androidx.annotation.UiThread
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.Job
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.launch
+import kotlinx.serialization.encodeToString
import net.taler.common.Amount
import net.taler.common.AmountParserException
import net.taler.common.Event
-import net.taler.common.assertUiThread
import net.taler.common.toEvent
import net.taler.wallet.accounts.AccountManager
+import net.taler.wallet.backend.BackendManager
+import net.taler.wallet.backend.NotificationPayload
+import net.taler.wallet.backend.NotificationReceiver
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.backend.VersionReceiver
import net.taler.wallet.backend.WalletBackendApi
-import net.taler.wallet.balances.BalanceItem
-import net.taler.wallet.balances.BalanceResponse
+import net.taler.wallet.backend.WalletCoreVersion
+import net.taler.wallet.backend.WalletRunConfig
+import net.taler.wallet.backend.WalletRunConfig.Testing
+import net.taler.wallet.balances.BalanceManager
+import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.deposit.DepositManager
+import net.taler.wallet.events.ObservabilityEvent
import net.taler.wallet.exchanges.ExchangeManager
import net.taler.wallet.payment.PaymentManager
import net.taler.wallet.peer.PeerManager
-import net.taler.wallet.pending.PendingOperationsManager
import net.taler.wallet.refund.RefundManager
import net.taler.wallet.settings.SettingsManager
-import net.taler.wallet.tip.TipManager
import net.taler.wallet.transactions.TransactionManager
import net.taler.wallet.withdraw.WithdrawManager
import org.json.JSONObject
-import java.util.concurrent.TimeUnit.DAYS
-import java.util.concurrent.TimeUnit.MINUTES
-import kotlin.random.Random
const val TAG = "taler-wallet"
+const val OBSERVABILITY_LIMIT = 100
private val transactionNotifications = listOf(
- "proposal-accepted",
- "refresh-revealed",
- "withdraw-group-finished"
+ "transaction-state-transition",
)
-class MainViewModel(val app: Application) : AndroidViewModel(app) {
+private val observabilityNotifications = listOf(
+ "task-observability-event",
+ "request-observability-event",
+)
+
+class MainViewModel(
+ app: Application,
+) : AndroidViewModel(app), VersionReceiver, NotificationReceiver {
- private val mBalances = MutableLiveData<List<BalanceItem>>()
- val balances: LiveData<List<BalanceItem>> = mBalances.distinctUntilChanged()
+ private val mDevMode = MutableLiveData(BuildConfig.DEBUG)
+ val devMode: LiveData<Boolean> = mDevMode
- val devMode = MutableLiveData(BuildConfig.DEBUG)
val showProgressBar = MutableLiveData<Boolean>()
+ var walletVersion: String? = null
+ private set
+ var walletVersionHash: String? = null
+ private set
var exchangeVersion: String? = null
private set
var merchantVersion: String? = null
private set
- private val api = WalletBackendApi(app) { payload ->
- if (payload.optString("operation") == "init") {
- val result = payload.getJSONObject("result")
- val versions = result.getJSONObject("versionInfo")
- exchangeVersion = versions.getString("exchange")
- merchantVersion = versions.getString("merchant")
- } else if (payload.getString("type") != "waiting-for-retry") { // ignore ping
- Log.i(TAG, "Received notification from wallet-core: ${payload.toString(2)}")
- loadBalances()
- if (payload.optString("type") in transactionNotifications) {
- assertUiThread()
- // TODO notification API should give us a currency to update
- // update currently selected transaction list
- transactionManager.loadTransactions()
- }
- // refresh pending ops and history with each notification
- if (devMode.value == true) {
- pendingOperationsManager.getPending()
- }
- }
- }
+ @set:Synchronized
+ private var walletConfig = WalletRunConfig(
+ testing = Testing(
+ emitObservabilityEvents = true,
+ devModeActive = devMode.value ?: false,
+ )
+ )
+ private val api = WalletBackendApi(app, walletConfig, this, this)
+
+ val networkManager = NetworkManager(app.applicationContext)
val withdrawManager = WithdrawManager(api, viewModelScope)
- val tipManager = TipManager(api, viewModelScope)
val paymentManager = PaymentManager(api, viewModelScope)
- val pendingOperationsManager: PendingOperationsManager = PendingOperationsManager(api)
val transactionManager: TransactionManager = TransactionManager(api, viewModelScope)
val refundManager = RefundManager(api, viewModelScope)
+ val balanceManager = BalanceManager(api, viewModelScope)
val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope)
- val peerManager: PeerManager = PeerManager(api, viewModelScope)
- val settingsManager: SettingsManager = SettingsManager(app.applicationContext, viewModelScope)
+ val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope)
+ val settingsManager: SettingsManager = SettingsManager(app.applicationContext, api, viewModelScope)
val accountManager: AccountManager = AccountManager(api, viewModelScope)
val depositManager: DepositManager = DepositManager(api, viewModelScope)
- private val mTransactionsEvent = MutableLiveData<Event<String>>()
- val transactionsEvent: LiveData<Event<String>> = mTransactionsEvent
+ private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>()
+ val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent
+
+ private val mObservabilityLog = MutableStateFlow<List<ObservabilityEvent>>(emptyList())
+ val observabilityLog: StateFlow<List<ObservabilityEvent>> = mObservabilityLog
private val mScanCodeEvent = MutableLiveData<Event<Boolean>>()
val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent
- private val mLastBackup = MutableLiveData(
- // fake backup time until we actually do backup
- System.currentTimeMillis() -
- Random.nextLong(MINUTES.toMillis(5), DAYS.toMillis(2))
- )
- val lastBackup: LiveData<Long> = mLastBackup
-
- override fun onCleared() {
- api.destroy()
- super.onCleared()
+ override fun onVersionReceived(versionInfo: WalletCoreVersion) {
+ walletVersion = versionInfo.implementationSemver
+ walletVersionHash = versionInfo.implementationGitHash
+ exchangeVersion = versionInfo.exchange
+ merchantVersion = versionInfo.merchant
}
- @UiThread
- fun loadBalances(): Job = viewModelScope.launch {
- showProgressBar.value = true
- val response = api.request("getBalances", BalanceResponse.serializer())
- showProgressBar.value = false
- response.onError {
- // TODO expose in UI
- Log.e(TAG, "Error retrieving balances: $it")
+ override fun onNotificationReceived(payload: NotificationPayload) {
+ if (payload.type == "waiting-for-retry") return // ignore ping)
+
+ val str = BackendManager.json.encodeToString(payload)
+ Log.i(TAG, "Received notification from wallet-core: $str")
+
+ // Only update balances when we're told they changed
+ if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) {
+ balanceManager.loadBalances()
}
- response.onSuccess {
- mBalances.value = it.balances
+
+ if (payload.type in observabilityNotifications && payload.event != null) {
+ mObservabilityLog.getAndUpdate { logs ->
+ logs.takeLast(OBSERVABILITY_LIMIT)
+ .toMutableList().apply {
+ add(payload.event)
+ }
+ }
+ }
+
+ if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) {
+ // TODO notification API should give us a currency to update
+ // update currently selected transaction list
+ transactionManager.loadTransactions()
}
}
/**
- * Navigates to the given currency's transaction list, when [MainFragment] is shown.
+ * Navigates to the given scope info's transaction list, when [MainFragment] is shown.
*/
@UiThread
- fun showTransactions(currency: String) {
- mTransactionsEvent.value = currency.toEvent()
+ fun showTransactions(scopeInfo: ScopeInfo) {
+ mTransactionsEvent.value = scopeInfo.toEvent()
}
@UiThread
- fun getCurrencies(): List<String> {
- return balances.value?.map { balanceItem ->
- balanceItem.currency
- } ?: emptyList()
- }
+ fun getCurrencies() = balanceManager.balances.value?.map { balanceItem ->
+ balanceItem.currency
+ } ?: emptyList()
@UiThread
fun createAmount(amountText: String, currency: String): AmountResult {
@@ -164,7 +176,7 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) {
@UiThread
fun hasSufficientBalance(amount: Amount): Boolean {
- balances.value?.forEach { balanceItem ->
+ balanceManager.balances.value?.forEach { balanceItem ->
if (balanceItem.currency == amount.currency) {
return balanceItem.available >= amount
}
@@ -174,22 +186,26 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) {
@UiThread
fun dangerouslyReset() {
- api.sendRequest("reset")
withdrawManager.testWithdrawalStatus.value = null
- mBalances.value = emptyList()
+ balanceManager.resetBalances()
}
fun startTunnel() {
- api.sendRequest("startTunnel")
+ viewModelScope.launch {
+ api.sendRequest("startTunnel")
+ }
}
fun stopTunnel() {
- api.sendRequest("stopTunnel")
+ viewModelScope.launch {
+ api.sendRequest("stopTunnel")
+ }
}
fun tunnelResponse(resp: String) {
- val respJson = JSONObject(resp)
- api.sendRequest("tunnelResponse", respJson)
+ viewModelScope.launch {
+ api.sendRequest("tunnelResponse", JSONObject(resp))
+ }
}
@UiThread
@@ -197,6 +213,45 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) {
mScanCodeEvent.value = true.toEvent()
}
+ fun setDevMode(enabled: Boolean, onError: (error: TalerErrorInfo) -> Unit) {
+ mDevMode.postValue(enabled)
+ viewModelScope.launch {
+ val config = walletConfig.copy(
+ testing = walletConfig.testing?.copy(
+ devModeActive = enabled,
+ ) ?: Testing(
+ devModeActive = enabled,
+ ),
+ )
+
+ api.setWalletConfig(config)
+ .onSuccess {
+ walletConfig = config
+ }.onError(onError)
+ }
+ }
+
+ fun runIntegrationTest() {
+ viewModelScope.launch {
+ api.request<Unit>("runIntegrationTestV2") {
+ put("amountToWithdraw", "KUDOS:42")
+ put("amountToSpend", "KUDOS:23")
+ put("corebankApiBaseUrl", "https://bank.demo.taler.net/")
+ put("exchangeBaseUrl", "https://exchange.demo.taler.net/")
+ put("merchantBaseUrl", "https://backend.demo.taler.net/")
+ put("merchantAuthToken", "secret-token:sandbox")
+ }
+ }
+ }
+
+ fun applyDevExperiment(uri: String, onError: (error: TalerErrorInfo) -> Unit) {
+ viewModelScope.launch {
+ api.request<Unit>("applyDevExperiment") {
+ put("devExperimentUri", uri)
+ }.onError(onError)
+ }
+ }
+
}
sealed class AmountResult {
diff --git a/wallet/src/main/java/net/taler/wallet/NetworkManager.kt b/wallet/src/main/java/net/taler/wallet/NetworkManager.kt
new file mode 100644
index 0000000..a45ad48
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/NetworkManager.kt
@@ -0,0 +1,64 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet
+
+import android.content.Context
+import android.content.Context.CONNECTIVITY_SERVICE
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+
+class NetworkManager(context: Context) : ConnectivityManager.NetworkCallback() {
+ private val connectivityManager: ConnectivityManager
+
+ private val _networkStatus: MutableLiveData<Boolean>
+ val networkStatus: LiveData<Boolean>
+
+ init {
+ // careful, the order below is important, should probably get simplified
+ connectivityManager = context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ _networkStatus = MutableLiveData(getCurrentStatus())
+ networkStatus = _networkStatus
+ connectivityManager.registerDefaultNetworkCallback(this)
+ }
+
+ @UiThread
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+ super.onCapabilitiesChanged(network, networkCapabilities)
+ _networkStatus.postValue(networkCapabilities.isOnline())
+ }
+
+ override fun onLost(network: Network) {
+ super.onLost(network)
+ _networkStatus.postValue(getCurrentStatus())
+ }
+
+ private fun getCurrentStatus(): Boolean {
+ return connectivityManager.activeNetwork?.let { network ->
+ connectivityManager.getNetworkCapabilities(network)?.isOnline()
+ } ?: false
+ }
+
+ private fun NetworkCapabilities.isOnline(): Boolean {
+ return hasCapability(NET_CAPABILITY_INTERNET) && hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
index ac6b0bd..2accaaf 100644
--- a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
@@ -29,13 +29,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -43,10 +41,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.KeyboardType.Companion.Decimal
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
@@ -55,13 +51,19 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import net.taler.common.Amount
-import net.taler.common.Amount.Companion.isValidAmountStr
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.compose.AmountInputField
+import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.exchanges.ExchangeItem
class ReceiveFundsFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
private val exchangeManager get() = model.exchangeManager
+ private val withdrawManager get() = model.withdrawManager
+ private val balanceManager get() = model.balanceManager
+ private val peerManager get() = model.peerManager
+ private val scopeInfo get() = model.transactionManager.selectedScope ?: error("No scope selected")
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@@ -70,7 +72,8 @@ class ReceiveFundsFragment : Fragment() {
setContent {
TalerSurface {
ReceiveFundsIntro(
- model.transactionManager.selectedCurrency ?: error("No currency selected"),
+ scopeInfo.currency,
+ balanceManager.getSpecForScopeInfo(scopeInfo),
this@ReceiveFundsFragment::onManualWithdraw,
this@ReceiveFundsFragment::onPeerPull,
)
@@ -80,7 +83,7 @@ class ReceiveFundsFragment : Fragment() {
override fun onStart() {
super.onStart()
- activity?.setTitle(R.string.transactions_receive_funds)
+ activity?.setTitle(getString(R.string.transactions_receive_funds_title, scopeInfo.currency))
}
private fun onManualWithdraw(amount: Amount) {
@@ -98,15 +101,16 @@ class ReceiveFundsFragment : Fragment() {
Toast.makeText(requireContext(), "No exchange available", LENGTH_LONG).show()
return
}
- exchangeManager.withdrawalExchange = exchange
+
// now that we have the exchange, we can navigate
- val bundle = bundleOf("amount" to amount.toJSONString())
- findNavController().navigate(
- R.id.action_receiveFunds_to_nav_exchange_manual_withdrawal, bundle)
+ exchangeManager.withdrawalExchange = exchange
+ withdrawManager.getWithdrawalDetails(exchange.exchangeBaseUrl, amount)
+ findNavController().navigate(R.id.action_receiveFunds_to_nav_prompt_withdraw)
}
private fun onPeerPull(amount: Amount) {
val bundle = bundleOf("amount" to amount.toJSONString())
+ peerManager.checkPeerPullCredit(amount)
findNavController().navigate(R.id.action_receiveFunds_to_nav_peer_pull, bundle)
}
}
@@ -114,6 +118,7 @@ class ReceiveFundsFragment : Fragment() {
@Composable
private fun ReceiveFundsIntro(
currency: String,
+ spec: CurrencySpecification?,
onManualWithdraw: (Amount) -> Unit,
onPeerPull: (Amount) -> Unit,
) {
@@ -123,47 +128,40 @@ private fun ReceiveFundsIntro(
.fillMaxWidth()
.verticalScroll(scrollState),
) {
- var text by rememberSaveable { mutableStateOf("") }
+ var text by rememberSaveable { mutableStateOf("0") }
var isError by rememberSaveable { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(16.dp),
) {
- OutlinedTextField(
+ AmountInputField(
modifier = Modifier
.weight(1f)
.padding(end = 16.dp),
value = text,
- keyboardOptions = KeyboardOptions.Default.copy(keyboardType = Decimal),
onValueChange = { input ->
isError = false
- val filtered = input.filter { it.isDigit() || it == '.' }
- if (filtered.endsWith('.') || isValidAmountStr(filtered)) text = filtered
+ text = input
+ },
+ label = { Text(stringResource(R.string.receive_amount)) },
+ supportingText = {
+ if (isError) Text(stringResource(R.string.receive_amount_invalid))
},
isError = isError,
- label = {
- if (isError) {
- Text(
- stringResource(R.string.receive_amount_invalid),
- color = Color.Red,
- )
- } else {
- Text(stringResource(R.string.receive_amount))
- }
- }
+ numberOfDecimals = spec?.numFractionalInputDigits ?: DEFAULT_INPUT_DECIMALS,
)
Text(
modifier = Modifier,
- text = currency,
+ text = spec?.symbol ?: currency,
softWrap = false,
- style = MaterialTheme.typography.h6,
+ style = MaterialTheme.typography.titleLarge,
)
}
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringResource(R.string.receive_intro),
- style = MaterialTheme.typography.h6,
+ style = MaterialTheme.typography.titleLarge,
)
Row(modifier = Modifier.padding(16.dp)) {
Button(
@@ -173,7 +171,7 @@ private fun ReceiveFundsIntro(
.weight(1f),
onClick = {
val amount = getAmount(currency, text)
- if (amount == null) isError = true
+ if (amount == null || amount.isZero()) isError = true
else onManualWithdraw(amount)
}) {
Text(text = stringResource(R.string.receive_withdraw))
@@ -184,7 +182,7 @@ private fun ReceiveFundsIntro(
.height(IntrinsicSize.Max),
onClick = {
val amount = getAmount(currency, text)
- if (amount == null) isError = true
+ if (amount == null || amount.isZero()) isError = true
else onPeerPull(amount)
},
) {
@@ -198,6 +196,6 @@ private fun ReceiveFundsIntro(
@Composable
fun PreviewReceiveFundsIntro() {
Surface {
- ReceiveFundsIntro("TESTKUDOS", {}) {}
+ ReceiveFundsIntro("TESTKUDOS", null, {}) {}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
index 90b4ee0..2581979 100644
--- a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
@@ -27,13 +27,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -41,10 +39,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
@@ -52,12 +48,16 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import net.taler.common.Amount
-import net.taler.common.Amount.Companion.isValidAmountStr
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.compose.AmountInputField
+import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS
import net.taler.wallet.compose.TalerSurface
class SendFundsFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
+ private val balanceManager get() = model.balanceManager
private val peerManager get() = model.peerManager
+ private val scopeInfo get() = model.transactionManager.selectedScope ?: error("No scope selected")
override fun onCreateView(
inflater: LayoutInflater,
@@ -67,8 +67,8 @@ class SendFundsFragment : Fragment() {
setContent {
TalerSurface {
SendFundsIntro(
- currency = model.transactionManager.selectedCurrency
- ?: error("No currency selected"),
+ currency = scopeInfo.currency,
+ spec = balanceManager.getSpecForScopeInfo(scopeInfo),
hasSufficientBalance = model::hasSufficientBalance,
onDeposit = this@SendFundsFragment::onDeposit,
onPeerPush = this@SendFundsFragment::onPeerPush,
@@ -79,21 +79,17 @@ class SendFundsFragment : Fragment() {
override fun onStart() {
super.onStart()
- activity?.setTitle(R.string.transactions_send_funds)
+ activity?.setTitle(getString(R.string.transactions_send_funds_title, scopeInfo.currency))
}
- override fun onDestroy() {
- super.onDestroy()
- if (!requireActivity().isChangingConfigurations) peerManager.resetPushPayment()
- }
-
- fun onDeposit(amount: Amount) {
+ private fun onDeposit(amount: Amount) {
val bundle = bundleOf("amount" to amount.toJSONString())
findNavController().navigate(R.id.action_sendFunds_to_nav_deposit, bundle)
}
- fun onPeerPush(amount: Amount) {
+ private fun onPeerPush(amount: Amount) {
val bundle = bundleOf("amount" to amount.toJSONString())
+ peerManager.checkPeerPushDebit(amount)
findNavController().navigate(R.id.action_sendFunds_to_nav_peer_push, bundle)
}
}
@@ -101,6 +97,7 @@ class SendFundsFragment : Fragment() {
@Composable
private fun SendFundsIntro(
currency: String,
+ spec: CurrencySpecification?,
hasSufficientBalance: (Amount) -> Boolean,
onDeposit: (Amount) -> Unit,
onPeerPush: (Amount) -> Unit,
@@ -111,7 +108,7 @@ private fun SendFundsIntro(
.fillMaxWidth()
.verticalScroll(scrollState),
) {
- var text by rememberSaveable { mutableStateOf("") }
+ var text by rememberSaveable { mutableStateOf("0") }
var isError by rememberSaveable { mutableStateOf(false) }
var insufficientBalance by rememberSaveable { mutableStateOf(false) }
Row(
@@ -119,57 +116,49 @@ private fun SendFundsIntro(
modifier = Modifier
.padding(16.dp),
) {
- OutlinedTextField(
+ AmountInputField(
modifier = Modifier
.weight(1f)
.padding(end = 16.dp),
value = text,
- keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Decimal),
onValueChange = { input ->
isError = false
insufficientBalance = false
- val filtered = input.filter { it.isDigit() || it == '.' }
- if (filtered.endsWith('.') || isValidAmountStr(filtered)) text = filtered
+ text = input
},
- isError = isError || insufficientBalance,
- label = {
- if (isError) {
- Text(
- stringResource(R.string.receive_amount_invalid),
- color = Color.Red,
- )
- } else if (insufficientBalance) {
- Text(
- stringResource(R.string.payment_balance_insufficient),
- color = Color.Red,
- )
- } else {
- Text(stringResource(R.string.send_amount))
+ label = { Text(stringResource(R.string.send_amount)) },
+ supportingText = {
+ if (isError) Text(stringResource(R.string.receive_amount_invalid))
+ else if (insufficientBalance) {
+ Text(stringResource(R.string.payment_balance_insufficient))
}
- }
+ },
+ isError = isError || insufficientBalance,
+ numberOfDecimals = spec?.numFractionalInputDigits ?: DEFAULT_INPUT_DECIMALS,
)
Text(
modifier = Modifier,
- text = currency,
+ text = spec?.symbol ?: currency,
softWrap = false,
- style = MaterialTheme.typography.h6,
+ style = MaterialTheme.typography.titleLarge,
)
}
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringResource(R.string.send_intro),
- style = MaterialTheme.typography.h6,
+ style = MaterialTheme.typography.titleLarge,
)
Row(modifier = Modifier.padding(16.dp)) {
fun onClickButton(block: (Amount) -> Unit) {
val amount = getAmount(currency, text)
- if (amount == null) isError = true
+ if (amount == null || amount.isZero()) isError = true
else if (!hasSufficientBalance(amount)) insufficientBalance = true
else block(amount)
}
Button(
modifier = Modifier
.padding(end = 16.dp)
+ .height(IntrinsicSize.Max)
.weight(1f),
onClick = {
onClickButton { amount -> onDeposit(amount) }
@@ -202,6 +191,6 @@ private fun SendFundsIntro(
@Composable
fun PreviewSendFundsIntro() {
Surface {
- SendFundsIntro("TESTKUDOS", { true }, {}) {}
+ SendFundsIntro("TESTKUDOS", null, { true }, {}) {}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/UriInputFragment.kt b/wallet/src/main/java/net/taler/wallet/UriInputFragment.kt
index c65c53a..63a46a4 100644
--- a/wallet/src/main/java/net/taler/wallet/UriInputFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/UriInputFragment.kt
@@ -56,10 +56,11 @@ class UriInputFragment : Fragment() {
}
}
ui.okButton.setOnClickListener {
- if (ui.uriView.text?.startsWith("taler://", ignoreCase = true) == true ||
- ui.uriView.text?.startsWith("payto://", ignoreCase = true) == true) {
+ val trimmedText = ui.uriView.text?.trim()
+ if (trimmedText?.startsWith("taler://", ignoreCase = true) == true ||
+ trimmedText?.startsWith("payto://", ignoreCase = true) == true) {
ui.uriLayout.error = null
- launchInAppBrowser(requireContext(), ui.uriView.text.toString())
+ launchInAppBrowser(requireContext(), trimmedText.toString())
} else {
ui.uriLayout.error = getString(R.string.uri_invalid)
}
diff --git a/wallet/src/main/java/net/taler/wallet/Utils.kt b/wallet/src/main/java/net/taler/wallet/Utils.kt
index ed1899f..5c4fedc 100644
--- a/wallet/src/main/java/net/taler/wallet/Utils.kt
+++ b/wallet/src/main/java/net/taler/wallet/Utils.kt
@@ -26,17 +26,21 @@ import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSpecifier
import android.os.Build.VERSION.SDK_INT
+import android.util.TypedValue
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.annotation.RequiresApi
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.getSystemService
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import net.taler.common.Amount
import net.taler.common.AmountParserException
+import net.taler.common.showError
import net.taler.common.startActivitySafe
-import net.taler.wallet.backend.TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED
import net.taler.wallet.backend.TalerErrorInfo
-import net.taler.wallet.transactions.Transaction
const val CURRENCY_BTC = "BITCOINBTC"
@@ -103,13 +107,10 @@ fun getAmount(currency: String, text: String): Amount? {
}
}
-fun <T> Transaction.handleKyc(notRequired: () -> T, required: (TalerErrorInfo) -> T): T {
- return error?.let { error ->
- when (error.code) {
- WALLET_WITHDRAWAL_KYC_REQUIRED -> required(error)
- else -> notRequired()
- }
- } ?: notRequired()
+fun Context.getAttrColor(attr: Int): Int {
+ val value = TypedValue()
+ theme.resolveAttribute(attr, value, true)
+ return value.data
}
fun launchInAppBrowser(context: Context, url: String) {
@@ -117,4 +118,30 @@ fun launchInAppBrowser(context: Context, url: String) {
val intent = builder.build().intent
intent.data = Uri.parse(url)
context.startActivitySafe(intent)
+}
+
+fun Fragment.showError(error: TalerErrorInfo) {
+ @Suppress("OPT_IN_USAGE")
+ val json = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+ val message = json.encodeToString(error)
+ showError(message)
+}
+
+fun FragmentActivity.showError(error: TalerErrorInfo) {
+ @Suppress("OPT_IN_USAGE")
+ val json = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+ val message = json.encodeToString(error)
+ showError(message)
+}
+
+fun Context.getThemeColor(attr: Int): Int {
+ val typedValue = TypedValue()
+ theme.resolveAttribute(attr, typedValue, true)
+ return typedValue.data
} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt b/wallet/src/main/java/net/taler/wallet/WalletApp.kt
index d1a111f..1384f76 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt
+++ b/wallet/src/main/java/net/taler/wallet/WalletApp.kt
@@ -1,6 +1,6 @@
/*
* This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
+ * (C) 2023 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
@@ -14,11 +14,14 @@
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-package net.taler.wallet.balances
+package net.taler.wallet
-import kotlinx.serialization.Serializable
+import android.app.Application
+import com.google.android.material.color.DynamicColors
-@Serializable
-data class BalanceResponse(
- val balances: List<BalanceItem>
-)
+class WalletApp : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ DynamicColors.applyToActivitiesIfAvailable(this)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
new file mode 100644
index 0000000..def4668
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
@@ -0,0 +1,63 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.backend
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonObject
+import net.taler.wallet.events.ObservabilityEvent
+
+@Serializable
+sealed class ApiMessage {
+
+ @Serializable
+ @SerialName("notification")
+ data class Notification(
+ val payload: NotificationPayload,
+ ) : ApiMessage()
+
+}
+
+@Serializable
+data class NotificationPayload(
+ val type: String,
+ val id: String? = null,
+ val event: ObservabilityEvent? = null,
+)
+
+@Serializable
+sealed class ApiResponse : ApiMessage() {
+
+ abstract val id: Int
+ abstract val operation: String
+
+ @Serializable
+ @SerialName("response")
+ data class Response(
+ override val id: Int,
+ override val operation: String,
+ val result: JsonObject,
+ ) : ApiResponse()
+
+ @Serializable
+ @SerialName("error")
+ data class Error(
+ override val id: Int,
+ override val operation: String,
+ val error: JsonObject,
+ ) : ApiResponse()
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
new file mode 100644
index 0000000..9292ef5
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
@@ -0,0 +1,95 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.backend
+
+import android.util.Log
+import kotlinx.serialization.json.Json
+import net.taler.qtart.TalerWalletCore
+import net.taler.wallet.BuildConfig
+import org.json.JSONObject
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+
+fun interface NotificationReceiver {
+ fun onNotificationReceived(payload: NotificationPayload)
+}
+
+class BackendManager(
+ private val notificationReceiver: NotificationReceiver,
+) {
+
+ companion object {
+ private const val TAG = "BackendManager"
+ private const val TAG_CORE = "taler-wallet-embedded"
+ val json = Json {
+ ignoreUnknownKeys = true
+ coerceInputValues = true
+ }
+ @JvmStatic
+ private val initialized = AtomicBoolean(false)
+ }
+
+ private val walletCore = TalerWalletCore()
+ private val requestManager = RequestManager()
+
+ init {
+ // TODO using Dagger/Hilt and @Singleton would be nice as well
+ if (initialized.getAndSet(true)) error("Already initialized")
+ walletCore.setMessageHandler { onMessageReceived(it) }
+ walletCore.setCurlHttpClient()
+ if (BuildConfig.DEBUG) walletCore.setStdoutHandler {
+ Log.d(TAG_CORE, it)
+ }
+ }
+
+ fun run() {
+ walletCore.run()
+ }
+
+ suspend fun send(operation: String, args: JSONObject? = null): ApiResponse =
+ suspendCoroutine { cont ->
+ requestManager.addRequest(cont) { id ->
+ val request = JSONObject().apply {
+ put("id", id)
+ put("operation", operation)
+ if (args != null) put("args", args)
+ }
+ Log.d(TAG, "sending message:\n${request.toString(2)}")
+ walletCore.sendRequest(request.toString())
+ }
+ }
+
+ private fun onMessageReceived(msg: String) {
+ Log.d(TAG, "message received: $msg")
+ when (val message = json.decodeFromString<ApiMessage>(msg)) {
+ is ApiMessage.Notification -> {
+ notificationReceiver.onNotificationReceived(message.payload)
+ }
+ is ApiResponse -> {
+ val id = message.id
+ val cont = requestManager.getAndRemoveContinuation(id)
+ if (cont == null) {
+ Log.e(TAG, "wallet returned unknown request ID ($id)")
+ } else {
+ cont.resume(message)
+ }
+ }
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
new file mode 100644
index 0000000..7fe1a6b
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
@@ -0,0 +1,84 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.backend
+
+import kotlinx.serialization.Serializable
+import net.taler.wallet.exchanges.BuiltinExchange
+
+@Serializable
+data class InitResponse(
+ val versionInfo: WalletCoreVersion,
+)
+
+@Serializable
+data class WalletRunConfig(
+ val builtin: Builtin? = Builtin(),
+ val testing: Testing? = Testing(),
+ val features: Features? = Features(),
+) {
+ /**
+ * Initialization values useful for a complete startup.
+ *
+ * These are values may be overridden by different wallets
+ */
+ @Serializable
+ data class Builtin(
+ val exchanges: List<BuiltinExchange> = emptyList(),
+ )
+
+ /**
+ * Unsafe options which it should only be used to create
+ * testing environment.
+ */
+ @Serializable
+ data class Testing(
+ /**
+ * Allow withdrawal of denominations even though they are about to expire.
+ */
+ val denomselAllowLate: Boolean = false,
+ val devModeActive: Boolean = false,
+ val insecureTrustExchange: Boolean = false,
+ val preventThrottling: Boolean = false,
+ val skipDefaults: Boolean = false,
+ val emitObservabilityEvents: Boolean? = false,
+ )
+
+ /**
+ * Configurations values that may be safe to show to the user
+ */
+ @Serializable
+ data class Features(
+ val allowHttp: Boolean = false,
+ )
+}
+
+fun interface VersionReceiver {
+ fun onVersionReceived(versionInfo: WalletCoreVersion)
+}
+
+@Serializable
+data class WalletCoreVersion(
+ val implementationSemver: String,
+ val implementationGitHash: String,
+ val version: String,
+ val exchange: String,
+ val merchant: String,
+ val bankIntegrationApiRange: String,
+ val bankConversionApiRange: String,
+ val corebankApiRange: String,
+ val devMode: Boolean,
+)
diff --git a/wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt b/wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt
new file mode 100644
index 0000000..041656e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt
@@ -0,0 +1,44 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.backend
+
+import androidx.annotation.GuardedBy
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.coroutines.Continuation
+
+class RequestManager {
+
+ @GuardedBy("this")
+ private val contMap = ConcurrentHashMap<Int, Continuation<ApiResponse>>()
+
+ @Volatile
+ @GuardedBy("this")
+ private var currentId = 0
+
+ @Synchronized
+ fun addRequest(cont: Continuation<ApiResponse>, block: (Int) -> Unit) {
+ val id = currentId++
+ contMap[id] = cont
+ block(id)
+ }
+
+ @Synchronized
+ fun getAndRemoveContinuation(id: Int): Continuation<ApiResponse>? {
+ return contMap.remove(id)
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt b/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt
index edcfd17..2242e33 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt
@@ -3879,9 +3879,10 @@ enum class TalerErrorCode(val code: Int) {
@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = TalerErrorCode::class)
-object TalerErrorCodeSerializer: KSerializer<TalerErrorCode> {
+object TalerErrorCodeSerializer : KSerializer<TalerErrorCode> {
- override val descriptor = PrimitiveSerialDescriptor("TalerErrorCodeSerializer", PrimitiveKind.INT)
+ override val descriptor =
+ PrimitiveSerialDescriptor("TalerErrorCodeSerializer", PrimitiveKind.INT)
override fun deserialize(decoder: Decoder): TalerErrorCode {
val code = decoder.decodeInt()
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
index 9fc5cac..fba9885 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -17,130 +17,66 @@
package net.taler.wallet.backend
import android.app.Application
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
-import android.os.Handler
-import android.os.IBinder
-import android.os.Message
-import android.os.Messenger
-import android.util.Log
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.KSerializer
-import kotlinx.serialization.json.Json
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.decodeFromJsonElement
import net.taler.wallet.backend.TalerErrorCode.NONE
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_COMMAND
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_NOTIFY
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_REPLY
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_SUBSCRIBE_NOTIFY
import org.json.JSONObject
-import java.lang.ref.WeakReference
-import java.util.LinkedList
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
+import java.io.File
+private const val WALLET_DB = "talerwalletdb.sqlite3"
+
+@OptIn(DelicateCoroutinesApi::class)
class WalletBackendApi(
private val app: Application,
- private val notificationHandler: ((payload: JSONObject) -> Unit),
+ private val initialConfig: WalletRunConfig,
+ private val versionReceiver: VersionReceiver,
+ notificationReceiver: NotificationReceiver,
) {
- private var walletBackendMessenger: Messenger? = null
- private val queuedMessages = LinkedList<Message>()
- private val handlers = ConcurrentHashMap<Int, (isError: Boolean, message: JSONObject) -> Unit>()
- private var nextRequestID = AtomicInteger(0)
- private val incomingMessenger = Messenger(IncomingHandler(this))
- private val walletBackendConn = object : ServiceConnection {
- override fun onServiceDisconnected(p0: ComponentName?) {
- Log.w(TAG, "wallet backend service disconnected (crash?)")
- walletBackendMessenger = null
- }
+ private val backendManager = BackendManager(notificationReceiver)
- override fun onServiceConnected(componentName: ComponentName?, binder: IBinder?) {
- Log.i(TAG, "connected to wallet backend service")
- val bm = Messenger(binder)
- walletBackendMessenger = bm
- pumpQueue(bm)
- val msg = Message.obtain(null, MSG_SUBSCRIBE_NOTIFY)
- msg.replyTo = incomingMessenger
- bm.send(msg)
+ init {
+ GlobalScope.launch(Dispatchers.IO) {
+ backendManager.run()
+ sendInitMessage()
}
}
- init {
- Intent(app, WalletBackendService::class.java).also { intent ->
- app.bindService(intent, walletBackendConn, Context.BIND_AUTO_CREATE)
+ private suspend fun sendInitMessage() {
+ val db = if (File(app.filesDir, "talerwalletdb.sql").isFile) {
+ // can be removed after a reasonable migration period (2024-02-02)
+ "${app.filesDir}/talerwalletdb.sql"
+ } else {
+ "${app.filesDir}/${WALLET_DB}"
}
- }
- private class IncomingHandler(strongApi: WalletBackendApi) : Handler() {
- private val weakApi = WeakReference(strongApi)
- override fun handleMessage(msg: Message) {
- val api = weakApi.get() ?: return
- when (msg.what) {
- MSG_REPLY -> {
- val requestID = msg.data.getInt("requestID", 0)
- val operation = msg.data.getString("operation", "??")
- Log.i(TAG, "got reply for operation $operation ($requestID)")
- val h = api.handlers.remove(requestID)
- if (h == null) {
- Log.e(TAG, "request ID not associated with a handler")
- return
- }
- val response = msg.data.getString("response")
- if (response == null) {
- Log.e(TAG, "response did not contain response payload")
- return
- }
- val isError = msg.data.getBoolean("isError")
- val json = JSONObject(response)
- h(isError, json)
- }
- MSG_NOTIFY -> {
- val payloadStr = msg.data.getString("payload")
- if (payloadStr == null) {
- Log.e(TAG, "Notification had no payload: $msg")
- } else {
- val payload = JSONObject(payloadStr)
- api.notificationHandler.invoke(payload)
- }
- }
- }
+ request("init", InitResponse.serializer()) {
+ put("persistentStoragePath", db)
+ put("logLevel", "INFO")
+ put("config", JSONObject(BackendManager.json.encodeToString(initialConfig)))
+ }.onSuccess { response ->
+ versionReceiver.onVersionReceived(response.versionInfo)
+ }.onError { error ->
+ // TODO expose this to the UI as it can happen when using an older DB version
+ error("Error on init message: $error")
}
}
- private fun pumpQueue(bm: Messenger) {
- while (true) {
- val msg = queuedMessages.pollFirst() ?: return
- bm.send(msg)
+ suspend fun setWalletConfig(config: WalletRunConfig): WalletResponse<InitResponse> {
+ return request("initWallet", InitResponse.serializer()) {
+ put("config", JSONObject(BackendManager.json.encodeToString(config)))
}
}
- fun sendRequest(
- operation: String,
- args: JSONObject? = null,
- onResponse: (isError: Boolean, message: JSONObject) -> Unit = { _, _ -> },
- ) {
- val requestID = nextRequestID.incrementAndGet()
- Log.i(TAG, "sending request for operation $operation ($requestID)\n${args?.toString(2)}")
- val msg = Message.obtain(null, MSG_COMMAND)
- handlers[requestID] = onResponse
- msg.replyTo = incomingMessenger
- val data = msg.data
- data.putString("operation", operation)
- data.putInt("requestID", requestID)
- if (args != null) {
- data.putString("args", args.toString())
- }
- val bm = walletBackendMessenger
- if (bm != null) {
- bm.send(msg)
- } else {
- queuedMessages.add(msg)
- }
+ suspend fun sendRequest(operation: String, args: JSONObject? = null): ApiResponse {
+ return backendManager.send(operation, args)
}
suspend inline fun <reified T> request(
@@ -148,36 +84,47 @@ class WalletBackendApi(
serializer: KSerializer<T>? = null,
noinline args: (JSONObject.() -> JSONObject)? = null,
): WalletResponse<T> = withContext(Dispatchers.Default) {
- suspendCoroutine { cont ->
- val json = Json {
- ignoreUnknownKeys = true
- }
- sendRequest(operation, args?.invoke(JSONObject())) { isError, message ->
- val response = try {
- if (isError) {
- val error =
- json.decodeFromString(TalerErrorInfo.serializer(), message.toString())
- WalletResponse.Error(error)
- } else {
- val t: T = serializer?.let {
- json.decodeFromString(serializer, message.toString())
- } ?: Unit as T
- WalletResponse.Success(t)
- }
- } catch (e: Exception) {
- val info = TalerErrorInfo(NONE, "", e.toString(), null)
- WalletResponse.Error(info)
+ val json = BackendManager.json
+ try {
+ when (val response = sendRequest(operation, args?.invoke(JSONObject()))) {
+ is ApiResponse.Response -> {
+ val t: T = serializer?.let {
+ json.decodeFromJsonElement(serializer, response.result)
+ } ?: Unit as T
+ WalletResponse.Success(t)
+ }
+
+ is ApiResponse.Error -> {
+ val error: TalerErrorInfo = json.decodeFromJsonElement(response.error)
+ WalletResponse.Error(error)
}
- cont.resume(response)
}
+ } catch (e: Exception) {
+ val info = TalerErrorInfo(NONE, "", e.toString())
+ WalletResponse.Error(info)
}
}
- fun destroy() {
- // FIXME: implement this!
- }
+ // Returns raw JSON response instead of serialized object
+ suspend inline fun rawRequest(
+ operation: String,
+ noinline args: (JSONObject.() -> JSONObject)? = null,
+ ): WalletResponse<JsonObject> = withContext(Dispatchers.Default) {
+ val json = BackendManager.json
+ try {
+ when (val response = sendRequest(operation, args?.invoke(JSONObject()))) {
+ is ApiResponse.Response -> {
+ WalletResponse.Success(response.result)
+ }
- companion object {
- const val TAG = "WalletBackendApi"
+ is ApiResponse.Error -> {
+ val error: TalerErrorInfo = json.decodeFromJsonElement(response.error)
+ WalletResponse.Error(error)
+ }
+ }
+ } catch (e: Exception) {
+ val info = TalerErrorInfo(NONE, "", e.toString())
+ WalletResponse.Error(info)
+ }
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
deleted file mode 100644
index 06e0627..0000000
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-
-package net.taler.wallet.backend
-
-import android.app.Service
-import android.content.Intent
-import android.os.Handler
-import android.os.IBinder
-import android.os.Message
-import android.os.Messenger
-import android.os.RemoteException
-import android.util.Log
-import net.taler.akono.AkonoJni
-import net.taler.wallet.BuildConfig.WALLET_CORE_VERSION
-import net.taler.wallet.HostCardEmulatorService
-import org.json.JSONObject
-import java.lang.ref.WeakReference
-import java.util.LinkedList
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.system.exitProcess
-
-private const val TAG = "taler-wallet-backend"
-const val WALLET_DB = "talerwalletdb-v30.json"
-
-class RequestData(val clientRequestId: Int, val messenger: Messenger)
-
-
-class WalletBackendService : Service() {
- /**
- * Target we publish for clients to send messages to IncomingHandler.
- */
- private val messenger: Messenger = Messenger(IncomingHandler(this))
-
- private lateinit var akono: AkonoJni
-
- private var initialized = false
-
- private var nextRequestID = 1
-
- private val requests = ConcurrentHashMap<Int, RequestData>()
-
- private val subscribers = LinkedList<Messenger>()
-
- override fun onCreate() {
- val talerWalletAndroidCode =
- assets.open("taler-wallet-embedded-$WALLET_CORE_VERSION.js").use {
- it.readBytes().toString(Charsets.UTF_8)
- }
-
-
- Log.i(TAG, "onCreate in wallet backend service")
- akono = AkonoJni()
- akono.putModuleCode("@gnu-taler/taler-wallet-embedded", talerWalletAndroidCode)
- akono.setMessageHandler(object : AkonoJni.MessageHandler {
- override fun handleMessage(message: String) {
- this@WalletBackendService.handleAkonoMessage(message)
- }
- })
- //akono.evalNodeCode("require('source-map-support').install();")
- akono.evalNodeCode("require('akono');")
- akono.evalNodeCode("tw = require('@gnu-taler/taler-wallet-embedded');")
- akono.evalNodeCode("tw.installNativeWalletListener();")
- sendInitMessage()
- super.onCreate()
- }
-
- private fun sendInitMessage() {
- val msg = JSONObject()
- msg.put("operation", "init")
- val args = JSONObject()
- msg.put("args", args)
- args.put("persistentStoragePath", "${application.filesDir}/$WALLET_DB")
- akono.sendMessage(msg.toString())
- }
-
- /**
- * Handler of incoming messages from clients.
- */
- class IncomingHandler(
- service: WalletBackendService,
- ) : Handler() {
-
- private val serviceWeakRef = WeakReference(service)
-
- override fun handleMessage(msg: Message) {
- val svc = serviceWeakRef.get() ?: return
- if (!svc.initialized) Log.w(TAG, "Warning: Not yet initialized")
- when (msg.what) {
- MSG_COMMAND -> {
- val data = msg.data
- val serviceRequestID = svc.nextRequestID++
- val clientRequestID = data.getInt("requestID", 0)
- if (clientRequestID == 0) {
- Log.e(TAG, "client requestID missing")
- return
- }
- val args = data.getString("args")
- val argsObj = if (args == null) {
- JSONObject()
- } else {
- JSONObject(args)
- }
- val operation = data.getString("operation", "")
- if (operation == "") {
- Log.e(TAG, "client command missing")
- return
- }
- Log.i(TAG, "got request for operation $operation")
- val request = JSONObject()
- request.put("operation", operation)
- request.put("id", serviceRequestID)
- request.put("args", argsObj)
- svc.akono.sendMessage(request.toString(2))
- Log.i(
- TAG,
- "mapping service request ID $serviceRequestID to client request ID $clientRequestID"
- )
- svc.requests[serviceRequestID] = RequestData(clientRequestID, msg.replyTo)
- }
- MSG_SUBSCRIBE_NOTIFY -> {
- Log.i(TAG, "subscribing client")
- val r = msg.replyTo
- if (r == null) {
- Log.e(
- TAG,
- "subscriber did not specify replyTo object in MSG_SUBSCRIBE_NOTIFY"
- )
- } else {
- svc.subscribers.add(msg.replyTo)
- }
- }
- MSG_UNSUBSCRIBE_NOTIFY -> {
- Log.i(TAG, "unsubscribing client")
- svc.subscribers.remove(msg.replyTo)
- }
- else -> {
- Log.e(TAG, "unknown message from client")
- super.handleMessage(msg)
- }
- }
- }
- }
-
- override fun onBind(p0: Intent?): IBinder? {
- return messenger.binder
- }
-
- private fun sendNotify(payload: String) {
- var rm: LinkedList<Messenger>? = null
- for (s in subscribers) {
- val m = Message.obtain(null, MSG_NOTIFY)
- val b = m.data
- b.putString("payload", payload)
- try {
- s.send(m)
- } catch (e: RemoteException) {
- if (rm == null) {
- rm = LinkedList()
- }
- rm.add(s)
- subscribers.remove(s)
- }
- }
- if (rm != null) {
- for (s in rm) {
- subscribers.remove(s)
- }
- }
- }
-
- private fun handleAkonoMessage(messageStr: String) {
- val message = JSONObject(messageStr)
- when (val type = message.getString("type")) {
- "notification" -> {
- val payload = message.getJSONObject("payload")
- if (payload.optString("type") != "waiting-for-retry") {
- Log.v(TAG, "got back notification: ${message.toString(2)}")
- }
- sendNotify(payload.toString())
- }
- "tunnelHttp" -> {
- Log.v(TAG, "got http tunnel request! ${message.toString(2)}")
- Intent().also { intent ->
- intent.action = HostCardEmulatorService.HTTP_TUNNEL_REQUEST
- intent.putExtra("tunnelMessage", messageStr)
- application.sendBroadcast(intent)
- }
- }
- "response" -> {
- when (message.getString("operation")) {
- "init" -> {
- Log.d(TAG, "got response for init operation: ${message.toString(2)}")
- initialized = true
- sendNotify(message.toString(2))
- }
- "reset" -> {
- Log.v(TAG, "got back message: ${message.toString(2)}")
- exitProcess(1)
- }
- else -> {
- Log.v(TAG, "got back response: ${message.toString(2)}")
- val payload = message.getJSONObject("result").toString(2)
- handleResponse(false, message, payload)
- }
- }
- }
- "error" -> {
- Log.v(TAG, "got back error: ${message.toString(2)}")
- val payload = message.getJSONObject("error").toString(2)
- handleResponse(true, message, payload)
- }
- else -> throw IllegalArgumentException("Unknown message type: $type")
- }
- }
-
- private fun handleResponse(isError: Boolean, message: JSONObject, payload: String) {
- val id = message.getInt("id")
- val rId = requests[id]
- if (rId == null) {
- Log.e(TAG, "wallet returned unknown request ID ($id)")
- return
- }
- val m = Message.obtain(null, MSG_REPLY)
- val b = m.data
- b.putInt("requestID", rId.clientRequestId)
- b.putBoolean("isError", isError)
- b.putString("response", payload)
- b.putString("operation", message.getString("operation"))
- rId.messenger.send(m)
- }
-
- companion object {
- const val MSG_SUBSCRIBE_NOTIFY = 1
- const val MSG_UNSUBSCRIBE_NOTIFY = 2
- const val MSG_COMMAND = 3
- const val MSG_REPLY = 4
- const val MSG_NOTIFY = 5
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
index 414d784..3946457 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
@@ -19,13 +19,18 @@ package net.taler.wallet.backend
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-import kotlinx.serialization.descriptors.PrimitiveKind.STRING
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
-import org.json.JSONObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
@Serializable
sealed class WalletResponse<T> {
@@ -52,7 +57,7 @@ sealed class WalletResponse<T> {
}
}
-@Serializable
+@Serializable(with = TalerErrorInfoSerializer::class)
data class TalerErrorInfo(
// Numeric error code defined defined in the
// GANA gnu-taler-error-codes registry.
@@ -65,42 +70,65 @@ data class TalerErrorInfo(
// for the instance of the error.
val message: String? = null,
- // Error details
- @Serializable(JSONObjectDeserializer::class)
- val details: JSONObject? = null,
-
- // KYC URL (in case KYC is required)
- val kycUrl: String? = null,
+ // Error extra details
+ val extra: Map<String, JsonElement> = mapOf(),
) {
val userFacingMsg: String
get() {
return StringBuilder().apply {
- hint?.let { append(it) }
- message?.let { append(" ").append(it) }
- details?.let { details ->
- if (details.length() > 0) {
- append("\n\n")
- details.optJSONObject("errorResponse")?.let { errorResponse ->
- append(errorResponse.optString("code")).append(" ")
- append(errorResponse.optString("hint"))
- } ?: append(details.toString(2))
+ // If there's a hint in errorResponse, use it.
+ if (extra.containsKey("errorResponse")) {
+ val errorResponse = extra["errorResponse"]!!.jsonObject
+ if (errorResponse.containsKey("hint")) {
+ val hint = errorResponse["hint"]!!.jsonPrimitive.content
+ append(hint)
}
+ } else {
+ // Otherwise, use the standard ones
+ hint?.let { append(it) }
+ message?.let { append(" ").append(it) }
}
}.toString()
}
+
+ fun getStringExtra(key: String): String? =
+ extra[key]?.jsonPrimitive?.content
}
-class JSONObjectDeserializer : KSerializer<JSONObject> {
+class TalerErrorInfoSerializer : KSerializer<TalerErrorInfo> {
+ private val stringToJsonElementSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
+
+ override val descriptor: SerialDescriptor
+ get() = stringToJsonElementSerializer.descriptor
+
+ override fun deserialize(decoder: Decoder): TalerErrorInfo {
+ // Decoder -> JsonInput
+ require(decoder is JsonDecoder)
+ val json = decoder.json
+ val filtersMap = decoder.decodeSerializableValue(stringToJsonElementSerializer)
+
+ val code = filtersMap["code"]?.let {
+ json.decodeFromJsonElement(TalerErrorCode.serializer(), it)
+ } ?: TalerErrorCode.UNKNOWN
+ val hint = filtersMap["hint"]?.let {
+ json.decodeFromJsonElement(String.serializer(), it)
+ }
+ val message = filtersMap["message"]?.let {
+ json.decodeFromJsonElement(String.serializer(), it)
+ }
- override val descriptor = PrimitiveSerialDescriptor("JSONObjectDeserializer", STRING)
+ val knownKeys = setOf("code", "hint", "message")
+ val unknownFilters = filtersMap.filter { (key, _) -> !knownKeys.contains(key) }
- override fun deserialize(decoder: Decoder): JSONObject {
- val input = decoder as JsonDecoder
- val tree = input.decodeJsonElement() as JsonObject
- return JSONObject(tree.toString())
+ return TalerErrorInfo(code, hint, message, unknownFilters)
}
- override fun serialize(encoder: Encoder, value: JSONObject) {
- error("not supported")
+ override fun serialize(encoder: Encoder, value: TalerErrorInfo) {
+ encoder.encodeSerializableValue(JsonObject.serializer(), buildJsonObject {
+ put("code", JsonPrimitive(value.code.code))
+ put("hint", JsonPrimitive(value.hint))
+ put("message", JsonPrimitive(value.message))
+ value.extra.forEach { (key, value) -> put(key, value) }
+ })
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
index 24ee1a1..f40def4 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
@@ -24,20 +24,12 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
-import kotlinx.serialization.Serializable
-import net.taler.common.Amount
import net.taler.wallet.R
import net.taler.wallet.balances.BalanceAdapter.BalanceViewHolder
-
-@Serializable
-data class BalanceItem(
- val available: Amount,
- val pendingIncoming: Amount,
- val pendingOutgoing: Amount
-) {
- val currency: String get() = available.currency
- val hasPending: Boolean get() = !pendingIncoming.isZero() || !pendingOutgoing.isZero()
-}
+import net.taler.wallet.balances.ScopeInfo.Auditor
+import net.taler.wallet.balances.ScopeInfo.Exchange
+import net.taler.wallet.balances.ScopeInfo.Global
+import net.taler.wallet.cleanExchange
class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<BalanceViewHolder>() {
@@ -66,16 +58,15 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan
}
inner class BalanceViewHolder(private val v: View) : RecyclerView.ViewHolder(v) {
- private val currencyView: TextView = v.findViewById(R.id.balanceCurrencyView)
private val amountView: TextView = v.findViewById(R.id.balanceAmountView)
+ private val scopeView: TextView = v.findViewById(R.id.scopeView)
private val balanceInboundAmount: TextView = v.findViewById(R.id.balanceInboundAmount)
private val balanceInboundLabel: TextView = v.findViewById(R.id.balanceInboundLabel)
private val pendingView: TextView = v.findViewById(R.id.pendingView)
fun bind(item: BalanceItem) {
- v.setOnClickListener { listener.onBalanceClick(item.available.currency) }
- currencyView.text = item.currency
- amountView.text = item.available.amountStr
+ v.setOnClickListener { listener.onBalanceClick(item.scopeInfo) }
+ amountView.text = item.available.toString()
val amountIncoming = item.pendingIncoming
if (amountIncoming.isZero()) {
@@ -84,9 +75,22 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan
} else {
balanceInboundAmount.visibility = VISIBLE
balanceInboundLabel.visibility = VISIBLE
- balanceInboundAmount.text =
- v.context.getString(R.string.amount_positive, amountIncoming)
+ balanceInboundAmount.text = v.context.getString(R.string.amount_positive, amountIncoming.toString(showSymbol = false))
}
+
+ val scopeInfo = item.scopeInfo
+ scopeView.visibility = when (scopeInfo) {
+ is Global -> GONE
+ is Exchange -> {
+ scopeView.text = v.context.getString(R.string.balance_scope_exchange, cleanExchange(scopeInfo.url))
+ VISIBLE
+ }
+ is Auditor -> {
+ scopeView.text = v.context.getString(R.string.balance_scope_auditor, cleanExchange(scopeInfo.url))
+ VISIBLE
+ }
+ }
+
pendingView.visibility = if (item.hasPending) VISIBLE else GONE
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt
new file mode 100644
index 0000000..42e67cf
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt
@@ -0,0 +1,137 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.balances
+
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.distinctUntilChanged
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.backend.WalletBackendApi
+import org.json.JSONObject
+
+@Serializable
+data class BalanceResponse(
+ val balances: List<BalanceItem>
+)
+
+@Serializable
+data class GetCurrencySpecificationResponse(
+ val currencySpecification: CurrencySpecification,
+)
+
+sealed class BalanceState {
+ data object None: BalanceState()
+ data object Loading: BalanceState()
+
+ data class Success(
+ val balances: List<BalanceItem>,
+ ): BalanceState()
+
+ data class Error(
+ val error: TalerErrorInfo,
+ ): BalanceState()
+}
+
+class BalanceManager(
+ private val api: WalletBackendApi,
+ private val scope: CoroutineScope,
+) {
+ private val mBalances = MutableLiveData<List<BalanceItem>>(emptyList())
+ val balances: LiveData<List<BalanceItem>> = mBalances
+
+ private val mState = MutableLiveData<BalanceState>(BalanceState.None)
+ val state: LiveData<BalanceState> = mState.distinctUntilChanged()
+
+ private val currencySpecs: MutableMap<ScopeInfo, CurrencySpecification?> = mutableMapOf()
+
+ @UiThread
+ fun loadBalances() {
+ mState.value = BalanceState.Loading
+ scope.launch {
+ val response = api.request("getBalances", BalanceResponse.serializer())
+ response.onError {
+ Log.e(TAG, "Error retrieving balances: $it")
+ mState.postValue(BalanceState.Error(it))
+ }
+ response.onSuccess {
+ mBalances.postValue(it.balances)
+ scope.launch {
+ // Fetch missing currency specs for all balances
+ it.balances.forEach { balance ->
+ if (!currencySpecs.containsKey(balance.scopeInfo)) {
+ currencySpecs[balance.scopeInfo] = getCurrencySpecification(balance.scopeInfo)
+ }
+ }
+
+ mState.postValue(
+ BalanceState.Success(it.balances.map { balance ->
+ val spec = currencySpecs[balance.scopeInfo]
+ balance.copy(
+ available = balance.available.withSpec(spec),
+ pendingIncoming = balance.pendingIncoming.withSpec(spec),
+ pendingOutgoing = balance.pendingOutgoing.withSpec(spec),
+ )
+ }),
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun getCurrencySpecification(scopeInfo: ScopeInfo): CurrencySpecification? {
+ var spec: CurrencySpecification? = null
+ api.request("getCurrencySpecification", GetCurrencySpecificationResponse.serializer()) {
+ val json = Json.encodeToString(scopeInfo)
+ Log.d(TAG, "BalanceManager: $json")
+ put("scope", JSONObject(json))
+ }.onSuccess {
+ spec = it.currencySpecification
+ }.onError {
+ Log.e(TAG, "Error getting currency spec for scope $scopeInfo: $it")
+ }
+
+ return spec
+ }
+
+ @Deprecated("Please find spec via scopeInfo instead", ReplaceWith("getSpecForScopeInfo"))
+ fun getSpecForCurrency(currency: String): CurrencySpecification? {
+ val state = mState.value
+ if (state !is BalanceState.Success) return null
+
+ return state.balances.find { it.currency == currency }?.available?.spec
+ }
+
+ fun getSpecForScopeInfo(scopeInfo: ScopeInfo): CurrencySpecification? {
+ val state = mState.value
+ if (state !is BalanceState.Success) return null
+
+ return state.balances.find { it.scopeInfo == scopeInfo }?.available?.spec
+ }
+
+ fun resetBalances() {
+ mState.value = BalanceState.None
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/balances/Balances.kt b/wallet/src/main/java/net/taler/wallet/balances/Balances.kt
new file mode 100644
index 0000000..dff2ffb
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/balances/Balances.kt
@@ -0,0 +1,57 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.balances
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+
+@Serializable
+data class BalanceItem(
+ val scopeInfo: ScopeInfo,
+ val available: Amount,
+ val pendingIncoming: Amount,
+ val pendingOutgoing: Amount,
+) {
+ val currency: String get() = available.currency
+ val hasPending: Boolean get() = !pendingIncoming.isZero() || !pendingOutgoing.isZero()
+}
+
+@Serializable
+sealed class ScopeInfo {
+ abstract val currency: String
+
+ @Serializable
+ @SerialName("global")
+ data class Global(
+ override val currency: String
+ ): ScopeInfo()
+
+ @Serializable
+ @SerialName("exchange")
+ data class Exchange(
+ override val currency: String,
+ val url: String,
+ ): ScopeInfo()
+
+ @Serializable
+ @SerialName("auditor")
+ data class Auditor(
+ override val currency: String,
+ val url: String,
+ ): ScopeInfo()
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt
index c1be674..93636ea 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt
@@ -29,11 +29,17 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
import net.taler.common.fadeIn
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
+import net.taler.wallet.balances.BalanceState.Error
+import net.taler.wallet.balances.BalanceState.Loading
+import net.taler.wallet.balances.BalanceState.None
+import net.taler.wallet.balances.BalanceState.Success
import net.taler.wallet.databinding.FragmentBalancesBinding
+import net.taler.wallet.showError
interface BalanceClickListener {
- fun onBalanceClick(currency: String)
+ fun onBalanceClick(scopeInfo: ScopeInfo)
}
class BalancesFragment : Fragment(),
@@ -59,25 +65,39 @@ class BalancesFragment : Fragment(),
addItemDecoration(DividerItemDecoration(context, VERTICAL))
}
- model.balances.observe(viewLifecycleOwner) {
+ model.balanceManager.state.observe(viewLifecycleOwner) {
onBalancesChanged(it)
}
}
- private fun onBalancesChanged(balances: List<BalanceItem>) {
- beginDelayedTransition(view as ViewGroup)
- if (balances.isEmpty()) {
- ui.mainEmptyState.visibility = VISIBLE
- ui.mainList.visibility = GONE
- } else {
- balancesAdapter.setItems(balances)
- ui.mainEmptyState.visibility = INVISIBLE
- ui.mainList.fadeIn()
+ private fun onBalancesChanged(state: BalanceState) {
+ model.showProgressBar.value = false
+ when (state) {
+ is None -> {}
+ is Loading -> {
+ model.showProgressBar.value = true
+ }
+ is Success -> {
+ beginDelayedTransition(view as ViewGroup)
+ if (state.balances.isEmpty()) {
+ ui.mainEmptyState.visibility = VISIBLE
+ ui.mainList.visibility = GONE
+ } else {
+ balancesAdapter.setItems(state.balances)
+ ui.mainEmptyState.visibility = INVISIBLE
+ ui.mainList.fadeIn()
+ }
+ }
+ is Error -> if (model.devMode.value == true) {
+ showError(state.error)
+ } else {
+ showError(state.error.userFacingMsg)
+ }
}
}
- override fun onBalanceClick(currency: String) {
- model.showTransactions(currency)
+ override fun onBalanceClick(scopeInfo: ScopeInfo) {
+ model.showTransactions(scopeInfo)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt
new file mode 100644
index 0000000..a524d1b
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt
@@ -0,0 +1,226 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.compose
+
+import android.os.Build
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
+import net.taler.common.Amount
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
+import kotlin.math.max
+import kotlin.math.pow
+import kotlin.math.roundToLong
+
+const val DEFAULT_INPUT_DECIMALS = 2
+
+@Composable
+fun AmountInputField(
+ value: String,
+ onValueChange: (value: String) -> Unit,
+ modifier: Modifier = Modifier,
+ label: @Composable (() -> Unit)? = null,
+ supportingText: @Composable (() -> Unit)? = null,
+ isError: Boolean = false,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ decimalFormatSymbols: DecimalFormatSymbols = DecimalFormat().decimalFormatSymbols,
+ numberOfDecimals: Int = DEFAULT_INPUT_DECIMALS,
+) {
+ var amountInput by remember { mutableStateOf(value) }
+
+ // React to external changes
+ val amountValue = remember(amountInput, value) {
+ transformOutput(amountInput).let {
+ if (value != it) transformInput(value, numberOfDecimals) else amountInput
+ }
+ }
+
+ OutlinedTextField(
+ value = amountValue,
+ onValueChange = { input ->
+ if (input.matches("0+".toRegex())) {
+ amountInput = "0"
+ onValueChange("")
+ } else transformOutput(input, numberOfDecimals)?.let { filtered ->
+ if (Amount.isValidAmountStr(filtered) && !input.contains("-")) {
+ amountInput = input.trimStart('0')
+ onValueChange(filtered)
+ }
+ }
+ },
+ modifier = modifier,
+ textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace),
+ label = label,
+ supportingText = supportingText,
+ isError = isError,
+ visualTransformation = AmountInputVisualTransformation(
+ symbols = decimalFormatSymbols,
+ fixedCursorAtTheEnd = true,
+ numberOfDecimals = numberOfDecimals,
+ ),
+ keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.NumberPassword),
+ keyboardActions = keyboardActions,
+ singleLine = true,
+ maxLines = 1,
+ )
+}
+
+// 500 -> 5.0
+private fun transformOutput(
+ input: String,
+ numberOfDecimals: Int = 2,
+) = if (input.isEmpty()) "0" else {
+ input.toLongOrNull()?.let { it / 10.0.pow(numberOfDecimals) }?.toBigDecimal()?.toPlainString()
+}
+
+// 5.0 -> 500
+private fun transformInput(
+ output: String,
+ numberOfDecimals: Int = 2,
+) = if (output.isEmpty()) "0" else {
+ (output.toDouble() * 10.0.pow(numberOfDecimals)).roundToLong().toString()
+}
+
+// Source: https://github.com/banmarkovic/CurrencyAmountInput
+
+private class AmountInputVisualTransformation(
+ private val symbols: DecimalFormatSymbols,
+ private val fixedCursorAtTheEnd: Boolean = true,
+ private val numberOfDecimals: Int = 2,
+): VisualTransformation {
+
+ override fun filter(text: AnnotatedString): TransformedText {
+ val thousandsSeparator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ symbols.monetaryGroupingSeparator
+ } else {
+ symbols.groupingSeparator
+ }
+ val decimalSeparator = symbols.monetaryDecimalSeparator
+ val zero = symbols.zeroDigit
+
+ val inputText = text.text
+
+ val intPart = inputText
+ .dropLast(numberOfDecimals)
+ .reversed()
+ .chunked(3)
+ .joinToString(thousandsSeparator.toString())
+ .reversed()
+ .ifEmpty {
+ zero.toString()
+ }
+
+ val fractionPart = inputText.takeLast(numberOfDecimals).let {
+ if (it.length != numberOfDecimals) {
+ List(numberOfDecimals - it.length) {
+ zero
+ }.joinToString("") + it
+ } else {
+ it
+ }
+ }
+
+ // Hide trailing decimal separator if decimals are 0
+ val formattedNumber = if (numberOfDecimals > 0) {
+ intPart + decimalSeparator + fractionPart
+ } else {
+ intPart
+ }
+
+ val newText = AnnotatedString(
+ text = formattedNumber,
+ spanStyles = text.spanStyles,
+ paragraphStyles = text.paragraphStyles
+ )
+
+ val offsetMapping = if (fixedCursorAtTheEnd) {
+ FixedCursorOffsetMapping(
+ contentLength = inputText.length,
+ formattedContentLength = formattedNumber.length
+ )
+ } else {
+ MovableCursorOffsetMapping(
+ unmaskedText = text.toString(),
+ maskedText = newText.toString(),
+ decimalDigits = numberOfDecimals
+ )
+ }
+
+ return TransformedText(newText, offsetMapping)
+ }
+
+ private class FixedCursorOffsetMapping(
+ private val contentLength: Int,
+ private val formattedContentLength: Int,
+ ) : OffsetMapping {
+ override fun originalToTransformed(offset: Int): Int = formattedContentLength
+ override fun transformedToOriginal(offset: Int): Int = contentLength
+ }
+
+ private class MovableCursorOffsetMapping(
+ private val unmaskedText: String,
+ private val maskedText: String,
+ private val decimalDigits: Int
+ ) : OffsetMapping {
+ override fun originalToTransformed(offset: Int): Int =
+ when {
+ unmaskedText.length <= decimalDigits -> {
+ maskedText.length - (unmaskedText.length - offset)
+ }
+ else -> {
+ offset + offsetMaskCount(offset, maskedText)
+ }
+ }
+
+ override fun transformedToOriginal(offset: Int): Int =
+ when {
+ unmaskedText.length <= decimalDigits -> {
+ max(unmaskedText.length - (maskedText.length - offset), 0)
+ }
+ else -> {
+ offset - maskedText.take(offset).count { !it.isDigit() }
+ }
+ }
+
+ private fun offsetMaskCount(offset: Int, maskedText: String): Int {
+ var maskOffsetCount = 0
+ var dataCount = 0
+ for (maskChar in maskedText) {
+ if (!maskChar.isDigit()) {
+ maskOffsetCount++
+ } else if (++dataCount > offset) {
+ break
+ }
+ }
+ return maskOffsetCount
+ }
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/settings/BackupSettingsFragment.kt b/wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt
index e9641a3..6412d63 100644
--- a/wallet/src/main/java/net/taler/wallet/settings/BackupSettingsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt
@@ -1,6 +1,6 @@
/*
* This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
+ * (C) 2024 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
@@ -14,16 +14,21 @@
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-package net.taler.wallet.settings
+package net.taler.wallet.compose
-import android.os.Bundle
-import androidx.preference.PreferenceFragmentCompat
-import net.taler.wallet.R
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
-class BackupSettingsFragment : PreferenceFragmentCompat() {
-
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.settings_backup, rootKey)
+@Composable
+fun LoadingScreen() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator()
}
-
-}
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt b/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt
new file mode 100644
index 0000000..47401cf
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt
@@ -0,0 +1,70 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.compose
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Remove
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun NumericInputField(
+ modifier: Modifier = Modifier,
+ value: Long,
+ onValueChange: (Long) -> Unit,
+ readOnly: Boolean = true,
+ label: @Composable () -> Unit,
+ minValue: Long? = 0L,
+ maxValue: Long? = null,
+) {
+ OutlinedTextField(
+ modifier = modifier,
+ value = value.toString(),
+ singleLine = true,
+ readOnly = readOnly,
+ onValueChange = {
+ val dd = it.toLongOrNull() ?: 0
+ onValueChange(dd)
+ },
+ trailingIcon = {
+ Row {
+ IconButton(
+ content = { Icon(Icons.Default.Remove, "add1") },
+ onClick = {
+ if (minValue != null && value - 1 >= minValue) {
+ onValueChange(value - 1)
+ }
+ },
+ )
+ IconButton(
+ content = { Icon(Icons.Default.Add, "add1") },
+ onClick = {
+ if (maxValue != null && value + 1 <= maxValue) {
+ onValueChange(value + 1)
+ }
+ },
+ )
+ }
+ },
+ label = label,
+ )
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
index 5359f1a..4991094 100644
--- a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
@@ -25,24 +25,23 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonColors
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Icon
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.produceState
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
@@ -86,7 +85,7 @@ fun ColumnScope.QrCodeUriComposable(
Text(
modifier = Modifier.horizontalScroll(scrollState),
fontFamily = FontFamily.Monospace,
- style = MaterialTheme.typography.body1,
+ style = MaterialTheme.typography.bodyLarge,
text = talerUri,
)
}
@@ -100,11 +99,17 @@ fun ColumnScope.QrCodeUriComposable(
label = clipBoardLabel,
content = talerUri,
buttonText = buttonText,
- colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ )
)
ShareButton(
content = talerUri,
- colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ )
)
}
}
@@ -131,14 +136,13 @@ fun CopyToClipboardButton(
colors = colors,
onClick = { copyToClipBoard(context, label, content) },
) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(Icons.Default.ContentCopy, buttonText)
- Text(
- modifier = Modifier.padding(start = 8.dp),
- text = buttonText,
- style = MaterialTheme.typography.body1,
- )
- }
+ Icon(
+ Icons.Default.ContentCopy,
+ buttonText,
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(buttonText)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt b/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt
new file mode 100644
index 0000000..c47f55d
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt
@@ -0,0 +1,46 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.compose
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SuggestionChip
+import androidx.compose.material3.SuggestionChipDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+
+@Composable
+fun <T> SelectionChip(
+ label: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ selected: Boolean,
+ value: T,
+ onSelected: (T) -> Unit,
+) {
+ val theme = MaterialTheme.colorScheme
+ SuggestionChip(
+ label = label,
+ modifier = modifier,
+ onClick = {
+ onSelected(value)
+ },
+ colors = SuggestionChipDefaults.suggestionChipColors(
+ containerColor = if (selected) theme.primaryContainer else Color.Transparent,
+ labelColor = if (selected) theme.onPrimaryContainer else theme.onSurface
+ )
+ )
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt b/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt
index 0ac7048..f3a84dd 100644
--- a/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt
+++ b/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt
@@ -19,22 +19,19 @@ package net.taler.wallet.compose
import android.content.Intent
import android.content.Intent.ACTION_SEND
import android.content.Intent.EXTRA_TEXT
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonColors
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Icon
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import net.taler.wallet.R
@@ -59,13 +56,12 @@ fun ShareButton(
startActivity(context, shareIntent, null)
},
) {
- Row(verticalAlignment = CenterVertically) {
- Icon(Icons.Default.Share, buttonText)
- Text(
- modifier = Modifier.padding(start = 8.dp),
- text = buttonText,
- style = MaterialTheme.typography.body1,
- )
- }
+ Icon(
+ Icons.Default.Share,
+ buttonText,
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(buttonText)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/Utils.kt b/wallet/src/main/java/net/taler/wallet/compose/Utils.kt
index 8e3a032..9a27431 100644
--- a/wallet/src/main/java/net/taler/wallet/compose/Utils.kt
+++ b/wallet/src/main/java/net/taler/wallet/compose/Utils.kt
@@ -16,7 +16,7 @@
package net.taler.wallet.compose
-import androidx.compose.material.Surface
+import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
@@ -25,7 +25,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.flowWithLifecycle
-import com.google.accompanist.themeadapter.material.MdcTheme
+import com.google.accompanist.themeadapter.material3.Mdc3Theme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlin.coroutines.CoroutineContext
@@ -56,7 +56,7 @@ fun <T> StateFlow<T>.collectAsStateLifecycleAware(
@Composable
fun TalerSurface(content: @Composable () -> Unit) {
- MdcTheme {
+ Mdc3Theme {
Surface {
content()
}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt
index c4b302f..20acee1 100644
--- a/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt
@@ -32,10 +32,13 @@ import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
class DepositFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
private val depositManager get() = model.depositManager
+ private val balanceManager get() = model.balanceManager
+ private val transactionManager get() = model.transactionManager
override fun onCreateView(
inflater: LayoutInflater,
@@ -45,12 +48,12 @@ class DepositFragment : Fragment() {
val amount = arguments?.getString("amount")?.let {
Amount.fromJSONString(it)
} ?: error("no amount passed")
+ val scopeInfo = transactionManager.selectedScope
+ val spec = scopeInfo?.let { balanceManager.getSpecForScopeInfo(it) }
val receiverName = arguments?.getString("receiverName")
val iban = arguments?.getString("IBAN")
- val bic = arguments?.getString("BIC") ?: ""
-
if (receiverName != null && iban != null) {
- onDepositButtonClicked(amount, receiverName, iban, bic)
+ onDepositButtonClicked(amount, receiverName, iban)
}
return ComposeView(requireContext()).apply {
setContent {
@@ -58,14 +61,14 @@ class DepositFragment : Fragment() {
val state = depositManager.depositState.collectAsStateLifecycleAware()
if (amount.currency == CURRENCY_BTC) MakeBitcoinDepositComposable(
state = state.value,
- amount = amount,
+ amount = amount.withSpec(spec),
bitcoinAddress = null,
onMakeDeposit = { amount, bitcoinAddress ->
depositManager.onDepositButtonClicked(amount, bitcoinAddress)
},
) else MakeDepositComposable(
state = state.value,
- amount = amount,
+ amount = amount.withSpec(spec),
presetName = receiverName,
presetIban = iban,
onMakeDeposit = this@DepositFragment::onDepositButtonClicked,
@@ -80,7 +83,11 @@ class DepositFragment : Fragment() {
lifecycleScope.launchWhenStarted {
depositManager.depositState.collect { state ->
if (state is DepositState.Error) {
- showError(state.msg)
+ if (model.devMode.value == false) {
+ showError(state.error.userFacingMsg)
+ } else {
+ showError(state.error)
+ }
} else if (state is DepositState.Success) {
findNavController().navigate(R.id.action_nav_deposit_to_nav_main)
}
@@ -104,8 +111,7 @@ class DepositFragment : Fragment() {
amount: Amount,
receiverName: String,
iban: String,
- bic: String,
) {
- depositManager.onDepositButtonClicked(amount, receiverName, iban, bic)
+ depositManager.onDepositButtonClicked(amount, receiverName, iban)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt
index 0f10b5f..0075f95 100644
--- a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt
@@ -46,10 +46,36 @@ class DepositManager(
}
@UiThread
- fun onDepositButtonClicked(amount: Amount, receiverName: String, iban: String, bic: String) {
+ fun onDepositButtonClicked(amount: Amount, receiverName: String, iban: String) {
+ if (depositState.value is DepositState.FeesChecked) {
+ // fees already checked, so IBAN was validated, can make deposit directly
+ makeIbanDeposit(amount, receiverName, iban)
+ } else {
+ // validate IBAN first
+ mDepositState.value = DepositState.CheckingFees
+ scope.launch {
+ api.request("validateIban", ValidateIbanResponse.serializer()) {
+ put("iban", iban)
+ }.onError {
+ Log.e(TAG, "Error validateIban $it")
+ mDepositState.value = DepositState.Error(it)
+ }.onSuccess { response ->
+ if (response.valid) {
+ // only prepare/make deposit, if IBAN is valid
+ makeIbanDeposit(amount, receiverName, iban)
+ } else {
+ mDepositState.value = DepositState.IbanInvalid
+ }
+ }
+ }
+ }
+ }
+
+ @UiThread
+ private fun makeIbanDeposit(amount: Amount, receiverName: String, iban: String) {
val paytoUri: String = PaytoUriIban(
iban = iban,
- bic = bic,
+ bic = null,
targetPath = "",
params = mapOf("receiver-name" to receiverName),
).paytoUri
@@ -66,7 +92,7 @@ class DepositManager(
}
private fun makeDeposit(amount: Amount, uri: String) {
- if (depositState.value.showFees) makeDeposit(
+ if (depositState.value is DepositState.FeesChecked) makeDeposit(
paytoUri = uri,
amount = amount,
totalDepositCost = depositState.value.totalDepositCost
@@ -86,7 +112,7 @@ class DepositManager(
put("amount", amount.toJSONString())
}.onError {
Log.e(TAG, "Error prepareDeposit $it")
- mDepositState.value = DepositState.Error(it.userFacingMsg)
+ mDepositState.value = DepositState.Error(it)
}.onSuccess {
mDepositState.value = DepositState.FeesChecked(
totalDepositCost = it.totalDepositCost,
@@ -112,7 +138,7 @@ class DepositManager(
put("amount", amount.toJSONString())
}.onError {
Log.e(TAG, "Error createDepositGroup $it")
- mDepositState.value = DepositState.Error(it.userFacingMsg)
+ mDepositState.value = DepositState.Error(it)
}.onSuccess {
mDepositState.value = DepositState.Success
}
@@ -126,6 +152,11 @@ class DepositManager(
}
@Serializable
+data class ValidateIbanResponse(
+ val valid: Boolean,
+)
+
+@Serializable
data class PrepareDepositResponse(
val totalDepositCost: Amount,
val effectiveDepositAmount: Amount,
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt
index 918d74c..168378f 100644
--- a/wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt
+++ b/wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt
@@ -17,6 +17,7 @@
package net.taler.wallet.deposit
import net.taler.common.Amount
+import net.taler.wallet.backend.TalerErrorInfo
sealed class DepositState {
@@ -26,6 +27,7 @@ sealed class DepositState {
object Start : DepositState()
object CheckingFees : DepositState()
+ object IbanInvalid : DepositState()
class FeesChecked(
override val totalDepositCost: Amount,
override val effectiveDepositAmount: Amount,
@@ -42,6 +44,6 @@ sealed class DepositState {
object Success : DepositState()
- class Error(val msg: String) : DepositState()
+ class Error(val error: TalerErrorInfo) : DepositState()
}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/MakeBitcoinDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/MakeBitcoinDepositComposable.kt
index 1ff3423..3fa0d98 100644
--- a/wallet/src/main/java/net/taler/wallet/deposit/MakeBitcoinDepositComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/deposit/MakeBitcoinDepositComposable.kt
@@ -22,10 +22,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -39,7 +40,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -71,6 +71,7 @@ fun MakeBitcoinDepositComposable(
.padding(16.dp)
.focusRequester(focusRequester),
value = address,
+ singleLine = true,
enabled = !state.showFees,
onValueChange = { input ->
address = input
@@ -80,7 +81,7 @@ fun MakeBitcoinDepositComposable(
Text(
stringResource(R.string.send_deposit_bitcoin_address),
color = if (address.isBlank()) {
- colorResource(R.color.red)
+ MaterialTheme.colorScheme.error
} else Color.Unspecified,
)
}
@@ -120,8 +121,8 @@ fun MakeBitcoinDepositComposable(
Text(
modifier = Modifier.padding(16.dp),
fontSize = 18.sp,
- color = colorResource(R.color.red),
- text = (state as? DepositState.Error)?.msg ?: "",
+ color = MaterialTheme.colorScheme.error,
+ text = (state as? DepositState.Error)?.error?.userFacingMsg ?: "",
)
}
val focusManager = LocalFocusManager.current
@@ -147,12 +148,12 @@ fun MakeBitcoinDepositComposable(
fun PreviewMakeBitcoinDepositComposable() {
Surface {
val state = DepositState.FeesChecked(
- effectiveDepositAmount = Amount.fromDouble(CURRENCY_BTC, 42.00),
- totalDepositCost = Amount.fromDouble(CURRENCY_BTC, 42.23),
+ effectiveDepositAmount = Amount.fromString(CURRENCY_BTC, "42.00"),
+ totalDepositCost = Amount.fromString(CURRENCY_BTC, "42.23"),
)
MakeBitcoinDepositComposable(
state = state,
- amount = Amount.fromDouble(CURRENCY_BTC, 42.23)) { _, _ ->
+ amount = Amount.fromString(CURRENCY_BTC, "42.23")) { _, _ ->
}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt
index 8b5cac1..9333ce1 100644
--- a/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt
@@ -22,10 +22,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -39,13 +40,15 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.taler.common.Amount
import net.taler.wallet.R
+import net.taler.wallet.transactions.AmountType.Negative
+import net.taler.wallet.transactions.AmountType.Positive
+import net.taler.wallet.transactions.TransactionAmountComposable
@Composable
fun MakeDepositComposable(
@@ -53,7 +56,7 @@ fun MakeDepositComposable(
amount: Amount,
presetName: String? = null,
presetIban: String? = null,
- onMakeDeposit: (Amount, String, String, String) -> Unit,
+ onMakeDeposit: (Amount, String, String) -> Unit,
) {
val scrollState = rememberScrollState()
Column(
@@ -64,23 +67,24 @@ fun MakeDepositComposable(
) {
var name by rememberSaveable { mutableStateOf(presetName ?: "") }
var iban by rememberSaveable { mutableStateOf(presetIban ?: "") }
- var bic by rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
OutlinedTextField(
modifier = Modifier
.padding(16.dp)
- .focusRequester(focusRequester),
+ .focusRequester(focusRequester)
+ .fillMaxWidth(),
value = name,
enabled = !state.showFees,
onValueChange = { input ->
name = input
},
+ singleLine = true,
isError = name.isBlank(),
label = {
Text(
stringResource(R.string.send_deposit_name),
color = if (name.isBlank()) {
- colorResource(R.color.red)
+ MaterialTheme.colorScheme.error
} else Color.Unspecified,
)
}
@@ -88,53 +92,40 @@ fun MakeDepositComposable(
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
+ val ibanError = state is DepositState.IbanInvalid
OutlinedTextField(
modifier = Modifier
- .padding(16.dp),
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
value = iban,
+ singleLine = true,
enabled = !state.showFees,
onValueChange = { input ->
- iban = input
+ iban = input.uppercase()
+ },
+ isError = ibanError,
+ supportingText = {
+ if (ibanError) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.send_deposit_iban_error),
+ color = MaterialTheme.colorScheme.error
+ )
+ }
},
- isError = iban.isBlank(),
label = {
Text(
text = stringResource(R.string.send_deposit_iban),
- color = if (iban.isBlank()) {
- colorResource(R.color.red)
+ color = if (ibanError) {
+ MaterialTheme.colorScheme.error
} else Color.Unspecified,
)
}
)
- OutlinedTextField(
- modifier = Modifier
- .padding(16.dp),
- value = bic,
- enabled = !state.showFees,
- onValueChange = { input ->
- bic = input
- },
- label = {
- Text(
- text = stringResource(R.string.send_deposit_bic),
- )
- }
- )
- val amountTitle = if (state.effectiveDepositAmount == null) {
- R.string.amount_chosen
- } else R.string.send_deposit_amount_effective
- Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- text = stringResource(id = amountTitle),
- )
- val shownAmount = if (state.effectiveDepositAmount == null) amount else {
- state.effectiveDepositAmount
- }
- Text(
- modifier = Modifier.padding(16.dp),
- fontSize = 24.sp,
- color = colorResource(R.color.green),
- text = shownAmount.toString(),
+ TransactionAmountComposable(
+ label = stringResource(R.string.amount_chosen),
+ amount = amount,
+ amountType = Positive,
)
AnimatedVisibility(visible = state.showFees) {
Column(
@@ -144,29 +135,17 @@ fun MakeDepositComposable(
val totalAmount = state.totalDepositCost ?: amount
val effectiveAmount = state.effectiveDepositAmount ?: Amount.zero(amount.currency)
val fee = totalAmount - effectiveAmount
- Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- text = stringResource(id = R.string.withdraw_fees),
- )
- Text(
- modifier = Modifier.padding(16.dp),
- fontSize = 24.sp,
- color = colorResource(if (fee.isZero()) R.color.green else R.color.red),
- text = if (fee.isZero()) {
- fee.toString()
- } else {
- stringResource(R.string.amount_negative, fee.toString())
- },
- )
- Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- text = stringResource(id = R.string.send_amount),
+
+ TransactionAmountComposable(
+ label = stringResource(R.string.withdraw_fees),
+ amount = fee.withSpec(amount.spec),
+ amountType = if (fee.isZero()) Positive else Negative,
)
- Text(
- modifier = Modifier.padding(16.dp),
- fontSize = 24.sp,
- color = colorResource(R.color.green),
- text = totalAmount.toString(),
+
+ TransactionAmountComposable(
+ label = stringResource(R.string.send_amount),
+ amount = effectiveAmount.withSpec(amount.spec),
+ amountType = Positive,
)
}
}
@@ -174,8 +153,8 @@ fun MakeDepositComposable(
Text(
modifier = Modifier.padding(16.dp),
fontSize = 18.sp,
- color = colorResource(R.color.red),
- text = (state as? DepositState.Error)?.msg ?: "",
+ color = MaterialTheme.colorScheme.error,
+ text = (state as? DepositState.Error)?.error?.userFacingMsg ?: "",
)
}
val focusManager = LocalFocusManager.current
@@ -184,13 +163,15 @@ fun MakeDepositComposable(
enabled = iban.isNotBlank(),
onClick = {
focusManager.clearFocus()
- onMakeDeposit(amount, name, iban, bic)
+ onMakeDeposit(amount, name, iban)
},
) {
- Text(text = stringResource(
- if (state.showFees) R.string.send_deposit_create_button
- else R.string.send_deposit_check_fees_button
- ))
+ Text(
+ text = stringResource(
+ if (state is DepositState.FeesChecked) R.string.send_deposit_create_button
+ else R.string.send_deposit_check_fees_button
+ )
+ )
}
}
}
@@ -200,12 +181,12 @@ fun MakeDepositComposable(
fun PreviewMakeDepositComposable() {
Surface {
val state = DepositState.FeesChecked(
- effectiveDepositAmount = Amount.fromDouble("TESTKUDOS", 42.00),
- totalDepositCost = Amount.fromDouble("TESTKUDOS", 42.23),
+ effectiveDepositAmount = Amount.fromString("TESTKUDOS", "42.00"),
+ totalDepositCost = Amount.fromString("TESTKUDOS", "42.23"),
)
MakeDepositComposable(
state = state,
- amount = Amount.fromDouble("TESTKUDOS", 42.23)) { _, _, _, _ ->
+ amount = Amount.fromString("TESTKUDOS", "42.23")) { _, _, _ ->
}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt
index 47e8cfb..8961016 100644
--- a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt
@@ -30,16 +30,15 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.DropdownMenu
-import androidx.compose.material.DropdownMenuItem
-import androidx.compose.material.LocalTextStyle
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.material.TextFieldDefaults
+import androidx.compose.material3.Button
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -48,14 +47,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
@@ -66,6 +64,7 @@ import net.taler.common.Amount
import net.taler.wallet.AmountResult
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
+import net.taler.wallet.compose.AmountInputField
import net.taler.wallet.compose.TalerSurface
class PayToUriFragment : Fragment() {
@@ -85,9 +84,9 @@ class PayToUriFragment : Fragment() {
TalerSurface {
if (currencies.isEmpty()) Text(
text = stringResource(id = R.string.payment_balance_insufficient),
- color = colorResource(id = R.color.red),
+ color = MaterialTheme.colorScheme.error,
) else if (depositManager.isSupportedPayToUri(uri)) PayToComposable(
- currencies = model.getCurrencies(),
+ currencies = currencies,
getAmount = model::createAmount,
onAmountChosen = { amount ->
val u = Uri.parse(uri)
@@ -101,7 +100,7 @@ class PayToUriFragment : Fragment() {
},
) else Text(
text = stringResource(id = R.string.uri_invalid),
- color = colorResource(id = R.color.red),
+ color = MaterialTheme.colorScheme.error,
)
}
}
@@ -130,30 +129,27 @@ private fun PayToComposable(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
- var amountText by rememberSaveable { mutableStateOf("") }
+ var amountText by rememberSaveable { mutableStateOf("0") }
var amountError by rememberSaveable { mutableStateOf("") }
var currency by rememberSaveable { mutableStateOf(currencies[0]) }
val focusRequester = remember { FocusRequester() }
- OutlinedTextField(
- modifier = Modifier
- .focusRequester(focusRequester),
+ AmountInputField(
+ modifier = Modifier.focusRequester(focusRequester),
value = amountText,
onValueChange = { input ->
amountError = ""
amountText = input
},
- keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Decimal),
- singleLine = true,
+ label = { Text(stringResource(R.string.send_amount)) },
+ supportingText = {
+ if (amountError.isNotBlank()) Text(amountError)
+ },
isError = amountError.isNotBlank(),
- label = {
- if (amountError.isBlank()) {
- Text(stringResource(R.string.send_amount))
- } else {
- Text(amountError, color = colorResource(R.color.red))
- }
- }
)
CurrencyDropdown(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(Center),
currencies = currencies,
onCurrencyChanged = { c -> currency = c },
)
@@ -187,25 +183,25 @@ private fun PayToComposable(
fun CurrencyDropdown(
currencies: List<String>,
onCurrencyChanged: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ initialCurrency: String? = null,
+ readOnly: Boolean = false,
) {
- var selectedIndex by remember { mutableStateOf(0) }
+ val initialIndex = currencies.indexOf(initialCurrency).let { if (it < 0) 0 else it }
+ var selectedIndex by remember { mutableStateOf(initialIndex) }
var expanded by remember { mutableStateOf(false) }
Box(
- modifier = Modifier
- .fillMaxSize()
- .wrapContentSize(Alignment.Center),
+ modifier = modifier,
) {
OutlinedTextField(
modifier = Modifier
- .clickable(onClick = { expanded = true }),
+ .clickable(onClick = { if (!readOnly) expanded = true }),
value = currencies[selectedIndex],
onValueChange = { },
readOnly = true,
enabled = false,
textStyle = LocalTextStyle.current.copy( // show text as if not disabled
- color = TextFieldDefaults.outlinedTextFieldColors().textColor(
- enabled = true,
- ).value
+ color = MaterialTheme.colorScheme.onSurfaceVariant
),
singleLine = true,
label = {
@@ -218,13 +214,16 @@ fun CurrencyDropdown(
modifier = Modifier,
) {
currencies.forEachIndexed { index, s ->
- DropdownMenuItem(onClick = {
- selectedIndex = index
- onCurrencyChanged(currencies[index])
- expanded = false
- }) {
- Text(text = s)
- }
+ DropdownMenuItem(
+ text = {
+ Text(text = s)
+ },
+ onClick = {
+ selectedIndex = index
+ onCurrencyChanged(currencies[index])
+ expanded = false
+ }
+ )
}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt
index 7c5c7a2..817dfac 100644
--- a/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt
@@ -21,9 +21,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -32,16 +32,31 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
import net.taler.common.Timestamp
import net.taler.common.toAbsoluteTime
import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.transactions.AmountType
-import net.taler.wallet.transactions.DeleteTransactionComposable
+import net.taler.wallet.transactions.ErrorTransactionButton
+import net.taler.wallet.transactions.TransactionAction
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionDeposit
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionState
+import net.taler.wallet.transactions.TransitionsComposable
@Composable
-fun TransactionDepositComposable(t: TransactionDeposit, onDelete: () -> Unit) {
+fun TransactionDepositComposable(
+ t: TransactionDeposit,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
@@ -53,27 +68,34 @@ fun TransactionDepositComposable(t: TransactionDeposit, onDelete: () -> Unit) {
Text(
modifier = Modifier.padding(16.dp),
text = t.timestamp.ms.toAbsoluteTime(context).toString(),
- style = MaterialTheme.typography.body1,
- )
- TransactionAmountComposable(
- label = stringResource(id = R.string.transaction_paid),
- amount = t.amountEffective,
- amountType = AmountType.Negative,
+ style = MaterialTheme.typography.bodyLarge,
)
+
TransactionAmountComposable(
- label = stringResource(id = R.string.transaction_order_total),
- amount = t.amountRaw,
+ label = stringResource(id = R.string.amount_chosen),
+ amount = t.amountRaw.withSpec(spec),
amountType = AmountType.Neutral,
)
+
val fee = t.amountEffective - t.amountRaw
if (!fee.isZero()) {
TransactionAmountComposable(
label = stringResource(id = R.string.withdraw_fees),
- amount = fee,
+ amount = fee.withSpec(spec),
amountType = AmountType.Negative,
)
}
- DeleteTransactionComposable(onDelete)
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_sent),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
}
}
@@ -83,13 +105,15 @@ fun TransactionDepositComposablePreview() {
val t = TransactionDeposit(
transactionId = "transactionId",
timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
- pending = true,
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
depositGroupId = "fooBar",
- amountRaw = Amount.fromDouble("TESTKUDOS", 42.1337),
- amountEffective = Amount.fromDouble("TESTKUDOS", 42.23),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.1337"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.23"),
targetPaytoUri = "https://exchange.example.org/peer/pull/credit",
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
)
Surface {
- TransactionDepositComposable(t) {}
+ TransactionDepositComposable(t, true, null) {}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt
new file mode 100644
index 0000000..0ce5c01
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt
@@ -0,0 +1,163 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.events
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.CopyToClipboardButton
+import net.taler.wallet.events.ObservabilityDialog.Companion.json
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+class ObservabilityDialog: DialogFragment() {
+ private val model: MainViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ val events by model.observabilityLog.collectAsState()
+ ObservabilityComposable(events.reversed()) {
+ dismiss()
+ }
+ }
+ }
+
+ companion object {
+ @OptIn(ExperimentalSerializationApi::class)
+ val json = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+ }
+}
+
+@Composable
+fun ObservabilityComposable(
+ events: List<ObservabilityEvent>,
+ onDismiss: () -> Unit,
+) {
+ var showJson by remember { mutableStateOf(false) }
+
+ AlertDialog(
+ title = { Text(stringResource(R.string.observability_title)) },
+ text = {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(events) { event ->
+ ObservabilityItem(event, showJson)
+ }
+ }
+ },
+ onDismissRequest = onDismiss,
+ dismissButton = {
+ Button(onClick = { showJson = !showJson }) {
+ Text(if (showJson) {
+ stringResource(R.string.observability_hide_json)
+ } else {
+ stringResource(R.string.observability_show_json)
+ })
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.close))
+ }
+ },
+ )
+}
+
+@Composable
+fun ObservabilityItem(
+ event: ObservabilityEvent,
+ showJson: Boolean,
+) {
+ val body = json.encodeToString(event.body)
+ val timestamp = DateTimeFormatter
+ .ofLocalizedDateTime(FormatStyle.MEDIUM)
+ .format(event.timestamp)
+
+ ListItem(
+ modifier = Modifier.fillMaxWidth(),
+ headlineContent = { Text(event.type) },
+ overlineContent = { Text(timestamp) },
+ supportingContent = if (!showJson) null else { ->
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(
+ modifier = Modifier.background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ shape = MaterialTheme.shapes.small,
+ )
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(10.dp)
+ .fillMaxWidth(),
+ text = body,
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
+
+ CopyToClipboardButton(
+ label = "Event",
+ content = body,
+ colors = ButtonDefaults.textButtonColors(),
+ )
+ }
+ },
+ )
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt b/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt
new file mode 100644
index 0000000..a50cde2
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt
@@ -0,0 +1,66 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.events
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import java.time.LocalDateTime
+
+
+@Serializable(with = ObservabilityEventSerializer::class)
+class ObservabilityEvent(
+ val body: JsonObject,
+ val timestamp: LocalDateTime,
+ val type: String,
+)
+
+class ObservabilityEventSerializer: KSerializer<ObservabilityEvent> {
+ private val jsonElementSerializer = JsonElement.serializer()
+
+ override val descriptor: SerialDescriptor
+ get() = jsonElementSerializer.descriptor
+
+ override fun deserialize(decoder: Decoder): ObservabilityEvent {
+ require(decoder is JsonDecoder)
+ val jsonObject = decoder
+ .decodeJsonElement()
+ .jsonObject
+
+ val type = jsonObject["type"]
+ ?.jsonPrimitive
+ ?.content
+ ?: "unknown"
+
+ return ObservabilityEvent(
+ body = jsonObject,
+ timestamp = LocalDateTime.now(),
+ type = type,
+ )
+ }
+
+ override fun serialize(encoder: Encoder, value: ObservabilityEvent) {
+ encoder.encodeSerializableValue(JsonObject.serializer(), value.body)
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt
index 5ea763a..fd942b0 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt
@@ -22,6 +22,7 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
@@ -32,7 +33,7 @@ class AddExchangeDialogFragment : DialogFragment() {
private val exchangeManager by lazy { model.exchangeManager }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- return AlertDialog.Builder(requireContext(), R.style.DialogTheme)
+ return MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
.setIcon(R.drawable.ic_account_balance)
.setTitle(R.string.exchange_list_add)
.setView(R.layout.dialog_exchange_add)
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
index e0cf5be..674632e 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
@@ -26,25 +26,15 @@ import android.widget.TextView
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
-import kotlinx.serialization.Serializable
import net.taler.wallet.R
-import net.taler.wallet.cleanExchange
import net.taler.wallet.exchanges.ExchangeAdapter.ExchangeItemViewHolder
-@Serializable
-data class ExchangeItem(
- val exchangeBaseUrl: String,
- // can be null before exchange info in wallet-core was fully loaded
- val currency: String? = null,
- val paytoUris: List<String>,
-) {
- val name: String get() = cleanExchange(exchangeBaseUrl)
-}
-
interface ExchangeClickListener {
fun onExchangeSelected(item: ExchangeItem)
fun onManualWithdraw(item: ExchangeItem)
fun onPeerReceive(item: ExchangeItem)
+ fun onExchangeReload(item: ExchangeItem)
+ fun onExchangeDelete(item: ExchangeItem)
}
internal class ExchangeAdapter(
@@ -80,8 +70,9 @@ internal class ExchangeAdapter(
fun bind(item: ExchangeItem) {
urlView.text = item.name
+ // If currency is null, it's because we have no data from the exchange...
currencyView.text = if (item.currency == null) {
- context.getString(R.string.settings_version_unknown)
+ context.getString(R.string.exchange_not_contacted)
} else {
context.getString(R.string.exchange_list_currency, item.currency)
}
@@ -91,7 +82,8 @@ internal class ExchangeAdapter(
} else {
itemView.setOnClickListener(null)
itemView.isClickable = false
- overflowIcon.visibility = VISIBLE
+ // ...thus, we should prevent the user from interacting with it.
+ overflowIcon.visibility = if (item.currency != null) VISIBLE else GONE
}
overflowIcon.setOnClickListener { openMenu(overflowIcon, item) }
}
@@ -108,6 +100,14 @@ internal class ExchangeAdapter(
listener.onPeerReceive(item)
true
}
+ R.id.action_reload -> {
+ listener.onExchangeReload(item)
+ true
+ }
+ R.id.action_delete -> {
+ listener.onExchangeDelete(item)
+ true
+ }
else -> false
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt
index d8242f3..d42ab2c 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt
@@ -22,7 +22,6 @@ import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import android.widget.TextView
-import androidx.core.content.ContextCompat.getColor
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView.Adapter
@@ -35,6 +34,7 @@ import net.taler.wallet.R
import net.taler.wallet.databinding.FragmentExchangeFeesBinding
import net.taler.wallet.exchanges.CoinFeeAdapter.CoinFeeViewHolder
import net.taler.wallet.exchanges.WireFeeAdapter.WireFeeViewHolder
+import net.taler.wallet.getAttrColor
class ExchangeFeesFragment : Fragment() {
@@ -71,7 +71,7 @@ class ExchangeFeesFragment : Fragment() {
if (amount.isZero()) text = amount.toString()
else {
text = getString(R.string.amount_negative, amount)
- setTextColor(getColor(context, R.color.red))
+ setText(requireContext().getAttrColor(R.attr.colorError))
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt
index 44c98c1..8a40bff 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt
@@ -18,21 +18,29 @@ package net.taler.wallet.exchanges
import android.os.Bundle
import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
+import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import net.taler.common.EventObserver
import net.taler.common.fadeIn
import net.taler.common.fadeOut
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.databinding.FragmentExchangeListBinding
+import net.taler.wallet.showError
open class ExchangeListFragment : Fragment(), ExchangeClickListener {
@@ -54,6 +62,21 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ requireActivity().addMenuProvider(object : MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ if (model.devMode.value == true) {
+ menuInflater.inflate(R.menu.exchange_list, menu)
+ }
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ if (menuItem.itemId == R.id.action_add_dev_exchanges) {
+ exchangeManager.addDevExchanges()
+ }
+ return true
+ }
+ }, viewLifecycleOwner, RESUMED)
+
ui.list.apply {
adapter = exchangeAdapter
addItemDecoration(DividerItemDecoration(context, VERTICAL))
@@ -69,7 +92,30 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
onExchangeUpdate(exchanges)
}
exchangeManager.addError.observe(viewLifecycleOwner, EventObserver { error ->
- if (error) onAddExchangeFailed()
+ onAddExchangeFailed()
+ if (model.devMode.value == true) {
+ showError(error)
+ }
+ })
+ exchangeManager.listError.observe(viewLifecycleOwner, EventObserver { error ->
+ onListExchangeFailed()
+ if (model.devMode.value == true) {
+ showError(error)
+ }
+ })
+ exchangeManager.deleteError.observe(viewLifecycleOwner, EventObserver { error ->
+ if (model.devMode.value == true) {
+ showError(error)
+ } else {
+ showError(error.userFacingMsg)
+ }
+ })
+ exchangeManager.reloadError.observe(viewLifecycleOwner, EventObserver { error ->
+ if (model.devMode.value == true) {
+ showError(error)
+ } else {
+ showError(error.userFacingMsg)
+ }
})
}
@@ -88,6 +134,10 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
Toast.makeText(requireContext(), R.string.exchange_add_error, LENGTH_LONG).show()
}
+ private fun onListExchangeFailed() {
+ Toast.makeText(requireContext(), R.string.exchange_list_error, LENGTH_LONG).show()
+ }
+
override fun onExchangeSelected(item: ExchangeItem) {
throw AssertionError("must not get triggered here")
}
@@ -98,8 +148,27 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
}
override fun onPeerReceive(item: ExchangeItem) {
- transactionManager.selectedCurrency = item.currency
- findNavController().navigate(R.id.receiveFunds)
+ transactionManager.selectedScope = item.scopeInfo
+ findNavController().navigate(R.id.action_global_receiveFunds)
+ }
+
+ override fun onExchangeReload(item: ExchangeItem) {
+ exchangeManager.reload(item.exchangeBaseUrl)
}
+ override fun onExchangeDelete(item: ExchangeItem) {
+ val optionsArray = arrayOf(getString(R.string.exchange_delete_force))
+ val checkedArray = BooleanArray(1) { false }
+
+ MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setTitle(R.string.exchange_delete)
+ .setMultiChoiceItems(optionsArray, checkedArray) { _, which, isChecked ->
+ checkedArray[which] = isChecked
+ }
+ .setNegativeButton(R.string.transactions_delete) { _, _ ->
+ exchangeManager.delete(item.exchangeBaseUrl, checkedArray[0])
+ }
+ .setPositiveButton(R.string.cancel) { _, _ -> }
+ .show()
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
index 5a4c6c2..fa357b5 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
@@ -17,9 +17,11 @@
package net.taler.wallet.exchanges
import android.util.Log
+import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
@@ -27,6 +29,7 @@ import kotlinx.serialization.Serializable
import net.taler.common.Event
import net.taler.common.toEvent
import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
@Serializable
@@ -34,6 +37,11 @@ data class ExchangeListResponse(
val exchanges: List<ExchangeItem>,
)
+@Serializable
+data class ExchangeDetailedResponse(
+ val exchange: ExchangeItem,
+)
+
class ExchangeManager(
private val api: WalletBackendApi,
private val scope: CoroutineScope,
@@ -45,8 +53,17 @@ class ExchangeManager(
private val mExchanges = MutableLiveData<List<ExchangeItem>>()
val exchanges: LiveData<List<ExchangeItem>> get() = list()
- private val mAddError = MutableLiveData<Event<Boolean>>()
- val addError: LiveData<Event<Boolean>> = mAddError
+ private val mAddError = MutableLiveData<Event<TalerErrorInfo>>()
+ val addError: LiveData<Event<TalerErrorInfo>> = mAddError
+
+ private val mListError = MutableLiveData<Event<TalerErrorInfo>>()
+ val listError: LiveData<Event<TalerErrorInfo>> = mListError
+
+ private val mDeleteError = MutableLiveData<Event<TalerErrorInfo>>()
+ val deleteError: LiveData<Event<TalerErrorInfo>> = mDeleteError
+
+ private val mReloadError = MutableLiveData<Event<TalerErrorInfo>>()
+ val reloadError: LiveData<Event<TalerErrorInfo>> = mReloadError
var withdrawalExchange: ExchangeItem? = null
@@ -55,7 +72,8 @@ class ExchangeManager(
scope.launch {
val response = api.request("listExchanges", ExchangeListResponse.serializer())
response.onError {
- throw AssertionError("Wallet core failed to return exchanges! ${it.userFacingMsg}")
+ mProgress.value = false
+ mListError.value = it.toEvent()
}.onSuccess {
Log.d(TAG, "Exchange list: ${it.exchanges}")
mProgress.value = false
@@ -70,9 +88,9 @@ class ExchangeManager(
api.request<Unit>("addExchange") {
put("exchangeBaseUrl", exchangeUrl)
}.onError {
- mProgress.value = false
Log.e(TAG, "Error adding exchange: $it")
- mAddError.value = true.toEvent()
+ mProgress.value = false
+ mAddError.value = it.toEvent()
}.onSuccess {
mProgress.value = false
Log.d(TAG, "Exchange $exchangeUrl added")
@@ -80,14 +98,98 @@ class ExchangeManager(
}
}
+ fun reload(exchangeUrl: String, force: Boolean = true) = scope.launch {
+ mProgress.value = true
+ api.request<Unit>("updateExchangeEntry") {
+ put("exchangeBaseUrl", exchangeUrl)
+ put("force", force)
+ }.onError {
+ Log.e(TAG, "Error reloading exchange: $it")
+ mProgress.value = false
+ mReloadError.value = it.toEvent()
+ }.onSuccess {
+ mProgress.value = false
+ Log.d(TAG, "Exchange $exchangeUrl reloaded")
+ list()
+ }
+ }
+
+ fun delete(exchangeUrl: String, purge: Boolean = false) = scope.launch {
+ mProgress.value = true
+ api.request<Unit>("deleteExchange") {
+ put("exchangeBaseUrl", exchangeUrl)
+ put("purge", purge)
+ }.onError {
+ Log.e(TAG, "Error deleting exchange: $it")
+ mProgress.value = false
+ mDeleteError.value = it.toEvent()
+ }.onSuccess {
+ mProgress.value = false
+ Log.d(TAG, "Exchange $exchangeUrl deleted")
+ list()
+ }
+ }
+
fun findExchangeForCurrency(currency: String): Flow<ExchangeItem?> = flow {
- val response = api.request("listExchanges", ExchangeListResponse.serializer())
+ emit(findExchange(currency))
+ }
+
+ @WorkerThread
+ suspend fun findExchange(currency: String): ExchangeItem? {
var exchange: ExchangeItem? = null
- response.onSuccess { exchangeListResponse ->
+ api.request(
+ operation = "listExchanges",
+ serializer = ExchangeListResponse.serializer()
+ ).onSuccess { exchangeListResponse ->
// just pick the first for now
exchange = exchangeListResponse.exchanges.find { it.currency == currency }
}
- emit(exchange)
+ return exchange
+ }
+
+ @WorkerThread
+ suspend fun findExchangeByUrl(exchangeUrl: String): ExchangeItem? {
+ var exchange: ExchangeItem? = null
+ api.request("getExchangeDetailedInfo", ExchangeDetailedResponse.serializer()) {
+ put("exchangeBaseUrl", exchangeUrl)
+ }.onError {
+ Log.e(TAG, "Error getExchangeDetailedInfo: $it")
+ }.onSuccess {
+ exchange = it.exchange
+ }
+ return exchange
+ }
+
+ fun addDevExchanges() {
+ scope.launch {
+ listOf(
+ "https://exchange.demo.taler.net/",
+ "https://exchange.test.taler.net/",
+ "https://exchange.head.taler.net/",
+ "https://exchange.taler.ar/",
+ "https://exchange.taler.fdold.eu/",
+ "https://exchange.taler.grothoff.org/",
+ ).forEach { exchangeUrl ->
+ add(exchangeUrl)
+ delay(100)
+ }
+ exchanges.value?.let { exs ->
+ exs.find {
+ it.exchangeBaseUrl.startsWith("https://exchange.taler.fdold.eu")
+ }?.let { fDoldExchange ->
+ api.request<Unit>("addGlobalCurrencyExchange") {
+ put("currency", fDoldExchange.currency)
+ put("exchangeBaseUrl", fDoldExchange.exchangeBaseUrl)
+ put("exchangeMasterPub",
+ "7ER30ZWJEXAG026H5KG9M19NGTFC2DKKFPV79GVXA6DK5DCNSWXG")
+ }.onError {
+ Log.e(TAG, "Error addGlobalCurrencyExchange: $it")
+ }.onSuccess {
+ Log.i(TAG, "fdold is global now!")
+ }
+ }
+ }
+ }
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
new file mode 100644
index 0000000..0015e1c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
@@ -0,0 +1,38 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.exchanges
+
+import kotlinx.serialization.Serializable
+import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.cleanExchange
+
+@Serializable
+data class BuiltinExchange(
+ val exchangeBaseUrl: String,
+ val currencyHint: String? = null,
+)
+
+@Serializable
+data class ExchangeItem(
+ val exchangeBaseUrl: String,
+ // can be null before exchange info in wallet-core was fully loaded
+ val currency: String? = null,
+ val paytoUris: List<String>,
+ val scopeInfo: ScopeInfo? = null,
+) {
+ val name: String get() = cleanExchange(exchangeBaseUrl)
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt
new file mode 100644
index 0000000..136738b
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt
@@ -0,0 +1,111 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.exchanges
+
+import android.app.Dialog
+import android.os.Bundle
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asFlow
+import com.google.accompanist.themeadapter.material3.Mdc3Theme
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import net.taler.common.Event
+import net.taler.common.toEvent
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+
+class SelectExchangeDialogFragment: DialogFragment() {
+ private var exchangeList = MutableLiveData<List<ExchangeItem>>()
+
+ private var mExchangeSelection = MutableLiveData<Event<ExchangeItem>>()
+ val exchangeSelection: LiveData<Event<ExchangeItem>> = mExchangeSelection
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val view = ComposeView(requireContext()).apply {
+ setContent {
+ val exchanges = exchangeList.asFlow().collectAsStateLifecycleAware(initial = emptyList())
+ SelectExchangeComposable(exchanges.value) {
+ onExchangeSelected(it)
+ }
+ }
+ }
+
+ return MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setIcon(R.drawable.ic_account_balance)
+ .setTitle(R.string.exchange_list_select)
+ .setView(view)
+ .setNegativeButton(R.string.cancel) { _, _ ->
+ dismiss()
+ }
+ .create()
+ }
+
+ fun setExchanges(exchanges: List<ExchangeItem>) {
+ exchangeList.value = exchanges
+ }
+
+ private fun onExchangeSelected(exchange: ExchangeItem) {
+ mExchangeSelection.value = exchange.toEvent()
+ dismiss()
+ }
+}
+
+@Composable
+fun SelectExchangeComposable(
+ exchanges: List<ExchangeItem>,
+ onExchangeSelected: (exchange: ExchangeItem) -> Unit,
+) {
+ Mdc3Theme {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ items(exchanges) {
+ ExchangeItemComposable(it) {
+ onExchangeSelected(it)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ExchangeItemComposable(exchange: ExchangeItem, onSelected: () -> Unit) {
+ ListItem(
+ modifier = Modifier.clickable { onSelected() },
+ headlineContent = { Text(cleanExchange(exchange.exchangeBaseUrl)) },
+ supportingContent = exchange.currency?.let {
+ { Text(stringResource(R.string.exchange_list_currency, it)) }
+ },
+ colors = ListItemDefaults.colors(
+ containerColor = Color.Transparent,
+ )
+ )
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt
deleted file mode 100644
index 61e0db5..0000000
--- a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.exchanges
-
-import androidx.navigation.fragment.findNavController
-import net.taler.common.fadeOut
-
-class SelectExchangeFragment : ExchangeListFragment() {
-
- private val withdrawManager by lazy { model.withdrawManager }
-
- override val isSelectOnly = true
- private val exchangeSelection by lazy {
- requireNotNull(withdrawManager.exchangeSelection.value?.getEvenIfConsumedAlready())
- }
-
- override fun onExchangeUpdate(exchanges: List<ExchangeItem>) {
- ui.progressBar.fadeOut()
- super.onExchangeUpdate(exchanges.filter { exchangeItem ->
- exchangeItem.currency == exchangeSelection.amount.currency
- })
- }
-
- override fun onExchangeSelected(item: ExchangeItem) {
- withdrawManager.getWithdrawalDetails(
- exchangeBaseUrl = item.exchangeBaseUrl,
- amount = exchangeSelection.amount,
- showTosImmediately = true,
- uri = exchangeSelection.talerWithdrawUri,
- )
- findNavController().navigateUp()
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt
deleted file mode 100644
index df2b2b8..0000000
--- a/wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.payment
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.navigation.fragment.findNavController
-import net.taler.wallet.databinding.FragmentAlreadyPaidBinding
-
-/**
- * Display the message that the user already paid for the order
- * that the merchant is proposing.
- */
-class AlreadyPaidFragment : Fragment() {
-
- private lateinit var ui: FragmentAlreadyPaidBinding
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- ui = FragmentAlreadyPaidBinding.inflate(inflater, container, false)
- return ui.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- ui.backButton.setOnClickListener {
- findNavController().navigateUp()
- }
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
new file mode 100644
index 0000000..ffa4875
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
@@ -0,0 +1,184 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.payment
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.ContractTerms
+import net.taler.wallet.AmountResult
+import net.taler.wallet.R
+import net.taler.wallet.compose.LoadingScreen
+import net.taler.wallet.compose.TalerSurface
+
+sealed class AmountFieldStatus {
+ object FixedAmount : AmountFieldStatus()
+ class Default(
+ val amountStr: String? = null,
+ val currency: String? = null,
+ ) : AmountFieldStatus()
+
+ object Invalid : AmountFieldStatus()
+}
+
+@Composable
+fun PayTemplateComposable(
+ defaultSummary: String?,
+ amountStatus: AmountFieldStatus,
+ currencies: List<String>,
+ payStatus: PayStatus,
+ onCreateAmount: (String, String) -> AmountResult,
+ onSubmit: (summary: String?, amount: Amount?) -> Unit,
+ onError: (resId: Int) -> Unit,
+) {
+ // If wallet is empty, there's no way the user can pay something
+ if (amountStatus is AmountFieldStatus.Invalid) {
+ PayTemplateError(stringResource(R.string.receive_amount_invalid))
+ } else if (currencies.isEmpty()) {
+ PayTemplateError(stringResource(R.string.payment_balance_insufficient))
+ } else when (val p = payStatus) {
+ is PayStatus.None -> PayTemplateOrderComposable(
+ currencies = currencies,
+ defaultSummary = defaultSummary,
+ amountStatus = amountStatus,
+ onCreateAmount = onCreateAmount,
+ onError = onError,
+ onSubmit = onSubmit,
+ )
+
+ is PayStatus.Loading -> PayTemplateLoading()
+ is PayStatus.AlreadyPaid -> PayTemplateError(stringResource(R.string.payment_already_paid))
+ is PayStatus.InsufficientBalance -> PayTemplateError(stringResource(R.string.payment_balance_insufficient))
+ is PayStatus.Pending -> {
+ val error = p.error
+ PayTemplateError(if (error != null) {
+ stringResource(R.string.payment_error, error.userFacingMsg)
+ } else {
+ stringResource(R.string.payment_template_error)
+ })
+ }
+ is PayStatus.Prepared -> {} // handled in fragment, will redirect
+ is PayStatus.Success -> {} // handled by other UI flow, no need for content here
+ }
+}
+
+@Composable
+fun PayTemplateError(message: String) {
+ Box(
+ modifier = Modifier.padding(16.dp).fillMaxSize(),
+ contentAlignment = Center,
+ ) {
+ Text(
+ text = message,
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+}
+
+@Composable
+fun PayTemplateLoading() {
+ LoadingScreen()
+}
+
+@Preview
+@Composable
+fun PayTemplateLoadingPreview() {
+ TalerSurface {
+ PayTemplateComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ payStatus = PayStatus.Loading,
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { _ -> },
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateInsufficientBalancePreview() {
+ TalerSurface {
+ PayTemplateComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ payStatus = PayStatus.InsufficientBalance(
+ ContractTerms(
+ "test",
+ amount = Amount.zero("TESTKUDOS"),
+ products = emptyList()
+ ), Amount.zero("TESTKUDOS")
+ ),
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { _ -> },
+ )
+ }
+}
+
+@Preview(widthDp = 300)
+@Composable
+fun PayTemplateAlreadyPaidPreview() {
+ TalerSurface {
+ PayTemplateComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ payStatus = PayStatus.AlreadyPaid(transactionId = "transactionId"),
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { _ -> },
+ )
+ }
+}
+
+
+@Preview
+@Composable
+fun PayTemplateNoCurrenciesPreview() {
+ TalerSurface {
+ PayTemplateComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ payStatus = PayStatus.None,
+ currencies = emptyList(),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { _ -> },
+ )
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt
new file mode 100644
index 0000000..4eb2c11
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt
@@ -0,0 +1,115 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.payment
+
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.asFlow
+import androidx.navigation.fragment.findNavController
+import net.taler.common.Amount
+import net.taler.common.showError
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
+
+class PayTemplateFragment : Fragment() {
+
+ private val model: MainViewModel by activityViewModels()
+ private lateinit var uriString: String
+ private lateinit var uri: Uri
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ uriString = arguments?.getString("uri") ?: error("no amount passed")
+ uri = Uri.parse(uriString)
+
+ val defaultSummary = uri.getQueryParameter("summary")
+ val defaultAmount = uri.getQueryParameter("amount")
+ val amountFieldStatus = getAmountFieldStatus(defaultAmount)
+
+ val payStatusFlow = model.paymentManager.payStatus.asFlow()
+
+ return ComposeView(requireContext()).apply {
+ setContent {
+ val payStatus = payStatusFlow.collectAsStateLifecycleAware(initial = PayStatus.None)
+ TalerSurface {
+ PayTemplateComposable(
+ currencies = model.getCurrencies(),
+ defaultSummary = defaultSummary,
+ amountStatus = amountFieldStatus,
+ payStatus = payStatus.value,
+ onCreateAmount = model::createAmount,
+ onSubmit = this@PayTemplateFragment::createOrder,
+ onError = { this@PayTemplateFragment.showError(it) },
+ )
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ if (uri.queryParameterNames?.isEmpty() == true) {
+ createOrder(null, null)
+ }
+
+ model.paymentManager.payStatus.observe(viewLifecycleOwner) { payStatus ->
+ when (payStatus) {
+ is PayStatus.Prepared -> {
+ findNavController().navigate(R.id.action_promptPayTemplate_to_promptPayment)
+ }
+
+ is PayStatus.Pending -> if (payStatus.error != null && model.devMode.value == true) {
+ showError(payStatus.error)
+ }
+
+ else -> {}
+ }
+ }
+ }
+
+ private fun getAmountFieldStatus(defaultAmount: String?): AmountFieldStatus {
+ return if (defaultAmount == null) {
+ AmountFieldStatus.FixedAmount
+ } else if (defaultAmount.isBlank()) {
+ AmountFieldStatus.Default()
+ } else {
+ val parts = defaultAmount.split(":")
+ when (parts.size) {
+ 0 -> AmountFieldStatus.Default()
+ 1 -> AmountFieldStatus.Default(currency = parts[0])
+ 2 -> AmountFieldStatus.Default(parts[1], parts[0])
+ else -> AmountFieldStatus.Invalid
+ }
+ }
+ }
+
+ private fun createOrder(summary: String?, amount: Amount?) {
+ model.paymentManager.preparePayForTemplate(uriString, summary, amount)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt
new file mode 100644
index 0000000..d6131c7
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt
@@ -0,0 +1,177 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.payment
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment.Companion.End
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.AmountResult
+import net.taler.wallet.R
+import net.taler.wallet.compose.AmountInputField
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.deposit.CurrencyDropdown
+
+@Composable
+fun PayTemplateOrderComposable(
+ currencies: List<String>, // assumed to have size > 0
+ defaultSummary: String? = null,
+ amountStatus: AmountFieldStatus,
+ onCreateAmount: (String, String) -> AmountResult,
+ onError: (msgRes: Int) -> Unit,
+ onSubmit: (summary: String?, amount: Amount?) -> Unit,
+) {
+ val amountDefault = amountStatus as? AmountFieldStatus.Default
+
+ var summary by remember { mutableStateOf(defaultSummary) }
+ var currency by remember { mutableStateOf(amountDefault?.currency ?: currencies[0]) }
+ var amount by remember { mutableStateOf(amountDefault?.amountStr ?: "0") }
+
+ Column(horizontalAlignment = End) {
+ if (defaultSummary != null) OutlinedTextField(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ value = summary ?: "",
+ isError = summary.isNullOrBlank(),
+ onValueChange = { summary = it },
+ singleLine = true,
+ label = { Text(stringResource(R.string.withdraw_manual_ready_subject)) },
+ )
+ if (amountDefault != null) AmountField(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ amount = amount,
+ currency = currency,
+ currencies = currencies,
+ fixedCurrency = (amountStatus as? AmountFieldStatus.Default)?.currency != null,
+ onAmountChosen = { a, c ->
+ amount = a
+ currency = c
+ },
+ )
+ Button(
+ modifier = Modifier.padding(16.dp),
+ enabled = defaultSummary == null || !summary.isNullOrBlank(),
+ onClick = {
+ when (val res = onCreateAmount(amount, currency)) {
+ is AmountResult.InsufficientBalance -> onError(R.string.payment_balance_insufficient)
+ is AmountResult.InvalidAmount -> onError(R.string.receive_amount_invalid)
+ is AmountResult.Success -> onSubmit(summary, res.amount)
+ }
+ },
+ ) {
+ Text(stringResource(R.string.payment_create_order))
+ }
+ }
+}
+
+@Composable
+private fun AmountField(
+ modifier: Modifier = Modifier,
+ currencies: List<String>,
+ fixedCurrency: Boolean,
+ amount: String,
+ currency: String,
+ onAmountChosen: (amount: String, currency: String) -> Unit,
+) {
+ Row(
+ modifier = modifier,
+ ) {
+ AmountInputField(
+ modifier = Modifier
+ .padding(end = 16.dp)
+ .weight(1f),
+ value = amount,
+ onValueChange = { onAmountChosen(it, currency) },
+ label = { Text(stringResource(R.string.send_amount)) }
+ )
+ CurrencyDropdown(
+ modifier = Modifier.weight(1f),
+ initialCurrency = currency,
+ currencies = currencies,
+ onCurrencyChanged = { onAmountChosen(amount, it) },
+ readOnly = fixedCurrency,
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateDefaultPreview() {
+ TalerSurface {
+ PayTemplateOrderComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { },
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateFixedAmountPreview() {
+ TalerSurface {
+ PayTemplateOrderComposable(
+ defaultSummary = "default summary",
+ amountStatus = AmountFieldStatus.FixedAmount,
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { },
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateBlankSubjectPreview() {
+ TalerSurface {
+ PayTemplateOrderComposable(
+ defaultSummary = "",
+ amountStatus = AmountFieldStatus.FixedAmount,
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { },
+ )
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
index 53cb259..35cd9e6 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
@@ -32,6 +32,7 @@ import net.taler.wallet.payment.PayStatus.InsufficientBalance
import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse
import net.taler.wallet.payment.PreparePayResponse.InsufficientBalanceResponse
import net.taler.wallet.payment.PreparePayResponse.PaymentPossibleResponse
+import org.json.JSONObject
val REGEX_PRODUCT_IMAGE = Regex("^data:image/(jpeg|png);base64,([A-Za-z0-9+/=]+)$")
@@ -40,7 +41,7 @@ sealed class PayStatus {
object Loading : PayStatus()
data class Prepared(
val contractTerms: ContractTerms,
- val proposalId: String,
+ val transactionId: String,
val amountRaw: Amount,
val amountEffective: Amount,
) : PayStatus()
@@ -50,10 +51,18 @@ sealed class PayStatus {
val amountRaw: Amount,
) : PayStatus()
- // TODO bring user to fulfilment URI
- object AlreadyPaid : PayStatus()
- data class Error(val error: String) : PayStatus()
- data class Success(val currency: String) : PayStatus()
+ data class AlreadyPaid(
+ val transactionId: String,
+ ) : PayStatus()
+
+ data class Pending(
+ val transactionId: String? = null,
+ val error: TalerErrorInfo? = null,
+ ) : PayStatus()
+ data class Success(
+ val transactionId: String,
+ val currency: String,
+ ) : PayStatus()
}
class PaymentManager(
@@ -75,42 +84,57 @@ class PaymentManager(
mPayStatus.value = when (response) {
is PaymentPossibleResponse -> response.toPayStatusPrepared()
is InsufficientBalanceResponse -> InsufficientBalance(
- response.contractTerms,
- response.amountRaw
+ contractTerms = response.contractTerms,
+ amountRaw = response.amountRaw
+ )
+ is AlreadyConfirmedResponse -> AlreadyPaid(
+ transactionId = response.transactionId,
)
- is AlreadyConfirmedResponse -> AlreadyPaid
}
}
}
- fun confirmPay(proposalId: String, currency: String) = scope.launch {
+ fun confirmPay(transactionId: String, currency: String) = scope.launch {
api.request("confirmPay", ConfirmPayResult.serializer()) {
- put("proposalId", proposalId)
+ put("transactionId", transactionId)
}.onError {
handleError("confirmPay", it)
- }.onSuccess {
- mPayStatus.postValue(PayStatus.Success(currency))
- }
- }
-
- @UiThread
- fun abortPay() {
- val ps = payStatus.value
- if (ps is PayStatus.Prepared) {
- abortProposal(ps.proposalId)
+ }.onSuccess { response ->
+ mPayStatus.postValue(when (response) {
+ is ConfirmPayResult.Done -> PayStatus.Success(
+ transactionId = response.transactionId,
+ currency = currency,
+ )
+ is ConfirmPayResult.Pending -> PayStatus.Pending(
+ transactionId = response.transactionId,
+ error = response.lastError,
+ )
+ })
}
- resetPayStatus()
}
- internal fun abortProposal(proposalId: String) = scope.launch {
- Log.i(TAG, "aborting proposal")
- api.request<Unit>("abortProposal") {
- put("proposalId", proposalId)
+ fun preparePayForTemplate(url: String, summary: String?, amount: Amount?) = scope.launch {
+ mPayStatus.value = PayStatus.Loading
+ api.request("preparePayForTemplate", PreparePayResponse.serializer()) {
+ put("talerPayTemplateUri", url)
+ put("templateParams", JSONObject().apply {
+ summary?.let { put("summary", it) }
+ amount?.let { put("amount", it.toJSONString()) }
+ })
}.onError {
- Log.e(TAG, "received error response to abortProposal")
- handleError("abortProposal", it)
- }.onSuccess {
- mPayStatus.postValue(PayStatus.None)
+ handleError("preparePayForTemplate", it)
+ }.onSuccess { response ->
+ mPayStatus.value = when (response) {
+ is PaymentPossibleResponse -> response.toPayStatusPrepared()
+ is InsufficientBalanceResponse -> InsufficientBalance(
+ contractTerms = response.contractTerms,
+ amountRaw = response.amountRaw,
+ )
+
+ is AlreadyConfirmedResponse -> AlreadyPaid(
+ transactionId = response.transactionId,
+ )
+ }
}
}
@@ -121,7 +145,7 @@ class PaymentManager(
private fun handleError(operation: String, error: TalerErrorInfo) {
Log.e(TAG, "got $operation error result $error")
- mPayStatus.value = PayStatus.Error(error.userFacingMsg)
+ mPayStatus.value = PayStatus.Pending(error = error)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
index 7e03472..407f55f 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
@@ -32,14 +32,14 @@ sealed class PreparePayResponse {
@Serializable
@SerialName("payment-possible")
data class PaymentPossibleResponse(
- val proposalId: String,
+ val transactionId: String,
val amountRaw: Amount,
val amountEffective: Amount,
val contractTerms: ContractTerms,
) : PreparePayResponse() {
fun toPayStatusPrepared() = PayStatus.Prepared(
contractTerms = contractTerms,
- proposalId = proposalId,
+ transactionId = transactionId,
amountRaw = amountRaw,
amountEffective = amountEffective,
)
@@ -48,7 +48,6 @@ sealed class PreparePayResponse {
@Serializable
@SerialName("insufficient-balance")
data class InsufficientBalanceResponse(
- val proposalId: String,
val amountRaw: Amount,
val contractTerms: ContractTerms,
) : PreparePayResponse()
@@ -56,13 +55,13 @@ sealed class PreparePayResponse {
@Serializable
@SerialName("already-confirmed")
data class AlreadyConfirmedResponse(
- val proposalId: String,
+ val transactionId: String,
/**
* Did the payment succeed?
*/
val paid: Boolean,
val amountRaw: Amount,
- val amountEffective: Amount,
+ val amountEffective: Amount? = null,
val contractTerms: ContractTerms,
) : PreparePayResponse()
}
@@ -71,9 +70,15 @@ sealed class PreparePayResponse {
sealed class ConfirmPayResult {
@Serializable
@SerialName("done")
- data class Done(val contractTerms: ContractTerms) : ConfirmPayResult()
+ data class Done(
+ val transactionId: String,
+ val contractTerms: ContractTerms,
+ ) : ConfirmPayResult()
@Serializable
@SerialName("pending")
- data class Pending(val lastError: TalerErrorInfo) : ConfirmPayResult()
+ data class Pending(
+ val transactionId: String,
+ val lastError: TalerErrorInfo? = null,
+ ) : ConfirmPayResult()
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt b/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
index 87b6387..289f0d7 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
@@ -67,21 +67,25 @@ internal class ProductAdapter(private val listener: ProductImageClickListener) :
fun bind(product: ContractProduct) {
quantity.text = product.quantity.toString()
- if (product.image == null) {
+ val productImage = product.image
+ if (productImage == null) {
image.visibility = GONE
- } else {
- image.visibility = VISIBLE
- // product.image was validated before, so non-null below
- val match = REGEX_PRODUCT_IMAGE.matchEntire(product.image!!)!!
- val decodedString = Base64.decode(match.groups[2]!!.value, Base64.DEFAULT)
- val bitmap = decodeByteArray(decodedString, 0, decodedString.size)
- image.setImageBitmap(bitmap)
- image.setOnClickListener {
- listener.onImageClick(bitmap)
+ } else REGEX_PRODUCT_IMAGE.matchEntire(productImage)?.let { match ->
+ match.groups[2]?.value?.let { group ->
+ image.visibility = VISIBLE
+ val decodedString = Base64.decode(group, Base64.DEFAULT)
+ val bitmap = decodeByteArray(decodedString, 0, decodedString.size)
+ image.setImageBitmap(bitmap)
+ image.setOnClickListener {
+ listener.onImageClick(bitmap)
+ }
}
}
name.text = product.description
- price.text = product.totalPrice.toString()
+ price.visibility = product.totalPrice?.let {
+ price.text = it.toString()
+ VISIBLE
+ } ?: GONE
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
index 7ed1bab..31c26a0 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
@@ -18,23 +18,29 @@ package net.taler.wallet.payment
import android.graphics.Bitmap
import android.os.Bundle
+import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
+import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.common.ContractTerms
import net.taler.common.fadeIn
import net.taler.common.fadeOut
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
+import net.taler.wallet.TAG
import net.taler.wallet.databinding.FragmentPromptPaymentBinding
+import net.taler.wallet.showError
/**
* Show a payment and ask the user to accept/decline.
@@ -43,6 +49,7 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
private val model: MainViewModel by activityViewModels()
private val paymentManager by lazy { model.paymentManager }
+ private val transactionManager by lazy { model.transactionManager }
private lateinit var ui: FragmentPromptPaymentBinding
private val adapter = ProductAdapter(this)
@@ -67,7 +74,15 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
override fun onDestroy() {
super.onDestroy()
if (!requireActivity().isChangingConfigurations) {
- paymentManager.abortPay()
+ val payStatus = paymentManager.payStatus.value as? PayStatus.Prepared ?: return
+ transactionManager.abortTransaction(payStatus.transactionId) { error ->
+ Log.e(TAG, "Error abortTransaction $error")
+ if (model.devMode.value == false) {
+ showError(error.userFacingMsg)
+ } else {
+ showError(error)
+ }
+ }
}
}
@@ -91,8 +106,8 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
ui.bottom.confirmButton.setOnClickListener {
model.showProgressBar.value = true
paymentManager.confirmPay(
- payStatus.proposalId,
- payStatus.contractTerms.amount.currency
+ transactionId = payStatus.transactionId,
+ currency = payStatus.contractTerms.amount.currency,
)
ui.bottom.confirmButton.fadeOut()
ui.bottom.confirmProgressBar.fadeIn()
@@ -107,19 +122,24 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
is PayStatus.Success -> {
showLoading(false)
paymentManager.resetPayStatus()
- findNavController().navigate(R.id.action_promptPayment_to_nav_main)
- model.showTransactions(payStatus.currency)
+ navigateToTransaction(payStatus.transactionId)
Snackbar.make(requireView(), R.string.payment_initiated, LENGTH_LONG).show()
}
is PayStatus.AlreadyPaid -> {
showLoading(false)
paymentManager.resetPayStatus()
- findNavController().navigate(R.id.action_promptPayment_to_alreadyPaid)
+ navigateToTransaction(payStatus.transactionId)
+ Snackbar.make(requireView(), R.string.payment_already_paid, LENGTH_LONG).show()
}
- is PayStatus.Error -> {
+ is PayStatus.Pending -> {
showLoading(false)
- ui.details.errorView.text = getString(R.string.payment_error, payStatus.error)
- ui.details.errorView.fadeIn()
+ paymentManager.resetPayStatus()
+ navigateToTransaction(payStatus.transactionId)
+ if (payStatus.error != null && model.devMode.value == true) {
+ showError(payStatus.error)
+ } else {
+ showError(getString(R.string.payment_pending))
+ }
}
is PayStatus.None -> {
// No payment active.
@@ -154,4 +174,13 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
f.show(parentFragmentManager, "image")
}
+ private fun navigateToTransaction(id: String?) {
+ lifecycleScope.launch {
+ if (id != null && transactionManager.selectTransaction(id)) {
+ findNavController().navigate(R.id.action_promptPayment_to_nav_transactions_detail_payment)
+ } else {
+ findNavController().navigate(R.id.action_promptPayment_to_nav_main)
+ }
+ }
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt
new file mode 100644
index 0000000..0f6d661
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt
@@ -0,0 +1,174 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.payment
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.ContractMerchant
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.ErrorTransactionButton
+import net.taler.wallet.transactions.TransactionAction
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfo
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionLinkComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionPayment
+import net.taler.wallet.transactions.TransactionState
+import net.taler.wallet.transactions.TransitionsComposable
+
+@Composable
+fun TransactionPaymentComposable(
+ t: TransactionPayment,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onFulfill: (url: String) -> Unit,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_order_total),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+
+ val fee = t.amountEffective - t.amountRaw
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_paid),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
+ if (t.posConfirmation != null) {
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.payment_confirmation_code),
+ info = t.posConfirmation,
+ )
+ }
+
+ PurchaseDetails(info = t.info) {
+ onFulfill(t.info.fulfillmentUrl ?: "")
+ }
+
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
+ }
+}
+
+@Composable
+fun PurchaseDetails(
+ info: TransactionInfo,
+ onClick: () -> Unit,
+) {
+ Column(
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ // Summary and fulfillment message
+ val text = if (info.fulfillmentMessage == null) {
+ info.summary
+ } else {
+ "${info.summary}\n\n${info.fulfillmentMessage}"
+ }
+ if (info.fulfillmentUrl != null) {
+ TransactionLinkComposable(
+ label = stringResource(id = R.string.transaction_order),
+ info = text,
+ ) { onClick() }
+ } else {
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.transaction_order),
+ info = text,
+ )
+ }
+ // Order ID
+ Text(
+ stringResource(id = R.string.transaction_order_id, info.orderId),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+}
+
+@Preview
+@Composable
+fun TransactionPaymentComposablePreview() {
+ val t = TransactionPayment(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ info = TransactionInfo(
+ orderId = "123",
+ merchant = ContractMerchant(name = "Taler"),
+ summary = "Some Product that was bought and can have quite a long label",
+ fulfillmentMessage = "This is some fulfillment message",
+ fulfillmentUrl = "https://bank.demo.taler.net/",
+ products = listOf(),
+ ),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.1337"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.23"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ )
+ TalerSurface {
+ TransactionPaymentComposable(t = t, devMode = true, spec = null, onFulfill = {}) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt
new file mode 100644
index 0000000..4393e47
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt
@@ -0,0 +1,128 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.wallet.R
+import net.taler.wallet.compose.NumericInputField
+import net.taler.wallet.compose.SelectionChip
+import net.taler.wallet.compose.TalerSurface
+
+enum class ExpirationOption(val hours: Long) {
+ DAYS_1(24),
+ DAYS_7(24 * 7),
+ DAYS_30(24 * 30),
+ CUSTOM(-1)
+}
+
+@Composable
+fun ExpirationComposable(
+ modifier: Modifier = Modifier,
+ option: ExpirationOption,
+ hours: Long,
+ onOptionChange: (ExpirationOption) -> Unit,
+ onHoursChange: (Long) -> Unit,
+) {
+ val options = listOf(
+ ExpirationOption.DAYS_1 to stringResource(R.string.send_peer_expiration_1d),
+ ExpirationOption.DAYS_7 to stringResource(R.string.send_peer_expiration_7d),
+ ExpirationOption.DAYS_30 to stringResource(R.string.send_peer_expiration_30d),
+ ExpirationOption.CUSTOM to stringResource(R.string.send_peer_expiration_custom),
+ )
+ Column(
+ modifier = modifier,
+ ) {
+ LazyRow(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ items(items = options, key = { it.first }) {
+ SelectionChip(
+ label = { Text(it.second) },
+ modifier = Modifier.padding(horizontal = 4.dp),
+ selected = it.first == option,
+ value = it.first,
+ onSelected = { o ->
+ onOptionChange(o)
+ if (o != ExpirationOption.CUSTOM) {
+ onHoursChange(o.hours)
+ }
+ },
+ )
+ }
+ }
+
+ if (option == ExpirationOption.CUSTOM) {
+ val d = hours / 24L
+ val h = hours - d * 24L
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ NumericInputField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(end = 4.dp),
+ value = d,
+ onValueChange = {
+ onHoursChange(it * 24 + h)
+ },
+ label = { Text(stringResource(R.string.send_peer_expiration_days)) },
+ maxValue = 365,
+ )
+ NumericInputField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(start = 4.dp),
+ value = h,
+ onValueChange = {
+ onHoursChange(d * 24 + it)
+ },
+ label = { Text(stringResource(R.string.send_peer_expiration_hours)) },
+ maxValue = 23,
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun ExpirationComposablePreview() {
+ TalerSurface {
+ var option = ExpirationOption.CUSTOM
+ var hours = 25L
+ ExpirationComposable(
+ option = option,
+ hours = hours,
+ onOptionChange = { option = it }
+ ) { hours = it }
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt
index 70f36a2..1ce0175 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt
@@ -27,13 +27,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Card
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
@@ -48,20 +48,25 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.taler.common.Amount
import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE
+import net.taler.wallet.backend.TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE
import net.taler.wallet.backend.TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED
import net.taler.wallet.backend.TalerErrorInfo
data class IncomingData(
+ val isCredit: Boolean,
@StringRes val intro: Int,
@StringRes val button: Int,
)
val incomingPush = IncomingData(
+ isCredit = true,
intro = R.string.receive_peer_payment_intro,
button = R.string.receive_peer_payment_title,
)
val incomingPull = IncomingData(
+ isCredit = false,
intro = R.string.pay_peer_intro,
button = R.string.payment_button_confirm,
)
@@ -86,12 +91,12 @@ fun IncomingComposable(
)
when (val s = state.value) {
IncomingChecking -> PeerPullCheckingComposable()
- is IncomingTerms -> PeerPullTermsComposable(s, onAccept, data)
is IncomingAccepting -> PeerPullTermsComposable(s, onAccept, data)
+ is IncomingTerms -> PeerPullTermsComposable(s, onAccept, data)
+ is IncomingError -> PeerPullErrorComposable(s)
IncomingAccepted -> {
// we navigate away, don't show anything
}
- is IncomingError -> PeerPullErrorComposable(s)
}
}
}
@@ -116,36 +121,46 @@ fun ColumnScope.PeerPullTermsComposable(
.padding(16.dp)
.align(CenterHorizontally),
text = terms.contractTerms.summary,
- style = MaterialTheme.typography.h5,
+ style = MaterialTheme.typography.headlineSmall,
)
Spacer(modifier = Modifier.weight(1f))
Card(modifier = Modifier.fillMaxWidth()) {
Column(
- modifier = Modifier.padding(8.dp)
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
) {
Row(
modifier = Modifier.align(End),
) {
Text(
text = stringResource(id = R.string.payment_label_amount_total),
- style = MaterialTheme.typography.body1,
+ style = MaterialTheme.typography.bodyLarge,
)
Text(
modifier = Modifier.padding(start = 8.dp),
text = terms.contractTerms.amount.toString(),
- style = MaterialTheme.typography.body1,
+ style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
)
}
- val fee =
- Amount.zero(terms.amount.currency) // terms.amount - terms.contractTerms.amount
- if (!fee.isZero()) {
- Text(
- modifier = Modifier.align(End),
- text = stringResource(id = R.string.payment_fee, fee),
- style = MaterialTheme.typography.body1,
- )
+ // this gets used for credit and debit, so fee calculation differs
+ val fee = if (data.isCredit) {
+ terms.amountRaw - terms.amountEffective
+ } else {
+ terms.amountEffective - terms.amountRaw
+ }
+ val feeStr = if (data.isCredit) {
+ stringResource(R.string.amount_negative, fee)
+ } else {
+ stringResource(R.string.amount_positive, fee)
}
+ if (!fee.isZero()) Text(
+ modifier = Modifier.align(End),
+ text = feeStr,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.error,
+ )
if (terms is IncomingAccepting) {
CircularProgressIndicator(
modifier = Modifier
@@ -158,7 +173,7 @@ fun ColumnScope.PeerPullTermsComposable(
.align(End)
.padding(top = 8.dp),
colors = ButtonDefaults.buttonColors(
- backgroundColor = colorResource(R.color.green),
+ containerColor = colorResource(R.color.green),
contentColor = Color.White,
),
onClick = { onAccept(terms) },
@@ -174,13 +189,19 @@ fun ColumnScope.PeerPullTermsComposable(
@Composable
fun ColumnScope.PeerPullErrorComposable(s: IncomingError) {
+ val message = when (s.info.code) {
+ WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE -> stringResource(R.string.payment_balance_insufficient)
+ WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE -> stringResource(R.string.payment_balance_insufficient)
+ else -> s.info.userFacingMsg
+ }
+
Text(
modifier = Modifier
.align(CenterHorizontally)
.padding(horizontal = 32.dp),
- text = s.info.userFacingMsg,
- style = MaterialTheme.typography.h5,
- color = colorResource(id = R.color.red),
+ text = message,
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.error,
)
}
@@ -199,10 +220,11 @@ fun PeerPullCheckingPreview() {
fun PeerPullTermsPreview() {
Surface {
val terms = IncomingTerms(
- amount = Amount.fromDouble("TESTKUDOS", 42.23),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.423"),
contractTerms = PeerContractTerms(
summary = "This is a long test summary that can be more than one line long for sure",
- amount = Amount.fromDouble("TESTKUDOS", 23.42),
+ amount = Amount.fromString("TESTKUDOS", "23.42"),
),
id = "ID123",
)
@@ -218,10 +240,11 @@ fun PeerPullTermsPreview() {
fun PeerPullAcceptingPreview() {
Surface {
val terms = IncomingTerms(
- amount = Amount.fromDouble("TESTKUDOS", 42.23),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.123"),
contractTerms = PeerContractTerms(
summary = "This is a long test summary that can be more than one line long for sure",
- amount = Amount.fromDouble("TESTKUDOS", 23.42),
+ amount = Amount.fromString("TESTKUDOS", "23.42"),
),
id = "ID123",
)
@@ -237,7 +260,11 @@ fun PeerPullAcceptingPreview() {
fun PeerPullPayErrorPreview() {
Surface {
@SuppressLint("UnrememberedMutableState")
- val s = mutableStateOf(IncomingError(TalerErrorInfo(WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "msg")))
+ val s = mutableStateOf(
+ IncomingError(
+ info = TalerErrorInfo(WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "msg"),
+ )
+ )
IncomingComposable(s, incomingPush) {}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt
index 6a74c30..df71c72 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt
@@ -25,10 +25,12 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
class IncomingPullPaymentFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
@@ -43,6 +45,12 @@ class IncomingPullPaymentFragment : Fragment() {
peerManager.incomingPullState.collect {
if (it is IncomingAccepted) {
findNavController().navigate(R.id.action_promptPullPayment_to_nav_main)
+ } else if (it is IncomingError) {
+ if (model.devMode.value == true) {
+ showError(it.info)
+ } else {
+ showError(it.info.userFacingMsg)
+ }
}
}
}
@@ -51,7 +59,7 @@ class IncomingPullPaymentFragment : Fragment() {
TalerSurface {
val state = peerManager.incomingPullState.collectAsStateLifecycleAware()
IncomingComposable(state, incomingPull) { terms ->
- peerManager.acceptPeerPullPayment(terms)
+ peerManager.confirmPeerPullDebit(terms)
}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt
index 56734e3..ced2b82 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt
@@ -25,10 +25,12 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
class IncomingPushPaymentFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
@@ -43,6 +45,12 @@ class IncomingPushPaymentFragment : Fragment() {
peerManager.incomingPushState.collect {
if (it is IncomingAccepted) {
findNavController().navigate(R.id.action_promptPushPayment_to_nav_main)
+ } else if (it is IncomingError) {
+ if (model.devMode.value == true) {
+ showError(it.info)
+ } else {
+ showError(it.info.userFacingMsg)
+ }
}
}
}
@@ -51,7 +59,7 @@ class IncomingPushPaymentFragment : Fragment() {
TalerSurface {
val state = peerManager.incomingPushState.collectAsStateLifecycleAware()
IncomingComposable(state, incomingPush) { terms ->
- peerManager.acceptPeerPushPayment(terms)
+ peerManager.confirmPeerPushCredit(terms)
}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt
index 7ca38c4..cd5b5dd 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt
@@ -23,13 +23,14 @@ import net.taler.wallet.backend.TalerErrorInfo
sealed class IncomingState
object IncomingChecking : IncomingState()
open class IncomingTerms(
- val amount: Amount,
+ val amountRaw: Amount,
+ val amountEffective: Amount,
val contractTerms: PeerContractTerms,
val id: String,
) : IncomingState()
class IncomingAccepting(s: IncomingTerms) :
- IncomingTerms(s.amount, s.contractTerms, s.id)
+ IncomingTerms(s.amountRaw, s.amountEffective, s.contractTerms, s.id)
object IncomingAccepted : IncomingState()
data class IncomingError(
@@ -43,15 +44,17 @@ data class PeerContractTerms(
)
@Serializable
-data class CheckPeerPullPaymentResponse(
- val amount: Amount,
+data class PreparePeerPullDebitResponse(
val contractTerms: PeerContractTerms,
- val peerPullPaymentIncomingId: String,
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+ val transactionId: String,
)
@Serializable
-data class CheckPeerPushPaymentResponse(
- val amount: Amount,
+data class PreparePeerPushCreditResponse(
val contractTerms: PeerContractTerms,
- val peerPushPaymentIncomingId: String,
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+ val transactionId: String,
)
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt
new file mode 100644
index 0000000..90b520e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt
@@ -0,0 +1,280 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.serialization.json.JsonPrimitive
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import kotlin.random.Random
+
+@Composable
+fun OutgoingPullComposable(
+ amount: Amount,
+ state: OutgoingState,
+ onCreateInvoice: (amount: Amount, subject: String, hours: Long, exchange: ExchangeItem) -> Unit,
+ onClose: () -> Unit,
+) {
+ when(state) {
+ is OutgoingChecking, is OutgoingCreating, is OutgoingResponse -> PeerCreatingComposable()
+ is OutgoingIntro, is OutgoingChecked -> OutgoingPullIntroComposable(
+ amount = amount,
+ state = state,
+ onCreateInvoice = onCreateInvoice,
+ )
+ is OutgoingError -> PeerErrorComposable(state, onClose)
+ }
+}
+
+@Composable
+fun PeerCreatingComposable() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(32.dp)
+ .align(Center),
+ )
+ }
+}
+
+@Composable
+fun OutgoingPullIntroComposable(
+ amount: Amount,
+ state: OutgoingState,
+ onCreateInvoice: (amount: Amount, subject: String, hours: Long, exchange: ExchangeItem) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ var subject by rememberSaveable { mutableStateOf("") }
+ val focusRequester = remember { FocusRequester() }
+
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(focusRequester),
+ singleLine = true,
+ value = subject,
+ onValueChange = { input ->
+ if (input.length <= MAX_LENGTH_SUBJECT)
+ subject = input.replace('\n', ' ')
+ },
+ isError = subject.isBlank(),
+ label = {
+ Text(
+ stringResource(R.string.send_peer_purpose),
+ color = if (subject.isBlank()) {
+ MaterialTheme.colorScheme.error
+ } else Color.Unspecified,
+ )
+ }
+ )
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 5.dp),
+ color = if (subject.isBlank()) MaterialTheme.colorScheme.error else Color.Unspecified,
+ text = stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT),
+ textAlign = TextAlign.End,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_chosen),
+ amount = amount,
+ amountType = AmountType.Positive,
+ )
+
+ if (state is OutgoingChecked) {
+ val fee = state.amountRaw - state.amountEffective
+ if (!fee.isZero()) TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(amount.spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ val exchangeItem = (state as? OutgoingChecked)?.exchangeItem
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.withdraw_exchange),
+ info = if (exchangeItem == null) "" else cleanExchange(exchangeItem.exchangeBaseUrl),
+ )
+
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = stringResource(R.string.send_peer_expiration_period),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+
+ var option by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY) }
+ var hours by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY.hours) }
+ ExpirationComposable(
+ modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
+ option = option,
+ hours = hours,
+ onOptionChange = { option = it }
+ ) { hours = it }
+
+ Button(
+ modifier = Modifier.padding(16.dp),
+ enabled = subject.isNotBlank() && state is OutgoingChecked,
+ onClick = {
+ onCreateInvoice(
+ amount,
+ subject,
+ hours,
+ exchangeItem ?: error("clickable without exchange")
+ )
+ },
+ ) {
+ Text(text = stringResource(R.string.receive_peer_create_button))
+ }
+ }
+}
+
+@Composable
+fun PeerErrorComposable(state: OutgoingError, onClose: () -> Unit) {
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ Text(
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodyLarge,
+ text = state.info.userFacingMsg,
+ )
+
+ Button(
+ modifier = Modifier.padding(16.dp),
+ onClick = onClose,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error,
+ contentColor = MaterialTheme.colorScheme.onError,
+ ),
+ ) {
+ Text(text = stringResource(R.string.close))
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullComposableCreatingPreview() {
+ TalerSurface {
+ OutgoingPullComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = OutgoingCreating,
+ onCreateInvoice = { _, _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullComposableCheckingPreview() {
+ TalerSurface {
+ OutgoingPullComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = if (Random.nextBoolean()) OutgoingIntro else OutgoingChecking,
+ onCreateInvoice = { _, _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullComposableCheckedPreview() {
+ TalerSurface {
+ val amountRaw = Amount.fromString("TESTKUDOS", "42.42")
+ val amountEffective = Amount.fromString("TESTKUDOS", "42.23")
+ val exchangeItem = ExchangeItem("https://example.org", "TESTKUDOS", emptyList())
+ OutgoingPullComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = OutgoingChecked(amountRaw, amountEffective, exchangeItem),
+ onCreateInvoice = { _, _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullComposableErrorPreview() {
+ TalerSurface {
+ val json = mapOf("foo" to JsonPrimitive("bar"))
+ val state = OutgoingError(TalerErrorInfo(TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "message", json))
+ OutgoingPullComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = state,
+ onCreateInvoice = { _, _, _, _ -> },
+ onClose = {},
+ )
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
index b9634d5..8f2fb96 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
@@ -23,18 +23,24 @@ import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
-import androidx.navigation.findNavController
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.findNavController
+import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.showError
class OutgoingPullFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
- private val exchangeManager get() = model.exchangeManager
private val peerManager get() = model.peerManager
+ private val transactionManager get() = model.transactionManager
+ private val balanceManager get() = model.balanceManager
override fun onCreateView(
inflater: LayoutInflater,
@@ -44,23 +50,41 @@ class OutgoingPullFragment : Fragment() {
val amount = arguments?.getString("amount")?.let {
Amount.fromJSONString(it)
} ?: error("no amount passed")
- val exchangeFlow = exchangeManager.findExchangeForCurrency(amount.currency)
+ val scopeInfo = transactionManager.selectedScope
+ val spec = scopeInfo?.let { balanceManager.getSpecForScopeInfo(it) }
+
return ComposeView(requireContext()).apply {
setContent {
TalerSurface {
- val state = peerManager.pullState.collectAsStateLifecycleAware()
- if (state.value is OutgoingIntro) {
- val exchangeState =
- exchangeFlow.collectAsStateLifecycleAware(initial = null)
- OutgoingPullIntroComposable(
- amount = amount,
- exchangeState = exchangeState,
- onCreateInvoice = this@OutgoingPullFragment::onCreateInvoice,
- )
- } else {
- OutgoingPullResultComposable(state.value) {
- findNavController().popBackStack()
+ val state = peerManager.pullState.collectAsStateLifecycleAware().value
+ OutgoingPullComposable(
+ amount = amount.withSpec(spec),
+ state = state,
+ onCreateInvoice = this@OutgoingPullFragment::onCreateInvoice,
+ onClose = {
+ findNavController().navigate(R.id.action_nav_peer_pull_to_nav_main)
}
+ )
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ peerManager.pullState.collect {
+ if (it is OutgoingResponse) {
+ if (transactionManager.selectTransaction(it.transactionId)) {
+ findNavController().navigate(R.id.action_nav_peer_pull_to_nav_transactions_detail_peer)
+ } else {
+ findNavController().navigate(R.id.action_nav_peer_pull_to_nav_main)
+ }
+ }
+
+ if (it is OutgoingError && model.devMode.value == true) {
+ showError(it.info)
}
}
}
@@ -77,7 +101,7 @@ class OutgoingPullFragment : Fragment() {
if (!requireActivity().isChangingConfigurations) peerManager.resetPullPayment()
}
- private fun onCreateInvoice(amount: Amount, summary: String, exchange: ExchangeItem) {
- peerManager.initiatePullPayment(amount, summary, exchange)
+ private fun onCreateInvoice(amount: Amount, summary: String, hours: Long, exchange: ExchangeItem) {
+ peerManager.initiatePeerPullCredit(amount, summary, hours, exchange)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt
deleted file mode 100644
index 9af3f5d..0000000
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.peer
-
-import android.annotation.SuppressLint
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment.Companion.CenterHorizontally
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import net.taler.common.Amount
-import net.taler.wallet.R
-import net.taler.wallet.cleanExchange
-import net.taler.wallet.exchanges.ExchangeItem
-
-@Composable
-fun OutgoingPullIntroComposable(
- amount: Amount,
- exchangeState: State<ExchangeItem?>,
- onCreateInvoice: (amount: Amount, subject: String, exchange: ExchangeItem) -> Unit,
-) {
- val scrollState = rememberScrollState()
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .verticalScroll(scrollState),
- horizontalAlignment = CenterHorizontally,
- ) {
- var subject by rememberSaveable { mutableStateOf("") }
- val focusRequester = remember { FocusRequester() }
- val exchangeItem = exchangeState.value
- OutlinedTextField(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp, start = 16.dp, end = 16.dp)
- .focusRequester(focusRequester),
- singleLine = true,
- value = subject,
- onValueChange = { input ->
- if (input.length <= MAX_LENGTH_SUBJECT)
- subject = input
- },
- isError = subject.isBlank(),
- label = {
- Text(
- stringResource(R.string.send_peer_purpose),
- color = if (subject.isBlank()) {
- colorResource(R.color.red)
- } else Color.Unspecified,
- )
- }
- )
- LaunchedEffect(Unit) {
- focusRequester.requestFocus()
- }
- Text(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 5.dp, end = 16.dp),
- color = if (subject.isBlank()) colorResource(R.color.red) else Color.Unspecified,
- text = stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT),
- textAlign = TextAlign.End,
- )
- Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- text = stringResource(id = R.string.amount_chosen),
- )
- Text(
- modifier = Modifier.padding(16.dp),
- fontSize = 24.sp,
- color = colorResource(R.color.green),
- text = amount.toString(),
- )
- Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- text = stringResource(R.string.withdraw_exchange),
- )
- Text(
- modifier = Modifier.padding(16.dp),
- fontSize = 24.sp,
- text = if (exchangeItem == null) "" else cleanExchange(exchangeItem.exchangeBaseUrl),
- )
- Button(
- modifier = Modifier.padding(16.dp),
- enabled = subject.isNotBlank() && exchangeItem != null,
- onClick = {
- onCreateInvoice(amount, subject, exchangeItem ?: error("clickable without exchange"))
- },
- ) {
- Text(text = stringResource(R.string.receive_peer_create_button))
- }
- }
-}
-
-@Preview
-@Composable
-fun PreviewReceiveFundsIntro() {
- Surface {
- @SuppressLint("UnrememberedMutableState")
- val exchangeFlow =
- mutableStateOf(ExchangeItem("https://example.org", "TESTKUDOS", emptyList()))
- OutgoingPullIntroComposable(Amount.fromDouble("TESTKUDOS", 42.23), exchangeFlow) { _, _, _ -> }
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.kt
deleted file mode 100644
index 84d9415..0000000
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.peer
-
-import android.content.res.Configuration.UI_MODE_NIGHT_YES
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment.Companion.CenterHorizontally
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import net.taler.common.QrCodeManager
-import net.taler.wallet.R
-import net.taler.wallet.backend.TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED
-import net.taler.wallet.backend.TalerErrorInfo
-import net.taler.wallet.compose.QrCodeUriComposable
-import net.taler.wallet.compose.TalerSurface
-import net.taler.wallet.compose.getQrCodeSize
-import org.json.JSONObject
-
-@Composable
-fun OutgoingPullResultComposable(state: OutgoingState, onClose: () -> Unit) {
- val scrollState = rememberScrollState()
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .verticalScroll(scrollState),
- ) {
- Text(
- modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
- style = MaterialTheme.typography.h6,
- text = stringResource(id = R.string.receive_peer_invoice_instruction),
- )
- when (state) {
- OutgoingIntro -> error("Result composable with PullPaymentIntro")
- is OutgoingCreating -> PeerPullCreatingComposable()
- is OutgoingResponse -> PeerPullResponseComposable(state)
- is OutgoingError -> PeerPullErrorComposable(state)
- }
- Button(modifier = Modifier
- .padding(16.dp)
- .align(CenterHorizontally),
- onClick = onClose) {
- Text(text = stringResource(R.string.close))
- }
- }
-}
-
-@Composable
-private fun ColumnScope.PeerPullCreatingComposable() {
- val qrCodeSize = getQrCodeSize()
- CircularProgressIndicator(
- modifier = Modifier
- .padding(32.dp)
- .size(qrCodeSize)
- .align(CenterHorizontally),
- )
-}
-
-@Composable
-private fun ColumnScope.PeerPullResponseComposable(state: OutgoingResponse) {
- QrCodeUriComposable(
- talerUri = state.talerUri,
- clipBoardLabel = "Invoice",
- ) {
- Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- style = MaterialTheme.typography.body1,
- text = stringResource(id = R.string.receive_peer_invoice_uri),
- )
- }
-}
-
-@Composable
-private fun ColumnScope.PeerPullErrorComposable(state: OutgoingError) {
- Text(
- modifier = Modifier
- .align(CenterHorizontally)
- .padding(16.dp),
- color = colorResource(R.color.red),
- style = MaterialTheme.typography.body1,
- text = state.info.userFacingMsg,
- )
-}
-
-@Preview
-@Composable
-fun PeerPullCreatingPreview() {
- Surface {
- OutgoingPullResultComposable(OutgoingCreating) {}
- }
-}
-
-@Preview(uiMode = UI_MODE_NIGHT_YES)
-@Composable
-fun PeerPullResponsePreview() {
- TalerSurface {
- val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen"
- val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri))
- OutgoingPullResultComposable(response) {}
- }
-}
-
-@Preview(widthDp = 720, uiMode = UI_MODE_NIGHT_YES)
-@Composable
-fun PeerPullResponseLandscapePreview() {
- TalerSurface {
- val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen"
- val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri))
- OutgoingPullResultComposable(response) {}
- }
-}
-
-@Preview
-@Composable
-fun PeerPullErrorPreview() {
- Surface {
- val json = JSONObject().apply { put("foo", "bar") }
- val response = OutgoingError(TalerErrorInfo(WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "message", json))
- OutgoingPullResultComposable(response) {}
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt
new file mode 100644
index 0000000..d39fdc8
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt
@@ -0,0 +1,218 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.serialization.json.JsonPrimitive
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.TalerSurface
+import kotlin.random.Random
+
+@Composable
+fun OutgoingPushComposable(
+ state: OutgoingState,
+ amount: Amount,
+ onSend: (amount: Amount, summary: String, hours: Long) -> Unit,
+ onClose: () -> Unit,
+) {
+ when(state) {
+ is OutgoingChecking, is OutgoingCreating, is OutgoingResponse -> PeerCreatingComposable()
+ is OutgoingIntro, is OutgoingChecked -> OutgoingPushIntroComposable(
+ amount = amount,
+ state = state,
+ onSend = onSend,
+ )
+ is OutgoingError -> PeerErrorComposable(state, onClose)
+ }
+}
+
+@Composable
+fun OutgoingPushIntroComposable(
+ state: OutgoingState,
+ amount: Amount,
+ onSend: (amount: Amount, summary: String, hours: Long) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier.padding(vertical = 16.dp),
+ text = amount.toString(),
+ softWrap = false,
+ style = MaterialTheme.typography.titleLarge,
+ )
+
+ if (state is OutgoingChecked) {
+ val fee = state.amountEffective - state.amountRaw
+ Text(
+ modifier = Modifier.padding(vertical = 16.dp),
+ text = stringResource(id = R.string.payment_fee, fee.withSpec(amount.spec)),
+ softWrap = false,
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+
+ var subject by rememberSaveable { mutableStateOf("") }
+ val focusRequester = remember { FocusRequester() }
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(focusRequester),
+ singleLine = true,
+ value = subject,
+ onValueChange = { input ->
+ if (input.length <= MAX_LENGTH_SUBJECT)
+ subject = input.replace('\n', ' ')
+ },
+ isError = subject.isBlank(),
+ label = {
+ Text(
+ stringResource(R.string.send_peer_purpose),
+ color = if (subject.isBlank()) {
+ MaterialTheme.colorScheme.error
+ } else Color.Unspecified,
+ )
+ }
+ )
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 5.dp),
+ color = if (subject.isBlank()) MaterialTheme.colorScheme.error else Color.Unspecified,
+ text = stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT),
+ textAlign = TextAlign.End,
+ )
+
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = stringResource(R.string.send_peer_expiration_period),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+
+ var option by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY) }
+ var hours by rememberSaveable { mutableLongStateOf(DEFAULT_EXPIRY.hours) }
+ ExpirationComposable(
+ modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
+ option = option,
+ hours = hours,
+ onOptionChange = { option = it }
+ ) { hours = it }
+
+ Button(
+ enabled = state is OutgoingChecked && subject.isNotBlank(),
+ onClick = { onSend(amount, subject, hours) },
+ ) {
+ Text(text = stringResource(R.string.send_peer_create_button))
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushComposableCreatingPreview() {
+ TalerSurface {
+ OutgoingPushComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = OutgoingCreating,
+ onSend = { _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushComposableCheckingPreview() {
+ TalerSurface {
+ val state = if (Random.nextBoolean()) OutgoingIntro else OutgoingChecking
+ OutgoingPushComposable(
+ state = state,
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ onSend = { _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushComposableCheckedPreview() {
+ TalerSurface {
+ val amountEffective = Amount.fromString("TESTKUDOS", "42.42")
+ val amountRaw = Amount.fromString("TESTKUDOS", "42.23")
+ val state = OutgoingChecked(amountRaw, amountEffective)
+ OutgoingPushComposable(
+ state = state,
+ amount = amountEffective,
+ onSend = { _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushComposableErrorPreview() {
+ TalerSurface {
+ val json = mapOf("foo" to JsonPrimitive("bar"))
+ val state = OutgoingError(TalerErrorInfo(TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "message", json))
+ OutgoingPushComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = state,
+ onSend = { _, _, _ -> },
+ onClose = {},
+ )
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
index b110876..01fb566 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
@@ -20,19 +20,35 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
+import androidx.navigation.fragment.findNavController
+import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
class OutgoingPushFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
private val peerManager get() = model.peerManager
+ private val transactionManager get() = model.transactionManager
+ private val balanceManager get() = model.balanceManager
+
+ // hacky way to change back action until we have navigation for compose
+ private val backPressedCallback = object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ findNavController().navigate(R.id.action_nav_peer_push_to_nav_main)
+ }
+ }
override fun onCreateView(
inflater: LayoutInflater,
@@ -42,20 +58,49 @@ class OutgoingPushFragment : Fragment() {
val amount = arguments?.getString("amount")?.let {
Amount.fromJSONString(it)
} ?: error("no amount passed")
+ val scopeInfo = transactionManager.selectedScope
+ val spec = scopeInfo?.let { balanceManager.getSpecForScopeInfo(it) }
+
+ requireActivity().onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner, backPressedCallback
+ )
+
return ComposeView(requireContext()).apply {
setContent {
TalerSurface {
- val state = peerManager.pushState.collectAsStateLifecycleAware()
- if (state.value is OutgoingIntro) {
- OutgoingPushIntroComposable(
- amount = amount,
- onSend = this@OutgoingPushFragment::onSend,
- )
- } else {
- OutgoingPushResultComposable(state.value) {
- findNavController().popBackStack()
+ val state = peerManager.pushState.collectAsStateLifecycleAware().value
+ OutgoingPushComposable(
+ amount = amount.withSpec(spec),
+ state = state,
+ onSend = this@OutgoingPushFragment::onSend,
+ onClose = {
+ findNavController().navigate(R.id.action_nav_peer_pull_to_nav_main)
+ }
+ )
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ peerManager.pushState.collect {
+ if (it is OutgoingResponse) {
+ if (transactionManager.selectTransaction(it.transactionId)) {
+ findNavController().navigate(R.id.action_nav_peer_push_to_nav_transactions_detail_peer)
+ } else {
+ findNavController().navigate(R.id.action_nav_peer_push_to_nav_main)
}
}
+
+ if (it is OutgoingError && model.devMode.value == true) {
+ showError(it.info)
+ }
+
+ // Disable back navigation when tx is being created
+ backPressedCallback.isEnabled = it !is OutgoingCreating
}
}
}
@@ -63,7 +108,7 @@ class OutgoingPushFragment : Fragment() {
override fun onStart() {
super.onStart()
- activity?.setTitle(R.string.receive_peer_title)
+ activity?.setTitle(R.string.send_peer_title)
}
override fun onDestroy() {
@@ -71,7 +116,7 @@ class OutgoingPushFragment : Fragment() {
if (!requireActivity().isChangingConfigurations) peerManager.resetPushPayment()
}
- private fun onSend(amount: Amount, summary: String) {
- peerManager.initiatePeerPushPayment(amount, summary)
+ private fun onSend(amount: Amount, summary: String, hours: Long) {
+ peerManager.initiatePeerPushDebit(amount, summary, hours)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt
deleted file mode 100644
index a77cfb7..0000000
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.peer
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Alignment.Companion.CenterHorizontally
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import net.taler.common.Amount
-import net.taler.wallet.R
-
-@Composable
-fun OutgoingPushIntroComposable(
- amount: Amount,
- onSend: (amount: Amount, summary: String) -> Unit,
-) {
- val scrollState = rememberScrollState()
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .verticalScroll(scrollState),
- horizontalAlignment = CenterHorizontally,
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .padding(16.dp),
- ) {
- Text(
- modifier = Modifier,
- text = amount.toString(),
- softWrap = false,
- style = MaterialTheme.typography.h6,
- )
- }
-
- var subject by rememberSaveable { mutableStateOf("") }
- OutlinedTextField(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
- singleLine = true,
- value = subject,
- onValueChange = { input ->
- if (input.length <= MAX_LENGTH_SUBJECT)
- subject = input
- },
- isError = subject.isBlank(),
- label = {
- Text(
- stringResource(R.string.send_peer_purpose),
- color = if (subject.isBlank()) {
- colorResource(R.color.red)
- } else Color.Unspecified,
- )
- }
- )
- Text(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 5.dp, end = 16.dp),
- color = if (subject.isBlank()) colorResource(R.color.red) else Color.Unspecified,
- text = stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT),
- textAlign = TextAlign.End,
- )
- Text(
- modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
- text = stringResource(R.string.send_peer_warning),
- )
- Button(
- modifier = Modifier.padding(16.dp),
- enabled = subject.isNotBlank(),
- onClick = {
- onSend(amount, subject)
- },
- ) {
- Text(text = stringResource(R.string.send_peer_create_button))
- }
- }
-}
-
-@Preview
-@Composable
-fun PeerPushIntroComposablePreview() {
- Surface {
- OutgoingPushIntroComposable(Amount.fromDouble("TESTKUDOS", 42.23)) { _, _ -> }
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.kt
deleted file mode 100644
index 27b04c8..0000000
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.peer
-
-import android.content.res.Configuration.UI_MODE_NIGHT_YES
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment.Companion.CenterHorizontally
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import net.taler.common.QrCodeManager
-import net.taler.wallet.R
-import net.taler.wallet.backend.TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED
-import net.taler.wallet.backend.TalerErrorInfo
-import net.taler.wallet.compose.QrCodeUriComposable
-import net.taler.wallet.compose.TalerSurface
-import net.taler.wallet.compose.getQrCodeSize
-import org.json.JSONObject
-
-@Composable
-fun OutgoingPushResultComposable(state: OutgoingState, onClose: () -> Unit) {
- val scrollState = rememberScrollState()
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .verticalScroll(scrollState),
- ) {
- Text(
- modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
- style = MaterialTheme.typography.h6,
- text = stringResource(id = R.string.send_peer_payment_instruction),
- )
- when (state) {
- OutgoingIntro -> error("Result composable with PullPaymentIntro")
- is OutgoingCreating -> PeerPushCreatingComposable()
- is OutgoingResponse -> PeerPushResponseComposable(state)
- is OutgoingError -> PeerPushErrorComposable(state)
- }
- Button(modifier = Modifier
- .padding(16.dp)
- .align(CenterHorizontally),
- onClick = onClose) {
- Text(text = stringResource(R.string.close))
- }
- }
-}
-
-@Composable
-private fun ColumnScope.PeerPushCreatingComposable() {
- val qrCodeSize = getQrCodeSize()
- CircularProgressIndicator(
- modifier = Modifier
- .padding(32.dp)
- .size(qrCodeSize)
- .align(CenterHorizontally),
- )
-}
-
-@Composable
-private fun ColumnScope.PeerPushResponseComposable(state: OutgoingResponse) {
- QrCodeUriComposable(
- talerUri = state.talerUri,
- clipBoardLabel = "Invoice",
- ) {
- Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- style = MaterialTheme.typography.body1,
- text = stringResource(id = R.string.receive_peer_invoice_uri),
- )
- }
-}
-
-@Composable
-private fun ColumnScope.PeerPushErrorComposable(state: OutgoingError) {
- Text(
- modifier = Modifier
- .align(CenterHorizontally)
- .padding(16.dp),
- color = colorResource(R.color.red),
- style = MaterialTheme.typography.body1,
- text = state.info.userFacingMsg,
- )
-}
-
-@Preview
-@Composable
-fun PeerPushCreatingPreview() {
- Surface {
- OutgoingPushResultComposable(OutgoingCreating) {}
- }
-}
-
-@Preview(uiMode = UI_MODE_NIGHT_YES)
-@Composable
-fun PeerPushResponsePreview() {
- TalerSurface {
- val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen"
- val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri))
- OutgoingPushResultComposable(response) {}
- }
-}
-
-@Preview(widthDp = 720, uiMode = UI_MODE_NIGHT_YES)
-@Composable
-fun PeerPushResponseLandscapePreview() {
- TalerSurface {
- val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen"
- val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri))
- OutgoingPushResultComposable(response) {}
- }
-}
-
-@Preview
-@Composable
-fun PeerPushErrorPreview() {
- Surface {
- val json = JSONObject().apply { put("foo", "bar") }
- val response = OutgoingError(TalerErrorInfo(WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "message", json))
- OutgoingPushResultComposable(response) {}
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt
index 0e01056..05da294 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt
@@ -16,16 +16,22 @@
package net.taler.wallet.peer
-import android.graphics.Bitmap
import kotlinx.serialization.Serializable
+import net.taler.common.Amount
import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.exchanges.ExchangeItem
sealed class OutgoingState
object OutgoingIntro : OutgoingState()
+object OutgoingChecking : OutgoingState()
+data class OutgoingChecked(
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+ val exchangeItem: ExchangeItem? = null,
+) : OutgoingState()
object OutgoingCreating : OutgoingState()
data class OutgoingResponse(
- val talerUri: String,
- val qrCode: Bitmap,
+ val transactionId: String,
) : OutgoingState()
data class OutgoingError(
@@ -33,15 +39,25 @@ data class OutgoingError(
) : OutgoingState()
@Serializable
+data class CheckPeerPullCreditResponse(
+ val exchangeBaseUrl: String,
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+)
+
+@Serializable
data class InitiatePeerPullPaymentResponse(
- /**
- * Taler URI for the other party to make the payment that was requested.
- */
- val talerUri: String,
+ val transactionId: String,
+)
+
+@Serializable
+data class CheckPeerPushDebitResponse(
+ val amountRaw: Amount,
+ val amountEffective: Amount,
)
@Serializable
-data class InitiatePeerPushPaymentResponse(
+data class InitiatePeerPushDebitResponse(
val exchangeBaseUrl: String,
- val talerUri: String,
+ val transactionId: String,
)
diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
index 270ca04..5bd2b0b 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
@@ -25,18 +25,22 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import net.taler.common.Amount
-import net.taler.common.QrCodeManager
import net.taler.common.Timestamp
import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorCode.UNKNOWN
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.exchanges.ExchangeManager
import org.json.JSONObject
-import java.util.concurrent.TimeUnit.DAYS
+import java.util.concurrent.TimeUnit.HOURS
const val MAX_LENGTH_SUBJECT = 100
+val DEFAULT_EXPIRY = ExpirationOption.DAYS_1
class PeerManager(
private val api: WalletBackendApi,
+ private val exchangeManager: ExchangeManager,
private val scope: CoroutineScope,
) {
@@ -52,11 +56,37 @@ class PeerManager(
private val _incomingPushState = MutableStateFlow<IncomingState>(IncomingChecking)
val incomingPushState: StateFlow<IncomingState> = _incomingPushState
- fun initiatePullPayment(amount: Amount, summary: String, exchange: ExchangeItem) {
+ fun checkPeerPullCredit(amount: Amount) {
+ _outgoingPullState.value = OutgoingChecking
+ scope.launch(Dispatchers.IO) {
+ val exchangeItem = exchangeManager.findExchange(amount.currency)
+ if (exchangeItem == null) {
+ _outgoingPullState.value = OutgoingError(
+ TalerErrorInfo(UNKNOWN, "No exchange found for ${amount.currency}")
+ )
+ return@launch
+ }
+ api.request("checkPeerPullCredit", CheckPeerPullCreditResponse.serializer()) {
+ put("exchangeBaseUrl", exchangeItem.exchangeBaseUrl)
+ put("amount", amount.toJSONString())
+ }.onSuccess {
+ _outgoingPullState.value = OutgoingChecked(
+ amountRaw = it.amountRaw,
+ amountEffective = it.amountEffective,
+ exchangeItem = exchangeItem,
+ )
+ }.onError { error ->
+ Log.e(TAG, "got checkPeerPullCredit error result $error")
+ _outgoingPullState.value = OutgoingError(error)
+ }
+ }
+ }
+
+ fun initiatePeerPullCredit(amount: Amount, summary: String, expirationHours: Long, exchange: ExchangeItem) {
_outgoingPullState.value = OutgoingCreating
scope.launch(Dispatchers.IO) {
- val expiry = Timestamp.fromMillis(System.currentTimeMillis() + DAYS.toMillis(3))
- api.request("initiatePeerPullPayment", InitiatePeerPullPaymentResponse.serializer()) {
+ val expiry = Timestamp.fromMillis(System.currentTimeMillis() + HOURS.toMillis(expirationHours))
+ api.request("initiatePeerPullCredit", InitiatePeerPullPaymentResponse.serializer()) {
put("exchangeBaseUrl", exchange.exchangeBaseUrl)
put("partialContractTerms", JSONObject().apply {
put("amount", amount.toJSONString())
@@ -64,10 +94,9 @@ class PeerManager(
put("purse_expiration", JSONObject(Json.encodeToString(expiry)))
})
}.onSuccess {
- val qrCode = QrCodeManager.makeQrCode(it.talerUri)
- _outgoingPullState.value = OutgoingResponse(it.talerUri, qrCode)
+ _outgoingPullState.value = OutgoingResponse(it.transactionId)
}.onError { error ->
- Log.e(TAG, "got initiatePeerPullPayment error result $error")
+ Log.e(TAG, "got initiatePeerPullCredit error result $error")
_outgoingPullState.value = OutgoingError(error)
}
}
@@ -77,11 +106,29 @@ class PeerManager(
_outgoingPullState.value = OutgoingIntro
}
- fun initiatePeerPushPayment(amount: Amount, summary: String) {
+ fun checkPeerPushDebit(amount: Amount) {
+ _outgoingPushState.value = OutgoingChecking
+ scope.launch(Dispatchers.IO) {
+ api.request("checkPeerPushDebit", CheckPeerPushDebitResponse.serializer()) {
+ put("amount", amount.toJSONString())
+ }.onSuccess { response ->
+ _outgoingPushState.value = OutgoingChecked(
+ amountRaw = response.amountRaw,
+ amountEffective = response.amountEffective,
+ // FIXME add exchangeItem once available in API
+ )
+ }.onError { error ->
+ Log.e(TAG, "got checkPeerPushDebit error result $error")
+ _outgoingPushState.value = OutgoingError(error)
+ }
+ }
+ }
+
+ fun initiatePeerPushDebit(amount: Amount, summary: String, expirationHours: Long) {
_outgoingPushState.value = OutgoingCreating
scope.launch(Dispatchers.IO) {
- val expiry = Timestamp.fromMillis(System.currentTimeMillis() + DAYS.toMillis(3))
- api.request("initiatePeerPushPayment", InitiatePeerPushPaymentResponse.serializer()) {
+ val expiry = Timestamp.fromMillis(System.currentTimeMillis() + HOURS.toMillis(expirationHours))
+ api.request("initiatePeerPushDebit", InitiatePeerPushDebitResponse.serializer()) {
put("amount", amount.toJSONString())
put("partialContractTerms", JSONObject().apply {
put("amount", amount.toJSONString())
@@ -89,10 +136,9 @@ class PeerManager(
put("purse_expiration", JSONObject(Json.encodeToString(expiry)))
})
}.onSuccess { response ->
- val qrCode = QrCodeManager.makeQrCode(response.talerUri)
- _outgoingPushState.value = OutgoingResponse(response.talerUri, qrCode)
+ _outgoingPushState.value = OutgoingResponse(response.transactionId)
}.onError { error ->
- Log.e(TAG, "got initiatePeerPushPayment error result $error")
+ Log.e(TAG, "got initiatePeerPushDebit error result $error")
_outgoingPushState.value = OutgoingError(error)
}
}
@@ -102,65 +148,67 @@ class PeerManager(
_outgoingPushState.value = OutgoingIntro
}
- fun checkPeerPullPayment(talerUri: String) {
+ fun preparePeerPullDebit(talerUri: String) {
_incomingPullState.value = IncomingChecking
scope.launch(Dispatchers.IO) {
- api.request("checkPeerPullPayment", CheckPeerPullPaymentResponse.serializer()) {
+ api.request("preparePeerPullDebit", PreparePeerPullDebitResponse.serializer()) {
put("talerUri", talerUri)
}.onSuccess { response ->
_incomingPullState.value = IncomingTerms(
- amount = response.amount,
+ amountRaw = response.amountRaw,
+ amountEffective = response.amountEffective,
contractTerms = response.contractTerms,
- id = response.peerPullPaymentIncomingId,
+ id = response.transactionId,
)
}.onError { error ->
- Log.e(TAG, "got checkPeerPushPayment error result $error")
+ Log.e(TAG, "got preparePeerPullDebit error result $error")
_incomingPullState.value = IncomingError(error)
}
}
}
- fun acceptPeerPullPayment(terms: IncomingTerms) {
+ fun confirmPeerPullDebit(terms: IncomingTerms) {
_incomingPullState.value = IncomingAccepting(terms)
scope.launch(Dispatchers.IO) {
- api.request<Unit>("acceptPeerPullPayment") {
- put("peerPullPaymentIncomingId", terms.id)
+ api.request<Unit>("confirmPeerPullDebit") {
+ put("transactionId", terms.id)
}.onSuccess {
_incomingPullState.value = IncomingAccepted
}.onError { error ->
- Log.e(TAG, "got checkPeerPushPayment error result $error")
+ Log.e(TAG, "got confirmPeerPullDebit error result $error")
_incomingPullState.value = IncomingError(error)
}
}
}
- fun checkPeerPushPayment(talerUri: String) {
+ fun preparePeerPushCredit(talerUri: String) {
_incomingPushState.value = IncomingChecking
scope.launch(Dispatchers.IO) {
- api.request("checkPeerPushPayment", CheckPeerPushPaymentResponse.serializer()) {
+ api.request("preparePeerPushCredit", PreparePeerPushCreditResponse.serializer()) {
put("talerUri", talerUri)
}.onSuccess { response ->
_incomingPushState.value = IncomingTerms(
- amount = response.amount,
+ amountRaw = response.amountRaw,
+ amountEffective = response.amountEffective,
contractTerms = response.contractTerms,
- id = response.peerPushPaymentIncomingId,
+ id = response.transactionId,
)
}.onError { error ->
- Log.e(TAG, "got checkPeerPushPayment error result $error")
+ Log.e(TAG, "got preparePeerPushCredit error result $error")
_incomingPushState.value = IncomingError(error)
}
}
}
- fun acceptPeerPushPayment(terms: IncomingTerms) {
+ fun confirmPeerPushCredit(terms: IncomingTerms) {
_incomingPushState.value = IncomingAccepting(terms)
scope.launch(Dispatchers.IO) {
- api.request<Unit>("acceptPeerPushPayment") {
- put("peerPushPaymentIncomingId", terms.id)
+ api.request<Unit>("confirmPeerPushCredit") {
+ put("transactionId", terms.id)
}.onSuccess {
_incomingPushState.value = IncomingAccepted
}.onError { error ->
- Log.e(TAG, "got checkPeerPushPayment error result $error")
+ Log.e(TAG, "got confirmPeerPushCredit error result $error")
_incomingPushState.value = IncomingError(error)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
index 92264c7..3b15b6f 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
@@ -17,82 +17,89 @@
package net.taler.wallet.peer
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
import net.taler.common.Timestamp
import net.taler.wallet.R
-import net.taler.wallet.compose.QrCodeUriComposable
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.transactions.AmountType
import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionMinorState.CreatePurse
+import net.taler.wallet.transactions.TransactionMinorState.Ready
import net.taler.wallet.transactions.TransactionPeerComposable
import net.taler.wallet.transactions.TransactionPeerPullCredit
+import net.taler.wallet.transactions.TransactionState
@Composable
-fun ColumnScope.TransactionPeerPullCreditComposable(t: TransactionPeerPullCredit) {
- TransactionAmountComposable(
- label = stringResource(id = R.string.receive_amount),
- amount = t.amountEffective,
- amountType = AmountType.Positive,
+fun ColumnScope.TransactionPeerPullCreditComposable(t: TransactionPeerPullCredit, spec: CurrencySpecification?) {
+ if (t.error == null) PeerQrCode(
+ state = t.txState,
+ talerUri = t.talerUri,
)
+
TransactionAmountComposable(
- label = stringResource(id = R.string.amount_chosen),
- amount = t.amountRaw,
+ label = stringResource(id = R.string.receive_peer_amount_invoiced),
+ amount = t.amountRaw.withSpec(spec),
amountType = AmountType.Neutral,
)
+
val fee = t.amountRaw - t.amountEffective
if (!fee.isZero()) {
TransactionAmountComposable(
label = stringResource(id = R.string.withdraw_fees),
- amount = fee,
+ amount = fee.withSpec(spec),
amountType = AmountType.Negative,
)
}
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_received),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Positive,
+ )
+
TransactionInfoComposable(
label = stringResource(id = R.string.send_peer_purpose),
info = t.info.summary ?: "",
)
- if (t.pending) {
- QrCodeUriComposable(
- talerUri = t.talerUri,
- clipBoardLabel = "Invoice",
- buttonText = stringResource(id = R.string.copy),
- ) {
- Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- style = MaterialTheme.typography.body1,
- text = stringResource(id = R.string.receive_peer_invoice_uri),
- )
- }
- }
}
@Preview
@Composable
-fun TransactionPeerPullCreditPreview() {
+fun TransactionPeerPullCreditPreview(loading: Boolean = false) {
val t = TransactionPeerPullCredit(
transactionId = "transactionId",
timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
- pending = true,
+ txState = TransactionState(Pending, if (loading) CreatePurse else Ready),
+ txActions = listOf(Retry, Suspend, Abort),
exchangeBaseUrl = "https://exchange.example.org/",
- amountRaw = Amount.fromDouble("TESTKUDOS", 42.23),
- amountEffective = Amount.fromDouble("TESTKUDOS", 42.1337),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
info = PeerInfoShort(
expiration = Timestamp.fromMillis(System.currentTimeMillis() + 60 * 60 * 1000),
summary = "test invoice",
),
talerUri = "https://exchange.example.org/peer/pull/credit",
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
)
Surface {
- TransactionPeerComposable(t) {}
+ TransactionPeerComposable(t, true, null) {}
}
}
+
+@Preview
+@Composable
+fun TransactionPeerPullCreditLoadingPreview() {
+ TransactionPeerPullCreditPreview(loading = true)
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt
index d09a659..dadff4a 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt
@@ -16,40 +16,51 @@
package net.taler.wallet.peer
-import androidx.compose.material.Surface
+import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
import net.taler.common.Timestamp
import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.transactions.AmountType
import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
import net.taler.wallet.transactions.TransactionPeerComposable
import net.taler.wallet.transactions.TransactionPeerPullDebit
+import net.taler.wallet.transactions.TransactionState
@Composable
-fun TransactionPeerPullDebitComposable(t: TransactionPeerPullDebit) {
- TransactionAmountComposable(
- label = stringResource(id = R.string.transaction_paid),
- amount = t.amountEffective,
- amountType = AmountType.Negative,
- )
+fun TransactionPeerPullDebitComposable(t: TransactionPeerPullDebit, spec: CurrencySpecification?) {
TransactionAmountComposable(
label = stringResource(id = R.string.transaction_order_total),
- amount = t.amountRaw,
+ amount = t.amountRaw.withSpec(spec),
amountType = AmountType.Neutral,
)
+
val fee = t.amountEffective - t.amountRaw
if (!fee.isZero()) {
TransactionAmountComposable(
label = stringResource(id = R.string.withdraw_fees),
- amount = fee,
+ amount = fee.withSpec(spec),
amountType = AmountType.Negative,
)
}
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_paid),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
TransactionInfoComposable(
label = stringResource(id = R.string.send_peer_purpose),
info = t.info.summary ?: "",
@@ -62,16 +73,18 @@ fun TransactionPeerPullDebitPreview() {
val t = TransactionPeerPullDebit(
transactionId = "transactionId",
timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
- pending = true,
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
exchangeBaseUrl = "https://exchange.example.org/",
- amountRaw = Amount.fromDouble("TESTKUDOS", 42.1337),
- amountEffective = Amount.fromDouble("TESTKUDOS", 42.23),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.1337"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.23"),
info = PeerInfoShort(
expiration = Timestamp.fromMillis(System.currentTimeMillis() + 60 * 60 * 1000),
summary = "test invoice",
),
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
)
Surface {
- TransactionPeerComposable(t) {}
+ TransactionPeerComposable(t, true, null) {}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt
index e918c42..dbf0fb9 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt
@@ -16,40 +16,51 @@
package net.taler.wallet.peer
-import androidx.compose.material.Surface
+import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
import net.taler.common.Timestamp
import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.transactions.AmountType
import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
import net.taler.wallet.transactions.TransactionPeerComposable
import net.taler.wallet.transactions.TransactionPeerPushCredit
+import net.taler.wallet.transactions.TransactionState
@Composable
-fun TransactionPeerPushCreditComposable(t: TransactionPeerPushCredit) {
+fun TransactionPeerPushCreditComposable(t: TransactionPeerPushCredit, spec: CurrencySpecification?) {
TransactionAmountComposable(
- label = stringResource(id = R.string.send_peer_payment_amount_received),
- amount = t.amountEffective,
- amountType = AmountType.Positive,
- )
- TransactionAmountComposable(
- label = stringResource(id = R.string.send_peer_payment_amount_sent),
- amount = t.amountRaw,
+ label = stringResource(id = R.string.amount_sent),
+ amount = t.amountRaw.withSpec(spec),
amountType = AmountType.Neutral,
)
+
val fee = t.amountRaw - t.amountEffective
if (!fee.isZero()) {
TransactionAmountComposable(
label = stringResource(id = R.string.withdraw_fees),
- amount = fee,
+ amount = fee.withSpec(spec),
amountType = AmountType.Negative,
)
}
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_received),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Positive,
+ )
+
TransactionInfoComposable(
label = stringResource(id = R.string.send_peer_purpose),
info = t.info.summary ?: "",
@@ -62,16 +73,18 @@ fun TransactionPeerPushCreditPreview() {
val t = TransactionPeerPushCredit(
transactionId = "transactionId",
timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
- pending = true,
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
exchangeBaseUrl = "https://exchange.example.org/",
- amountRaw = Amount.fromDouble("TESTKUDOS", 42.23),
- amountEffective = Amount.fromDouble("TESTKUDOS", 42.1337),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
info = PeerInfoShort(
expiration = Timestamp.fromMillis(System.currentTimeMillis() + 60 * 60 * 1000),
summary = "test invoice",
),
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
)
Surface {
- TransactionPeerComposable(t) {}
+ TransactionPeerComposable(t, true, null) {}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
index 88c27fe..e592c3e 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
@@ -18,79 +18,135 @@ package net.taler.wallet.peer
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
import net.taler.common.Timestamp
import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.compose.QrCodeUriComposable
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.getQrCodeSize
import net.taler.wallet.transactions.AmountType
import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionMinorState.CreatePurse
+import net.taler.wallet.transactions.TransactionMinorState.Ready
import net.taler.wallet.transactions.TransactionPeerComposable
import net.taler.wallet.transactions.TransactionPeerPushDebit
+import net.taler.wallet.transactions.TransactionState
@Composable
-fun ColumnScope.TransactionPeerPushDebitComposable(t: TransactionPeerPushDebit) {
- TransactionAmountComposable(
- label = stringResource(id = R.string.transaction_paid),
- amount = t.amountEffective,
- amountType = AmountType.Negative,
+fun ColumnScope.TransactionPeerPushDebitComposable(t: TransactionPeerPushDebit, spec: CurrencySpecification?) {
+ if (t.error == null) PeerQrCode(
+ state = t.txState,
+ talerUri = t.talerUri,
)
+
TransactionAmountComposable(
label = stringResource(id = R.string.transaction_order_total),
- amount = t.amountRaw,
+ amount = t.amountRaw.withSpec(spec),
amountType = AmountType.Neutral,
)
+
val fee = t.amountEffective - t.amountRaw
if (!fee.isZero()) {
TransactionAmountComposable(
label = stringResource(id = R.string.withdraw_fees),
- amount = fee,
+ amount = fee.withSpec(spec),
amountType = AmountType.Negative,
)
}
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_paid),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
TransactionInfoComposable(
label = stringResource(id = R.string.send_peer_purpose),
info = t.info.summary ?: "",
)
- QrCodeUriComposable(
- talerUri = t.talerUri,
- clipBoardLabel = "Push payment",
- buttonText = stringResource(id = R.string.copy),
- ) {
+}
+
+@Composable
+fun ColumnScope.PeerQrCode(state: TransactionState, talerUri: String?) {
+ if (state == TransactionState(Pending)) {
Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- style = MaterialTheme.typography.body1,
- text = stringResource(id = R.string.receive_peer_invoice_uri),
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ style = MaterialTheme.typography.titleLarge,
+ text = stringResource(id = R.string.send_peer_payment_instruction),
+ textAlign = TextAlign.Center,
)
+
+ if (state.minor == Ready && talerUri != null) {
+ QrCodeUriComposable(
+ talerUri = talerUri,
+ clipBoardLabel = "Push payment",
+ buttonText = stringResource(id = R.string.copy),
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ style = MaterialTheme.typography.bodyLarge,
+ text = stringResource(id = R.string.receive_peer_invoice_uri),
+ )
+ }
+ } else {
+ val qrCodeSize = getQrCodeSize()
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(32.dp)
+ .size(qrCodeSize)
+ .align(CenterHorizontally),
+ )
+ }
}
+
}
@Preview
@Composable
-fun TransactionPeerPushDebitPreview() {
+fun TransactionPeerPushDebitPreview(loading: Boolean = false) {
val t = TransactionPeerPushDebit(
transactionId = "transactionId",
timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
- pending = true,
+ txState = TransactionState(Pending, if (loading) CreatePurse else Ready),
+ txActions = listOf(Retry, Suspend, Abort),
exchangeBaseUrl = "https://exchange.example.org/",
- amountRaw = Amount.fromDouble("TESTKUDOS", 42.1337),
- amountEffective = Amount.fromDouble("TESTKUDOS", 42.23),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.1337"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.23"),
info = PeerInfoShort(
expiration = Timestamp.fromMillis(System.currentTimeMillis() + 60 * 60 * 1000),
summary = "test invoice",
),
talerUri = "https://exchange.example.org/peer/pull/credit",
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
)
- Surface {
- TransactionPeerComposable(t) {}
+
+ TalerSurface {
+ TransactionPeerComposable(t, true, null) {}
}
}
+
+@Preview
+@Composable
+fun TransactionPeerPushDebitLoadingPreview() {
+ TransactionPeerPushDebitPreview(loading = true)
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
deleted file mode 100644
index 6bfcf90..0000000
--- a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.pending
-
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import android.view.View
-import android.view.View.GONE
-import android.view.View.VISIBLE
-import android.view.ViewGroup
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import net.taler.common.showError
-import net.taler.wallet.MainViewModel
-import net.taler.wallet.R
-import net.taler.wallet.TAG
-import net.taler.wallet.databinding.FragmentPendingOperationsBinding
-import org.json.JSONObject
-
-interface PendingOperationClickListener {
- fun onPendingOperationClick(type: String, detail: JSONObject)
- fun onPendingOperationActionClick(type: String, detail: JSONObject)
-}
-
-class PendingOperationsFragment : Fragment(), PendingOperationClickListener {
-
- private val model: MainViewModel by activityViewModels()
- private val pendingOperationsManager by lazy { model.pendingOperationsManager }
-
- private lateinit var ui: FragmentPendingOperationsBinding
- private val pendingAdapter = PendingOperationsAdapter(emptyList(), this)
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(true)
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- ui = FragmentPendingOperationsBinding.inflate(inflater, container, false)
- return ui.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- ui.listPending.apply {
- val myLayoutManager = LinearLayoutManager(requireContext())
- val myItemDecoration =
- DividerItemDecoration(requireContext(), myLayoutManager.orientation)
- layoutManager = myLayoutManager
- adapter = pendingAdapter
- addItemDecoration(myItemDecoration)
- }
-
- pendingOperationsManager.pendingOperations.observe(viewLifecycleOwner) {
- updatePending(it)
- }
- }
-
- override fun onStart() {
- super.onStart()
- pendingOperationsManager.getPending()
- }
-
- @Deprecated("Deprecated in Java")
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.retry_pending -> {
- pendingOperationsManager.retryPendingNow()
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
- }
-
- @Deprecated("Deprecated in Java")
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.pending_operations, menu)
- super.onCreateOptionsMenu(menu, inflater)
- }
-
- private fun updatePending(pendingOperations: List<PendingOperationInfo>) {
- pendingAdapter.update(pendingOperations)
- }
-
- override fun onPendingOperationClick(type: String, detail: JSONObject) {
- requireActivity().showError("No detail view for $type implemented yet.")
- }
-
- override fun onPendingOperationActionClick(type: String, detail: JSONObject) {
- when (type) {
- "proposal-choice" -> {
- Log.v(TAG, "got action click on proposal-choice")
- val proposalId = detail.optString("proposalId", "")
- if (proposalId == "") {
- return
- }
- model.paymentManager.abortProposal(proposalId)
- }
- }
- }
-
-}
-
-class PendingOperationsAdapter(
- private var items: List<PendingOperationInfo>,
- private val listener: PendingOperationClickListener
-) :
- RecyclerView.Adapter<PendingOperationsAdapter.MyViewHolder>() {
-
- init {
- setHasStableIds(false)
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
- val rowView =
- LayoutInflater.from(parent.context).inflate(R.layout.list_item_pending_operation, parent, false)
- return MyViewHolder(rowView)
- }
-
- override fun getItemCount(): Int {
- return items.size
- }
-
- override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
- val p = items[position]
- val pendingContainer = holder.rowView.findViewById<LinearLayout>(R.id.pending_container)
- pendingContainer.setOnClickListener {
- listener.onPendingOperationClick(p.type, p.detail)
- }
- when (p.type) {
- "proposal-choice" -> {
- val btn1 = holder.rowView.findViewById<TextView>(R.id.button_pending_action_1)
- btn1.text = btn1.context.getString(R.string.pending_operations_refuse)
- btn1.visibility = VISIBLE
- btn1.setOnClickListener {
- listener.onPendingOperationActionClick(p.type, p.detail)
- }
- }
- else -> {
- val btn1 = holder.rowView.findViewById<TextView>(R.id.button_pending_action_1)
- btn1.text = btn1.context.getString(R.string.pending_operations_no_action)
- btn1.visibility = GONE
- btn1.setOnClickListener {}
- }
- }
- val textView = holder.rowView.findViewById<TextView>(R.id.pending_text)
- val subTextView = holder.rowView.findViewById<TextView>(R.id.pending_subtext)
- subTextView.text = p.detail.toString(1)
- textView.text = p.type
- }
-
- fun update(items: List<PendingOperationInfo>) {
- this.items = items
- this.notifyDataSetChanged()
- }
-
- class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
deleted file mode 100644
index df778ed..0000000
--- a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.pending
-
-import android.util.Log
-import androidx.lifecycle.MutableLiveData
-import net.taler.wallet.TAG
-import net.taler.wallet.backend.WalletBackendApi
-import org.json.JSONObject
-
-open class PendingOperationInfo(
- val type: String,
- val detail: JSONObject
-)
-
-class PendingOperationsManager(private val walletBackendApi: WalletBackendApi) {
-
- val pendingOperations = MutableLiveData<List<PendingOperationInfo>>()
-
- internal fun getPending() {
- walletBackendApi.sendRequest("getPendingOperations") { isError, result ->
- if (isError) {
- Log.i(TAG, "got getPending error result: ${result.toString(2)}")
- return@sendRequest
- }
- Log.i(TAG, "got getPending result")
- val pendingList = mutableListOf<PendingOperationInfo>()
- val pendingJson = result.getJSONArray("pendingOperations")
- for (i in 0 until pendingJson.length()) {
- val p = pendingJson.getJSONObject(i)
- val type = p.getString("type")
- pendingList.add(PendingOperationInfo(type, p))
- }
- Log.i(TAG, "Got ${pendingList.size} pending operations")
- pendingOperations.postValue((pendingList))
- }
- }
-
- fun retryPendingNow() {
- walletBackendApi.sendRequest("retryPendingNow")
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt b/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt
index f3c41e8..96e939b 100644
--- a/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt
@@ -21,20 +21,17 @@ import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
-import net.taler.common.Amount
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
sealed class RefundStatus {
- data class Error(val msg: String) : RefundStatus()
- data class Success(val response: RefundResponse) : RefundStatus()
+ data class Error(val error: TalerErrorInfo) : RefundStatus()
+ data class Success(val response: StartRefundQueryForUriResponse) : RefundStatus()
}
@Serializable
-data class RefundResponse(
- val amountEffectivePaid: Amount,
- val amountRefundGranted: Amount,
- val amountRefundGone: Amount,
- val pendingAtExchange: Boolean
+data class StartRefundQueryForUriResponse(
+ val transactionId: String,
)
class RefundManager(
@@ -45,10 +42,10 @@ class RefundManager(
fun refund(refundUri: String): LiveData<RefundStatus> {
val liveData = MutableLiveData<RefundStatus>()
scope.launch {
- api.request("applyRefund", RefundResponse.serializer()) {
+ api.request("startRefundQueryForUri", StartRefundQueryForUriResponse.serializer()) {
put("talerRefundUri", refundUri)
}.onError {
- liveData.postValue(RefundStatus.Error(it.userFacingMsg))
+ liveData.postValue(RefundStatus.Error(it))
}.onSuccess {
liveData.postValue(RefundStatus.Success(it))
}
diff --git a/wallet/src/main/java/net/taler/wallet/refund/RefundPaymentInfo.kt b/wallet/src/main/java/net/taler/wallet/refund/RefundPaymentInfo.kt
new file mode 100644
index 0000000..d5f59be
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/refund/RefundPaymentInfo.kt
@@ -0,0 +1,39 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.refund
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+class RefundPaymentInfo(
+ val summary: String,
+ @SerialName("summary_i18n")
+ val summaryI18n: Map<String, String>? = null,
+ /**
+ * More information about the merchant
+ */
+ val merchant: MerchantInfo,
+)
+
+@Serializable
+class MerchantInfo(
+ val name: String,
+ val logo: String? = null,
+ val website: String? = null,
+ val email: String? = null,
+)
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionTipFragment.kt b/wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt
index 3f370c7..637b41a 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionTipFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt
@@ -1,6 +1,6 @@
/*
* This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
+ * (C) 2023 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
@@ -14,54 +14,50 @@
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-package net.taler.wallet.transactions
+package net.taler.wallet.refund
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
import net.taler.common.Timestamp
import net.taler.common.toAbsoluteTime
import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.compose.TalerSurface
-
-class TransactionTipFragment : TransactionDetailFragment() {
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View = ComposeView(requireContext()).apply {
- setContent {
- TalerSurface {
- val t = transaction as? TransactionTip ?: error("No or wrong transaction")
- TransactionTipComposable(t) {
- onDeleteButtonClicked(t)
- }
- }
- }
- }
-}
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.ErrorTransactionButton
+import net.taler.wallet.transactions.TransactionAction
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionRefund
+import net.taler.wallet.transactions.TransactionState
+import net.taler.wallet.transactions.TransitionsComposable
@Composable
-fun TransactionTipComposable(t: TransactionTip, onDelete: () -> Unit) {
+fun TransactionRefundComposable(
+ t: TransactionRefund,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
@@ -73,47 +69,55 @@ fun TransactionTipComposable(t: TransactionTip, onDelete: () -> Unit) {
Text(
modifier = Modifier.padding(16.dp),
text = t.timestamp.ms.toAbsoluteTime(context).toString(),
- style = MaterialTheme.typography.body1,
+ style = MaterialTheme.typography.bodyLarge,
)
-
TransactionAmountComposable(
- label = stringResource(id = R.string.send_peer_payment_amount_received),
- amount = t.amountEffective,
+ label = stringResource(id = R.string.transaction_refund),
+ amount = t.amountEffective.withSpec(spec),
amountType = AmountType.Positive,
)
TransactionAmountComposable(
- label = stringResource(id = R.string.send_peer_payment_amount_sent),
- amount = t.amountRaw,
+ label = stringResource(id = R.string.transaction_order_total),
+ amount = t.amountRaw.withSpec(spec),
amountType = AmountType.Neutral,
)
val fee = t.amountRaw - t.amountEffective
if (!fee.isZero()) {
TransactionAmountComposable(
label = stringResource(id = R.string.withdraw_fees),
- amount = fee,
+ amount = fee.withSpec(spec),
amountType = AmountType.Negative,
)
}
TransactionInfoComposable(
- label = stringResource(id = R.string.tip_merchant_url),
- info = t.merchantBaseUrl,
+ label = stringResource(id = R.string.transaction_order),
+ info = t.paymentInfo?.summary ?: "",
)
- DeleteTransactionComposable(onDelete)
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
}
}
@Preview
@Composable
-fun TransactionTipPreview() {
- val t = TransactionTip(
+fun TransactionRefundComposablePreview() {
+ val t = TransactionRefund(
transactionId = "transactionId",
timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
- pending = true,
- merchantBaseUrl = "https://merchant.example.org/",
- amountRaw = Amount.fromDouble("TESTKUDOS", 42.23),
- amountEffective = Amount.fromDouble("TESTKUDOS", 42.1337),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ paymentInfo = RefundPaymentInfo(
+ merchant = MerchantInfo(name = "Taler"),
+ summary = "Some Product that was bought and can have quite a long label",
+ ),
+ refundedTransactionId = "transactionId",
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
)
- Surface {
- TransactionTipComposable(t) {}
+ TalerSurface {
+ TransactionRefundComposable(t = t, devMode = true, spec = null) {}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt b/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt
index 3d31dc5..38eeb9b 100644
--- a/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt
@@ -18,22 +18,22 @@ package net.taler.wallet.settings
import android.os.Bundle
import android.view.View
+import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
-import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_SHORT
import com.google.android.material.snackbar.Snackbar
import net.taler.common.showError
-import net.taler.common.toRelativeTime
import net.taler.wallet.BuildConfig.FLAVOR
import net.taler.wallet.BuildConfig.VERSION_CODE
import net.taler.wallet.BuildConfig.VERSION_NAME
-import net.taler.wallet.BuildConfig.WALLET_CORE_VERSION
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
+import net.taler.wallet.showError
import net.taler.wallet.withdraw.WithdrawTestStatus
import java.lang.System.currentTimeMillis
@@ -44,71 +44,74 @@ class SettingsFragment : PreferenceFragmentCompat() {
private val settingsManager get() = model.settingsManager
private val withdrawManager by lazy { model.withdrawManager }
- private lateinit var prefBackup: Preference
private lateinit var prefDevMode: SwitchPreferenceCompat
private lateinit var prefWithdrawTest: Preference
private lateinit var prefLogcat: Preference
private lateinit var prefExportDb: Preference
+ private lateinit var prefImportDb: Preference
private lateinit var prefVersionApp: Preference
private lateinit var prefVersionCore: Preference
private lateinit var prefVersionExchange: Preference
private lateinit var prefVersionMerchant: Preference
+ private lateinit var prefTest: Preference
private lateinit var prefReset: Preference
private val devPrefs by lazy {
listOf(
- prefBackup,
+ prefVersionCore,
prefWithdrawTest,
prefLogcat,
prefExportDb,
- prefVersionApp,
- prefVersionCore,
+ prefImportDb,
prefVersionExchange,
prefVersionMerchant,
- prefReset
+ prefTest,
+ prefReset,
)
}
private val logLauncher = registerForActivityResult(CreateDocument("text/plain")) { uri ->
settingsManager.exportLogcat(uri)
}
- private val dbExportLauncher = registerForActivityResult(CreateDocument("application/json")) { uri ->
- settingsManager.exportDb(uri)
- }
+ private val dbExportLauncher =
+ registerForActivityResult(CreateDocument("application/json")) { uri ->
+ settingsManager.exportDb(uri)
+ }
+ private val dbImportLauncher =
+ registerForActivityResult(OpenDocument()) { uri ->
+ settingsManager.importDb(uri)
+ }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_main, rootKey)
- prefBackup = findPreference("pref_backup")!!
prefDevMode = findPreference("pref_dev_mode")!!
prefWithdrawTest = findPreference("pref_testkudos")!!
prefLogcat = findPreference("pref_logcat")!!
prefExportDb = findPreference("pref_export_db")!!
+ prefImportDb = findPreference("pref_import_db")!!
prefVersionApp = findPreference("pref_version_app")!!
prefVersionCore = findPreference("pref_version_core")!!
prefVersionExchange = findPreference("pref_version_protocol_exchange")!!
prefVersionMerchant = findPreference("pref_version_protocol_merchant")!!
+ prefTest = findPreference("pref_test")!!
prefReset = findPreference("pref_reset")!!
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- model.lastBackup.observe(viewLifecycleOwner) {
- val time = it.toRelativeTime(requireContext())
- prefBackup.summary = getString(R.string.backup_last, time)
- }
+ prefVersionApp.summary = "$VERSION_NAME ($FLAVOR $VERSION_CODE)"
+ prefVersionCore.summary = "${model.walletVersion} (${model.walletVersionHash?.take(7)})"
+ model.exchangeVersion?.let { prefVersionExchange.summary = it }
+ model.merchantVersion?.let { prefVersionMerchant.summary = it }
model.devMode.observe(viewLifecycleOwner) { enabled ->
prefDevMode.isChecked = enabled
- if (enabled) {
- prefVersionApp.summary = "$VERSION_NAME ($FLAVOR $VERSION_CODE)"
- prefVersionCore.summary = WALLET_CORE_VERSION
- model.exchangeVersion?.let { prefVersionExchange.summary = it }
- model.merchantVersion?.let { prefVersionMerchant.summary = it }
- }
devPrefs.forEach { it.isVisible = enabled }
}
prefDevMode.setOnPreferenceChangeListener { _, newValue ->
- model.devMode.value = newValue as Boolean
+ model.setDevMode(newValue as Boolean) { error ->
+ showError(error)
+ }
true
}
@@ -135,22 +138,43 @@ class SettingsFragment : PreferenceFragmentCompat() {
dbExportLauncher.launch("taler-wallet-db-${currentTimeMillis()}.json")
true
}
-
+ prefImportDb.setOnPreferenceClickListener {
+ showImportDialog()
+ true
+ }
+ prefTest.setOnPreferenceClickListener {
+ model.runIntegrationTest()
+ true
+ }
prefReset.setOnPreferenceClickListener {
showResetDialog()
true
}
}
+ private fun showImportDialog() {
+ MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setMessage(R.string.settings_dialog_import_message)
+ .setNegativeButton(R.string.import_db) { _, _ ->
+ dbImportLauncher.launch(arrayOf("application/json"))
+ }
+ .setPositiveButton(R.string.cancel) { _, _ ->
+ Snackbar.make(requireView(), getString(R.string.settings_alert_import_canceled), LENGTH_SHORT).show()
+ }
+ .show()
+ }
+
private fun showResetDialog() {
- AlertDialog.Builder(requireContext(), R.style.DialogTheme)
- .setMessage("Do you really want to reset the wallet and lose all coins and purchases?")
- .setPositiveButton("Reset") { _, _ ->
- model.dangerouslyReset()
- Snackbar.make(requireView(), "Wallet has been reset", LENGTH_SHORT).show()
+ MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setMessage(R.string.settings_dialog_reset_message)
+ .setNegativeButton(R.string.reset) { _, _ ->
+ settingsManager.clearDb {
+ model.dangerouslyReset()
+ }
+ Snackbar.make(requireView(), getString(R.string.settings_alert_reset_done), LENGTH_SHORT).show()
}
- .setNegativeButton("Cancel") { _, _ ->
- Snackbar.make(requireView(), "Reset cancelled", LENGTH_SHORT).show()
+ .setPositiveButton(R.string.cancel) { _, _ ->
+ Snackbar.make(requireView(), getString(R.string.settings_alert_reset_canceled), LENGTH_SHORT).show()
}
.show()
}
diff --git a/wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt b/wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt
index 349c7b1..8331d59 100644
--- a/wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt
@@ -25,14 +25,19 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import net.taler.wallet.R
-import net.taler.wallet.backend.WALLET_DB
+import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.backend.WalletResponse.Error
+import net.taler.wallet.backend.WalletResponse.Success
+import org.json.JSONObject
class SettingsManager(
private val context: Context,
+ private val api: WalletBackendApi,
private val scope: CoroutineScope,
) {
-
fun exportLogcat(uri: Uri?) {
if (uri == null) {
onLogExportError()
@@ -65,20 +70,88 @@ class SettingsManager(
onDbExportError()
return
}
+
scope.launch(Dispatchers.IO) {
- try {
- context.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
- context.openFileInput(WALLET_DB).use { inputStream ->
- inputStream.copyTo(outputStream)
+ when (val response = api.rawRequest("exportDb")) {
+ is Success -> {
+ try {
+ context.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
+ val data = Json.encodeToString(response.result)
+ val writer = outputStream.bufferedWriter()
+ writer.write(data)
+ writer.close()
+ }
+ } catch(e: Exception) {
+ Log.e(SettingsManager::class.simpleName, "Error exporting db: ", e)
+ withContext(Dispatchers.Main) {
+ onDbExportError()
+ }
+ return@launch
}
- } ?: onDbExportError()
- } catch (e: Exception) {
- Log.e(SettingsManager::class.simpleName, "Error exporting db: ", e)
- onDbExportError()
- return@launch
+
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, R.string.settings_db_export_success, LENGTH_LONG).show()
+ }
+ }
+ is Error -> {
+ Log.e(SettingsManager::class.simpleName, "Error exporting db: ${response.error}")
+ withContext(Dispatchers.Main) {
+ onDbExportError()
+ }
+ return@launch
+ }
}
- withContext(Dispatchers.Main) {
- Toast.makeText(context, R.string.settings_db_export_success, LENGTH_LONG).show()
+ }
+ }
+
+ fun importDb(uri: Uri?) {
+ if (uri == null) {
+ onDbImportError()
+ return
+ }
+
+ scope.launch(Dispatchers.IO) {
+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
+ try {
+ val reader = inputStream.bufferedReader()
+ val strData = reader.readText()
+ reader.close()
+ val jsonData = JSONObject(strData)
+ when (val response = api.rawRequest("importDb") {
+ put("dump", jsonData)
+ }) {
+ is Success -> {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, R.string.settings_db_import_success, LENGTH_LONG).show()
+ }
+ }
+ is Error -> {
+ Log.e(SettingsManager::class.simpleName, "Error importing db: ${response.error}")
+ withContext(Dispatchers.Main) {
+ onDbImportError()
+ }
+ return@launch
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(SettingsManager::class.simpleName, "Error importing db: ", e)
+ withContext(Dispatchers.Main) {
+ onDbImportError()
+ }
+ return@launch
+ }
+ }
+ }
+ }
+
+ fun clearDb(onSuccess: () -> Unit) {
+ scope.launch {
+ when (val response = api.rawRequest("clearDb")) {
+ is Success -> onSuccess()
+ is Error -> {
+ Log.e(SettingsManager::class.simpleName, "Error cleaning db: ${response.error}")
+ onDbClearError()
+ }
}
}
}
@@ -87,4 +160,12 @@ class SettingsManager(
Toast.makeText(context, R.string.settings_db_export_error, LENGTH_LONG).show()
}
+ private fun onDbImportError() {
+ Toast.makeText(context, R.string.settings_db_import_error, LENGTH_LONG).show()
+ }
+
+ private fun onDbClearError() {
+ Toast.makeText(context, R.string.settings_db_clear_error, LENGTH_LONG).show()
+ }
+
}
diff --git a/wallet/src/main/java/net/taler/wallet/tip/AlreadyAcceptedFragment.kt b/wallet/src/main/java/net/taler/wallet/tip/AlreadyAcceptedFragment.kt
deleted file mode 100644
index 637a2da..0000000
--- a/wallet/src/main/java/net/taler/wallet/tip/AlreadyAcceptedFragment.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.tip
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.navigation.fragment.findNavController
-import net.taler.wallet.databinding.FragmentAlreadyAcceptedBinding
-
-/**
- * Display the message that the user already paid for the order
- * that the merchant is proposing.
- */
-class AlreadyAcceptedFragment : Fragment() {
-
- private lateinit var ui: FragmentAlreadyAcceptedBinding
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- ui = FragmentAlreadyAcceptedBinding.inflate(inflater, container, false)
- return ui.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- ui.backButton.setOnClickListener {
- findNavController().navigateUp()
- }
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/tip/PromptTipFragment.kt b/wallet/src/main/java/net/taler/wallet/tip/PromptTipFragment.kt
deleted file mode 100644
index b0f5a35..0000000
--- a/wallet/src/main/java/net/taler/wallet/tip/PromptTipFragment.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.tip
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.navigation.fragment.findNavController
-import com.google.android.material.snackbar.Snackbar
-import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
-import net.taler.common.Amount
-import net.taler.common.fadeIn
-import net.taler.common.fadeOut
-import net.taler.wallet.MainViewModel
-import net.taler.wallet.R
-import net.taler.wallet.cleanExchange
-import net.taler.wallet.databinding.FragmentPromptTipBinding
-
-/**
- * Show a tip and ask the user to accept/decline.
- */
-class PromptTipFragment : Fragment() {
-
- private val model: MainViewModel by activityViewModels()
- private val tipManager by lazy { model.tipManager }
-
- private lateinit var ui: FragmentPromptTipBinding
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- ui = FragmentPromptTipBinding.inflate(inflater, container, false)
- return ui.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- tipManager.tipStatus.observe(viewLifecycleOwner, ::onPaymentStatusChanged)
-
- }
-
- override fun onDestroy() {
- super.onDestroy()
- if (!requireActivity().isChangingConfigurations) {
- // tipManager.abortTip()
- }
- }
-
- private fun showLoading(show: Boolean) {
- model.showProgressBar.value = show
- if (show) {
- ui.progressBar.fadeIn()
- } else {
- ui.progressBar.fadeOut()
- }
- }
-
- private fun onPaymentStatusChanged(payStatus: TipStatus) = when (payStatus) {
- is TipStatus.Prepared -> {
- showLoading(false)
- showContent(
- amountRaw = payStatus.tipAmountRaw,
- amountEffective = payStatus.tipAmountEffective,
- exchange = payStatus.exchangeBaseUrl,
- merchant = payStatus.merchantBaseUrl
- )
- ui.confirmWithdrawButton.isEnabled = true
- ui.confirmWithdrawButton.setOnClickListener {
- tipManager.confirmTip(
- payStatus.walletTipId,
- payStatus.tipAmountRaw.currency
- )
- }
- }
- is TipStatus.Accepting -> {
- model.showProgressBar.value = true
- ui.confirmProgressBar.fadeIn()
- ui.confirmWithdrawButton.fadeOut()
- }
- is TipStatus.AlreadyAccepted -> {
- showLoading(false)
- tipManager.resetTipStatus()
- findNavController().navigate(R.id.action_promptTip_to_alreadyAccepted)
- }
- is TipStatus.Success -> {
- showLoading(false)
- tipManager.resetTipStatus()
- findNavController().navigate(R.id.action_promptTip_to_nav_main)
- model.showTransactions(payStatus.currency)
- Snackbar.make(requireView(), R.string.tip_received, LENGTH_LONG).show()
- }
- is TipStatus.Error -> {
- showLoading(false)
- ui.introView.text = getString(R.string.payment_error, payStatus.error)
- ui.introView.fadeIn()
- }
- is TipStatus.None -> {
- // No tip active
- showLoading(false)
- }
- is TipStatus.Loading -> {
- // Wait until loaded ...
- showLoading(true)
- }
- }
-
- private fun showContent(
- amountRaw: Amount,
- amountEffective: Amount,
- exchange: String,
- merchant: String,
- ) {
- model.showProgressBar.value = false
- ui.progressBar.fadeOut()
-
- ui.introView.fadeIn()
- ui.effectiveAmountView.text = amountEffective.toString()
- ui.effectiveAmountView.fadeIn()
-
- ui.chosenAmountLabel.fadeIn()
- ui.chosenAmountView.text = amountRaw.toString()
- ui.chosenAmountView.fadeIn()
-
- ui.feeLabel.fadeIn()
- ui.feeView.text =
- getString(R.string.amount_negative, (amountRaw - amountEffective).toString())
- ui.feeView.fadeIn()
-
- ui.exchangeIntroView.fadeIn()
- ui.withdrawExchangeUrl.text = cleanExchange(exchange)
- ui.withdrawExchangeUrl.fadeIn()
-
- ui.merchantIntroView.fadeIn()
- ui.withdrawMerchantUrl.text = cleanExchange(merchant)
- ui.withdrawMerchantUrl.fadeIn()
-
- ui.withdrawCard.fadeIn()
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/tip/TipManager.kt b/wallet/src/main/java/net/taler/wallet/tip/TipManager.kt
deleted file mode 100644
index 5548687..0000000
--- a/wallet/src/main/java/net/taler/wallet/tip/TipManager.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.tip
-
-import android.util.Log
-import androidx.annotation.UiThread
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import net.taler.common.Amount
-import net.taler.common.Timestamp
-import net.taler.wallet.TAG
-import net.taler.wallet.backend.TalerErrorInfo
-import net.taler.wallet.backend.WalletBackendApi
-import net.taler.wallet.tip.PrepareTipResponse.AlreadyAcceptedResponse
-import net.taler.wallet.tip.PrepareTipResponse.TipPossibleResponse
-
-sealed class TipStatus {
- object None : TipStatus()
- object Loading : TipStatus()
- data class Prepared(
- val walletTipId: String,
- val merchantBaseUrl: String,
- val exchangeBaseUrl: String,
- val expirationTimestamp: Timestamp,
- val tipAmountRaw: Amount,
- val tipAmountEffective: Amount,
- ) : TipStatus()
- object Accepting : TipStatus()
- data class AlreadyAccepted(
- val walletTipId: String,
- ) : TipStatus()
-
- // TODO bring user to fulfilment URI (not yet in wallet API)
- data class Error(val error: String) : TipStatus()
- data class Success(val currency: String) : TipStatus()
-}
-
-class TipManager(
- private val api: WalletBackendApi,
- private val scope: CoroutineScope,
-) {
-
- private val mTipStatus = MutableLiveData<TipStatus>(TipStatus.None)
- internal val tipStatus: LiveData<TipStatus> = mTipStatus
-
- @UiThread
- fun prepareTip(url: String) = scope.launch {
- mTipStatus.value = TipStatus.Loading
- api.request("prepareTip", PrepareTipResponse.serializer()) {
- put("talerTipUri", url)
- }.onError {
- handleError("prepareTip", it)
- }.onSuccess { response ->
- mTipStatus.value = when (response) {
- is TipPossibleResponse -> response.toTipStatusPrepared()
- is AlreadyAcceptedResponse -> TipStatus.AlreadyAccepted(
- response.walletTipId
- )
- }
- }
- }
-
- fun confirmTip(tipId: String, currency: String) = scope.launch {
- mTipStatus.value = TipStatus.Accepting
- api.request("acceptTip", ConfirmTipResult.serializer()) {
- put("walletTipId", tipId)
- }.onError {
- handleError("acceptTip", it)
- }.onSuccess {
- mTipStatus.postValue(TipStatus.Success(currency))
- }
- }
-
- @UiThread
- fun resetTipStatus() {
- mTipStatus.value = TipStatus.None
- }
-
- private fun handleError(operation: String, error: TalerErrorInfo) {
- Log.e(TAG, "got $operation error result $error")
- mTipStatus.value = TipStatus.Error(error.userFacingMsg)
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/tip/TipResponses.kt b/wallet/src/main/java/net/taler/wallet/tip/TipResponses.kt
deleted file mode 100644
index b0f6273..0000000
--- a/wallet/src/main/java/net/taler/wallet/tip/TipResponses.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.tip
-
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.JsonClassDiscriminator
-import net.taler.common.Amount
-import net.taler.common.Timestamp
-
-@OptIn(ExperimentalSerializationApi::class)
-@Serializable
-@JsonClassDiscriminator("accepted")
-sealed class PrepareTipResponse {
-
- @Serializable
- @SerialName("false")
- data class TipPossibleResponse(
- val walletTipId: String,
- val merchantBaseUrl: String,
- val exchangeBaseUrl: String,
- val expirationTimestamp: Timestamp,
- val tipAmountRaw: Amount,
- val tipAmountEffective: Amount,
- ) : PrepareTipResponse() {
- fun toTipStatusPrepared() = TipStatus.Prepared(
- walletTipId = walletTipId,
- merchantBaseUrl = merchantBaseUrl,
- exchangeBaseUrl = exchangeBaseUrl,
- expirationTimestamp = expirationTimestamp,
- tipAmountEffective = tipAmountEffective,
- tipAmountRaw = tipAmountRaw,
- )
- }
-
- @Serializable
- @SerialName("true")
- data class AlreadyAcceptedResponse(
- val walletTipId: String,
- ) : PrepareTipResponse()
-}
-
-@Serializable
-class ConfirmTipResult
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt
new file mode 100644
index 0000000..4e4bbe0
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt
@@ -0,0 +1,116 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountBalance
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import net.taler.wallet.R
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionMinorState.BankConfirmTransfer
+import net.taler.wallet.transactions.TransactionMinorState.ExchangeWaitReserve
+import net.taler.wallet.transactions.TransactionMinorState.KycRequired
+
+interface ActionListener {
+ enum class Type {
+ COMPLETE_KYC,
+ CONFIRM_WITH_BANK,
+ CONFIRM_MANUAL
+ }
+
+ fun onActionButtonClicked(tx: Transaction, type: Type)
+}
+
+@Composable
+fun ActionButton(
+ modifier: Modifier = Modifier,
+ tx: TransactionWithdrawal,
+ listener: ActionListener,
+) {
+ if (tx.txState.major == Pending) {
+ when (tx.txState.minor) {
+ KycRequired -> KycButton(modifier, tx, listener)
+ BankConfirmTransfer -> ConfirmBankButton(modifier, tx, listener)
+ ExchangeWaitReserve -> ConfirmManualButton(modifier, tx, listener)
+ else -> {}
+ }
+ }
+}
+
+@Composable
+private fun KycButton(
+ modifier: Modifier = Modifier,
+ tx: TransactionWithdrawal,
+ listener: ActionListener,
+) {
+ Button(
+ onClick = { listener.onActionButtonClicked(tx, ActionListener.Type.COMPLETE_KYC) },
+ modifier = modifier,
+ ) {
+ Text(stringResource(R.string.transaction_action_kyc))
+ }
+}
+
+@Composable
+private fun ConfirmBankButton(
+ modifier: Modifier = Modifier,
+ tx: TransactionWithdrawal,
+ listener: ActionListener,
+) {
+ Button(
+ onClick = { listener.onActionButtonClicked(tx, ActionListener.Type.CONFIRM_WITH_BANK) },
+ modifier = modifier,
+ ) {
+ val label = stringResource(R.string.withdraw_button_confirm_bank)
+ Icon(
+ Icons.Default.AccountBalance,
+ label,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(label)
+ }
+}
+
+@Composable
+private fun ConfirmManualButton(
+ modifier: Modifier = Modifier,
+ tx: TransactionWithdrawal,
+ listener: ActionListener,
+) {
+ Button(
+ onClick = { listener.onActionButtonClicked(tx, ActionListener.Type.CONFIRM_MANUAL) },
+ modifier = modifier,
+ ) {
+ val label = stringResource(R.string.withdraw_manual_ready_details_intro)
+ Icon(
+ Icons.Default.AccountBalance,
+ label,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(label)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/DeleteTransactionComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/DeleteTransactionComposable.kt
deleted file mode 100644
index 7b69e2b..0000000
--- a/wallet/src/main/java/net/taler/wallet/transactions/DeleteTransactionComposable.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.transactions
-
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Icon
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import net.taler.wallet.R
-
-@Composable
-fun DeleteTransactionComposable(onDelete: () -> Unit) {
- Button(
- modifier = Modifier.padding(16.dp),
- colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(R.color.red)),
- onClick = onDelete,
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- painter = painterResource(id = R.drawable.ic_delete),
- contentDescription = null,
- tint = Color.White,
- )
- Text(
- modifier = Modifier.padding(start = 8.dp),
- text = stringResource(R.string.transactions_delete),
- color = Color.White,
- )
- }
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/ErrorTransactionComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/ErrorTransactionComposable.kt
new file mode 100644
index 0000000..ea875d7
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/ErrorTransactionComposable.kt
@@ -0,0 +1,116 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.sp
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.copyToClipBoard
+
+@Composable
+fun ErrorTransactionButton(
+ modifier: Modifier = Modifier,
+ error: TalerErrorInfo,
+) {
+ val showDialog = remember { mutableStateOf(false) }
+
+ if (showDialog.value) {
+ @Suppress("OPT_IN_USAGE")
+ val json = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+ val message = json.encodeToString(error)
+ AlertDialog(
+ onDismissRequest = {
+ showDialog.value = false
+ },
+ title = {
+ Text(stringResource(R.string.nav_error))
+ },
+ text = {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier.verticalScroll(scrollState),
+ ) {
+ Text(
+ fontFamily = FontFamily.Monospace,
+ fontSize = 10.sp,
+ text = message,
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = {
+ showDialog.value = false
+ }) {
+ Text(stringResource(R.string.close))
+ }
+ },
+ confirmButton = {
+ val context = LocalContext.current
+ TextButton(onClick = {
+ copyToClipBoard(context, context.getString(R.string.nav_error), message)
+ }) {
+ Text(stringResource(R.string.copy))
+ }
+ })
+ }
+
+ Button(
+ modifier = modifier,
+ colors = ButtonDefaults.buttonColors(
+ contentColor = MaterialTheme.colorScheme.onError,
+ containerColor = MaterialTheme.colorScheme.error,
+ ),
+ onClick = {
+ showDialog.value = true
+ }
+ ) {
+ val label = stringResource(R.string.nav_error)
+ Icon(
+ imageVector = Icons.Default.Error,
+ contentDescription = label,
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(label)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
index fa30f5c..3b686a6 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
@@ -32,18 +32,25 @@ import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
-import com.google.android.material.button.MaterialButton
+import net.taler.common.CurrencySpecification
import net.taler.common.exhaustive
import net.taler.common.toRelativeTime
import net.taler.wallet.R
-import net.taler.wallet.handleKyc
+import net.taler.wallet.getThemeColor
import net.taler.wallet.transactions.TransactionAdapter.TransactionViewHolder
+import net.taler.wallet.transactions.TransactionMajorState.Aborted
+import net.taler.wallet.transactions.TransactionMajorState.Failed
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionMinorState.BankConfirmTransfer
+import net.taler.wallet.transactions.TransactionMinorState.KycRequired
internal class TransactionAdapter(
- private val listener: OnTransactionClickListener
+ private val listener: OnTransactionClickListener,
) : Adapter<TransactionViewHolder>() {
private var transactions: List<Transaction> = ArrayList()
+ private var currencySpec: CurrencySpecification? = null
+
lateinit var tracker: SelectionTracker<String>
val keyProvider = TransactionKeyProvider()
@@ -64,6 +71,11 @@ internal class TransactionAdapter(
holder.bind(transaction, tracker.isSelected(transaction.transactionId))
}
+ fun setCurrencySpec(spec: CurrencySpecification?) {
+ this.currencySpec = spec
+ this.notifyDataSetChanged()
+ }
+
fun update(updatedTransactions: List<Transaction>) {
this.transactions = updatedTransactions
this.notifyDataSetChanged()
@@ -80,13 +92,13 @@ internal class TransactionAdapter(
private val icon: ImageView = v.findViewById(R.id.icon)
private val title: TextView = v.findViewById(R.id.title)
private val extraInfoView: TextView = v.findViewById(R.id.extraInfoView)
- private val actionButton: MaterialButton = v.findViewById(R.id.actionButton)
private val time: TextView = v.findViewById(R.id.time)
private val amount: TextView = v.findViewById(R.id.amount)
private val pendingView: TextView = v.findViewById(R.id.pendingView)
private val amountColor = amount.currentTextColor
- private val red = getColor(context, R.color.red)
+ private val extraInfoColor = extraInfoView.currentTextColor
+ private val red = context.getThemeColor(R.attr.colorError)
private val green = getColor(context, R.color.green)
fun bind(transaction: Transaction, selected: Boolean) {
@@ -98,54 +110,100 @@ internal class TransactionAdapter(
}
title.text = transaction.getTitle(context)
bindExtraInfo(transaction)
- bindActionButton(transaction)
time.text = transaction.timestamp.ms.toRelativeTime(context)
bindAmount(transaction)
- pendingView.visibility = if (transaction.pending) VISIBLE else GONE
- val bgColor = getColor(context,
+ pendingView.visibility = if (transaction.txState.major == Pending) VISIBLE else GONE
+ val bgColor = getColor(
+ context,
if (selected) R.color.selectedBackground
- else android.R.color.transparent)
+ else android.R.color.transparent
+ )
root.setBackgroundColor(bgColor)
}
private fun bindExtraInfo(transaction: Transaction) {
- if (transaction.error != null) {
- extraInfoView.text =
- context.getString(R.string.payment_error, transaction.error!!.userFacingMsg)
- extraInfoView.setTextColor(red)
- extraInfoView.visibility = VISIBLE
- } else if (transaction is TransactionWithdrawal && !transaction.confirmed) {
- extraInfoView.setText(R.string.withdraw_waiting_confirm)
- extraInfoView.setTextColor(amountColor)
- extraInfoView.visibility = VISIBLE
- } else if (transaction is TransactionPayment && transaction.status != PaymentStatus.Paid && transaction.status != PaymentStatus.Accepted) {
- extraInfoView.setText(if (transaction.status == PaymentStatus.Aborted) R.string.payment_aborted else R.string.payment_failed)
- extraInfoView.setTextColor(amountColor)
- extraInfoView.visibility = VISIBLE
- } else {
- extraInfoView.visibility = GONE
- }
- }
+ when {
+ // Goes first so it always shows errors when present
+ transaction.error != null -> {
+ extraInfoView.text =
+ context.getString(R.string.payment_error, transaction.error!!.userFacingMsg)
+ extraInfoView.setTextColor(red)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction.txState.major == Aborted -> {
+ extraInfoView.setText(R.string.payment_aborted)
+ extraInfoView.setTextColor(red)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction.txState.major == Failed -> {
+ extraInfoView.setText(R.string.payment_failed)
+ extraInfoView.setTextColor(red)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction.txState.major == Pending -> when (transaction.txState.minor) {
+ BankConfirmTransfer -> {
+ extraInfoView.setText(R.string.withdraw_waiting_confirm)
+ extraInfoView.setTextColor(amountColor)
+ extraInfoView.visibility = VISIBLE
+ }
+ KycRequired -> {
+ extraInfoView.setText(R.string.transaction_action_kyc)
+ extraInfoView.setTextColor(amountColor)
+ extraInfoView.visibility = VISIBLE
+ }
+ else -> extraInfoView.visibility = GONE
+ }
+
+ transaction is TransactionWithdrawal && !transaction.confirmed -> {
+ extraInfoView.setText(R.string.withdraw_waiting_confirm)
+ extraInfoView.setTextColor(amountColor)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction is TransactionPeerPushCredit && transaction.info.summary != null -> {
+ extraInfoView.text = transaction.info.summary
+ extraInfoView.setTextColor(extraInfoColor)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction is TransactionPeerPushDebit && transaction.info.summary != null -> {
+ extraInfoView.text = transaction.info.summary
+ extraInfoView.setTextColor(extraInfoColor)
+ extraInfoView.visibility = VISIBLE
+ }
- private fun bindActionButton(transaction: Transaction) {
- actionButton.visibility = transaction.handleKyc({ GONE }) {
- actionButton.setOnClickListener { listener.onActionButtonClicked(transaction) }
- actionButton.setText(R.string.transaction_action_kyc)
- VISIBLE
+ transaction is TransactionPeerPullCredit && transaction.info.summary != null -> {
+ extraInfoView.text = transaction.info.summary
+ extraInfoView.setTextColor(extraInfoColor)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction is TransactionPeerPullDebit && transaction.info.summary != null -> {
+ extraInfoView.text = transaction.info.summary
+ extraInfoView.setTextColor(extraInfoColor)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ else -> extraInfoView.visibility = GONE
}
}
private fun bindAmount(transaction: Transaction) {
- val amountStr = transaction.amountEffective.amountStr
+ val amountStr = transaction.amountEffective.withSpec(currencySpec).toString(showSymbol = false)
when (transaction.amountType) {
AmountType.Positive -> {
amount.text = context.getString(R.string.amount_positive, amountStr)
- amount.setTextColor(if (transaction.pending) amountColor else green)
+ amount.setTextColor(if (transaction.txState.major == Pending) amountColor else green)
}
+
AmountType.Negative -> {
amount.text = context.getString(R.string.amount_negative, amountStr)
- amount.setTextColor(if (transaction.pending) amountColor else red)
+ amount.setTextColor(if (transaction.txState.major == Pending) amountColor else red)
}
+
AmountType.Neutral -> {
amount.text = amountStr
amount.setTextColor(amountColor)
@@ -165,12 +223,13 @@ internal class TransactionAdapter(
internal class TransactionLookup(
private val list: RecyclerView,
- private val adapter: TransactionAdapter
+ private val adapter: TransactionAdapter,
) : ItemDetailsLookup<String>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
list.findChildViewUnder(e.x, e.y)?.let { view ->
val holder = list.getChildViewHolder(view)
val position = holder.bindingAdapterPosition
+ if (position < 0) return null
return object : ItemDetails<String>() {
override fun getPosition(): Int = position
override fun getSelectionKey(): String = adapter.keyProvider.getKey(position)
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt
index 60f9b20..d2be3cf 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt
@@ -20,6 +20,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.ComposeView
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.deposit.TransactionDepositComposable
@@ -33,9 +34,13 @@ class TransactionDepositFragment : TransactionDetailFragment() {
): View = ComposeView(requireContext()).apply {
setContent {
TalerSurface {
- val t = transaction ?: error("No transaction")
- TransactionDepositComposable(t as TransactionDeposit) {
- onDeleteButtonClicked(t)
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionDeposit) TransactionDepositComposable(
+ t = t,
+ devMode = devMode,
+ spec = balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ ) {
+ onTransitionButtonClicked(t, it)
}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt
index ed42066..09ca05b 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt
@@ -16,105 +16,151 @@
package net.taler.wallet.transactions
-import android.content.Intent
-import android.net.Uri
import android.os.Bundle
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import android.widget.TextView
-import androidx.annotation.StringRes
-import androidx.appcompat.app.AlertDialog
+import android.util.Log
+import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
-import net.taler.common.Amount
-import net.taler.common.startActivitySafe
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
+import net.taler.wallet.TAG
+import net.taler.wallet.showError
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Delete
+import net.taler.wallet.transactions.TransactionAction.Fail
+import net.taler.wallet.transactions.TransactionAction.Resume
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
abstract class TransactionDetailFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
- private val transactionManager by lazy { model.transactionManager }
- protected val transaction: Transaction? get() = transactionManager.selectedTransaction
+ protected val transactionManager by lazy { model.transactionManager }
+ protected val balanceManager by lazy { model.balanceManager }
+ protected val devMode get() = model.devMode.value == true
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(model.devMode.value == true)
- }
-
- @Deprecated("Deprecated in Java")
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- requireActivity().apply {
- transaction?.generalTitleRes?.let {
- title = getString(it)
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ transactionManager.selectedTransaction.observe(viewLifecycleOwner) {
+ requireActivity().apply {
+ it?.generalTitleRes?.let {
+ title = getString(it)
+ }
}
}
}
- @Deprecated("Deprecated in Java")
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.transactions_detail, menu)
+ private fun dialogTitle(t: TransactionAction): Int = when (t) {
+ Delete -> R.string.transactions_delete_dialog_title
+ Abort -> R.string.transactions_abort_dialog_title
+ Fail -> R.string.transactions_fail_dialog_title
+ else -> error("unsupported action: $t")
}
- @Deprecated("Deprecated in Java")
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- else -> super.onOptionsItemSelected(item)
- }
+ private fun dialogMessage(t: TransactionAction): Int = when (t) {
+ Delete -> R.string.transactions_delete_dialog_message
+ Abort -> R.string.transactions_abort_dialog_message
+ Fail -> R.string.transactions_fail_dialog_message
+ else -> error("unsupported action: $t")
}
- protected fun bindOrderAndFee(
- orderSummaryView: TextView,
- orderAmountView: TextView,
- orderIdView: TextView,
- feeView: TextView,
- info: TransactionInfo,
- raw: Amount,
- fee: Amount,
- ) {
- orderAmountView.text = raw.toString()
- feeView.text = getString(R.string.amount_negative, fee.toString())
- orderSummaryView.text = if (info.fulfillmentMessage == null) {
- info.summary
- } else {
- "${info.summary}\n\n${info.fulfillmentMessage}"
- }
- if (info.fulfillmentUrl?.startsWith("http", ignoreCase = true) == true) {
- val i = Intent().apply {
- data = Uri.parse(info.fulfillmentUrl)
- }
- orderSummaryView.setOnClickListener { requireContext().startActivitySafe(i) }
- }
- orderIdView.text = getString(R.string.transaction_order_id, info.orderId)
+ private fun dialogButton(t: TransactionAction): Int = when (t) {
+ Delete -> R.string.transactions_delete
+ Abort -> R.string.transactions_abort
+ Fail -> R.string.transactions_fail
+ else -> error("unsupported")
+ }
+
+ protected fun onTransitionButtonClicked(t: Transaction, ta: TransactionAction) = when (ta) {
+ Delete -> showDialog(ta) { deleteTransaction(t) }
+ Abort -> showDialog(ta) { abortTransaction(t) }
+ Fail -> showDialog(ta) { failTransaction(t) }
+ Retry -> retryTransaction(t)
+ Suspend -> suspendTransaction(t)
+ Resume -> resumeTransaction(t)
}
- @StringRes
- protected open val deleteDialogTitle = R.string.transactions_delete
- @StringRes
- protected open val deleteDialogMessage = R.string.transactions_delete_dialog_message
- @StringRes
- protected open val deleteDialogButton = R.string.transactions_delete
-
- protected fun onDeleteButtonClicked(t: Transaction) {
- AlertDialog.Builder(requireContext(), R.style.DialogTheme)
- .setTitle(deleteDialogTitle)
- .setMessage(deleteDialogMessage)
+ private fun showDialog(tt: TransactionAction, onAction: () -> Unit) {
+ MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setTitle(dialogTitle(tt))
+ .setMessage(dialogMessage(tt))
.setNeutralButton(R.string.cancel) { dialog, _ ->
dialog.cancel()
}
- .setNegativeButton(deleteDialogButton) { dialog, _ ->
- deleteTransaction(t)
+ .setNegativeButton(dialogButton(tt)) { dialog, _ ->
+ onAction()
dialog.dismiss()
}
.show()
}
private fun deleteTransaction(t: Transaction) {
- transactionManager.deleteTransaction(t.transactionId)
+ transactionManager.deleteTransaction(t.transactionId) {
+ Log.e(TAG, "Error deleteTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
findNavController().popBackStack()
}
+ private fun retryTransaction(t: Transaction) {
+ transactionManager.retryTransaction(t.transactionId) {
+ Log.e(TAG, "Error retryTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
+ }
+
+ private fun abortTransaction(t: Transaction) {
+ transactionManager.abortTransaction(t.transactionId) {
+ Log.e(TAG, "Error abortTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
+ }
+
+ private fun failTransaction(t: Transaction) {
+ transactionManager.failTransaction(t.transactionId) {
+ Log.e(TAG, "Error failTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
+ }
+
+ private fun suspendTransaction(t: Transaction) {
+ transactionManager.suspendTransaction(t.transactionId) {
+ Log.e(TAG, "Error suspendTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
+ }
+
+ private fun resumeTransaction(t: Transaction) {
+ transactionManager.resumeTransaction(t.transactionId) {
+ Log.e(TAG, "Error resumeTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDummyFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDummyFragment.kt
new file mode 100644
index 0000000..ad5e19c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDummyFragment.kt
@@ -0,0 +1,64 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.unit.dp
+import net.taler.wallet.compose.TalerSurface
+
+class TransactionDummyFragment : TransactionDetailFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState(null).value
+ if (t is DummyTransaction) TransactionDummyComposable(t)
+ }
+ }
+ }
+}
+
+@Composable
+fun TransactionDummyComposable(t: DummyTransaction) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ ErrorTransactionButton(error = t.error)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionLinkComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionLinkComposable.kt
new file mode 100644
index 0000000..e8fca0f
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionLinkComposable.kt
@@ -0,0 +1,86 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import android.R
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.getAttrColor
+
+@Composable
+// FIXME this assumes that it is used in a column and applies its own padding, not really re-usable
+fun TransactionLinkComposable(label: String, info: String, onClick: () -> Unit) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ val context = LocalContext.current
+ val linkColor = Color(context.getAttrColor(R.attr.textColorLink))
+ val annotatedString = buildAnnotatedString {
+ pushStringAnnotation(tag = "url", annotation = info)
+ withStyle(style = SpanStyle(color = linkColor)) {
+ append(info)
+ }
+ pop()
+ }
+ ClickableText(
+ modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
+ text = annotatedString,
+ style = TextStyle(fontSize = 24.sp, textAlign = TextAlign.Center),
+ ) { offset ->
+ annotatedString.getStringAnnotations(
+ tag = "url",
+ start = offset,
+ end = offset,
+ ).firstOrNull()?.let {
+ onClick()
+ }
+ }
+}
+
+@Preview
+@Composable
+fun TransactionLinkComposablePreview() {
+ TalerSurface {
+ Column(
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ TransactionLinkComposable(
+ label = "This is a label",
+ info = "This is some fulfillment message"
+ ) {}
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt
new file mode 100644
index 0000000..9138345
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt
@@ -0,0 +1,163 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.transactions.LossEventType.DenomExpired
+import net.taler.wallet.transactions.LossEventType.DenomUnoffered
+import net.taler.wallet.transactions.LossEventType.DenomVanished
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+
+class TransactionLossFragment: TransactionDetailFragment() {
+ val scope get() = transactionManager.selectedScope
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ val spec = scope?.let { balanceManager.getSpecForScopeInfo(it) }
+
+ TalerSurface {
+ if (t is TransactionDenomLoss) {
+ TransitionLossComposable(t, devMode, spec) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TransitionLossComposable(
+ t: TransactionDenomLoss,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ val context = LocalContext.current
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.loss_amount),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.loss_reason),
+ info = stringResource(
+ when(t.lossEventType) {
+ DenomExpired -> R.string.loss_reason_expired
+ DenomVanished -> R.string.loss_reason_vanished
+ DenomUnoffered -> R.string.loss_reason_unoffered
+ }
+ )
+ )
+
+ TransitionsComposable(t, devMode, onTransition)
+
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
+ }
+}
+
+fun previewLossTransaction(lossEventType: LossEventType) =
+ TransactionDenomLoss(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ amountRaw = Amount.fromString("TESTKUDOS", "0.3"),
+ amountEffective = Amount.fromString("TESTKUDOS", "0.3"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ lossEventType = lossEventType,
+ )
+
+@Composable
+@Preview
+fun TransitionLossComposableExpiredPreview() {
+ val t = previewLossTransaction(DenomExpired)
+ Surface {
+ TransitionLossComposable(t, true, null) {}
+ }
+}
+
+@Composable
+@Preview
+fun TransitionLossComposableVanishedPreview() {
+ val t = previewLossTransaction(DenomVanished)
+ Surface {
+ TransitionLossComposable(t, true, null) {}
+ }
+}
+
+@Composable
+@Preview
+fun TransactionLossComposableUnofferedPreview() {
+ val t = previewLossTransaction(DenomUnoffered)
+ Surface {
+ TransitionLossComposable(t, true, null) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
index bbae22b..d0dec41 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
@@ -23,12 +23,19 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.switchMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.transactions.TransactionAction.Delete
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import org.json.JSONObject
import java.util.LinkedList
sealed class TransactionsResult {
- class Error(val msg: String) : TransactionsResult()
+ class Error(val error: TalerErrorInfo) : TransactionsResult()
class Success(val transactions: List<Transaction>) : TransactionsResult()
}
@@ -40,69 +47,174 @@ class TransactionManager(
private val mProgress = MutableLiveData<Boolean>()
val progress: LiveData<Boolean> = mProgress
- var selectedCurrency: String? = null
- var selectedTransaction: Transaction? = null
+ // FIXME if the app gets killed, this will not be restored and thus be unexpected null
+ // we should keep this in a savable, maybe using Hilt and SavedStateViewModel
+ var selectedScope: ScopeInfo? = null
val searchQuery = MutableLiveData<String>(null)
- private val allTransactions = HashMap<String, List<Transaction>>()
- private val mTransactions = HashMap<String, MutableLiveData<TransactionsResult>>()
+ private val mSelectedTransaction = MutableLiveData<Transaction?>(null)
+ val selectedTransaction: LiveData<Transaction?> = mSelectedTransaction
+ private val allTransactions = HashMap<ScopeInfo, List<Transaction>>()
+ private val mTransactions = HashMap<ScopeInfo, MutableLiveData<TransactionsResult>>()
val transactions: LiveData<TransactionsResult>
@UiThread
get() = searchQuery.switchMap { query ->
- val currency = selectedCurrency
- check(currency != null) { "Did not select currency before getting transactions" }
+ val scopeInfo = selectedScope
+ check(scopeInfo != null) { "Did not select scope before getting transactions" }
loadTransactions(query)
- mTransactions[currency]!! // non-null because filled in [loadTransactions]
+ mTransactions[scopeInfo]!! // non-null because filled in [loadTransactions]
}
@UiThread
fun loadTransactions(searchQuery: String? = null) = scope.launch {
- val currency = selectedCurrency ?: return@launch
- val liveData = mTransactions.getOrPut(currency) { MutableLiveData() }
- if (searchQuery == null && allTransactions.containsKey(currency)) {
- liveData.value = TransactionsResult.Success(allTransactions[currency]!!)
+ val scopeInfo = selectedScope ?: return@launch
+ val liveData = mTransactions.getOrPut(scopeInfo) { MutableLiveData() }
+ if (searchQuery == null && allTransactions.containsKey(scopeInfo)) {
+ liveData.value = TransactionsResult.Success(allTransactions[scopeInfo]!!)
}
if (liveData.value == null) mProgress.value = true
api.request("getTransactions", Transactions.serializer()) {
if (searchQuery != null) put("search", searchQuery)
- put("currency", currency)
+ put("scopeInfo", JSONObject(Json.encodeToString(scopeInfo)))
}.onError {
- liveData.postValue(TransactionsResult.Error(it.userFacingMsg))
+ liveData.postValue(TransactionsResult.Error(it))
mProgress.postValue(false)
}.onSuccess { result ->
val transactions = LinkedList(result.transactions)
- // TODO remove when fixed in wallet-core
- val comparator = compareBy<Transaction>(
- { it.pending },
- { it.timestamp.ms },
- { it.transactionId }
- )
+ val comparator = compareBy<Transaction> { it.txState.major == Pending }
transactions.sortWith(comparator)
transactions.reverse() // show latest first
mProgress.value = false
liveData.value = TransactionsResult.Success(transactions)
- // update all transactions on UiThread if there was a currency
- if (searchQuery == null) allTransactions[currency] = transactions
+ // update selected transaction on UiThread (if it exists)
+ val selected = selectedTransaction.value
+ if (selected != null) transactions.find {
+ it.transactionId == selected.transactionId
+ }?.let {
+ mSelectedTransaction.value = it
+ }
+
+ // update all transactions on UiThread if there was a scope info
+ if (searchQuery == null) allTransactions[scopeInfo] = transactions
+ }
+ }
+
+ /**
+ * Returns true if given [transactionId] was found and selected, false otherwise.
+ */
+ @UiThread
+ suspend fun selectTransaction(transactionId: String): Boolean {
+ var transaction: Transaction? = null
+ api.request("getTransactionById", Transaction.serializer()) {
+ put("transactionId", transactionId)
+ }.onError {
+ Log.e(TAG, "Error getting transaction $it")
+ }.onSuccess { result ->
+ transaction = result
+ }
+ return if (transaction != null) {
+ mSelectedTransaction.value = transaction
+ true
+ } else {
+ false
}
}
- fun deleteTransaction(transactionId: String) = scope.launch {
- api.request<Unit>("deleteTransaction") {
+ suspend fun getTransactionById(transactionId: String): Transaction? {
+ var transaction: Transaction? = null
+ api.request("getTransactionById", Transaction.serializer()) {
put("transactionId", transactionId)
}.onError {
- Log.e(TAG, "Error deleteTransaction $it")
- }.onSuccess {
- // re-load transactions as our list is stale otherwise
- loadTransactions()
+ Log.e(TAG, "Error getting transaction $it")
+ }.onSuccess { result ->
+ transaction = result
}
+ return transaction
+ }
+
+ fun selectTransaction(transaction: Transaction) {
+ mSelectedTransaction.postValue(transaction)
}
- fun deleteTransactions(transactionIds: List<String>) {
- transactionIds.forEach { id ->
- deleteTransaction(id)
+ fun deleteTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("deleteTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ // re-load transactions as our list is stale otherwise
+ loadTransactions()
+ }
+ }
+
+ fun retryTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("retryTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun abortTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("abortTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun failTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("failTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun suspendTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("suspendTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun resumeTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("resumeTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun deleteTransactions(transactionIds: List<String>, onError: (it: TalerErrorInfo) -> Unit) {
+ allTransactions[selectedScope]?.filter { transaction ->
+ transaction.transactionId in transactionIds
+ }?.forEach { toBeDeletedTx ->
+ if (Delete in toBeDeletedTx.txActions) {
+ deleteTransaction(toBeDeletedTx.transactionId) {
+ onError(it)
+ }
+ }
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt
index e2e4f9f..596a4a9 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt
@@ -20,40 +20,32 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import net.taler.common.toAbsoluteTime
-import net.taler.wallet.databinding.FragmentTransactionPaymentBinding
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.platform.ComposeView
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.launchInAppBrowser
+import net.taler.wallet.payment.TransactionPaymentComposable
class TransactionPaymentFragment : TransactionDetailFragment() {
- private lateinit var ui: FragmentTransactionPaymentBinding
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
- ): View {
- ui = FragmentTransactionPaymentBinding.inflate(inflater, container, false)
- return ui.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val t = transaction as TransactionPayment
- ui.timeView.text = t.timestamp.ms.toAbsoluteTime(requireContext())
-
- ui.amountPaidWithFeesView.text = t.amountEffective.toString()
- val fee = t.amountEffective - t.amountRaw
- bindOrderAndFee(
- ui.orderSummaryView,
- ui.orderAmountView,
- ui.orderIdView,
- ui.feeView,
- t.info,
- t.amountRaw,
- fee
- )
- ui.deleteButton.setOnClickListener {
- onDeleteButtonClicked(t)
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionPayment) TransactionPaymentComposable(t, devMode,
+ balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ onFulfill = { url ->
+ launchInAppBrowser(requireContext(), url)
+ },
+ onTransition = {
+ onTransitionButtonClicked(t, it)
+ }
+ )
+ }
}
}
-
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt
index b7c347f..27809a7 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt
@@ -25,9 +25,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -37,6 +38,7 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
import net.taler.common.toAbsoluteTime
import net.taler.wallet.R
import net.taler.wallet.compose.TalerSurface
@@ -54,9 +56,11 @@ class TransactionPeerFragment : TransactionDetailFragment() {
): View = ComposeView(requireContext()).apply {
setContent {
TalerSurface {
- val t = transaction ?: error("No transaction")
- TransactionPeerComposable(t) {
- onDeleteButtonClicked(t)
+ val t = transactionManager.selectedTransaction.observeAsState(null).value
+ if (t != null) TransactionPeerComposable(t, devMode,
+ balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ ) {
+ onTransitionButtonClicked(t, it)
}
}
}
@@ -64,7 +68,12 @@ class TransactionPeerFragment : TransactionDetailFragment() {
}
@Composable
-fun TransactionPeerComposable(t: Transaction, onDelete: () -> Unit) {
+fun TransactionPeerComposable(
+ t: Transaction,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
@@ -76,16 +85,19 @@ fun TransactionPeerComposable(t: Transaction, onDelete: () -> Unit) {
Text(
modifier = Modifier.padding(16.dp),
text = t.timestamp.ms.toAbsoluteTime(context).toString(),
- style = MaterialTheme.typography.body1,
+ style = MaterialTheme.typography.bodyLarge,
)
when (t) {
- is TransactionPeerPullCredit -> TransactionPeerPullCreditComposable(t)
- is TransactionPeerPushCredit -> TransactionPeerPushCreditComposable(t)
- is TransactionPeerPullDebit -> TransactionPeerPullDebitComposable(t)
- is TransactionPeerPushDebit -> TransactionPeerPushDebitComposable(t)
+ is TransactionPeerPullCredit -> TransactionPeerPullCreditComposable(t, spec)
+ is TransactionPeerPushCredit -> TransactionPeerPushCreditComposable(t, spec)
+ is TransactionPeerPullDebit -> TransactionPeerPullDebitComposable(t, spec)
+ is TransactionPeerPushDebit -> TransactionPeerPushDebitComposable(t, spec)
else -> error("unexpected transaction: ${t::class.simpleName}")
}
- DeleteTransactionComposable(onDelete)
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error!!)
+ }
}
}
@@ -94,15 +106,15 @@ fun TransactionAmountComposable(label: String, amount: Amount, amountType: Amoun
Text(
modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
text = label,
- style = MaterialTheme.typography.body2,
+ style = MaterialTheme.typography.bodyMedium,
)
Text(
modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
- text = if (amountType == AmountType.Negative) "-$amount" else amount.toString(),
+ text = amount.toString(negative = amountType == AmountType.Negative),
fontSize = 24.sp,
color = when (amountType) {
AmountType.Positive -> colorResource(R.color.green)
- AmountType.Negative -> colorResource(R.color.red)
+ AmountType.Negative -> MaterialTheme.colorScheme.error
AmountType.Neutral -> Color.Unspecified
},
)
@@ -113,7 +125,7 @@ fun TransactionInfoComposable(label: String, info: String) {
Text(
modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
text = label,
- style = MaterialTheme.typography.body2,
+ style = MaterialTheme.typography.bodyMedium,
)
Text(
modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt
index d44db43..8f474f9 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt
@@ -19,41 +19,102 @@ package net.taler.wallet.transactions
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
-import android.view.View.GONE
import android.view.ViewGroup
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
import net.taler.common.toAbsoluteTime
import net.taler.wallet.R
-import net.taler.wallet.cleanExchange
-import net.taler.wallet.databinding.FragmentTransactionWithdrawalBinding
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionMajorState.Pending
class TransactionRefreshFragment : TransactionDetailFragment() {
- private lateinit var ui: FragmentTransactionWithdrawalBinding
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
- ): View {
- ui = FragmentTransactionWithdrawalBinding.inflate(inflater, container, false)
- return ui.root
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionRefresh) TransactionRefreshComposable(t, devMode,
+ balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ ) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
+ }
}
+}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val t = transaction as TransactionRefresh
- ui.timeView.text = t.timestamp.ms.toAbsoluteTime(requireContext())
-
- ui.effectiveAmountLabel.visibility = GONE
- ui.effectiveAmountView.visibility = GONE
- ui.confirmWithdrawalButton.visibility = GONE
- ui.chosenAmountLabel.visibility = GONE
- ui.chosenAmountView.visibility = GONE
- val fee = t.amountEffective
- ui.feeView.text = getString(R.string.amount_negative, fee.toString())
- ui.exchangeView.text = cleanExchange(t.exchangeBaseUrl)
- ui.deleteButton.setOnClickListener {
- onDeleteButtonClicked(t)
+@Composable
+private fun TransactionRefreshComposable(
+ t: TransactionRefresh,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
}
}
+}
+@Preview
+@Composable
+private fun TransactionRefreshComposablePreview() {
+ val t = TransactionRefresh(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ )
+ Surface {
+ TransactionRefreshComposable(t, true, null) {}
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt
index 5e6eef4..7992565 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt
@@ -20,45 +20,27 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.core.content.ContextCompat.getColor
-import net.taler.common.toAbsoluteTime
-import net.taler.wallet.R
-import net.taler.wallet.databinding.FragmentTransactionPaymentBinding
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.platform.ComposeView
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.refund.TransactionRefundComposable
class TransactionRefundFragment : TransactionDetailFragment() {
- private lateinit var ui: FragmentTransactionPaymentBinding
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
- ): View {
- ui = FragmentTransactionPaymentBinding.inflate(inflater, container, false)
- return ui.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val t = transaction as TransactionRefund
- ui.timeView.text = t.timestamp.ms.toAbsoluteTime(requireContext())
-
- ui.amountPaidWithFeesLabel.text = getString(R.string.transaction_refund)
- ui.amountPaidWithFeesView.setTextColor(getColor(requireContext(), R.color.green))
- ui.amountPaidWithFeesView.text =
- getString(R.string.amount_positive, t.amountEffective.toString())
- val fee = t.amountRaw - t.amountEffective
- bindOrderAndFee(
- ui.orderSummaryView,
- ui.orderAmountView,
- ui.orderIdView,
- ui.feeView,
- t.info,
- t.amountRaw,
- fee
- )
- ui.deleteButton.setOnClickListener {
- onDeleteButtonClicked(t)
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionRefund) TransactionRefundComposable(t, devMode,
+ balanceManager.getSpecForCurrency(t.amountRaw.currency)
+ ) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
}
}
-
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt
new file mode 100644
index 0000000..f89be83
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt
@@ -0,0 +1,99 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TransactionState(
+ val major: TransactionMajorState,
+ val minor: TransactionMinorState? = null,
+) {
+ override fun equals(other: Any?): Boolean {
+ return if (other is TransactionState)
+ // if other.minor is null, then ignore minor in comparison
+ major == other.major && (other.minor == null || minor == other.minor)
+ else false
+ }
+
+ override fun hashCode(): Int {
+ var result = major.hashCode()
+ result = 31 * result + (minor?.hashCode() ?: 0)
+ return result
+ }
+}
+
+@Serializable
+enum class TransactionMajorState {
+ @SerialName("none")
+ None,
+
+ @SerialName("pending")
+ Pending,
+
+ @SerialName("done")
+ Done,
+
+ @SerialName("aborting")
+ Aborting,
+
+ @SerialName("aborted")
+ Aborted,
+
+ @SerialName("suspended")
+ Suspended,
+
+ @SerialName("dialog")
+ Dialog,
+
+ @SerialName("suspended-aborting")
+ SuspendedAborting,
+
+ @SerialName("failed")
+ Failed,
+
+ @SerialName("deleted")
+ Deleted,
+
+ @SerialName("expired")
+ Expired,
+
+ @SerialName("unknown")
+ Unknown;
+}
+
+@Serializable
+enum class TransactionMinorState {
+ @SerialName("kyc")
+ KycRequired,
+
+ @SerialName("exchange")
+ Exchange,
+
+ @SerialName("create-purse")
+ CreatePurse,
+
+ @SerialName("ready")
+ Ready,
+
+ @SerialName("bank-confirm-transfer")
+ BankConfirmTransfer,
+
+ @SerialName("exchange-wait-reserve")
+ ExchangeWaitReserve,
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
index ffc9005..27e59bb 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
@@ -19,104 +19,78 @@ package net.taler.wallet.transactions
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
-import android.view.View.GONE
-import android.view.View.VISIBLE
import android.view.ViewGroup
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
-import net.taler.common.toAbsoluteTime
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
-import net.taler.wallet.cleanExchange
-import net.taler.wallet.databinding.FragmentTransactionWithdrawalBinding
-import net.taler.wallet.handleKyc
+import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.launchInAppBrowser
import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer
import net.taler.wallet.transactions.WithdrawalDetails.TalerBankIntegrationApi
+import net.taler.wallet.withdraw.TransactionWithdrawalComposable
import net.taler.wallet.withdraw.createManualTransferRequired
-class TransactionWithdrawalFragment : TransactionDetailFragment() {
+class TransactionWithdrawalFragment : TransactionDetailFragment(), ActionListener {
private val model: MainViewModel by activityViewModels()
private val withdrawManager by lazy { model.withdrawManager }
- private lateinit var ui: FragmentTransactionWithdrawalBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
- ): View {
- ui = FragmentTransactionWithdrawalBinding.inflate(inflater, container, false)
- return ui.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val t = transaction as TransactionWithdrawal
- ui.timeView.text = t.timestamp.ms.toAbsoluteTime(requireContext())
-
- ui.effectiveAmountLabel.text = getString(R.string.withdraw_total)
- ui.effectiveAmountView.text = t.amountEffective.toString()
- setupConfirmWithdrawalButton(t)
- setupActionButton(t)
- ui.chosenAmountLabel.text = getString(R.string.amount_chosen)
- ui.chosenAmountView.text =
- getString(R.string.amount_positive, t.amountRaw.toString())
- val fee = t.amountRaw - t.amountEffective
- ui.feeView.text = getString(R.string.amount_negative, fee.toString())
- ui.exchangeView.text = cleanExchange(t.exchangeBaseUrl)
- if (t.pending) {
- ui.deleteButton.setIconResource(R.drawable.ic_cancel)
- ui.deleteButton.setText(R.string.cancel)
- }
- ui.deleteButton.setOnClickListener {
- onDeleteButtonClicked(t)
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionWithdrawal) TransactionWithdrawalComposable(
+ t = t,
+ devMode = devMode,
+ spec = balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ actionListener = this@TransactionWithdrawalFragment,
+ ) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
}
}
- override val deleteDialogTitle: Int
- get() = if (transaction?.pending == true) R.string.cancel else super.deleteDialogTitle
- override val deleteDialogMessage: Int
- get() = if (transaction?.pending == true) R.string.transactions_cancel_dialog_message
- else super.deleteDialogMessage
- override val deleteDialogButton: Int
- get() = if (transaction?.pending == true) R.string.ok else super.deleteDialogButton
-
- private fun setupConfirmWithdrawalButton(t: TransactionWithdrawal) {
- if (t.pending && !t.confirmed) {
- if (t.withdrawalDetails is TalerBankIntegrationApi &&
- t.withdrawalDetails.bankConfirmationUrl != null
- ) {
- ui.confirmWithdrawalButton.setOnClickListener {
- launchInAppBrowser(requireContext(), t.withdrawalDetails.bankConfirmationUrl)
- }
- } else if (t.withdrawalDetails is ManualTransfer) {
- ui.confirmWithdrawalButton.setText(R.string.withdraw_manual_ready_details_intro)
- ui.confirmWithdrawalButton.setOnClickListener {
- val status = createManualTransferRequired(
- amount = t.amountRaw,
- exchangeBaseUrl = t.exchangeBaseUrl,
- // TODO what if there's more than one or no URI?
- uriStr = t.withdrawalDetails.exchangePaytoUris[0],
- transactionId = t.transactionId,
- )
- withdrawManager.viewManualWithdrawal(status)
- findNavController().navigate(
- R.id.action_nav_transactions_detail_withdrawal_to_nav_exchange_manual_withdrawal_success
- )
+ override fun onActionButtonClicked(tx: Transaction, type: ActionListener.Type) {
+ when (type) {
+ ActionListener.Type.COMPLETE_KYC -> {
+ if (tx !is TransactionWithdrawal) return
+ tx.kycUrl?.let {
+ launchInAppBrowser(requireContext(), it)
}
- } else ui.confirmWithdrawalButton.visibility = GONE
- } else ui.confirmWithdrawalButton.visibility = GONE
- }
+ }
- private fun setupActionButton(t: TransactionWithdrawal) {
- ui.actionButton.visibility = t.handleKyc({ GONE }) { error ->
- ui.actionButton.setText(R.string.transaction_action_kyc)
- error.kycUrl?.let { kycUrl ->
- ui.actionButton.setOnClickListener {
- launchInAppBrowser(requireContext(), kycUrl)
+ ActionListener.Type.CONFIRM_WITH_BANK -> {
+ if (tx !is TransactionWithdrawal) return
+ if (tx.withdrawalDetails !is TalerBankIntegrationApi) return
+ tx.withdrawalDetails.bankConfirmationUrl?.let { url ->
+ launchInAppBrowser(requireContext(), url)
}
}
- VISIBLE
+
+ ActionListener.Type.CONFIRM_MANUAL -> {
+ if (tx !is TransactionWithdrawal) return
+ if (tx.withdrawalDetails !is ManualTransfer) return
+ if (tx.withdrawalDetails.exchangeCreditAccountDetails.isNullOrEmpty()) return
+ val status = createManualTransferRequired(
+ transactionId = tx.transactionId,
+ exchangeBaseUrl = tx.exchangeBaseUrl,
+ amountRaw = tx.amountRaw,
+ amountEffective = tx.amountEffective,
+ withdrawalAccountList = tx.withdrawalDetails.exchangeCreditAccountDetails,
+ )
+ withdrawManager.viewManualWithdrawal(status)
+ findNavController().navigate(
+ R.id.action_nav_transactions_detail_withdrawal_to_nav_exchange_manual_withdrawal_success,
+ )
+ }
}
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
index 740aab6..7ccdbde 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
@@ -17,30 +17,92 @@
package net.taler.wallet.transactions
import android.content.Context
+import android.util.Log
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.StringRes
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
import kotlinx.serialization.Transient
+import kotlinx.serialization.builtins.ListSerializer
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonElement
import net.taler.common.Amount
import net.taler.common.ContractMerchant
import net.taler.common.ContractProduct
import net.taler.common.Timestamp
import net.taler.wallet.R
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.common.CurrencySpecification
import net.taler.wallet.cleanExchange
+import net.taler.wallet.refund.RefundPaymentInfo
+import net.taler.wallet.transactions.TransactionMajorState.None
+import net.taler.wallet.transactions.TransactionMajorState.Pending
import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer
import net.taler.wallet.transactions.WithdrawalDetails.TalerBankIntegrationApi
+import java.util.UUID
@Serializable
-data class Transactions(val transactions: List<Transaction>)
+data class Transactions(
+ @Serializable(with = TransactionListSerializer::class)
+ val transactions: List<Transaction>,
+)
+
+class TransactionListSerializer : KSerializer<List<Transaction>> {
+ private val serializer = ListSerializer(TransactionSerializer())
+ override val descriptor: SerialDescriptor = serializer.descriptor
+
+ override fun deserialize(decoder: Decoder): List<Transaction> {
+ return decoder.decodeSerializableValue(serializer)
+ }
+
+ override fun serialize(encoder: Encoder, value: List<Transaction>) {
+ throw NotImplementedError()
+ }
+}
+
+class TransactionSerializer : KSerializer<Transaction> {
+
+ private val serializer = Transaction.serializer()
+ override val descriptor: SerialDescriptor = serializer.descriptor
+ private val jsonSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
+
+ override fun deserialize(decoder: Decoder): Transaction {
+ return try {
+ decoder.decodeSerializableValue(serializer)
+ } catch (e: SerializationException) {
+ Log.e(TAG, "Error deserializing transaction.", e)
+ DummyTransaction(
+ transactionId = UUID.randomUUID().toString(),
+ timestamp = Timestamp.now(),
+ error = TalerErrorInfo(
+ code = TalerErrorCode.UNKNOWN,
+ message = e.message,
+ extra = decoder.decodeSerializableValue(jsonSerializer)
+ ),
+ )
+ }
+ }
+
+ override fun serialize(encoder: Encoder, value: Transaction) {
+ throw NotImplementedError()
+ }
+}
@Serializable
sealed class Transaction {
abstract val transactionId: String
abstract val timestamp: Timestamp
- abstract val pending: Boolean
+ abstract val txState: TransactionState
+ abstract val txActions: List<TransactionAction>
abstract val error: TalerErrorInfo?
abstract val amountRaw: Amount
abstract val amountEffective: Amount
@@ -59,6 +121,28 @@ sealed class Transaction {
abstract val generalTitleRes: Int
}
+@Serializable
+enum class TransactionAction {
+ // Common States
+ @SerialName("delete")
+ Delete,
+
+ @SerialName("suspend")
+ Suspend,
+
+ @SerialName("resume")
+ Resume,
+
+ @SerialName("abort")
+ Abort,
+
+ @SerialName("fail")
+ Fail,
+
+ @SerialName("retry")
+ Retry,
+}
+
sealed class AmountType {
object Positive : AmountType()
object Negative : AmountType()
@@ -70,7 +154,9 @@ sealed class AmountType {
class TransactionWithdrawal(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
+ val kycUrl: String? = null,
val exchangeBaseUrl: String,
val withdrawalDetails: WithdrawalDetails,
override val error: TalerErrorInfo? = null,
@@ -86,7 +172,7 @@ class TransactionWithdrawal(
override fun getTitle(context: Context) = cleanExchange(exchangeBaseUrl)
override val generalTitleRes = R.string.withdraw_title
val confirmed: Boolean
- get() = !pending && (
+ get() = txState.major != Pending && (
(withdrawalDetails is TalerBankIntegrationApi && withdrawalDetails.confirmed) ||
withdrawalDetails is ManualTransfer
)
@@ -97,12 +183,7 @@ sealed class WithdrawalDetails {
@Serializable
@SerialName("manual-transfer")
class ManualTransfer(
- /**
- * Payto URIs that the exchange supports.
- *
- * Already contains the amount and message.
- */
- val exchangePaytoUris: List<String>,
+ val exchangeCreditAccountDetails: List<WithdrawalExchangeAccountDetails>? = null,
) : WithdrawalDetails()
@Serializable
@@ -124,16 +205,108 @@ sealed class WithdrawalDetails {
}
@Serializable
+data class WithdrawalExchangeAccountDetails (
+ /**
+ * Payto URI to credit the exchange.
+ *
+ * Depending on whether the (manual!) withdrawal is accepted or just
+ * being checked, this already includes the subject with the
+ * reserve public key.
+ */
+ val paytoUri: String,
+
+ /**
+ * Status that indicates whether the account can be used
+ * by the user to send funds for a withdrawal.
+ *
+ * ok: account should be shown to the user
+ * error: account should not be shown to the user, UIs might render the error (in conversionError),
+ * especially in dev mode.
+ */
+ val status: Status,
+
+ /**
+ * Transfer amount. Might be in a different currency than the requested
+ * amount for withdrawal.
+ *
+ * Redundant with the amount in paytoUri, just included to avoid parsing.
+ */
+ val transferAmount: Amount? = null,
+
+ /**
+ * Currency specification for the external currency.
+ *
+ * Only included if this account requires a currency conversion.
+ */
+ val currencySpecification: CurrencySpecification? = null,
+
+ /**
+ * Further restrictions for sending money to the
+ * exchange.
+ */
+ val creditRestrictions: List<AccountRestriction>? = null,
+
+ /**
+ * Label given to the account or the account's bank by the exchange.
+ */
+ val bankLabel: String? = null,
+
+ val priority: Int? = null,
+) {
+ @Serializable
+ enum class Status {
+ @SerialName("ok")
+ Ok,
+
+ @SerialName("error")
+ Error;
+ }
+}
+
+@Serializable
+sealed class AccountRestriction {
+ @Serializable
+ @SerialName("deny")
+ data object DenyAllAccount: AccountRestriction()
+
+ @Serializable
+ @SerialName("regex")
+ data class RegexAccount(
+ // Regular expression that the payto://-URI of the
+ // partner account must follow. The regular expression
+ // should follow posix-egrep, but without support for character
+ // classes, GNU extensions, back-references or intervals. See
+ // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
+ // for a description of the posix-egrep syntax. Applications
+ // may support regexes with additional features, but exchanges
+ // must not use such regexes.
+ @SerialName("payto_regex")
+ val paytoRegex: String,
+
+ // Hint for a human to understand the restriction
+ // (that is hopefully easier to comprehend than the regex itself).
+ @SerialName("human_hint")
+ val humanHint: String,
+
+ // Map from IETF BCP 47 language tags to localized
+ // human hints.
+ @SerialName("human_hint_i18n")
+ val humanHintI18n: Map<String, String>? = null,
+ ): AccountRestriction()
+}
+
+@Serializable
@SerialName("payment")
class TransactionPayment(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val info: TransactionInfo,
- val status: PaymentStatus,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
+ val posConfirmation: String? = null,
) : Transaction() {
override val icon = R.drawable.ic_cash_usd_outline
override val detailPageNav = R.id.action_nav_transactions_detail_payment
@@ -151,7 +324,7 @@ class TransactionInfo(
val summary: String,
@SerialName("summary_i18n")
val summaryI18n: Map<String, String>? = null,
- val products: List<ContractProduct>,
+ val products: List<ContractProduct> = emptyList(),
val fulfillmentUrl: String? = null,
/**
* Message shown to the user after the payment is complete.
@@ -164,32 +337,14 @@ class TransactionInfo(
)
@Serializable
-enum class PaymentStatus {
- @SerialName("aborted")
- Aborted,
-
- @SerialName("failed")
- Failed,
-
- @SerialName("paid")
- Paid,
-
- @SerialName("accepted")
- Accepted
-}
-
-@Serializable
@SerialName("refund")
class TransactionRefund(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val refundedTransactionId: String,
- val info: TransactionInfo,
- /**
- * Part of the refund that couldn't be applied because the refund permissions were expired
- */
- val amountInvalid: Amount? = null,
+ val paymentInfo: RefundPaymentInfo? = null,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
@@ -200,42 +355,20 @@ class TransactionRefund(
@Transient
override val amountType = AmountType.Positive
override fun getTitle(context: Context): String {
- return context.getString(R.string.transaction_refund_from, info.merchant.name)
+ val merchantName = paymentInfo?.merchant?.name ?: "null"
+ return context.getString(R.string.transaction_refund_from, merchantName)
}
override val generalTitleRes = R.string.refund_title
}
@Serializable
-@SerialName("tip")
-class TransactionTip(
- override val transactionId: String,
- override val timestamp: Timestamp,
- override val pending: Boolean,
- val merchantBaseUrl: String,
- override val error: TalerErrorInfo? = null,
- override val amountRaw: Amount,
- override val amountEffective: Amount,
-) : Transaction() {
- override val icon = R.drawable.transaction_tip_accepted
- override val detailPageNav = R.id.action_nav_transactions_detail_tip
-
- @Transient
- override val amountType = AmountType.Positive
- override fun getTitle(context: Context): String {
- return context.getString(R.string.transaction_tip_from, merchantBaseUrl)
- }
-
- override val generalTitleRes = R.string.tip_title
-}
-
-@Serializable
@SerialName("refresh")
class TransactionRefresh(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
- val exchangeBaseUrl: String,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
@@ -257,7 +390,8 @@ class TransactionRefresh(
class TransactionDeposit(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
@@ -290,7 +424,8 @@ data class PeerInfoShort(
class TransactionPeerPullDebit(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val exchangeBaseUrl: String,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
@@ -317,7 +452,8 @@ class TransactionPeerPullDebit(
class TransactionPeerPullCredit(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val exchangeBaseUrl: String,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
@@ -345,13 +481,14 @@ class TransactionPeerPullCredit(
class TransactionPeerPushDebit(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val exchangeBaseUrl: String,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
val info: PeerInfoShort,
- val talerUri: String,
+ val talerUri: String? = null,
// val completed: Boolean, definitely
) : Transaction() {
override val icon = R.drawable.ic_cash_usd_outline
@@ -374,7 +511,8 @@ class TransactionPeerPushDebit(
class TransactionPeerPushCredit(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val exchangeBaseUrl: String,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
@@ -392,3 +530,65 @@ class TransactionPeerPushCredit(
override val generalTitleRes = R.string.transaction_peer_push_credit
}
+
+/**
+ * A transaction to indicate financial loss due to denominations
+ * that became unusable for deposits.
+ */
+@Serializable
+@SerialName("denom-loss")
+class TransactionDenomLoss(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ val lossEventType: LossEventType,
+): Transaction() {
+ override val icon: Int = R.drawable.transaction_loss
+ override val detailPageNav = R.id.nav_transactions_detail_loss
+
+ @Transient
+ override val amountType: AmountType = AmountType.Negative
+
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_denom_loss)
+ }
+
+ override val generalTitleRes: Int = R.string.transaction_denom_loss
+}
+
+@Serializable
+enum class LossEventType {
+ @SerialName("denom-expired")
+ DenomExpired,
+
+ @SerialName("denom-vanished")
+ DenomVanished,
+
+ @SerialName("denom-unoffered")
+ DenomUnoffered
+}
+
+/**
+ * This represents a transaction that we can not parse for some reason.
+ */
+class DummyTransaction(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val error: TalerErrorInfo,
+) : Transaction() {
+ override val txState: TransactionState = TransactionState(None)
+ override val txActions: List<TransactionAction> = emptyList()
+ override val amountRaw: Amount = Amount.zero("TESTKUDOS")
+ override val amountEffective: Amount = Amount.zero("TESTKUDOS")
+ override val icon: Int = R.drawable.ic_bug_report
+ override val detailPageNav: Int = R.id.nav_transactions_detail_dummy
+ override val amountType: AmountType = AmountType.Neutral
+ override val generalTitleRes: Int = R.string.transaction_dummy_title
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_dummy_title)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
index 09adb3e..5243427 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
@@ -17,6 +17,7 @@
package net.taler.wallet.transactions
import android.os.Bundle
+import android.util.Log
import android.view.ActionMode
import android.view.LayoutInflater
import android.view.Menu
@@ -25,7 +26,6 @@ import android.view.MenuItem
import android.view.View
import android.view.View.INVISIBLE
import android.view.ViewGroup
-import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.fragment.app.Fragment
@@ -36,28 +36,30 @@ import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.selection.StorageStrategy
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
-import net.taler.common.Amount
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import net.taler.common.fadeIn
import net.taler.common.fadeOut
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
+import net.taler.wallet.TAG
+import net.taler.wallet.balances.BalanceState.Success
import net.taler.wallet.databinding.FragmentTransactionsBinding
-import net.taler.wallet.handleKyc
-import net.taler.wallet.launchInAppBrowser
+import net.taler.wallet.showError
interface OnTransactionClickListener {
fun onTransactionClicked(transaction: Transaction)
- fun onActionButtonClicked(transaction: Transaction)
}
class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.Callback {
private val model: MainViewModel by activityViewModels()
private val transactionManager by lazy { model.transactionManager }
+ private val balanceManager by lazy { model.balanceManager }
private lateinit var ui: FragmentTransactionsBinding
private val transactionAdapter by lazy { TransactionAdapter(this) }
- private val currency by lazy { transactionManager.selectedCurrency!! }
+ private val scopeInfo by lazy { transactionManager.selectedScope!! }
private var tracker: SelectionTracker<String>? = null
private var actionMode: ActionMode? = null
@@ -106,11 +108,15 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
}
})
- model.balances.observe(viewLifecycleOwner) { balances ->
+ balanceManager.state.observe(viewLifecycleOwner) { state ->
+ if (state !is Success) return@observe
+ val balances = state.balances
// hide extra fab when in single currency mode (uses MainFragment's FAB)
if (balances.size == 1) ui.mainFab.visibility = INVISIBLE
- balances.find { it.currency == currency }?.available?.let { amount: Amount ->
- ui.amount.text = amount.amountStr
+
+ balances.find { it.scopeInfo == scopeInfo }?.let { balance ->
+ ui.amount.text = balance.available.toString(showSymbol = false)
+ transactionAdapter.setCurrencySpec(balance.available.spec)
}
}
transactionManager.progress.observe(viewLifecycleOwner) { show ->
@@ -123,7 +129,7 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
findNavController().navigate(R.id.sendFunds)
}
ui.receiveButton.setOnClickListener {
- findNavController().navigate(R.id.receiveFunds)
+ findNavController().navigate(R.id.action_global_receiveFunds)
}
ui.mainFab.setOnClickListener {
model.scanCode()
@@ -147,7 +153,7 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
override fun onStart() {
super.onStart()
- requireActivity().title = getString(R.string.transactions_detail_title_currency, currency)
+ requireActivity().title = getString(R.string.transactions_detail_title_currency, scopeInfo.currency)
}
private fun setupSearch(item: MenuItem) {
@@ -175,25 +181,18 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
override fun onTransactionClicked(transaction: Transaction) {
if (actionMode != null) return // don't react on clicks while in action mode
if (transaction.detailPageNav != 0) {
- transactionManager.selectedTransaction = transaction
+ transactionManager.selectTransaction(transaction)
findNavController().navigate(transaction.detailPageNav)
}
}
- override fun onActionButtonClicked(transaction: Transaction) {
- transaction.handleKyc({ error("Unhandled Action Button Event") }) { error ->
- error.kycUrl?.let {
- launchInAppBrowser(requireContext(), it)
- }
- }
- }
-
private fun onTransactionsResult(result: TransactionsResult) = when (result) {
is TransactionsResult.Error -> {
ui.list.fadeOut()
- ui.emptyState.text = getString(R.string.transactions_error, result.msg)
+ ui.emptyState.text = getString(R.string.transactions_error, result.error.userFacingMsg)
ui.emptyState.fadeIn()
}
+
is TransactionsResult.Success -> {
if (result.transactions.isEmpty()) {
val isSearch = transactionManager.searchQuery.value != null
@@ -232,20 +231,31 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
when (item.itemId) {
R.id.transaction_delete -> {
tracker?.selection?.toList()?.let { transactionIds ->
- AlertDialog.Builder(requireContext(), R.style.DialogTheme)
+ MaterialAlertDialogBuilder(
+ requireContext(),
+ R.style.MaterialAlertDialog_Material3,
+ )
.setTitle(R.string.transactions_delete)
.setMessage(R.string.transactions_delete_selected_dialog_message)
.setNeutralButton(R.string.cancel) { dialog, _ ->
dialog.cancel()
}
.setNegativeButton(R.string.transactions_delete) { dialog, _ ->
- transactionManager.deleteTransactions(transactionIds)
+ transactionManager.deleteTransactions(transactionIds) {
+ Log.e(TAG, "Error deleteTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
dialog.dismiss()
}
.show()
}
mode.finish()
}
+
R.id.transaction_select_all -> transactionAdapter.selectAll()
}
return true
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt
new file mode 100644
index 0000000..424cc2a
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt
@@ -0,0 +1,113 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import androidx.compose.foundation.layout.Arrangement.Center
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import net.taler.wallet.R
+import net.taler.wallet.transactions.TransactionAction.*
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun TransitionsComposable(
+ t: Transaction,
+ devMode: Boolean,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ FlowRow(horizontalArrangement = Center) {
+ t.txActions.forEach {
+ if (it in arrayOf(Resume, Suspend)) {
+ if (devMode) TransitionComposable(it, onTransition)
+ } else {
+ TransitionComposable(it, onTransition)
+ }
+ }
+ }
+}
+
+@Composable
+fun TransitionComposable(t: TransactionAction, onClick: (t: TransactionAction) -> Unit) {
+ Button(
+ modifier = Modifier.padding(16.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = when (t) {
+ Delete -> MaterialTheme.colorScheme.error
+ Retry -> MaterialTheme.colorScheme.primary
+ Abort -> MaterialTheme.colorScheme.error
+ Fail -> MaterialTheme.colorScheme.error
+ Resume -> MaterialTheme.colorScheme.primary
+ Suspend -> MaterialTheme.colorScheme.primary
+ }
+ ),
+ onClick = { onClick(t) },
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ painter = when (t) {
+ Delete -> painterResource(id = R.drawable.ic_delete)
+ Retry -> painterResource(id = R.drawable.ic_retry)
+ Abort -> painterResource(id = R.drawable.ic_cancel)
+ Fail -> painterResource(id = R.drawable.ic_fail)
+ Resume -> painterResource(id = R.drawable.ic_resume)
+ Suspend -> painterResource(id = R.drawable.ic_suspend)
+ },
+ contentDescription = null,
+ tint = when (t) {
+ Delete -> MaterialTheme.colorScheme.onError
+ Retry -> MaterialTheme.colorScheme.onPrimary
+ Abort -> MaterialTheme.colorScheme.onError
+ Fail -> MaterialTheme.colorScheme.onError
+ Resume -> MaterialTheme.colorScheme.onPrimary
+ Suspend -> MaterialTheme.colorScheme.onPrimary
+ },
+ )
+ Text(
+ modifier = Modifier.padding(start = 8.dp),
+ text = when (t) {
+ Delete -> stringResource(R.string.transactions_delete)
+ Retry -> stringResource(R.string.transactions_retry)
+ Abort -> stringResource(R.string.transactions_abort)
+ Fail -> stringResource(R.string.transactions_fail)
+ Resume -> stringResource(R.string.transactions_resume)
+ Suspend -> stringResource(R.string.transactions_suspend)
+ },
+ color = when (t) {
+ Delete -> MaterialTheme.colorScheme.onError
+ Retry -> MaterialTheme.colorScheme.onPrimary
+ Abort -> MaterialTheme.colorScheme.onError
+ Fail -> MaterialTheme.colorScheme.onError
+ Resume -> MaterialTheme.colorScheme.onPrimary
+ Suspend -> MaterialTheme.colorScheme.onPrimary
+ },
+ )
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
index dbf901a..56f56f7 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
@@ -23,9 +23,11 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
+import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.common.EventObserver
import net.taler.common.fadeIn
@@ -34,15 +36,21 @@ import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.cleanExchange
import net.taler.wallet.databinding.FragmentPromptWithdrawBinding
+import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.exchanges.SelectExchangeDialogFragment
import net.taler.wallet.withdraw.WithdrawStatus.Loading
import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails
import net.taler.wallet.withdraw.WithdrawStatus.TosReviewRequired
import net.taler.wallet.withdraw.WithdrawStatus.Withdrawing
+import net.taler.wallet.withdraw.WithdrawStatus.NeedsExchange
class PromptWithdrawFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
private val withdrawManager by lazy { model.withdrawManager }
+ private val transactionManager by lazy { model.transactionManager }
+
+ private val selectExchangeDialog = SelectExchangeDialogFragment()
private lateinit var ui: FragmentPromptWithdrawBinding
@@ -61,22 +69,20 @@ class PromptWithdrawFragment : Fragment() {
withdrawManager.withdrawStatus.observe(viewLifecycleOwner) {
showWithdrawStatus(it)
}
- withdrawManager.exchangeSelection.observe(viewLifecycleOwner, EventObserver {
- findNavController().navigate(R.id.action_promptWithdraw_to_selectExchangeFragment)
+
+ selectExchangeDialog.exchangeSelection.observe(viewLifecycleOwner, EventObserver {
+ onExchangeSelected(it)
})
}
private fun showWithdrawStatus(status: WithdrawStatus?): Any = when (status) {
null -> model.showProgressBar.value = false
is Loading -> model.showProgressBar.value = true
- is WithdrawStatus.NeedsExchange -> {
+ is NeedsExchange -> {
model.showProgressBar.value = false
- val exchangeSelection = status.exchangeSelection.getIfNotConsumed()
- if (exchangeSelection == null) { // already consumed
- findNavController().popBackStack()
- } else {
- withdrawManager.selectExchange(exchangeSelection)
- }
+ if (selectExchangeDialog.dialog?.isShowing != true) {
+ selectExchange()
+ } else {}
}
is TosReviewRequired -> onTosReviewRequired(status)
is ReceivedDetails -> onReceivedDetails(status)
@@ -88,9 +94,15 @@ class PromptWithdrawFragment : Fragment() {
is WithdrawStatus.Success -> {
model.showProgressBar.value = false
withdrawManager.withdrawStatus.value = null
- findNavController().navigate(R.id.action_promptWithdraw_to_nav_main)
- model.showTransactions(status.currency)
- Snackbar.make(requireView(), R.string.withdraw_initiated, LENGTH_LONG).show()
+ lifecycleScope.launch {
+ // now select new transaction based on currency and ID
+ if (transactionManager.selectTransaction(status.transactionId)) {
+ findNavController().navigate(R.id.action_promptWithdraw_to_nav_transactions_detail_withdrawal)
+ } else {
+ findNavController().navigate(R.id.action_promptWithdraw_to_nav_main)
+ }
+ Snackbar.make(requireView(), R.string.withdraw_initiated, LENGTH_LONG).show()
+ }
}
is WithdrawStatus.Error -> {
model.showProgressBar.value = false
@@ -103,7 +115,13 @@ class PromptWithdrawFragment : Fragment() {
if (s.showImmediately.getIfNotConsumed() == true) {
findNavController().navigate(R.id.action_promptWithdraw_to_reviewExchangeTOS)
} else {
- showContent(s.amountRaw, s.amountEffective, s.exchangeBaseUrl, s.talerWithdrawUri)
+ showContent(
+ amountRaw = s.amountRaw,
+ amountEffective = s.amountEffective,
+ exchange = s.exchangeBaseUrl,
+ uri = s.talerWithdrawUri,
+ exchanges = s.possibleExchanges,
+ )
ui.confirmWithdrawButton.apply {
text = getString(R.string.withdraw_button_tos)
setOnClickListener {
@@ -115,8 +133,14 @@ class PromptWithdrawFragment : Fragment() {
}
private fun onReceivedDetails(s: ReceivedDetails) {
- showContent(s.amountRaw, s.amountEffective, s.exchangeBaseUrl, s.talerWithdrawUri,
- s.ageRestrictionOptions)
+ showContent(
+ amountRaw = s.amountRaw,
+ amountEffective = s.amountEffective,
+ exchange = s.exchangeBaseUrl,
+ uri = s.talerWithdrawUri,
+ ageRestrictionOptions = s.ageRestrictionOptions,
+ exchanges = s.possibleExchanges,
+ )
ui.confirmWithdrawButton.apply {
text = getString(R.string.withdraw_button_confirm)
setOnClickListener {
@@ -137,6 +161,7 @@ class PromptWithdrawFragment : Fragment() {
amountEffective: Amount,
exchange: String,
uri: String?,
+ exchanges: List<ExchangeItem> = emptyList(),
ageRestrictionOptions: List<Int>? = null,
) {
model.showProgressBar.value = false
@@ -150,20 +175,22 @@ class PromptWithdrawFragment : Fragment() {
ui.chosenAmountView.text = amountRaw.toString()
ui.chosenAmountView.fadeIn()
- ui.feeLabel.fadeIn()
- ui.feeView.text =
- getString(R.string.amount_negative, (amountRaw - amountEffective).toString())
- ui.feeView.fadeIn()
+ val fee = amountRaw - amountEffective
+ if (!fee.isZero()) {
+ ui.feeLabel.fadeIn()
+ ui.feeView.text = getString(R.string.amount_negative, fee.toString())
+ ui.feeView.fadeIn()
+ }
ui.exchangeIntroView.fadeIn()
ui.withdrawExchangeUrl.text = cleanExchange(exchange)
ui.withdrawExchangeUrl.fadeIn()
- if (uri != null) { // no Uri for manual withdrawals
+ // no Uri for manual withdrawals, no selection for single exchange
+ if (uri != null && exchanges.size > 1) {
ui.selectExchangeButton.fadeIn()
ui.selectExchangeButton.setOnClickListener {
- val exchangeSelection = ExchangeSelection(amountRaw, uri)
- withdrawManager.selectExchange(exchangeSelection)
+ selectExchange()
}
}
@@ -179,4 +206,44 @@ class PromptWithdrawFragment : Fragment() {
ui.withdrawCard.fadeIn()
}
+ private fun selectExchange() {
+ val exchanges = when (val status = withdrawManager.withdrawStatus.value) {
+ is ReceivedDetails -> status.possibleExchanges
+ is NeedsExchange -> status.possibleExchanges
+ is TosReviewRequired -> status.possibleExchanges
+ else -> return
+ }
+ selectExchangeDialog.setExchanges(exchanges)
+ selectExchangeDialog.show(parentFragmentManager, "SELECT_EXCHANGE")
+ }
+
+ private fun onExchangeSelected(exchange: ExchangeItem) {
+ val status = withdrawManager.withdrawStatus.value
+ val amount = when (status) {
+ is ReceivedDetails -> status.amountRaw
+ is NeedsExchange -> status.amount
+ is TosReviewRequired -> status.amountRaw
+ else -> return
+ }
+ val uri = when (status) {
+ is ReceivedDetails -> status.talerWithdrawUri
+ is NeedsExchange -> status.talerWithdrawUri
+ is TosReviewRequired -> status.talerWithdrawUri
+ else -> return
+ }
+ val exchanges = when (status) {
+ is ReceivedDetails -> status.possibleExchanges
+ is NeedsExchange -> status.possibleExchanges
+ is TosReviewRequired -> status.possibleExchanges
+ else -> return
+ }
+
+ withdrawManager.getWithdrawalDetails(
+ exchangeBaseUrl = exchange.exchangeBaseUrl,
+ amount = amount,
+ showTosImmediately = false,
+ uri = uri,
+ possibleExchanges = exchanges,
+ )
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
new file mode 100644
index 0000000..20f8280
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
@@ -0,0 +1,155 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.Timestamp
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.cleanExchange
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.transactions.ActionButton
+import net.taler.wallet.transactions.ActionListener
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.ErrorTransactionButton
+import net.taler.wallet.transactions.Transaction
+import net.taler.wallet.transactions.TransactionAction
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionState
+import net.taler.wallet.transactions.TransactionWithdrawal
+import net.taler.wallet.transactions.TransitionsComposable
+import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
+
+@Composable
+fun TransactionWithdrawalComposable(
+ t: TransactionWithdrawal,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ actionListener: ActionListener,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+
+ ActionButton(tx = t, listener = actionListener)
+
+ TransactionAmountComposable(
+ label = stringResource(R.string.amount_chosen),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+
+ val fee = t.amountRaw - t.amountEffective
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_total),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Positive,
+ )
+
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.withdraw_exchange),
+ info = cleanExchange(t.exchangeBaseUrl),
+ )
+
+ TransitionsComposable(t, devMode, onTransition)
+
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
+ }
+}
+
+@Preview
+@Composable
+fun TransactionWithdrawalComposablePreview() {
+ val t = TransactionWithdrawal(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ exchangeBaseUrl = "https://exchange.demo.taler.net/",
+ withdrawalDetails = ManualTransfer(
+ exchangeCreditAccountDetails = listOf(
+ WithdrawalExchangeAccountDetails(
+ paytoUri = "payto://IBAN/1231231231",
+ transferAmount = Amount.fromJSONString("NETZBON:42.23"),
+ status = WithdrawalExchangeAccountDetails.Status.Ok,
+ currencySpecification = CurrencySpecification(
+ name = "NETZBON",
+ numFractionalInputDigits = 2,
+ numFractionalNormalDigits = 2,
+ numFractionalTrailingZeroDigits = 2,
+ altUnitNames = mapOf(0 to "NETZBON"),
+ ),
+ ),
+ ),
+ ),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ )
+
+ val listener = object : ActionListener {
+ override fun onActionButtonClicked(tx: Transaction, type: ActionListener.Type) {}
+ }
+
+ Surface {
+ TransactionWithdrawalComposable(t, true, null, listener) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
index 1698a10..e308b2a 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
@@ -19,7 +19,7 @@ package net.taler.wallet.withdraw
import android.net.Uri
import android.util.Log
import androidx.annotation.UiThread
-import androidx.lifecycle.LiveData
+import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -31,23 +31,32 @@ import net.taler.common.toEvent
import net.taler.wallet.TAG
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.exchanges.ExchangeFees
import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails
sealed class WithdrawStatus {
data class Loading(val talerWithdrawUri: String? = null) : WithdrawStatus()
- data class NeedsExchange(val exchangeSelection: Event<ExchangeSelection>) : WithdrawStatus()
+
+ data class NeedsExchange(
+ val talerWithdrawUri: String,
+ val amount: Amount,
+ val possibleExchanges: List<ExchangeItem>,
+ ) : WithdrawStatus()
data class TosReviewRequired(
val talerWithdrawUri: String? = null,
val exchangeBaseUrl: String,
val amountRaw: Amount,
val amountEffective: Amount,
+ val withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
val ageRestrictionOptions: List<Int>? = null,
val tosText: String,
val tosEtag: String,
val showImmediately: Event<Boolean>,
+ val possibleExchanges: List<ExchangeItem> = emptyList(),
) : WithdrawStatus()
data class ReceivedDetails(
@@ -55,38 +64,62 @@ sealed class WithdrawStatus {
val exchangeBaseUrl: String,
val amountRaw: Amount,
val amountEffective: Amount,
+ val withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
val ageRestrictionOptions: List<Int>? = null,
+ val possibleExchanges: List<ExchangeItem> = emptyList(),
) : WithdrawStatus()
- object Withdrawing : WithdrawStatus()
- data class Success(val currency: String) : WithdrawStatus()
- sealed class ManualTransferRequired : WithdrawStatus() {
- abstract val uri: Uri
- abstract val transactionId: String?
- }
+ data object Withdrawing : WithdrawStatus()
- data class ManualTransferRequiredIBAN(
- val exchangeBaseUrl: String,
- override val uri: Uri,
- val iban: String,
- val subject: String,
- val amountRaw: Amount,
- override val transactionId: String?,
- ) : ManualTransferRequired()
+ data class Success(val currency: String, val transactionId: String) : WithdrawStatus()
- data class ManualTransferRequiredBitcoin(
+ class ManualTransferRequired(
+ val transactionId: String?,
+ val transactionAmountRaw: Amount,
+ val transactionAmountEffective: Amount,
val exchangeBaseUrl: String,
- override val uri: Uri,
- val account: String,
- val segwitAddrs: List<String>,
- val subject: String,
- val amountRaw: Amount,
- override val transactionId: String?,
- ) : ManualTransferRequired()
+ val withdrawalTransfers: List<TransferData>,
+ ) : WithdrawStatus()
data class Error(val message: String?) : WithdrawStatus()
}
+sealed class TransferData {
+ abstract val subject: String
+ abstract val amountRaw: Amount
+ abstract val amountEffective: Amount
+ abstract val withdrawalAccount: WithdrawalExchangeAccountDetails
+
+ val currency get() = withdrawalAccount.transferAmount?.currency
+
+ data class Taler(
+ override val subject: String,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ override val withdrawalAccount: WithdrawalExchangeAccountDetails,
+ val receiverName: String? = null,
+ val account: String,
+ ): TransferData()
+
+ data class IBAN(
+ override val subject: String,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ override val withdrawalAccount: WithdrawalExchangeAccountDetails,
+ val receiverName: String? = null,
+ val iban: String,
+ ): TransferData()
+
+ data class Bitcoin(
+ override val subject: String,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ override val withdrawalAccount: WithdrawalExchangeAccountDetails,
+ val account: String,
+ val segwitAddresses: List<String>,
+ ): TransferData()
+}
+
sealed class WithdrawTestStatus {
object Withdrawing : WithdrawTestStatus()
object Success : WithdrawTestStatus()
@@ -101,21 +134,31 @@ data class WithdrawalDetailsForUri(
)
@Serializable
-data class WithdrawalDetails(
+data class WithdrawExchangeResponse(
+ val exchangeBaseUrl: String,
+ val amount: Amount? = null,
+)
+
+@Serializable
+data class ManualWithdrawalDetails(
val tosAccepted: Boolean,
val amountRaw: Amount,
val amountEffective: Amount,
+ val withdrawalAccountsList: List<WithdrawalExchangeAccountDetails>,
val ageRestrictionOptions: List<Int>? = null,
+ val scopeInfo: ScopeInfo,
)
@Serializable
-data class AcceptManualWithdrawalResponse(
- val exchangePaytoUris: List<String>,
+data class AcceptWithdrawalResponse(
+ val transactionId: String,
)
-data class ExchangeSelection(
- val amount: Amount,
- val talerWithdrawUri: String,
+@Serializable
+data class AcceptManualWithdrawalResponse(
+ val reservePub: String,
+ val withdrawalAccountsList: List<WithdrawalExchangeAccountDetails>,
+ val transactionId: String,
)
class WithdrawManager(
@@ -126,8 +169,6 @@ class WithdrawManager(
val withdrawStatus = MutableLiveData<WithdrawStatus>()
val testWithdrawalStatus = MutableLiveData<WithdrawTestStatus>()
- private val _exchangeSelection = MutableLiveData<Event<ExchangeSelection>>()
- val exchangeSelection: LiveData<Event<ExchangeSelection>> = _exchangeSelection
var exchangeFees: ExchangeFees? = null
private set
@@ -140,11 +181,6 @@ class WithdrawManager(
}
}
- @UiThread
- fun selectExchange(selection: ExchangeSelection) {
- _exchangeSelection.value = selection.toEvent()
- }
-
fun getWithdrawalDetails(uri: String) = scope.launch {
withdrawStatus.value = WithdrawStatus.Loading(uri)
api.request("getWithdrawalDetailsForUri", WithdrawalDetailsForUri.serializer()) {
@@ -153,13 +189,17 @@ class WithdrawManager(
handleError("getWithdrawalDetailsForUri", error)
}.onSuccess { details ->
if (details.defaultExchangeBaseUrl == null) {
- val exchangeSelection = ExchangeSelection(details.amount, uri)
- withdrawStatus.value = WithdrawStatus.NeedsExchange(exchangeSelection.toEvent())
+ withdrawStatus.value = WithdrawStatus.NeedsExchange(
+ talerWithdrawUri = uri,
+ amount = details.amount,
+ possibleExchanges = details.possibleExchanges,
+ )
} else getWithdrawalDetails(
exchangeBaseUrl = details.defaultExchangeBaseUrl,
amount = details.amount,
showTosImmediately = false,
uri = uri,
+ possibleExchanges = details.possibleExchanges,
)
}
}
@@ -169,9 +209,10 @@ class WithdrawManager(
amount: Amount,
showTosImmediately: Boolean = false,
uri: String? = null,
+ possibleExchanges: List<ExchangeItem> = emptyList(),
) = scope.launch {
withdrawStatus.value = WithdrawStatus.Loading(uri)
- api.request("getWithdrawalDetailsForAmount", WithdrawalDetails.serializer()) {
+ api.request("getWithdrawalDetailsForAmount", ManualWithdrawalDetails.serializer()) {
put("exchangeBaseUrl", exchangeBaseUrl)
put("amount", amount.toJSONString())
}.onError { error ->
@@ -183,17 +224,34 @@ class WithdrawManager(
exchangeBaseUrl = exchangeBaseUrl,
amountRaw = details.amountRaw,
amountEffective = details.amountEffective,
+ withdrawalAccountList = details.withdrawalAccountsList,
ageRestrictionOptions = details.ageRestrictionOptions,
+ possibleExchanges = possibleExchanges,
)
- } else getExchangeTos(exchangeBaseUrl, details, showTosImmediately, uri)
+ } else getExchangeTos(exchangeBaseUrl, details, showTosImmediately, uri, possibleExchanges)
+ }
+ }
+
+ @WorkerThread
+ suspend fun prepareManualWithdrawal(uri: String): WithdrawExchangeResponse? {
+ withdrawStatus.postValue(WithdrawStatus.Loading(uri))
+ var response: WithdrawExchangeResponse? = null
+ api.request("prepareWithdrawExchange", WithdrawExchangeResponse.serializer()) {
+ put("talerUri", uri)
+ }.onError {
+ handleError("prepareWithdrawExchange", it)
+ }.onSuccess {
+ response = it
}
+ return response
}
private fun getExchangeTos(
exchangeBaseUrl: String,
- details: WithdrawalDetails,
+ details: ManualWithdrawalDetails,
showImmediately: Boolean,
uri: String?,
+ possibleExchanges: List<ExchangeItem>,
) = scope.launch {
api.request("getExchangeTos", TosResponse.serializer()) {
put("exchangeBaseUrl", exchangeBaseUrl)
@@ -205,10 +263,12 @@ class WithdrawManager(
exchangeBaseUrl = exchangeBaseUrl,
amountRaw = details.amountRaw,
amountEffective = details.amountEffective,
+ withdrawalAccountList = details.withdrawalAccountsList,
ageRestrictionOptions = details.ageRestrictionOptions,
tosText = it.content,
tosEtag = it.currentEtag,
showImmediately = showImmediately.toEvent(),
+ possibleExchanges = possibleExchanges,
)
}
}
@@ -229,7 +289,9 @@ class WithdrawManager(
exchangeBaseUrl = s.exchangeBaseUrl,
amountRaw = s.amountRaw,
amountEffective = s.amountEffective,
+ withdrawalAccountList = s.withdrawalAccountList,
ageRestrictionOptions = s.ageRestrictionOptions,
+ possibleExchanges = s.possibleExchanges,
)
}
}
@@ -249,14 +311,15 @@ class WithdrawManager(
status: ReceivedDetails,
restrictAge: Int? = null,
) {
- api.request<Unit>("acceptBankIntegratedWithdrawal") {
+ api.request("acceptBankIntegratedWithdrawal", AcceptWithdrawalResponse.serializer()) {
restrictAge?.let { put("restrictAge", restrictAge) }
put("exchangeBaseUrl", status.exchangeBaseUrl)
put("talerWithdrawUri", status.talerWithdrawUri)
}.onError {
handleError("acceptBankIntegratedWithdrawal", it)
}.onSuccess {
- withdrawStatus.value = WithdrawStatus.Success(status.amountRaw.currency)
+ withdrawStatus.value =
+ WithdrawStatus.Success(status.amountRaw.currency, it.transactionId)
}
}
@@ -269,18 +332,15 @@ class WithdrawManager(
handleError("acceptManualWithdrawal", it)
}.onSuccess { response ->
withdrawStatus.value = createManualTransferRequired(
- amount = status.amountRaw,
- exchangeBaseUrl = status.exchangeBaseUrl,
- // TODO what if there's more than one or no URI?
- uriStr = response.exchangePaytoUris[0],
+ status = status,
+ response = response,
)
}
}
- @UiThread
private fun handleError(operation: String, error: TalerErrorInfo) {
Log.e(TAG, "Error $operation $error")
- withdrawStatus.value = WithdrawStatus.Error(error.userFacingMsg)
+ withdrawStatus.postValue(WithdrawStatus.Error(error.userFacingMsg))
}
/**
@@ -295,33 +355,60 @@ class WithdrawManager(
}
fun createManualTransferRequired(
- amount: Amount,
+ transactionId: String,
exchangeBaseUrl: String,
- uriStr: String,
- transactionId: String? = null,
-): WithdrawStatus.ManualTransferRequired {
- val uri = Uri.parse(uriStr.replace("receiver-name=", "receiver_name="))
- if ("bitcoin".equals(uri.authority, true)) {
- val msg = uri.getQueryParameter("message").orEmpty()
- val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg)
- val reserve = reg?.value ?: uri.getQueryParameter("subject")!!
- val segwitAddrs = Bech32.generateFakeSegwitAddress(reserve, uri.pathSegments.first())
- return WithdrawStatus.ManualTransferRequiredBitcoin(
- exchangeBaseUrl = exchangeBaseUrl,
- uri = uri,
- account = uri.lastPathSegment!!,
- segwitAddrs = segwitAddrs,
- subject = reserve,
- amountRaw = amount,
- transactionId = transactionId,
- )
- }
- return WithdrawStatus.ManualTransferRequiredIBAN(
- exchangeBaseUrl = exchangeBaseUrl,
- uri = uri,
- iban = uri.lastPathSegment!!,
- subject = uri.getQueryParameter("message") ?: "Error: No message in URI",
- amountRaw = amount,
- transactionId = transactionId,
- )
-}
+ amountRaw: Amount,
+ amountEffective: Amount,
+ withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
+) = WithdrawStatus.ManualTransferRequired(
+ transactionId = transactionId,
+ transactionAmountRaw = amountRaw,
+ transactionAmountEffective = amountEffective,
+ exchangeBaseUrl = exchangeBaseUrl,
+ withdrawalTransfers = withdrawalAccountList.mapNotNull {
+ val uri = Uri.parse(it.paytoUri.replace("receiver-name=", "receiver_name="))
+ if ("bitcoin".equals(uri.authority, true)) {
+ val msg = uri.getQueryParameter("message").orEmpty()
+ val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg)
+ val reserve = reg?.value ?: uri.getQueryParameter("subject")!!
+ val segwitAddresses = Bech32.generateFakeSegwitAddress(reserve, uri.pathSegments.first())
+ TransferData.Bitcoin(
+ account = uri.lastPathSegment!!,
+ segwitAddresses = segwitAddresses,
+ subject = reserve,
+ amountRaw = amountRaw,
+ amountEffective = amountEffective,
+ withdrawalAccount = it.copy(paytoUri = uri.toString())
+ )
+ } else if (uri.authority.equals("x-taler-bank", true)) {
+ TransferData.Taler(
+ account = uri.lastPathSegment!!,
+ receiverName = uri.getQueryParameter("receiver_name"),
+ subject = uri.getQueryParameter("message") ?: "Error: No message in URI",
+ amountRaw = amountRaw,
+ amountEffective = amountEffective,
+ withdrawalAccount = it.copy(paytoUri = uri.toString()),
+ )
+ } else if (uri.authority.equals("iban", true)) {
+ TransferData.IBAN(
+ iban = uri.lastPathSegment!!,
+ receiverName = uri.getQueryParameter("receiver_name"),
+ subject = uri.getQueryParameter("message") ?: "Error: No message in URI",
+ amountRaw = amountRaw,
+ amountEffective = amountEffective,
+ withdrawalAccount = it.copy(paytoUri = uri.toString()),
+ )
+ } else null
+ },
+)
+
+fun createManualTransferRequired(
+ status: ReceivedDetails,
+ response: AcceptManualWithdrawalResponse,
+): WithdrawStatus.ManualTransferRequired = createManualTransferRequired(
+ transactionId = response.transactionId,
+ exchangeBaseUrl = status.exchangeBaseUrl,
+ amountRaw = status.amountRaw,
+ amountEffective = status.amountEffective,
+ withdrawalAccountList = response.withdrawalAccountsList,
+) \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt
index aae8c95..c499c3b 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt
@@ -25,6 +25,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import net.taler.common.Amount
+import net.taler.common.AmountParserException
import net.taler.common.hideKeyboard
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
@@ -73,14 +74,13 @@ class ManualWithdrawFragment : Fragment() {
return
}
ui.amountLayout.error = null
- val value: Double
+ val amount: Amount
try {
- value = ui.amountView.text.toString().replace(',', '.').toDouble()
- } catch (e: NumberFormatException) {
+ amount = Amount.fromString(currency, ui.amountView.text.toString())
+ } catch (e: AmountParserException) {
ui.amountLayout.error = getString(R.string.withdraw_amount_error)
return
}
- val amount = Amount.fromDouble(currency, value)
ui.amountView.hideKeyboard()
withdrawManager.getWithdrawalDetails(exchangeItem.exchangeBaseUrl, amount)
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
index 3102123..63413c2 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
@@ -16,59 +16,78 @@
package net.taler.wallet.withdraw.manual
-import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
-import net.taler.common.startActivitySafe
+import net.taler.common.openUri
+import net.taler.common.shareText
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.withdraw.TransferData
import net.taler.wallet.withdraw.WithdrawStatus
class ManualWithdrawSuccessFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
- private val transactionManager by lazy { model.transactionManager }
private val withdrawManager by lazy { model.withdrawManager }
+ private val balanceManager by lazy { model.balanceManager }
+
+ private lateinit var status: WithdrawStatus.ManualTransferRequired
+
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View = ComposeView(requireContext()).apply {
- val status = withdrawManager.withdrawStatus.value as WithdrawStatus.ManualTransferRequired
- val intent = Intent().apply {
- data = status.uri
- }
- // TODO test if this works with an actual payto:// handling app
- val componentName = intent.resolveActivity(requireContext().packageManager)
- val onBankAppClick = if (componentName == null) null else {
- { requireContext().startActivitySafe(intent) }
- }
- val tid = status.transactionId
- val onCancelClick = if (tid == null) null else {
- {
- transactionManager.deleteTransaction(tid)
- findNavController().navigate(R.id.action_nav_exchange_manual_withdrawal_success_to_nav_main)
+ status = withdrawManager.withdrawStatus.value as WithdrawStatus.ManualTransferRequired
+
+ // Set action bar subtitle and unset on exit
+ if (status.withdrawalTransfers.size > 1) {
+ val activity = requireActivity() as AppCompatActivity
+
+ activity.apply {
+ supportActionBar?.subtitle = getString(R.string.withdraw_subtitle)
+ }
+
+ findNavController().addOnDestinationChangedListener { controller, destination, args ->
+ if (destination.id != R.id.nav_exchange_manual_withdrawal_success) {
+ activity.apply {
+ supportActionBar?.subtitle = null
+ }
+ }
}
}
+
setContent {
TalerSurface {
- when (status) {
- is WithdrawStatus.ManualTransferRequiredBitcoin -> {
- ScreenBitcoin(status, onBankAppClick, onCancelClick)
- }
- is WithdrawStatus.ManualTransferRequiredIBAN -> {
- ScreenIBAN(status, onBankAppClick, onCancelClick)
- }
- }
+ ScreenTransfer(
+ status = status,
+ spec = balanceManager.getSpecForCurrency(status.transactionAmountRaw.currency),
+ bankAppClick = { onBankAppClick(it) },
+ shareClick = { onShareClick(it) },
+ )
}
}
}
+ private fun onBankAppClick(transfer: TransferData) {
+ requireContext().openUri(
+ uri = transfer.withdrawalAccount.paytoUri,
+ title = requireContext().getString(R.string.share_payment)
+ )
+ }
+
+ private fun onShareClick(transfer: TransferData) {
+ requireContext().shareText(
+ text = transfer.withdrawalAccount.paytoUri,
+ )
+ }
+
override fun onStart() {
super.onStart()
activity?.setTitle(R.string.withdraw_title)
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt
deleted file mode 100644
index d6ed7df..0000000
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.withdraw.manual
-
-import android.net.Uri
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Alignment.Companion.End
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.em
-import net.taler.common.Amount
-import net.taler.wallet.CURRENCY_BTC
-import net.taler.wallet.R
-import net.taler.wallet.compose.CopyToClipboardButton
-import net.taler.wallet.withdraw.WithdrawStatus
-
-@Composable
-fun ScreenBitcoin(
- status: WithdrawStatus.ManualTransferRequiredBitcoin,
- bankAppClick: (() -> Unit)?,
- onCancelClick: (() -> Unit)?,
-) {
- val scrollState = rememberScrollState()
- Column(modifier = Modifier
- .wrapContentWidth(Alignment.CenterHorizontally)
- .verticalScroll(scrollState)
- .padding(all = 16.dp)
- ) {
- Text(
- text = stringResource(R.string.withdraw_manual_bitcoin_title),
- style = MaterialTheme.typography.h5,
- )
- Text(
- text = stringResource(R.string.withdraw_manual_bitcoin_intro),
- style = MaterialTheme.typography.body1,
- modifier = Modifier
- .padding(vertical = 8.dp)
- )
- BitcoinSegwitAddrs(
- amount = status.amountRaw,
- addr = status.account,
- segwitAddresses = status.segwitAddrs
- )
- if (bankAppClick != null) {
- Button(
- onClick = bankAppClick,
- modifier = Modifier
- .padding(vertical = 16.dp)
- .align(Alignment.CenterHorizontally),
- ) {
- Text(text = stringResource(R.string.withdraw_manual_ready_bank_button))
- }
- }
- if (onCancelClick != null) {
- Button(
- onClick = onCancelClick,
- colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(R.color.red)),
- modifier = Modifier
- .padding(vertical = 16.dp)
- .align(End),
- ) {
- Text(
- text = stringResource(R.string.withdraw_manual_ready_cancel),
- color = Color.White,
- )
- }
- }
- }
-}
-
-@Composable
-fun BitcoinSegwitAddrs(amount: Amount, addr: String, segwitAddresses: List<String>) {
- Column {
- CopyToClipboardButton(
- modifier = Modifier.align(End),
- label = "Bitcoin",
- content = getCopyText(amount, addr, segwitAddresses),
- )
- Row(modifier = Modifier.padding(vertical = 8.dp)) {
- Column(modifier = Modifier.weight(0.3f)) {
- Text(
- text = addr,
- style = MaterialTheme.typography.body1,
- fontWeight = FontWeight.Normal,
- fontSize = 3.em
- )
- Text(
- text = amount.withCurrency("BTC").toString(),
- style = MaterialTheme.typography.body1,
- fontWeight = FontWeight.Bold,
- )
- }
- }
- for (segwitAddress in segwitAddresses) {
- Row(modifier = Modifier.padding(vertical = 8.dp)) {
- Column(modifier = Modifier.weight(0.3f)) {
- Text(
- text = segwitAddress,
- style = MaterialTheme.typography.body1,
- fontWeight = FontWeight.Normal,
- fontSize = 3.em,
- )
- Text(
- text = SEGWIT_MIN.toString(),
- style = MaterialTheme.typography.body1,
- fontWeight = FontWeight.Bold,
- )
- }
- }
- }
- }
-}
-
-private val SEGWIT_MIN = Amount("BTC", 0, 294)
-
-private fun getCopyText(amount: Amount, addr: String, segwitAddresses: List<String>): String {
- val sr = segwitAddresses.joinToString(separator = "\n") { s ->
- "\n$s ${SEGWIT_MIN}\n"
- }
- return "$addr ${amount.withCurrency("BTC")}\n$sr"
-}
-
-@Preview
-@Composable
-fun PreviewScreenBitcoin() {
- Surface {
- ScreenBitcoin(WithdrawStatus.ManualTransferRequiredBitcoin(
- exchangeBaseUrl = "bitcoin.ice.bfh.ch",
- uri = Uri.parse("https://taler.net"),
- account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
- segwitAddrs = listOf(
- "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq",
- "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c"
- ),
- subject = "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- amountRaw = Amount(CURRENCY_BTC, 0, 14000000),
- transactionId = "",
- ), {}) {}
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt
deleted file mode 100644
index 79ca364..0000000
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.withdraw.manual
-
-import android.net.Uri
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ContentCopy
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import net.taler.common.Amount
-import net.taler.wallet.R
-import net.taler.wallet.compose.copyToClipBoard
-import net.taler.wallet.withdraw.WithdrawStatus
-
-@Composable
-fun ScreenIBAN(
- status: WithdrawStatus.ManualTransferRequiredIBAN,
- bankAppClick: (() -> Unit)?,
- onCancelClick: (() -> Unit)?,
-) {
- val scrollState = rememberScrollState()
- Column(modifier = Modifier
- .wrapContentWidth(Alignment.CenterHorizontally)
- .verticalScroll(scrollState)
- .padding(all = 16.dp)
- ) {
- Text(
- text = stringResource(R.string.withdraw_manual_ready_title),
- style = MaterialTheme.typography.h5,
- )
- Text(
- text = stringResource(R.string.withdraw_manual_ready_intro,
- status.amountRaw.toString()),
- style = MaterialTheme.typography.body1,
- modifier = Modifier
- .padding(vertical = 8.dp)
- )
- DetailRow(stringResource(R.string.withdraw_manual_ready_iban), status.iban)
- DetailRow(stringResource(R.string.withdraw_manual_ready_subject), status.subject)
- DetailRow(stringResource(R.string.amount_chosen), status.amountRaw.toString())
- DetailRow(stringResource(R.string.withdraw_exchange), status.exchangeBaseUrl, false)
- Text(
- text = stringResource(R.string.withdraw_manual_ready_warning),
- style = MaterialTheme.typography.body2,
- color = colorResource(R.color.notice_text),
- modifier = Modifier
- .align(Alignment.CenterHorizontally)
- .padding(all = 8.dp)
- .background(colorResource(R.color.notice_background))
- .border(BorderStroke(2.dp, colorResource(R.color.notice_border)))
- .padding(all = 16.dp)
- )
- if (bankAppClick != null) {
- Button(
- onClick = bankAppClick,
- modifier = Modifier
- .padding(vertical = 16.dp)
- .align(Alignment.CenterHorizontally),
- ) {
- Text(text = stringResource(R.string.withdraw_manual_ready_bank_button))
- }
- }
- if (onCancelClick != null) {
- Button(
- onClick = onCancelClick,
- colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(R.color.red)),
- modifier = Modifier
- .padding(vertical = 16.dp)
- .align(Alignment.End),
- ) {
- Text(
- text = stringResource(R.string.withdraw_manual_ready_cancel),
- color = Color.White,
- )
- }
- }
- }
-}
-
-@Composable
-fun DetailRow(label: String, content: String, copy: Boolean = true) {
- val context = LocalContext.current
- Row {
- Column(
- modifier = Modifier
- .weight(0.3f)) {
- Text(
- text = label,
- style = MaterialTheme.typography.body1,
- fontWeight = if (copy) FontWeight.Bold else FontWeight.Normal,
- )
- if (copy) {
- IconButton(
- onClick = { copyToClipBoard(context, label, content) },
- ) { Icon(Icons.Default.ContentCopy, stringResource(R.string.copy)) }
- }
- }
- Text(
- text = content,
- style = MaterialTheme.typography.body1,
- modifier = Modifier
- .padding(bottom = 8.dp)
- .weight(0.7f)
- .then(if (copy) Modifier else Modifier.alpha(0.7f))
- )
- }
-}
-
-@Preview
-@Composable
-fun PreviewScreenIBAN() {
- Surface {
- ScreenIBAN(WithdrawStatus.ManualTransferRequiredIBAN(
- exchangeBaseUrl = "test.exchange.taler.net",
- uri = Uri.parse("https://taler.net"),
- iban = "ASDQWEASDZXCASDQWE",
- subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG",
- amountRaw = Amount("KUDOS", 10, 0),
- transactionId = "",
- ), {}) {}
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt
new file mode 100644
index 0000000..75d03b5
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt
@@ -0,0 +1,326 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.CURRENCY_BTC
+import net.taler.wallet.R
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.compose.ShareButton
+import net.taler.wallet.compose.copyToClipBoard
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails.Status.*
+import net.taler.wallet.withdraw.TransferData
+import net.taler.wallet.withdraw.WithdrawStatus
+
+@Composable
+fun ScreenTransfer(
+ status: WithdrawStatus.ManualTransferRequired,
+ spec: CurrencySpecification?,
+ bankAppClick: ((transfer: TransferData) -> Unit)?,
+ shareClick: ((transfer: TransferData) -> Unit)?,
+) {
+ // TODO: show some placeholder
+ if (status.withdrawalTransfers.isEmpty()) return
+
+ val transfers = status.withdrawalTransfers.filter {
+ // TODO: in dev mode, show debug info when status is `Error'
+ it.withdrawalAccount.status == Ok
+ }.sortedByDescending {
+ it.withdrawalAccount.priority
+ }
+
+ val defaultTransfer = transfers[0]
+ var selectedTransfer by remember { mutableStateOf(defaultTransfer) }
+
+ Column {
+ if (status.withdrawalTransfers.size > 1) {
+ TransferAccountChooser(
+ accounts = transfers.map { it.withdrawalAccount },
+ selectedAccount = selectedTransfer.withdrawalAccount,
+ onSelectAccount = { account ->
+ status.withdrawalTransfers.find {
+ it.withdrawalAccount.paytoUri == account.paytoUri
+ }?.let { selectedTransfer = it }
+ }
+ )
+ }
+
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ when (val transfer = selectedTransfer) {
+ is TransferData.Taler -> TransferTaler(
+ transfer = transfer,
+ exchangeBaseUrl = status.exchangeBaseUrl,
+ transactionAmountRaw = status.transactionAmountRaw.withSpec(spec),
+ transactionAmountEffective = status.transactionAmountEffective.withSpec(spec),
+ )
+
+ is TransferData.IBAN -> TransferIBAN(
+ transfer = transfer,
+ exchangeBaseUrl = status.exchangeBaseUrl,
+ transactionAmountRaw = status.transactionAmountRaw.withSpec(spec),
+ transactionAmountEffective = status.transactionAmountEffective.withSpec(spec),
+ )
+
+ is TransferData.Bitcoin -> TransferBitcoin(
+ transfer = transfer,
+ transactionAmountRaw = status.transactionAmountRaw.withSpec(spec),
+ transactionAmountEffective = status.transactionAmountEffective.withSpec(spec),
+ )
+ }
+
+ if (bankAppClick != null) {
+ Button(
+ onClick = { bankAppClick(selectedTransfer) },
+ modifier = Modifier
+ .padding(bottom = 16.dp),
+ ) {
+ Text(text = stringResource(R.string.withdraw_manual_ready_bank_button))
+ }
+ }
+
+ if (shareClick != null) {
+ ShareButton(
+ content = selectedTransfer.withdrawalAccount.paytoUri,
+ modifier = Modifier
+ .padding(bottom = 16.dp),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun DetailRow(
+ label: String,
+ content: String,
+ copy: Boolean = true,
+) {
+ val context = LocalContext.current
+
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 6.dp, end = 6.dp),
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+
+ Text(
+ modifier = Modifier.padding(
+ top = 8.dp,
+ start = 6.dp,
+ end = 6.dp,
+ ),
+ text = content,
+ style = MaterialTheme.typography.bodyLarge,
+ fontFamily = if (copy) FontFamily.Monospace else FontFamily.Default,
+ textAlign = TextAlign.Center,
+ )
+
+ if (copy) {
+ IconButton(
+ onClick = { copyToClipBoard(context, label, content) },
+ ) {
+ Icon(
+ imageVector = Icons.Default.ContentCopy,
+ contentDescription = stringResource(R.string.copy),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun WithdrawalAmountTransfer(
+ amountRaw: Amount,
+ amountEffective: Amount,
+ conversionAmountRaw: Amount,
+) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TransactionAmountComposable(
+ label = stringResource(R.string.withdraw_transfer),
+ amount = conversionAmountRaw,
+ amountType = AmountType.Neutral,
+ )
+
+ if (amountRaw.currency != conversionAmountRaw.currency) {
+ TransactionAmountComposable(
+ label = stringResource(R.string.withdraw_conversion),
+ amount = amountRaw,
+ amountType = AmountType.Neutral,
+ )
+ }
+
+ val fee = amountRaw - amountEffective
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee,
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_total),
+ amount = amountEffective,
+ amountType = AmountType.Positive,
+ )
+ }
+}
+
+@Composable
+fun TransferAccountChooser(
+ modifier: Modifier = Modifier,
+ accounts: List<WithdrawalExchangeAccountDetails>,
+ selectedAccount: WithdrawalExchangeAccountDetails,
+ onSelectAccount: (account: WithdrawalExchangeAccountDetails) -> Unit,
+) {
+ val selectedIndex = accounts.indexOfFirst {
+ it.paytoUri == selectedAccount.paytoUri
+ }
+
+ ScrollableTabRow(
+ selectedTabIndex = selectedIndex,
+ modifier = modifier,
+ edgePadding = 8.dp,
+ ) {
+ accounts.forEachIndexed { index, account ->
+ Tab(
+ selected = selectedAccount.paytoUri == account.paytoUri,
+ onClick = { onSelectAccount(account) },
+ text = {
+ if (!account.bankLabel.isNullOrEmpty()) {
+ Text(account.bankLabel)
+ } else if (account.currencySpecification?.name != null) {
+ Text(stringResource(
+ R.string.withdraw_account_currency,
+ index + 1,
+ account.currencySpecification.name,
+ ))
+ } else if (account.transferAmount?.currency != null) {
+ Text(stringResource(
+ R.string.withdraw_account_currency,
+ index + 1,
+ account.transferAmount.currency,
+ ))
+ } else Text(stringResource(R.string.withdraw_account, index + 1))
+ },
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun ScreenTransferPreview() {
+ Surface {
+ ScreenTransfer(
+ status = WithdrawStatus.ManualTransferRequired(
+ transactionId = "",
+ transactionAmountRaw = Amount.fromJSONString("KUDOS:10"),
+ transactionAmountEffective = Amount.fromJSONString("KUDOS:9.5"),
+ exchangeBaseUrl = "test.exchange.taler.net",
+ withdrawalTransfers = listOf(
+ TransferData.IBAN(
+ iban = "ASDQWEASDZXCASDQWE",
+ subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG",
+ amountRaw = Amount("KUDOS", 10, 0),
+ amountEffective = Amount("KUDOS", 9, 5),
+ withdrawalAccount = WithdrawalExchangeAccountDetails(
+ paytoUri = "https://taler.net/kudos",
+ transferAmount = Amount("KUDOS", 10, 0),
+ status = Ok,
+ currencySpecification = CurrencySpecification(
+ "KUDOS",
+ numFractionalInputDigits = 2,
+ numFractionalNormalDigits = 2,
+ numFractionalTrailingZeroDigits = 2,
+ altUnitNames = emptyMap(),
+ ),
+ ),
+ ),
+ TransferData.Bitcoin(
+ account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
+ segwitAddresses = listOf(
+ "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq",
+ "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c"
+ ),
+ subject = "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
+ amountRaw = Amount(CURRENCY_BTC, 0, 14000000),
+ amountEffective = Amount(CURRENCY_BTC, 0, 14000000),
+ withdrawalAccount = WithdrawalExchangeAccountDetails(
+ paytoUri = "https://taler.net/btc",
+ transferAmount = Amount("BTC", 0, 14000000),
+ status = Ok,
+ currencySpecification = CurrencySpecification(
+ "Bitcoin",
+ numFractionalInputDigits = 2,
+ numFractionalNormalDigits = 2,
+ numFractionalTrailingZeroDigits = 2,
+ altUnitNames = emptyMap(),
+ ),
+ ),
+ )
+ ),
+ ),
+ spec = null,
+ bankAppClick = {},
+ shareClick = {},
+ )
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt
new file mode 100644
index 0000000..c21ca7e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt
@@ -0,0 +1,112 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.compose.CopyToClipboardButton
+import net.taler.wallet.withdraw.TransferData
+
+@Composable
+fun TransferBitcoin(
+ transfer: TransferData.Bitcoin,
+ transactionAmountRaw: Amount,
+ transactionAmountEffective: Amount,
+) {
+ Column(
+ modifier = Modifier.padding(all = 16.dp),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(R.string.withdraw_manual_bitcoin_intro),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ )
+
+ BitcoinSegwitAddresses(
+ amount = transfer.amountRaw,
+ address = transfer.account,
+ segwitAddresses = transfer.segwitAddresses,
+ )
+
+ transfer.withdrawalAccount.transferAmount?.let { amount ->
+ WithdrawalAmountTransfer(
+ amountRaw = transactionAmountRaw,
+ amountEffective = transactionAmountEffective,
+ conversionAmountRaw = amount.withSpec(
+ transfer.withdrawalAccount.currencySpecification,
+ ),
+ )
+ }
+ }
+}
+
+@Composable
+fun BitcoinSegwitAddresses(amount: Amount, address: String, segwitAddresses: List<String>) {
+ Column {
+ val allSegwitAddresses = listOf(address) + segwitAddresses
+ for (segwitAddress in allSegwitAddresses) {
+ Row(modifier = Modifier.padding(vertical = 8.dp)) {
+ Column(modifier = Modifier.weight(0.3f)) {
+ Text(
+ text = segwitAddress,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.bodySmall,
+ )
+ Text(
+ text = if (segwitAddress == address)
+ amount.withCurrency("BTC").toString()
+ else SEGWIT_MIN.toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+ }
+ }
+
+ CopyToClipboardButton(
+ modifier = Modifier
+ .padding(top = 16.dp, start = 6.dp, end = 6.dp)
+ .align(CenterHorizontally),
+ label = "Bitcoin",
+ content = getCopyText(amount, address, segwitAddresses),
+ )
+ }
+}
+
+private val SEGWIT_MIN = Amount("BTC", 0, 294)
+
+private fun getCopyText(amount: Amount, addr: String, segwitAddresses: List<String>): String {
+ val sr = segwitAddresses.joinToString(separator = "\n") { s ->
+ "\n$s ${SEGWIT_MIN}\n"
+ }
+ return "$addr ${amount.withCurrency("BTC")}\n$sr"
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt
new file mode 100644
index 0000000..d0bc893
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt
@@ -0,0 +1,93 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.withdraw.TransferData
+
+@Composable
+fun TransferIBAN(
+ transfer: TransferData.IBAN,
+ exchangeBaseUrl: String,
+ transactionAmountRaw: Amount,
+ transactionAmountEffective: Amount,
+) {
+ val transferAmount = transfer
+ .withdrawalAccount
+ .transferAmount
+ ?.withSpec(transfer.withdrawalAccount.currencySpecification)
+ ?: transfer.amountRaw
+
+ Column(
+ modifier = Modifier.padding(all = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(
+ R.string.withdraw_manual_ready_intro,
+ transferAmount),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.withdraw_manual_ready_warning),
+ style = MaterialTheme.typography.bodyMedium,
+ color = colorResource(R.color.notice_text),
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(all = 8.dp)
+ .background(colorResource(R.color.notice_background))
+ .border(BorderStroke(2.dp, colorResource(R.color.notice_border)))
+ .padding(all = 16.dp)
+ )
+
+ transfer.receiverName?.let {
+ DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it)
+ }
+ DetailRow(stringResource(R.string.withdraw_manual_ready_iban), transfer.iban)
+ DetailRow(stringResource(R.string.withdraw_manual_ready_subject), transfer.subject)
+
+ TransactionInfoComposable(
+ label = stringResource(R.string.withdraw_exchange),
+ info = cleanExchange(exchangeBaseUrl),
+ )
+
+ WithdrawalAmountTransfer(
+ amountRaw = transactionAmountRaw,
+ amountEffective = transactionAmountEffective,
+ conversionAmountRaw = transferAmount,
+ )
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt
new file mode 100644
index 0000000..2ec43b9
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt
@@ -0,0 +1,93 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.withdraw.TransferData
+
+@Composable
+fun TransferTaler(
+ transfer: TransferData.Taler,
+ exchangeBaseUrl: String,
+ transactionAmountRaw: Amount,
+ transactionAmountEffective: Amount,
+) {
+ val transferAmount = transfer
+ .withdrawalAccount
+ .transferAmount
+ ?.withSpec(transfer.withdrawalAccount.currencySpecification)
+ ?: transfer.amountRaw
+
+ Column(
+ modifier = Modifier.padding(all = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(
+ R.string.withdraw_manual_ready_intro,
+ transferAmount),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.withdraw_manual_ready_warning),
+ style = MaterialTheme.typography.bodyMedium,
+ color = colorResource(R.color.notice_text),
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(all = 8.dp)
+ .background(colorResource(R.color.notice_background))
+ .border(BorderStroke(2.dp, colorResource(R.color.notice_border)))
+ .padding(all = 16.dp)
+ )
+
+ transfer.receiverName?.let {
+ DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it)
+ }
+ DetailRow(stringResource(R.string.withdraw_manual_ready_account), transfer.account)
+ DetailRow(stringResource(R.string.withdraw_manual_ready_subject), transfer.subject)
+
+ TransactionInfoComposable(
+ label = stringResource(R.string.withdraw_exchange),
+ info = cleanExchange(exchangeBaseUrl),
+ )
+
+ WithdrawalAmountTransfer(
+ amountRaw = transactionAmountRaw,
+ amountEffective = transactionAmountEffective,
+ conversionAmountRaw = transferAmount,
+ )
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/res/drawable/ic_archive.xml b/wallet/src/main/res/drawable/ic_archive.xml
new file mode 100644
index 0000000..58a032c
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_archive.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2023 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<vector android:height="24dp" android:tint="?attr/colorControlNormal"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M20.54,5.23l-1.39,-1.68C18.88,3.21 18.47,3 18,3H6c-0.47,0 -0.88,0.21 -1.16,0.55L3.46,5.23C3.17,5.57 3,6.02 3,6.5V19c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V6.5c0,-0.48 -0.17,-0.93 -0.46,-1.27zM12,17.5L6.5,12H10v-2h4v2h3.5L12,17.5zM5.12,5l0.81,-1h12l0.94,1H5.12z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_baseline_account_tree.xml b/wallet/src/main/res/drawable/ic_baseline_account_tree.xml
deleted file mode 100644
index 7067bd3..0000000
--- a/wallet/src/main/res/drawable/ic_baseline_account_tree.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?attr/colorControlNormal"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:fillColor="@android:color/white"
- android:pathData="M22,11V3h-7v3H9V3H2v8h7V8h2v10h4v3h7v-8h-7v3h-2V8h2v3z" />
-</vector>
diff --git a/wallet/src/main/res/drawable/ic_baseline_backup.xml b/wallet/src/main/res/drawable/ic_baseline_backup.xml
deleted file mode 100644
index 006b014..0000000
--- a/wallet/src/main/res/drawable/ic_baseline_backup.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?attr/colorControlNormal"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:fillColor="@android:color/white"
- android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM14,13v4h-4v-4H7l5,-5 5,5h-3z" />
-</vector>
diff --git a/wallet/src/main/res/drawable/ic_baseline_vpn_key.xml b/wallet/src/main/res/drawable/ic_baseline_vpn_key.xml
deleted file mode 100644
index 7b554c9..0000000
--- a/wallet/src/main/res/drawable/ic_baseline_vpn_key.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?attr/colorControlNormal"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:fillColor="@android:color/white"
- android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
-</vector>
diff --git a/wallet/src/main/res/drawable/ic_fail.xml b/wallet/src/main/res/drawable/ic_fail.xml
new file mode 100644
index 0000000..a25c5ed
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_fail.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M14.69,2.21L4.33,11.49c-0.64,0.58 -0.28,1.65 0.58,1.73L13,14l-4.85,6.76c-0.22,0.31 -0.19,0.74 0.08,1.01h0c0.3,0.3 0.77,0.31 1.08,0.02l10.36,-9.28c0.64,-0.58 0.28,-1.65 -0.58,-1.73L11,10l4.85,-6.76c0.22,-0.31 0.19,-0.74 -0.08,-1.01l0,0C15.47,1.93 15,1.92 14.69,2.21z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_resume.xml b/wallet/src/main/res/drawable/ic_resume.xml
new file mode 100644
index 0000000..e3fd2e9
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_resume.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_retry.xml b/wallet/src/main/res/drawable/ic_retry.xml
new file mode 100644
index 0000000..98469ca
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_retry.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_suspend.xml b/wallet/src/main/res/drawable/ic_suspend.xml
new file mode 100644
index 0000000..938bd7f
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_suspend.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_unarchive.xml b/wallet/src/main/res/drawable/ic_unarchive.xml
index e6faf51..8a48bba 100644
--- a/wallet/src/main/res/drawable/ic_unarchive.xml
+++ b/wallet/src/main/res/drawable/ic_unarchive.xml
@@ -17,10 +17,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="#000000"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="?attr/colorControlNormal"
+ android:fillColor="#FF000000"
android:pathData="M20.55,5.22l-1.39,-1.68C18.88,3.21 18.47,3 18,3H6C5.53,3 5.12,3.21 4.85,3.55L3.46,5.22C3.17,5.57 3,6.01 3,6.5V19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6.5C21,6.01 20.83,5.57 20.55,5.22zM12,9.5l5.5,5.5H14v2h-4v-2H6.5L12,9.5zM5.12,5l0.82,-1h12l0.93,1H5.12z" />
</vector>
diff --git a/wallet/src/main/res/drawable/transaction_loss.xml b/wallet/src/main/res/drawable/transaction_loss.xml
new file mode 100644
index 0000000..ffc9a2e
--- /dev/null
+++ b/wallet/src/main/res/drawable/transaction_loss.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2024 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M17.12,9.88C16.56,9.32 15.8,9 15,9S13.44,9.32 12.88,9.88C12.32,10.44 12,11.2 12,12S12.32,13.56 12.88,14.12 14.2,15 15,15 16.56,14.68 17.12,14.12 18,12.8 18,12 17.68,10.44 17.12,9.88M7,6V18H23V6H7M21,14C20.47,14 19.96,14.21 19.59,14.59C19.21,14.96 19,15.47 19,16H11C11,15.47 10.79,14.96 10.41,14.59C10.04,14.21 9.53,14 9,14V10C9.53,10 10.04,9.79 10.41,9.41C10.79,9.04 11,8.53 11,8H19C19,8.53 19.21,9.04 19.59,9.41C19.96,9.79 20.47,10 21,10V14M5,8H3C2.45,8 2,7.55 2,7C2,6.45 2.45,6 3,6H5V8M5,13H2C1.45,13 1,12.55 1,12C1,11.45 1.45,11 2,11H5V13M5,18H1C0.448,18 0,17.55 0,17C0,16.45 0.448,16 1,16H5V18Z"/>
+</vector>
diff --git a/wallet/src/main/res/layout/app_content_main.xml b/wallet/src/main/res/layout/app_content_main.xml
index 6937e59..71fbcd4 100644
--- a/wallet/src/main/res/layout/app_content_main.xml
+++ b/wallet/src/main/res/layout/app_content_main.xml
@@ -36,20 +36,42 @@
style="@style/AppTheme.Toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+ android:theme="@style/Widget.Material3.ActionBar.Solid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+ <FrameLayout
+ android:id="@+id/offline_banner"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:background="?attr/colorPrimary"
+ app:layout_constraintTop_toBottomOf="@id/toolbar"
+ app:layout_constraintBottom_toTopOf="@id/progress_bar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:animateLayoutChanges="true"
+ android:visibility="gone"
+ tools:visibility="visible">
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:textAlignment="center"
+ android:textColor="?attr/colorOnPrimary"
+ android:text="@string/offline_banner" />
+ </FrameLayout>
+
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/progress_bar"
- style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal"
+ style="@style/Widget.MaterialProgressBar.ProgressBar"
android:layout_width="0dp"
android:layout_height="4dp"
android:elevation="4dp"
android:indeterminate="true"
android:visibility="invisible"
- app:layout_constraintBottom_toBottomOf="@+id/toolbar"
+ app:layout_constraintBottom_toBottomOf="@+id/offline_banner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:mpb_progressStyle="horizontal"
diff --git a/wallet/src/main/res/layout/dialog_exchange_add.xml b/wallet/src/main/res/layout/dialog_exchange_add.xml
index dfa0f70..bf90177 100644
--- a/wallet/src/main/res/layout/dialog_exchange_add.xml
+++ b/wallet/src/main/res/layout/dialog_exchange_add.xml
@@ -22,7 +22,7 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/urlLayout"
- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
diff --git a/wallet/src/main/res/layout/fragment_already_accepted.xml b/wallet/src/main/res/layout/fragment_already_accepted.xml
deleted file mode 100644
index b1a7bb1..0000000
--- a/wallet/src/main/res/layout/fragment_already_accepted.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="15dp"
- android:orientation="vertical"
- tools:context=".tip.AlreadyAcceptedFragment">
-
- <Space
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="50dp"
- android:layout_gravity="center"
- android:text="@string/tip_already_accepted"
- android:textAlignment="center"
- android:textColor="@android:color/holo_green_dark"
- app:autoSizeTextType="uniform" />
-
-
- <Space
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
-
- <Button
- android:id="@+id/backButton"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/button_back" />
-
-</LinearLayout>
diff --git a/wallet/src/main/res/layout/fragment_already_paid.xml b/wallet/src/main/res/layout/fragment_already_paid.xml
deleted file mode 100644
index 18ce0b5..0000000
--- a/wallet/src/main/res/layout/fragment_already_paid.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="15dp"
- android:orientation="vertical"
- tools:context=".payment.AlreadyPaidFragment">
-
- <Space
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="50dp"
- android:layout_gravity="center"
- android:text="@string/payment_already_paid"
- android:textAlignment="center"
- android:textColor="@android:color/holo_green_dark"
- app:autoSizeTextType="uniform" />
-
-
- <Space
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
-
- <Button
- android:id="@+id/backButton"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/button_back" />
-
-</LinearLayout>
diff --git a/wallet/src/main/res/layout/fragment_error.xml b/wallet/src/main/res/layout/fragment_error.xml
index b9f585f..2af166a 100644
--- a/wallet/src/main/res/layout/fragment_error.xml
+++ b/wallet/src/main/res/layout/fragment_error.xml
@@ -29,13 +29,13 @@
android:alpha="0.56"
android:src="@drawable/ic_error"
app:layout_constraintBottom_toTopOf="@+id/errorTitle"
- app:layout_constraintDimensionRatio="1:1"
+ app:layout_constraintDimensionRatio="1.5:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed"
- app:tint="@color/red"
+ app:tint="?colorError"
tools:ignore="ContentDescription" />
<TextView
@@ -45,7 +45,7 @@
android:layout_margin="16dp"
android:gravity="center_horizontal|top"
android:minHeight="64dp"
- android:textColor="@color/red"
+ android:textColor="?colorError"
app:autoSizeMaxTextSize="40sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toTopOf="@+id/errorMessage"
@@ -60,7 +60,7 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
- android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintBottom_toTopOf="@+id/errorDevMessage"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -74,7 +74,7 @@
android:layout_margin="16dp"
android:fontFamily="monospace"
android:gravity="center"
- android:textColor="@color/red"
+ android:textColor="?colorError"
android:textIsSelectable="true"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/backButton"
diff --git a/wallet/src/main/res/layout/fragment_exchange_fees.xml b/wallet/src/main/res/layout/fragment_exchange_fees.xml
index 6f8814f..03d69f5 100644
--- a/wallet/src/main/res/layout/fragment_exchange_fees.xml
+++ b/wallet/src/main/res/layout/fragment_exchange_fees.xml
@@ -44,7 +44,7 @@
app:layout_constraintStart_toEndOf="@+id/withdrawFeeLabel"
app:layout_constraintTop_toTopOf="@+id/withdrawFeeLabel"
tools:text="-0.23 TESTKUDOS"
- tools:textColor="@color/red" />
+ tools:textColor="?colorError" />
<TextView
android:id="@+id/overheadLabel"
@@ -64,7 +64,7 @@
app:layout_constraintStart_toEndOf="@+id/overheadLabel"
app:layout_constraintTop_toTopOf="@+id/overheadLabel"
tools:text="-0.42 TESTKUDOS"
- tools:textColor="@color/red" />
+ tools:textColor="?colorError" />
<TextView
android:id="@+id/expirationLabel"
diff --git a/wallet/src/main/res/layout/fragment_main.xml b/wallet/src/main/res/layout/fragment_main.xml
index 3f680ba..6f9e693 100644
--- a/wallet/src/main/res/layout/fragment_main.xml
+++ b/wallet/src/main/res/layout/fragment_main.xml
@@ -23,13 +23,14 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
- <com.google.android.material.floatingactionbutton.FloatingActionButton
+ <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/mainFab"
style="@style/FabStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/button_scan_qr_code"
- android:src="@drawable/ic_scan_qr"
+ android:text="@string/button_scan_qr_code_label"
+ app:icon="@drawable/ic_scan_qr"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/wallet/src/main/res/layout/fragment_manual_withdraw.xml b/wallet/src/main/res/layout/fragment_manual_withdraw.xml
index ec176ff..6d5f78b 100644
--- a/wallet/src/main/res/layout/fragment_manual_withdraw.xml
+++ b/wallet/src/main/res/layout/fragment_manual_withdraw.xml
@@ -14,104 +14,109 @@
~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-->
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/qrCodeButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ app:icon="@drawable/ic_scan_qr"
+ app:iconTint="?colorOnPrimary"
+ android:text="@string/button_scan_qr_code"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:ignore="RtlHardcoded" />
- <Button
- android:id="@+id/qrCodeButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="32dp"
- android:drawableLeft="@drawable/ic_scan_qr"
- android:text="@string/button_scan_qr_code"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_chainStyle="packed"
- tools:ignore="RtlHardcoded" />
-
- <TextView
- android:id="@+id/orView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="32dp"
- android:text="@string/or"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/qrCodeButton" />
-
- <TextView
- android:id="@+id/manualWithdrawIntro"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="32dp"
- android:text="@string/withdraw_manual_title"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orView" />
+ <TextView
+ android:id="@+id/orView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:text="@string/or"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/qrCodeButton" />
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/amountLayout"
- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginTop="16dp"
- android:layout_marginEnd="16dp"
- android:minWidth="128dp"
- app:boxBackgroundMode="outline"
- app:endIconDrawable="@drawable/ic_cancel"
- app:endIconMode="clear_text"
- app:layout_constraintEnd_toStartOf="@+id/currencyView"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/manualWithdrawIntro">
+ <TextView
+ android:id="@+id/manualWithdrawIntro"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:text="@string/withdraw_manual_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/orView" />
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/amountView"
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/amountLayout"
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:inputType="numberDecimal" />
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:minWidth="128dp"
+ app:boxBackgroundMode="outline"
+ app:endIconDrawable="@drawable/ic_cancel"
+ app:endIconMode="clear_text"
+ app:layout_constraintEnd_toStartOf="@+id/currencyView"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/manualWithdrawIntro">
- </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/amountView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inputType="numberDecimal" />
- <TextView
- android:id="@+id/currencyView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:layout_constraintBottom_toBottomOf="@+id/amountLayout"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@+id/amountLayout"
- app:layout_constraintTop_toTopOf="@+id/amountLayout"
- tools:text="TESTKUDOS123" />
+ </com.google.android.material.textfield.TextInputLayout>
- <TextView
- android:id="@+id/paymentOptionsLabel"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginTop="32dp"
- android:layout_marginEnd="16dp"
- android:text="@string/withdraw_manual_payment_options"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/amountLayout" />
+ <TextView
+ android:id="@+id/currencyView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="@+id/amountLayout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/amountLayout"
+ app:layout_constraintTop_toTopOf="@+id/amountLayout"
+ tools:text="TESTKUDOS123" />
- <Button
- android:id="@+id/checkFeesButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="32dp"
- android:layout_marginEnd="16dp"
- android:text="@string/withdraw_manual_check_fees"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/paymentOptionsLabel"
- app:layout_constraintVertical_bias="0.0" />
+ <TextView
+ android:id="@+id/paymentOptionsLabel"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:text="@string/withdraw_manual_payment_options"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/amountLayout" />
+
+ <Button
+ android:id="@+id/checkFeesButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:text="@string/withdraw_manual_check_fees"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/paymentOptionsLabel"
+ app:layout_constraintVertical_bias="0.0" />
-</androidx.constraintlayout.widget.ConstraintLayout>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</ScrollView> \ No newline at end of file
diff --git a/wallet/src/main/res/layout/fragment_pending_operations.xml b/wallet/src/main/res/layout/fragment_pending_operations.xml
deleted file mode 100644
index 5030234..0000000
--- a/wallet/src/main/res/layout/fragment_pending_operations.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/list_pending"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:scrollbars="vertical"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- tools:listitem="@layout/list_item_pending_operation" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/fragment_prompt_tip.xml b/wallet/src/main/res/layout/fragment_prompt_tip.xml
deleted file mode 100644
index 941274f..0000000
--- a/wallet/src/main/res/layout/fragment_prompt_tip.xml
+++ /dev/null
@@ -1,257 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tools:context=".tip.PromptTipFragment">
-
- <TextView
- android:id="@+id/introView"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="16dp"
- android:layout_marginBottom="8dp"
- android:gravity="center"
- android:text="@string/tip_total"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/effectiveAmountView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_chainStyle="packed"
- tools:visibility="visible" />
-
- <TextView
- android:id="@+id/effectiveAmountView"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="16dp"
- android:gravity="center"
- android:textColor="@color/green"
- android:textSize="24sp"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/chosenAmountLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/introView"
- tools:text="9.8 TESTKUDOS"
- tools:visibility="visible" />
-
- <TextView
- android:id="@+id/chosenAmountLabel"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginTop="32dp"
- android:layout_marginEnd="16dp"
- android:gravity="center"
- android:text="@string/amount_chosen"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/chosenAmountView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
- tools:visibility="visible" />
-
- <TextView
- android:id="@+id/chosenAmountView"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginTop="8dp"
- android:layout_marginEnd="16dp"
- android:gravity="center"
- android:textSize="20sp"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/feeLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountLabel"
- tools:text="10 TESTKUDOS"
- tools:visibility="visible" />
-
- <TextView
- android:id="@+id/feeLabel"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginTop="32dp"
- android:layout_marginEnd="16dp"
- android:gravity="center"
- android:text="@string/tip_fees"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/feeView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountView"
- tools:visibility="visible" />
-
- <TextView
- android:id="@+id/feeView"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginTop="8dp"
- android:layout_marginEnd="16dp"
- android:gravity="center"
- android:textColor="@color/red"
- android:textSize="20sp"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/exchangeIntroView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeLabel"
- tools:text="-0.2 TESTKUDOS"
- tools:visibility="visible" />
-
- <TextView
- android:id="@+id/exchangeIntroView"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginTop="32dp"
- android:layout_marginEnd="16dp"
- android:layout_marginBottom="8dp"
- android:gravity="center"
- android:text="@string/tip_exchange"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/withdrawExchangeUrl"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeView"
- tools:visibility="visible" />
-
- <TextView
- android:id="@+id/withdrawExchangeUrl"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="8dp"
- android:layout_marginBottom="8dp"
- android:gravity="center"
- android:textSize="24sp"
- android:visibility="invisible"
- app:layout_constrainedWidth="true"
- app:layout_constraintBottom_toTopOf="@+id/merchantIntroView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/exchangeIntroView"
- tools:text="demo.taler.net"
- tools:visibility="visible" />
-
- <TextView
- android:id="@+id/merchantIntroView"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="16dp"
- android:layout_marginBottom="8dp"
- android:gravity="center"
- android:text="@string/tip_merchant_url"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/withdrawMerchantUrl"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/withdrawExchangeUrl"
- tools:visibility="visible" />
-
-
- <TextView
- android:id="@+id/withdrawMerchantUrl"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="8dp"
- android:gravity="center"
- android:textSize="24sp"
- android:visibility="invisible"
- app:layout_constrainedWidth="true"
- app:layout_constraintBottom_toTopOf="@+id/withdrawCard"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.502"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/merchantIntroView"
- tools:text="merchant.demo.taler.net"
- tools:visibility="visible" />
-
- <ProgressBar
- android:id="@+id/progressBar"
- style="?android:attr/progressBarStyleLarge"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <com.google.android.material.card.MaterialCardView
- android:id="@+id/withdrawCard"
- style="@style/BottomCard"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:visibility="invisible"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- tools:visibility="visible">
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="8dp">
-
- <Button
- android:id="@+id/confirmWithdrawButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:backgroundTint="@color/green"
- android:enabled="false"
- android:text="@string/tip_button_confirm"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="1.0"
- app:layout_constraintStart_toStartOf="parent"
- tools:enabled="true"
- tools:text="@string/tip_button_confirm" />
-
- <ProgressBar
- android:id="@+id/confirmProgressBar"
- style="?android:attr/progressBarStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="invisible"
- app:layout_constraintBottom_toBottomOf="@+id/confirmWithdrawButton"
- app:layout_constraintEnd_toEndOf="@+id/confirmWithdrawButton"
- app:layout_constraintStart_toStartOf="@+id/confirmWithdrawButton"
- app:layout_constraintTop_toTopOf="@+id/confirmWithdrawButton"
- tools:visibility="visible" />
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
- </com.google.android.material.card.MaterialCardView>
-
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml
index 03e7d1a..44d95de 100644
--- a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml
+++ b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml
@@ -22,106 +22,108 @@
tools:context=".withdraw.PromptWithdrawFragment">
<TextView
- android:id="@+id/introView"
+ android:id="@+id/chosenAmountLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
- android:layout_marginBottom="8dp"
android:gravity="center"
- android:text="@string/withdraw_total"
+ android:text="@string/amount_chosen"
android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/effectiveAmountView"
+ app:layout_constraintBottom_toTopOf="@+id/chosenAmountView"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:visibility="visible" />
<TextView
- android:id="@+id/effectiveAmountView"
+ android:id="@+id/chosenAmountView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:gravity="center"
- android:textColor="@color/green"
- android:textSize="24sp"
+ android:textSize="20sp"
android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/chosenAmountLabel"
+ app:layout_constraintBottom_toTopOf="@+id/feeLabel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/introView"
- tools:text="9.8 TESTKUDOS"
+ app:layout_constraintTop_toBottomOf="@id/chosenAmountLabel"
+ tools:text="10 TESTKUDOS"
tools:visibility="visible" />
<TextView
- android:id="@+id/chosenAmountLabel"
+ android:id="@+id/feeLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:gravity="center"
- android:text="@string/amount_chosen"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/chosenAmountView"
+ android:text="@string/withdraw_fees"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/feeView"
app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
+ app:layout_constraintTop_toBottomOf="@+id/chosenAmountView"
tools:visibility="visible" />
<TextView
- android:id="@+id/chosenAmountView"
+ android:id="@+id/feeView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:gravity="center"
+ android:textColor="?colorError"
android:textSize="20sp"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/feeLabel"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/introView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountLabel"
- tools:text="10 TESTKUDOS"
+ app:layout_constraintTop_toBottomOf="@+id/feeLabel"
+ tools:text="-0.2 TESTKUDOS"
tools:visibility="visible" />
<TextView
- android:id="@+id/feeLabel"
+ android:id="@+id/introView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
android:gravity="center"
- android:text="@string/withdraw_fees"
+ android:text="@string/withdraw_total"
android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/feeView"
+ app:layout_constraintBottom_toTopOf="@+id/effectiveAmountView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountView"
+ app:layout_constraintTop_toBottomOf="@id/feeView"
tools:visibility="visible" />
<TextView
- android:id="@+id/feeView"
+ android:id="@+id/effectiveAmountView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
- android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:gravity="center"
- android:textColor="@color/red"
- android:textSize="20sp"
+ android:textColor="@color/green"
+ android:textSize="24sp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/exchangeIntroView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeLabel"
- tools:text="-0.2 TESTKUDOS"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:text="9.8 TESTKUDOS"
tools:visibility="visible" />
<TextView
@@ -139,7 +141,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeView"
+ app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
tools:visibility="visible" />
<TextView
diff --git a/wallet/src/main/res/layout/fragment_review_exchange_tos.xml b/wallet/src/main/res/layout/fragment_review_exchange_tos.xml
index ec8d996..20c1013 100644
--- a/wallet/src/main/res/layout/fragment_review_exchange_tos.xml
+++ b/wallet/src/main/res/layout/fragment_review_exchange_tos.xml
@@ -48,7 +48,7 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
- android:textColor="@color/red"
+ android:textColor="?colorError"
android:textSize="16sp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
diff --git a/wallet/src/main/res/layout/fragment_transaction_payment.xml b/wallet/src/main/res/layout/fragment_transaction_payment.xml
deleted file mode 100644
index c5f5f81..0000000
--- a/wallet/src/main/res/layout/fragment_transaction_payment.xml
+++ /dev/null
@@ -1,128 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fillViewport="true"
- tools:context=".transactions.TransactionDetailFragment">
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:id="@+id/timeView"
- style="@style/TransactionLabel.Time"
- app:layout_constraintBottom_toTopOf="@+id/amountPaidWithFeesLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- tools:text="23 March 2020 23:42pm" />
-
- <TextView
- android:id="@+id/amountPaidWithFeesLabel"
- style="@style/TransactionLabel"
- android:text="@string/transaction_paid"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/timeView" />
-
- <TextView
- android:id="@+id/amountPaidWithFeesView"
- style="@style/TransactionContent"
- android:textColor="@color/red"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/amountPaidWithFeesLabel"
- tools:text="-23.42 TESTKUDOS" />
-
- <TextView
- android:id="@+id/orderAmountLabel"
- style="@style/TransactionLabel"
- android:text="@string/transaction_order_total"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/amountPaidWithFeesView" />
-
- <TextView
- android:id="@+id/orderAmountView"
- style="@style/TransactionContent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orderAmountLabel"
- tools:text="23 TESTKUDOS" />
-
- <TextView
- android:id="@+id/feeLabel"
- style="@style/TransactionLabel"
- android:text="@string/withdraw_fees"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orderAmountView" />
-
- <TextView
- android:id="@+id/feeView"
- style="@style/TransactionContent"
- android:textColor="@color/red"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeLabel"
- tools:text="-0.42 TESTKUDOS" />
-
- <TextView
- android:id="@+id/orderSummaryLabel"
- style="@style/TransactionLabel"
- android:text="@string/transaction_order"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeView" />
-
- <TextView
- android:id="@+id/orderSummaryView"
- style="@style/TransactionContent"
- android:textColor="?android:textColorPrimary"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orderSummaryLabel"
- tools:text="Some Product that was bought and can have quite a long label" />
-
- <TextView
- android:id="@+id/orderIdView"
- style="@style/TransactionLabel"
- android:layout_marginBottom="16dp"
- android:text="@string/transaction_order_id"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orderSummaryView" />
-
- <com.google.android.material.button.MaterialButton
- android:id="@+id/deleteButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginVertical="10dp"
- android:text="@string/transactions_delete"
- app:backgroundTint="@color/red"
- app:icon="@drawable/ic_delete"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orderIdView" />
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
-</ScrollView>
diff --git a/wallet/src/main/res/layout/fragment_transaction_withdrawal.xml b/wallet/src/main/res/layout/fragment_transaction_withdrawal.xml
deleted file mode 100644
index 47dbafb..0000000
--- a/wallet/src/main/res/layout/fragment_transaction_withdrawal.xml
+++ /dev/null
@@ -1,153 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fillViewport="true"
- tools:context=".transactions.TransactionDetailFragment">
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:id="@+id/timeView"
- style="@style/TransactionLabel.Time"
- app:layout_constraintBottom_toTopOf="@+id/effectiveAmountLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- tools:text="23 March 2020 23:42pm" />
-
- <TextView
- android:id="@+id/effectiveAmountLabel"
- style="@style/TransactionLabel"
- app:layout_constraintBottom_toTopOf="@+id/effectiveAmountView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/timeView"
- tools:text="@string/withdraw_total" />
-
- <TextView
- android:id="@+id/effectiveAmountView"
- style="@style/TransactionContent"
- android:textColor="@color/green"
- app:layout_constraintBottom_toTopOf="@+id/confirmWithdrawalButton"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/effectiveAmountLabel"
- tools:text="23.42 TESTKUDOS" />
-
- <Button
- android:id="@+id/confirmWithdrawalButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:drawableLeft="@drawable/ic_account_balance"
- android:text="@string/withdraw_button_confirm_bank"
- app:drawableTint="?attr/colorOnPrimarySurface"
- app:layout_constraintBottom_toTopOf="@+id/actionButton"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
- tools:ignore="RtlHardcoded" />
-
- <Button
- android:id="@+id/actionButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginVertical="10dp"
- android:backgroundTint="@color/colorAccent"
- android:visibility="gone"
- app:layout_constraintBottom_toTopOf="@id/chosenAmountLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/confirmWithdrawalButton"
- tools:text="@string/transaction_action_kyc"
- tools:visibility="visible" />
-
- <TextView
- android:id="@+id/chosenAmountLabel"
- style="@style/TransactionLabel"
- app:layout_constraintBottom_toTopOf="@+id/chosenAmountView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/actionButton"
- tools:text="@string/amount_chosen" />
-
- <TextView
- android:id="@+id/chosenAmountView"
- style="@style/TransactionContent"
- app:layout_constraintBottom_toTopOf="@+id/feeLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountLabel"
- tools:text="24 TESTKUDOS" />
-
- <TextView
- android:id="@+id/feeLabel"
- style="@style/TransactionLabel"
- android:text="@string/withdraw_fees"
- app:layout_constraintBottom_toTopOf="@+id/feeView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountView" />
-
- <TextView
- android:id="@+id/feeView"
- style="@style/TransactionContent"
- android:textColor="@color/red"
- app:layout_constraintBottom_toTopOf="@+id/exchangeLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeLabel"
- tools:text="-0.38 TESTKUDOS" />
-
- <TextView
- android:id="@+id/exchangeLabel"
- style="@style/TransactionLabel"
- android:text="@string/withdraw_exchange"
- app:layout_constraintBottom_toTopOf="@+id/exchangeView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeView" />
-
- <TextView
- android:id="@+id/exchangeView"
- style="@style/TransactionContent"
- app:layout_constraintBottom_toTopOf="@+id/deleteButton"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/exchangeLabel"
- tools:text="exchange.demo.taler.net" />
-
- <com.google.android.material.button.MaterialButton
- android:id="@+id/deleteButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginVertical="10dp"
- android:text="@string/transactions_delete"
- app:backgroundTint="@color/red"
- app:icon="@drawable/ic_delete"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/exchangeView" />
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
-</ScrollView>
diff --git a/wallet/src/main/res/layout/fragment_transactions.xml b/wallet/src/main/res/layout/fragment_transactions.xml
index bad79ea..8fa46f5 100644
--- a/wallet/src/main/res/layout/fragment_transactions.xml
+++ b/wallet/src/main/res/layout/fragment_transactions.xml
@@ -22,9 +22,9 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/sendButton"
- style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginStart="10dp"
android:text="@string/transactions_send_funds"
app:layout_constraintBottom_toTopOf="@+id/divider"
app:layout_constraintStart_toStartOf="parent"
@@ -32,9 +32,9 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/receiveButton"
- style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginHorizontal="10dp"
android:text="@string/transactions_receive_funds"
app:layout_constraintBottom_toTopOf="@+id/divider"
app:layout_constraintEnd_toStartOf="@+id/amount"
@@ -131,13 +131,14 @@
app:layout_constraintTop_toBottomOf="@+id/divider"
tools:visibility="visible" />
- <com.google.android.material.floatingactionbutton.FloatingActionButton
+ <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/mainFab"
style="@style/FabStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/button_scan_qr_code"
- android:src="@drawable/ic_scan_qr"
+ android:text="@string/button_scan_qr_code_label"
+ app:icon="@drawable/ic_scan_qr"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
diff --git a/wallet/src/main/res/layout/fragment_uri_input.xml b/wallet/src/main/res/layout/fragment_uri_input.xml
index 1e9934f..95c2297 100644
--- a/wallet/src/main/res/layout/fragment_uri_input.xml
+++ b/wallet/src/main/res/layout/fragment_uri_input.xml
@@ -22,7 +22,7 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/uriLayout"
- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
@@ -42,7 +42,7 @@
</com.google.android.material.textfield.TextInputLayout>
- <Button
+ <com.google.android.material.button.MaterialButton
android:id="@+id/pasteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -50,9 +50,10 @@
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
- android:drawableLeft="@drawable/ic_content_paste"
android:text="@string/paste"
- app:drawableTint="?attr/colorOnPrimarySurface"
+ android:textColor="?colorOnPrimary"
+ app:icon="@drawable/ic_content_paste"
+ app:iconTint="?colorOnPrimary"
app:layout_constraintEnd_toStartOf="@+id/okButton"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
@@ -68,6 +69,7 @@
android:layout_marginEnd="16dp"
android:backgroundTint="@color/green"
android:text="@string/ok"
+ android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/pasteButton"
app:layout_constraintTop_toBottomOf="@+id/uriLayout" />
diff --git a/wallet/src/main/res/layout/list_item_balance.xml b/wallet/src/main/res/layout/list_item_balance.xml
index 475e7d6..53e3d89 100644
--- a/wallet/src/main/res/layout/list_item_balance.xml
+++ b/wallet/src/main/res/layout/list_item_balance.xml
@@ -27,8 +27,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
- android:textSize="40sp"
- app:layout_constraintEnd_toStartOf="@+id/balanceCurrencyView"
+ style="?textAppearanceDisplaySmall"
+ app:layout_constraintEnd_toStartOf="@+id/pendingView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
@@ -36,17 +36,18 @@
tools:text="100.50" />
<TextView
- android:id="@+id/balanceCurrencyView"
- android:layout_width="wrap_content"
+ android:id="@+id/scopeView"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginEnd="8dp"
- android:textSize="20sp"
- app:layout_constrainedWidth="true"
- app:layout_constraintBottom_toBottomOf="@+id/balanceAmountView"
- app:layout_constraintEnd_toStartOf="@+id/pendingView"
- app:layout_constraintStart_toEndOf="@+id/balanceAmountView"
- app:layout_constraintTop_toTopOf="@+id/balanceAmountView"
- tools:text="TESTKUDOS" />
+ android:layout_marginBottom="8dp"
+ style="?textAppearanceBodyMedium"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@id/balanceAmountView"
+ app:layout_constraintBottom_toTopOf="@id/balanceInboundAmount"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/pendingView"
+ tools:text="@string/balance_scope_exchange"
+ tools:visibility="visible"/>
<TextView
android:id="@+id/balanceInboundAmount"
@@ -54,12 +55,13 @@
android:layout_height="wrap_content"
android:textColor="@color/green"
android:textSize="20sp"
+ style="?textAppearanceBodyLarge"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/balanceInboundLabel"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/balanceAmountView"
+ app:layout_constraintTop_toBottomOf="@+id/scopeView"
tools:text="+10 TESTKUDOS"
tools:visibility="visible" />
@@ -70,6 +72,7 @@
android:layout_marginStart="8dp"
android:text="@string/balances_inbound_label"
android:textColor="@color/green"
+ style="?textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="@+id/balanceInboundAmount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/balanceInboundAmount"
diff --git a/wallet/src/main/res/layout/list_item_pending_operation.xml b/wallet/src/main/res/layout/list_item_pending_operation.xml
deleted file mode 100644
index bd606c3..0000000
--- a/wallet/src/main/res/layout/list_item_pending_operation.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/pending_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_margin="3dp"
- android:background="@drawable/pending_border"
- android:orientation="vertical"
- android:padding="3dp">
-
- <TextView
- android:id="@+id/pending_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="24sp"
- tools:text="My Pending Operation" />
-
- <Button
- android:id="@+id/button_pending_action_1"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- tools:text="Cancel Operation" />
-
- <TextView
- android:id="@+id/pending_subtext"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="14sp"
- tools:text="My further details" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/wallet/src/main/res/layout/list_item_transaction.xml b/wallet/src/main/res/layout/list_item_transaction.xml
index 232afb8..ad792ae 100644
--- a/wallet/src/main/res/layout/list_item_transaction.xml
+++ b/wallet/src/main/res/layout/list_item_transaction.xml
@@ -22,9 +22,9 @@
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
- android:paddingTop="8dp"
+ android:paddingTop="12dp"
android:paddingEnd="16dp"
- android:paddingBottom="8dp">
+ android:paddingBottom="12dp">
<ImageView
android:id="@+id/icon"
@@ -50,11 +50,11 @@
<TextView
android:id="@+id/extraInfoView"
+ style="@style/TransactionSubtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
- android:textSize="14sp"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="@+id/title"
@@ -62,28 +62,17 @@
tools:text="@string/withdraw_waiting_confirm"
tools:visibility="visible" />
- <com.google.android.material.button.MaterialButton
- android:id="@+id/actionButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="gone"
- app:backgroundTint="@color/colorAccent"
- app:layout_constraintStart_toStartOf="@id/title"
- app:layout_constraintTop_toBottomOf="@id/extraInfoView"
- tools:text="Complete KYC"
- tools:visibility="visible"/>
-
<TextView
android:id="@+id/time"
+ style="@style/TransactionTimestamp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
- android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="@+id/title"
- app:layout_constraintTop_toBottomOf="@+id/actionButton"
+ app:layout_constraintTop_toBottomOf="@+id/extraInfoView"
tools:text="23 min ago" />
<androidx.constraintlayout.widget.Barrier
@@ -95,9 +84,9 @@
<TextView
android:id="@+id/amount"
+ style="@style/TransactionAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/pendingView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
diff --git a/wallet/src/main/res/layout/nav_header_main.xml b/wallet/src/main/res/layout/nav_header_main.xml
index e70e80f..6837814 100644
--- a/wallet/src/main/res/layout/nav_header_main.xml
+++ b/wallet/src/main/res/layout/nav_header_main.xml
@@ -39,7 +39,7 @@
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/nav_header_title"
- android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/talerLogoView" />
diff --git a/wallet/src/main/res/menu/activity_main_drawer.xml b/wallet/src/main/res/menu/activity_main_drawer.xml
index d1cc462..42e4311 100644
--- a/wallet/src/main/res/menu/activity_main_drawer.xml
+++ b/wallet/src/main/res/menu/activity_main_drawer.xml
@@ -39,10 +39,7 @@
<group
android:id="@+id/nav_group_dev"
android:checkableBehavior="single">
- <item
- android:id="@+id/nav_pending_operations"
- android:icon="@drawable/ic_sync"
- android:title="@string/pending_operations_title" />
+ <!-- Future dev options go here -->
</group>
</menu>
</item>
diff --git a/wallet/src/main/res/menu/exchange.xml b/wallet/src/main/res/menu/exchange.xml
index d6a18e7..d99ff00 100644
--- a/wallet/src/main/res/menu/exchange.xml
+++ b/wallet/src/main/res/menu/exchange.xml
@@ -21,4 +21,10 @@
<item
android:id="@+id/action_receive_peer"
android:title="@string/receive_peer" />
+ <item
+ android:id="@+id/action_reload"
+ android:title="@string/exchange_reload" />
+ <item
+ android:id="@+id/action_delete"
+ android:title="@string/transactions_delete" />
</menu>
diff --git a/wallet/src/main/res/menu/pending_operations.xml b/wallet/src/main/res/menu/exchange_list.xml
index b632021..21c028d 100644
--- a/wallet/src/main/res/menu/pending_operations.xml
+++ b/wallet/src/main/res/menu/exchange_list.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
+ ~ (C) 2024 Taler Systems S.A.
~
~ GNU Taler is free software; you can redistribute it and/or modify it under the
~ terms of the GNU General Public License as published by the Free Software
@@ -16,9 +16,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
+
<item
- android:id="@+id/retry_pending"
- android:orderInCategory="100"
- android:title="@string/menu_retry_pending_operations"
+ android:id="@+id/action_add_dev_exchanges"
+ android:title="@string/exchange_list_add_dev"
app:showAsAction="never" />
+
</menu>
diff --git a/wallet/src/main/res/menu/global_dev.xml b/wallet/src/main/res/menu/global_dev.xml
new file mode 100644
index 0000000..d6f73b9
--- /dev/null
+++ b/wallet/src/main/res/menu/global_dev.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2024 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/action_show_logs"
+ android:title="@string/show_logs"
+ android:icon="@drawable/ic_bug_report"
+ app:showAsAction="ifRoom" />
+</menu> \ No newline at end of file
diff --git a/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index ac94b34..0648fb4 100644
--- a/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> \ No newline at end of file
diff --git a/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index ac94b34..0648fb4 100644
--- a/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> \ No newline at end of file
diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml
index ec5ec08..f6c90ab 100644
--- a/wallet/src/main/res/navigation/nav_graph.xml
+++ b/wallet/src/main/res/navigation/nav_graph.xml
@@ -34,12 +34,71 @@
</fragment>
<fragment
+ android:id="@+id/handleUri"
+ android:name="net.taler.wallet.HandleUriFragment"
+ android:label="@string/handle_uri_title">
+ <argument
+ android:name="uri"
+ app:argType="string"
+ app:nullable="false" />
+ <argument
+ android:name="from"
+ app:argType="string"
+ app:nullable="false" />
+
+ <action
+ android:id="@+id/action_handleUri_to_receiveFunds"
+ app:destination="@id/receiveFunds"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_sendFunds"
+ app:destination="@id/sendFunds"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptWithdraw"
+ app:destination="@id/promptWithdraw"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_manualWithdrawal"
+ app:destination="@id/nav_exchange_manual_withdrawal"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptPayment"
+ app:destination="@id/promptPayment"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptPullPayment"
+ app:destination="@id/promptPullPayment"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptPushPayment"
+ app:destination="@id/promptPushPayment"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptPayTemplate"
+ app:destination="@id/promptPayTemplate"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_nav_payto_uri"
+ app:destination="@id/nav_payto_uri"
+ app:popUpTo="@id/nav_main" />
+ </fragment>
+
+ <fragment
android:id="@+id/receiveFunds"
android:name="net.taler.wallet.ReceiveFundsFragment"
android:label="@string/transactions_receive_funds">
<action
- android:id="@+id/action_receiveFunds_to_nav_exchange_manual_withdrawal"
- app:destination="@id/nav_exchange_manual_withdrawal" />
+ android:id="@+id/action_receiveFunds_to_nav_prompt_withdraw"
+ app:destination="@id/promptWithdraw" />
<action
android:id="@+id/action_receiveFunds_to_nav_peer_pull"
app:destination="@id/nav_peer_pull" />
@@ -70,32 +129,17 @@
</fragment>
<fragment
- android:id="@+id/promptTip"
- android:name="net.taler.wallet.tip.PromptTipFragment"
- android:label="Review Tip"
- tools:layout="@layout/fragment_prompt_tip">
- <action
- android:id="@+id/action_promptTip_to_nav_main"
- app:destination="@id/nav_main"
- app:popUpTo="@id/nav_main" />
- <action
- android:id="@+id/action_promptTip_to_alreadyAccepted"
- app:destination="@id/alreadyAccepted"
- app:popUpTo="@id/nav_main" />
- </fragment>
-
- <fragment
android:id="@+id/promptPayment"
android:name="net.taler.wallet.payment.PromptPaymentFragment"
- android:label="Review Payment"
+ android:label="@string/payment_prompt_title"
tools:layout="@layout/fragment_prompt_payment">
<action
android:id="@+id/action_promptPayment_to_nav_main"
app:destination="@id/nav_main"
app:popUpTo="@id/nav_main" />
<action
- android:id="@+id/action_promptPayment_to_alreadyPaid"
- app:destination="@id/alreadyPaid"
+ android:id="@+id/action_promptPayment_to_nav_transactions_detail_payment"
+ app:destination="@id/nav_transactions_detail_payment"
app:popUpTo="@id/nav_main" />
</fragment>
@@ -106,9 +150,6 @@
<action
android:id="@+id/action_nav_settings_to_nav_settings_exchanges"
app:destination="@id/nav_settings_exchanges" />
- <action
- android:id="@+id/action_nav_settings_to_nav_settings_backup"
- app:destination="@id/nav_settings_backup" />
</fragment>
<fragment
@@ -168,11 +209,6 @@
</fragment>
<fragment
- android:id="@+id/nav_settings_backup"
- android:name="net.taler.wallet.settings.BackupSettingsFragment"
- android:label="@string/nav_settings_backup" />
-
- <fragment
android:id="@+id/nav_peer_pull"
android:name="net.taler.wallet.peer.OutgoingPullFragment"
android:label="@string/receive_peer_title">
@@ -181,6 +217,14 @@
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
+ <action
+ android:id="@+id/action_nav_peer_pull_to_nav_main"
+ app:destination="@id/nav_main"
+ app:popUpTo="@id/nav_main" />
+ <action
+ android:id="@+id/action_nav_peer_pull_to_nav_transactions_detail_peer"
+ app:destination="@id/nav_transactions_detail_peer"
+ app:popUpTo="@id/nav_main" />
</fragment>
<fragment
@@ -192,6 +236,14 @@
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
+ <action
+ android:id="@+id/action_nav_peer_push_to_nav_main"
+ app:destination="@id/nav_main"
+ app:popUpTo="@id/nav_main" />
+ <action
+ android:id="@+id/action_nav_peer_push_to_nav_transactions_detail_peer"
+ app:destination="@id/nav_transactions_detail_peer"
+ app:popUpTo="@id/nav_main" />
</fragment>
<fragment
@@ -215,6 +267,19 @@
</fragment>
<fragment
+ android:id="@+id/promptPayTemplate"
+ android:name="net.taler.wallet.payment.PayTemplateFragment"
+ android:label="@string/payment_pay_template_title">
+ <action
+ android:id="@+id/action_promptPayTemplate_to_promptPayment"
+ app:destination="@+id/promptPayment"
+ app:popUpTo="@id/nav_main" />
+ <argument
+ android:name="uri"
+ app:argType="string" />
+ </fragment>
+
+ <fragment
android:id="@+id/nav_transactions"
android:name="net.taler.wallet.transactions.TransactionsFragment"
android:label="@string/transactions_title"
@@ -227,8 +292,7 @@
<fragment
android:id="@+id/nav_transactions_detail_withdrawal"
android:name="net.taler.wallet.transactions.TransactionWithdrawalFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_withdrawal">
+ android:label="@string/transactions_detail_title">
<action
android:id="@+id/action_nav_transactions_detail_withdrawal_to_nav_exchange_manual_withdrawal_success"
app:destination="@id/nav_exchange_manual_withdrawal_success" />
@@ -237,50 +301,37 @@
<fragment
android:id="@+id/nav_transactions_detail_payment"
android:name="net.taler.wallet.transactions.TransactionPaymentFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_payment" />
+ android:label="@string/transactions_detail_title" />
<fragment
android:id="@+id/nav_transactions_detail_refund"
android:name="net.taler.wallet.transactions.TransactionRefundFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_payment" />
+ android:label="@string/transactions_detail_title" />
<fragment
android:id="@+id/nav_transactions_detail_refresh"
android:name="net.taler.wallet.transactions.TransactionRefreshFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_withdrawal" />
+ android:label="@string/transactions_detail_title" />
<fragment
android:id="@+id/nav_transactions_detail_deposit"
android:name="net.taler.wallet.transactions.TransactionDepositFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_withdrawal" />
-
- <fragment
- android:id="@+id/nav_transactions_detail_tip"
- android:name="net.taler.wallet.transactions.TransactionTipFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_withdrawal" />
+ android:label="@string/transactions_detail_title" />
<fragment
android:id="@+id/nav_transactions_detail_peer"
android:name="net.taler.wallet.transactions.TransactionPeerFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_payment" />
+ android:label="@string/transactions_detail_title" />
<fragment
- android:id="@+id/alreadyAccepted"
- android:name="net.taler.wallet.tip.AlreadyAcceptedFragment"
- android:label="@string/tip_already_accepted"
- tools:layout="@layout/fragment_already_accepted" />
+ android:id="@+id/nav_transactions_detail_loss"
+ android:name="net.taler.wallet.transactions.TransactionLossFragment"
+ android:label="@string/transactions_detail_title" />
<fragment
- android:id="@+id/alreadyPaid"
- android:name="net.taler.wallet.payment.AlreadyPaidFragment"
- android:label="@string/payment_already_paid_title"
- tools:layout="@layout/fragment_already_paid" />
+ android:id="@+id/nav_transactions_detail_dummy"
+ android:name="net.taler.wallet.transactions.TransactionDummyFragment"
+ android:label="@string/transactions_detail_title" />
<fragment
android:id="@+id/promptWithdraw"
@@ -288,9 +339,6 @@
android:label="@string/nav_prompt_withdraw"
tools:layout="@layout/fragment_prompt_withdraw">
<action
- android:id="@+id/action_promptWithdraw_to_selectExchangeFragment"
- app:destination="@id/selectExchangeFragment" />
- <action
android:id="@+id/action_promptWithdraw_to_reviewExchangeTOS"
app:destination="@id/reviewExchangeTOS" />
<action
@@ -302,6 +350,10 @@
app:destination="@id/nav_exchange_manual_withdrawal_success"
app:popUpTo="@id/nav_main" />
<action
+ android:id="@+id/action_promptWithdraw_to_nav_transactions_detail_withdrawal"
+ app:destination="@id/nav_transactions_detail_withdrawal"
+ app:popUpTo="@id/nav_main" />
+ <action
android:id="@+id/action_promptWithdraw_to_errorFragment"
app:destination="@id/errorFragment"
app:popUpTo="@id/nav_main" />
@@ -317,17 +369,6 @@
app:destination="@id/promptWithdraw"
app:popUpTo="@id/nav_main" />
</fragment>
- <fragment
- android:id="@+id/selectExchangeFragment"
- android:name="net.taler.wallet.exchanges.SelectExchangeFragment"
- android:label="@string/nav_exchange_select"
- tools:layout="@layout/fragment_exchange_list" />
-
- <fragment
- android:id="@+id/nav_pending_operations"
- android:name="net.taler.wallet.pending.PendingOperationsFragment"
- android:label="@string/pending_operations_title"
- tools:layout="@layout/fragment_pending_operations" />
<fragment
android:id="@+id/nav_uri_input"
@@ -342,6 +383,10 @@
tools:layout="@layout/fragment_error" />
<action
+ android:id="@+id/action_global_handle_uri"
+ app:destination="@id/handleUri" />
+
+ <action
android:id="@+id/action_global_receiveFunds"
app:destination="@id/receiveFunds" />
@@ -350,30 +395,10 @@
app:destination="@id/sendFunds" />
<action
- android:id="@+id/action_global_promptWithdraw"
- app:destination="@id/promptWithdraw" />
-
- <action
android:id="@+id/action_global_promptPayment"
app:destination="@id/promptPayment" />
<action
- android:id="@+id/action_global_promptTip"
- app:destination="@id/promptTip" />
-
- <action
- android:id="@+id/action_global_prompt_pull_payment"
- app:destination="@id/promptPullPayment" />
-
- <action
- android:id="@+id/action_global_prompt_push_payment"
- app:destination="@id/promptPushPayment" />
-
- <action
- android:id="@+id/action_global_pending_operations"
- app:destination="@id/nav_pending_operations" />
-
- <action
android:id="@+id/action_nav_transactions_detail_withdrawal"
app:destination="@id/nav_transactions_detail_withdrawal" />
@@ -394,10 +419,6 @@
app:destination="@id/nav_transactions_detail_deposit" />
<action
- android:id="@+id/action_nav_transactions_detail_tip"
- app:destination="@id/nav_transactions_detail_tip" />
-
- <action
android:id="@+id/action_nav_payto_uri"
app:destination="@id/nav_payto_uri" />
diff --git a/wallet/src/main/res/values-ca/strings.xml b/wallet/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000..591820b
--- /dev/null
+++ b/wallet/src/main/res/values-ca/strings.xml
@@ -0,0 +1,265 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2020 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ --><resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="app_name">Cartera Taler</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">Cartera Taler (experimental)</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler et permet fer transaccions privades, fàcil i ràpidament.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Aquesta aplicació és una cartera per al GNU Taler. És altament experimental i encara no està llesta per a l\'ús quotidià seriós. GNUTaler és un sistema de pagament respectuós amb la privadesa. Els clients es poden mantenir anònims, però els comerciants no poden amagar els seus ingressos fets amb GNU Taler. Això ajuda a evitar l\'evasió d\'impostos i el blanqueig de diners. L\'ús principal de GNU Taler és fer pagaments; no com a bé de refugi o d\'especulació. Els pagaments sempre estan recolzats per una divisa ja existent. Els pagaments es fan després de canviar diners existents per diners digitals amb l\'ajuda d\'un servei d\'intercanvi; és a dir, un proveïdor de serveis de pagament per al Taler. Per a fer un pagament, el client només necessita una cartera carregada de diners. El comerciant pot acceptar els pagaments sense fer que el client es registri al seu lloc web. El GNU Taler és immune a molts tipus de frau, com el «phishing» de dades bancàries o el frau de devolució. En cas de pèrdua o robament, només la quantitat limitada de diners que quedin a la cartera podrien ser perduts.</string>
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="nav_header_subtitle">Cartera</string>
+ <string name="nav_prompt_withdraw">Retira diners digitals</string>
+ <string name="nav_exchange_tos">Termes de servei de l\'intercanvi</string>
+ <string name="nav_exchange_select">Selecciona l\'intercanvi</string>
+ <string name="nav_exchange_fees">Comissions d\'intercanvi</string>
+ <string name="nav_error">Error</string>
+ <string name="button_back">Ves enrere</string>
+ <string name="button_scan_qr_code">Escaneja el codi QR de Taler</string>
+ <string name="enter_uri">Entra l\'URI de Taler</string>
+ <string name="copy" tools:override="true">Copia</string>
+ <string name="copy_uri">Copia l\'URI de Taler</string>
+ <string name="paste">Enganxa</string>
+ <string name="paste_invalid">El portaretalls conté un tipus invàlid de dada</string>
+ <string name="uri_invalid">No és una URI de Taler vàlida</string>
+ <string name="ok">D\'acord</string>
+ <string name="cancel">Cancel·la</string>
+ <string name="search">Cerca</string>
+ <string name="menu">Menú</string>
+ <string name="or">o bé</string>
+ <string name="currency">Divisa</string>
+ <!-- The count should be mirrored in RTL languages -->
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="offline">L\'operació requereix accés a internet. Si us plau, assegura\'t que la connexió a internet funciona i torna a intentar-ho.</string>
+ <string name="error_unsupported_uri">Error: Aquesta URI de Taler no està suportada.</string>
+ <string name="menu_settings">Paràmetres</string>
+ <string name="menu_retry_pending_operations">Reintenta les operacions pendents</string>
+ <string name="host_apdu_service_desc">Pagaments de Taler per NFC</string>
+ <string name="balances_title">Saldos</string>
+ <string name="amount_positive">+%s</string>
+ <string name="amount_negative">-%s</string>
+ <string name="amount_chosen">Quantitat escollida</string>
+ <string name="balances_inbound_label">entrant</string>
+ <string name="balances_empty_state">No hi ha diners digitals a la teva cartera.
+\n
+\nPots aconseguir diners de prova del banc de demostració:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="transactions_title">Transaccions</string>
+ <string name="transactions_balance">Saldo</string>
+ <string name="transactions_send_funds">Envia
+\nFons</string>
+ <string name="transactions_receive_funds">Rep
+\nFons</string>
+ <string name="transactions_empty">No tens cap transacció</string>
+ <string name="transactions_empty_search">No s\'han trobat transaccions. Prova una cerca diferent.</string>
+ <string name="transactions_error">No s\'han pogut carregar transaccions
+\n
+\n%s</string>
+ <string name="transactions_detail_title">Transacció</string>
+ <string name="transactions_detail_title_currency">%s Transaccions</string>
+ <string name="transactions_delete">Esborrar</string>
+ <string name="transactions_select_all">Seleccionar-ho tot</string>
+ <string name="transactions_delete_dialog_title">Esborrar la transacció</string>
+ <string name="transactions_delete_dialog_message">Estàs segur que vols eliminar aquesta transacció de la teva cartera\?</string>
+ <string name="transactions_delete_selected_dialog_message">Estàs segur que vols eliminar les transaccions seleccionades de la teva cartera\?</string>
+ <string name="transactions_cancel_dialog_message">Estàs segur que vols cancel·lar aquesta retirada\? Podries perdre els diners que encara estiguin en trànsit.</string>
+ <!-- Transactions -->
+ <string name="transaction_paid">Pagat</string>
+ <string name="transaction_order_total">Total</string>
+ <string name="transaction_order">Compra</string>
+ <string name="transaction_order_id">Rebut #%1$s</string>
+ <string name="transaction_tip_from">Recompensa de %s</string>
+ <string name="transaction_refund">Reemborsament</string>
+ <string name="transaction_refund_from">Reemborsament de %s</string>
+ <string name="transaction_pending">PENDENT</string>
+ <string name="transaction_refresh">Comissió de canvi de venciment de monedes</string>
+ <string name="transaction_deposit">Dipòsit</string>
+ <string name="transaction_peer_push_debit">Pagament anticipat</string>
+ <string name="transaction_peer_pull_credit">Factura</string>
+ <string name="transaction_peer_pull_debit">Factura pagada</string>
+ <string name="transaction_peer_push_credit">Pagament anticipat</string>
+ <string name="transaction_action_kyc">Completa KYC (política \'Know Your Customer\')</string>
+ <string name="payment_title">Pagament</string>
+ <string name="payment_fee">+%s comissió de pagament</string>
+ <string name="payment_button_confirm">Confirma el pagament</string>
+ <string name="payment_label_amount_total">Quantitat total:</string>
+ <string name="payment_label_order_summary">Compra</string>
+ <string name="payment_error">Error: %s</string>
+ <string name="payment_balance_insufficient">Saldo insuficient!</string>
+ <string name="payment_aborted">Avortat</string>
+ <string name="payment_failed">Ha fallat</string>
+ <string name="payment_initiated">Pagament començat</string>
+ <string name="payment_already_paid_title">Ja pagat</string>
+ <string name="payment_already_paid">Ja has pagat aquesta compra.</string>
+ <string name="receive_amount">Quantitat a rebre</string>
+ <string name="receive_amount_invalid">Quantitat invàlida</string>
+ <string name="receive_intro">Tria d\'on rebre els diners:</string>
+ <string name="receive_withdraw">Retirar diners d\'un compte bancari</string>
+ <string name="receive_peer">Factura una altra cartera</string>
+ <string name="receive_peer_title">Sol·licitud de pagament</string>
+ <string name="receive_peer_create_button">Crea factura</string>
+ <string name="receive_peer_invoice_instruction">Fes que el client escanegi aquest codi QR per pagar:</string>
+ <string name="receive_peer_invoice_uri">Alternativament, copia i envia aquesta URI:</string>
+ <string name="send_amount">Quantitat a enviar</string>
+ <string name="send_intro">Tria on vols enviar els diners:</string>
+ <string name="send_deposit">A un compte bancari</string>
+ <string name="send_deposit_bitcoin">A una cartera de Bitcoin</string>
+ <string name="send_deposit_title">Deposita a un compte bancari</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="send_deposit_name">Titular del compte</string>
+ <string name="send_deposit_bitcoin_address">Adreça de Bitcoin</string>
+ <string name="send_deposit_check_fees_button">Despeses de comprovació</string>
+ <string name="send_deposit_amount_effective">Quantitat efectiva</string>
+ <string name="send_deposit_create_button">Fes un dipòsit</string>
+ <string name="send_deposit_bitcoin_create_button">Transferir Bitcoin</string>
+ <string name="send_peer">A una altra cartera</string>
+ <string name="send_peer_bitcoin">A una altra cartera de Taler</string>
+ <string name="send_peer_title">Envia diners a una altra cartera</string>
+ <string name="send_peer_create_button">Envia diners ara</string>
+ <string name="send_peer_warning">Avís: Els diners sortiran de la cartera immediatament.</string>
+ <string name="send_peer_payment_instruction">Fes que el beneficiari escanegi aquest codi QR per rebre:</string>
+ <string name="send_peer_payment_amount_received">Quantitat rebuda</string>
+ <string name="send_peer_payment_amount_sent">Quantitat enviada</string>
+ <string name="send_peer_expiration_period">Expira en</string>
+ <string name="send_peer_expiration_1d">1 dia</string>
+ <string name="send_peer_expiration_7d">7 dies</string>
+ <string name="send_peer_expiration_30d">30 dies</string>
+ <string name="send_peer_expiration_custom">Personalitzat</string>
+ <string name="send_peer_expiration_days">Dies</string>
+ <string name="send_peer_expiration_hours">Hores</string>
+ <string name="send_peer_purpose">Propòsit</string>
+ <string name="pay_peer_title">Factura de pagament</string>
+ <string name="pay_peer_intro">Vols pagar aquesta factura\?</string>
+ <string name="receive_peer_payment_title">Rebre un pagament</string>
+ <string name="receive_peer_payment_intro">Vols rebre aquest pagament\?</string>
+ <string name="withdraw_initiated">Retirada iniciada</string>
+ <string name="withdraw_title">Retirada</string>
+ <string name="withdraw_total">Retirar</string>
+ <string name="withdraw_fees">Comissió</string>
+ <string name="withdraw_restrict_age">Restringeix l\'ús a l\'edat de</string>
+ <string name="withdraw_restrict_age_unrestricted">No restringit</string>
+ <string name="withdraw_exchange">Intercanvi</string>
+ <string name="withdraw_button_confirm">Confirmar retirada de diners</string>
+ <string name="withdraw_button_confirm_bank">Confirma amb el banc</string>
+ <string name="withdraw_button_tos">Comprova els termes</string>
+ <string name="withdraw_waiting_confirm">Esperant confirmació</string>
+ <string name="withdraw_manual_title">Fes una transferència manual a l\'intercanvi</string>
+ <string name="withdraw_amount">Quant vols retirar\?</string>
+ <string name="withdraw_amount_error">Insereix una quantitat vàlida</string>
+ <string name="withdraw_manual_payment_options">Les opcions de pagament estan suportades per %1$s:
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">Comprova la comissió</string>
+ <string name="withdraw_manual_ready_title">L\'intercanvi està llest perquè retiris els diners!</string>
+ <string name="withdraw_manual_ready_intro">Per completar el procés necessites transferir %s al teu compte bancari</string>
+ <string name="withdraw_manual_ready_details_intro">Detalls de transferència bancària</string>
+ <string name="withdraw_manual_bitcoin_title">L\'intercanvi de Bitcoin està llest perquè retiris els dieners</string>
+ <string name="withdraw_manual_bitcoin_intro">Ara fes una transacció dividida amb aquestes tres sortides.</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_subject">Concepte</string>
+ <string name="withdraw_manual_ready_bank_button">Obre en una app bancària</string>
+ <string name="withdraw_manual_ready_cancel">Cancel·la la retidada de diners</string>
+ <string name="withdraw_manual_ready_warning">Assegura\'t d\'emprar el concepte correcte, del contrari els diners no arribaran a la cartera.</string>
+ <string name="withdraw_error_title">Error retirant els diners</string>
+ <string name="withdraw_error_message">Retirar diners no és possible ara mateix. Si us plau, intenta-ho més tard!</string>
+ <string name="withdraw_error_test">Error retirant TESTKUDOS</string>
+ <string name="exchange_settings_title">Canvis</string>
+ <string name="exchange_settings_summary">Gestionar la llista de canvis coneguts per a aquesta cartera</string>
+ <string name="exchange_list_title">Canvis</string>
+ <string name="exchange_list_empty">No es coneixen canvis
+\n
+\nAfegeix-ne un manualment o retira diners digitals!</string>
+ <string name="exchange_list_currency">Divisa: %s</string>
+ <string name="exchange_list_add">Afegeix un canvi</string>
+ <string name="exchange_add_url">Afegeix adreça de canvi</string>
+ <string name="exchange_add_error">No s\'ha pogut afegir el canvi</string>
+ <string name="exchange_menu_manual_withdraw">Retirar</string>
+ <string name="exchange_fee_withdrawal_fee_label">Comissió de retirament:</string>
+ <string name="exchange_fee_overhead_label"/>
+ <string name="exchange_fee_coin_expiration_label"/>
+ <string name="exchange_fee_coin_fees_label"/>
+ <string name="exchange_fee_wire_fees_label"/>
+ <plurals name="exchange_fee_coin">
+ <item quantity="one"/>
+ <item quantity="other"/>
+ </plurals>
+ <string name="exchange_fee_withdraw_fee"/>
+ <string name="exchange_fee_deposit_fee"/>
+ <string name="exchange_fee_refresh_fee"/>
+ <string name="exchange_fee_refund_fee"/>
+ <string name="exchange_fee_wire_fee_timespan"/>
+ <string name="exchange_fee_wire_fee_wire_fee"/>
+ <string name="exchange_fee_wire_fee_closing_fee"/>
+ <string name="exchange_tos_accept"/>
+ <string name="exchange_tos_error"/>
+ <string name="pending_operations_title"/>
+ <string name="pending_operations_refuse"/>
+ <string name="pending_operations_no_action"/>
+ <string name="settings_dev_mode"/>
+ <string name="settings_dev_mode_summary"/>
+ <string name="settings_withdraw_testkudos"/>
+ <string name="settings_withdraw_testkudos_summary"/>
+ <string name="settings_logcat"/>
+ <string name="settings_logcat_summary"/>
+ <string name="settings_logcat_error"/>
+ <string name="settings_logcat_success"/>
+ <string name="settings_db_export"/>
+ <string name="settings_db_export_summary"/>
+ <string name="settings_db_export_error"/>
+ <string name="settings_db_export_success"/>
+ <string name="settings_version_app"/>
+ <string name="settings_version_core"/>
+ <string name="settings_version_protocol_exchange"/>
+ <string name="settings_version_protocol_merchant"/>
+ <string name="settings_version_unknown"/>
+ <string name="settings_reset"/>
+ <string name="settings_reset_summary"/>
+ <string name="nav_settings_backup"/>
+ <string name="backup_last"/>
+ <string name="refund_title"/>
+ <string name="refund_error"/>
+ <string name="refund_success"/>
+ <string name="tip_title"/>
+ <string name="tip_already_accepted"/>
+ <string name="tip_total"/>
+ <string name="tip_fees"/>
+ <string name="tip_exchange"/>
+ <string name="tip_merchant_url"/>
+ <string name="tip_button_confirm"/>
+ <string name="tip_received"/>
+ <string name="wifi_disabled_error"/>
+ <string name="wifi_connect_error"/>
+ <string name="transaction_dummy_title">Transacció desconeguda</string>
+ <string name="send_deposit_iban_error">L\'IBAN és invàlid</string>
+ <string name="send_deposit_bic_error">El BIC és invàlid</string>
+ <string name="button_scan_qr_code_label">Escaneja el codi QR</string>
+ <string name="payment_prompt_title">Revisar pagament</string>
+ <string name="payment_pay_template_title">Personalitza la teva comanda</string>
+ <string name="payment_create_order">Crea una comanda</string>
+ <string name="payment_confirmation_code">Codi de confirmació</string>
+ <string name="payment_template_error">Error creant la comanda</string>
+ <string name="transactions_retry">Reintentar</string>
+ <string name="transactions_abort">Avortar</string>
+ <string name="transactions_fail">Matar</string>
+ <string name="transactions_suspend">Suspendre</string>
+ <string name="transactions_fail_dialog_title">Matar transacció</string>
+ <string name="transactions_fail_dialog_message">Estàs segur que vols matar aquesta transacció\? Els diners en transit ES PERDRAN.</string>
+ <string name="transactions_resume">Continuar</string>
+ <string name="transactions_abort_dialog_title">Avortar transacció</string>
+ <string name="transactions_abort_dialog_message">Estàs segur que vols avortar aquesta transacció\? Els diners en trànsit es podrien perdre.</string>
+ <string name="offline_banner">Ni hi ha accés a internet</string>
+ <string name="exchange_list_error">No s\'han pogut llistar serveis de canvi</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-de/strings.xml b/wallet/src/main/res/values-de/strings.xml
index 8460f65..f4e3fed 100644
--- a/wallet/src/main/res/values-de/strings.xml
+++ b/wallet/src/main/res/values-de/strings.xml
@@ -2,16 +2,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="amount_chosen">Gewählter Betrag</string>
<string name="settings_version_app">App-Version</string>
- <string name="host_apdu_service_desc">Taler NFC-Zahlungen</string>
- <string name="exchange_settings_title">Taler-Exchanges</string>
+ <string name="host_apdu_service_desc">Taler-NFC-Zahlungen</string>
+ <string name="exchange_settings_title">Zahlungsdienstleister</string>
<string name="balances_inbound_label">Eingehend</string>
<string name="or">oder</string>
- <string name="offline">Internetzugang erforderlich. Bitte stellen Sie sicher, daß Ihre Internetverbindung funktioniert und versuchen Sie es noch einmal.</string>
+ <string name="offline">Internetzugang erforderlich. Bitte stellen Sie sicher, dass Ihre Internetverbindung funktioniert, und versuchen Sie es noch einmal.</string>
<string name="error_unsupported_uri">Fehler: Dieser Taler-URI wird nicht unterstützt.</string>
<string name="menu_settings">Einstellungen</string>
- <string name="menu_retry_pending_operations">Wiederholen Sie ausstehende Vorgänge</string>
+ <string name="menu_retry_pending_operations">Ausstehende Vorgänge wiederholen</string>
<string name="balances_title">Salden</string>
- <string name="app_name">Taler-Wallet</string>
+ <string name="app_name">Taler Wallet</string>
<string name="transactions_title">Transaktionen</string>
<string name="transactions_empty">Sie haben keine Transaktionen</string>
<string name="transactions_empty_search">Keine Transaktion gefunden. Versuchen Sie eine andere Suche.</string>
@@ -42,12 +42,12 @@
<string name="withdraw_title">Abhebung</string>
<string name="withdraw_total">Abheben</string>
<string name="withdraw_fees">Gebühr</string>
- <string name="withdraw_exchange">Taler-Exchange</string>
+ <string name="withdraw_exchange">Zahlungsdienstleister</string>
<string name="withdraw_button_confirm">Abhebung bestätigen</string>
<string name="withdraw_button_confirm_bank">Bestätigen mit</string>
<string name="withdraw_button_tos">Nutzungsbedingungen überprüfen</string>
<string name="withdraw_waiting_confirm">Warte auf Bestätigung</string>
- <string name="withdraw_manual_title">Führen Sie eine manuelle Einzahlung an die Geldbörse durch</string>
+ <string name="withdraw_manual_title">Manuelle Überweisung an den Zahlungsdienstleister</string>
<string name="withdraw_amount">Wie viel soll abgehoben werden\?</string>
<string name="withdraw_amount_error">Gültigen Betrag eingeben</string>
<string name="withdraw_manual_payment_options">Zahlungsoptionen unterstützt von %1$s:
@@ -57,33 +57,33 @@
<string name="withdraw_error_title">Auszahlungsfehler</string>
<string name="withdraw_error_message">Ein Abheben ist derzeit nicht möglich. Bitte zu einem späteren Zeitpunkt erneut versuchen!</string>
<string name="withdraw_error_test">Fehler beim Abheben von TESTKUDOS aufgetreten</string>
- <string name="exchange_settings_summary">Verwalten Sie die Liste der Exchanges, die dieser Geldbörse bekannt sind</string>
+ <string name="exchange_settings_summary">Verwaltung der Zahlungsdienstleister, die diesem Wallet bekannt sind</string>
<string name="payment_fee">+%s Zahlungsgebühr</string>
- <string name="exchange_list_title">Liste von Exchanges</string>
- <string name="exchange_list_empty">Keine Exchanges bekannt
+ <string name="exchange_list_title">Liste von Zahlungsdienstleistern</string>
+ <string name="exchange_list_empty">Keine Zahlungsdienstleister bekannt
\n
-\nFügen Sie einen Exchange manuell hinzu oder heben Sie digitales Geld ab!</string>
+\nFügen Sie einen Zahlungsdienstleister manuell hinzu oder heben Sie digitales Bargeld ab!</string>
<string name="exchange_list_currency">Währung: %s</string>
- <string name="exchange_list_add">Ein Exchange hinzufügen</string>
- <string name="exchange_add_error">Exchange konnte nicht hinzugefügt werden</string>
+ <string name="exchange_list_add">Zahlungsdienstleister hinzufügen</string>
+ <string name="exchange_add_error">Zahlungsdienstleister konnte nicht hinzugefügt werden</string>
<string name="exchange_menu_manual_withdraw">Abheben</string>
- <string name="exchange_fee_withdrawal_fee_label">Abhebungsgebühr:</string>
+ <string name="exchange_fee_withdrawal_fee_label">Abhebegebühr:</string>
<string name="exchange_fee_overhead_label">Rundungsverlust:</string>
- <string name="exchange_fee_coin_expiration_label">Frühester Münzablauf:</string>
- <string name="exchange_fee_coin_fees_label">Münzgebühren</string>
- <string name="exchange_fee_wire_fees_label">Übermittlungsgebühren</string>
+ <string name="exchange_fee_coin_expiration_label">Frühestes Gültigkeitsende:</string>
+ <string name="exchange_fee_coin_fees_label">Münzgebühr</string>
+ <string name="exchange_fee_wire_fees_label">Überweisungsgebühr</string>
<plurals name="exchange_fee_coin">
<item quantity="one">Münze: %s (verwendet %d Mal)</item>
<item quantity="other">Münze: %s (verwendet %d Male)</item>
</plurals>
- <string name="exchange_fee_withdraw_fee">Abhebungsgebühr: %s</string>
- <string name="exchange_fee_deposit_fee">Einzahlungsgebühr: %s</string>
- <string name="exchange_fee_refresh_fee">Änderungsgebühr: %s</string>
+ <string name="exchange_fee_withdraw_fee">Abhebegebühr: %s</string>
+ <string name="exchange_fee_deposit_fee">Einlösegebühr: %s</string>
+ <string name="exchange_fee_refresh_fee">Wechselgebühr: %s</string>
<string name="exchange_fee_refund_fee">Rückerstattungsgebühr: %s</string>
- <string name="exchange_fee_wire_fee_timespan">Zeitspanne: %1$s - %2$s</string>
- <string name="exchange_fee_wire_fee_wire_fee">Übermittlungsgebühren: %s</string>
- <string name="exchange_fee_wire_fee_closing_fee">Abschlussgebühr: %s</string>
- <string name="exchange_tos_accept">Nutzungsbedingungen akzeptieren</string>
+ <string name="exchange_fee_wire_fee_timespan">Zeitraum: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Überweisungsgebühr: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Rücküberweisungsgebühr: %s</string>
+ <string name="exchange_tos_accept">Allgemeine Geschäftsbedingungen akzeptieren</string>
<string name="pending_operations_title">Schwebende Vorgänge</string>
<string name="pending_operations_refuse">Vorschlag ablehnen</string>
<string name="pending_operations_no_action">(keine Aktion)</string>
@@ -92,7 +92,7 @@
<string name="settings_withdraw_testkudos">TESTKUDOS abheben</string>
<string name="settings_withdraw_testkudos_summary">Geld für Testen bereitstellen</string>
<string name="settings_logcat">Fehlerprotokoll</string>
- <string name="settings_logcat_summary">Internes Protokoll anzeigen / senden</string>
+ <string name="settings_logcat_summary">Internes Protokoll speichern</string>
<string name="settings_version_core">Wallet Core-Version</string>
<string name="settings_version_protocol_merchant">Unterstützte Händler-Versionen</string>
<string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler ermöglicht schnelle und einfache Online-Transaktionen mit Datenschutz.</string>
@@ -104,23 +104,23 @@
<string name="backup_last">Letzte Sicherung: %s</string>
<string name="refund_title">Erstattung</string>
<string name="refund_error">Fehler bei der Verarbeitung der Rückerstattung</string>
- <string name="refund_success">Rückerstattung erhalten: %s</string>
+ <string name="refund_success">Rückerstattung erhalten!</string>
<string name="tip_title">Trinkgeld</string>
<string name="wifi_disabled_error">Schalten Sie Wi-Fi ein, um kostenloses Wi-Fi zu erhalten</string>
<string name="wifi_connect_error">Es konnte keine Verbindung zu kostenlosem WLAN hergestellt werden: %s</string>
- <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Diese App ist eine Geldbörse für GNU Taler (Wallet). Die Anwendung läuft zurzeit in experimentellem Stadium und dient nur zu Demonstrationszwecken. Taler bieten allen Zahlenden ein Bezahlsystem mit Datenschutz. Die Käufer bleiben anonym. Händler müssen dagegen ihr Einkommen vor Auditoren offenlegen. Dadurch werden Steuerhinterziehung und Geldwäsche vermieden. Die Hauptfunktion von GNU Taler ist die Abwicklung von Zahlungsvorgängen; es ist kein Mittel zur Geldaufbewahrung. Alle Zahlungen werden durch eine existierende Währung gedeckt. Zahlungen werden mit Hilfe eines Taler-Exchange durchgeführt, indem die Käufer Geld an den Exchange überweisen und dieser elektronische Zahlungsmittel (Coins) in die Wallets der Käufer überträgt. Um eine Zahlung durchzuführen benötigen Kunden nur ausreichende Coin-Werte im Wallet. Verkäufer können Zahlungen akzeptieren ohne Kunden zur Registrierung auf Webseiten zu zwingen. GNU Taler ist immun gegen viele Formen von Betrug wie z.B. Phishing von Kreditkartendaten oder Betrug durch Rückbuchungen. Im Fall von Verlust oder Diebstahl von Wallets verliert man höchstens den Geldbetrag, welcher sich noch im Wallet befindet.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Diese App ist eine Geldbörse für GNU Taler (Wallet). Taler ist ein Bezahlsystem mit Datenschutz für die Käufer. Die Käufer bleiben anonym, das Einkommen der Händler hingegen ist offengelegt. Dies vermindert systematisch die Möglichkeit von Steuerhinterziehung und Geldwäsche. Die Hauptfunktion von GNU Taler ist die Abwicklung von Zahlungsvorgängen; es ist kein Mittel zur Geldaufbewahrung. Alle Zahlungen werden durch eine existierende Währung gedeckt. Zahlungen werden mit Hilfe eines Taler-Zahlungsdiensts (Exchange) durchgeführt, indem die Käufer Geld an den Zahlungsdienst überweisen und dieser elektronische Zahlungsmittel (Coins, Token) in die Wallets überträgt. Um eine Zahlung durchzuführen, benötigen Kunden ausreichende Coin-Werte im Wallet. Verkäufer können Zahlungen akzeptieren, ohne Kunden zur Registrierung auf Webseiten zwingen zu müssen. GNU Taler ist immun gegen viele Formen von Betrug wie z.B. Phishing von Kreditkartendaten oder Betrug durch Rückbuchungen. Im Fall von Verlust oder Diebstahl von Wallets verliert man höchstens die Werte, welche sich noch im Wallet befinden.</string>
<string name="nav_header_title">GNU Taler</string>
<string name="nav_header_subtitle">Wallet</string>
<string name="nav_prompt_withdraw">Digitales Bargeld abheben</string>
- <string name="nav_exchange_tos">Nutzungsbedingungen des Exchange</string>
- <string name="nav_exchange_select">Exchange auswählen</string>
- <string name="nav_exchange_fees">Exchange Gebühren</string>
+ <string name="nav_exchange_tos">Allgemeine Geschäftsbedingungen des Zahlungsdienstleisters</string>
+ <string name="nav_exchange_select">Zahlungsdienstleister auswählen</string>
+ <string name="nav_exchange_fees">Gebühren des Zahlungsdienstleisters</string>
<string name="enter_uri">Geben Sie die Taler-URI ein</string>
<string name="paste">Einfügen</string>
<string name="paste_invalid">Die Zwischenablage enthält einen ungültigen Datentyp</string>
<string name="uri_invalid">Keine gültige Taler-URI</string>
- <string name="ok">OK</string>
- <string name="cancel">Abbrechen</string>
+ <string name="ok">Bestätigen</string>
+ <string name="cancel">Zurück</string>
<string name="search">Suche</string>
<string name="menu">Menü</string>
<string name="nav_error">Fehler</string>
@@ -132,20 +132,135 @@
<string name="transactions_error">Transaktionen konnten nicht geladen werden
\n
\n%s</string>
- <string name="exchange_tos_error">Fehler beim Anzeigen der Nutzungsbedingungen: %s</string>
+ <string name="exchange_tos_error">Fehler beim Anzeigen der AGB: %s</string>
<string name="settings_version_protocol_exchange">Unterstützte Exchange-Versionen</string>
<string name="amount_negative">- %s</string>
- <string name="amount_positive">+ %s</string>
+ <string name="amount_positive">+%s</string>
<string name="transaction_tip_from">Trinkgeld von %s</string>
- <string name="exchange_add_url">Adresse des Exchange eingeben</string>
+ <string name="exchange_add_url">Adresse des Zahlungsdienstleisters eingeben</string>
<string name="button_back">Zurück</string>
- <string name="button_scan_qr_code">Taler QR Code Einscannen</string>
+ <string name="button_scan_qr_code">Taler-QR-Code einscannen</string>
<string name="withdraw_manual_ready_iban">IBAN</string>
- <string name="withdraw_manual_ready_title">Der Exchange ist für den Abhebevorgang eingerichtet!</string>
- <string name="withdraw_manual_ready_intro">Um den Abhebevorgang durchführen zu lassen, müssen Sie %s auf das Bankkonto des Exchange überweisen</string>
+ <string name="withdraw_manual_ready_title">Der Zahlungsdienstleister ist für den Abhebevorgang bereit!</string>
+ <string name="withdraw_manual_ready_intro">Zum Abschluss des Abhebevorgangs müssen Sie %s auf das Bankkonto des Zahlungsdienstleisters überweisen</string>
<string name="withdraw_manual_ready_details_intro">Details der Überweisung</string>
<string name="withdraw_manual_ready_subject">Überweisungsvermerk</string>
<string name="withdraw_manual_ready_bank_button">In Banking-App öffnen</string>
<string name="withdraw_manual_ready_cancel">Abhebevorgang abbrechen</string>
<string name="withdraw_manual_ready_warning">Bitte vergewissern Sie sich, dass Sie den Überweisungsvermerk richtig angeben, da sonst der überwiesene Betrag nicht im Wallet ankommt.</string>
+ <string name="reset">Zurücksetzen</string>
+ <string name="payment_prompt_title">Zahlung überprüfen</string>
+ <string name="send_deposit_iban_error">IBAN ist ungültig</string>
+ <string name="send_peer_create_button">Jetzt Geld senden</string>
+ <string name="send_peer_title">Geld an ein anderes Wallet senden</string>
+ <string name="send_deposit_bitcoin_address">Bitcoin-Adresse</string>
+ <string name="transaction_action_kyc">KYC abschließen</string>
+ <string name="withdraw_restrict_age">Nutzung auf Alter beschränken</string>
+ <string name="send_deposit_bitcoin_create_button">Bitcoin übertragen</string>
+ <string name="settings_db_export">Datenbank exportieren</string>
+ <string name="transactions_delete_dialog_message">Sind Sie sicher, dass Sie diese Transaktion aus Ihrem Wallet entfernen möchten?</string>
+ <string name="transactions_delete_dialog_title">Transaktion löschen</string>
+ <string name="receive_peer_payment_intro">Möchten Sie diese Zahlung erhalten?</string>
+ <string name="transactions_abort">Abbruch ausführen</string>
+ <string name="payment_pay_template_title">Passen Sie Ihre Bestellung an</string>
+ <string name="send_intro">Wählen Sie aus, wohin Sie Geld senden möchten:</string>
+ <string name="send_deposit_title">Einzahlung auf ein Bankkonto</string>
+ <string name="withdraw_bank">Bank</string>
+ <string name="withdraw_manual_ready_account">Konto</string>
+ <string name="withdraw_manual_ready_receiver">Name des Empfängers</string>
+ <string name="withdraw_transfer">Überweisung</string>
+ <string name="exchange_delete_force">Löschung erzwingen (bereinigen)</string>
+ <string name="settings_db_export_summary">Interne Datenbank speichern</string>
+ <string name="settings_db_clear_error">Fehler beim Bereinigen der Datenbank</string>
+ <string name="settings_alert_reset_canceled">Zurücksetzen abgebrochen</string>
+ <string name="exchange_not_contacted">Zahlungsdienstleister nicht kontaktiert</string>
+ <string name="send_peer_expiration_1d">1Tag</string>
+ <string name="settings_test">Integrationstest durchführen</string>
+ <string name="settings_test_summary">Führt Test-Transaktionen mit Demo-Setup durch</string>
+ <string name="copy" tools:override="true">Kopieren</string>
+ <string name="copy_uri">Taler-URI kopieren</string>
+ <string name="transaction_peer_pull_debit">Rechnung bezahlt</string>
+ <string name="transaction_peer_pull_credit">Rechnung</string>
+ <string name="transactions_receive_funds">Geld
+\nempfangen</string>
+ <string name="send_amount">Zu sendender Betrag</string>
+ <string name="pay_peer_title">Rechnung bezahlen</string>
+ <string name="pay_peer_intro">Möchten Sie diese Rechnung bezahlen?</string>
+ <string name="receive_peer_payment_title">Zahlung erhalten</string>
+ <string name="send_peer_expiration_hours">Stunden</string>
+ <string name="share_payment">Zahlungs-Link teilen</string>
+ <string name="offline_banner">Kein Internetzugang</string>
+ <string name="transactions_fail_dialog_title">Transaktion sofort abbrechen</string>
+ <string name="transaction_dummy_title">Unbekannte Transaktion</string>
+ <string name="receive_peer_title">Zahlung anfordern</string>
+ <string name="send_deposit">Auf ein Bankkonto</string>
+ <string name="send_peer_bitcoin">An ein anderes Taler-Wallet</string>
+ <string name="send_peer_expiration_custom">Benutzerdefiniert</string>
+ <string name="exchange_delete">Zahlungsdienstleister löschen</string>
+ <string name="settings_db_export_success">Datenbank in Datei exportiert</string>
+ <string name="receive_peer_create_button">Rechnung erstellen</string>
+ <string name="receive_peer_invoice_uri">Alternativ kopieren und senden Sie diesen URI:</string>
+ <string name="receive_peer_invoice_instruction">Lassen Sie die Käufer diesen QR-Code scannen, um den Betrag zu bezahlen:</string>
+ <string name="button_scan_qr_code_label">QR-Code scannen</string>
+ <string name="send_deposit_check_fees_button">Gebühren prüfen</string>
+ <string name="send_deposit_name">Kontoinhaber/in</string>
+ <string name="transactions_send_funds">Geld
+\nsenden</string>
+ <string name="currency">Währung</string>
+ <string name="transactions_balance">Guthaben</string>
+ <string name="withdraw_restrict_age_unrestricted">Uneingeschränkt</string>
+ <string name="settings_logcat_success">Protokoll wurde in Datei exportiert</string>
+ <string name="settings_logcat_error">Fehler beim Exportieren des Protokolls</string>
+ <string name="transactions_delete_selected_dialog_message">Sind Sie sicher, dass Sie die ausgewählten Transaktionen aus Ihrem Wallet entfernen möchten?</string>
+ <string name="transactions_resume">Fortsetzen</string>
+ <string name="transactions_abort_dialog_title">Transaktion abbrechen</string>
+ <string name="transactions_cancel_dialog_message">Sind Sie sicher, dass Sie diese Abhebung stornieren möchten? Gelder, die gerade noch übertragen werden, könnten verloren gehen.</string>
+ <string name="send_deposit_bic">BIC / SWIFT</string>
+ <string name="send_peer">An ein anderes Wallet</string>
+ <string name="payment_confirmation_code">Bestätigungscode</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_bic_error">BIC ist ungültig</string>
+ <string name="withdraw_subtitle">Ziel-Bankkonto auswählen</string>
+ <string name="settings_db_import">Datenbank importieren</string>
+ <string name="settings_db_import_summary">Datenbank aus Datei wiederherstellen</string>
+ <string name="settings_db_export_error">Fehler beim Exportieren der Datenbank</string>
+ <string name="settings_db_import_error">Fehler beim Importieren der Datenbank</string>
+ <string name="settings_dialog_import_message">Dieser Vorgang überschreibt Ihre vorhandene Datenbank. Möchten Sie fortfahren?</string>
+ <string name="settings_dialog_reset_message">Möchten Sie das Wallet wirklich zurücksetzen und alle Münzen und Einkäufe verlieren?</string>
+ <string name="settings_alert_reset_done">Wallet wurde zurückgesetzt</string>
+ <string name="send_deposit_bitcoin">An ein Bitcoin-Wallet</string>
+ <string name="send_peer_purpose">Zweck</string>
+ <string name="exchange_list_select">Zahlungsdienstleister auswählen</string>
+ <string name="exchange_dialog_delete_message">Sind Sie sicher, dass Sie diesen Zahlungsdienstleister löschen möchten? Wenn Sie diesen Vorgang erzwingen, führt dies zu einem Geldverlust.</string>
+ <string name="settings_db_import_success">Datenbank aus Datei importiert</string>
+ <string name="error_broken_uri">Fehler: Diese Taler-URI funktioniert (im Moment) nicht.</string>
+ <string name="settings_alert_import_canceled">Import abgebrochen</string>
+ <string name="import_db">Importieren</string>
+ <string name="transactions_detail_title_currency">%s Transaktionen</string>
+ <string name="receive_withdraw">Vom Bankkonto abheben</string>
+ <string name="send_deposit_amount_effective">Effektiver Betrag</string>
+ <string name="send_deposit_create_button">Einzahlung tätigen</string>
+ <string name="receive_amount">Zu erhaltender Betrag</string>
+ <string name="receive_amount_invalid">Betrag ungültig</string>
+ <string name="receive_intro">Wählen Sie aus, woher Sie Geld erhalten möchten:</string>
+ <string name="amount_sent">Gesendeter Betrag</string>
+ <string name="amount_received">Empfangener Betrag</string>
+ <string name="payment_pending">Zahlung nicht abgeschlossen - sie wird erneut versucht</string>
+ <string name="receive_peer_amount_invoiced">Rechnungsbetrag</string>
+ <string name="send_peer_payment_instruction">Lassen Sie den Geldempfänger diesen QR-Code scannen, um den Wert zu erhalten:</string>
+ <string name="transaction_deposit">Einlösen</string>
+ <string name="exchange_list_error">Es konnten keine Zahlungsdienstleister angezeigt werden</string>
+ <string name="withdraw_conversion_support">Dieser Zahlungsdienstleister unterstützt Währungsumtausch</string>
+ <string name="exchange_list_add_dev">Zahlungsdienstleister im Entwicklermodus hinzufügen</string>
+ <string name="send_peer_expiration_30d">30 Tage</string>
+ <string name="send_peer_expiration_7d">7 Tage</string>
+ <string name="balance_scope_exchange">Zahlungsdienstleister: %1$s</string>
+ <string name="balance_scope_auditor">Auditor: %1$s</string>
+ <string name="receive_peer">Rechnung an anderes Wallet stellen</string>
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="withdraw_manual_bitcoin_title">Bitcoin-Exchange zur Auszahlung bereit</string>
+ <string name="transactions_retry">Wiederholen</string>
+ <string name="send_peer_expiration_period">Läuft ab in</string>
+ <string name="transactions_suspend">Aussetzen</string>
+ <string name="transactions_abort_dialog_message">Sind Sie sicher, dass Sie diese Transaktion abbrechen möchten? Gelder, die gerade noch übertragen werden, könnten verloren gehen.</string>
</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-es/strings.xml b/wallet/src/main/res/values-es/strings.xml
index 1ccced5..bcc216d 100644
--- a/wallet/src/main/res/values-es/strings.xml
+++ b/wallet/src/main/res/values-es/strings.xml
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="google_play_title" tools:keep="@string/google_play_title">Cartera Taler (experimental)</string>
- <string name="nav_prompt_withdraw">Retirar Efectivo Digital</string>
+ <string name="nav_prompt_withdraw">Retirar efectivo digital</string>
<string name="nav_exchange_tos">Términos de servicio del cambiario</string>
<string name="nav_exchange_select">Seleccionar cambiario</string>
<string name="button_back">Regresar</string>
- <string name="enter_uri">Insertar URI de Taler</string>
+ <string name="enter_uri">Ingresa la URI de Taler</string>
<string name="paste">Pegar</string>
<string name="paste_invalid">El portapapeles contiene un tipo de dato inválido</string>
<string name="ok">OK</string>
@@ -15,29 +15,29 @@
<string name="or">o</string>
<string name="error_unsupported_uri">Error: esta URI de Taler no está soportada.</string>
<string name="menu_settings">Ajustes</string>
- <string name="balances_title">Saldo</string>
- <string name="amount_positive">+ %s</string>
- <string name="amount_chosen">Monto Elegido</string>
+ <string name="balances_title">Saldos</string>
+ <string name="amount_positive">+%s</string>
+ <string name="amount_chosen">Monto elegido</string>
<string name="balances_inbound_label">entrada</string>
<string name="transactions_empty">No tienes ninguna transacción</string>
<string name="transactions_detail_title">Transacción</string>
- <string name="transactions_delete">Borrar</string>
- <string name="transactions_select_all">Seleccionar Todo</string>
+ <string name="transactions_delete">Eliminar</string>
+ <string name="transactions_select_all">Seleccionar todo</string>
<string name="transaction_paid">Pagado</string>
<string name="transaction_order_total">Total</string>
<string name="transaction_order_id">Recibo #%1$s</string>
- <string name="transaction_tip_from">Propina de %s</string>
+ <string name="transaction_tip_from">Premio de %s</string>
<string name="transaction_refund">Devolución</string>
<string name="transaction_refund_from">Devolución de %s</string>
<string name="transaction_pending">PENDIENTE</string>
<string name="transaction_refresh">Comisión de cambio por expiración de moneda</string>
<string name="payment_title">Pago</string>
- <string name="payment_button_confirm">Confirmar Pago</string>
- <string name="payment_label_amount_total">Monto Total:</string>
+ <string name="payment_button_confirm">Confirmar pago</string>
+ <string name="payment_label_amount_total">Monto total:</string>
<string name="payment_label_order_summary">Compra</string>
<string name="payment_error">Error: %s</string>
<string name="payment_balance_insufficient">¡Saldo insuficiente!</string>
- <string name="payment_aborted">Cancelado</string>
+ <string name="payment_aborted">Abortado</string>
<string name="payment_failed">Fallido</string>
<string name="payment_already_paid_title">Ya fue pagado</string>
<string name="payment_already_paid">Ya has pagado por esta compra.</string>
@@ -46,8 +46,8 @@
<string name="withdraw_total">Retirar</string>
<string name="withdraw_fees">Comisión</string>
<string name="withdraw_exchange">Cambiario</string>
- <string name="withdraw_button_confirm">Confirmar Retiro</string>
- <string name="withdraw_button_tos">Revisar Términos</string>
+ <string name="withdraw_button_confirm">Confirmar retiro</string>
+ <string name="withdraw_button_tos">Revisar términos</string>
<string name="withdraw_waiting_confirm">Esperando confirmación</string>
<string name="withdraw_manual_title">Realizar transferencia manual al cambiario</string>
<string name="withdraw_amount">¿Cuánto deseas retirar\?</string>
@@ -59,51 +59,51 @@
<string name="exchange_list_title">Cambiarios</string>
<string name="exchange_list_empty">Sin cambiarios conocidos
\n
-\n¡Añade uno manualmente or retira efectivo digital!</string>
- <string name="exchange_list_currency">Moneda: %s</string>
+\nAñade uno manualmente or retira efectivo digital.</string>
+ <string name="exchange_list_currency">Divisa: %s</string>
<string name="exchange_list_add">Añadir cambiario</string>
<string name="exchange_add_url">Ingresa dirección del cambiario</string>
<string name="exchange_add_error">No se pudo añadir el cambiario</string>
- <string name="exchange_fee_withdrawal_fee_label">Comisión de Retiro:</string>
- <string name="exchange_fee_overhead_label">Pérdida de Redondeo:</string>
+ <string name="exchange_fee_withdrawal_fee_label">Comisión por retiro:</string>
+ <string name="exchange_fee_overhead_label">Pérdida por redondeo:</string>
<string name="exchange_fee_coin_expiration_label">Expiración de moneda más temprana:</string>
- <string name="exchange_fee_coin_fees_label">Comisiones de Moneda</string>
- <string name="exchange_fee_wire_fees_label">Comisiones de Transferencia</string>
- <string name="exchange_fee_withdraw_fee">Comisión de Retiro: %s</string>
- <string name="exchange_fee_deposit_fee">Comisión de Depósito: %s</string>
- <string name="exchange_fee_refresh_fee">Comisión de Cambio: %s</string>
- <string name="exchange_fee_refund_fee">Comisión de Devolución: %s</string>
- <string name="exchange_fee_wire_fee_wire_fee">Comisión de Transferencia: %s</string>
- <string name="exchange_fee_wire_fee_closing_fee">Comisión de Cierre: %s</string>
- <string name="exchange_tos_accept">Aceptar Términos de Servicio</string>
- <string name="exchange_tos_error">Error mostrando los Términos de Servicio: %s</string>
- <string name="pending_operations_title">Operaciones Pendientes</string>
- <string name="pending_operations_refuse">Rechazar Propuesta</string>
+ <string name="exchange_fee_coin_fees_label">Comisiones de moneda</string>
+ <string name="exchange_fee_wire_fees_label">Comisiones de transferencia</string>
+ <string name="exchange_fee_withdraw_fee">Comisión por retiro: %s</string>
+ <string name="exchange_fee_deposit_fee">Comisión por depósito: %s</string>
+ <string name="exchange_fee_refresh_fee">Comisión por cambio: %s</string>
+ <string name="exchange_fee_refund_fee">Comisión por devolución: %s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Comisión por transferencia: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Comisión por cierre: %s</string>
+ <string name="exchange_tos_accept">Aceptar términos de servicio</string>
+ <string name="exchange_tos_error">Error mostrando los términos de servicio: %s</string>
+ <string name="pending_operations_title">Operaciones pendientes</string>
+ <string name="pending_operations_refuse">Rechazar propuesta</string>
<string name="pending_operations_no_action">(sin acción)</string>
- <string name="settings_dev_mode">Modo Desarrollador</string>
+ <string name="settings_dev_mode">Modo desarrollador</string>
<string name="settings_withdraw_testkudos">Retirar TESTKUDOS</string>
- <string name="settings_withdraw_testkudos_summary">Obtener dinero para realizar pruebas</string>
- <string name="settings_logcat">Bitácora de Depuración</string>
+ <string name="settings_withdraw_testkudos_summary">Obtener dinero para pruebas</string>
+ <string name="settings_logcat">Bitácora de depuración</string>
<string name="settings_dev_mode_summary">Muestra más información de depuración</string>
<string name="settings_logcat_summary">Guardar bitácora interna</string>
- <string name="settings_version_app">Versión de la Aplicación</string>
+ <string name="settings_version_app">Versión de la aplicación</string>
<string name="settings_version_core">Versión de Wallet Core</string>
- <string name="settings_version_protocol_exchange">Versiones de Exchange Soportadas</string>
- <string name="settings_version_protocol_merchant">Versiones de Merchant Soportadas</string>
+ <string name="settings_version_protocol_exchange">Versiones de Exchange soportadas</string>
+ <string name="settings_version_protocol_merchant">Versiones de Merchant soportadas</string>
<string name="settings_version_unknown">Desconocido</string>
- <string name="settings_reset">Resetear Cartera (¡peligroso!)</string>
- <string name="settings_reset_summary">Tira tu dinero</string>
+ <string name="settings_reset">Reiniciar cartera (¡peligroso!)</string>
+ <string name="settings_reset_summary">Se deshace de tu dinero</string>
<string name="nav_settings_backup">Copia de seguridad</string>
<string name="backup_last">Última copia de seguridad: %s</string>
<string name="refund_title">Devolución</string>
<string name="refund_error">Error procesando la devolución</string>
<string name="refund_success">Devolución recibida: %s</string>
- <string name="tip_title">Propina</string>
- <string name="wifi_disabled_error">Conecta tu Wi-Fi para obtener Wi-Fi gratis</string>
+ <string name="tip_title">Premio</string>
+ <string name="wifi_disabled_error">Enciende tu Wi-Fi para obtener Wi-Fi gratis</string>
<string name="wifi_connect_error">No se pudo conectar al Wi-Fi gratis: %s</string>
<string name="app_name">Cartera Taler</string>
<string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler hace transacciones en línea amigables con la privacidad, de forma rápida y sencilla.</string>
- <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Esta aplicación es una cartera para GNU Taler. Es altamente experimental, y no está lista para producción aún. GNU Taler es un sistema de pago que preserva la privacidad. Los clientes pueden permanecer anónimos, pero los comerciantes no pueden esconder sus ingresos a través de GNU Taler. Eso ayuda a evitar la evasión de impuestos y el lavado de dinero. El uso principal de GNU Taler son los pagos; su intención no es servir como reserva de valor. Los pagos son siempre respaldados por una moneda existente. El pago es realizado después de cambiar dinero existente en dinero electrónico con la ayuda de un servicio de cambiario, que es un proveedor de servicios de pagos para Taler. Cuando se realiza un pago, los clientes solo necesitan una cartera con fondos. Un comerciante puede aceptar pagos sin hacer registrar a sus clientes en su página Web. GNU Taler es inmune contra varios tipos de fraude, como el phising de información de tarjetas de crédito o el fraude por devolución. En caso de pérdida o robo, solo una cantidad limitada de dinero guardado en la cartera podría perderse.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Esta aplicación es una cartera para GNU Taler. Es altamente experimental, y aún no está lista para producción. GNU Taler es un sistema de pago que preserva la privacidad. Los clientes pueden permanecer anónimos, pero los comerciantes no pueden esconder sus ingresos a través de GNU Taler. Esto ayuda a evitar la evasión de impuestos y el lavado de dinero. El uso principal de GNU Taler son los pagos; su intención no es servir como reserva de valor. Los pagos siempre están respaldados por una divisa existente. El pago se realiza después de cambiar dinero existente por dinero electrónico con la ayuda de un servicio de cambiario, que es un proveedor de servicios de pagos para Taler. Cuando se realiza un pago, los clientes solo necesitan una cartera con fondos. Un comerciante puede aceptar pagos sin hacer que sus clientes se registren en su página Web. GNU Taler es inmune contra varios tipos de fraude, como el phising de información de tarjetas de crédito o el fraude por devolución. En caso de pérdida o robo, solo una cantidad limitada de dinero guardado en la cartera podría perderse.</string>
<string name="nav_header_title">GNU Taler</string>
<string name="nav_header_subtitle">Cartera</string>
<string name="nav_exchange_fees">Comisiones del cambiario</string>
@@ -111,8 +111,8 @@
<string name="button_scan_qr_code">Escanear código QR de Taler</string>
<string name="uri_invalid">URI de Taler inválida</string>
<string name="offline">La operación requiere acceso a Internet. Por favor asegúrate que tu conexión a Internet funcione y prueba de nuevo.</string>
- <string name="menu_retry_pending_operations">Reintentar Operaciones Pendientes</string>
- <string name="amount_negative">- %s</string>
+ <string name="menu_retry_pending_operations">Reintentar operaciones pendientes</string>
+ <string name="amount_negative">-%s</string>
<string name="balances_empty_state">No hay efectivo digital en tu cartera.
\n
\nPuedes obtener dinero de prueba desde el banco demostrativo:
@@ -125,36 +125,37 @@
\n%s</string>
<string name="transactions_detail_title_balance">Balance: %s</string>
<string name="transaction_order">Compra</string>
- <string name="payment_fee">+%s comisión por pago</string>
+ <string name="payment_fee">+%s de comisión por pago</string>
<string name="payment_initiated">Pago iniciado</string>
<string name="withdraw_button_confirm_bank">Confirmar con el banco</string>
<string name="withdraw_amount_error">Inserta un monto válido</string>
<string name="withdraw_manual_payment_options">Opciones de pago soportadas por %1$s:
\n
\n%2$s</string>
- <string name="withdraw_error_message">El retiro ahora mismo no es posible. ¡Por favor prueba de nuevo más tarde!</string>
+ <string name="withdraw_error_message">El retiro no es posible ahora mismo. Por favor prueba de nuevo más tarde.</string>
<string name="exchange_menu_manual_withdraw">Retirar</string>
<plurals name="exchange_fee_coin">
<item quantity="one">Moneda: %s (usada %d vez)</item>
+ <item quantity="many">Moneda: %s (usada %d veces)</item>
<item quantity="other">Moneda: %s (usada %d veces)</item>
</plurals>
<string name="exchange_fee_wire_fee_timespan">Intervalo de tiempo: %1$s - %2$s</string>
<string name="host_apdu_service_desc">Pagos Taler con NFC</string>
- <string name="transactions_delete_dialog_title">Eliminar Transacción</string>
+ <string name="transactions_delete_dialog_title">Eliminar transacción</string>
<string name="transactions_delete_dialog_message">¿Estás seguro de que deseas eliminar esta transacción de tu cartera\?</string>
<string name="withdraw_manual_ready_cancel">Cancelar retiro</string>
- <string name="tip_already_accepted">Esta propina ya fue aceptada.</string>
+ <string name="tip_already_accepted">Este premio ya fue aceptado.</string>
<string name="transaction_deposit">Depósito</string>
<string name="transaction_peer_pull_debit">Cobro pagado</string>
<string name="transaction_peer_push_credit">Pago push</string>
- <string name="transaction_peer_pull_credit">Factura</string>
- <string name="receive_peer_invoice_instruction">Muestra al pagador este código QR para pagar:</string>
+ <string name="transaction_peer_pull_credit">Cobro</string>
+ <string name="receive_peer_invoice_instruction">Muestra este código QR a quien pagará:</string>
<string name="receive_peer_invoice_uri">O bien, copia y envía esta URI:</string>
- <string name="receive_peer_create_button">Crear factura</string>
+ <string name="receive_peer_create_button">Crear cobro</string>
<string name="pay_peer_title">Pagar factura</string>
<string name="send_peer_create_button">Enviar fondos ahora</string>
- <string name="send_peer_warning">Advertencia: los fondos saldrán de esta cartera inmediatamente.</string>
- <string name="send_peer_payment_instruction">Permite al pagador escanear este código QR para recibir:</string>
+ <string name="send_peer_warning">Los fondos saldrán de esta cartera inmediatamente.</string>
+ <string name="send_peer_payment_instruction">Muestra este código a quien deba recibir el dinero:</string>
<string name="tip_exchange">Cambiario</string>
<string name="tip_merchant_url">URL de comerciante</string>
<string name="withdraw_manual_ready_title">¡El cambiario está listo para el retiro!</string>
@@ -164,10 +165,10 @@
<string name="withdraw_manual_ready_bank_button">Abrir en aplicación de banco</string>
<string name="copy_uri">Copiar URI de Taler</string>
<string name="transactions_send_funds">Enviar
-\nFondos</string>
+\nfondos</string>
<string name="transactions_receive_funds">Recibir
-\nFondos</string>
- <string name="transactions_cancel_dialog_message">¿Estás seguro de que quieres cancelar este retiro\? Los fondos que están en tránsito podrían perderse.</string>
+\nfondos</string>
+ <string name="transactions_cancel_dialog_message">¿Estás seguro de que deseas cancelar este retiro\? Los fondos que están en tránsito podrían perderse.</string>
<string name="transactions_detail_title_currency">Transacciones de %s</string>
<string name="receive_peer_title">Solicitar pago</string>
<string name="receive_amount">Monto a recibir</string>
@@ -182,16 +183,16 @@
<string name="send_intro">Elige hacia dónde enviar el dinero:</string>
<string name="send_deposit">A una cuenta de banco</string>
<string name="send_deposit_title">Deposita a una cuenta de banco</string>
- <string name="send_deposit_iban"></string>
- <string name="send_deposit_bic"></string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
<string name="receive_peer_payment_title">Recibir pago</string>
- <string name="currency">Moneda</string>
+ <string name="currency">Divisa</string>
<string name="send_peer">A otra cartera</string>
<string name="send_peer_title">Enviar dinero a otra cartera</string>
- <string name="transaction_peer_push_debit">Pagos push</string>
+ <string name="transaction_peer_push_debit">Pago push</string>
<string name="send_peer_payment_amount_received">Monto recibido</string>
<string name="send_peer_payment_amount_sent">Monto enviado</string>
- <string name="pay_peer_intro">¿Quieres pagar esta factura\?</string>
+ <string name="pay_peer_intro">¿Quieres pagar este cobro\?</string>
<string name="receive_peer_payment_intro">¿Quieres recibir este pago\?</string>
<string name="withdraw_manual_bitcoin_intro">Ahora haz una transacción dividida con las siguientes tres salidas.</string>
<string name="withdraw_manual_ready_subject">Sujeto</string>
@@ -199,16 +200,74 @@
<string name="settings_logcat_success">Bitácora exportada al archivo</string>
<string name="tip_total">Total</string>
<string name="send_peer_purpose">Motivo</string>
- <string name="send_peer_bitcoin">A otra cartera Taler</string>
+ <string name="send_peer_bitcoin">A otra cartera de Taler</string>
<string name="withdraw_manual_ready_intro">Para completar el proceso, necesitas transferir %s a la cuenta bancaria del cambiario</string>
<string name="withdraw_manual_bitcoin_title">Cambiario de Bitcoin listo para el retiro</string>
<string name="settings_db_export_summary">Guardar base de datos interna</string>
<string name="settings_db_export_success">Base de datos exportada al archivo</string>
- <string name="tip_button_confirm">Aceptar propina</string>
- <string name="tip_received">Propina recibida</string>
+ <string name="tip_button_confirm">Aceptar premio</string>
+ <string name="tip_received">Premio recibido</string>
<string name="tip_fees">Comisión</string>
- <string name="send_deposit_bitcoin">A una cartera Bitcoin</string>
- <string name="settings_db_export">Base de datos</string>
+ <string name="send_deposit_bitcoin">A una cartera de Bitcoin</string>
+ <string name="settings_db_export">Exportar base de datos</string>
<string name="settings_db_export_error">Error exportando base de datos</string>
<string name="withdraw_manual_ready_warning">Asegúrate de usar el sujeto correcto; de lo contrario, el dinero no llegará a esta cartera.</string>
+ <string name="transaction_action_kyc">Completar KYC</string>
+ <string name="withdraw_restrict_age">Restringir uso a edad</string>
+ <string name="withdraw_restrict_age_unrestricted">Sin restricciones</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="send_deposit_check_fees_button">Revisar comisiones</string>
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="transactions_delete_selected_dialog_message">¿Estás seguro de que deseas eliminar las transacciones seleccionadas de tu cartera\?</string>
+ <string name="send_deposit_bitcoin_address">Dirección Bitcoin</string>
+ <string name="send_deposit_bitcoin_create_button">Transferir Bitcoin</string>
+ <string name="transaction_dummy_title">Transacción desconocida</string>
+ <string name="send_deposit_bic_error">BIC es inválido</string>
+ <string name="send_peer_expiration_period">Expira en</string>
+ <string name="send_peer_expiration_1d">1 día</string>
+ <string name="send_peer_expiration_7d">7 días</string>
+ <string name="settings_test_summary">Realizar transacciones de prueba con el banco demostrativo</string>
+ <string name="settings_test">Ejecutar pruebas de integración</string>
+ <string name="button_scan_qr_code_label">Escanear código QR</string>
+ <string name="send_deposit_iban_error">IBAN es inválido</string>
+ <string name="send_peer_expiration_30d">30 días</string>
+ <string name="send_peer_expiration_custom">Personalizado</string>
+ <string name="send_peer_expiration_days">Días</string>
+ <string name="send_peer_expiration_hours">Horas</string>
+ <string name="exchange_list_error">No se pudo listar los cambiarios</string>
+ <string name="payment_prompt_title">Revisar pago</string>
+ <string name="payment_pay_template_title">Personaliza tu orden</string>
+ <string name="payment_create_order">Crear orden</string>
+ <string name="payment_confirmation_code">Código de confirmación</string>
+ <string name="payment_template_error">Error creando orden</string>
+ <string name="transactions_retry">Reintentar</string>
+ <string name="transactions_abort">Abortar</string>
+ <string name="transactions_fail">Matar</string>
+ <string name="transactions_suspend">Suspender</string>
+ <string name="transactions_resume">Resumir</string>
+ <string name="transactions_abort_dialog_title">Abortar transacción</string>
+ <string name="transactions_fail_dialog_message">¿Estás seguro de que deseas matar esta transacción\? Los fondos en tránsito SE PERDERÁN.</string>
+ <string name="exchange_not_contacted">Cambiario aún no contactado</string>
+ <string name="transactions_fail_dialog_title">Matar transacción</string>
+ <string name="transactions_abort_dialog_message">¿Estás seguro de que deseas abortar esta transacción\? Los fondos en tránsito podrían perderse.</string>
+ <string name="offline_banner">Sin acceso a internet</string>
+ <string name="error_broken_uri">Error: Este URI taler no funciona actualmente.</string>
+ <string name="withdraw_subtitle">Seleccione un cuenta bancaria destino</string>
+ <string name="withdraw_account">Cuenta #%1$d</string>
+ <string name="withdraw_account_currency">Cuenta #%1$d (%2$s)</string>
+ <string name="withdraw_transfer">Transferencia</string>
+ <string name="withdraw_conversion">Conversión</string>
+ <string name="withdraw_conversion_support">Este exchange soporta conversión de moneda</string>
+ <string name="exchange_list_add_dev">Agregar exchange de desarrollo</string>
+ <string name="settings_db_import">Importar base de datos</string>
+ <string name="settings_db_import_summary">Restaurar base de datos desde archivo</string>
+ <string name="settings_db_import_error">Error al importar la base de datos</string>
+ <string name="settings_db_import_success">Base de datos importada desde archivo</string>
+ <string name="settings_dialog_reset_message">Realmente quieres reiniciar la biiletera y perder todas las monedas y compras?</string>
+ <string name="settings_dialog_import_message">Esta operación sobreescribirá toda tu base de datos. Realmente quieres continuar?</string>
+ <string name="settings_alert_reset_done">La billetera ha sido reiniciada</string>
+ <string name="settings_alert_reset_canceled">Reinicio cancelado</string>
+ <string name="settings_alert_import_canceled">Importación cancelada</string>
+ <string name="import_db">Importar</string>
+ <string name="reset">Reiniciar</string>
</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-fi/strings.xml b/wallet/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000..7e88596
--- /dev/null
+++ b/wallet/src/main/res/values-fi/strings.xml
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2020 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ --><resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="app_name">Taler lompakko</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">Taler lompakko (kokeilu)</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler mahdollistaa yksityisyyttä kunnioittavat verkkomaksut nopeasti ja helposti.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Tämä sovellus on lompakko GNU Talerille. Se on erittäin kokeellinen eikä vielä valmis tuotantokäyttöön. GNU Taler on yksityisyyttä kunnioittava maksujärjestelmä. Asiakkaat voivat pysyä anonyymeinä, mutta kauppiaat eivät voi piilottaa tulojaan maksujen avulla GNU Talerilla. Tämä auttaa välttämään veronkiertoa ja rahanpesua. GNU Talerin ensisijainen käyttötarkoitus on maksut; sitä ei tarkoiteta arvon säilyttämiseen. Maksut ovat aina tuettuja olemassa olevalla valuutalla. Maksut suoritetaan vaihtamalla olemassa oleva raha sähköiseksi rahaksi Exchange-palvelun avulla, eli Talerin maksupalveluntarjoajan avulla. Maksua tehdessään asiakkaat tarvitsevat vain ladatun lompakon. Kauppias voi hyväksyä maksut ilman, että asiakkaiden tarvitsee rekisteröityä kauppiaan verkkosivustolle. GNU Taler on immuuni monille huijauksen muodoille, kuten luottokorttitietojen kalastelu tai palautuspetos. Jos lompakko katoaa tai varastetaan, vain lompakossa oleva rajoitettu rahasumma voi kadota.</string>
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="nav_header_subtitle">Lompakko</string>
+ <string name="nav_prompt_withdraw">Nosta sähköistä käteistä</string>
+ <string name="nav_exchange_tos">Vaidon käyttöehdot</string>
+ <string name="nav_exchange_select">Valitse vaihto</string>
+ <string name="nav_exchange_fees">Vaihdon maksut</string>
+ <string name="nav_error">Virhe</string>
+ <string name="button_back">Takaisin</string>
+ <string name="button_scan_qr_code">Skannaa Taler QR koodi</string>
+ <string name="button_scan_qr_code_label">Saknnaa QR koodi</string>
+ <string name="enter_uri">Syötä Taler URI</string>
+ <string name="copy" tools:override="true">Kopioi</string>
+ <string name="copy_uri">Kopioi Taler URI</string>
+ <string name="paste">Liitä</string>
+ <string name="paste_invalid">Leikepöytä sisältää virheellisen tietotyypin</string>
+ <string name="uri_invalid">Taler URI ei ole kelvollinen</string>
+ <string name="ok">OK</string>
+ <string name="cancel">Hylkää</string>
+ <string name="search">Haku</string>
+ <string name="menu">Valikko</string>
+ <string name="or">tai</string>
+ <string name="currency">Valuutta</string>
+ <!-- The count should be mirrored in RTL languages -->
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="import_db">Tuonti</string>
+ <string name="reset">Nollaa</string>
+ <string name="share_payment">Jaa maksu linkki</string>
+ <string name="offline">Toiminta vaatii Internetyhteyden. Varmista, että internetyhteytesi toimii ja yritä uudelleen.</string>
+ <string name="offline_banner">Ei yhteyttä</string>
+ <string name="error_unsupported_uri">Virhe: Tätä Taler URI:a ei tueta.</string>
+ <string name="error_broken_uri">Virhe: Tämä Taler URI ei (tällä hetkellä) toimi.</string>
+ <string name="menu_settings">Asetukset</string>
+ <string name="menu_retry_pending_operations">Yritä uudelleen odottavia toimintoja</string>
+ <string name="host_apdu_service_desc">Taler NFC maksut</string>
+ <string name="balances_title">Saldot</string>
+ <string name="amount_positive">+%s</string>
+ <string name="amount_negative">-%s</string>
+ <string name="amount_chosen">Valittu summa</string>
+ <string name="amount_sent">Summa lähetetty</string>
+ <string name="amount_received">Summa saatu</string>
+ <string name="balances_inbound_label">saapuva</string>
+ <string name="balances_empty_state">Lompakossasi ei ole sähköistä rahaa.
+\n
+\nVoit saada testirahaa demopankista:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="balance_scope_exchange">Vaihda:%1$s</string>
+ <string name="balance_scope_auditor">Tarkastaja: %1$s</string>
+ <string name="transactions_title">Tapahtumat</string>
+ <string name="transactions_balance">Saldo</string>
+ <string name="transactions_send_funds">Lähetä
+\nVarat</string>
+ <string name="transactions_receive_funds">Vastaanota
+\nVarat</string>
+ <string name="transactions_empty">Sinulla ei ole tapahtumia</string>
+ <string name="transactions_empty_search">Tapahtumia ei löytynyt. Yritä toista hakua.</string>
+ <string name="transactions_error">Tapahtumaa ei voinut ladata
+\n
+\n%s</string>
+ <string name="transactions_detail_title">Tapahtuma</string>
+ <string name="transactions_detail_title_currency">%s Tapahtumat</string>
+ <string name="transactions_delete">Poista</string>
+ <string name="transactions_retry">Yritä uudelleen</string>
+ <string name="transactions_abort">Peruuta</string>
+ <string name="transactions_fail">Lopeta</string>
+ <string name="transactions_suspend">Keskeytä</string>
+ <string name="transactions_resume">Jatka</string>
+ <string name="transactions_select_all">Valitse kaikki</string>
+ <string name="transactions_delete_dialog_title">Poista tapahtuma</string>
+ <string name="transactions_delete_dialog_message">Haluatko varmasti poistaa tämän tapahtuman lompakostasi?</string>
+ <string name="transactions_delete_selected_dialog_message">Haluatko varmasti poistaa valitut tapahtumat lompakostasi?</string>
+ <string name="transactions_abort_dialog_title">Peruuta tapahtuma</string>
+ <string name="transactions_abort_dialog_message">Haluatko varmasti keskeyttää tämän tapahtuman? Siirrettävät varat saattavat kadota.</string>
+ <string name="transactions_fail_dialog_title">Lopeta tapahtuma</string>
+ <string name="transactions_fail_dialog_message">Haluatko varmasti lopettaa tämän tapahtuman? Vielä siirrettävät varat MENETÄÄN.</string>
+ <string name="transactions_cancel_dialog_message">Haluatko varmasti peruuttaa tämän noston? Siirrettävät varat saattavat kadota.</string>
+ <!-- Transactions -->
+ <string name="transaction_paid">Maksettu</string>
+ <string name="transaction_order_total">Yhteensä</string>
+ <string name="transaction_order">Osto</string>
+ <string name="transaction_order_id">Kuitti #%1$s</string>
+ <string name="transaction_refund">Maksun palautus</string>
+ <string name="transaction_refund_from">Maksun palautus %s</string>
+ <string name="transaction_pending">ODOTTAA</string>
+ <string name="transaction_refresh">Kolikon voimassaolon vaihtomaksu</string>
+ <string name="transaction_deposit">Talletus</string>
+ <string name="transaction_peer_push_debit">Siirrä maksua</string>
+ <string name="transaction_peer_pull_credit">Lasku</string>
+ <string name="transaction_peer_pull_debit">Lasku maksettu</string>
+ <string name="transaction_peer_push_credit">Siirrä maksua</string>
+ <string name="transaction_action_kyc">Täydellinen KYC</string>
+ <string name="transaction_dummy_title">Tuntematon tapahtuma</string>
+ <string name="payment_title">Maksu</string>
+ <string name="payment_prompt_title">Näytä maksu</string>
+ <string name="payment_fee">+%s maksun käsittelymaksu</string>
+ <string name="payment_button_confirm">Vahvista maksu</string>
+ <string name="payment_label_amount_total">Kokonais summa:</string>
+ <string name="payment_label_order_summary">Osta</string>
+ <string name="payment_error">Virhe: %s</string>
+ <string name="payment_pending">Maksua ei suoritettu, sitä yritetään uudelleen</string>
+ <string name="payment_balance_insufficient">Saldo ei riitä!</string>
+ <string name="payment_aborted">Peruutettu</string>
+ <string name="payment_failed">Epäonnistui</string>
+ <string name="payment_initiated">Maksu aloitettu</string>
+ <string name="payment_already_paid_title">Jo maskettu</string>
+ <string name="payment_already_paid">Olet jo maksanut tämän ostoksen.</string>
+ <string name="payment_pay_template_title">Muokkaa tilausta</string>
+ <string name="payment_create_order">Luo tilaus</string>
+ <string name="payment_confirmation_code">Vahvistuskoodi</string>
+ <string name="payment_template_error">Virhe tilausken tekemisessä</string>
+ <string name="receive_amount">Vastaanotettava summa</string>
+ <string name="receive_amount_invalid">Virrheellinen summa</string>
+ <string name="receive_intro">Valitse mistä saat rahaa:</string>
+ <string name="receive_withdraw">Nosto pankkitililtä</string>
+ <string name="receive_peer">Laskuta toista lompakkoa</string>
+ <string name="receive_peer_title">Pyydä maksu</string>
+ <string name="receive_peer_create_button">Luo lasku</string>
+ <string name="receive_peer_invoice_instruction">Anna maksajan skannata tämä QR koodi maksaakseen:</string>
+ <string name="receive_peer_invoice_uri">Vaihtoehtoisesti kopioi ja lähetä tämä URI:</string>
+ <string name="receive_peer_amount_invoiced">Summa laskutettu</string>
+ <string name="send_amount">Lähetettävä summa</string>
+ <string name="send_intro">Valitse mihin lähetät rahaa:</string>
+ <string name="send_deposit">Pankkitilille</string>
+ <string name="send_deposit_bitcoin">Bitcoin lompakkoon</string>
+ <string name="send_deposit_title">Talletus pankkitilille</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_iban_error">IBAN on virheellinen</string>
+ <string name="send_deposit_bic_error">BIC on virheellinen</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="send_deposit_name">Tilin haltija</string>
+ <string name="send_deposit_bitcoin_address">Bitcoin osoite</string>
+ <string name="send_deposit_check_fees_button">Tarkista maksut</string>
+ <string name="send_deposit_amount_effective">Todellinen summa</string>
+ <string name="send_deposit_create_button">Tee talletus</string>
+ <string name="send_deposit_bitcoin_create_button">Siirrä Bitcoineja</string>
+ <string name="send_peer">Toiseen lompakkoon</string>
+ <string name="send_peer_bitcoin">Toiseen Taler lompakkoon</string>
+ <string name="send_peer_title">Lähetä rahaa toiseen lompakkoon</string>
+ <string name="send_peer_create_button">Lähetä varat nyt</string>
+ <string name="send_peer_payment_instruction">Anna maksunsaajan skannata tämä QR-koodi saadaksesi:</string>
+ <string name="send_peer_expiration_period">Vanhenee</string>
+ <string name="send_peer_expiration_1d">1 päivässä</string>
+ <string name="send_peer_expiration_7d">7 päivässä</string>
+ <string name="send_peer_expiration_30d">30 päivässä</string>
+ <string name="send_peer_expiration_custom">Mukautettu</string>
+ <string name="send_peer_expiration_days">Päivässä</string>
+ <string name="send_peer_expiration_hours">tunnissa</string>
+ <string name="send_peer_purpose">Tarkoitus</string>
+ <string name="pay_peer_title">Maksa lasku</string>
+ <string name="pay_peer_intro">Haluatko maksaa tämän laskun?</string>
+ <string name="receive_peer_payment_title">Vastaanota maksu</string>
+ <string name="receive_peer_payment_intro">Haluatko vastaanottaa tämän maksun?</string>
+ <string name="withdraw_initiated">Nosto aloitettu</string>
+ <string name="withdraw_title">Nosto</string>
+ <string name="withdraw_subtitle">Valiste kohdepankkitili</string>
+ <string name="withdraw_total">Nosta</string>
+ <string name="withdraw_fees">Maksu</string>
+ <string name="withdraw_restrict_age">Ikärajoitus</string>
+ <string name="withdraw_restrict_age_unrestricted">Rajoittamaton</string>
+ <string name="withdraw_exchange">Vaihto</string>
+ <string name="withdraw_bank">Pankki</string>
+ <string name="withdraw_button_confirm">Vahvista nosto</string>
+ <string name="withdraw_button_confirm_bank">Vahvista pankilla</string>
+ <string name="withdraw_button_tos">Tarkista ehdot</string>
+ <string name="withdraw_waiting_confirm">Odottaa vahvistusta</string>
+ <string name="withdraw_manual_title">Tee manuaalinen siirto vaihtoon</string>
+ <string name="withdraw_amount">Kuinka paljon nostetaan?</string>
+ <string name="withdraw_amount_error">Syötä kelvollinen summa</string>
+ <string name="withdraw_manual_payment_options">%1$s:n tukemat maksuvaihtoehdot:
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">Tarkista maksut</string>
+ <string name="withdraw_manual_ready_title">Vaihto on valmis nostettavaksi!</string>
+ <string name="withdraw_manual_ready_intro">Suorittaaksesi prosessin sinun on siirrettävä %s vaihtopankkitilille</string>
+ <string name="withdraw_manual_ready_details_intro">Pankkisiirtotiedot</string>
+ <string name="withdraw_manual_bitcoin_title">Bitcoin vaihto valmis nostettavaksi</string>
+ <string name="withdraw_manual_bitcoin_intro">Tee nyt jaettu tapahtuma seuraavalla kolmella ulostulolla.</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_account">Tili</string>
+ <string name="withdraw_manual_ready_receiver">Vastaanotajan nimi</string>
+ <string name="withdraw_manual_ready_subject">Aihe</string>
+ <string name="withdraw_manual_ready_bank_button">Avaa pankkisovelluksessa</string>
+ <string name="withdraw_manual_ready_cancel">Peruuta nosto</string>
+ <string name="withdraw_manual_ready_warning">Varmista, että käytät oikeaa aihetta, muuten rahat eivät tule tähän lompakkoon.</string>
+ <string name="withdraw_error_title">Nosto virhe</string>
+ <string name="withdraw_error_message">Nosto ei ole tällä hetkellä mahdollista. Yritä uudelleen myöhemmin!</string>
+ <string name="withdraw_error_test">Virhe nostaessa TESTKUDOS</string>
+ <string name="withdraw_account">Tili #%1$d</string>
+ <string name="withdraw_account_currency">Tili #%1$d (%2$s)</string>
+ <string name="withdraw_transfer">Vaihda</string>
+ <string name="withdraw_conversion">Muuntaminen</string>
+ <string name="withdraw_conversion_support">Tämä vaihto tukee valuutan muuntamista</string>
+ <string name="exchange_settings_title">Vaihdot</string>
+ <string name="exchange_settings_summary">Hallinnoi tämän lompakon tuntemien vaihtojen luetteloa</string>
+ <string name="exchange_list_title">Vaihdot</string>
+ <string name="exchange_list_empty">Vaihtoja ei tiedossa
+\n
+\nLisää yksi manuaalisesti tai nosta digitaalista käteistä!</string>
+ <string name="exchange_list_currency">Valuutta: %s</string>
+ <string name="exchange_list_add">Lisää vaihto</string>
+ <string name="exchange_list_select">Valitse vaihto</string>
+ <string name="exchange_delete">Poista vaihto</string>
+ <string name="exchange_delete_force">Pakota poisto (puhdistus)</string>
+ <string name="exchange_dialog_delete_message">Haluatko varmasti poistaa tämän vaihdon? Tämän toimenpiteen pakottaminen johtaa varojen menetykseen.</string>
+ <string name="exchange_not_contacted">Vaihtoon ei otettu yhteyttä</string>
+ <string name="exchange_add_url">Anna vaihton osoite</string>
+ <string name="exchange_add_error">Vaihtoa ei voitu lisätä</string>
+ <string name="exchange_list_error">Vaihtoja ei voitu luetteloida</string>
+ <string name="exchange_list_add_dev">Lisää kehitysvaihtoja</string>
+ <string name="exchange_menu_manual_withdraw">Nosta</string>
+ <string name="exchange_fee_withdrawal_fee_label">Noston maksu:</string>
+ <string name="exchange_fee_overhead_label">Pyöristystappio:</string>
+ <string name="exchange_fee_coin_expiration_label">Aikaisin kolikon vanheneminen:</string>
+ <string name="exchange_fee_coin_fees_label">Kolikon maksut</string>
+ <string name="exchange_fee_wire_fees_label">Sirtomaksut</string>
+ <plurals name="exchange_fee_coin">
+ <item quantity="one">Kolikko: %s (Käytetty %d kerran)</item>
+ <item quantity="other">Kolikko: %s (Käytetty %d kertaa)</item>
+ </plurals>
+ <string name="exchange_fee_withdraw_fee">Nostomaksu: %s</string>
+ <string name="exchange_fee_deposit_fee">Talletus maksu: %s</string>
+ <string name="exchange_fee_refresh_fee">Vahto maksu: %s</string>
+ <string name="exchange_fee_refund_fee">Hyvitysmaksu: %s</string>
+ <string name="exchange_fee_wire_fee_timespan">Ajanjakso: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Siirtomaksu: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Sulkemismaksu: %s</string>
+ <string name="exchange_tos_accept">Hyväksy käyttöehdot</string>
+ <string name="exchange_tos_error">Virhe näytettäessä käyttöehtoja: %s</string>
+ <string name="pending_operations_title">Odottavat toiminnot</string>
+ <string name="pending_operations_refuse">Hylkää ehdotus</string>
+ <string name="pending_operations_no_action">(ei toimintaa)</string>
+ <string name="settings_dev_mode">Kehittäjätila</string>
+ <string name="settings_dev_mode_summary">Näyttää enemmän virheenkorjaukseen tarkoitettuja tietoja</string>
+ <string name="settings_withdraw_testkudos">Nosta TESTKUDOS</string>
+ <string name="settings_withdraw_testkudos_summary">Hanki rahaa testauksesta</string>
+ <string name="settings_logcat">Virheenkorjausloki</string>
+ <string name="settings_logcat_summary">Tallenna sisäinen loki</string>
+ <string name="settings_logcat_error">Virhe lokin viemisessä</string>
+ <string name="settings_logcat_success">Loki viety tiedostoon</string>
+ <string name="settings_db_export">Vie tietokanta</string>
+ <string name="settings_db_export_summary">Tallenna sisäinen tietokanta</string>
+ <string name="settings_db_import">Tuo tietokanta</string>
+ <string name="settings_db_import_summary">Palauta tietokanta tiedostosta</string>
+ <string name="settings_db_export_error">Virhe vietäessä tietokantaa</string>
+ <string name="settings_db_import_error">Virhe tietokannan tuonnissa</string>
+ <string name="settings_db_clear_error">Virhe tietokannan puhdistuksessa</string>
+ <string name="settings_db_export_success">Tietokanta viety tiedostoon</string>
+ <string name="settings_db_import_success">Tietokanta tuotu tiedostosta</string>
+ <string name="settings_version_app">Sovelluksen versio</string>
+ <string name="settings_version_core">Lompakon ydinversio</string>
+ <string name="settings_version_protocol_exchange">Tuetut Exchange-versiot</string>
+ <string name="settings_version_protocol_merchant">Tuetut kauppiasversiot</string>
+ <string name="settings_version_unknown">Tuntematon</string>
+ <string name="settings_test">Suorita integrointitesti</string>
+ <string name="settings_test_summary">Suorittaa testitapahtumia demoasetuksella</string>
+ <string name="settings_reset">Nollaa lompakko (vaarallista!)</string>
+ <string name="settings_reset_summary">Heittää rahasi pois</string>
+ <string name="settings_dialog_reset_message">Haluatko todella nollata lompakon ja menettää kaikki kolikot ja ostokset?</string>
+ <string name="settings_dialog_import_message">Tämä toiminto korvaa olemassa olevan tietokantasi. Haluatko jatkaa?</string>
+ <string name="settings_alert_reset_done">Lompakko on nollattu</string>
+ <string name="settings_alert_reset_canceled">Nollaus peruutettu</string>
+ <string name="settings_alert_import_canceled">Tuonti peruutettu</string>
+ <string name="refund_title">Palautus</string>
+ <string name="refund_error">Palautuksen käsittelyssä tapahtui virhe</string>
+ <string name="refund_success">Palautus saatu!</string>
+ <string name="wifi_disabled_error">Ota Wi-Fi käyttöön saadaksesi ilmaisen Wi-Fi-yhteyden</string>
+ <string name="wifi_connect_error">Ei voitu muodostaa yhteyttä ilmaiseen Wi-Fi-verkkoon: %s</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-fr/strings.xml b/wallet/src/main/res/values-fr/strings.xml
index 7d260a6..e4ea6c8 100644
--- a/wallet/src/main/res/values-fr/strings.xml
+++ b/wallet/src/main/res/values-fr/strings.xml
@@ -1,50 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="menu_retry_pending_operations">Réessayer les opérations en attente</string>
- <string name="balances_empty_state">Il n\'y a pas d\'argent éléctronique dans votre portefeuille.
+ <string name="balances_empty_state">Il n\'y a pas d\'argent numérique dans votre portefeuille.
\n
\nVous pouvez obtenir de l\'argent de test auprès de la banque de démonstration :
\n
\nhttps://bank.demo.taler.net</string>
- <string name="withdraw_error_message">Pour l\'instant il n\'est pas possible de retirer. Veuillez réessayer plus tard !</string>
+ <string name="withdraw_error_message">Le retrait n\'est actuellement pas possible. Veuillez réessayer plus tard !</string>
<string name="settings_dev_mode">Mode développeur</string>
- <string name="menu_settings">Réglages</string>
+ <string name="menu_settings">Paramètres</string>
<string name="settings_reset">Réinitialiser le portefeuille (dangereux!)</string>
<string name="app_name">Portefeuille Taler</string>
<string name="google_play_title" tools:keep="@string/google_play_title">Portefeuille Taler (expérimental)</string>
- <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler facilite et accélère les transactions en ligne respectueuses de la vie privée.</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler rend les transactions en ligne respectueuses de la vie privée, rapides et faciles.</string>
<string name="button_scan_qr_code">Scanner le QR code Taler</string>
<string name="balances_title">Soldes</string>
<string name="amount_positive">+ %s</string>
<string name="amount_chosen">Montant choisi</string>
<string name="balances_inbound_label">entrant</string>
- <string name="transactions_title">Opérations de transaction</string>
+ <string name="transactions_title">Transactions</string>
<string name="transactions_empty">Vous n\'avez aucune transaction</string>
<string name="transactions_error">Impossible de charger les transactions\n\n%s</string>
- <string name="transactions_detail_title">Opération de transaction</string>
+ <string name="transactions_detail_title">Transaction</string>
<string name="transactions_detail_title_balance">Solde : %s</string>
<string name="transactions_delete">Supprimer</string>
<string name="transactions_select_all">Tout sélectionner</string>
<string name="transaction_paid">Payé</string>
<string name="transaction_order_total">Total</string>
- <string name="transaction_order">Acheter</string>
- <string name="transaction_order_id">Reçu numéro %1$s</string>
+ <string name="transaction_order">Achat</string>
+ <string name="transaction_order_id">Reçu #%1$s</string>
<string name="transaction_refund">Remboursement</string>
- <string name="transaction_refresh">Frais d\'expiration de taux de change</string>
+ <string name="transaction_refresh">Frais de change pour la monnaie expirée</string>
<string name="payment_fee">+%s frais de transaction</string>
<string name="payment_button_confirm">Confirmer le paiement</string>
<string name="nav_exchange_fees">Frais de change</string>
<string name="amount_negative">- %s</string>
<string name="nav_header_title">GNU Taler</string>
<string name="nav_header_subtitle">Portefeuille</string>
- <string name="nav_prompt_withdraw">Retirer de l\'argent électronique</string>
- <string name="nav_exchange_tos">Conditions générale d\'utilisation du change</string>
+ <string name="nav_prompt_withdraw">Retirer de l\'argent numérique</string>
+ <string name="nav_exchange_tos">Conditions générales d\'utilisation du service d\'échange</string>
<string name="nav_error">Erreur</string>
- <string name="button_back">Retour</string>
+ <string name="button_back">Retour en arrière</string>
<string name="payment_label_order_summary">Achat</string>
<string name="payment_balance_insufficient">Solde insuffisant !</string>
<string name="payment_already_paid_title">Déjà payé</string>
- <string name="payment_already_paid">Vous avez déjà payé cet achat.</string>
+ <string name="payment_already_paid">Vous avez déjà payé pour cet achat.</string>
<string name="withdraw_total">Retirer</string>
<string name="withdraw_fees">Frais</string>
<string name="withdraw_exchange">Change</string>
@@ -62,7 +62,7 @@
<string name="exchange_fee_wire_fee_timespan">Durée : %1$s - %2$s</string>
<string name="exchange_fee_wire_fee_wire_fee">Frais de virement : %s</string>
<string name="exchange_fee_wire_fee_closing_fee">Frais de clôture : %s</string>
- <string name="exchange_tos_accept">Accepter les conditions générales d\'utilisations</string>
+ <string name="exchange_tos_accept">Accepter les conditions générales d\'utilisation</string>
<string name="exchange_tos_error">Erreur lors de l\'affichage des conditions générales d\'utilisation : %s</string>
<string name="pending_operations_title">Opérations en attente</string>
<string name="pending_operations_refuse">Refuser la proposition</string>
@@ -74,24 +74,24 @@
<string name="refund_error">Erreur lors du traitement du remboursement</string>
<string name="refund_success">Remboursement reçu : %s</string>
<string name="payment_error">Erreur : %s</string>
- <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Cette application est un portefeuille pour GNU Taler. Elle est très expérimentale et n\'est pas encore prête à être utilisée en production. GNU Taler est un système de paiement préservant la vie privée. Les clients peuvent rester anonymes, mais les commerçants ne peuvent pas cacher leurs revenus avec GNU Taler. Cela permet d\'éviter l\'évasion fiscale et le blanchiment d\'argent. Le principal cas d\'utilisation de GNU Taler est celui des paiements ; il ne s\'agit pas d\'une réserve de valeur. Les paiements sont toujours garantis par une devise existante. Les paiements sont effectués après avoir échangé l\'argent existant en monnaie électronique avec l\'aide d\'un service d\'échange, c\'est-à-dire un fournisseur de services de paiement pour Taler. Pour effectuer un paiement, les clients n\'ont besoin que d\'un portefeuille chargé. Un commerçant peut accepter des paiements sans que ses clients ne s\'inscrivent sur son site web. GNU Taler est immunisé contre de nombreux types de fraude, tels que le hameçonnage des informations relatives aux cartes de crédit ou la fraude par rétro-facturation. En cas de perte ou de vol, seule la quantité limitée d\'argent restant dans le portefeuille peut disparaître.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Cette application est un portefeuille pour GNU Taler. Cette dernière est expérimentale et n\'est pas encore prête à être utilisée en production. GNU Taler est un système de paiement préservant la vie privée. Les clients peuvent rester anonymes, mais les commerçants ne peuvent pas cacher leurs revenus avec GNU Taler. Cela permet d\'éviter l\'évasion fiscale et le blanchiment d\'argent. Le principal cas d\'utilisation de GNU Taler est celui des paiements ; il ne s\'agit pas d\'une réserve de valeur. Les paiements sont toujours garantis par une devise existante. Les paiements sont effectués après avoir échangé l\'argent existant en monnaie électronique avec l\'aide d\'un service d\'échange, qui est un prestataire de services de paiement pour Taler. Pour effectuer un paiement, les clients n\'ont besoin que d\'un portefeuille chargé. Un commerçant peut accepter des paiements sans demander à ses clients de s\'inscrire sur son site web. GNU Taler est immunisé contre de nombreux types de fraude, tels que l\'hameçonnage de cartes de crédit ou la fraude par rétrofacturation. En cas de perte ou de vol, seul le montant limité d\'argent restant dans le portefeuille peut être perdu.</string>
<string name="payment_label_amount_total">Montant total :</string>
<string name="exchange_fee_coin_expiration_label">Expiration de monnaie prochaine :</string>
<string name="ok">OK</string>
<string name="cancel">Annuler</string>
- <string name="offline">L\'opération nécessite un accès Internet. Veuillez vérifier que votre connection internet fonctionne et réessayez.</string>
+ <string name="offline">L\'opération nécessite un accès Internet. Veuillez vous assurer que votre connexion internet fonctionne et réessayez.</string>
<string name="error_unsupported_uri">Erreur : Cet URI Taler n\'est pas pris en charge.</string>
<string name="transactions_empty_search">Aucune transaction trouvée. Essayez une recherche différente.</string>
<string name="transaction_refund_from">Remboursement %s</string>
<string name="transaction_pending">EN ATTENTE</string>
<string name="payment_title">Paiement</string>
- <string name="payment_aborted">Abandonné</string>
+ <string name="payment_aborted">Annulé</string>
<string name="payment_initiated">Paiement initié</string>
- <string name="nav_exchange_select">Sélectionnez Change</string>
- <string name="enter_uri">Entrez l\'URI de Taler</string>
+ <string name="nav_exchange_select">Sélectionner le change</string>
+ <string name="enter_uri">Entrer l\'URI de Taler</string>
<string name="paste">Coller</string>
<string name="paste_invalid">Le presse-papiers contient un type de données non valide</string>
- <string name="uri_invalid">Pas un URI Taler valide</string>
+ <string name="uri_invalid">URI Taler non valide</string>
<string name="search">Rechercher</string>
<string name="menu">Menu</string>
<string name="or">ou</string>
@@ -107,7 +107,7 @@
\n
\n%2$s</string>
<string name="withdraw_manual_check_fees">Vérifier les frais</string>
- <string name="host_apdu_service_desc">Paiements Taler CCP</string>
+ <string name="host_apdu_service_desc">Paiements Taler NFC</string>
<string name="withdraw_manual_title">Effectuer un transfert manuel vers la bourse</string>
<string name="exchange_settings_title">Échanges</string>
<string name="exchange_settings_summary">Gérer la liste des échanges connus par ce portefeuille</string>
@@ -134,4 +134,71 @@
<string name="settings_version_protocol_merchant">Versions commerçantes compatibles</string>
<string name="settings_version_core">Version Wallet Core</string>
<string name="settings_version_protocol_exchange">Versions Echange compatibles</string>
+ <string name="transaction_action_kyc">Compléter le KYC</string>
+ <string name="transactions_cancel_dialog_message">Êtes-vous sûr de vouloir annuler ce retrait \? Les fonds encore en transit peuvent être perdus.</string>
+ <string name="transaction_deposit">Dépôt</string>
+ <string name="receive_withdraw">Retrait du compte bancaire</string>
+ <string name="receive_peer">Facture d\'un autre portefeuille</string>
+ <string name="receive_peer_title">Demande de paiement</string>
+ <string name="receive_peer_create_button">Créer une facture</string>
+ <string name="transactions_delete_dialog_title">Supprimer une transaction</string>
+ <string name="transactions_delete_dialog_message">Êtes-vous sûr de vouloir supprimer cette transaction de votre portefeuille \?</string>
+ <string name="receive_amount_invalid">Montant invalide</string>
+ <string name="receive_intro">Choisir où recevoir l\'argent :</string>
+ <string name="receive_peer_invoice_instruction">Laissez le payeur scanner ce QR code pour payer :</string>
+ <string name="copy" tools:override="true">Copier</string>
+ <string name="transactions_balance">Solde</string>
+ <string name="transaction_peer_pull_debit">Facture payée</string>
+ <string name="transaction_peer_push_credit">Paiement push</string>
+ <string name="copy_uri">Copier l\'URI de Taler</string>
+ <string name="transaction_peer_push_debit">Paiement push</string>
+ <string name="transaction_peer_pull_credit">Facture</string>
+ <string name="receive_amount">Montant à recevoir</string>
+ <string name="send_amount">Montant à envoyer</string>
+ <string name="send_intro">Choisir où envoyer l\'argent :</string>
+ <string name="transactions_detail_title_currency">%s Transactions</string>
+ <string name="currency">Devise</string>
+ <string name="transactions_receive_funds">Recevoir
+\ndes fonds</string>
+ <string name="transactions_send_funds">Envoyer
+\ndes fonds</string>
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="transactions_delete_selected_dialog_message">Êtes-vous sûr de vouloir supprimer les transactions sélectionnées de votre portefeuille \?</string>
+ <string name="receive_peer_invoice_uri">Sinon, copier et envoyer cet URI :</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="send_deposit_name">Titulaire du compte</string>
+ <string name="send_deposit_bitcoin_address">Adresse Bitcoin</string>
+ <string name="send_deposit_bitcoin_create_button">Transfert de Bitcoin</string>
+ <string name="send_peer_title">Envoyer de l\'argent vers un autre portefeuille</string>
+ <string name="send_peer_payment_amount_received">Montant reçu</string>
+ <string name="send_peer_payment_amount_sent">Montant envoyé</string>
+ <string name="withdraw_restrict_age">Limiter l\'utilisation selon l\'âge</string>
+ <string name="pay_peer_title">Payer la facture</string>
+ <string name="send_peer_warning">Avertissement : Les fonds quittent immédiatement le portefeuille.</string>
+ <string name="send_peer_payment_instruction">Laissez le bénéficiaire scanner ce code QR pour recevoir :</string>
+ <string name="send_deposit_check_fees_button">Vérifier les frais</string>
+ <string name="send_deposit_amount_effective">Montant effectif</string>
+ <string name="send_deposit_create_button">Effectuer un dépôt</string>
+ <string name="send_peer">Vers un autre portefeuille</string>
+ <string name="pay_peer_intro">Voulez-vous payer cette facture \?</string>
+ <string name="receive_peer_payment_title">Recevoir le paiement</string>
+ <string name="receive_peer_payment_intro">Voulez-vous recevoir ce paiement \?</string>
+ <string name="send_peer_purpose">Objectif</string>
+ <string name="send_deposit_bitcoin">Vers un portefeuille Bitcoin</string>
+ <string name="send_peer_bitcoin">Vers un autre portefeuille Taler</string>
+ <string name="send_peer_create_button">Envoyer des fonds maintenant</string>
+ <string name="send_deposit">Vers un compte bancaire</string>
+ <string name="send_deposit_title">Dépôt sur un compte bancaire</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="withdraw_restrict_age_unrestricted">Sans restriction</string>
+ <string name="withdraw_manual_ready_subject">Sujet</string>
+ <string name="withdraw_manual_ready_cancel">Annuler le retrait</string>
+ <string name="withdraw_manual_bitcoin_title">Le change de Bitcoin prêt pour les retraits</string>
+ <string name="withdraw_manual_bitcoin_intro">Effectuez maintenant une transaction fractionnée avec les trois sorties suivantes.</string>
+ <string name="withdraw_manual_ready_bank_button">Ouvrir dans l\'application bancaire</string>
+ <string name="withdraw_manual_ready_title">Le change est prêt pour le retrait !</string>
+ <string name="withdraw_manual_ready_intro">Pour compléter le processus, vous devez virer %s sur le compte bancaire de change</string>
+ <string name="withdraw_manual_ready_details_intro">Détails du virement bancaire</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_warning">Assurez-vous d\'utiliser le bon sujet, sinon l\'argent n\'arrivera pas dans ce portefeuille.</string>
</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-it/strings.xml b/wallet/src/main/res/values-it/strings.xml
index 7f43827..61bb306 100644
--- a/wallet/src/main/res/values-it/strings.xml
+++ b/wallet/src/main/res/values-it/strings.xml
@@ -49,14 +49,14 @@
<string name="transaction_paid">Pagato</string>
<string name="transaction_order_total">Totale</string>
<string name="transaction_order">Acquisto</string>
- <string name="transaction_tip_from">Suggerimento di %s</string>
+ <string name="transaction_tip_from">Ricompensa da %s</string>
<string name="transaction_refund">Rimborso</string>
<string name="transaction_refund_from">Rimborso di %s</string>
- <string name="transaction_pending">In sospeso</string>
+ <string name="transaction_pending">IN ATTESA</string>
<string name="payment_title">Pagamento</string>
<string name="transactions_empty_search">Nessun transazione trovata. Prova una ricerca diversa.</string>
<string name="transaction_order_id">Ricevuta #%1$s</string>
- <string name="transaction_refresh">Commissione per cambio scadenza moneta</string>
+ <string name="transaction_refresh">Commissione per cambio della moneta a scadenza</string>
<string name="payment_fee">+%s tassa di commissione</string>
<string name="payment_button_confirm">Confermare pagamento</string>
<string name="payment_already_paid">Hai già pagato questo ordine.</string>
@@ -67,7 +67,7 @@
<item quantity="other">Moneta: %s (usate %d volte)</item>
</plurals>
<string name="settings_withdraw_testkudos">Ritirare TESTKUDOS</string>
- <string name="settings_logcat_summary">Vedere/Inviare log interno</string>
+ <string name="settings_logcat_summary">Salvare log interno</string>
<string name="settings_reset_summary">Scarta il tuo denaro</string>
<string name="wifi_disabled_error">Accendi il Wi-Fi per avere Wi-Fi gratis</string>
<string name="payment_label_amount_total">Quantità totale:</string>
@@ -137,7 +137,106 @@
<string name="refund_title">Rimborso</string>
<string name="refund_error">Errore nel processare il rimborso</string>
<string name="refund_success">Rimborso ricevuto: %s</string>
- <string name="tip_title">Consiglio</string>
+ <string name="tip_title">Ricompensa</string>
<string name="wifi_connect_error">Impossibile connettersi alla Wi-Fi gratuita: %s</string>
<string name="withdraw_manual_title">Effettuare un trasferimento manuale per lo scambio</string>
+ <string name="exchange_list_error">Impossibile elencare i cambi</string>
+ <string name="tip_total">Totale</string>
+ <string name="tip_already_accepted">Questa ricompensa è già stata accettata.</string>
+ <string name="receive_peer">Fattura un altro portafoglio</string>
+ <string name="transaction_dummy_title">Transazione Sconosciuta</string>
+ <string name="receive_peer_title">Richiedi pagamento</string>
+ <string name="send_deposit_iban_error">IBAN non valido</string>
+ <string name="send_deposit_bic_error">BIC non valido</string>
+ <string name="send_deposit_name">Titolare del conto</string>
+ <string name="pay_peer_title">Paga fattura</string>
+ <string name="send_peer_purpose">Causale</string>
+ <string name="withdraw_restrict_age_unrestricted">Senza restrizioni</string>
+ <string name="settings_db_export_success">Banca dati esportata nel file</string>
+ <string name="tip_received">Ricompensa ricevuta</string>
+ <string name="transactions_retry">Riprova</string>
+ <string name="transactions_suspend">Sospendi</string>
+ <string name="transactions_resume">Riprendi</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="send_peer_title">Invia denaro ad un altro portafoglio</string>
+ <string name="send_peer_create_button">Invia fondi adesso</string>
+ <string name="send_peer_warning">Attenzione: I fondi lasceranno il portafogli immediatamente.</string>
+ <string name="withdraw_manual_ready_title">Lo scambio è pronto per essere prelevato!</string>
+ <string name="withdraw_manual_ready_details_intro">Dettagli del trasferimento bancario</string>
+ <string name="withdraw_manual_ready_subject">Soggetto</string>
+ <string name="transaction_action_kyc">Completa KYC</string>
+ <string name="pay_peer_intro">Vuoi pagare questa fattura\?</string>
+ <string name="transactions_delete_dialog_title">Rimuovi Transazione</string>
+ <string name="transactions_delete_dialog_message">Sei sicuro di voler rimuovere questa transazione dal tuo portafogli?</string>
+ <string name="transaction_deposit">Deposita</string>
+ <string name="receive_peer_payment_title">Ricevi pagamento</string>
+ <string name="receive_peer_payment_intro">Vuoi ricevere questo pagamento\?</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="transactions_send_funds">Invia
+\nFondi</string>
+ <string name="withdraw_manual_ready_warning">Assicurati di usare il soggetto corretto, altrimenti il denaro non arriverà in questo portafogli.</string>
+ <string name="settings_db_export">Banca dati</string>
+ <string name="withdraw_manual_ready_intro">Per completare il processo devi trasferire %s al tuo conto bancario di cambio</string>
+ <string name="send_peer_expiration_1d">1 giorno</string>
+ <string name="send_peer_expiration_7d">7 giorni</string>
+ <string name="send_peer_expiration_period">Scade in</string>
+ <string name="send_peer_expiration_custom">Personalizzato</string>
+ <string name="send_peer_expiration_30d">30 giorni</string>
+ <string name="send_peer_expiration_days">Giorni</string>
+ <string name="send_peer_expiration_hours">Ore</string>
+ <string name="withdraw_manual_ready_bank_button">Apri nell\'applicazione bancaria</string>
+ <string name="settings_db_export_summary">Salva banca dati interna</string>
+ <string name="settings_test_summary">Realizza transazioni di prova con la configurazione demo</string>
+ <string name="settings_test">Avvia test d\'integrazione</string>
+ <string name="copy" tools:override="true">Copia</string>
+ <string name="transactions_receive_funds">Ricevi
+\nFondi</string>
+ <string name="transaction_peer_pull_credit">Fattura</string>
+ <string name="send_amount">Importo da inviare</string>
+ <string name="withdraw_manual_ready_cancel">Cancella prelievo</string>
+ <string name="currency">Valuta</string>
+ <string name="send_peer_payment_amount_sent">Importo inviato</string>
+ <string name="send_peer_payment_amount_received">Importo ricevuto</string>
+ <string name="settings_logcat_error">Errore di espostazione del log</string>
+ <string name="settings_logcat_success">Log esportato nel file</string>
+ <string name="tip_fees">Commissione</string>
+ <string name="tip_merchant_url">URL del commerciante</string>
+ <string name="button_scan_qr_code_label">Scannerizza codice QR</string>
+ <string name="transaction_peer_pull_debit">Fattura pagata</string>
+ <string name="receive_intro">Scegli da dove ricevere il denaro:</string>
+ <string name="receive_withdraw">Preleva dall\'account bancario</string>
+ <string name="receive_peer_create_button">Crea fattura</string>
+ <string name="receive_peer_invoice_instruction">Fai scansionare al pagante questo codice QR per pagare:</string>
+ <string name="receive_peer_invoice_uri">In alternativa, copia e invia questo URI:</string>
+ <string name="send_intro">Scegli dove inviare il denaro:</string>
+ <string name="send_deposit">Ad un conto bancario</string>
+ <string name="send_deposit_title">Deposita ad un conto bancario</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="tip_button_confirm">Accetta ricompensa</string>
+ <string name="send_deposit_bitcoin">Ad un portafoglio Bitcoin</string>
+ <string name="send_peer_bitcoin">Ad un altro portafogli Taler</string>
+ <string name="transactions_delete_selected_dialog_message">Sei sicuro di voler rimuovere le transazioni selezionate dal tuo portafogli?</string>
+ <string name="transactions_cancel_dialog_message">Sei sicuro di voler cancellare questo prelievo? I fondi in transito potrebbero andare persi.</string>
+ <string name="send_deposit_bitcoin_address">Indirizzo Bitcoin</string>
+ <string name="send_deposit_check_fees_button">Controlla commissioni</string>
+ <string name="send_deposit_amount_effective">Importo Effetivo</string>
+ <string name="send_deposit_create_button">Fai un deposito</string>
+ <string name="send_deposit_bitcoin_create_button">Trasferisci Bitcoin</string>
+ <string name="send_peer">Ad un altro portafoglio</string>
+ <string name="settings_db_export_error">Errore nell\'esportazione della banca dati</string>
+ <string name="transactions_abort">Annulla</string>
+ <string name="transactions_fail">Arresta</string>
+ <string name="transactions_abort_dialog_title">Annulla la transazione</string>
+ <string name="transactions_fail_dialog_title">Annulla la Transazione</string>
+ <string name="transactions_fail_dialog_message">Sei sicuro di voler annullare questa transazione? I fondi ancora in transito ANDRANNO PERSI.</string>
+ <string name="transactions_abort_dialog_message">Sei sicuro di voler annullare questa transazione? I fondi ancora in transito potrebbero andare persi.</string>
+ <string name="copy_uri">Copia URI Taler</string>
+ <string name="transactions_balance">Saldo</string>
+ <string name="offline_banner">Nessun accesso ad internet</string>
+ <string name="transaction_peer_push_debit">Pagamento Push</string>
+ <string name="transactions_detail_title_currency">%sTransazioni</string>
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="error_broken_uri">Errore: Questo URI Taler è ( al momento ) non funzionante.</string>
+ <string name="import_db">Importa</string>
+ <string name="reset">Resetta</string>
</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-night/styles.xml b/wallet/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..d61986e
--- /dev/null
+++ b/wallet/src/main/res/values-night/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2023 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <style name="AppTheme" parent="AppTheme.Dark">
+ <item name="windowActionModeOverlay">true</item>
+ <item name="android:statusBarColor">?colorSurface</item>
+ <item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
+ <item name="android:navigationBarColor">?colorSurface</item>
+ <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
+ </style>
+
+</resources>
diff --git a/wallet/src/main/res/menu/transactions_detail.xml b/wallet/src/main/res/values-nl/strings.xml
index d4568d4..f23dced 100644
--- a/wallet/src/main/res/menu/transactions_detail.xml
+++ b/wallet/src/main/res/values-nl/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="utf-8"?>
+<!--
~ This file is part of GNU Taler
~ (C) 2020 Taler Systems S.A.
~
@@ -13,7 +14,5 @@
~ You should have received a copy of the GNU General Public License along with
~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-</menu>
+<resources>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-tr/strings.xml b/wallet/src/main/res/values-tr/strings.xml
index 6360667..18c098b 100644
--- a/wallet/src/main/res/values-tr/strings.xml
+++ b/wallet/src/main/res/values-tr/strings.xml
@@ -27,8 +27,8 @@
<string name="menu_retry_pending_operations">Bekleyen İşlemleri Yeniden Deneyin</string>
<string name="host_apdu_service_desc">Taler NFC Ödemeleri</string>
<string name="balances_title">Bakiyeler</string>
- <string name="amount_positive">+ %s</string>
- <string name="amount_negative">- %s</string>
+ <string name="amount_positive">+. %s</string>
+ <string name="amount_negative">-. %s</string>
<string name="amount_chosen">Seçilen Miktar</string>
<string name="balances_inbound_label">gelen</string>
<string name="transactions_title">İşlemler</string>
@@ -148,4 +148,13 @@
<item quantity="one">Madeni para: %s (%d kez kullanıldı)</item>
<item quantity="other">Madeni paralar: %s (%d kez kullanıldılar)</item>
</plurals>
+ <string name="transactions_send_funds">Gönder
+\nFonlar</string>
+ <string name="transactions_receive_funds">Al
+\nFonlar</string>
+ <string name="transactions_detail_title_currency">%s İşlemleri</string>
+ <string name="transactions_retry">Tekrar Dene</string>
+ <string name="transactions_abort">İptal Et</string>
+ <string name="transactions_cancel_dialog_message">Bu çekimi iptal etmek istediğinizden emin misiniz? Transit halindeki fonlar kaybolabilir.</string>
+ <string name="transactions_balance">Bakiye</string>
</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-v27/styles.xml b/wallet/src/main/res/values-v27/styles.xml
new file mode 100644
index 0000000..5a21b91
--- /dev/null
+++ b/wallet/src/main/res/values-v27/styles.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2023 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<resources>
+
+ <style name="AppTheme" parent="AppTheme.Light">
+ <item name="android:statusBarColor">?colorSurface</item>
+ <item name="android:windowLightStatusBar">true</item>
+ <item name="android:navigationBarColor">?colorSurface</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values/colors.xml b/wallet/src/main/res/values/colors.xml
index ba2804e..ce76c1d 100644
--- a/wallet/src/main/res/values/colors.xml
+++ b/wallet/src/main/res/values/colors.xml
@@ -18,4 +18,67 @@
<color name="colorPrimary">#3047a3</color>
<color name="colorPrimaryDark">#152238</color>
<color name="colorAccent">#BB1D24</color>
+
+ <!-- Material 3 theme builder -->
+ <color name="seed">#3047a3</color>
+ <color name="md_theme_light_primary">#4157B4</color>
+ <color name="md_theme_light_onPrimary">#FFFFFF</color>
+ <color name="md_theme_light_primaryContainer">#DDE1FF</color>
+ <color name="md_theme_light_onPrimaryContainer">#001355</color>
+ <color name="md_theme_light_secondary">#2B5EA7</color>
+ <color name="md_theme_light_onSecondary">#FFFFFF</color>
+ <color name="md_theme_light_secondaryContainer">#D7E3FF</color>
+ <color name="md_theme_light_onSecondaryContainer">#001B3E</color>
+ <color name="md_theme_light_tertiary">#B91B23</color>
+ <color name="md_theme_light_onTertiary">#FFFFFF</color>
+ <color name="md_theme_light_tertiaryContainer">#FFDAD7</color>
+ <color name="md_theme_light_onTertiaryContainer">#410004</color>
+ <color name="md_theme_light_error">#BA1A1A</color>
+ <color name="md_theme_light_errorContainer">#FFDAD6</color>
+ <color name="md_theme_light_onError">#FFFFFF</color>
+ <color name="md_theme_light_onErrorContainer">#410002</color>
+ <color name="md_theme_light_background">#F8FDFF</color>
+ <color name="md_theme_light_onBackground">#001F25</color>
+ <color name="md_theme_light_surface">#F8FDFF</color>
+ <color name="md_theme_light_onSurface">#001F25</color>
+ <color name="md_theme_light_surfaceVariant">#E2E1EC</color>
+ <color name="md_theme_light_onSurfaceVariant">#45464F</color>
+ <color name="md_theme_light_outline">#767680</color>
+ <color name="md_theme_light_inverseOnSurface">#D6F6FF</color>
+ <color name="md_theme_light_inverseSurface">#00363F</color>
+ <color name="md_theme_light_inversePrimary">#B8C3FF</color>
+ <color name="md_theme_light_shadow">#000000</color>
+ <color name="md_theme_light_surfaceTint">#4157B4</color>
+ <color name="md_theme_light_outlineVariant">#C6C5D0</color>
+ <color name="md_theme_light_scrim">#000000</color>
+ <color name="md_theme_dark_primary">#B8C3FF</color>
+ <color name="md_theme_dark_onPrimary">#042584</color>
+ <color name="md_theme_dark_primaryContainer">#263E9A</color>
+ <color name="md_theme_dark_onPrimaryContainer">#DDE1FF</color>
+ <color name="md_theme_dark_secondary">#AAC7FF</color>
+ <color name="md_theme_dark_onSecondary">#002F65</color>
+ <color name="md_theme_dark_secondaryContainer">#00458E</color>
+ <color name="md_theme_dark_onSecondaryContainer">#D7E3FF</color>
+ <color name="md_theme_dark_tertiary">#FFB3AD</color>
+ <color name="md_theme_dark_onTertiary">#680009</color>
+ <color name="md_theme_dark_tertiaryContainer">#930012</color>
+ <color name="md_theme_dark_onTertiaryContainer">#FFDAD7</color>
+ <color name="md_theme_dark_error">#FFB4AB</color>
+ <color name="md_theme_dark_errorContainer">#93000A</color>
+ <color name="md_theme_dark_onError">#690005</color>
+ <color name="md_theme_dark_onErrorContainer">#FFDAD6</color>
+ <color name="md_theme_dark_background">#001F25</color>
+ <color name="md_theme_dark_onBackground">#A6EEFF</color>
+ <color name="md_theme_dark_surface">#001F25</color>
+ <color name="md_theme_dark_onSurface">#A6EEFF</color>
+ <color name="md_theme_dark_surfaceVariant">#45464F</color>
+ <color name="md_theme_dark_onSurfaceVariant">#C6C5D0</color>
+ <color name="md_theme_dark_outline">#90909A</color>
+ <color name="md_theme_dark_inverseOnSurface">#001F25</color>
+ <color name="md_theme_dark_inverseSurface">#A6EEFF</color>
+ <color name="md_theme_dark_inversePrimary">#4157B4</color>
+ <color name="md_theme_dark_shadow">#000000</color>
+ <color name="md_theme_dark_surfaceTint">#B8C3FF</color>
+ <color name="md_theme_dark_outlineVariant">#45464F</color>
+ <color name="md_theme_dark_scrim">#000000</color>
</resources>
diff --git a/wallet/src/main/res/values/defaults.xml b/wallet/src/main/res/values/defaults.xml
index 0e2a6a1..dffc14e 100644
--- a/wallet/src/main/res/values/defaults.xml
+++ b/wallet/src/main/res/values/defaults.xml
@@ -16,6 +16,4 @@
<resources>
- <bool name="settings_backup_default">true</bool>
-
</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 52dacfe..8466e2d 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -44,6 +44,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="button_back">Go Back</string>
<string name="button_scan_qr_code">Scan Taler QR Code</string>
+ <string name="button_scan_qr_code_label">Scan QR code</string>
<string name="enter_uri">Enter Taler URI</string>
<string name="copy" tools:override="true">Copy</string>
<string name="copy_uri">Copy Taler URI</string>
@@ -58,36 +59,58 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="currency">Currency</string>
<!-- The count should be mirrored in RTL languages -->
<string name="char_count">%1$d/%2$d</string>
+ <string name="import_db">Import</string>
+ <string name="reset">Reset</string>
+ <string name="share_payment">Share payment link</string>
<string name="offline">Operation requires internet access. Please ensure your internet connection works and try again.</string>
+ <string name="offline_banner">No internet access</string>
<string name="error_unsupported_uri">Error: This Taler URI is not supported.</string>
+ <string name="error_broken_uri">Error: This Taler URI is (currently) not working.</string>
<string name="menu_settings">Settings</string>
<string name="menu_retry_pending_operations">Retry Pending Operations</string>
<string name="host_apdu_service_desc">Taler NFC Payments</string>
+ <string name="handle_uri_title">Loading action</string>
+
<string name="balances_title">Balances</string>
<string name="amount_positive">+%s</string>
<string name="amount_negative">-%s</string>
<string name="amount_chosen">Chosen Amount</string>
+ <string name="amount_sent">Amount sent</string>
+ <string name="amount_received">Amount received</string>
<string name="balances_inbound_label">inbound</string>
<string name="balances_empty_state">There is no digital cash in your wallet.\n\nYou can get test money from the demo bank:\n\nhttps://bank.demo.taler.net</string>
+ <string name="balance_scope_exchange">Exchange: %1$s</string>
+ <string name="balance_scope_auditor">Auditor: %1$s</string>
<string name="transactions_title">Transactions</string>
<string name="transactions_balance">Balance</string>
<string name="transactions_send_funds">Send\nFunds</string>
+ <string name="transactions_send_funds_title">Send %1$s</string>
<string name="transactions_receive_funds">Receive\nFunds</string>
+ <string name="transactions_receive_funds_title">Receive %1$s</string>
<string name="transactions_empty">You don\'t have any transactions</string>
<string name="transactions_empty_search">No transactions found. Try a different search.</string>
<string name="transactions_error">Could not load transactions\n\n%s</string>
<string name="transactions_detail_title">Transaction</string>
<string name="transactions_detail_title_currency">%s Transactions</string>
<string name="transactions_delete">Delete</string>
+ <string name="transactions_retry">Retry</string>
+ <string name="transactions_abort">Abort</string>
+ <string name="transactions_fail">Kill</string>
+ <string name="transactions_suspend">Suspend</string>
+ <string name="transactions_resume">Resume</string>
<string name="transactions_select_all">Select All</string>
<string name="transactions_delete_dialog_title">Delete Transaction</string>
<string name="transactions_delete_dialog_message">Are you sure you want to remove this transaction from your wallet?</string>
<string name="transactions_delete_selected_dialog_message">Are you sure you want to remove the selected transactions from your wallet?</string>
+ <string name="transactions_abort_dialog_title">Abort Transaction</string>
+ <string name="transactions_abort_dialog_message">Are you sure you want to abort this transaction? Funds still in transit might get lost.</string>
+ <string name="transactions_fail_dialog_title">Kill Transaction</string>
+ <string name="transactions_fail_dialog_message">Are you sure you want to kill this transaction? Funds still in transit WILL GET LOST.</string>
<string name="transactions_cancel_dialog_message">Are you sure you want to cancel this withdrawal? Funds still in transit might get lost.</string>
<!-- Transactions -->
@@ -95,7 +118,6 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="transaction_order_total">Total</string>
<string name="transaction_order">Purchase</string>
<string name="transaction_order_id">Receipt #%1$s</string>
- <string name="transaction_tip_from">Tip from %s</string>
<string name="transaction_refund">Refund</string>
<string name="transaction_refund_from">Refund from %s</string>
<string name="transaction_pending">PENDING</string>
@@ -106,19 +128,27 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="transaction_peer_pull_debit">Invoice paid</string>
<string name="transaction_peer_push_credit">Push payment</string>
<string name="transaction_action_kyc">Complete KYC</string>
+ <string name="transaction_denom_loss">Loss of funds</string>
+ <string name="transaction_dummy_title">Unknown Transaction</string>
<string name="payment_title">Payment</string>
+ <string name="payment_prompt_title">Review Payment</string>
<string name="payment_fee">+%s payment fee</string>
<string name="payment_button_confirm">Confirm Payment</string>
<string name="payment_label_amount_total">Total Amount:</string>
<string name="payment_label_order_summary">Purchase</string>
<string name="payment_error">Error: %s</string>
+ <string name="payment_pending">Payment not completed, it will be retried</string>
<string name="payment_balance_insufficient">Balance insufficient!</string>
<string name="payment_aborted">Aborted</string>
<string name="payment_failed">Failed</string>
<string name="payment_initiated">Payment initiated</string>
<string name="payment_already_paid_title">Already paid</string>
<string name="payment_already_paid">You\'ve already paid for this purchase.</string>
+ <string name="payment_pay_template_title">Customize your order</string>
+ <string name="payment_create_order">Create order</string>
+ <string name="payment_confirmation_code">Confirmation code</string>
+ <string name="payment_template_error">Error creating order</string>
<string name="receive_amount">Amount to receive</string>
<string name="receive_amount_invalid">Amount invalid</string>
@@ -129,6 +159,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="receive_peer_create_button">Create invoice</string>
<string name="receive_peer_invoice_instruction">Let the payer scan this QR code to pay:</string>
<string name="receive_peer_invoice_uri">Alternatively, copy and send this URI:</string>
+ <string name="receive_peer_amount_invoiced">Amount invoiced</string>
<string name="send_amount">Amount to send</string>
<string name="send_intro">Choose where to send money to:</string>
@@ -136,6 +167,8 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="send_deposit_bitcoin">To a Bitcoin wallet</string>
<string name="send_deposit_title">Deposit to a bank account</string>
<string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_iban_error">IBAN is invalid</string>
+ <string name="send_deposit_bic_error">BIC is invalid</string>
<string name="send_deposit_bic">BIC/SWIFT</string>
<string name="send_deposit_name">Account holder</string>
<string name="send_deposit_bitcoin_address">Bitcoin address</string>
@@ -147,10 +180,14 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="send_peer_bitcoin">To another Taler wallet</string>
<string name="send_peer_title">Send money to another wallet</string>
<string name="send_peer_create_button">Send funds now</string>
- <string name="send_peer_warning">Warning: Funds will leave the wallet immediately.</string>
<string name="send_peer_payment_instruction">Let the payee scan this QR code to receive:</string>
- <string name="send_peer_payment_amount_received">Amount received</string>
- <string name="send_peer_payment_amount_sent">Amount sent</string>
+ <string name="send_peer_expiration_period">Expires in</string>
+ <string name="send_peer_expiration_1d">1 day</string>
+ <string name="send_peer_expiration_7d">7 days</string>
+ <string name="send_peer_expiration_30d">30 days</string>
+ <string name="send_peer_expiration_custom">Custom</string>
+ <string name="send_peer_expiration_days">Days</string>
+ <string name="send_peer_expiration_hours">Hours</string>
<string name="send_peer_purpose">Purpose</string>
<string name="pay_peer_title">Pay invoice</string>
@@ -160,11 +197,13 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="withdraw_initiated">Withdrawal initiated</string>
<string name="withdraw_title">Withdrawal</string>
+ <string name="withdraw_subtitle">Select target bank account</string>
<string name="withdraw_total">Withdraw</string>
<string name="withdraw_fees">Fee</string>
<string name="withdraw_restrict_age">Restrict Usage to Age</string>
<string name="withdraw_restrict_age_unrestricted">Unrestricted</string>
<string name="withdraw_exchange">Exchange</string>
+ <string name="withdraw_bank">Bank</string>
<string name="withdraw_button_confirm">Confirm Withdraw</string>
<string name="withdraw_button_confirm_bank">Confirm with bank</string>
<string name="withdraw_button_tos">Review Terms</string>
@@ -180,6 +219,8 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="withdraw_manual_bitcoin_title">Bitcoin exchange ready for withdrawal</string>
<string name="withdraw_manual_bitcoin_intro">Now make a split transaction with the following three outputs.</string>
<string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_account">Account</string>
+ <string name="withdraw_manual_ready_receiver">Receiver name</string>
<string name="withdraw_manual_ready_subject">Subject</string>
<string name="withdraw_manual_ready_bank_button">Open in banking app</string>
<string name="withdraw_manual_ready_cancel">Cancel withdrawal</string>
@@ -187,6 +228,11 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="withdraw_error_title">Withdrawal Error</string>
<string name="withdraw_error_message">Withdrawing is currently not possible. Please try again later!</string>
<string name="withdraw_error_test">Error withdrawing TESTKUDOS</string>
+ <string name="withdraw_account">Account #%1$d</string>
+ <string name="withdraw_account_currency">Account #%1$d (%2$s)</string>
+ <string name="withdraw_transfer">Transfer</string>
+ <string name="withdraw_conversion">Conversion</string>
+ <string name="withdraw_conversion_support">This exchange supports currency conversion</string>
<string name="exchange_settings_title">Exchanges</string>
<string name="exchange_settings_summary">Manage list of exchanges known to this wallet</string>
@@ -194,8 +240,16 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="exchange_list_empty">No exchanges known\n\nAdd one manually or withdraw digital cash!</string>
<string name="exchange_list_currency">Currency: %s</string>
<string name="exchange_list_add">Add exchange</string>
+ <string name="exchange_list_select">Select exchange</string>
+ <string name="exchange_delete">Delete exchange</string>
+ <string name="exchange_delete_force">Force deletion (purge)</string>
+ <string name="exchange_dialog_delete_message">Are you sure you want to delete this exchange? Forcing this operation will result in a loss of funds.</string>
+ <string name="exchange_reload">Reload information</string>
+ <string name="exchange_not_contacted">Exchange not contacted</string>
<string name="exchange_add_url">Enter address of exchange</string>
<string name="exchange_add_error">Could not add exchange</string>
+ <string name="exchange_list_error">Could not list exchanges</string>
+ <string name="exchange_list_add_dev">Add development exchanges</string>
<string name="exchange_menu_manual_withdraw">Withdraw</string>
<string name="exchange_fee_withdrawal_fee_label">Withdrawal Fee:</string>
@@ -217,10 +271,23 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="exchange_tos_accept">Accept Terms of Service</string>
<string name="exchange_tos_error">Error showing Terms of Service: %s</string>
+ <string name="loss_amount">Amount lost</string>
+ <string name="loss_reason">Reason</string>
+ <string name="loss_reason_expired">Funds were not renewed, because the wallet was not opened for a long time</string>
+ <string name="loss_reason_vanished">The payment provider lost the record of the funds</string>
+ <string name="loss_reason_unoffered">The payment provider stopped offering the denomination backing the funds</string>
+
+
<string name="pending_operations_title">Pending Operations</string>
<string name="pending_operations_refuse">Refuse Proposal</string>
<string name="pending_operations_no_action">(no action)</string>
+ <!-- Observability -->
+ <string name="show_logs">Show logs</string>
+ <string name="observability_title">Internal event log</string>
+ <string name="observability_show_json">Show JSON</string>
+ <string name="observability_hide_json">Hide JSON</string>
+
<string name="settings_dev_mode">Developer Mode</string>
<string name="settings_dev_mode_summary">Shows more information intended for debugging</string>
<string name="settings_withdraw_testkudos">Withdraw TESTKUDOS</string>
@@ -229,33 +296,34 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="settings_logcat_summary">Save internal log</string>
<string name="settings_logcat_error">Error exporting log</string>
<string name="settings_logcat_success">Log exported to file</string>
- <string name="settings_db_export">Database</string>
+ <string name="settings_db_export">Export database</string>
<string name="settings_db_export_summary">Save internal database</string>
+ <string name="settings_db_import">Import database</string>
+ <string name="settings_db_import_summary">Restore database from file</string>
<string name="settings_db_export_error">Error exporting database</string>
+ <string name="settings_db_import_error">Error importing database</string>
+ <string name="settings_db_clear_error">Error cleaning database</string>
<string name="settings_db_export_success">Database exported to file</string>
+ <string name="settings_db_import_success">Database imported from file</string>
<string name="settings_version_app">App Version</string>
<string name="settings_version_core">Wallet Core Version</string>
<string name="settings_version_protocol_exchange">Supported Exchange Versions</string>
<string name="settings_version_protocol_merchant">Supported Merchant Versions</string>
<string name="settings_version_unknown">Unknown</string>
+ <string name="settings_test">Run integration test</string>
+ <string name="settings_test_summary">Performs test transactions with demo setup</string>
<string name="settings_reset">Reset Wallet (dangerous!)</string>
<string name="settings_reset_summary">Throws away your money</string>
- <string name="nav_settings_backup">Backup</string>
- <string name="backup_last">Last Backup: %s</string>
+ <string name="settings_dialog_reset_message">Do you really want to reset the wallet and lose all coins and purchases?</string>
+ <string name="settings_dialog_import_message">This operation will overwrite your existing database. Do you want to continue?</string>
+ <string name="settings_alert_reset_done">Wallet has been reset</string>
+ <string name="settings_alert_reset_canceled">Reset cancelled</string>
+ <string name="settings_alert_import_canceled">Import cancelled</string>
<string name="refund_title">Refund</string>
<string name="refund_error">Error processing refund</string>
- <string name="refund_success">Refund received: %s</string>
-
- <string name="tip_title">Tip</string>
- <string name="tip_already_accepted">This tip is already accepted.</string>
- <string name="tip_total">Total</string>
- <string name="tip_fees">Fee</string>
- <string name="tip_exchange">Exchange</string>
- <string name="tip_merchant_url">Merchant URL</string>
- <string name="tip_button_confirm">Accept tip</string>
- <string name="tip_received">Tip received</string>
+ <string name="refund_success">Refund received!</string>
<string name="wifi_disabled_error">Turn on Wi-Fi to get free Wi-Fi</string>
<string name="wifi_connect_error">Could not connect to free Wi-Fi: %s</string>
diff --git a/wallet/src/main/res/values/styles.xml b/wallet/src/main/res/values/styles.xml
index 33e31a3..961c8da 100644
--- a/wallet/src/main/res/values/styles.xml
+++ b/wallet/src/main/res/values/styles.xml
@@ -14,32 +14,103 @@
~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-->
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools">
- <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
- <item name="colorPrimary">@color/colorPrimary</item>
- <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
- <item name="colorAccent">@color/colorAccent</item>
- <item name="colorOnPrimary">@android:color/white</item>
+ <style name="AppTheme.Light" parent="Theme.Material3.Light">
+ <!-- Material 3 theme builder -->
+ <item name="colorPrimary">@color/md_theme_light_primary</item>
+ <item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
+ <item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
+ <item name="colorOnPrimaryContainer">@color/md_theme_light_onPrimaryContainer</item>
+ <item name="colorSecondary">@color/md_theme_light_secondary</item>
+ <item name="colorOnSecondary">@color/md_theme_light_onSecondary</item>
+ <item name="colorSecondaryContainer">@color/md_theme_light_secondaryContainer</item>
+ <item name="colorOnSecondaryContainer">@color/md_theme_light_onSecondaryContainer</item>
+ <item name="colorTertiary">@color/md_theme_light_tertiary</item>
+ <item name="colorOnTertiary">@color/md_theme_light_onTertiary</item>
+ <item name="colorTertiaryContainer">@color/md_theme_light_tertiaryContainer</item>
+ <item name="colorOnTertiaryContainer">@color/md_theme_light_onTertiaryContainer</item>
+ <item name="colorError">@color/md_theme_light_error</item>
+ <item name="colorErrorContainer">@color/md_theme_light_errorContainer</item>
+ <item name="colorOnError">@color/md_theme_light_onError</item>
+ <item name="colorOnErrorContainer">@color/md_theme_light_onErrorContainer</item>
+ <item name="android:colorBackground">@color/md_theme_light_background</item>
+ <item name="colorOnBackground">@color/md_theme_light_onBackground</item>
+ <item name="colorSurface">@color/md_theme_light_surface</item>
+ <item name="colorOnSurface">@color/md_theme_light_onSurface</item>
+ <item name="colorSurfaceVariant">@color/md_theme_light_surfaceVariant</item>
+ <item name="colorOnSurfaceVariant">@color/md_theme_light_onSurfaceVariant</item>
+ <item name="colorOutline">@color/md_theme_light_outline</item>
+ <item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item>
+ <item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item>
+ <item name="colorPrimaryInverse">@color/md_theme_light_inversePrimary</item>
+ </style>
+
+ <style name="AppTheme.Dark" parent="Theme.Material3.Dark">
+ <item name="windowActionModeOverlay">true</item>
+ <item name="android:statusBarColor" tools:targetApi="m">?colorSurface</item>
+ <item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
+ <item name="android:navigationBarColor" tools:targetApi="o_mr1">?colorSurface</item>
+ <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
+ <!-- Material 3 theme builder -->
+ <item name="colorPrimary">@color/md_theme_dark_primary</item>
+ <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
+ <item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
+ <item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
+ <item name="colorSecondary">@color/md_theme_dark_secondary</item>
+ <item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
+ <item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
+ <item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
+ <item name="colorTertiary">@color/md_theme_dark_tertiary</item>
+ <item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
+ <item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
+ <item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
+ <item name="colorError">@color/md_theme_dark_error</item>
+ <item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
+ <item name="colorOnError">@color/md_theme_dark_onError</item>
+ <item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
+ <item name="android:colorBackground">@color/md_theme_dark_background</item>
+ <item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
+ <item name="colorSurface">@color/md_theme_dark_surface</item>
+ <item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
+ <item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
+ <item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
+ <item name="colorOutline">@color/md_theme_dark_outline</item>
+ <item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
+ <item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
+ <item name="colorPrimaryInverse">@color/md_theme_dark_inversePrimary</item>
+ </style>
+
+ <style name="AppTheme" parent="AppTheme.Light">
<item name="windowActionModeOverlay">true</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
- <item name="android:statusBarColor">@android:color/transparent</item>
</style>
- <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.ActionBar" />
+ <style name="AppTheme.AppBarOverlay" parent="Widget.Material3.ActionBar.Solid" />
- <style name="AppTheme.Toolbar" parent="Widget.MaterialComponents.Toolbar.Primary" />
+ <style name="AppTheme.Toolbar" parent="Widget.Material3.Toolbar" />
- <style name="DialogTheme" parent="Theme.MaterialComponents.DayNight.Dialog.Alert" />
+ <style name="DialogTheme" parent="Theme.Material3.DayNight.Dialog.Alert" />
<style name="TransactionTitle">
- <item name="android:textSize">16sp</item>
- <item name="android:textColor">?android:textColorPrimary</item>
+ <item name="android:textAppearance">@style/TextAppearance.Material3.TitleMedium</item>
+ </style>
+
+ <style name="TransactionSubtitle">
+ <item name="android:textAppearance">@style/TextAppearance.Material3.BodyMedium</item>
+ </style>
+
+ <style name="TransactionTimestamp">
+ <item name="android:textAppearance">@style/TextAppearance.Material3.LabelMedium</item>
+ </style>
+
+ <style name="TransactionAmount">
+ <item name="android:textAppearance">@style/TextAppearance.Material3.TitleLarge</item>
</style>
<style name="TransactionLabel">
@@ -72,13 +143,11 @@
<item name="cardElevation">8dp</item>
</style>
- <style name="FabStyle" parent="Widget.MaterialComponents.FloatingActionButton">
+ <style name="FabStyle" parent="Widget.Material3.FloatingActionButton.Primary">
<item name="android:layout_gravity">bottom|end</item>
<item name="android:layout_marginEnd">16dp</item>
<item name="android:layout_marginBottom">16dp</item>
- <item name="backgroundTint">@color/colorPrimary</item>
<item name="layout_dodgeInsetEdges">bottom</item>
- <item name="tint">?attr/colorOnPrimary</item>
</style>
</resources>
diff --git a/wallet/src/main/res/xml/network_security_config.xml b/wallet/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..de61259
--- /dev/null
+++ b/wallet/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+ <domain-config cleartextTrafficPermitted="true">
+ <domain includeSubdomains="true">localhost</domain>
+ </domain-config>
+</network-security-config>
diff --git a/wallet/src/main/res/xml/settings_backup.xml b/wallet/src/main/res/xml/settings_backup.xml
deleted file mode 100644
index aa9b129..0000000
--- a/wallet/src/main/res/xml/settings_backup.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<PreferenceScreen
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <SwitchPreferenceCompat
- app:defaultValue="@bool/settings_backup_default"
- app:fragment="net.taler.wallet.settings.BackupSettingsFragment"
- app:icon="@drawable/ic_baseline_backup"
- app:key="pref_backup_switch"
- app:summaryOff="Backup is disabled"
- app:summaryOn="Backup is enabled"
- app:title="Backup my wallet" />
-
- <Preference
- app:dependency="pref_backup_switch"
- app:icon="@drawable/ic_baseline_account_tree"
- app:key="pref_backup_services"
- app:selectable="false"
- app:summary="1 backup service set up"
- app:title="Backup services" />
-
- <PreferenceCategory
- app:iconSpaceReserved="false"
- app:summary="At least one of these is required to restore from backup"
- app:title="Recovery Options">
-
- <Preference
- app:dependency="pref_backup_switch"
- app:icon="@drawable/ic_baseline_vpn_key"
- app:key="pref_backup_secret"
- app:selectable="false"
- app:title="Show backup secret" />
-
- </PreferenceCategory>
-
-</PreferenceScreen>
diff --git a/wallet/src/main/res/xml/settings_main.xml b/wallet/src/main/res/xml/settings_main.xml
index 101aa37..3567e49 100644
--- a/wallet/src/main/res/xml/settings_main.xml
+++ b/wallet/src/main/res/xml/settings_main.xml
@@ -24,13 +24,6 @@
app:summary="@string/exchange_settings_summary"
app:title="@string/exchange_settings_title" />
- <Preference
- app:fragment="net.taler.wallet.settings.BackupSettingsFragment"
- app:icon="@drawable/ic_baseline_backup"
- app:isPreferenceVisible="false"
- app:key="pref_backup"
- app:title="Backup"
- tools:isPreferenceVisible="true" />
<SwitchPreferenceCompat
app:icon="@drawable/ic_developer_mode"
@@ -63,8 +56,16 @@
tools:isPreferenceVisible="true" />
<Preference
- app:icon="@drawable/ic_account_balance_wallet"
+ app:icon="@drawable/ic_archive"
app:isPreferenceVisible="false"
+ app:key="pref_import_db"
+ app:summary="@string/settings_db_import_summary"
+ app:title="@string/settings_db_import"
+ tools:isPreferenceVisible="true" />
+
+ <Preference
+ app:icon="@drawable/ic_account_balance_wallet"
+ app:isPreferenceVisible="true"
app:key="pref_version_app"
app:selectable="false"
app:summary="@string/settings_version_unknown"
@@ -99,6 +100,13 @@
tools:isPreferenceVisible="true" />
<Preference
+ app:isPreferenceVisible="false"
+ app:key="pref_test"
+ app:summary="@string/settings_test_summary"
+ app:title="@string/settings_test"
+ tools:isPreferenceVisible="true" />
+
+ <Preference
app:icon="@drawable/ic_nuke"
app:isPreferenceVisible="false"
app:key="pref_reset"
diff --git a/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt b/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
index e2389d8..5d3a80b 100644
--- a/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
+++ b/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
@@ -39,6 +39,11 @@ class WalletResponseTest {
"result": {
"balances": [
{
+ "scopeInfo": {
+ "currency": "TESTKUDOS",
+ "type": "exchange",
+ "url": "https://exchange.test.taler.net/"
+ },
"available": "TESTKUDOS:15.8",
"pendingIncoming": "TESTKUDOS:0",
"pendingOutgoing": "TESTKUDOS:0",
diff --git a/wallet/src/test/java/net/taler/wallet/tip/TipResponsesTest.kt b/wallet/src/test/java/net/taler/wallet/tip/TipResponsesTest.kt
deleted file mode 100644
index 0300933..0000000
--- a/wallet/src/test/java/net/taler/wallet/tip/TipResponsesTest.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.tip
-
-import kotlinx.serialization.json.Json
-import net.taler.common.Amount
-import org.junit.Test
-
-class TipResponsesTest {
-
- private val json = Json {
- ignoreUnknownKeys = true
- }
-
- @Test
- fun testConfirmTipResult() {
- val jsonStr = """
- {
- "type": "response",
- "operation": "acceptTip",
- "id": 47,
- "result": {}
- }
- """.trimIndent()
- json.decodeFromString(ConfirmTipResult.serializer(), jsonStr)
- }
-
- @Test
- fun testTipPossibleSerializer() {
- val jsonStr = """
- {
- "accepted": false,
- "tipAmountRaw": "ARS:2",
- "exchangeBaseUrl": "http://exchange.taler:8081/",
- "merchantBaseUrl": "http://merchant-backend.taler:9966/",
- "expirationTimestamp": {
- "t_s": 1688217455
- },
- "tipAmountEffective": "ARS:1.4",
- "walletTipId": "SZH86ATJC4NZ427JHFVQ9M3S1TCQKVWSSZGSBW8MQ8VTVWD4M4GG"
- }
- """.trimIndent()
- val response = json.decodeFromString(PrepareTipResponse.serializer(), jsonStr)
- response as PrepareTipResponse.TipPossibleResponse
- assert(response.walletTipId == "SZH86ATJC4NZ427JHFVQ9M3S1TCQKVWSSZGSBW8MQ8VTVWD4M4GG")
- assert(response.tipAmountEffective == Amount(currency = "ARS", fraction = 40000000, value = 1))
- }
-
- @Test
- fun testTipAcceptedSerializer() {
- val jsonStr = """
- {
- "accepted": true,
- "tipAmountRaw": "ARS:2",
- "exchangeBaseUrl": "http://exchange.taler:8081/",
- "merchantBaseUrl": "http://merchant-backend.taler:9966/",
- "expirationTimestamp": {
- "t_s": 1688217455
- },
- "tipAmountEffective": "ARS:1.4",
- "walletTipId": "SZH86ATJC4NZ427JHFVQ9M3S1TCQKVWSSZGSBW8MQ8VTVWD4M4GG"
- }
- """.trimIndent()
- val response = json.decodeFromString(PrepareTipResponse.serializer(), jsonStr)
- assert(response is PrepareTipResponse.AlreadyAcceptedResponse)
- assert((response as PrepareTipResponse.AlreadyAcceptedResponse).walletTipId == "SZH86ATJC4NZ427JHFVQ9M3S1TCQKVWSSZGSBW8MQ8VTVWD4M4GG")
- }
-
-}