[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-taler-android] 01/01: Merge all three apps into one repository
From: |
gnunet |
Subject: |
[taler-taler-android] 01/01: Merge all three apps into one repository |
Date: |
Wed, 18 Mar 2020 18:25:16 +0100 |
This is an automated email from the git hooks/post-receive script.
torsten-grote pushed a commit to branch master
in repository taler-android.
commit a4796ec47d89a851b260b6fc195494547208a025
Author: Torsten Grote <address@hidden>
AuthorDate: Wed Mar 18 14:24:41 2020 -0300
Merge all three apps into one repository
---
.gitignore | 19 +
.gitlab-ci.yml | 20 +
.idea/codeStyles/Project.xml | 124 ++++
.idea/codeStyles/codeStyleConfig.xml | 5 +
.idea/compiler.xml | 15 +
.idea/copyright/Taler.xml | 7 +
.idea/copyright/profiles_settings.xml | 7 +
.idea/dictionaries/user.xml | 14 +
.idea/encodings.xml | 6 +
.idea/gradle.xml | 23 +
.idea/scopes/Copyright_Files.xml | 3 +
COPYING | 674 +++++++++++++++++++++
akono/.gitignore | 1 +
akono/build.gradle | 18 +
artwork/ic_bottom_left.svg | 56 ++
artwork/ic_bottom_right.svg | 56 ++
artwork/ic_launcher_cashier.svg | 55 ++
build.gradle | 24 +
cashier/.gitignore | 1 +
cashier/.gitlab-ci.yml | 35 ++
cashier/README.md | 10 +
cashier/build.gradle | 72 +++
cashier/lint.xml | 4 +
cashier/proguard-rules.pro | 21 +
cashier/src/main/AndroidManifest.xml | 32 +
cashier/src/main/ic_launcher-web.png | Bin 0 -> 30434 bytes
cashier/src/main/java/net/taler/cashier/Amount.kt | 45 ++
.../main/java/net/taler/cashier/BalanceFragment.kt | 182 ++++++
.../main/java/net/taler/cashier/ConfigFragment.kt | 139 +++++
.../src/main/java/net/taler/cashier/HttpHelper.kt | 102 ++++
.../main/java/net/taler/cashier/MainActivity.kt | 62 ++
.../main/java/net/taler/cashier/MainViewModel.kt | 148 +++++
cashier/src/main/java/net/taler/cashier/Utils.kt | 91 +++
.../net/taler/cashier/withdraw/ErrorFragment.kt | 55 ++
.../java/net/taler/cashier/withdraw/NfcManager.kt | 234 +++++++
.../net/taler/cashier/withdraw/QrCodeManager.kt | 42 ++
.../taler/cashier/withdraw/TransactionFragment.kt | 174 ++++++
.../net/taler/cashier/withdraw/WithdrawManager.kt | 232 +++++++
cashier/src/main/res/drawable-w550dp/ic_arrow.xml | 11 +
cashier/src/main/res/drawable/ic_arrow.xml | 11 +
cashier/src/main/res/drawable/ic_check_circle.xml | 10 +
cashier/src/main/res/drawable/ic_clear.xml | 9 +
cashier/src/main/res/drawable/ic_error.xml | 11 +
.../main/res/drawable/ic_launcher_foreground.xml | 15 +
cashier/src/main/res/drawable/ic_withdraw.xml | 10 +
.../main/res/layout-w550dp/fragment_balance.xml | 222 +++++++
.../res/layout-w550dp/fragment_transaction.xml | 111 ++++
cashier/src/main/res/layout/activity_main.xml | 51 ++
cashier/src/main/res/layout/fragment_balance.xml | 225 +++++++
cashier/src/main/res/layout/fragment_config.xml | 112 ++++
cashier/src/main/res/layout/fragment_error.xml | 65 ++
.../src/main/res/layout/fragment_transaction.xml | 100 +++
cashier/src/main/res/menu/balance.xml | 30 +
.../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 5 +
cashier/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3687 bytes
cashier/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2408 bytes
cashier/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4875 bytes
cashier/src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7673 bytes
.../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10362 bytes
cashier/src/main/res/navigation/nav_graph.xml | 73 +++
cashier/src/main/res/values-night/colors.xml | 20 +
cashier/src/main/res/values/colors.xml | 10 +
cashier/src/main/res/values/dimens.xml | 3 +
.../src/main/res/values/ic_launcher_background.xml | 4 +
cashier/src/main/res/values/strings.xml | 39 ++
cashier/src/main/res/values/styles.xml | 28 +
cashier/src/main/res/xml/backup_descriptor.xml | 19 +
gradle.properties | 21 +
gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes
gradle/wrapper/gradle-wrapper.properties | 6 +
gradlew | 172 ++++++
gradlew.bat | 84 +++
merchant-terminal/.gitignore | 1 +
merchant-terminal/.gitlab-ci.yml | 36 ++
merchant-terminal/build.gradle | 76 +++
merchant-terminal/proguard-rules.pro | 21 +
merchant-terminal/src/main/AndroidManifest.xml | 56 ++
merchant-terminal/src/main/ic_taler_logo-web.png | Bin 0 -> 25951 bytes
.../src/main/java/net/taler/merchantpos/Amount.kt | 48 ++
.../java/net/taler/merchantpos/MainActivity.kt | 123 ++++
.../java/net/taler/merchantpos/MainViewModel.kt | 51 ++
.../main/java/net/taler/merchantpos/NfcManager.kt | 233 +++++++
.../java/net/taler/merchantpos/QrCodeManager.kt | 42 ++
.../src/main/java/net/taler/merchantpos/Utils.kt | 155 +++++
.../merchantpos/config/ConfigFetcherFragment.kt | 66 ++
.../net/taler/merchantpos/config/ConfigManager.kt | 181 ++++++
.../net/taler/merchantpos/config/MerchantConfig.kt | 47 ++
.../merchantpos/config/MerchantConfigFragment.kt | 165 +++++
.../taler/merchantpos/config/MerchantRequest.kt | 41 ++
.../taler/merchantpos/history/HistoryManager.kt | 106 ++++
.../merchantpos/history/MerchantHistoryFragment.kt | 160 +++++
.../taler/merchantpos/history/RefundFragment.kt | 99 +++
.../net/taler/merchantpos/history/RefundManager.kt | 111 ++++
.../taler/merchantpos/history/RefundUriFragment.kt | 65 ++
.../taler/merchantpos/order/CategoriesFragment.kt | 106 ++++
.../net/taler/merchantpos/order/Definitions.kt | 205 +++++++
.../java/net/taler/merchantpos/order/LiveOrder.kt | 109 ++++
.../net/taler/merchantpos/order/OrderFragment.kt | 115 ++++
.../net/taler/merchantpos/order/OrderManager.kt | 196 ++++++
.../taler/merchantpos/order/OrderStateFragment.kt | 213 +++++++
.../taler/merchantpos/order/ProductsFragment.kt | 111 ++++
.../java/net/taler/merchantpos/payment/Payment.kt | 29 +
.../taler/merchantpos/payment/PaymentManager.kt | 154 +++++
.../merchantpos/payment/PaymentSuccessFragment.kt | 44 ++
.../merchantpos/payment/ProcessPaymentFragment.kt | 96 +++
.../src/main/res/color/button_bottom.xml | 5 +
.../src/main/res/drawable/ic_cash_refund.xml | 9 +
.../src/main/res/drawable/ic_check_circle.xml | 10 +
.../main/res/drawable/ic_history_black_24dp.xml | 9 +
.../main/res/drawable/ic_launcher_background.xml | 74 +++
.../src/main/res/drawable/ic_menu_manage.xml | 9 +
.../src/main/res/drawable/ic_move_money_24dp.xml | 9 +
.../main/res/drawable/selectable_background.xml | 5 +
.../src/main/res/drawable/side_nav_bar.xml | 9 +
.../src/main/res/layout/activity_main.xml | 42 ++
.../src/main/res/layout/app_bar_main.xml | 53 ++
.../src/main/res/layout/fragment_categories.xml | 46 ++
.../main/res/layout/fragment_config_fetcher.xml | 45 ++
.../main/res/layout/fragment_merchant_config.xml | 152 +++++
.../main/res/layout/fragment_merchant_history.xml | 29 +
.../src/main/res/layout/fragment_order.xml | 138 +++++
.../src/main/res/layout/fragment_order_state.xml | 52 ++
.../main/res/layout/fragment_payment_success.xml | 78 +++
.../main/res/layout/fragment_process_payment.xml | 110 ++++
.../src/main/res/layout/fragment_products.xml | 44 ++
.../src/main/res/layout/fragment_refund.xml | 122 ++++
.../src/main/res/layout/fragment_refund_uri.xml | 93 +++
.../src/main/res/layout/list_item_category.xml | 33 +
.../src/main/res/layout/list_item_history.xml | 97 +++
.../src/main/res/layout/list_item_order.xml | 61 ++
.../src/main/res/layout/list_item_product.xml | 56 ++
.../src/main/res/layout/nav_header_main.xml | 55 ++
.../src/main/res/menu/activity_main_drawer.xml | 36 ++
.../main/res/mipmap-anydpi-v26/ic_taler_logo.xml | 5 +
.../res/mipmap-anydpi-v26/ic_taler_logo_round.xml | 5 +
.../res/mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 4307 bytes
.../src/main/res/mipmap-hdpi/ic_taler_logo.png | Bin 0 -> 2347 bytes
.../main/res/mipmap-hdpi/ic_taler_logo_round.png | Bin 0 -> 3638 bytes
.../res/mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2625 bytes
.../src/main/res/mipmap-mdpi/ic_taler_logo.png | Bin 0 -> 1532 bytes
.../main/res/mipmap-mdpi/ic_taler_logo_round.png | Bin 0 -> 2240 bytes
.../res/mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 6077 bytes
.../src/main/res/mipmap-xhdpi/ic_taler_logo.png | Bin 0 -> 3336 bytes
.../main/res/mipmap-xhdpi/ic_taler_logo_round.png | Bin 0 -> 5273 bytes
.../res/mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 10228 bytes
.../src/main/res/mipmap-xxhdpi/ic_taler_logo.png | Bin 0 -> 5422 bytes
.../main/res/mipmap-xxhdpi/ic_taler_logo_round.png | Bin 0 -> 8454 bytes
.../res/mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 14083 bytes
.../src/main/res/mipmap-xxxhdpi/ic_taler_logo.png | Bin 0 -> 7786 bytes
.../res/mipmap-xxxhdpi/ic_taler_logo_round.png | Bin 0 -> 12377 bytes
.../src/main/res/navigation/nav_graph.xml | 137 +++++
.../src/main/res/values-night/colors.xml | 5 +
merchant-terminal/src/main/res/values/colors.xml | 14 +
merchant-terminal/src/main/res/values/dimens.xml | 6 +
merchant-terminal/src/main/res/values/strings.xml | 68 +++
merchant-terminal/src/main/res/values/styles.xml | 21 +
.../src/main/res/xml/backup_descriptor.xml | 4 +
.../taler/merchantpos/order/OrderManagerTest.kt | 151 +++++
nightly-stats.patch | 38 ++
settings.gradle | 17 +
wallet/.gitignore | 2 +
wallet/.gitlab-ci.yml | 42 ++
wallet/README.md | 40 ++
wallet/build.gradle | 81 +++
wallet/proguard-rules.pro | 21 +
.../net/taler/wallet/ExampleInstrumentedTest.kt | 38 ++
wallet/src/main/AndroidManifest.xml | 81 +++
wallet/src/main/ic_launcher-web.png | Bin 0 -> 14129 bytes
wallet/src/main/java/net/taler/wallet/Amount.kt | 141 +++++
.../main/java/net/taler/wallet/BalanceFragment.kt | 198 ++++++
.../net/taler/wallet/HostCardEmulatorService.kt | 187 ++++++
.../src/main/java/net/taler/wallet/MainActivity.kt | 209 +++++++
wallet/src/main/java/net/taler/wallet/Settings.kt | 140 +++++
wallet/src/main/java/net/taler/wallet/Utils.kt | 40 ++
.../main/java/net/taler/wallet/WalletViewModel.kt | 124 ++++
.../net/taler/wallet/backend/WalletBackendApi.kt | 141 +++++
.../taler/wallet/backend/WalletBackendService.kt | 239 ++++++++
.../main/java/net/taler/wallet/crypto/Encoding.kt | 134 ++++
.../java/net/taler/wallet/history/HistoryEvent.kt | 452 ++++++++++++++
.../net/taler/wallet/history/HistoryManager.kt | 71 +++
.../net/taler/wallet/history/JsonDialogFragment.kt | 50 ++
.../net/taler/wallet/history/ReserveTransaction.kt | 58 ++
.../taler/wallet/history/WalletHistoryAdapter.kt | 243 ++++++++
.../taler/wallet/history/WalletHistoryFragment.kt | 115 ++++
.../taler/wallet/payment/AlreadyPaidFragment.kt | 47 ++
.../java/net/taler/wallet/payment/ContractTerms.kt | 56 ++
.../net/taler/wallet/payment/PaymentManager.kt | 160 +++++
.../wallet/payment/PaymentSuccessfulFragment.kt | 49 ++
.../net/taler/wallet/payment/ProductAdapter.kt | 92 +++
.../taler/wallet/payment/ProductImageFragment.kt | 52 ++
.../taler/wallet/payment/PromptPaymentFragment.kt | 168 +++++
.../wallet/pending/PendingOperationsFragment.kt | 180 ++++++
.../wallet/pending/PendingOperationsManager.kt | 64 ++
.../net/taler/wallet/withdraw/ErrorFragment.kt | 64 ++
.../wallet/withdraw/PromptWithdrawFragment.kt | 109 ++++
.../wallet/withdraw/ReviewExchangeTosFragment.kt | 80 +++
.../net/taler/wallet/withdraw/WithdrawManager.kt | 209 +++++++
.../wallet/withdraw/WithdrawSuccessfulFragment.kt | 44 ++
.../main/res/drawable/history_payment_aborted.xml | 25 +
wallet/src/main/res/drawable/history_refresh.xml | 28 +
wallet/src/main/res/drawable/history_refund.xml | 25 +
.../src/main/res/drawable/history_tip_accepted.xml | 25 +
.../src/main/res/drawable/history_tip_declined.xml | 25 +
wallet/src/main/res/drawable/history_withdrawn.xml | 25 +
.../src/main/res/drawable/ic_account_balance.xml | 25 +
.../res/drawable/ic_account_balance_wallet.xml | 9 +
wallet/src/main/res/drawable/ic_add_circle.xml | 25 +
wallet/src/main/res/drawable/ic_cancel.xml | 25 +
.../src/main/res/drawable/ic_cash_usd_outline.xml | 25 +
wallet/src/main/res/drawable/ic_check_circle.xml | 26 +
wallet/src/main/res/drawable/ic_directions.xml | 25 +
wallet/src/main/res/drawable/ic_error.xml | 25 +
.../main/res/drawable/ic_history_black_24dp.xml | 25 +
.../src/main/res/drawable/ic_home_black_24dp.xml | 25 +
.../main/res/drawable/ic_launcher_foreground.xml | 68 +++
wallet/src/main/res/drawable/ic_scan_qr.xml | 10 +
wallet/src/main/res/drawable/ic_settings.xml | 9 +
wallet/src/main/res/drawable/ic_sync.xml | 9 +
wallet/src/main/res/drawable/pending_border.xml | 37 ++
wallet/src/main/res/drawable/side_nav_bar.xml | 24 +
.../main/res/layout-w550dp/payment_bottom_bar.xml | 123 ++++
wallet/src/main/res/layout/activity_main.xml | 40 ++
wallet/src/main/res/layout/app_bar_main.xml | 75 +++
.../src/main/res/layout/fragment_already_paid.xml | 52 ++
wallet/src/main/res/layout/fragment_error.xml | 97 +++
wallet/src/main/res/layout/fragment_json.xml | 41 ++
.../res/layout/fragment_payment_successful.xml | 63 ++
.../res/layout/fragment_pending_operations.xml | 34 ++
.../src/main/res/layout/fragment_product_image.xml | 24 +
.../main/res/layout/fragment_prompt_payment.xml | 44 ++
.../main/res/layout/fragment_prompt_withdraw.xml | 171 ++++++
.../res/layout/fragment_review_exchange_tos.xml | 105 ++++
wallet/src/main/res/layout/fragment_settings.xml | 104 ++++
.../src/main/res/layout/fragment_show_balance.xml | 91 +++
.../src/main/res/layout/fragment_show_history.xml | 47 ++
.../res/layout/fragment_withdraw_successful.xml | 63 ++
wallet/src/main/res/layout/history_payment.xml | 87 +++
wallet/src/main/res/layout/history_receive.xml | 105 ++++
wallet/src/main/res/layout/history_row.xml | 73 +++
wallet/src/main/res/layout/list_item_balance.xml | 77 +++
wallet/src/main/res/layout/list_item_product.xml | 75 +++
.../main/res/layout/list_item_product_single.xml | 78 +++
wallet/src/main/res/layout/nav_header_main.xml | 73 +++
wallet/src/main/res/layout/payment_bottom_bar.xml | 123 ++++
wallet/src/main/res/layout/payment_details.xml | 119 ++++
wallet/src/main/res/layout/pending_row.xml | 48 ++
wallet/src/main/res/menu/activity_main_drawer.xml | 41 ++
wallet/src/main/res/menu/balance.xml | 28 +
wallet/src/main/res/menu/history.xml | 31 +
wallet/src/main/res/menu/pending_operations.xml | 24 +
.../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 21 +
.../res/mipmap-anydpi-v26/ic_launcher_round.xml | 21 +
wallet/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1611 bytes
.../src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2898 bytes
wallet/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1101 bytes
.../src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1836 bytes
wallet/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2314 bytes
.../main/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 4158 bytes
wallet/src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3405 bytes
.../main/res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 6328 bytes
wallet/src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 4592 bytes
.../main/res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 8828 bytes
wallet/src/main/res/navigation/nav_graph.xml | 125 ++++
wallet/src/main/res/values/colors.xml | 25 +
wallet/src/main/res/values/dimens.xml | 24 +
.../src/main/res/values/ic_launcher_background.xml | 20 +
wallet/src/main/res/values/strings.xml | 105 ++++
wallet/src/main/res/values/styles.xml | 46 ++
wallet/src/main/res/xml/apduservice.xml | 25 +
wallet/src/main/res/xml/backup_descriptor.xml | 20 +
.../test/java/net/taler/wallet/ExampleUnitTest.kt | 33 +
.../net/taler/wallet/crypto/Base32CrockfordTest.kt | 35 ++
.../net/taler/wallet/history/HistoryEventTest.kt | 459 ++++++++++++++
.../taler/wallet/history/ReserveTransactionTest.kt | 52 ++
274 files changed, 17431 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..52aa44f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/misc.xml
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+/.idea/runConfigurations.xml
+/.idea/vcs.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+/akono/akono.aar
+/*/release/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..e51d33b
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,20 @@
+image: registry.gitlab.com/fdroid/ci-images-client:latest
+
+cache:
+ paths:
+ - .gradle/wrapper
+ - .gradle/caches
+
+stages:
+ - test
+ - deploy
+
+include:
+ - local: 'cashier/.gitlab-ci.yml'
+ - local: 'merchant-terminal/.gitlab-ci.yml'
+ - local: 'wallet/.gitlab-ci.yml'
+
+after_script:
+ # this file changes every time but should not be cached
+ - rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
+ - rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..26724fb
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,124 @@
+<component name="ProjectCodeStyleConfiguration">
+ <code_scheme name="Project" version="173">
+ <JetCodeStyleSettings>
+ <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
+ <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS"
value="2147483647" />
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ </JetCodeStyleSettings>
+ <codeStyleSettings language="XML">
+ <indentOptions>
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ </indentOptions>
+ <arrangement>
+ <rules>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:android</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:id</NAME>
+ <XML_ATTRIBUTE />
+
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:name</NAME>
+ <XML_ATTRIBUTE />
+
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>name</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>style</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>ANDROID_ATTRIBUTE_ORDER</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>.*</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ </rules>
+ </arrangement>
+ </codeStyleSettings>
+ <codeStyleSettings language="kotlin">
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ </codeStyleSettings>
+ </code_scheme>
+</component>
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml
b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+ <state>
+ <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+ </state>
+</component>
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..40ed937
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <wildcardResourcePatterns>
+ <entry name="!?*.java" />
+ <entry name="!?*.form" />
+ <entry name="!?*.class" />
+ <entry name="!?*.groovy" />
+ <entry name="!?*.scala" />
+ <entry name="!?*.flex" />
+ <entry name="!?*.kt" />
+ <entry name="!?*.clj" />
+ </wildcardResourcePatterns>
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/copyright/Taler.xml b/.idea/copyright/Taler.xml
new file mode 100644
index 0000000..96abfa5
--- /dev/null
+++ b/.idea/copyright/Taler.xml
@@ -0,0 +1,7 @@
+<component name="CopyrightManager">
+ <copyright>
+ <option name="keyword" value="(Copyright|Public License)" />
+ <option name="notice" value="This file is part of GNU Taler (C)
&#36;today.year 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 MERCH [...]
+ <option name="myName" value="Taler" />
+ </copyright>
+</component>
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml
b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..31766eb
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+<component name="CopyrightManager">
+ <settings default="Taler">
+ <module2copyright>
+ <element module="Copyright Files" copyright="Taler" />
+ </module2copyright>
+ </settings>
+</component>
\ No newline at end of file
diff --git a/.idea/dictionaries/user.xml b/.idea/dictionaries/user.xml
new file mode 100644
index 0000000..4693d75
--- /dev/null
+++ b/.idea/dictionaries/user.xml
@@ -0,0 +1,14 @@
+<component name="ProjectDictionaryState">
+ <dictionary name="user">
+ <words>
+ <w>abcdef</w>
+ <w>aiddescription</w>
+ <w>akono</w>
+ <w>apdu</w>
+ <w>servicedesc</w>
+ <w>snackbar</w>
+ <w>taler</w>
+ <w>testkudos</w>
+ </words>
+ </dictionary>
+</component>
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..97626ba
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="Encoding">
+ <file url="PROJECT" charset="UTF-8" />
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..65dee6e
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="GradleMigrationSettings" migrationVersion="1" />
+ <component name="GradleSettings">
+ <option name="linkedExternalProjectsSettings">
+ <GradleProjectSettings>
+ <option name="testRunner" value="PLATFORM" />
+ <option name="distributionType" value="DEFAULT_WRAPPED" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
+ <option name="modules">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ <option value="$PROJECT_DIR$/akono" />
+ <option value="$PROJECT_DIR$/cashier" />
+ <option value="$PROJECT_DIR$/merchant-terminal" />
+ <option value="$PROJECT_DIR$/wallet" />
+ </set>
+ </option>
+ <option name="resolveModulePerSourceSet" value="false" />
+ </GradleProjectSettings>
+ </option>
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/scopes/Copyright_Files.xml b/.idea/scopes/Copyright_Files.xml
new file mode 100644
index 0000000..d0ebcb8
--- /dev/null
+++ b/.idea/scopes/Copyright_Files.xml
@@ -0,0 +1,3 @@
+<component name="DependencyValidationManager">
+ <scope name="Copyright Files"
pattern="file[cashier]:src/main/java//*||file[cashier]:src/main/res/layout//*||file[cashier]:src/main/res/layout-w550dp//*||file[merchant-terminal]:src/main/java//*||file[merchant-terminal]:src/main/res/layout//*||file[wallet]:src/main/java//*||file[wallet]:src/main/res/layout//*||file[wallet]:src/main/res/layout-w550dp//*"
/>
+</component>
\ No newline at end of file
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program 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 of the License, or
+ (at your option) any later version.
+
+ This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/akono/.gitignore b/akono/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/akono/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/akono/build.gradle b/akono/build.gradle
new file mode 100644
index 0000000..45fbf89
--- /dev/null
+++ b/akono/build.gradle
@@ -0,0 +1,18 @@
+/*
+ * 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/>
+ */
+
+configurations.maybeCreate("default")
+artifacts.add("default", file('akono.aar'))
\ No newline at end of file
diff --git a/artwork/ic_bottom_left.svg b/artwork/ic_bottom_left.svg
new file mode 100644
index 0000000..c3aa7e4
--- /dev/null
+++ b/artwork/ic_bottom_left.svg
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ height="24"
+ version="1.1"
+ viewBox="0 0 24 24"
+ width="24"
+ id="svg4"
+ sodipodi:docname="ic_bottom_left.svg"
+ inkscape:version="0.92.4 (unknown)">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs8" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="982"
+ id="namedview6"
+ showgrid="false"
+ inkscape:zoom="9.8333333"
+ inkscape:cx="-10.728814"
+ inkscape:cy="12"
+ inkscape:window-x="1920"
+ inkscape:window-y="72"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg4" />
+ <path
+ d="M 20,5.41 18.59,4 7,15.59 V 9 H 5 V 19 H 15 V 17 H 8.41"
+ id="path2"
+ inkscape:connector-curvature="0"
+ style="fill:#000000" />
+</svg>
diff --git a/artwork/ic_bottom_right.svg b/artwork/ic_bottom_right.svg
new file mode 100644
index 0000000..26869ba
--- /dev/null
+++ b/artwork/ic_bottom_right.svg
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ height="24"
+ version="1.1"
+ viewBox="0 0 24 24"
+ width="24"
+ id="svg4"
+ sodipodi:docname="ic_bottom_right.svg"
+ inkscape:version="0.92.4 (unknown)">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs8" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="982"
+ id="namedview6"
+ showgrid="false"
+ inkscape:zoom="9.8333333"
+ inkscape:cx="-10.728814"
+ inkscape:cy="12"
+ inkscape:window-x="1920"
+ inkscape:window-y="72"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg4" />
+ <path
+ d="M 5,5.41 6.41,4 18,15.59 V 9 h 2 V 19 H 10 v -2 h 6.59"
+ id="path2"
+ inkscape:connector-curvature="0"
+ style="fill:#000000" />
+</svg>
diff --git a/artwork/ic_launcher_cashier.svg b/artwork/ic_launcher_cashier.svg
new file mode 100644
index 0000000..4868fe4
--- /dev/null
+++ b/artwork/ic_launcher_cashier.svg
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ id="svg4"
+ sodipodi:docname="ic_launcher.svg"
+ inkscape:version="0.92.4 (unknown)">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs8" />
+ <sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="982"
+ id="namedview6"
+ showgrid="false"
+ inkscape:zoom="9.8333335"
+ inkscape:cx="-21.143993"
+ inkscape:cy="20.1778"
+ inkscape:window-x="1920"
+ inkscape:window-y="72"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg4" />
+ <path
+ d="M 6 3 L 6 6 L 9 6 L 9 7 L 6.25 7 C 5.05 7 4.0507812 8 4.0507812 9 L
3.5 16 L 20.5 16 L 20 9 C 19.8 8 18.800781 7 17.800781 7 L 11 7 L 11 6 L 14 6 L
14 3 L 6 3 z M 7 4 L 13 4 L 13 5 L 7 5 L 7 4 z M 6 9 L 8 9 L 8 10 L 6 10 L 6 9
z M 9 9 L 11 9 L 11 10 L 9 10 L 9 9 z M 13 9 L 18 9 L 18 11 L 13 11 L 13 9 z M
6 11 L 8 11 L 8 12 L 6 12 L 6 11 z M 9 11 L 11 11 L 11 12 L 9 12 L 9 11 z M 6
13 L 8 13 L 8 14 L 6 14 L 6 13 z M 9 13 L 11 13 L 11 14 L 9 14 L 9 13 z M 2 17
L 2 21 L 22 21 L 22 1 [...]
+ id="path2"
+ style="fill:#f9f9f9;fill-opacity:1" />
+</svg>
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..f286dfe
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,24 @@
+buildscript {
+ ext.kotlin_version = '1.3.70'
+ ext.nav_version = "2.2.1"
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.6.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath
"androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/cashier/.gitignore b/cashier/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/cashier/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/cashier/.gitlab-ci.yml b/cashier/.gitlab-ci.yml
new file mode 100644
index 0000000..f8cc7f3
--- /dev/null
+++ b/cashier/.gitlab-ci.yml
@@ -0,0 +1,35 @@
+image: registry.gitlab.com/fdroid/ci-images-client:latest
+
+cashier_test:
+ stage: test
+ only:
+ changes:
+ - "cashier"
+ script: ./gradlew :cashier:lint :cashier:assembleRelease
+
+cashier_deploy_nightly:
+ stage: deploy
+ only:
+ refs:
+ - master
+ changes:
+ - "cashier"
+ script:
+ # Ensure that key exists
+ - test -z "$DEBUG_KEYSTORE" && exit 0
+ # Rename nightly app
+ - sed -i
+ 's,<string name="app_name">.*</string>,<string name="app_name">Cashier
Nightly</string>,'
+ cashier/src/main/res/values*/strings.xml
+ # Set time-based version code
+ - export versionCode=$(date '+%s')
+ - sed -i "s,^\(\s*versionCode\) *[0-9].*,\1 $versionCode,"
cashier/build.gradle
+ # Set nightly application ID
+ - sed -i "s,^\(\s*applicationId\) \"*[a-z\.].*\",\1
\"net.taler.cashier.nightly\"," cashier/build.gradle
+ # Build the APK
+ - ./gradlew :cashier:assembleDebug
+ # START only needed while patch not accepted/released upstream
+ - apt update && apt install patch
+ - patch /usr/lib/python3/dist-packages/fdroidserver/nightly.py
nightly-stats.patch
+ # END
+ - CI_PROJECT_URL="https://gitlab.com/gnu-taler/fdroid-repo"
CI_PROJECT_PATH="gnu-taler/fdroid-repo" fdroid nightly -v
diff --git a/cashier/README.md b/cashier/README.md
new file mode 100644
index 0000000..e884f25
--- /dev/null
+++ b/cashier/README.md
@@ -0,0 +1,10 @@
+# GNU Taler Cashier App
+
+The purpose of this app is to enable people (a cashier) to take cash and give
out e-cash.
+
+## Building
+
+You can import the project into Android Studio
+or build it with Gradle on the command line:
+
+ $ ./gradlew build
diff --git a/cashier/build.gradle b/cashier/build.gradle
new file mode 100644
index 0000000..5915f8a
--- /dev/null
+++ b/cashier/build.gradle
@@ -0,0 +1,72 @@
+/*
+ * 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/>
+ */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'androidx.navigation.safeargs.kotlin'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+
+ defaultConfig {
+ applicationId "net.taler.cashier"
+ minSdkVersion 23
+ targetSdkVersion 29
+ versionCode 1
+ versionName "0.1"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles
getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.2.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.security:security-crypto:1.0.0-alpha02'
+ implementation 'com.google.android.material:material:1.1.0'
+
+ implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
+ implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
+
+ // ViewModel and LiveData
+ def lifecycle_version = "2.2.0"
+ implementation
"androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
+
+ // QR codes
+ implementation 'com.google.zxing:core:3.4.0'
+
+ implementation "com.squareup.okhttp3:okhttp:3.12.6"
+
+ testImplementation 'junit:junit:4.13'
+
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+}
diff --git a/cashier/lint.xml b/cashier/lint.xml
new file mode 100644
index 0000000..164e244
--- /dev/null
+++ b/cashier/lint.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+
+</lint>
diff --git a/cashier/proguard-rules.pro b/cashier/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/cashier/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/cashier/src/main/AndroidManifest.xml
b/cashier/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..345c9a1
--- /dev/null
+++ b/cashier/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="net.taler.cashier">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.NFC" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:fullBackupContent="@xml/backup_descriptor"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme"
+ android:roundIcon="@mipmap/ic_launcher"
+ tools:ignore="GoogleAppIndexingWarning">
+
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme.NoActionBar">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/cashier/src/main/ic_launcher-web.png
b/cashier/src/main/ic_launcher-web.png
new file mode 100644
index 0000000..04a58c6
Binary files /dev/null and b/cashier/src/main/ic_launcher-web.png differ
diff --git a/cashier/src/main/java/net/taler/cashier/Amount.kt
b/cashier/src/main/java/net/taler/cashier/Amount.kt
new file mode 100644
index 0000000..2c237c8
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/Amount.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.cashier
+
+data class Amount(val currency: String, val amount: String) {
+
+ companion object {
+
+ private val SIGNED_REGEX = Regex("""([+\-])(\w+):([0-9.]+)""")
+
+ @Suppress("unused")
+ fun fromString(strAmount: String): Amount {
+ val components = strAmount.split(":")
+ return Amount(components[0], components[1])
+ }
+
+ fun fromStringSigned(strAmount: String): Amount? {
+ val groups = SIGNED_REGEX.matchEntire(strAmount)?.groupValues ?:
emptyList()
+ if (groups.size < 4) return null
+ var amount = groups[3].toDoubleOrNull() ?: return null
+ if (groups[1] == "-") amount *= -1
+ val currency = groups[2]
+ val amountStr = amount.toString()
+ // only display as many digits as required to precisely render the
balance
+ return Amount(currency, amountStr.removeSuffix(".0"))
+ }
+ }
+
+ override fun toString() = "$amount $currency"
+
+}
diff --git a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
new file mode 100644
index 0000000..b3a0221
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.cashier
+
+import android.annotation.SuppressLint
+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.View.VISIBLE
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_balance.*
+import
net.taler.cashier.BalanceFragmentDirections.Companion.actionBalanceFragmentToTransactionFragment
+import net.taler.cashier.withdraw.LastTransaction
+import net.taler.cashier.withdraw.WithdrawStatus
+
+sealed class BalanceResult {
+ object Error : BalanceResult()
+ object Offline : BalanceResult()
+ class Success(val amount: Amount) : BalanceResult()
+}
+
+class BalanceFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { viewModel.withdrawManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ setHasOptionsMenu(true)
+ return inflater.inflate(R.layout.fragment_balance, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ withdrawManager.lastTransaction.observe(viewLifecycleOwner, Observer {
lastTransaction ->
+ onLastTransaction(lastTransaction)
+ })
+ viewModel.balance.observe(viewLifecycleOwner, Observer { result ->
+ when (result) {
+ is BalanceResult.Success -> onBalanceUpdated(result.amount)
+ else -> onBalanceUpdated(null, result is BalanceResult.Offline)
+ }
+ })
+ button5.setOnClickListener { onAmountButtonPressed(5) }
+ button10.setOnClickListener { onAmountButtonPressed(10) }
+ button20.setOnClickListener { onAmountButtonPressed(20) }
+ button50.setOnClickListener { onAmountButtonPressed(50) }
+
+ if (savedInstanceState != null) {
+
amountView.editText!!.setText(savedInstanceState.getCharSequence("amountView"))
+ }
+ amountView.editText!!.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_GO) {
+ onAmountConfirmed(getAmountFromView())
+ true
+ } else false
+ }
+ viewModel.currency.observe(viewLifecycleOwner, Observer { currency ->
+ currencyView.text = currency
+ })
+ confirmWithdrawalButton.setOnClickListener {
onAmountConfirmed(getAmountFromView()) }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ // update balance if there's a config
+ if (viewModel.hasConfig()) {
+ viewModel.getBalance()
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ // for some reason automatic restore isn't working at the moment!?
+ amountView?.editText?.text.let {
+ outState.putCharSequence("amountView", it)
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.balance, menu)
+ super.onCreateOptionsMenu(menu, inflater)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.action_reconfigure -> {
+ findNavController().navigate(viewModel.configDestination)
+ true
+ }
+ R.id.action_lock -> {
+ viewModel.lock()
+ findNavController().navigate(viewModel.configDestination)
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ private fun onBalanceUpdated(amount: Amount?, isOffline: Boolean = false) {
+ val uiList = listOf(
+ introView,
+ button5, button10, button20, button50,
+ amountView, currencyView, confirmWithdrawalButton
+ )
+ if (amount == null) {
+ balanceView.text =
+ getString(if (isOffline) R.string.balance_offline else
R.string.balance_error)
+ uiList.forEach { it.fadeOut() }
+ } else {
+ @SuppressLint("SetTextI18n")
+ balanceView.text = "${amount.amount} ${amount.currency}"
+ uiList.forEach { it.fadeIn() }
+ }
+ progressBar.fadeOut()
+ }
+
+ private fun onAmountButtonPressed(amount: Int) {
+ amountView.editText!!.setText(amount.toString())
+ amountView.error = null
+ }
+
+ private fun getAmountFromView(): Int {
+ val str = amountView.editText!!.text.toString()
+ if (str.isBlank()) return 0
+ return Integer.parseInt(str)
+ }
+
+ private fun onAmountConfirmed(amount: Int) {
+ if (amount <= 0) {
+ amountView.error = getString(R.string.withdraw_error_zero)
+ } else if (!withdrawManager.hasSufficientBalance(amount)) {
+ amountView.error =
getString(R.string.withdraw_error_insufficient_balance)
+ } else {
+ amountView.error = null
+ withdrawManager.withdraw(amount)
+ actionBalanceFragmentToTransactionFragment().let {
+ findNavController().navigate(it)
+ }
+ }
+ }
+
+ private fun onLastTransaction(lastTransaction: LastTransaction?) {
+ val status = lastTransaction?.withdrawStatus
+ val text = when (status) {
+ is WithdrawStatus.Success -> getString(
+ R.string.transaction_last_success,
lastTransaction.withdrawAmount
+ )
+ is WithdrawStatus.Aborted ->
getString(R.string.transaction_last_aborted)
+ else -> getString(R.string.transaction_last_error)
+ }
+ lastTransactionView.text = text
+ val drawable = if (status == WithdrawStatus.Success)
+ R.drawable.ic_check_circle
+ else
+ R.drawable.ic_error
+
lastTransactionView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable,
0, 0, 0)
+ lastTransactionView.visibility = VISIBLE
+ }
+
+}
diff --git a/cashier/src/main/java/net/taler/cashier/ConfigFragment.kt
b/cashier/src/main/java/net/taler/cashier/ConfigFragment.kt
new file mode 100644
index 0000000..b9a97e5
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/ConfigFragment.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.cashier
+
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import androidx.core.content.ContextCompat.getSystemService
+import androidx.core.text.HtmlCompat
+import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
+import kotlinx.android.synthetic.main.fragment_config.*
+
+private const val URL_BANK_TEST = "https://bank.test.taler.net"
+private const val URL_BANK_TEST_REGISTER = "$URL_BANK_TEST/accounts/register"
+
+class ConfigFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_config, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ if (savedInstanceState == null) {
+ if (viewModel.config.bankUrl.isBlank()) {
+ urlView.editText!!.setText(URL_BANK_TEST)
+ } else {
+ urlView.editText!!.setText(viewModel.config.bankUrl)
+ }
+ usernameView.editText!!.setText(viewModel.config.username)
+ passwordView.editText!!.setText(viewModel.config.password)
+ } else {
+
urlView.editText!!.setText(savedInstanceState.getCharSequence("urlView"))
+
usernameView.editText!!.setText(savedInstanceState.getCharSequence("usernameView"))
+
passwordView.editText!!.setText(savedInstanceState.getCharSequence("passwordView"))
+ }
+ saveButton.setOnClickListener {
+ val config = Config(
+ bankUrl = urlView.editText!!.text.toString(),
+ username = usernameView.editText!!.text.toString(),
+ password = passwordView.editText!!.text.toString()
+ )
+ if (checkConfig(config)) {
+ // show progress
+ saveButton.visibility = INVISIBLE
+ progressBar.visibility = VISIBLE
+ // kick off check and observe result
+ viewModel.checkAndSaveConfig(config)
+ viewModel.configResult.observe(viewLifecycleOwner,
onConfigResult)
+ // hide keyboard
+ val inputMethodManager =
+ getSystemService(requireContext(),
InputMethodManager::class.java)!!
+ inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
+ }
+ }
+ demoView.text = HtmlCompat.fromHtml(
+ getString(R.string.config_demo_hint, URL_BANK_TEST_REGISTER),
FROM_HTML_MODE_LEGACY
+ )
+ demoView.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ // focus on password if it is the only missing value (like after
locking)
+ if (urlView.editText!!.text.isNotBlank()
+ && usernameView.editText!!.text.isNotBlank()
+ && passwordView.editText!!.text.isBlank()) {
+ passwordView.editText!!.requestFocus()
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ // for some reason automatic restore isn't working at the moment!?
+ outState.putCharSequence("urlView", urlView.editText?.text)
+ outState.putCharSequence("usernameView", usernameView.editText?.text)
+ outState.putCharSequence("passwordView", passwordView.editText?.text)
+ }
+
+ private fun checkConfig(config: Config): Boolean {
+ if (!config.bankUrl.startsWith("https://")) {
+ urlView.error = getString(R.string.config_bank_url_error)
+ urlView.requestFocus()
+ return false
+ }
+ if (config.username.isBlank()) {
+ usernameView.error = getString(R.string.config_username_error)
+ usernameView.requestFocus()
+ return false
+ }
+ urlView.isErrorEnabled = false
+ return true
+ }
+
+ private val onConfigResult = Observer<ConfigResult> { result ->
+ if (result == null) return@Observer
+ if (result.success) {
+ val action =
ConfigFragmentDirections.actionConfigFragmentToBalanceFragment()
+ findNavController().navigate(action)
+ } else {
+ val res = if (result.authError) R.string.config_error_auth else
R.string.config_error
+ Snackbar.make(view!!, res, LENGTH_LONG).show()
+ }
+ saveButton.visibility = VISIBLE
+ progressBar.visibility = INVISIBLE
+ viewModel.configResult.removeObservers(viewLifecycleOwner)
+ }
+
+}
diff --git a/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
new file mode 100644
index 0000000..06b06db
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.cashier
+
+import android.util.Log
+import androidx.annotation.WorkerThread
+import okhttp3.Credentials
+import okhttp3.MediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import org.json.JSONObject
+
+object HttpHelper {
+
+ private val TAG = HttpHelper::class.java.simpleName
+ private const val MIME_TYPE_JSON = "application/json"
+
+ @WorkerThread
+ fun makeJsonGetRequest(url: String, config: Config): HttpJsonResult {
+ val request = Request.Builder()
+ .addHeader("Accept", MIME_TYPE_JSON)
+ .url(url)
+ .get()
+ .build()
+ val response = try {
+ getHttpClient(config.username, config.password)
+ .newCall(request)
+ .execute()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error retrieving $url", e)
+ return HttpJsonResult.Error(500)
+ }
+ return if (response.code() == 200 && response.body() != null) {
+ val jsonObject = JSONObject(response.body()!!.string())
+ HttpJsonResult.Success(jsonObject)
+ } else {
+ Log.e(TAG, "Received status ${response.code()} from $url expected
200")
+ HttpJsonResult.Error(response.code())
+ }
+ }
+
+ private val MEDIA_TYPE_JSON = MediaType.parse("$MIME_TYPE_JSON;
charset=utf-8")
+
+ @WorkerThread
+ fun makeJsonPostRequest(url: String, body: String, config: Config):
HttpJsonResult {
+ val request = Request.Builder()
+ .addHeader("Accept", MIME_TYPE_JSON)
+ .url(url)
+ .post(RequestBody.create(MEDIA_TYPE_JSON, body))
+ .build()
+ val response = try {
+ getHttpClient(config.username, config.password)
+ .newCall(request)
+ .execute()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error retrieving $url", e)
+ return HttpJsonResult.Error(500)
+ }
+ return if (response.code() == 200 && response.body() != null) {
+ val jsonObject = JSONObject(response.body()!!.string())
+ HttpJsonResult.Success(jsonObject)
+ } else {
+ Log.e(TAG, "Received status ${response.code()} from $url expected
200")
+ HttpJsonResult.Error(response.code())
+ }
+ }
+
+ private fun getHttpClient(username: String, password: String) =
+ OkHttpClient.Builder().authenticator { _, response ->
+ val credential = Credentials.basic(username, password)
+ if (credential == response.request().header("Authorization")) {
+ // If we already failed with these credentials, don't retry
+ return@authenticator null
+ }
+ response
+ .request()
+ .newBuilder()
+ .header("Authorization", credential)
+ .build()
+ }.build()
+
+}
+
+sealed class HttpJsonResult {
+ class Error(val statusCode: Int) : HttpJsonResult()
+ class Success(val json: JSONObject) : HttpJsonResult()
+}
diff --git a/cashier/src/main/java/net/taler/cashier/MainActivity.kt
b/cashier/src/main/java/net/taler/cashier/MainActivity.kt
new file mode 100644
index 0000000..b238054
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/MainActivity.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.cashier
+
+import android.content.Intent
+import android.content.Intent.*
+import android.os.Bundle
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.NavController
+import androidx.navigation.fragment.NavHostFragment
+import kotlinx.android.synthetic.main.activity_main.*
+
+class MainActivity : AppCompatActivity() {
+
+ private val viewModel: MainViewModel by viewModels()
+ private lateinit var nav: NavController
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ setSupportActionBar(toolbar)
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as
NavHostFragment
+ nav = navHostFragment.navController
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (!viewModel.hasConfig()) {
+ nav.navigate(viewModel.configDestination)
+ }
+ }
+
+ override fun onBackPressed() {
+ if (!viewModel.hasConfig() && nav.currentDestination?.id ==
R.id.configFragment) {
+ // we are in the configuration screen and need a config to continue
+ val intent = Intent(ACTION_MAIN).apply {
+ addCategory(CATEGORY_HOME)
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+ startActivity(intent)
+ } else {
+ super.onBackPressed()
+ }
+ }
+
+}
diff --git a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
new file mode 100644
index 0000000..3874038
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.cashier
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import androidx.security.crypto.EncryptedSharedPreferences
+import
androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV
+import
androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+import androidx.security.crypto.MasterKeys
+import androidx.security.crypto.MasterKeys.AES256_GCM_SPEC
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import net.taler.cashier.Amount.Companion.fromStringSigned
+import net.taler.cashier.HttpHelper.makeJsonGetRequest
+import net.taler.cashier.withdraw.WithdrawManager
+
+private val TAG = MainViewModel::class.java.simpleName
+
+private const val PREF_NAME = "net.taler.cashier.prefs"
+private const val PREF_KEY_BANK_URL = "bankUrl"
+private const val PREF_KEY_USERNAME = "username"
+private const val PREF_KEY_PASSWORD = "password"
+private const val PREF_KEY_CURRENCY = "currency"
+
+class MainViewModel(private val app: Application) : AndroidViewModel(app) {
+
+ val configDestination =
ConfigFragmentDirections.actionGlobalConfigFragment()
+
+ private val masterKeyAlias = MasterKeys.getOrCreate(AES256_GCM_SPEC)
+ private val prefs = EncryptedSharedPreferences.create(
+ PREF_NAME, masterKeyAlias, app, AES256_SIV, AES256_GCM
+ )
+
+ internal var config = Config(
+ bankUrl = prefs.getString(PREF_KEY_BANK_URL, "")!!,
+ username = prefs.getString(PREF_KEY_USERNAME, "")!!,
+ password = prefs.getString(PREF_KEY_PASSWORD, "")!!
+ )
+
+ private val mCurrency = MutableLiveData<String>(
+ prefs.getString(PREF_KEY_CURRENCY, null)
+ )
+ internal val currency: LiveData<String> = mCurrency
+
+ private val mConfigResult = MutableLiveData<ConfigResult>()
+ val configResult: LiveData<ConfigResult> = mConfigResult
+
+ private val mBalance = MutableLiveData<BalanceResult>()
+ val balance: LiveData<BalanceResult> = mBalance
+
+ internal val withdrawManager = WithdrawManager(app, this)
+
+ fun hasConfig() = config.bankUrl.isNotEmpty()
+ && config.username.isNotEmpty()
+ && config.password.isNotEmpty()
+
+ /**
+ * Start observing [configResult] after calling this to get the result
async.
+ * Warning: Ignore null results that are used to reset old results.
+ */
+ @UiThread
+ fun checkAndSaveConfig(config: Config) {
+ mConfigResult.value = null
+ viewModelScope.launch(Dispatchers.IO) {
+ val url = "${config.bankUrl}/accounts/${config.username}/balance"
+ Log.d(TAG, "Checking config: $url")
+ val result = when (val response = makeJsonGetRequest(url, config))
{
+ is HttpJsonResult.Success -> {
+ val balance = response.json.getString("balance")
+ val amount = fromStringSigned(balance)!!
+ mCurrency.postValue(amount.currency)
+ prefs.edit().putString(PREF_KEY_CURRENCY,
amount.currency).apply()
+ // save config
+ saveConfig(config)
+ ConfigResult(true)
+ }
+ is HttpJsonResult.Error -> {
+ val authError = response.statusCode == 401
+ ConfigResult(false, authError)
+ }
+ }
+ mConfigResult.postValue(result)
+ }
+ }
+
+ @WorkerThread
+ @SuppressLint("ApplySharedPref")
+ private fun saveConfig(config: Config) {
+ this.config = config
+ prefs.edit()
+ .putString(PREF_KEY_BANK_URL, config.bankUrl)
+ .putString(PREF_KEY_USERNAME, config.username)
+ .putString(PREF_KEY_PASSWORD, config.password)
+ .commit()
+ }
+
+ fun getBalance() = viewModelScope.launch(Dispatchers.IO) {
+ check(hasConfig()) { "No config to get balance" }
+ val url = "${config.bankUrl}/accounts/${config.username}/balance"
+ Log.d(TAG, "Checking balance at $url")
+ val result = when (val response = makeJsonGetRequest(url, config)) {
+ is HttpJsonResult.Success -> {
+ val balance = response.json.getString("balance")
+ fromStringSigned(balance)?.let { BalanceResult.Success(it) }
?: BalanceResult.Error
+ }
+ is HttpJsonResult.Error -> {
+ if (app.isOnline()) BalanceResult.Error
+ else BalanceResult.Offline
+ }
+ }
+ mBalance.postValue(result)
+ }
+
+ fun lock() {
+ saveConfig(config.copy(password = ""))
+ }
+
+}
+
+data class Config(
+ val bankUrl: String,
+ val username: String,
+ val password: String
+)
+
+class ConfigResult(val success: Boolean, val authError: Boolean = false)
diff --git a/cashier/src/main/java/net/taler/cashier/Utils.kt
b/cashier/src/main/java/net/taler/cashier/Utils.kt
new file mode 100644
index 0000000..62f7a77
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/Utils.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.cashier
+
+import android.content.Context
+import android.content.Context.CONNECTIVITY_SERVICE
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.os.Build.VERSION.SDK_INT
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+
+object Utils {
+
+ private const val HEX_CHARS = "0123456789ABCDEF"
+
+ fun hexStringToByteArray(data: String): ByteArray {
+ val result = ByteArray(data.length / 2)
+
+ for (i in data.indices step 2) {
+ val firstIndex = HEX_CHARS.indexOf(data[i])
+ val secondIndex = HEX_CHARS.indexOf(data[i + 1])
+
+ val octet = firstIndex.shl(4).or(secondIndex)
+ result[i.shr(1)] = octet.toByte()
+ }
+ return result
+ }
+
+
+ private val HEX_CHARS_ARRAY = HEX_CHARS.toCharArray()
+
+ @Suppress("unused")
+ fun toHex(byteArray: ByteArray): String {
+ val result = StringBuffer()
+
+ byteArray.forEach {
+ val octet = it.toInt()
+ val firstIndex = (octet and 0xF0).ushr(4)
+ val secondIndex = octet and 0x0F
+ result.append(HEX_CHARS_ARRAY[firstIndex])
+ result.append(HEX_CHARS_ARRAY[secondIndex])
+ }
+ return result.toString()
+ }
+
+}
+
+fun View.fadeIn(endAction: () -> Unit = {}) {
+ if (visibility == VISIBLE) return
+ alpha = 0f
+ visibility = VISIBLE
+ animate().alpha(1f).withEndAction {
+ endAction.invoke()
+ }.start()
+}
+
+fun View.fadeOut(endAction: () -> Unit = {}) {
+ if (visibility == INVISIBLE) return
+ animate().alpha(0f).withEndAction {
+ visibility = INVISIBLE
+ alpha = 1f
+ endAction.invoke()
+ }.start()
+}
+
+fun Context.isOnline(): Boolean {
+ val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ return if (SDK_INT < 29) {
+ @Suppress("DEPRECATION")
+ cm.activeNetworkInfo?.isConnected == true
+ } else {
+ val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?:
return false
+ capabilities.hasCapability(NET_CAPABILITY_INTERNET)
+ }
+}
diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt
b/cashier/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt
new file mode 100644
index 0000000..ceffcec
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.cashier.withdraw
+
+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.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_error.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+
+class ErrorFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { viewModel.withdrawManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_error, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer {
status ->
+ if (status is WithdrawStatus.Aborted) {
+ textView.setText(R.string.transaction_aborted)
+ }
+ })
+ withdrawManager.completeTransaction()
+ backButton.setOnClickListener {
+ findNavController().popBackStack()
+ }
+ }
+
+}
diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt
b/cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt
new file mode 100644
index 0000000..a487b5f
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt
@@ -0,0 +1,234 @@
+/*
+ * 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.cashier.withdraw
+
+import android.app.Activity
+import android.content.Context
+import android.nfc.NfcAdapter
+import android.nfc.NfcAdapter.FLAG_READER_NFC_A
+import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
+import android.nfc.NfcAdapter.getDefaultAdapter
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+import android.util.Log
+import net.taler.cashier.Utils.hexStringToByteArray
+import org.json.JSONObject
+import java.io.ByteArrayOutputStream
+import java.net.URL
+import javax.net.ssl.HttpsURLConnection
+
+@Suppress("unused")
+private const val TALER_AID = "A0000002471001"
+
+class NfcManager : NfcAdapter.ReaderCallback {
+
+ companion object {
+ const val TAG = "taler-merchant"
+
+ /**
+ * Returns true if NFC is supported and false otherwise.
+ */
+ fun hasNfc(context: Context): Boolean {
+ return getNfcAdapter(context) != null
+ }
+
+ /**
+ * Enables NFC reader mode. Don't forget to call [stop] afterwards.
+ */
+ fun start(activity: Activity, nfcManager: NfcManager) {
+ getNfcAdapter(activity)?.enableReaderMode(activity, nfcManager,
nfcManager.flags, null)
+ }
+
+ /**
+ * Disables NFC reader mode. Call after [start].
+ */
+ fun stop(activity: Activity) {
+ getNfcAdapter(activity)?.disableReaderMode(activity)
+ }
+
+ private fun getNfcAdapter(context: Context): NfcAdapter? {
+ return getDefaultAdapter(context)
+ }
+ }
+
+ private val flags = FLAG_READER_NFC_A or FLAG_READER_SKIP_NDEF_CHECK
+
+ private var tagString: String? = null
+ private var currentTag: IsoDep? = null
+
+ fun setTagString(tagString: String) {
+ this.tagString = tagString
+ }
+
+ override fun onTagDiscovered(tag: Tag?) {
+
+ Log.v(TAG, "tag discovered")
+
+ val isoDep = IsoDep.get(tag)
+ isoDep.connect()
+
+ currentTag = isoDep
+
+ isoDep.transceive(apduSelectFile())
+
+ val tagString: String? = tagString
+ if (tagString != null) {
+ isoDep.transceive(apduPutTalerData(1, tagString.toByteArray()))
+ }
+
+ // FIXME: use better pattern for sleeps in between requests
+ // -> start with fast polling, poll more slowly if no requests are
coming
+
+ while (true) {
+ try {
+ val reqFrame = isoDep.transceive(apduGetData())
+ if (reqFrame.size < 2) {
+ Log.v(TAG, "request frame too small")
+ break
+ }
+ val req = ByteArray(reqFrame.size - 2)
+ if (req.isEmpty()) {
+ continue
+ }
+ reqFrame.copyInto(req, 0, 0, reqFrame.size - 2)
+ val jsonReq = JSONObject(req.toString(Charsets.UTF_8))
+ val reqId = jsonReq.getInt("id")
+ Log.v(TAG, "got request $jsonReq")
+ val jsonInnerReq = jsonReq.getJSONObject("request")
+ val method = jsonInnerReq.getString("method")
+ val urlStr = jsonInnerReq.getString("url")
+ Log.v(TAG, "url '$urlStr'")
+ Log.v(TAG, "method '$method'")
+ val url = URL(urlStr)
+ val conn: HttpsURLConnection = url.openConnection() as
HttpsURLConnection
+ conn.setRequestProperty("Accept", "application/json")
+ conn.connectTimeout = 5000
+ conn.doInput = true
+ when (method) {
+ "get" -> {
+ conn.requestMethod = "GET"
+ }
+ "postJson" -> {
+ conn.requestMethod = "POST"
+ conn.doOutput = true
+ conn.setRequestProperty("Content-Type",
"application/json; utf-8")
+ val body = jsonInnerReq.getString("body")
+
conn.outputStream.write(body.toByteArray(Charsets.UTF_8))
+ }
+ else -> {
+ throw Exception("method not supported")
+ }
+ }
+ Log.v(TAG, "connecting")
+ conn.connect()
+ Log.v(TAG, "connected")
+
+ val statusCode = conn.responseCode
+ val tunnelResp = JSONObject()
+ tunnelResp.put("id", reqId)
+ tunnelResp.put("status", conn.responseCode)
+
+ if (statusCode == 200) {
+ val stream = conn.inputStream
+ val httpResp = stream.buffered().readBytes()
+ tunnelResp.put("responseJson",
JSONObject(httpResp.toString(Charsets.UTF_8)))
+ }
+
+ Log.v(TAG, "sending: $tunnelResp")
+
+ isoDep.transceive(apduPutTalerData(2,
tunnelResp.toString().toByteArray()))
+ } catch (e: Exception) {
+ Log.v(TAG, "exception during NFC loop: $e")
+ break
+ }
+ }
+
+ isoDep.close()
+ }
+
+ private fun writeApduLength(stream: ByteArrayOutputStream, size: Int) {
+ when {
+ size == 0 -> {
+ // No size field needed!
+ }
+ size <= 255 -> // One byte size field
+ stream.write(size)
+ size <= 65535 -> {
+ stream.write(0)
+ // FIXME: is this supposed to be little or big endian?
+ stream.write(size and 0xFF)
+ stream.write((size ushr 8) and 0xFF)
+ }
+ else -> throw Error("payload too big")
+ }
+ }
+
+ private fun apduSelectFile(): ByteArray {
+ return hexStringToByteArray("00A4040007A0000002471001")
+ }
+
+ private fun apduPutData(payload: ByteArray): ByteArray {
+ val stream = ByteArrayOutputStream()
+
+ // Class
+ stream.write(0x00)
+
+ // Instruction 0xDA = put data
+ stream.write(0xDA)
+
+ // Instruction parameters
+ // (proprietary encoding)
+ stream.write(0x01)
+ stream.write(0x00)
+
+ writeApduLength(stream, payload.size)
+
+ stream.write(payload)
+
+ return stream.toByteArray()
+ }
+
+ private fun apduPutTalerData(talerInst: Int, payload: ByteArray):
ByteArray {
+ val realPayload = ByteArrayOutputStream()
+ realPayload.write(talerInst)
+ realPayload.write(payload)
+ return apduPutData(realPayload.toByteArray())
+ }
+
+ private fun apduGetData(): ByteArray {
+ val stream = ByteArrayOutputStream()
+
+ // Class
+ stream.write(0x00)
+
+ // Instruction 0xCA = get data
+ stream.write(0xCA)
+
+ // Instruction parameters
+ // (proprietary encoding)
+ stream.write(0x01)
+ stream.write(0x00)
+
+ // Max expected response size, two
+ // zero bytes denotes 65536
+ stream.write(0x0)
+ stream.write(0x0)
+
+ return stream.toByteArray()
+ }
+
+}
diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt
b/cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt
new file mode 100644
index 0000000..e3ffa92
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.cashier.withdraw
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.RGB_565
+import android.graphics.Color.BLACK
+import android.graphics.Color.WHITE
+import com.google.zxing.BarcodeFormat.QR_CODE
+import com.google.zxing.qrcode.QRCodeWriter
+
+object QrCodeManager {
+
+ fun makeQrCode(text: String, size: Int = 256): Bitmap {
+ val qrCodeWriter = QRCodeWriter()
+ val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size)
+ val height = bitMatrix.height
+ val width = bitMatrix.width
+ val bmp = Bitmap.createBitmap(width, height, RGB_565)
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ bmp.setPixel(x, y, if (bitMatrix.get(x, y)) BLACK else WHITE)
+ }
+ }
+ return bmp
+ }
+
+}
diff --git
a/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt
b/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt
new file mode 100644
index 0000000..8b782b0
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt
@@ -0,0 +1,174 @@
+/*
+ * 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.cashier.withdraw
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat.getColor
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_transaction.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+import net.taler.cashier.fadeIn
+import net.taler.cashier.fadeOut
+import
net.taler.cashier.withdraw.TransactionFragmentDirections.Companion.actionTransactionFragmentToBalanceFragment
+import
net.taler.cashier.withdraw.TransactionFragmentDirections.Companion.actionTransactionFragmentToErrorFragment
+import net.taler.cashier.withdraw.WithdrawResult.Error
+import net.taler.cashier.withdraw.WithdrawResult.InsufficientBalance
+import net.taler.cashier.withdraw.WithdrawResult.Success
+
+class TransactionFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { viewModel.withdrawManager }
+ private val nfcManager = NfcManager()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_transaction, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ withdrawManager.withdrawAmount.observe(viewLifecycleOwner, Observer {
amount ->
+ amountView.text = amount
+ })
+ withdrawManager.withdrawResult.observe(viewLifecycleOwner, Observer {
result ->
+ onWithdrawResultReceived(result)
+ })
+ withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer {
status ->
+ onWithdrawStatusChanged(status)
+ })
+
+ // change intro text depending on whether NFC is available or not
+ val hasNfc = NfcManager.hasNfc(requireContext())
+ val intro = if (hasNfc) R.string.transaction_intro_nfc else
R.string.transaction_intro
+ introView.setText(intro)
+
+ cancelButton.setOnClickListener {
+ findNavController().popBackStack()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (withdrawManager.withdrawResult.value is Success) {
+ NfcManager.start(requireActivity(), nfcManager)
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ NfcManager.stop(requireActivity())
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!requireActivity().isChangingConfigurations) {
+ withdrawManager.abort()
+ }
+ }
+
+ private fun onWithdrawResultReceived(result: WithdrawResult?) {
+ if (result != null) {
+ progressBar.animate()
+ .alpha(0f)
+ .withEndAction { progressBar?.visibility = INVISIBLE }
+ .setDuration(750)
+ .start()
+ }
+ when (result) {
+ is InsufficientBalance -> {
+ val c = getColor(requireContext(),
R.color.design_default_color_error)
+ introView.setTextColor(c)
+ introView.text =
getString(R.string.withdraw_error_insufficient_balance)
+ }
+ is Error -> {
+ val c = getColor(requireContext(),
R.color.design_default_color_error)
+ introView.setTextColor(c)
+ introView.text = result.msg
+ }
+ is Success -> {
+ // start NFC
+ nfcManager.setTagString(result.talerUri)
+ NfcManager.start(
+ requireActivity(),
+ nfcManager
+ )
+ // show QR code
+ qrCodeView.alpha = 0f
+ qrCodeView.animate()
+ .alpha(1f)
+ .withStartAction {
+ qrCodeView.visibility = VISIBLE
+ qrCodeView.setImageBitmap(result.qrCode)
+ }
+ .setDuration(750)
+ .start()
+ }
+ }
+ }
+
+ private fun onWithdrawStatusChanged(status: WithdrawStatus?): Any = when
(status) {
+ is WithdrawStatus.SelectionDone -> {
+ qrCodeView.fadeOut {
+ qrCodeView?.setImageResource(R.drawable.ic_arrow)
+ qrCodeView?.fadeIn()
+ }
+ introView.fadeOut {
+ introView?.text = getString(R.string.transaction_intro_scanned)
+ introView?.fadeIn {
+ confirmButton?.isEnabled = true
+ confirmButton?.setOnClickListener {
+ withdrawManager.confirm(status.withdrawalId)
+ }
+ }
+ }
+ }
+ is WithdrawStatus.Confirming -> {
+ confirmButton.isEnabled = false
+ qrCodeView.fadeOut()
+ progressBar.fadeIn()
+ }
+ is WithdrawStatus.Success -> {
+ withdrawManager.completeTransaction()
+ actionTransactionFragmentToBalanceFragment().let {
+ findNavController().navigate(it)
+ }
+ }
+ is WithdrawStatus.Aborted -> onError()
+ is WithdrawStatus.Error -> onError()
+ null -> {
+ // no-op
+ }
+ }
+
+ private fun onError() {
+ actionTransactionFragmentToErrorFragment().let {
+ findNavController().navigate(it)
+ }
+ }
+
+}
diff --git
a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
new file mode 100644
index 0000000..4c618ac
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
@@ -0,0 +1,232 @@
+/*
+ * 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.cashier.withdraw
+
+import android.app.Application
+import android.graphics.Bitmap
+import android.os.CountDownTimer
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import net.taler.cashier.BalanceResult
+import net.taler.cashier.HttpHelper.makeJsonGetRequest
+import net.taler.cashier.HttpHelper.makeJsonPostRequest
+import net.taler.cashier.HttpJsonResult.Error
+import net.taler.cashier.HttpJsonResult.Success
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+import org.json.JSONObject
+import java.util.concurrent.TimeUnit.MINUTES
+import java.util.concurrent.TimeUnit.SECONDS
+
+private val TAG = WithdrawManager::class.java.simpleName
+
+private val INTERVAL = SECONDS.toMillis(1)
+private val TIMEOUT = MINUTES.toMillis(2)
+
+class WithdrawManager(
+ private val app: Application,
+ private val viewModel: MainViewModel
+) {
+ private val scope
+ get() = viewModel.viewModelScope
+
+ private val config
+ get() = viewModel.config
+
+ private val currency: String?
+ get() = viewModel.currency.value
+
+ private var withdrawStatusCheck: Job? = null
+
+ private val mWithdrawAmount = MutableLiveData<String>()
+ val withdrawAmount: LiveData<String> = mWithdrawAmount
+
+ private val mWithdrawResult = MutableLiveData<WithdrawResult>()
+ val withdrawResult: LiveData<WithdrawResult> = mWithdrawResult
+
+ private val mWithdrawStatus = MutableLiveData<WithdrawStatus>()
+ val withdrawStatus: LiveData<WithdrawStatus> = mWithdrawStatus
+
+ private val mLastTransaction = MutableLiveData<LastTransaction>()
+ val lastTransaction: LiveData<LastTransaction> = mLastTransaction
+
+ @UiThread
+ fun hasSufficientBalance(amount: Int): Boolean {
+ val balanceResult = viewModel.balance.value
+ if (balanceResult !is BalanceResult.Success) return false
+ val balanceStr = balanceResult.amount.amount
+ val balanceDouble = balanceStr.toDouble()
+ return amount <= balanceDouble
+ }
+
+ @UiThread
+ fun withdraw(amount: Int) {
+ check(amount > 0) { "Withdraw amount was <= 0" }
+ check(currency != null) { "Currency is null" }
+ mWithdrawResult.value = null
+ mWithdrawAmount.value = "$amount $currency"
+ scope.launch(Dispatchers.IO) {
+ val url =
"${config.bankUrl}/accounts/${config.username}/withdrawals"
+ Log.d(TAG, "Starting withdrawal at $url")
+ val body = JSONObject(mapOf("amount" to
"${currency}:${amount}")).toString()
+ when (val result = makeJsonPostRequest(url, body, config)) {
+ is Success -> {
+ val talerUri = result.json.getString("taler_withdraw_uri")
+ val withdrawResult = WithdrawResult.Success(
+ id = result.json.getString("withdrawal_id"),
+ talerUri = talerUri,
+ qrCode = QrCodeManager.makeQrCode(talerUri)
+ )
+ mWithdrawResult.postValue(withdrawResult)
+ timer.start()
+ }
+ is Error -> {
+ val errorStr = app.getString(R.string.withdraw_error_fetch)
+ mWithdrawResult.postValue(WithdrawResult.Error(errorStr))
+ }
+ }
+ }
+ }
+
+ private val timer: CountDownTimer = object : CountDownTimer(TIMEOUT,
INTERVAL) {
+ override fun onTick(millisUntilFinished: Long) {
+ val result = withdrawResult.value
+ if (result is WithdrawResult.Success) {
+ // check for active jobs and only do one at a time
+ val hasActiveCheck = withdrawStatusCheck?.isActive ?: false
+ if (!hasActiveCheck) {
+ withdrawStatusCheck = checkWithdrawStatus(result.id)
+ }
+ } else {
+ cancel()
+ }
+ }
+
+ override fun onFinish() {
+ abort()
+ mWithdrawStatus.postValue(WithdrawStatus.Error)
+ cancel()
+ }
+ }
+
+ private fun checkWithdrawStatus(withdrawalId: String) =
scope.launch(Dispatchers.IO) {
+ val url =
"${config.bankUrl}/accounts/${config.username}/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
+ when {
+ response.json.getBoolean("aborted") -> {
+ cancelWithdrawStatusCheck()
+ mWithdrawStatus.postValue(WithdrawStatus.Aborted)
+ }
+ response.json.getBoolean("confirmation_done") -> {
+ 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))
+ }
+ }
+ }
+ }
+
+ private fun cancelWithdrawStatusCheck() {
+ timer.cancel()
+ withdrawStatusCheck?.cancel()
+ }
+
+ /**
+ * Aborts the last [withdrawResult], if it exists und there is no
[withdrawStatus].
+ * Otherwise this is a no-op.
+ */
+ @UiThread
+ fun abort() {
+ val result = withdrawResult.value
+ val status = withdrawStatus.value
+ if (result is WithdrawResult.Success && status == null) {
+ cancelWithdrawStatusCheck()
+ abort(result.id)
+ }
+ }
+
+ private fun abort(withdrawalId: String) = scope.launch(Dispatchers.IO) {
+ val url =
"${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/abort"
+ Log.d(TAG, "Aborting withdrawal at $url")
+ makeJsonPostRequest(url, "", config)
+ }
+
+ @UiThread
+ fun confirm(withdrawalId: String) {
+ mWithdrawStatus.value = WithdrawStatus.Confirming
+ scope.launch(Dispatchers.IO) {
+ val url =
+
"${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/confirm"
+ Log.d(TAG, "Confirming withdrawal at $url")
+ when (val result = makeJsonPostRequest(url, "", config)) {
+ is Success -> {
+ // no-op still waiting for [timer] to confirm our
confirmation
+ }
+ is Error -> {
+ Log.e(TAG, "Error confirming withdrawal. Status code:
${result.statusCode}")
+ mWithdrawStatus.postValue(WithdrawStatus.Error)
+ }
+ }
+ }
+ }
+
+ @UiThread
+ fun completeTransaction() {
+ mLastTransaction.value = LastTransaction(withdrawAmount.value!!,
withdrawStatus.value!!)
+ withdrawStatusCheck = null
+ mWithdrawAmount.value = null
+ mWithdrawResult.value = null
+ mWithdrawStatus.value = null
+ }
+
+}
+
+sealed class WithdrawResult {
+ object InsufficientBalance : WithdrawResult()
+ class Error(val msg: String) : WithdrawResult()
+ class Success(val id: String, val talerUri: String, val qrCode: Bitmap) :
WithdrawResult()
+}
+
+sealed class WithdrawStatus {
+ object Error : WithdrawStatus()
+ object Aborted : WithdrawStatus()
+ class SelectionDone(val withdrawalId: String) : WithdrawStatus()
+ object Confirming : WithdrawStatus()
+ object Success : WithdrawStatus()
+}
+
+data class LastTransaction(
+ val withdrawAmount: String,
+ val withdrawStatus: WithdrawStatus
+)
diff --git a/cashier/src/main/res/drawable-w550dp/ic_arrow.xml
b/cashier/src/main/res/drawable-w550dp/ic_arrow.xml
new file mode 100644
index 0000000..331ea06
--- /dev/null
+++ b/cashier/src/main/res/drawable-w550dp/ic_arrow.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:alpha="0.56"
+ android:tint="@color/green"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#000000"
+ android:pathData="M20,5.41 L18.59,4 7,15.59V9H5V19H15V17H8.41" />
+</vector>
diff --git a/cashier/src/main/res/drawable/ic_arrow.xml
b/cashier/src/main/res/drawable/ic_arrow.xml
new file mode 100644
index 0000000..d7578bd
--- /dev/null
+++ b/cashier/src/main/res/drawable/ic_arrow.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:alpha="0.56"
+ android:tint="@color/green"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#000000"
+ android:pathData="M5,5.41 L6.41,4 18,15.59V9h2V19H10v-2h6.59" />
+</vector>
diff --git a/cashier/src/main/res/drawable/ic_check_circle.xml
b/cashier/src/main/res/drawable/ic_check_circle.xml
new file mode 100644
index 0000000..d43d6ba
--- /dev/null
+++ b/cashier/src/main/res/drawable/ic_check_circle.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:alpha="0.56"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48
10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
+</vector>
diff --git a/cashier/src/main/res/drawable/ic_clear.xml
b/cashier/src/main/res/drawable/ic_clear.xml
new file mode 100644
index 0000000..f50fd99
--- /dev/null
+++ b/cashier/src/main/res/drawable/ic_clear.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12
5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
+</vector>
diff --git a/cashier/src/main/res/drawable/ic_error.xml
b/cashier/src/main/res/drawable/ic_error.xml
new file mode 100644
index 0000000..b7e22a0
--- /dev/null
+++ b/cashier/src/main/res/drawable/ic_error.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:alpha="0.56"
+ android:tint="@color/red"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48
10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
+</vector>
diff --git a/cashier/src/main/res/drawable/ic_launcher_foreground.xml
b/cashier/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..fbaac05
--- /dev/null
+++ b/cashier/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <group
+ android:translateX="12"
+ android:translateY="12">
+ <path
+ android:pathData="M6,3L6,6L9,6L9,7L6.25,7C5.05,7 4.0508,8
4.0508,9L3.5,16L20.5,16L20,9C19.8,8 18.8008,7
17.8008,7L11,7L11,6L14,6L14,3L6,3zM7,4L13,4L13,5L7,5L7,4zM6,9L8,9L8,10L6,10L6,9zM9,9L11,9L11,10L9,10L9,9zM13,9L18,9L18,11L13,11L13,9zM6,11L8,11L8,12L6,12L6,11zM9,11L11,11L11,12L9,12L9,11zM6,13L8,13L8,14L6,14L6,13zM9,13L11,13L11,14L9,14L9,13zM2,17L2,21L22,21L22,17L2,17zM4.7422,17.291L7.2695,17.291L7.2695,17.7793L6.3574,17.7793L6.3574,20.6777L5.6543,20.6777L5.6543,17.7793L4.7422,
[...]
+ android:fillColor="#f9f9f9"
+ tools:ignore="VectorPath" />
+ </group>
+</vector>
diff --git a/cashier/src/main/res/drawable/ic_withdraw.xml
b/cashier/src/main/res/drawable/ic_withdraw.xml
new file mode 100644
index 0000000..b694a2b
--- /dev/null
+++ b/cashier/src/main/res/drawable/ic_withdraw.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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="#000000"
+ android:pathData="M3 0V3H0V5H3V8H5V5H8V3H5V0H3M9 3V6H6V9H3V19C3 20.1
3.89 21 5 21H19C20.11 21 21 20.11 21 19V18H12C10.9 18 10 17.11 10 16V8C10 6.9
10.89 6 12 6H21V5C21 3.9 20.11 3 19 3H9M12 8V16H22V8H12M16 10.5C16.83 10.5 17.5
11.17 17.5 12C17.5 12.83 16.83 13.5 16 13.5C15.17 13.5 14.5 12.83 14.5 12C14.5
11.17 15.17 10.5 16 10.5Z" />
+</vector>
diff --git a/cashier/src/main/res/layout-w550dp/fragment_balance.xml
b/cashier/src/main/res/layout-w550dp/fragment_balance.xml
new file mode 100644
index 0000000..d04698b
--- /dev/null
+++ b/cashier/src/main/res/layout-w550dp/fragment_balance.xml
@@ -0,0 +1,222 @@
+<?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=".BalanceFragment">
+
+ <TextView
+ android:id="@+id/lastTransactionView"
+ style="@style/Widget.MaterialComponents.Snackbar.FullWidth"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:background="?attr/colorPrimaryDark"
+ android:drawableStart="@drawable/ic_check_circle"
+ android:drawablePadding="8dp"
+ android:drawableTint="?attr/colorOnPrimarySurface"
+ android:gravity="center_vertical"
+ android:padding="8dp"
+ android:textColor="?attr/colorOnPrimarySurface"
+ android:visibility="gone"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="@string/transaction_last_success"
+ tools:visibility="visible" />
+
+ <View
+ android:id="@+id/balanceBackground"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@color/background"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="@+id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/lastTransactionView" />
+
+ <TextView
+ android:id="@+id/balanceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="32dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="32dp"
+ android:text="@string/balance_current_label"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ app:layout_constraintBottom_toTopOf="@+id/balanceView"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/lastTransactionView"
+ app:layout_constraintVertical_bias="0.0"
+ app:layout_constraintVertical_chainStyle="packed" />
+
+ <TextView
+ android:id="@+id/balanceView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/balanceLabel"
+ tools:text="100 KUDOS" />
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="@+id/balanceView"
+ app:layout_constraintEnd_toEndOf="@+id/balanceView"
+ app:layout_constraintStart_toStartOf="@+id/balanceView"
+ app:layout_constraintTop_toTopOf="@+id/balanceView" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.5" />
+
+ <TextView
+ android:id="@+id/introView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="32dp"
+ android:text="@string/withdraw_into"
+ android:textAlignment="center"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/button5"
+ style="@style/AmountButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="5"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toStartOf="@+id/button10"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:ignore="HardcodedText"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/button10"
+ style="@style/AmountButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="10"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toStartOf="@+id/button20"
+ app:layout_constraintStart_toEndOf="@+id/button5"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:ignore="HardcodedText"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/button20"
+ style="@style/AmountButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="20"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toStartOf="@+id/button50"
+ app:layout_constraintStart_toEndOf="@+id/button10"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:ignore="HardcodedText"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/button50"
+ style="@style/AmountButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="50"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/button20"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:ignore="HardcodedText"
+ tools:visibility="visible" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/amountView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="32dp"
+ android:hint="@string/withdraw_input_amount"
+ android:visibility="invisible"
+ app:endIconDrawable="@drawable/ic_clear"
+ app:endIconMode="clear_text"
+ app:endIconTint="?attr/colorControlNormal"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toBottomOf="@+id/button5"
+ tools:visibility="visible">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="6"
+ android:imeOptions="actionGo"
+ android:inputType="number"
+ android:maxLength="4" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/currencyView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/amountView"
+ app:layout_constraintTop_toTopOf="@+id/amountView"
+ tools:text="TESTKUDOS"
+ tools:visibility="visible" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/confirmWithdrawalButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:backgroundTint="@color/green"
+ android:drawableLeft="@drawable/ic_withdraw"
+ android:drawableTint="?attr/colorOnPrimarySurface"
+ android:text="@string/withdraw_button_confirm"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toBottomOf="@+id/amountView"
+ app:layout_constraintVertical_bias="1.0"
+ tools:ignore="RtlHardcoded"
+ tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/cashier/src/main/res/layout-w550dp/fragment_transaction.xml
b/cashier/src/main/res/layout-w550dp/fragment_transaction.xml
new file mode 100644
index 0000000..610ed28
--- /dev/null
+++ b/cashier/src/main/res/layout-w550dp/fragment_transaction.xml
@@ -0,0 +1,111 @@
+<?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=".withdraw.TransactionFragment">
+
+ <TextView
+ android:id="@+id/introView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="32dp"
+ android:gravity="center_horizontal"
+ android:text="@string/transaction_intro"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/amountView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="32dp"
+ android:gravity="center_horizontal"
+ android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:text="50 KUDOS" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.5" />
+
+ <ImageView
+ android:id="@+id/qrCodeView"
+ android:layout_width="256dp"
+ android:layout_height="256dp"
+ android:layout_margin="32dp"
+ android:keepScreenOn="true"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="ContentDescription"
+ tools:src="@drawable/ic_arrow"
+ 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="@+id/qrCodeView"
+ app:layout_constraintEnd_toEndOf="@+id/qrCodeView"
+ app:layout_constraintStart_toStartOf="@+id/qrCodeView"
+ app:layout_constraintTop_toTopOf="@+id/qrCodeView" />
+
+ <Button
+ android:id="@+id/cancelButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/red"
+ android:text="@string/transaction_abort"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/confirmButton"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/qrCodeView"
+ app:layout_constraintVertical_bias="1.0" />
+
+ <Button
+ android:id="@+id/confirmButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/green"
+ android:enabled="false"
+ android:text="@string/transaction_confirm"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/cancelButton"
+ tools:enabled="true" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/cashier/src/main/res/layout/activity_main.xml
b/cashier/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..e41b842
--- /dev/null
+++ b/cashier/src/main/res/layout/activity_main.xml
@@ -0,0 +1,51 @@
+<?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.coordinatorlayout.widget.CoordinatorLayout
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=".MainActivity">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:theme="@style/AppTheme.AppBarOverlay">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar"
+ style="@style/AppTheme.Toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/nav_host_fragment"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:defaultNavHost="true"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/nav_graph" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/cashier/src/main/res/layout/fragment_balance.xml
b/cashier/src/main/res/layout/fragment_balance.xml
new file mode 100644
index 0000000..5dafc59
--- /dev/null
+++ b/cashier/src/main/res/layout/fragment_balance.xml
@@ -0,0 +1,225 @@
+<?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=".BalanceFragment">
+
+ <TextView
+ android:id="@+id/lastTransactionView"
+ style="@style/Widget.MaterialComponents.Snackbar.FullWidth"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:background="?attr/colorPrimaryDark"
+ android:drawableStart="@drawable/ic_check_circle"
+ android:drawablePadding="8dp"
+ android:drawableTint="?attr/colorOnPrimarySurface"
+ android:gravity="center_vertical"
+ android:padding="8dp"
+ android:textColor="?attr/colorOnPrimarySurface"
+ android:visibility="gone"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="@string/transaction_last_success"
+ tools:visibility="visible" />
+
+ <View
+ android:id="@+id/balanceBackground"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@color/background"
+ app:layout_constraintBottom_toBottomOf="@+id/balanceView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/lastTransactionView" />
+
+ <TextView
+ android:id="@+id/balanceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:text="@string/balance_current_label"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/lastTransactionView" />
+
+ <TextView
+ android:id="@+id/balanceView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/default_margin"
+ android:paddingTop="8dp"
+ android:gravity="center"
+ android:paddingEnd="@dimen/default_margin"
+ android:paddingBottom="@dimen/default_margin"
+ android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/balanceLabel"
+ tools:text="100 KUDOS" />
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="@+id/balanceView"
+ app:layout_constraintEnd_toEndOf="@+id/balanceView"
+ app:layout_constraintStart_toStartOf="@+id/balanceView"
+ app:layout_constraintTop_toTopOf="@+id/balanceView" />
+
+ <TextView
+ android:id="@+id/introView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/default_margin"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="@dimen/default_margin"
+ android:text="@string/withdraw_into"
+ android:textAlignment="center"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/button5"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/balanceBackground"
+ app:layout_constraintVertical_bias="0.25"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/button5"
+ style="@style/AmountButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="5"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/amountView"
+ app:layout_constraintEnd_toStartOf="@+id/button10"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:ignore="HardcodedText"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/button10"
+ style="@style/AmountButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="10"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toStartOf="@+id/button20"
+ app:layout_constraintStart_toEndOf="@+id/button5"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:ignore="HardcodedText"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/button20"
+ style="@style/AmountButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="20"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toStartOf="@+id/button50"
+ app:layout_constraintStart_toEndOf="@+id/button10"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:ignore="HardcodedText"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/button50"
+ style="@style/AmountButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="50"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/button20"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:ignore="HardcodedText"
+ tools:visibility="visible" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/amountView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/default_margin"
+ android:layout_marginTop="@dimen/default_margin"
+ android:hint="@string/withdraw_input_amount"
+ android:visibility="invisible"
+ app:endIconDrawable="@drawable/ic_clear"
+ app:endIconMode="clear_text"
+ app:endIconTint="?attr/colorControlNormal"
+ app:layout_constraintBottom_toTopOf="@+id/confirmWithdrawalButton"
+ app:layout_constraintEnd_toStartOf="@+id/currencyView"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/button5"
+ tools:visibility="visible">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="6"
+ android:imeOptions="actionGo"
+ android:inputType="number"
+ android:maxLength="4" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/currencyView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/amountView"
+ app:layout_constraintTop_toTopOf="@+id/amountView"
+ tools:text="TESTKUDOS"
+ tools:visibility="visible" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/confirmWithdrawalButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:backgroundTint="@color/green"
+ android:drawableLeft="@drawable/ic_withdraw"
+ android:drawableTint="?attr/colorOnPrimarySurface"
+ android:text="@string/withdraw_button_confirm"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/amountView"
+ tools:ignore="RtlHardcoded"
+ tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/cashier/src/main/res/layout/fragment_config.xml
b/cashier/src/main/res/layout/fragment_config.xml
new file mode 100644
index 0000000..47ec6f9
--- /dev/null
+++ b/cashier/src/main/res/layout/fragment_config.xml
@@ -0,0 +1,112 @@
+<?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">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/urlView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:hint="@string/config_bank_url"
+ app:endIconMode="clear_text"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textUri" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/usernameView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:hint="@string/config_username"
+ app:boxBackgroundMode="outline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/urlView">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="text" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/passwordView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:hint="@string/config_password"
+ app:boxBackgroundMode="outline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/usernameView"
+ app:passwordToggleEnabled="true">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textWebPassword" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <Button
+ android:id="@+id/saveButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:text="@string/config_button_save"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/passwordView" />
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/saveButton"
+ app:layout_constraintEnd_toEndOf="@+id/saveButton"
+ app:layout_constraintStart_toStartOf="@+id/saveButton"
+ app:layout_constraintTop_toTopOf="@+id/saveButton"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/demoView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/default_margin"
+ android:text="@string/config_demo_hint"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/saveButton" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/cashier/src/main/res/layout/fragment_error.xml
b/cashier/src/main/res/layout/fragment_error.xml
new file mode 100644
index 0000000..ac34c85
--- /dev/null
+++ b/cashier/src/main/res/layout/fragment_error.xml
@@ -0,0 +1,65 @@
+<?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:id="@+id/frameLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".withdraw.ErrorFragment">
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="32dp"
+ android:src="@drawable/ic_error"
+ app:layout_constraintBottom_toTopOf="@+id/textView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="ContentDescription" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/textView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="32dp"
+ android:text="@string/transaction_error"
+ android:textAlignment="center"
+ android:textColor="@color/red"
+ app:autoSizeMaxTextSize="42sp"
+ app:autoSizeTextType="uniform"
+ app:layout_constraintBottom_toTopOf="@+id/backButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/imageView" />
+
+ <Button
+ android:id="@+id/backButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/transaction_button_back"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/cashier/src/main/res/layout/fragment_transaction.xml
b/cashier/src/main/res/layout/fragment_transaction.xml
new file mode 100644
index 0000000..3affbf2
--- /dev/null
+++ b/cashier/src/main/res/layout/fragment_transaction.xml
@@ -0,0 +1,100 @@
+<?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=".withdraw.TransactionFragment">
+
+ <TextView
+ android:id="@+id/introView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="32dp"
+ android:gravity="center_horizontal"
+ android:text="@string/transaction_intro"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/amountView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="32dp"
+ android:gravity="center_horizontal"
+ android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:text="50 KUDOS" />
+
+ <ImageView
+ android:id="@+id/qrCodeView"
+ android:layout_width="256dp"
+ android:layout_height="256dp"
+ android:layout_margin="32dp"
+ android:keepScreenOn="true"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/amountView"
+ tools:ignore="ContentDescription"
+ tools:src="@drawable/ic_arrow"
+ 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="@+id/qrCodeView"
+ app:layout_constraintEnd_toEndOf="@+id/qrCodeView"
+ app:layout_constraintStart_toStartOf="@+id/qrCodeView"
+ app:layout_constraintTop_toTopOf="@+id/qrCodeView" />
+
+ <Button
+ android:id="@+id/cancelButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/red"
+ android:text="@string/transaction_abort"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/confirmButton"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/qrCodeView"
+ app:layout_constraintVertical_bias="1.0" />
+
+ <Button
+ android:id="@+id/confirmButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/green"
+ android:enabled="false"
+ android:text="@string/transaction_confirm"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/cancelButton"
+ tools:enabled="true" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/cashier/src/main/res/menu/balance.xml
b/cashier/src/main/res/menu/balance.xml
new file mode 100644
index 0000000..bc64af3
--- /dev/null
+++ b/cashier/src/main/res/menu/balance.xml
@@ -0,0 +1,30 @@
+<?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/>
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/action_reconfigure"
+ android:title="@string/action_reconfigure"
+ app:showAsAction="never" />
+ <item
+ android:id="@+id/action_lock"
+ android:title="@string/action_lock"
+ app:showAsAction="never" />
+
+</menu>
diff --git a/cashier/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
b/cashier/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/cashier/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/cashier/src/main/res/mipmap-hdpi/ic_launcher.png
b/cashier/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..c52928c
Binary files /dev/null and b/cashier/src/main/res/mipmap-hdpi/ic_launcher.png
differ
diff --git a/cashier/src/main/res/mipmap-mdpi/ic_launcher.png
b/cashier/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..b97178b
Binary files /dev/null and b/cashier/src/main/res/mipmap-mdpi/ic_launcher.png
differ
diff --git a/cashier/src/main/res/mipmap-xhdpi/ic_launcher.png
b/cashier/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..8f92c07
Binary files /dev/null and b/cashier/src/main/res/mipmap-xhdpi/ic_launcher.png
differ
diff --git a/cashier/src/main/res/mipmap-xxhdpi/ic_launcher.png
b/cashier/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..214cbea
Binary files /dev/null and b/cashier/src/main/res/mipmap-xxhdpi/ic_launcher.png
differ
diff --git a/cashier/src/main/res/mipmap-xxxhdpi/ic_launcher.png
b/cashier/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b959cd3
Binary files /dev/null and
b/cashier/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/cashier/src/main/res/navigation/nav_graph.xml
b/cashier/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..49f8881
--- /dev/null
+++ b/cashier/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,73 @@
+<?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/>
+ -->
+
+<navigation 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:id="@+id/nav_graph"
+ app:startDestination="@id/balanceFragment"
+ tools:ignore="UnusedNavigation">
+
+ <fragment
+ android:id="@+id/configFragment"
+ android:name="net.taler.cashier.ConfigFragment"
+ android:label="ConfigFragment"
+ tools:layout="@layout/fragment_config">
+ <action
+ android:id="@+id/action_configFragment_to_balanceFragment"
+ app:destination="@id/balanceFragment"
+ app:launchSingleTop="true"
+ app:popUpTo="@id/balanceFragment" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/balanceFragment"
+ android:name="net.taler.cashier.BalanceFragment"
+ android:label="fragment_balance"
+ tools:layout="@layout/fragment_balance">
+ <action
+ android:id="@+id/action_balanceFragment_to_transactionFragment"
+ app:destination="@id/transactionFragment" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/transactionFragment"
+ android:name="net.taler.cashier.withdraw.TransactionFragment"
+ android:label="fragment_transaction"
+ tools:layout="@layout/fragment_transaction">
+ <action
+ android:id="@+id/action_transactionFragment_to_errorFragment"
+ app:destination="@id/errorFragment"
+ app:launchSingleTop="true"
+ app:popUpTo="@+id/balanceFragment" />
+ <action
+ android:id="@+id/action_transactionFragment_to_balanceFragment"
+ app:destination="@id/balanceFragment"
+ app:launchSingleTop="true"
+ app:popUpTo="@+id/balanceFragment" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/errorFragment"
+ android:name="net.taler.cashier.withdraw.ErrorFragment"
+ tools:layout="@layout/fragment_error" />
+
+ <action
+ android:id="@+id/action_global_configFragment"
+ app:destination="@id/configFragment"
+ app:launchSingleTop="true" />
+
+</navigation>
diff --git a/cashier/src/main/res/values-night/colors.xml
b/cashier/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..55dde58
--- /dev/null
+++ b/cashier/src/main/res/values-night/colors.xml
@@ -0,0 +1,20 @@
+<?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>
+ <color name="background">#222222</color>
+</resources>
diff --git a/cashier/src/main/res/values/colors.xml
b/cashier/src/main/res/values/colors.xml
new file mode 100644
index 0000000..61338da
--- /dev/null
+++ b/cashier/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#1565C0</color>
+ <color name="colorPrimaryDark">#6A1B9A</color>
+ <color name="colorAccent">#D81B60</color>
+
+ <color name="background">#F1F1F1</color>
+ <color name="green">#388E3C</color>
+ <color name="red">#D32F2F</color>
+</resources>
diff --git a/cashier/src/main/res/values/dimens.xml
b/cashier/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..9d9d85a
--- /dev/null
+++ b/cashier/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+<resources>
+ <dimen name="default_margin">16dp</dimen>
+</resources>
diff --git a/cashier/src/main/res/values/ic_launcher_background.xml
b/cashier/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..3862264
--- /dev/null
+++ b/cashier/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="ic_launcher_background">#1565C0</color>
+</resources>
\ No newline at end of file
diff --git a/cashier/src/main/res/values/strings.xml
b/cashier/src/main/res/values/strings.xml
new file mode 100644
index 0000000..5df5bfa
--- /dev/null
+++ b/cashier/src/main/res/values/strings.xml
@@ -0,0 +1,39 @@
+<resources>
+ <string name="app_name">Taler Cashier</string>
+
+ <string name="config_bank_url">Bank API address</string>
+ <string name="config_username">Username</string>
+ <string name="config_password">Password</string>
+ <string name="config_button_save">Save</string>
+ <string name="config_bank_url_error">The address in invalid.</string>
+ <string name="config_username_error">Please enter your username</string>
+ <string name="config_error">Error retrieving configuration</string>
+ <string name="config_error_auth">Invalid username or password</string>
+ <string name="config_demo_hint">For testing, you can <![CDATA[<a
href="%s">create a test account at the demo bank</a>]]>.</string>
+
+ <string name="balance_current_label">Current balance</string>
+ <string name="balance_error">ERROR</string>
+ <string name="balance_offline">Offline. Please connect to the
Internet</string>
+ <string name="action_reconfigure">Reconfigure</string>
+ <string name="action_lock">Lock</string>
+
+ <string name="withdraw_input_amount">Amount</string>
+ <string name="withdraw_into">How much e-cash should be withdrawn?</string>
+ <string name="withdraw_error_zero">Enter positive amount</string>
+ <string name="withdraw_error_insufficient_balance">Insufficient
balance</string>
+ <string name="withdraw_error_fetch">Error communicating with bank</string>
+ <string name="withdraw_button_confirm">Withdraw</string>
+
+ <string name="transaction_intro">Scan code\nwith the Taler wallet app\nto
get</string>
+ <string name="transaction_intro_nfc">Scan code or use NFC\nwith the Taler
wallet app\nto get</string>
+ <string name="transaction_intro_scanned">Please confirm the
transaction!</string>
+ <string name="transaction_confirm">Confirm</string>
+ <string name="transaction_abort">Abort</string>
+ <string name="transaction_error">Transaction error</string>
+ <string name="transaction_aborted">Transaction aborted</string>
+ <string name="transaction_button_back">Go back</string>
+ <string name="transaction_last_success">Last Transaction: %s
withdrawn</string>
+ <string name="transaction_last_aborted">Last Transaction: Aborted</string>
+ <string name="transaction_last_error">Last Transaction: Error</string>
+
+</resources>
diff --git a/cashier/src/main/res/values/styles.xml
b/cashier/src/main/res/values/styles.xml
new file mode 100644
index 0000000..4339684
--- /dev/null
+++ b/cashier/src/main/res/values/styles.xml
@@ -0,0 +1,28 @@
+<resources>
+
+ <style name="AppTheme"
parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item
name="colorOnPrimary">@color/design_default_color_background</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorSecondary">@color/colorAccent</item>
+ <item
name="colorOnSecondary">@color/design_default_color_background</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+ <style name="AppTheme.NoActionBar">
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
+ </style>
+
+ <style name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.MaterialComponents.ActionBar" />
+
+ <style name="AppTheme.Toolbar"
parent="Widget.MaterialComponents.Toolbar.Primary" />
+
+ <style name="AmountButton" parent="Widget.MaterialComponents.Button">
+ <item name="android:minWidth">48dp</item>
+ <item name="android:layout_marginStart">@dimen/default_margin</item>
+ <item name="android:layout_marginEnd">@dimen/default_margin</item>
+ <item name="android:layout_marginTop">16dp</item>
+ </style>
+
+</resources>
diff --git a/cashier/src/main/res/xml/backup_descriptor.xml
b/cashier/src/main/res/xml/backup_descriptor.xml
new file mode 100644
index 0000000..a298494
--- /dev/null
+++ b/cashier/src/main/res/xml/backup_descriptor.xml
@@ -0,0 +1,19 @@
+<?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/>
+ -->
+
+<full-backup-content>
+
+</full-backup-content>
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..00f6d64
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# 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
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+#
http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled
with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=false
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
diff --git a/gradle/wrapper/gradle-wrapper.jar
b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties
b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..daff887
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Mar 18 13:36:36 BRT 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to
pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no
'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ;
then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\"
\"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ###
Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ###
Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5"
"$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5"
"$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5"
"$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ;
done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and
substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
"\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\""
org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder
on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS
to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your
PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS%
"-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%"
org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code
instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/merchant-terminal/.gitignore b/merchant-terminal/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/merchant-terminal/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/merchant-terminal/.gitlab-ci.yml b/merchant-terminal/.gitlab-ci.yml
new file mode 100644
index 0000000..4c03405
--- /dev/null
+++ b/merchant-terminal/.gitlab-ci.yml
@@ -0,0 +1,36 @@
+merchant_test:
+ stage: test
+ only:
+ changes:
+ - "merchant-terminal"
+ script: ./gradlew :merchant-terminal:lint :merchant-terminal:assembleRelease
+
+merchant_deploy_nightly:
+ stage: deploy
+ only:
+ refs:
+ - master
+ changes:
+ - "merchant-terminal"
+ script:
+ # Ensure that key exists
+ - test -z "$DEBUG_KEYSTORE" && exit 0
+ # Rename nightly app
+ - sed -i
+ 's,<string name="app_name">.*</string>,<string name="app_name">Merchant
PoS Nightly</string>,'
+ merchant-terminal/src/main/res/values*/strings.xml
+ # Set time-based version code
+ - export versionCode=$(date '+%s')
+ - sed -i "s,^\(\s*versionCode\) *[0-9].*,\1 $versionCode,"
merchant-terminal/build.gradle
+ # Add commit to version name
+ - export versionName=$(git rev-parse --short=7 HEAD)
+ - sed -i "s,^\(\s*versionName\ *\"[0-9].*\)\",\1 ($versionName)\","
merchant-terminal/build.gradle
+ # Set nightly application ID
+ - sed -i "s,^\(\s*applicationId\) \"*[a-z\.].*\",\1
\"net.taler.merchantpos.nightly\"," merchant-terminal/build.gradle
+ # Build the APK
+ - ./gradlew :merchant-terminal:assembleDebug
+ # START only needed while patch not accepted/released upstream
+ - apt update && apt install patch
+ - patch /usr/lib/python3/dist-packages/fdroidserver/nightly.py
nightly-stats.patch
+ # END
+ - CI_PROJECT_URL="https://gitlab.com/gnu-taler/fdroid-repo"
CI_PROJECT_PATH="gnu-taler/fdroid-repo" fdroid nightly -v
diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle
new file mode 100644
index 0000000..594cab3
--- /dev/null
+++ b/merchant-terminal/build.gradle
@@ -0,0 +1,76 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: "androidx.navigation.safeargs.kotlin"
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+ defaultConfig {
+ applicationId "net.taler.merchantpos"
+ minSdkVersion 26
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles
getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+
+ lintOptions {
+ abortOnError true
+ ignoreWarnings false
+ }
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.2.0'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation "androidx.recyclerview:recyclerview:1.1.0"
+ implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc01"
+
+ // Navigation
+ implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
+ implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
+
+ // ViewModel and LiveData
+ def lifecycle_version = "2.2.0"
+ implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
+ implementation
"androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
+
+ // HTTP Requests
+ implementation 'com.android.volley:volley:1.1.1'
+
+ // QR codes
+ implementation 'com.google.zxing:core:3.4.0'
+
+ implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
+
+ // JSON parsing and serialization
+ implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2"
+
+ testImplementation 'androidx.test.ext:junit:1.1.1'
+ testImplementation 'org.robolectric:robolectric:4.3.1'
+}
diff --git a/merchant-terminal/proguard-rules.pro
b/merchant-terminal/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/merchant-terminal/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/merchant-terminal/src/main/AndroidManifest.xml
b/merchant-terminal/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f52995f
--- /dev/null
+++ b/merchant-terminal/src/main/AndroidManifest.xml
@@ -0,0 +1,56 @@
+<?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/>
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="net.taler.merchantpos">
+
+ <uses-permission android:name="android.permission.NFC" />
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <uses-feature
+ android:name="android.hardware.nfc"
+ android:required="false" />
+
+ <uses-feature
+ android:name="android.hardware.telephony"
+ android:required="false" />
+
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_descriptor"
+ android:icon="@mipmap/ic_taler_logo"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_taler_logo_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme"
+ tools:ignore="GoogleAppIndexingWarning">
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name"
+ android:screenOrientation="landscape"
+ android:theme="@style/AppTheme.NoActionBar"
+ tools:ignore="LockedOrientationActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/merchant-terminal/src/main/ic_taler_logo-web.png
b/merchant-terminal/src/main/ic_taler_logo-web.png
new file mode 100644
index 0000000..e3b8075
Binary files /dev/null and b/merchant-terminal/src/main/ic_taler_logo-web.png
differ
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt
new file mode 100644
index 0000000..17ddd61
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.merchantpos
+
+import org.json.JSONObject
+
+data class Amount(val currency: String, val amount: String) {
+ @Suppress("unused")
+ fun isZero(): Boolean {
+ return amount.toDouble() == 0.0
+ }
+
+ companion object {
+ private const val FRACTIONAL_BASE = 1e8
+
+ @Suppress("unused")
+ fun fromJson(jsonAmount: JSONObject): Amount {
+ val amountCurrency = jsonAmount.getString("currency")
+ val amountValue = jsonAmount.getString("value")
+ val amountFraction = jsonAmount.getString("fraction")
+ val amountIntValue = Integer.parseInt(amountValue)
+ val amountIntFraction = Integer.parseInt(amountFraction)
+ return Amount(
+ amountCurrency,
+ (amountIntValue + amountIntFraction /
FRACTIONAL_BASE).toString()
+ )
+ }
+
+ fun fromString(strAmount: String): Amount {
+ val components = strAmount.split(":")
+ return Amount(components[0], components[1])
+ }
+ }
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt
new file mode 100644
index 0000000..0c6bdfa
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.merchantpos
+
+import android.content.Intent
+import android.content.Intent.ACTION_MAIN
+import android.content.Intent.CATEGORY_HOME
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.os.Bundle
+import android.os.Handler
+import android.view.MenuItem
+import android.widget.Toast
+import android.widget.Toast.LENGTH_SHORT
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.GravityCompat.START
+import androidx.lifecycle.Observer
+import androidx.navigation.NavController
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.setupWithNavController
+import
com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.app_bar_main.*
+
+class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener {
+
+ private val model: MainViewModel by viewModels()
+ private val nfcManager = NfcManager()
+
+ private lateinit var nav: NavController
+
+ private var reallyExit = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ model.paymentManager.payment.observe(this, Observer { payment ->
+ payment?.talerPayUri?.let {
+ nfcManager.setTagString(it)
+ }
+ })
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.navHostFragment) as
NavHostFragment
+ nav = navHostFragment.navController
+
+ nav_view.setupWithNavController(nav)
+ nav_view.setNavigationItemSelectedListener(this)
+
+ setSupportActionBar(toolbar)
+ val appBarConfiguration = AppBarConfiguration(nav.graph, drawer_layout)
+ toolbar.setupWithNavController(nav, appBarConfiguration)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (!model.configManager.config.isValid() &&
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)
+ }
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ // TODO should we only read tags when a payment is to be made?
+ NfcManager.start(this, nfcManager)
+ }
+
+ public override fun onPause() {
+ super.onPause()
+ NfcManager.stop(this)
+ }
+
+ override fun onNavigationItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.nav_order -> nav.navigate(R.id.action_global_order)
+ R.id.nav_history ->
nav.navigate(R.id.action_global_merchantHistory)
+ R.id.nav_settings ->
nav.navigate(R.id.action_global_merchantSettings)
+ }
+ drawer_layout.closeDrawer(START)
+ return true
+ }
+
+ override fun onBackPressed() {
+ val currentDestination = nav.currentDestination?.id
+ if (drawer_layout.isDrawerOpen(START)) {
+ drawer_layout.closeDrawer(START)
+ } else if (currentDestination == R.id.nav_settings &&
!model.configManager.config.isValid()) {
+ // we are in the configuration screen and need a config to continue
+ val intent = Intent(ACTION_MAIN).apply {
+ addCategory(CATEGORY_HOME)
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+ startActivity(intent)
+ } else if (currentDestination == R.id.nav_order) {
+ if (reallyExit) super.onBackPressed()
+ else {
+ // this closes the app and causes orders to be lost, so let's
confirm first
+ reallyExit = true
+ Toast.makeText(this, R.string.toast_back_to_exit,
LENGTH_SHORT).show()
+ Handler().postDelayed({ reallyExit = false }, 3000)
+ }
+ } else super.onBackPressed()
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt
new file mode 100644
index 0000000..3fe472d
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.merchantpos
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.android.volley.toolbox.Volley
+import
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.history.HistoryManager
+import net.taler.merchantpos.history.RefundManager
+import net.taler.merchantpos.order.OrderManager
+import net.taler.merchantpos.payment.PaymentManager
+
+class MainViewModel(app: Application) : AndroidViewModel(app) {
+
+ private val mapper = ObjectMapper()
+ .registerModule(KotlinModule())
+ .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
+ private val queue = Volley.newRequestQueue(app)
+
+ val orderManager = OrderManager(app, mapper)
+ val configManager = ConfigManager(app, viewModelScope, mapper,
queue).apply {
+ addConfigurationReceiver(orderManager)
+ }
+ val paymentManager = PaymentManager(configManager, queue, mapper)
+ val historyManager = HistoryManager(configManager, queue, mapper)
+ val refundManager = RefundManager(configManager, queue)
+
+ override fun onCleared() {
+ queue.cancelAll { !it.isCanceled }
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt
new file mode 100644
index 0000000..09c1470
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.merchantpos
+
+import android.app.Activity
+import android.content.Context
+import android.nfc.NfcAdapter
+import android.nfc.NfcAdapter.FLAG_READER_NFC_A
+import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+import android.util.Log
+import net.taler.merchantpos.Utils.hexStringToByteArray
+import org.json.JSONObject
+import java.io.ByteArrayOutputStream
+import java.net.URL
+import javax.net.ssl.HttpsURLConnection
+
+@Suppress("unused")
+private const val TALER_AID = "A0000002471001"
+
+class NfcManager : NfcAdapter.ReaderCallback {
+
+ companion object {
+ const val TAG = "taler-merchant"
+
+ /**
+ * Returns true if NFC is supported and false otherwise.
+ */
+ fun hasNfc(context: Context): Boolean {
+ return getNfcAdapter(context) != null
+ }
+
+ /**
+ * Enables NFC reader mode. Don't forget to call [stop] afterwards.
+ */
+ fun start(activity: Activity, nfcManager: NfcManager) {
+ getNfcAdapter(activity)?.enableReaderMode(activity, nfcManager,
nfcManager.flags, null)
+ }
+
+ /**
+ * Disables NFC reader mode. Call after [start].
+ */
+ fun stop(activity: Activity) {
+ getNfcAdapter(activity)?.disableReaderMode(activity)
+ }
+
+ private fun getNfcAdapter(context: Context): NfcAdapter? {
+ return NfcAdapter.getDefaultAdapter(context)
+ }
+ }
+
+ private val flags = FLAG_READER_NFC_A or FLAG_READER_SKIP_NDEF_CHECK
+
+ private var tagString: String? = null
+ private var currentTag: IsoDep? = null
+
+ fun setTagString(tagString: String) {
+ this.tagString = tagString
+ }
+
+ override fun onTagDiscovered(tag: Tag?) {
+
+ Log.v(TAG, "tag discovered")
+
+ val isoDep = IsoDep.get(tag)
+ isoDep.connect()
+
+ currentTag = isoDep
+
+ isoDep.transceive(apduSelectFile())
+
+ val tagString: String? = tagString
+ if (tagString != null) {
+ isoDep.transceive(apduPutTalerData(1, tagString.toByteArray()))
+ }
+
+ // FIXME: use better pattern for sleeps in between requests
+ // -> start with fast polling, poll more slowly if no requests are
coming
+
+ while (true) {
+ try {
+ val reqFrame = isoDep.transceive(apduGetData())
+ if (reqFrame.size < 2) {
+ Log.v(TAG, "request frame too small")
+ break
+ }
+ val req = ByteArray(reqFrame.size - 2)
+ if (req.isEmpty()) {
+ continue
+ }
+ reqFrame.copyInto(req, 0, 0, reqFrame.size - 2)
+ val jsonReq = JSONObject(req.toString(Charsets.UTF_8))
+ val reqId = jsonReq.getInt("id")
+ Log.v(TAG, "got request $jsonReq")
+ val jsonInnerReq = jsonReq.getJSONObject("request")
+ val method = jsonInnerReq.getString("method")
+ val urlStr = jsonInnerReq.getString("url")
+ Log.v(TAG, "url '$urlStr'")
+ Log.v(TAG, "method '$method'")
+ val url = URL(urlStr)
+ val conn: HttpsURLConnection = url.openConnection() as
HttpsURLConnection
+ conn.setRequestProperty("Accept", "application/json")
+ conn.connectTimeout = 5000
+ conn.doInput = true
+ when (method) {
+ "get" -> {
+ conn.requestMethod = "GET"
+ }
+ "postJson" -> {
+ conn.requestMethod = "POST"
+ conn.doOutput = true
+ conn.setRequestProperty("Content-Type",
"application/json; utf-8")
+ val body = jsonInnerReq.getString("body")
+
conn.outputStream.write(body.toByteArray(Charsets.UTF_8))
+ }
+ else -> {
+ throw Exception("method not supported")
+ }
+ }
+ Log.v(TAG, "connecting")
+ conn.connect()
+ Log.v(TAG, "connected")
+
+ val statusCode = conn.responseCode
+ val tunnelResp = JSONObject()
+ tunnelResp.put("id", reqId)
+ tunnelResp.put("status", conn.responseCode)
+
+ if (statusCode == 200) {
+ val stream = conn.inputStream
+ val httpResp = stream.buffered().readBytes()
+ tunnelResp.put("responseJson",
JSONObject(httpResp.toString(Charsets.UTF_8)))
+ }
+
+ Log.v(TAG, "sending: $tunnelResp")
+
+ isoDep.transceive(apduPutTalerData(2,
tunnelResp.toString().toByteArray()))
+ } catch (e: Exception) {
+ Log.v(TAG, "exception during NFC loop: $e")
+ break
+ }
+ }
+
+ isoDep.close()
+ }
+
+ private fun writeApduLength(stream: ByteArrayOutputStream, size: Int) {
+ when {
+ size == 0 -> {
+ // No size field needed!
+ }
+ size <= 255 -> // One byte size field
+ stream.write(size)
+ size <= 65535 -> {
+ stream.write(0)
+ // FIXME: is this supposed to be little or big endian?
+ stream.write(size and 0xFF)
+ stream.write((size ushr 8) and 0xFF)
+ }
+ else -> throw Error("payload too big")
+ }
+ }
+
+ private fun apduSelectFile(): ByteArray {
+ return hexStringToByteArray("00A4040007A0000002471001")
+ }
+
+ private fun apduPutData(payload: ByteArray): ByteArray {
+ val stream = ByteArrayOutputStream()
+
+ // Class
+ stream.write(0x00)
+
+ // Instruction 0xDA = put data
+ stream.write(0xDA)
+
+ // Instruction parameters
+ // (proprietary encoding)
+ stream.write(0x01)
+ stream.write(0x00)
+
+ writeApduLength(stream, payload.size)
+
+ stream.write(payload)
+
+ return stream.toByteArray()
+ }
+
+ private fun apduPutTalerData(talerInst: Int, payload: ByteArray):
ByteArray {
+ val realPayload = ByteArrayOutputStream()
+ realPayload.write(talerInst)
+ realPayload.write(payload)
+ return apduPutData(realPayload.toByteArray())
+ }
+
+ private fun apduGetData(): ByteArray {
+ val stream = ByteArrayOutputStream()
+
+ // Class
+ stream.write(0x00)
+
+ // Instruction 0xCA = get data
+ stream.write(0xCA)
+
+ // Instruction parameters
+ // (proprietary encoding)
+ stream.write(0x01)
+ stream.write(0x00)
+
+ // Max expected response size, two
+ // zero bytes denotes 65536
+ stream.write(0x0)
+ stream.write(0x0)
+
+ return stream.toByteArray()
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt
new file mode 100644
index 0000000..595e7ac
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.merchantpos
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.RGB_565
+import android.graphics.Color.BLACK
+import android.graphics.Color.WHITE
+import com.google.zxing.BarcodeFormat.QR_CODE
+import com.google.zxing.qrcode.QRCodeWriter
+
+object QrCodeManager {
+
+ fun makeQrCode(text: String, size: Int = 256): Bitmap {
+ val qrCodeWriter = QRCodeWriter()
+ val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size)
+ val height = bitMatrix.height
+ val width = bitMatrix.width
+ val bmp = Bitmap.createBitmap(width, height, RGB_565)
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ bmp.setPixel(x, y, if (bitMatrix.get(x, y)) BLACK else WHITE)
+ }
+ }
+ return bmp
+ }
+
+}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt
new file mode 100644
index 0000000..a0c30d6
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.merchantpos
+
+import android.content.Context
+import android.text.format.DateUtils.DAY_IN_MILLIS
+import android.text.format.DateUtils.FORMAT_ABBREV_MONTH
+import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
+import android.text.format.DateUtils.FORMAT_NO_YEAR
+import android.text.format.DateUtils.FORMAT_SHOW_DATE
+import android.text.format.DateUtils.FORMAT_SHOW_TIME
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.formatDateTime
+import android.text.format.DateUtils.getRelativeTimeSpanString
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.Observer
+import androidx.navigation.NavController
+import androidx.navigation.NavDirections
+import androidx.navigation.fragment.findNavController
+import
com.google.android.material.snackbar.BaseTransientBottomBar.ANIMATION_MODE_FADE
+import com.google.android.material.snackbar.BaseTransientBottomBar.Duration
+import com.google.android.material.snackbar.Snackbar.make
+
+object Utils {
+
+ private const val HEX_CHARS = "0123456789ABCDEF"
+
+ fun hexStringToByteArray(data: String): ByteArray {
+ val result = ByteArray(data.length / 2)
+
+ for (i in data.indices step 2) {
+ val firstIndex = HEX_CHARS.indexOf(data[i])
+ val secondIndex = HEX_CHARS.indexOf(data[i + 1])
+
+ val octet = firstIndex.shl(4).or(secondIndex)
+ result[i.shr(1)] = octet.toByte()
+ }
+ return result
+ }
+
+
+ private val HEX_CHARS_ARRAY = HEX_CHARS.toCharArray()
+
+ @Suppress("unused")
+ fun toHex(byteArray: ByteArray): String {
+ val result = StringBuffer()
+
+ byteArray.forEach {
+ val octet = it.toInt()
+ val firstIndex = (octet and 0xF0).ushr(4)
+ val secondIndex = octet and 0x0F
+ result.append(HEX_CHARS_ARRAY[firstIndex])
+ result.append(HEX_CHARS_ARRAY[secondIndex])
+ }
+ return result.toString()
+ }
+
+}
+
+fun View.fadeIn(endAction: () -> Unit = {}) {
+ if (visibility == VISIBLE) return
+ alpha = 0f
+ visibility = VISIBLE
+ animate().alpha(1f).withEndAction {
+ if (context != null) endAction.invoke()
+ }.start()
+}
+
+fun View.fadeOut(endAction: () -> Unit = {}) {
+ if (visibility == INVISIBLE) return
+ animate().alpha(0f).withEndAction {
+ if (context == null) return@withEndAction
+ visibility = INVISIBLE
+ alpha = 1f
+ endAction.invoke()
+ }.start()
+}
+
+fun topSnackbar(view: View, text: CharSequence, @Duration duration: Int) {
+ make(view, text, duration)
+ .setAnimationMode(ANIMATION_MODE_FADE)
+ .setAnchorView(R.id.navHostFragment)
+ .show()
+}
+
+fun topSnackbar(view: View, @StringRes resId: Int, @Duration duration: Int) {
+ topSnackbar(view, view.resources.getText(resId), duration)
+}
+
+fun NavDirections.navigate(nav: NavController) = nav.navigate(this)
+
+fun Fragment.navigate(directions: NavDirections) =
findNavController().navigate(directions)
+
+fun Long.toRelativeTime(context: Context): CharSequence {
+ val now = System.currentTimeMillis()
+ return if (now - this > DAY_IN_MILLIS * 2) {
+ val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or
FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR
+ formatDateTime(context, this, flags)
+ } else getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS,
FORMAT_ABBREV_RELATIVE)
+}
+
+class CombinedLiveData<T, K, S>(
+ source1: LiveData<T>,
+ source2: LiveData<K>,
+ private val combine: (data1: T?, data2: K?) -> S
+) : MediatorLiveData<S>() {
+
+ private var data1: T? = null
+ private var data2: K? = null
+
+ init {
+ super.addSource(source1) { t ->
+ data1 = t
+ value = combine(data1, data2)
+ }
+ super.addSource(source2) { k ->
+ data2 = k
+ value = combine(data1, data2)
+ }
+ }
+
+ override fun <S : Any?> addSource(source: LiveData<S>, onChanged:
Observer<in S>) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun <T : Any?> removeSource(toRemote: LiveData<T>) {
+ throw UnsupportedOperationException()
+ }
+}
+
+/**
+ * Use this with 'when' expressions when you need it to handle all
possibilities/branches.
+ */
+val <T> T.exhaustive: T
+ get() = this
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
new file mode 100644
index 0000000..c370e33
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.merchantpos.config
+
+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.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import
net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToMerchantSettings
+import
net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToOrder
+import net.taler.merchantpos.navigate
+
+class ConfigFetcherFragment : Fragment() {
+
+ private val model: MainViewModel by activityViewModels()
+ private val configManager by lazy { model.configManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_config_fetcher, container,
false)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ configManager.fetchConfig(configManager.config, false)
+ configManager.configUpdateResult.observe(viewLifecycleOwner, Observer
{ result ->
+ when (result) {
+ null -> return@Observer
+ is ConfigUpdateResult.Error -> onNetworkError(result.msg)
+ is ConfigUpdateResult.Success -> {
+ actionConfigFetcherToOrder().navigate(findNavController())
+ }
+ }
+ })
+ }
+
+ private fun onNetworkError(msg: String) {
+ Snackbar.make(view!!, msg, LENGTH_SHORT).show()
+ actionConfigFetcherToMerchantSettings().navigate(findNavController())
+ }
+
+}
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
new file mode 100644
index 0000000..edb8059
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.merchantpos.config
+
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.util.Base64.NO_WRAP
+import android.util.Base64.encodeToString
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.volley.Request.Method.GET
+import com.android.volley.RequestQueue
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
+import com.android.volley.VolleyError
+import com.android.volley.toolbox.JsonObjectRequest
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import net.taler.merchantpos.R
+import org.json.JSONObject
+
+private const val SETTINGS_NAME = "taler-merchant-terminal"
+
+private const val SETTINGS_CONFIG_URL = "configUrl"
+private const val SETTINGS_USERNAME = "username"
+private const val SETTINGS_PASSWORD = "password"
+
+internal const val CONFIG_URL_DEMO =
"https://docs.taler.net/_static/sample-pos-config.json"
+internal const val CONFIG_USERNAME_DEMO = ""
+internal const val CONFIG_PASSWORD_DEMO = ""
+
+private val TAG = ConfigManager::class.java.simpleName
+
+interface ConfigurationReceiver {
+ /**
+ * Returns null if the configuration was valid, or a error string for user
display otherwise.
+ */
+ suspend fun onConfigurationReceived(json: JSONObject, currency: String):
String?
+}
+
+class ConfigManager(
+ private val context: Context,
+ private val scope: CoroutineScope,
+ private val mapper: ObjectMapper,
+ private val queue: RequestQueue
+) {
+
+ private val prefs = context.getSharedPreferences(SETTINGS_NAME,
MODE_PRIVATE)
+ private val configurationReceivers = ArrayList<ConfigurationReceiver>()
+
+ var config = Config(
+ configUrl = prefs.getString(SETTINGS_CONFIG_URL, CONFIG_URL_DEMO)!!,
+ username = prefs.getString(SETTINGS_USERNAME, CONFIG_USERNAME_DEMO)!!,
+ password = prefs.getString(SETTINGS_PASSWORD, CONFIG_PASSWORD_DEMO)!!
+ )
+ var merchantConfig: MerchantConfig? = null
+ private set
+
+ private val mConfigUpdateResult = MutableLiveData<ConfigUpdateResult>()
+ val configUpdateResult: LiveData<ConfigUpdateResult> = mConfigUpdateResult
+
+ fun addConfigurationReceiver(receiver: ConfigurationReceiver) {
+ configurationReceivers.add(receiver)
+ }
+
+ @UiThread
+ fun fetchConfig(config: Config, save: Boolean, savePassword: Boolean =
false) {
+ mConfigUpdateResult.value = null
+ val configToSave = if (save) {
+ if (savePassword) config else config.copy(password = "")
+ } else null
+
+ val stringRequest = object : JsonObjectRequest(GET, config.configUrl,
null,
+ Listener { onConfigReceived(it, configToSave) },
+ ErrorListener { onNetworkError(it) }
+ ) {
+ // send basic auth header
+ override fun getHeaders(): MutableMap<String, String> {
+ val credentials = "${config.username}:${config.password}"
+ val auth = ("Basic ${encodeToString(credentials.toByteArray(),
NO_WRAP)}")
+ return mutableMapOf("Authorization" to auth)
+ }
+ }
+ queue.add(stringRequest)
+ }
+
+ @UiThread
+ private fun onConfigReceived(json: JSONObject, config: Config?) {
+ val merchantConfig: MerchantConfig = try {
+ mapper.readValue(json.getString("config"))
+ } catch (e: Exception) {
+ Log.e(TAG, "Error parsing merchant config", e)
+ val msg = context.getString(R.string.config_error_malformed)
+ mConfigUpdateResult.value = ConfigUpdateResult.Error(msg)
+ return
+ }
+
+ val params = mapOf("instance" to merchantConfig.instance)
+ val req = MerchantRequest(GET, merchantConfig, "config", params, null,
+ Listener { onMerchantConfigReceived(config, json, merchantConfig,
it) },
+ ErrorListener { onNetworkError(it) }
+ )
+ queue.add(req)
+ }
+
+ private fun onMerchantConfigReceived(
+ newConfig: Config?,
+ configJson: JSONObject,
+ merchantConfig: MerchantConfig,
+ json: JSONObject
+ ) = scope.launch(Dispatchers.Default) {
+ val currency = json.getString("currency")
+
+ for (receiver in configurationReceivers) {
+ val result = try {
+ receiver.onConfigurationReceived(configJson, currency)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error handling configuration by
${receiver::class.java.simpleName}", e)
+ context.getString(R.string.config_error_unknown)
+ }
+ if (result != null) { // error
+ mConfigUpdateResult.postValue(ConfigUpdateResult.Error(result))
+ return@launch
+ }
+ }
+ newConfig?.let {
+ config = it
+ saveConfig(it)
+ }
+ this@ConfigManager.merchantConfig = merchantConfig.copy(currency =
currency)
+ mConfigUpdateResult.postValue(ConfigUpdateResult.Success(currency))
+ }
+
+ fun forgetPassword() {
+ config = config.copy(password = "")
+ saveConfig(config)
+ merchantConfig = null
+ }
+
+ private fun saveConfig(config: Config) {
+ prefs.edit()
+ .putString(SETTINGS_CONFIG_URL, config.configUrl)
+ .putString(SETTINGS_USERNAME, config.username)
+ .putString(SETTINGS_PASSWORD, config.password)
+ .apply()
+ }
+
+ @UiThread
+ private fun onNetworkError(it: VolleyError?) {
+ val msg = context.getString(
+ if (it?.networkResponse?.statusCode == 401)
R.string.config_auth_error
+ else R.string.config_error_network
+ )
+ mConfigUpdateResult.value = ConfigUpdateResult.Error(msg)
+ }
+
+}
+
+sealed class ConfigUpdateResult {
+ data class Error(val msg: String) : ConfigUpdateResult()
+ data class Success(val currency: String) : ConfigUpdateResult()
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
new file mode 100644
index 0000000..2050e28
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.merchantpos.config
+
+import android.net.Uri
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class Config(
+ val configUrl: String,
+ val username: String,
+ val password: String
+) {
+ fun isValid() = !configUrl.isBlank()
+ fun hasPassword() = !password.isBlank()
+}
+
+data class MerchantConfig(
+ @JsonProperty("base_url")
+ val baseUrl: String,
+ val instance: String,
+ @JsonProperty("api_key")
+ val apiKey: String,
+ val currency: String?
+) {
+ fun urlFor(endpoint: String, params: Map<String, String>?): String {
+ val uriBuilder = Uri.parse(baseUrl).buildUpon()
+ uriBuilder.appendPath(endpoint)
+ params?.forEach {
+ uriBuilder.appendQueryParameter(it.key, it.value)
+ }
+ return uriBuilder.toString()
+ }
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
new file mode 100644
index 0000000..aad1c93
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.merchantpos.config
+
+import android.net.Uri
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.android.synthetic.main.fragment_merchant_config.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import
net.taler.merchantpos.config.MerchantConfigFragmentDirections.Companion.actionSettingsToOrder
+import net.taler.merchantpos.navigate
+import net.taler.merchantpos.topSnackbar
+
+/**
+ * Fragment that displays merchant settings.
+ */
+class MerchantConfigFragment : Fragment() {
+
+ private val model: MainViewModel by activityViewModels()
+ private val configManager by lazy { model.configManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_merchant_config, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ configUrlView.editText!!.setOnFocusChangeListener { _, hasFocus ->
+ if (!hasFocus) checkForUrlCredentials()
+ }
+ okButton.setOnClickListener {
+ checkForUrlCredentials()
+ val inputUrl = configUrlView.editText!!.text
+ val url = if (inputUrl.startsWith("http")) {
+ inputUrl.toString()
+ } else {
+ "https://$inputUrl".also {
configUrlView.editText!!.setText(it) }
+ }
+ progressBar.visibility = VISIBLE
+ okButton.visibility = INVISIBLE
+ val config = Config(
+ configUrl = url,
+ username = usernameView.editText!!.text.toString(),
+ password = passwordView.editText!!.text.toString()
+ )
+ configManager.fetchConfig(config, true,
savePasswordCheckBox.isChecked)
+ configManager.configUpdateResult.observe(viewLifecycleOwner,
Observer { result ->
+ if (onConfigUpdate(result)) {
+
configManager.configUpdateResult.removeObservers(viewLifecycleOwner)
+ }
+ })
+ }
+ forgetPasswordButton.setOnClickListener {
+ configManager.forgetPassword()
+ passwordView.editText!!.text = null
+ forgetPasswordButton.visibility = GONE
+ }
+ configDocsView.movementMethod = LinkMovementMethod.getInstance()
+ updateView(savedInstanceState == null)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ // focus password if this is the only empty field
+ if (passwordView.editText!!.text.isBlank()
+ && !configUrlView.editText!!.text.isBlank()
+ && !usernameView.editText!!.text.isBlank()
+ ) {
+ passwordView.requestFocus()
+ }
+ }
+
+ private fun updateView(isInitialization: Boolean = false) {
+ val config = configManager.config
+ configUrlView.editText!!.setText(
+ if (isInitialization && config.configUrl.isBlank()) CONFIG_URL_DEMO
+ else config.configUrl
+ )
+ usernameView.editText!!.setText(
+ if (isInitialization && config.username.isBlank())
CONFIG_USERNAME_DEMO
+ else config.username
+ )
+ passwordView.editText!!.setText(
+ if (isInitialization && config.password.isBlank())
CONFIG_PASSWORD_DEMO
+ else config.password
+ )
+ forgetPasswordButton.visibility = if (config.hasPassword()) VISIBLE
else GONE
+ }
+
+ private fun checkForUrlCredentials() {
+ val text = configUrlView.editText!!.text.toString()
+ Uri.parse(text)?.userInfo?.let { userInfo ->
+ if (userInfo.contains(':')) {
+ val (user, pass) = userInfo.split(':')
+ val strippedUrl = text.replace("${userInfo}@", "")
+ configUrlView.editText!!.setText(strippedUrl)
+ usernameView.editText!!.setText(user)
+ passwordView.editText!!.setText(pass)
+ }
+ }
+ }
+
+ /**
+ * Processes updated config and returns true, if observer can be removed.
+ */
+ private fun onConfigUpdate(result: ConfigUpdateResult?) = when (result) {
+ null -> false
+ is ConfigUpdateResult.Error -> {
+ onError(result.msg)
+ true
+ }
+ is ConfigUpdateResult.Success -> {
+ onConfigReceived(result.currency)
+ true
+ }
+ }
+
+ private fun onConfigReceived(currency: String) {
+ onResultReceived()
+ updateView()
+ topSnackbar(view!!, getString(R.string.config_changed, currency),
LENGTH_LONG)
+ actionSettingsToOrder().navigate(findNavController())
+ }
+
+ private fun onError(msg: String) {
+ onResultReceived()
+ Snackbar.make(view!!, msg, LENGTH_LONG).show()
+ }
+
+ private fun onResultReceived() {
+ progressBar.visibility = INVISIBLE
+ okButton.visibility = VISIBLE
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt
new file mode 100644
index 0000000..8d95378
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.merchantpos.config
+
+
+import android.util.ArrayMap
+import com.android.volley.Response
+import com.android.volley.toolbox.JsonObjectRequest
+import org.json.JSONObject
+
+class MerchantRequest(
+ method: Int,
+ private val merchantConfig: MerchantConfig,
+ endpoint: String,
+ params: Map<String, String>?,
+ jsonRequest: JSONObject?,
+ listener: Response.Listener<JSONObject>,
+ errorListener: Response.ErrorListener
+) :
+ JsonObjectRequest(method, merchantConfig.urlFor(endpoint, params),
jsonRequest, listener, errorListener) {
+
+ override fun getHeaders(): MutableMap<String, String> {
+ val headerMap = ArrayMap<String, String>()
+ headerMap["Authorization"] = "ApiKey " + merchantConfig.apiKey
+ return headerMap
+ }
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
new file mode 100644
index 0000000..594e7cc
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.merchantpos.history
+
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.volley.Request.Method.GET
+import com.android.volley.Request.Method.POST
+import com.android.volley.RequestQueue
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
+import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import net.taler.merchantpos.Amount
+import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.config.MerchantRequest
+import org.json.JSONObject
+
+@JsonInclude(NON_EMPTY)
+class Timestamp(
+ @JsonProperty("t_ms")
+ val ms: Long
+)
+
+data class HistoryItem(
+ @JsonProperty("order_id")
+ val orderId: String,
+ @JsonProperty("amount")
+ val amountStr: String,
+ val summary: String,
+ val timestamp: Timestamp
+) {
+ @get:JsonIgnore
+ val amount: Amount by lazy { Amount.fromString(amountStr) }
+
+ @get:JsonIgnore
+ val time = timestamp.ms
+}
+
+sealed class HistoryResult {
+ object Error : HistoryResult()
+ class Success(val items: List<HistoryItem>) : HistoryResult()
+}
+
+class HistoryManager(
+ private val configManager: ConfigManager,
+ private val queue: RequestQueue,
+ private val mapper: ObjectMapper
+) {
+
+ private val mIsLoading = MutableLiveData(false)
+ val isLoading: LiveData<Boolean> = mIsLoading
+
+ private val mItems = MutableLiveData<HistoryResult>()
+ val items: LiveData<HistoryResult> = mItems
+
+ @UiThread
+ internal fun fetchHistory() {
+ mIsLoading.value = true
+ val merchantConfig = configManager.merchantConfig!!
+ val params = mapOf("instance" to merchantConfig.instance)
+ val req = MerchantRequest(GET, merchantConfig, "history", params, null,
+ Listener { onHistoryResponse(it) },
+ ErrorListener { onHistoryError() })
+ queue.add(req)
+ }
+
+ @UiThread
+ private fun onHistoryResponse(body: JSONObject) {
+ mIsLoading.value = false
+ val items = arrayListOf<HistoryItem>()
+ val historyJson = body.getJSONArray("history")
+ for (i in 0 until historyJson.length()) {
+ val historyItem: HistoryItem =
mapper.readValue(historyJson.getString(i))
+ items.add(historyItem)
+ }
+ mItems.value = HistoryResult.Success(items)
+ }
+
+ @UiThread
+ private fun onHistoryError() {
+ mIsLoading.value = false
+ mItems.value = HistoryResult.Error
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
new file mode 100644
index 0000000..0c53f71
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.merchantpos.history
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import kotlinx.android.synthetic.main.fragment_merchant_history.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.exhaustive
+import net.taler.merchantpos.history.HistoryItemAdapter.HistoryItemViewHolder
+import
net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionGlobalMerchantSettings
+import
net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionNavHistoryToRefundFragment
+import net.taler.merchantpos.navigate
+import net.taler.merchantpos.toRelativeTime
+import java.util.*
+
+private interface RefundClickListener {
+ fun onRefundClicked(item: HistoryItem)
+}
+
+/**
+ * Fragment to display the merchant's payment history, received from the
backend.
+ */
+class MerchantHistoryFragment : Fragment(), RefundClickListener {
+
+ companion object {
+ const val TAG = "taler-merchant"
+ }
+
+ private val model: MainViewModel by activityViewModels()
+ private val historyManager by lazy { model.historyManager }
+ private val refundManager by lazy { model.refundManager }
+
+ private val historyListAdapter = HistoryItemAdapter(this)
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_merchant_history, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ list_history.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ addItemDecoration(DividerItemDecoration(context, VERTICAL))
+ adapter = historyListAdapter
+ }
+
+ swipeRefresh.setOnRefreshListener {
+ Log.v(TAG, "refreshing!")
+ historyManager.fetchHistory()
+ }
+ historyManager.isLoading.observe(viewLifecycleOwner, Observer {
loading ->
+ Log.v(TAG, "setting refreshing to $loading")
+ swipeRefresh.isRefreshing = loading
+ })
+ historyManager.items.observe(viewLifecycleOwner, Observer { result ->
+ when (result) {
+ is HistoryResult.Error -> onError()
+ is HistoryResult.Success ->
historyListAdapter.setData(result.items)
+ }.exhaustive
+ })
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (model.configManager.merchantConfig?.instance == null) {
+ navigate(actionGlobalMerchantSettings())
+ } else {
+ historyManager.fetchHistory()
+ }
+ }
+
+ private fun onError() {
+ Snackbar.make(view!!, R.string.error_network, LENGTH_SHORT).show()
+ }
+
+ override fun onRefundClicked(item: HistoryItem) {
+ refundManager.startRefund(item)
+ navigate(actionNavHistoryToRefundFragment())
+ }
+
+}
+
+private class HistoryItemAdapter(private val listener: RefundClickListener) :
+ Adapter<HistoryItemViewHolder>() {
+
+ private val items = ArrayList<HistoryItem>()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
HistoryItemViewHolder {
+ val v =
+
LayoutInflater.from(parent.context).inflate(R.layout.list_item_history, parent,
false)
+ return HistoryItemViewHolder(v)
+ }
+
+ override fun getItemCount() = items.size
+
+ override fun onBindViewHolder(holder: HistoryItemViewHolder, position:
Int) {
+ holder.bind(items[position])
+ }
+
+ fun setData(items: List<HistoryItem>) {
+ this.items.clear()
+ this.items.addAll(items)
+ this.notifyDataSetChanged()
+ }
+
+ private inner class HistoryItemViewHolder(private val v: View) :
ViewHolder(v) {
+
+ private val orderSummaryView: TextView =
v.findViewById(R.id.orderSummaryView)
+ private val orderAmountView: TextView =
v.findViewById(R.id.orderAmountView)
+ private val orderTimeView: TextView =
v.findViewById(R.id.orderTimeView)
+ private val orderIdView: TextView = v.findViewById(R.id.orderIdView)
+ private val refundButton: ImageButton =
v.findViewById(R.id.refundButton)
+
+ fun bind(item: HistoryItem) {
+ orderSummaryView.text = item.summary
+ val amount = item.amount
+ @SuppressLint("SetTextI18n")
+ orderAmountView.text = "${amount.amount} ${amount.currency}"
+ orderIdView.text = v.context.getString(R.string.history_ref_no,
item.orderId)
+ orderTimeView.text = item.time.toRelativeTime(v.context)
+ refundButton.setOnClickListener { listener.onRefundClicked(item) }
+ }
+
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
new file mode 100644
index 0000000..1797cea
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.merchantpos.history
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.android.synthetic.main.fragment_refund.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.fadeIn
+import net.taler.merchantpos.fadeOut
+import
net.taler.merchantpos.history.RefundFragmentDirections.Companion.actionRefundFragmentToRefundUriFragment
+import net.taler.merchantpos.history.RefundResult.Error
+import net.taler.merchantpos.history.RefundResult.PastDeadline
+import net.taler.merchantpos.history.RefundResult.Success
+import net.taler.merchantpos.navigate
+
+class RefundFragment : Fragment() {
+
+ private val model: MainViewModel by activityViewModels()
+ private val refundManager by lazy { model.refundManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_refund, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val item = refundManager.toBeRefunded ?: throw IllegalStateException()
+ amountInputView.setText(item.amount.amount)
+ currencyView.text = item.amount.currency
+ abortButton.setOnClickListener { findNavController().navigateUp() }
+ refundButton.setOnClickListener { onRefundButtonClicked(item) }
+
+ refundManager.refundResult.observe(viewLifecycleOwner, Observer {
result ->
+ onRefundResultChanged(result)
+ })
+ }
+
+ private fun onRefundButtonClicked(item: HistoryItem) {
+ val inputAmount = amountInputView.text.toString().toDouble()
+ if (inputAmount > item.amount.amount.toDouble()) {
+ amountView.error = getString(R.string.refund_error_max_amount,
item.amount.amount)
+ return
+ }
+ if (inputAmount <= 0.0) {
+ amountView.error = getString(R.string.refund_error_zero)
+ return
+ }
+ amountView.error = null
+ refundButton.fadeOut()
+ progressBar.fadeIn()
+ refundManager.refund(item, inputAmount,
reasonInputView.text.toString())
+ }
+
+ private fun onRefundResultChanged(result: RefundResult?): Any = when
(result) {
+ Error -> onError(R.string.refund_error_backend)
+ PastDeadline -> onError(R.string.refund_error_deadline)
+ is Success -> {
+ progressBar.fadeOut()
+ refundButton.fadeIn()
+ navigate(actionRefundFragmentToRefundUriFragment())
+ }
+ null -> { // no-op
+ }
+ }
+
+ private fun onError(@StringRes res: Int) {
+ Snackbar.make(view!!, res, LENGTH_LONG).show()
+ progressBar.fadeOut()
+ refundButton.fadeIn()
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt
new file mode 100644
index 0000000..270b3b8
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.merchantpos.history
+
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.volley.Request.Method.POST
+import com.android.volley.RequestQueue
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
+import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.config.MerchantRequest
+import org.json.JSONObject
+
+sealed class RefundResult {
+ object Error : RefundResult()
+ object PastDeadline : RefundResult()
+ class Success(
+ val refundUri: String,
+ val item: HistoryItem,
+ val amount: Double,
+ val reason: String
+ ) : RefundResult()
+}
+
+class RefundManager(
+ private val configManager: ConfigManager,
+ private val queue: RequestQueue
+) {
+
+ var toBeRefunded: HistoryItem? = null
+ private set
+
+ private val mRefundResult = MutableLiveData<RefundResult>()
+ internal val refundResult: LiveData<RefundResult> = mRefundResult
+
+ @UiThread
+ internal fun startRefund(item: HistoryItem) {
+ toBeRefunded = item
+ mRefundResult.value = null
+ }
+
+ @UiThread
+ internal fun refund(item: HistoryItem, amount: Double, reason: String) {
+ val merchantConfig = configManager.merchantConfig!!
+ val refundRequest = mapOf(
+ "order_id" to item.orderId,
+ "refund" to "${item.amount.currency}:$amount",
+ "reason" to reason
+ )
+ val body = JSONObject(refundRequest)
+ val req = MerchantRequest(POST, merchantConfig, "refund", null, body,
+ Listener { onRefundResponse(it, item, amount, reason) },
+ ErrorListener { onRefundError() }
+ )
+ queue.add(req)
+ }
+
+ @UiThread
+ private fun onRefundResponse(
+ json: JSONObject,
+ item: HistoryItem,
+ amount: Double,
+ reason: String
+ ) {
+ if (!json.has("contract_terms")) {
+ Log.e("TEST", "json: $json")
+ onRefundError()
+ return
+ }
+
+ val contractTerms = json.getJSONObject("contract_terms")
+ val refundDeadline = if (contractTerms.has("refund_deadline")) {
+ contractTerms.getJSONObject("refund_deadline").getLong("t_ms")
+ } else null
+ val autoRefund = contractTerms.has("auto_refund")
+ val refundUri = json.getString("taler_refund_uri")
+
+ Log.e("TEST", "refundDeadline: $refundDeadline")
+ if (refundDeadline != null) Log.e(
+ "TEST",
+ "refundDeadline passed: ${System.currentTimeMillis() >
refundDeadline}"
+ )
+ Log.e("TEST", "autoRefund: $autoRefund")
+ Log.e("TEST", "refundUri: $refundUri")
+
+ mRefundResult.value = RefundResult.Success(refundUri, item, amount,
reason)
+ }
+
+ @UiThread
+ private fun onRefundError() {
+ mRefundResult.value = RefundResult.Error
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
new file mode 100644
index 0000000..f2bd569
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.merchantpos.history
+
+import android.annotation.SuppressLint
+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 kotlinx.android.synthetic.main.fragment_refund_uri.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.NfcManager.Companion.hasNfc
+import net.taler.merchantpos.QrCodeManager.makeQrCode
+import net.taler.merchantpos.R
+
+class RefundUriFragment : Fragment() {
+
+ private val model: MainViewModel by activityViewModels()
+ private val refundManager by lazy { model.refundManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_refund_uri, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val result = refundManager.refundResult.value
+ if (result !is RefundResult.Success) throw IllegalStateException()
+
+ refundQrcodeView.setImageBitmap(makeQrCode(result.refundUri))
+
+ val introRes =
+ if (hasNfc(requireContext())) R.string.refund_intro_nfc else
R.string.refund_intro
+ refundIntroView.setText(introRes)
+
+ @SuppressLint("SetTextI18n")
+ refundAmountView.text = "${result.amount}
${result.item.amount.currency}"
+
+ refundRefView.text =
+ getString(R.string.refund_order_ref, result.item.orderId,
result.reason)
+
+ cancelRefundButton.setOnClickListener {
findNavController().navigateUp() }
+ }
+
+}
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
new file mode 100644
index 0000000..34b97c0
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.merchantpos.order
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.ViewGroup
+import android.widget.Button
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import kotlinx.android.synthetic.main.fragment_categories.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.order.CategoryAdapter.CategoryViewHolder
+
+interface CategorySelectionListener {
+ fun onCategorySelected(category: Category)
+}
+
+class CategoriesFragment : Fragment(), CategorySelectionListener {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val orderManager by lazy { viewModel.orderManager }
+ private val adapter = CategoryAdapter(this)
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_categories, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ categoriesList.apply {
+ adapter = this@CategoriesFragment.adapter
+ layoutManager = LinearLayoutManager(requireContext())
+ }
+
+ orderManager.categories.observe(viewLifecycleOwner, Observer {
categories ->
+ adapter.setItems(categories)
+ progressBar.visibility = INVISIBLE
+ })
+ }
+
+ override fun onCategorySelected(category: Category) {
+ orderManager.setCurrentCategory(category)
+ }
+
+}
+
+private class CategoryAdapter(
+ private val listener: CategorySelectionListener
+) : Adapter<CategoryViewHolder>() {
+
+ private val categories = ArrayList<Category>()
+
+ override fun getItemCount() = categories.size
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
CategoryViewHolder {
+ val view =
+
LayoutInflater.from(parent.context).inflate(R.layout.list_item_category,
parent, false)
+ return CategoryViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) {
+ holder.bind(categories[position])
+ }
+
+ fun setItems(items: List<Category>) {
+ categories.clear()
+ categories.addAll(items)
+ notifyDataSetChanged()
+ }
+
+ private inner class CategoryViewHolder(v: View) :
RecyclerView.ViewHolder(v) {
+ private val button: Button = v.findViewById(R.id.button)
+
+ fun bind(category: Category) {
+ button.text = category.localizedName
+ button.isPressed = category.selected
+ button.setOnClickListener { listener.onCategorySelected(category) }
+ }
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt
new file mode 100644
index 0000000..63eda17
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.merchantpos.order
+
+import androidx.core.os.LocaleListCompat
+import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL
+import com.fasterxml.jackson.annotation.JsonProperty
+import net.taler.merchantpos.Amount
+import java.util.*
+import java.util.Locale.LanguageRange
+import kotlin.collections.ArrayList
+import kotlin.collections.HashMap
+
+data class Category(
+ val id: Int,
+ val name: String,
+ @JsonProperty("name_i18n")
+ val nameI18n: Map<String, String>?
+) {
+ var selected: Boolean = false
+ val localizedName: String get() = getLocalizedString(nameI18n, name)
+}
+
+@JsonInclude(NON_NULL)
+abstract class Product {
+ @get:JsonProperty("product_id")
+ abstract val productId: String?
+ abstract val description: String
+ @get:JsonProperty("description_i18n")
+ abstract val descriptionI18n: Map<String, String>?
+ abstract val price: String
+ @get:JsonProperty("delivery_location")
+ abstract val location: String?
+ abstract val image: String?
+ @get:JsonIgnore
+ val localizedDescription: String
+ get() = getLocalizedString(descriptionI18n, description)
+}
+
+data class ConfigProduct(
+ @JsonIgnore
+ val id: String = UUID.randomUUID().toString(),
+ override val productId: String?,
+ override val description: String,
+ override val descriptionI18n: Map<String, String>?,
+ override val price: String,
+ override val location: String?,
+ override val image: String?,
+ val categories: List<Int>,
+ @JsonIgnore
+ val quantity: Int = 0
+) : Product() {
+ val priceAsDouble by lazy { Amount.fromString(price).amount.toDouble() }
+
+ override fun equals(other: Any?) = other is ConfigProduct && id == other.id
+ override fun hashCode() = id.hashCode()
+}
+
+data class ContractProduct(
+ override val productId: String?,
+ override val description: String,
+ override val descriptionI18n: Map<String, String>?,
+ override val price: String,
+ override val location: String?,
+ override val image: String?,
+ val quantity: Int
+) : Product() {
+ constructor(product: ConfigProduct) : this(
+ product.productId,
+ product.description,
+ product.descriptionI18n,
+ product.price,
+ product.location,
+ product.image,
+ product.quantity
+ )
+}
+
+private fun getLocalizedString(map: Map<String, String>?, default: String):
String {
+ // just return the default, if it is the only element
+ if (map == null) return default
+ // create a priority list of language ranges from system locales
+ val locales = LocaleListCompat.getDefault()
+ val priorityList = ArrayList<LanguageRange>(locales.size())
+ for (i in 0 until locales.size()) {
+ priorityList.add(LanguageRange(locales[i].toLanguageTag()))
+ }
+ // create a list of locales available in the given map
+ val availableLocales = map.keys.mapNotNull {
+ if (it == "_") return@mapNotNull null
+ val list = it.split("_")
+ when (list.size) {
+ 1 -> Locale(list[0])
+ 2 -> Locale(list[0], list[1])
+ 3 -> Locale(list[0], list[1], list[2])
+ else -> null
+ }
+ }
+ val match = Locale.lookup(priorityList, availableLocales)
+ return match?.toString()?.let { map[it] } ?: default
+}
+
+data class Order(val id: Int, val availableCategories: Map<Int, Category>) {
+ val products = ArrayList<ConfigProduct>()
+ val title: String = id.toString()
+ val summary: String
+ get() {
+ if (products.size == 1) return products[0].description
+ return getCategoryQuantities().map { (category: Category,
quantity: Int) ->
+ "$quantity x ${category.localizedName}"
+ }.joinToString()
+ }
+ val total: Double
+ get() {
+ var total = 0.0
+ products.forEach { product ->
+ val price = product.priceAsDouble
+ total += price * product.quantity
+ }
+ return total
+ }
+ val totalAsString: String
+ get() = String.format("%.2f", total)
+
+ operator fun plus(product: ConfigProduct): Order {
+ val i = products.indexOf(product)
+ if (i == -1) {
+ products.add(product.copy(quantity = 1))
+ } else {
+ val quantity = products[i].quantity
+ products[i] = products[i].copy(quantity = quantity + 1)
+ }
+ return this
+ }
+
+ operator fun minus(product: ConfigProduct): Order {
+ val i = products.indexOf(product)
+ if (i == -1) return this
+ val quantity = products[i].quantity
+ if (quantity <= 1) {
+ products.remove(product)
+ } else {
+ products[i] = products[i].copy(quantity = quantity - 1)
+ }
+ return this
+ }
+
+ private fun getCategoryQuantities(): HashMap<Category, Int> {
+ val categories = HashMap<Category, Int>()
+ products.forEach { product ->
+ val categoryId = product.categories[0]
+ val category = availableCategories.getValue(categoryId)
+ val oldQuantity = categories[category] ?: 0
+ categories[category] = oldQuantity + product.quantity
+ }
+ return categories
+ }
+
+ /**
+ * Returns a map of i18n summaries for each locale present in *all* given
[Category]s
+ * or null if there's no locale that fulfills this criteria.
+ */
+ val summaryI18n: Map<String, String>?
+ get() {
+ if (products.size == 1) return products[0].descriptionI18n
+ val categoryQuantities = getCategoryQuantities()
+ // get all available locales
+ val availableLocales = categoryQuantities.mapNotNull { (category,
_) ->
+ val nameI18n = category.nameI18n
+ // if one category doesn't have locales, we can return null
here already
+ nameI18n?.keys ?: return null
+ }.flatten().toHashSet()
+ // remove all locales not supported by all categories
+ categoryQuantities.forEach { (category, _) ->
+ // category.nameI18n should be non-null now
+ availableLocales.retainAll(category.nameI18n!!.keys)
+ if (availableLocales.isEmpty()) return null
+ }
+ return availableLocales.map { locale ->
+ Pair(
+ locale, categoryQuantities.map { (category, quantity) ->
+ // category.nameI18n should be non-null now
+ "$quantity x ${category.nameI18n!![locale]}"
+ }.joinToString()
+ )
+ }.toMap()
+ }
+
+}
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
new file mode 100644
index 0000000..ff6061a
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.merchantpos.order
+
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import net.taler.merchantpos.CombinedLiveData
+import net.taler.merchantpos.order.RestartState.DISABLED
+import net.taler.merchantpos.order.RestartState.ENABLED
+import net.taler.merchantpos.order.RestartState.UNDO
+
+internal enum class RestartState { ENABLED, DISABLED, UNDO }
+
+internal interface LiveOrder {
+ val order: LiveData<Order>
+ val orderTotal: LiveData<Double>
+ val restartState: LiveData<RestartState>
+ val modifyOrderAllowed: LiveData<Boolean>
+ val lastAddedProduct: ConfigProduct?
+ val selectedProductKey: String?
+ fun restartOrUndo()
+ fun selectOrderLine(product: ConfigProduct?)
+ fun increaseSelectedOrderLine()
+ fun decreaseSelectedOrderLine()
+}
+
+internal class MutableLiveOrder(
+ val id: Int,
+ private val productsByCategory: HashMap<Category, ArrayList<ConfigProduct>>
+) : LiveOrder {
+ private val availableCategories: Map<Int, Category>
+ get() = productsByCategory.keys.map { it.id to it }.toMap()
+ override val order: MutableLiveData<Order> = MutableLiveData(Order(id,
availableCategories))
+ override val orderTotal: LiveData<Double> = Transformations.map(order) {
it.total }
+ override val restartState = MutableLiveData(DISABLED)
+ private val selectedOrderLine = MutableLiveData<ConfigProduct>()
+ override val selectedProductKey: String?
+ get() = selectedOrderLine.value?.id
+ override val modifyOrderAllowed =
+ CombinedLiveData(restartState, selectedOrderLine) { restartState,
selectedOrderLine ->
+ restartState != DISABLED && selectedOrderLine != null
+ }
+ override var lastAddedProduct: ConfigProduct? = null
+ private var undoOrder: Order? = null
+
+ @UiThread
+ internal fun addProduct(product: ConfigProduct) {
+ lastAddedProduct = product
+ order.value = order.value!! + product
+ restartState.value = ENABLED
+ }
+
+ @UiThread
+ internal fun removeProduct(product: ConfigProduct) {
+ val modifiedOrder = order.value!! - product
+ order.value = modifiedOrder
+ restartState.value = if (modifiedOrder.products.isEmpty()) DISABLED
else ENABLED
+ }
+
+ @UiThread
+ internal fun isEmpty() = order.value!!.products.isEmpty()
+
+ @UiThread
+ override fun restartOrUndo() {
+ if (restartState.value == UNDO) {
+ order.value = undoOrder
+ restartState.value = ENABLED
+ undoOrder = null
+ } else {
+ undoOrder = order.value
+ order.value = Order(id, availableCategories)
+ restartState.value = UNDO
+ }
+ }
+
+ @UiThread
+ override fun selectOrderLine(product: ConfigProduct?) {
+ selectedOrderLine.value = product
+ }
+
+ @UiThread
+ override fun increaseSelectedOrderLine() {
+ val orderLine = selectedOrderLine.value ?: throw
IllegalStateException()
+ addProduct(orderLine)
+ }
+
+ @UiThread
+ override fun decreaseSelectedOrderLine() {
+ val orderLine = selectedOrderLine.value ?: throw
IllegalStateException()
+ removeProduct(orderLine)
+ }
+
+}
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
new file mode 100644
index 0000000..49f7cf2
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.merchantpos.order
+
+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.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import androidx.transition.TransitionManager.beginDelayedTransition
+import kotlinx.android.synthetic.main.fragment_order.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.navigate
+import
net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionGlobalConfigFetcher
+import
net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionOrderToMerchantSettings
+import
net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionOrderToProcessPayment
+import net.taler.merchantpos.order.RestartState.ENABLED
+import net.taler.merchantpos.order.RestartState.UNDO
+
+class OrderFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val orderManager by lazy { viewModel.orderManager }
+ private val paymentManager by lazy { viewModel.paymentManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_order, container, false)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ orderManager.currentOrderId.observe(viewLifecycleOwner, Observer {
orderId ->
+ val liveOrder = orderManager.getOrder(orderId)
+ onOrderSwitched(orderId, liveOrder)
+ // add a new OrderStateFragment for each order
+ // as switching its internals (like we do here) would be too messy
+ childFragmentManager.beginTransaction()
+ .replace(R.id.fragment1, OrderStateFragment())
+ .commit()
+ })
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (!viewModel.configManager.config.isValid()) {
+ actionOrderToMerchantSettings().navigate(findNavController())
+ } else if (viewModel.configManager.merchantConfig?.currency == null) {
+ actionGlobalConfigFetcher().navigate(findNavController())
+ }
+ }
+
+ private fun onOrderSwitched(orderId: Int, liveOrder: LiveOrder) {
+ // order title
+ liveOrder.order.observe(viewLifecycleOwner, Observer { order ->
+ activity?.title = getString(R.string.order_label_title,
order.title)
+ })
+ // restart button
+ restartButton.setOnClickListener { liveOrder.restartOrUndo() }
+ liveOrder.restartState.observe(viewLifecycleOwner, Observer { state ->
+ beginDelayedTransition(view as ViewGroup)
+ if (state == UNDO) {
+ restartButton.setText(R.string.order_undo)
+ restartButton.isEnabled = true
+ completeButton.isEnabled = false
+ } else {
+ restartButton.setText(R.string.order_restart)
+ restartButton.isEnabled = state == ENABLED
+ completeButton.isEnabled = state == ENABLED
+ }
+ })
+ // -1 and +1 buttons
+ liveOrder.modifyOrderAllowed.observe(viewLifecycleOwner, Observer {
allowed ->
+ minusButton.isEnabled = allowed
+ plusButton.isEnabled = allowed
+ })
+ minusButton.setOnClickListener { liveOrder.decreaseSelectedOrderLine()
}
+ plusButton.setOnClickListener { liveOrder.increaseSelectedOrderLine() }
+ // previous and next button
+ prevButton.isEnabled = orderManager.hasPreviousOrder(orderId)
+ orderManager.hasNextOrder(orderId).observe(viewLifecycleOwner,
Observer { hasNextOrder ->
+ nextButton.isEnabled = hasNextOrder
+ })
+ prevButton.setOnClickListener { orderManager.previousOrder() }
+ nextButton.setOnClickListener { orderManager.nextOrder() }
+ // complete button
+ completeButton.setOnClickListener {
+ val order = liveOrder.order.value ?: return@setOnClickListener
+ paymentManager.createPayment(order)
+ actionOrderToProcessPayment().navigate(findNavController())
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..48ddc57
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.merchantpos.order
+
+import android.content.Context
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations.map
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.ObjectMapper
+import net.taler.merchantpos.Amount.Companion.fromString
+import net.taler.merchantpos.R
+import net.taler.merchantpos.config.ConfigurationReceiver
+import net.taler.merchantpos.order.RestartState.ENABLED
+import org.json.JSONObject
+
+class OrderManager(
+ private val context: Context,
+ private val mapper: ObjectMapper
+) : ConfigurationReceiver {
+
+ companion object {
+ val TAG = OrderManager::class.java.simpleName
+ }
+
+ private var orderCounter: Int = 0
+ private val mCurrentOrderId = MutableLiveData<Int>()
+ internal val currentOrderId: LiveData<Int> = mCurrentOrderId
+
+ private val productsByCategory = HashMap<Category,
ArrayList<ConfigProduct>>()
+
+ private val orders = LinkedHashMap<Int, MutableLiveOrder>()
+
+ private val mProducts = MutableLiveData<List<ConfigProduct>>()
+ internal val products: LiveData<List<ConfigProduct>> = mProducts
+
+ private val mCategories = MutableLiveData<List<Category>>()
+ internal val categories: LiveData<List<Category>> = mCategories
+
+ override suspend fun onConfigurationReceived(json: JSONObject, currency:
String): String? {
+ // parse categories
+ val categoriesStr = json.getJSONArray("categories").toString()
+ val categoriesType = object : TypeReference<List<Category>>() {}
+ val categories: List<Category> = mapper.readValue(categoriesStr,
categoriesType)
+ if (categories.isEmpty()) {
+ Log.e(TAG, "No valid category found.")
+ return context.getString(R.string.config_error_category)
+ }
+ // pre-select the first category
+ categories[0].selected = true
+
+ // parse products (live data gets updated in setCurrentCategory())
+ val productsStr = json.getJSONArray("products").toString()
+ val productsType = object : TypeReference<List<ConfigProduct>>() {}
+ val products: List<ConfigProduct> = mapper.readValue(productsStr,
productsType)
+
+ // group products by categories
+ productsByCategory.clear()
+ products.forEach { product ->
+ val productCurrency = fromString(product.price).currency
+ if (productCurrency != currency) {
+ Log.e(TAG, "Product $product has currency $productCurrency,
$currency expected")
+ return context.getString(
+ R.string.config_error_currency, product.description,
productCurrency, currency
+ )
+ }
+ product.categories.forEach { categoryId ->
+ val category = categories.find { it.id == categoryId }
+ if (category == null) {
+ Log.e(TAG, "Product $product has unknown category
$categoryId")
+ return context.getString(
+ R.string.config_error_product_category_id,
product.description, categoryId
+ )
+ }
+ if (productsByCategory.containsKey(category)) {
+ productsByCategory[category]?.add(product)
+ } else {
+ productsByCategory[category] =
ArrayList<ConfigProduct>().apply { add(product) }
+ }
+ }
+ }
+ return if (productsByCategory.size > 0) {
+ mCategories.postValue(categories)
+ mProducts.postValue(productsByCategory[categories[0]])
+ // Initialize first empty order, note this won't work when
updating config mid-flight
+ if (orders.isEmpty()) {
+ val id = orderCounter++
+ orders[id] = MutableLiveOrder(id, productsByCategory)
+ mCurrentOrderId.postValue(id)
+ }
+ null // success, no error string
+ } else context.getString(R.string.config_error_product_zero)
+ }
+
+ @UiThread
+ internal fun getOrder(orderId: Int): LiveOrder {
+ return orders[orderId] ?: throw IllegalArgumentException()
+ }
+
+ @UiThread
+ internal fun nextOrder() {
+ val currentId = currentOrderId.value!!
+ var foundCurrentOrder = false
+ var nextId: Int? = null
+ for (orderId in orders.keys) {
+ if (foundCurrentOrder) {
+ nextId = orderId
+ break
+ }
+ if (orderId == currentId) foundCurrentOrder = true
+ }
+ if (nextId == null) {
+ nextId = orderCounter++
+ orders[nextId] = MutableLiveOrder(nextId, productsByCategory)
+ }
+ val currentOrder = order(currentId)
+ if (currentOrder.isEmpty()) orders.remove(currentId)
+ else currentOrder.lastAddedProduct = null // not needed anymore and
it would get selected
+ mCurrentOrderId.value = nextId
+ }
+
+ @UiThread
+ internal fun previousOrder() {
+ val currentId = currentOrderId.value!!
+ var previousId: Int? = null
+ var foundCurrentOrder = false
+ for (orderId in orders.keys) {
+ if (orderId == currentId) {
+ foundCurrentOrder = true
+ break
+ }
+ previousId = orderId
+ }
+ if (previousId == null || !foundCurrentOrder) {
+ throw AssertionError("Could not find previous order for
$currentId")
+ }
+ val currentOrder = order(currentId)
+ // remove current order if empty, or lastAddedProduct as it is not
needed anymore
+ // and would get selected when navigating back instead of last
selection
+ if (currentOrder.isEmpty()) orders.remove(currentId)
+ else currentOrder.lastAddedProduct = null
+ mCurrentOrderId.value = previousId
+ }
+
+ fun hasPreviousOrder(currentOrderId: Int): Boolean {
+ return currentOrderId != orders.keys.first()
+ }
+
+ fun hasNextOrder(currentOrderId: Int) =
map(order(currentOrderId).restartState) { state ->
+ state == ENABLED || currentOrderId != orders.keys.last()
+ }
+
+ internal fun setCurrentCategory(category: Category) {
+ val newCategories = categories.value?.apply {
+ forEach { if (it.selected) it.selected = false }
+ category.selected = true
+ }
+ mCategories.postValue(newCategories)
+ mProducts.postValue(productsByCategory[category])
+ }
+
+ @UiThread
+ internal fun addProduct(orderId: Int, product: ConfigProduct) {
+ order(orderId).addProduct(product)
+ }
+
+ @UiThread
+ internal fun onOrderPaid(orderId: Int) {
+ if (currentOrderId.value == orderId) {
+ if (hasPreviousOrder(orderId)) previousOrder()
+ else nextOrder()
+ }
+ orders.remove(orderId)
+ }
+
+ private fun order(orderId: Int): MutableLiveOrder {
+ return orders[orderId] ?: throw IllegalStateException()
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
new file mode 100644
index 0000000..1b70016
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.merchantpos.order
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.selection.ItemDetailsLookup
+import androidx.recyclerview.selection.ItemKeyProvider
+import androidx.recyclerview.selection.SelectionPredicates
+import androidx.recyclerview.selection.SelectionTracker
+import androidx.recyclerview.selection.StorageStrategy
+import androidx.recyclerview.widget.AsyncListDiffer
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import kotlinx.android.synthetic.main.fragment_order_state.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.fadeIn
+import net.taler.merchantpos.fadeOut
+import net.taler.merchantpos.order.OrderAdapter.OrderLineLookup
+import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder
+
+class OrderStateFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val orderManager by lazy { viewModel.orderManager }
+ private val liveOrder by lazy {
orderManager.getOrder(orderManager.currentOrderId.value!!) }
+ private val adapter = OrderAdapter()
+ private var tracker: SelectionTracker<String>? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_order_state, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ orderList.apply {
+ adapter = this@OrderStateFragment.adapter
+ layoutManager = LinearLayoutManager(requireContext())
+ }
+ val detailsLookup = OrderLineLookup(orderList)
+ val tracker = SelectionTracker.Builder(
+ "order-selection-id",
+ orderList,
+ adapter.keyProvider,
+ detailsLookup,
+ StorageStrategy.createStringStorage()
+ ).withSelectionPredicate(
+ SelectionPredicates.createSelectSingleAnything()
+ ).build()
+ savedInstanceState?.let { tracker.onRestoreInstanceState(it) }
+ adapter.tracker = tracker
+ this.tracker = tracker
+ if (savedInstanceState == null) {
+ // select last selected order line when re-creating this fragment
+ // do it before attaching the tracker observer
+ liveOrder.selectedProductKey?.let { tracker.select(it) }
+ }
+ tracker.addObserver(object :
SelectionTracker.SelectionObserver<String>() {
+ override fun onItemStateChanged(key: String, selected: Boolean) {
+ super.onItemStateChanged(key, selected)
+ val item = if (selected) adapter.getItemByKey(key) else null
+ liveOrder.selectOrderLine(item)
+ }
+ })
+ liveOrder.order.observe(viewLifecycleOwner, Observer { order ->
+ onOrderChanged(order, tracker)
+ })
+ liveOrder.orderTotal.observe(viewLifecycleOwner, Observer { orderTotal
->
+ if (orderTotal == 0.0) {
+ totalView.fadeOut()
+ totalView.text = null
+ } else {
+ val currency = viewModel.configManager.merchantConfig?.currency
+ totalView.text = getString(R.string.order_total, orderTotal,
currency)
+ totalView.fadeIn()
+ }
+ })
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ tracker?.onSaveInstanceState(outState)
+ }
+
+ private fun onOrderChanged(order: Order, tracker:
SelectionTracker<String>) {
+ adapter.setItems(order.products) {
+ liveOrder.lastAddedProduct?.let {
+ val position = adapter.findPosition(it)
+ if (position >= 0) {
+ // orderList can be null m(
+ orderList?.scrollToPosition(position)
+ orderList?.post { this.tracker?.select(it.id) }
+ }
+ }
+ // workaround for bug: SelectionObserver doesn't update when
removing selected item
+ if (tracker.hasSelection()) {
+ val key = tracker.selection.first()
+ val product = order.products.find { it.id == key }
+ if (product == null) tracker.clearSelection()
+ }
+ }
+ }
+
+}
+
+private class OrderAdapter : Adapter<OrderViewHolder>() {
+
+ lateinit var tracker: SelectionTracker<String>
+ val keyProvider = OrderKeyProvider()
+ private val itemCallback = object : DiffUtil.ItemCallback<ConfigProduct>()
{
+ override fun areItemsTheSame(oldItem: ConfigProduct, newItem:
ConfigProduct): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areContentsTheSame(oldItem: ConfigProduct, newItem:
ConfigProduct): Boolean {
+ return oldItem.quantity == newItem.quantity
+ }
+ }
+ private val differ = AsyncListDiffer(this, itemCallback)
+
+ override fun getItemCount() = differ.currentList.size
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
OrderViewHolder {
+ val view =
+
LayoutInflater.from(parent.context).inflate(R.layout.list_item_order, parent,
false)
+ return OrderViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
+ val item = getItem(position)!!
+ holder.bind(item, tracker.isSelected(item.id))
+ }
+
+ fun setItems(items: List<ConfigProduct>, commitCallback: () -> Unit) {
+ // toMutableList() is needed for some reason, otherwise doesn't update
adapter
+ differ.submitList(items.toMutableList(), commitCallback)
+ }
+
+ fun getItem(position: Int): ConfigProduct? = differ.currentList[position]
+
+ fun getItemByKey(key: String): ConfigProduct? {
+ return differ.currentList.find { it.id == key }
+ }
+
+ fun findPosition(product: ConfigProduct): Int {
+ return differ.currentList.indexOf(product)
+ }
+
+ private inner class OrderViewHolder(private val v: View) : ViewHolder(v) {
+ private val quantity: TextView = v.findViewById(R.id.quantity)
+ private val name: TextView = v.findViewById(R.id.name)
+ private val price: TextView = v.findViewById(R.id.price)
+
+ fun bind(product: ConfigProduct, selected: Boolean) {
+ v.isActivated = selected
+ quantity.text = product.quantity.toString()
+ name.text = product.localizedDescription
+ price.text = String.format("%.2f", product.priceAsDouble *
product.quantity)
+ }
+ }
+
+ private inner class OrderKeyProvider :
ItemKeyProvider<String>(SCOPE_MAPPED) {
+ override fun getKey(position: Int) = getItem(position)!!.id
+ override fun getPosition(key: String): Int {
+ return differ.currentList.indexOfFirst { it.id == key }
+ }
+ }
+
+ internal class OrderLineLookup(private val list: RecyclerView) :
ItemDetailsLookup<String>() {
+ override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
+ list.findChildViewUnder(e.x, e.y)?.let { view ->
+ val holder = list.getChildViewHolder(view)
+ val adapter = list.adapter as OrderAdapter
+ val position = holder.adapterPosition
+ return object : ItemDetails<String>() {
+ override fun getPosition(): Int = position
+ override fun getSelectionKey(): String =
adapter.keyProvider.getKey(position)
+ override fun inSelectionHotspot(e: MotionEvent) = true
+ }
+ }
+ return null
+ }
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
new file mode 100644
index 0000000..4704ad0
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.merchantpos.order
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import kotlinx.android.synthetic.main.fragment_products.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.order.ProductAdapter.ProductViewHolder
+
+interface ProductSelectionListener {
+ fun onProductSelected(product: ConfigProduct)
+}
+
+class ProductsFragment : Fragment(), ProductSelectionListener {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val orderManager by lazy { viewModel.orderManager }
+ private val adapter = ProductAdapter(this)
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_products, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ productsList.apply {
+ adapter = this@ProductsFragment.adapter
+ layoutManager = GridLayoutManager(requireContext(), 3)
+ }
+
+ orderManager.products.observe(viewLifecycleOwner, Observer { products
->
+ if (products == null) {
+ adapter.setItems(emptyList())
+ } else {
+ adapter.setItems(products)
+ }
+ progressBar.visibility = INVISIBLE
+ })
+ }
+
+ override fun onProductSelected(product: ConfigProduct) {
+ orderManager.addProduct(orderManager.currentOrderId.value!!, product)
+ }
+
+}
+
+private class ProductAdapter(
+ private val listener: ProductSelectionListener
+) : Adapter<ProductViewHolder>() {
+
+ private val products = ArrayList<ConfigProduct>()
+
+ override fun getItemCount() = products.size
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ProductViewHolder {
+ val view =
+
LayoutInflater.from(parent.context).inflate(R.layout.list_item_product, parent,
false)
+ return ProductViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
+ holder.bind(products[position])
+ }
+
+ fun setItems(items: List<ConfigProduct>) {
+ products.clear()
+ products.addAll(items)
+ notifyDataSetChanged()
+ }
+
+ private inner class ProductViewHolder(private val v: View) : ViewHolder(v)
{
+ private val name: TextView = v.findViewById(R.id.name)
+ private val price: TextView = v.findViewById(R.id.price)
+
+ fun bind(product: ConfigProduct) {
+ name.text = product.localizedDescription
+ price.text = product.priceAsDouble.toString()
+ v.setOnClickListener { listener.onProductSelected(product) }
+ }
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt
new file mode 100644
index 0000000..b7e4a4b
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.merchantpos.payment
+
+import net.taler.merchantpos.order.Order
+
+data class Payment(
+ val order: Order,
+ val summary: String,
+ val currency: String,
+ val orderId: String? = null,
+ val talerPayUri: String? = null,
+ val paid: Boolean = false,
+ val error: Boolean = false
+)
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
new file mode 100644
index 0000000..7f15816
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.merchantpos.payment
+
+import android.os.CountDownTimer
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.volley.Request.Method.GET
+import com.android.volley.Request.Method.POST
+import com.android.volley.RequestQueue
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
+import com.android.volley.VolleyError
+import com.fasterxml.jackson.databind.ObjectMapper
+import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.config.MerchantRequest
+import net.taler.merchantpos.order.ContractProduct
+import net.taler.merchantpos.order.Order
+import org.json.JSONArray
+import org.json.JSONObject
+import java.net.URLEncoder
+import java.util.concurrent.TimeUnit.MINUTES
+import java.util.concurrent.TimeUnit.SECONDS
+
+private val TIMEOUT = MINUTES.toMillis(2)
+private val CHECK_INTERVAL = SECONDS.toMillis(1)
+private const val FULFILLMENT_PREFIX = "taler://fulfillment-success/"
+
+class PaymentManager(
+ private val configManager: ConfigManager,
+ private val queue: RequestQueue,
+ private val mapper: ObjectMapper
+) {
+
+ companion object {
+ val TAG = PaymentManager::class.java.simpleName
+ }
+
+ private val mPayment = MutableLiveData<Payment>()
+ val payment: LiveData<Payment> = mPayment
+
+ private val checkTimer = object : CountDownTimer(TIMEOUT, CHECK_INTERVAL) {
+ override fun onTick(millisUntilFinished: Long) {
+ val orderId = payment.value?.orderId
+ if (orderId == null) cancel()
+ else checkPayment(orderId)
+ }
+
+ override fun onFinish() {
+ payment.value?.copy(error = true)?.let { mPayment.value = it }
+ }
+ }
+
+ @UiThread
+ fun createPayment(order: Order) {
+ val merchantConfig = configManager.merchantConfig!!
+
+ val currency = merchantConfig.currency!!
+ val amount = "$currency:${order.totalAsString}"
+ val summary = order.summary
+ val summaryI18n = order.summaryI18n
+
+ mPayment.value = Payment(order, summary, currency)
+
+ val fulfillmentId = "${System.currentTimeMillis()}-${order.hashCode()}"
+ val fulfillmentUrl =
+ "${FULFILLMENT_PREFIX}${URLEncoder.encode(summary,
"UTF-8")}#$fulfillmentId"
+ val body = JSONObject().apply {
+ put("order", JSONObject().apply {
+ put("amount", amount)
+ put("summary", summary)
+ if (summaryI18n != null) put("summary_i18n", order.summaryI18n)
+ // fulfillment_url needs to be unique per order
+ put("fulfillment_url", fulfillmentUrl)
+ put("instance", "default")
+ put("products", order.getProductsJson())
+ })
+ }
+
+ Log.d(TAG, body.toString(4))
+
+ val req = MerchantRequest(POST, merchantConfig, "order", null, body,
+ Listener { onOrderCreated(it) },
+ ErrorListener { onNetworkError(it) }
+ )
+ queue.add(req)
+ }
+
+ private fun Order.getProductsJson(): JSONArray {
+ val contractProducts = products.map { ContractProduct(it) }
+ val productsStr = mapper.writeValueAsString(contractProducts)
+ return JSONArray(productsStr)
+ }
+
+ private fun onOrderCreated(orderResponse: JSONObject) {
+ val orderId = orderResponse.getString("order_id")
+ mPayment.value = mPayment.value!!.copy(orderId = orderId)
+ checkTimer.start()
+ }
+
+ private fun checkPayment(orderId: String) {
+ val merchantConfig = configManager.merchantConfig!!
+ val params = mapOf(
+ "order_id" to orderId,
+ "instance" to merchantConfig.instance
+ )
+
+ val req = MerchantRequest(GET, merchantConfig, "check-payment",
params, null,
+ Listener { onPaymentChecked(it) },
+ ErrorListener { onNetworkError(it) })
+ queue.add(req)
+ }
+
+ /**
+ * Called when the /check-payment response gave a result.
+ */
+ private fun onPaymentChecked(checkPaymentResponse: JSONObject) {
+ val currentValue = requireNotNull(mPayment.value)
+ if (checkPaymentResponse.getBoolean("paid")) {
+ mPayment.value = currentValue.copy(paid = true)
+ checkTimer.cancel()
+ } else if (currentValue.talerPayUri == null) {
+ val talerPayUri = checkPaymentResponse.getString("taler_pay_uri")
+ mPayment.value = currentValue.copy(talerPayUri = talerPayUri)
+ }
+ }
+
+ private fun onNetworkError(volleyError: VolleyError) {
+ Log.e(PaymentManager::class.java.simpleName, volleyError.toString())
+ cancelPayment()
+ }
+
+ fun cancelPayment() {
+ mPayment.value = mPayment.value!!.copy(error = true)
+ checkTimer.cancel()
+ }
+
+}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt
new file mode 100644
index 0000000..10d538d
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.merchantpos.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 kotlinx.android.synthetic.main.fragment_payment_success.*
+import net.taler.merchantpos.R
+
+class PaymentSuccessFragment : Fragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_payment_success, container,
false)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ paymentButton.setOnClickListener {
+ findNavController().navigateUp()
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..24f67f1
--- /dev/null
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.merchantpos.payment
+
+import android.annotation.SuppressLint
+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.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
+import kotlinx.android.synthetic.main.fragment_process_payment.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.NfcManager.Companion.hasNfc
+import net.taler.merchantpos.QrCodeManager.makeQrCode
+import net.taler.merchantpos.R
+import net.taler.merchantpos.fadeIn
+import net.taler.merchantpos.fadeOut
+import net.taler.merchantpos.navigate
+import
net.taler.merchantpos.payment.ProcessPaymentFragmentDirections.Companion.actionProcessPaymentToPaymentSuccess
+import net.taler.merchantpos.topSnackbar
+
+class ProcessPaymentFragment : Fragment() {
+
+ private val model: MainViewModel by activityViewModels()
+ private val paymentManager by lazy { model.paymentManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_process_payment, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val introRes =
+ if (hasNfc(requireContext())) R.string.payment_intro_nfc else
R.string.payment_intro
+ payIntroView.setText(introRes)
+ paymentManager.payment.observe(viewLifecycleOwner, Observer { payment
->
+ onPaymentStateChanged(payment)
+ })
+ cancelPaymentButton.setOnClickListener {
+ onPaymentCancel()
+ }
+ }
+
+ private fun onPaymentStateChanged(payment: Payment) {
+ if (payment.error) {
+ topSnackbar(view!!, R.string.error_network, LENGTH_LONG)
+ findNavController().navigateUp()
+ return
+ }
+ if (payment.paid) {
+ model.orderManager.onOrderPaid(payment.order.id)
+
actionProcessPaymentToPaymentSuccess().navigate(findNavController())
+ return
+ }
+ payIntroView.fadeIn()
+ @SuppressLint("SetTextI18n")
+ amountView.text = "${payment.order.totalAsString} ${payment.currency}"
+ payment.orderId?.let {
+ orderRefView.text = getString(R.string.payment_order_ref, it)
+ orderRefView.fadeIn()
+ }
+ payment.talerPayUri?.let {
+ val qrcodeBitmap = makeQrCode(it)
+ qrcodeView.setImageBitmap(qrcodeBitmap)
+ qrcodeView.fadeIn()
+ progressBar.fadeOut()
+ }
+ }
+
+ private fun onPaymentCancel() {
+ paymentManager.cancelPayment()
+ findNavController().navigateUp()
+ topSnackbar(view!!, R.string.payment_canceled, LENGTH_LONG)
+ }
+
+}
diff --git a/merchant-terminal/src/main/res/color/button_bottom.xml
b/merchant-terminal/src/main/res/color/button_bottom.xml
new file mode 100644
index 0000000..83363e9
--- /dev/null
+++ b/merchant-terminal/src/main/res/color/button_bottom.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/bottomButtons" android:state_enabled="true" />
+ <item android:alpha="0.12" android:color="?attr/colorOnSurface" />
+</selector>
diff --git a/merchant-terminal/src/main/res/drawable/ic_cash_refund.xml
b/merchant-terminal/src/main/res/drawable/ic_cash_refund.xml
new file mode 100644
index 0000000..7359ca3
--- /dev/null
+++ b/merchant-terminal/src/main/res/drawable/ic_cash_refund.xml
@@ -0,0 +1,9 @@
+<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="#000000"
+ android:pathData="M3,11H21V23H3V11M12,15A2,2 0 0,1 14,17A2,2 0 0,1
12,19A2,2 0 0,1 10,17A2,2 0 0,1 12,15M7,13A2,2 0 0,1 5,15V19A2,2 0 0,1
7,21H17A2,2 0 0,1 19,19V15A2,2 0 0,1
17,13H7M17,5V10H15.5V6.5H9.88L12.3,8.93L11.24,10L7,5.75L11.24,1.5L12.3,2.57L9.88,5H17Z"
/>
+</vector>
diff --git a/merchant-terminal/src/main/res/drawable/ic_check_circle.xml
b/merchant-terminal/src/main/res/drawable/ic_check_circle.xml
new file mode 100644
index 0000000..61e1b5a
--- /dev/null
+++ b/merchant-terminal/src/main/res/drawable/ic_check_circle.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:alpha="0.56"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/green"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48
10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
+</vector>
diff --git a/merchant-terminal/src/main/res/drawable/ic_history_black_24dp.xml
b/merchant-terminal/src/main/res/drawable/ic_history_black_24dp.xml
new file mode 100644
index 0000000..a61de1b
--- /dev/null
+++ b/merchant-terminal/src/main/res/drawable/ic_history_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89
0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0
-3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03
9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
+</vector>
diff --git a/merchant-terminal/src/main/res/drawable/ic_launcher_background.xml
b/merchant-terminal/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..2408e30
--- /dev/null
+++ b/merchant-terminal/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+ android:height="108dp"
+ android:width="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#008577"
+ android:pathData="M0,0h108v108h-108z"/>
+ <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+</vector>
diff --git a/merchant-terminal/src/main/res/drawable/ic_menu_manage.xml
b/merchant-terminal/src/main/res/drawable/ic_menu_manage.xml
new file mode 100644
index 0000000..a0e423c
--- /dev/null
+++ b/merchant-terminal/src/main/res/drawable/ic_menu_manage.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9
-2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9
4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1
0.1,-1.4z"/>
+</vector>
\ No newline at end of file
diff --git a/merchant-terminal/src/main/res/drawable/ic_move_money_24dp.xml
b/merchant-terminal/src/main/res/drawable/ic_move_money_24dp.xml
new file mode 100644
index 0000000..349f48f
--- /dev/null
+++ b/merchant-terminal/src/main/res/drawable/ic_move_money_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19,3L4.99,3c-1.11,0 -1.98,0.9 -1.98,2L3,19c0,1.1
0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2
-2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34
-3,-3L4.99,15L4.99,5L19,5v10zM16,10h-2L14,7h-4v3L8,10l4,4 4,-4z"/>
+</vector>
diff --git a/merchant-terminal/src/main/res/drawable/selectable_background.xml
b/merchant-terminal/src/main/res/drawable/selectable_background.xml
new file mode 100644
index 0000000..b82de92
--- /dev/null
+++ b/merchant-terminal/src/main/res/drawable/selectable_background.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@color/selectedBackground"
android:state_activated="true" />
+ <item android:drawable="@android:color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/merchant-terminal/src/main/res/drawable/side_nav_bar.xml
b/merchant-terminal/src/main/res/drawable/side_nav_bar.xml
new file mode 100644
index 0000000..50dc048
--- /dev/null
+++ b/merchant-terminal/src/main/res/drawable/side_nav_bar.xml
@@ -0,0 +1,9 @@
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient
+ android:angle="135"
+ android:centerColor="@color/colorPrimaryDark"
+ android:endColor="@color/colorPrimaryDark"
+ android:startColor="@color/colorPrimary"
+ android:type="linear"/>
+</shape>
\ No newline at end of file
diff --git a/merchant-terminal/src/main/res/layout/activity_main.xml
b/merchant-terminal/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..6523caa
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/activity_main.xml
@@ -0,0 +1,42 @@
+<?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.drawerlayout.widget.DrawerLayout
+ 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:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:openDrawer="start">
+
+ <include
+ layout="@layout/app_bar_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <com.google.android.material.navigation.NavigationView
+ android:id="@+id/nav_view"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:fitsSystemWindows="true"
+ app:menu="@menu/activity_main_drawer"
+ app:headerLayout="@layout/nav_header_main" />
+
+</androidx.drawerlayout.widget.DrawerLayout>
diff --git a/merchant-terminal/src/main/res/layout/app_bar_main.xml
b/merchant-terminal/src/main/res/layout/app_bar_main.xml
new file mode 100644
index 0000000..0254c71
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/app_bar_main.xml
@@ -0,0 +1,53 @@
+<?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.coordinatorlayout.widget.CoordinatorLayout
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=".MainActivity">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:theme="@style/AppTheme.AppBarOverlay">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ app:popupTheme="@style/AppTheme.PopupOverlay" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/navHostFragment"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:defaultNavHost="true"
+ app:layout_insetEdge="top"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/nav_graph" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_categories.xml
b/merchant-terminal/src/main/res/layout/fragment_categories.xml
new file mode 100644
index 0000000..a90585f
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_categories.xml
@@ -0,0 +1,46 @@
+<?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"
+ android:layout_width="match_parent"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/categoriesList"
+ android:layout_width="0dp"
+ tools:listitem="@layout/list_item_category"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <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" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_config_fetcher.xml
b/merchant-terminal/src/main/res/layout/fragment_config_fetcher.xml
new file mode 100644
index 0000000..af7dcaf
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_config_fetcher.xml
@@ -0,0 +1,45 @@
+<?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"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="16dp">
+
+ <TextView
+ android:id="@+id/titleView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/config_fetching"
+ android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/titleView" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_merchant_config.xml
b/merchant-terminal/src/main/res/layout/fragment_merchant_config.xml
new file mode 100644
index 0000000..2541887
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_merchant_config.xml
@@ -0,0 +1,152 @@
+<?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">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:context=".config.MerchantConfigFragment">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/configUrlView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:hint="@string/config_url"
+ app:boxBackgroundColor="@android:color/transparent"
+ app:boxBackgroundMode="outline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textUri" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/usernameView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:hint="@string/config_username"
+ app:boxBackgroundColor="@android:color/transparent"
+ app:boxBackgroundMode="outline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/configUrlView">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="text" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/passwordView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:hint="@string/config_password"
+ app:boxBackgroundColor="@android:color/transparent"
+ app:boxBackgroundMode="outline"
+ app:endIconMode="password_toggle"
+ app:layout_constraintEnd_toStartOf="@+id/forgetPasswordButton"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/usernameView">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textWebPassword" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <Button
+ android:id="@+id/forgetPasswordButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/config_forget_password"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="@+id/passwordView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/passwordView"
+ tools:visibility="visible" />
+
+ <CheckBox
+ android:id="@+id/savePasswordCheckBox"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:checked="true"
+ android:text="@string/config_save_password"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/okButton"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/passwordView"
+ app:layout_constraintVertical_bias="0.0" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/okButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/config_ok"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/savePasswordCheckBox"
+ app:layout_constraintTop_toBottomOf="@+id/passwordView"
+ app:layout_constraintVertical_bias="0.0" />
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/okButton"
+ app:layout_constraintEnd_toEndOf="@+id/okButton"
+ app:layout_constraintStart_toStartOf="@+id/okButton"
+ app:layout_constraintTop_toTopOf="@+id/okButton"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/configDocsView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/config_docs"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/okButton" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</ScrollView>
diff --git
a/merchant-terminal/src/main/res/layout/fragment_merchant_history.xml
b/merchant-terminal/src/main/res/layout/fragment_merchant_history.xml
new file mode 100644
index 0000000..21e6f08
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_merchant_history.xml
@@ -0,0 +1,29 @@
+<?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.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/swipeRefresh"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_history"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_order.xml
b/merchant-terminal/src/main/res/layout/fragment_order.xml
new file mode 100644
index 0000000..4af9c77
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_order.xml
@@ -0,0 +1,138 @@
+<?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.fragment.app.FragmentContainerView
+ android:id="@+id/fragment1"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginBottom="8dp"
+ app:layout_constraintBottom_toTopOf="@+id/restartButton"
+ app:layout_constraintEnd_toStartOf="@+id/guideline1"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:layout="@layout/fragment_order_state" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.25" />
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/fragment2"
+ android:name="net.taler.merchantpos.order.ProductsFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginBottom="8dp"
+ app:layout_constraintBottom_toTopOf="@+id/restartButton"
+ app:layout_constraintEnd_toStartOf="@+id/guideline2"
+ app:layout_constraintStart_toStartOf="@+id/guideline1"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:layout="@layout/fragment_products" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.75" />
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/fragment3"
+ android:name="net.taler.merchantpos.order.CategoriesFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginBottom="8dp"
+ app:layout_constraintBottom_toTopOf="@+id/restartButton"
+ 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"
+ 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/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/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/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/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/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: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" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_order_state.xml
b/merchant-terminal/src/main/res/layout/fragment_order_state.xml
new file mode 100644
index 0000000..7d6b258
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_order_state.xml
@@ -0,0 +1,52 @@
+<?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/orderList"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toTopOf="@+id/totalView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:listitem="@layout/list_item_order" />
+
+ <TextView
+ android:id="@+id/totalView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:background="@color/highlightedBackground"
+ android:elevation="2dp"
+ android:gravity="center_vertical|end"
+ android:padding="8dp"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/orderList"
+ tools:text="Total: 23.75 TESTKUDOS"
+ tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_payment_success.xml
b/merchant-terminal/src/main/res/layout/fragment_payment_success.xml
new file mode 100644
index 0000000..1bc9be7
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_payment_success.xml
@@ -0,0 +1,78 @@
+<?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=".payment.PaymentSuccessFragment">
+
+ <ImageView
+ android:id="@+id/paymentIcon"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="16dp"
+ android:src="@drawable/ic_check_circle"
+ app:layout_constraintBottom_toTopOf="@+id/paymentLabel"
+ 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="spread_inside"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:id="@+id/paymentLabel"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="16dp"
+ android:gravity="center_horizontal|top"
+ android:text="@string/payment_received"
+ android:textColor="@color/green"
+ app:autoSizeMaxTextSize="42sp"
+ app:autoSizeTextType="uniform"
+ app:layout_constraintBottom_toTopOf="@+id/paymentButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/paymentIcon" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guidelineLeft"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.25" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guidelineRight"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.75" />
+
+ <Button
+ android:id="@+id/paymentButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/payment_back_button"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/guidelineRight"
+ app:layout_constraintStart_toStartOf="@+id/guidelineLeft" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_process_payment.xml
b/merchant-terminal/src/main/res/layout/fragment_process_payment.xml
new file mode 100644
index 0000000..6cd8ea1
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_process_payment.xml
@@ -0,0 +1,110 @@
+<?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=".payment.ProcessPaymentFragment">
+
+ <ImageView
+ android:id="@+id/qrcodeView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="32dp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="ContentDescription"
+ tools:src="@tools:sample/avatars"
+ 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="@+id/qrcodeView"
+ app:layout_constraintEnd_toEndOf="@+id/qrcodeView"
+ app:layout_constraintStart_toStartOf="@+id/qrcodeView"
+ app:layout_constraintTop_toTopOf="@+id/qrcodeView" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.54" />
+
+ <TextView
+ android:id="@+id/payIntroView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/payment_intro_nfc"
+ android:textAlignment="center"
+ android:textSize="24sp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/amountView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="spread"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/amountView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+ app:layout_constraintBottom_toTopOf="@+id/orderRefView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toBottomOf="@+id/payIntroView"
+ tools:text="10.49 TESTKUDOS" />
+
+ <TextView
+ android:id="@+id/orderRefView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:textAlignment="center"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@id/cancelPaymentButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toBottomOf="@+id/amountView"
+ tools:text="@string/payment_order_ref"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/cancelPaymentButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/red"
+ android:text="@string/payment_cancel"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="@+id/guideline" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_products.xml
b/merchant-terminal/src/main/res/layout/fragment_products.xml
new file mode 100644
index 0000000..f0e86e7
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_products.xml
@@ -0,0 +1,44 @@
+<?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"
+ android:layout_width="match_parent"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/productsList"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:listitem="@layout/list_item_product" />
+
+ <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" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_refund.xml
b/merchant-terminal/src/main/res/layout/fragment_refund.xml
new file mode 100644
index 0000000..5a78cdd
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_refund.xml
@@ -0,0 +1,122 @@
+<?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=".history.RefundFragment">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/amountView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:hint="@string/refund_amount"
+ app:boxBackgroundMode="outline"
+ app:endIconMode="clear_text"
+ app:endIconTint="?attr/colorControlNormal"
+ app:layout_constraintBottom_toTopOf="@+id/reasonView"
+ app:layout_constraintEnd_toStartOf="@+id/currencyView"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="spread">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/amountInputView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="6"
+ android:inputType="numberDecimal"
+ tools:text="23.42" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/currencyView"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:gravity="start|center_vertical"
+ app:layout_constraintBottom_toBottomOf="@+id/amountView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/amountView"
+ app:layout_constraintTop_toTopOf="@+id/amountView"
+ tools:text="TESTKUDOS" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/reasonView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:hint="@string/refund_reason"
+ app:endIconMode="clear_text"
+ app:layout_constraintBottom_toTopOf="@+id/abortButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/amountView">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/reasonInputView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+
android:inputType="textAutoComplete|textAutoCorrect|textMultiLine" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <Button
+ android:id="@+id/abortButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/red"
+ android:text="@string/refund_abort"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/refundButton"
+ app:layout_constraintHorizontal_bias="0.76"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <Button
+ android:id="@+id/refundButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/green"
+ android:text="@string/refund_confirm"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/abortButton" />
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/refundButton"
+ app:layout_constraintEnd_toEndOf="@+id/refundButton"
+ app:layout_constraintStart_toStartOf="@+id/refundButton"
+ app:layout_constraintTop_toTopOf="@+id/refundButton"
+ tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/fragment_refund_uri.xml
b/merchant-terminal/src/main/res/layout/fragment_refund_uri.xml
new file mode 100644
index 0000000..8447d28
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/fragment_refund_uri.xml
@@ -0,0 +1,93 @@
+<?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=".payment.ProcessPaymentFragment">
+
+ <ImageView
+ android:id="@+id/refundQrcodeView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="32dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="ContentDescription"
+ tools:src="@tools:sample/avatars" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.54" />
+
+ <TextView
+ android:id="@+id/refundIntroView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/refund_intro_nfc"
+ android:textAlignment="center"
+ android:textSize="24sp"
+ app:layout_constraintBottom_toTopOf="@+id/refundAmountView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="spread" />
+
+ <TextView
+ android:id="@+id/refundAmountView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+ app:layout_constraintBottom_toTopOf="@+id/refundRefView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toBottomOf="@+id/refundIntroView"
+ tools:text="10.49 TESTKUDOS" />
+
+ <TextView
+ android:id="@+id/refundRefView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:textAlignment="center"
+ app:layout_constraintBottom_toTopOf="@id/cancelRefundButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toBottomOf="@+id/refundAmountView"
+ tools:text="@string/refund_order_ref" />
+
+ <Button
+ android:id="@+id/cancelRefundButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/red"
+ android:text="@string/refund_abort"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="@+id/guideline" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/list_item_category.xml
b/merchant-terminal/src/main/res/layout/list_item_category.xml
new file mode 100644
index 0000000..cbdbd34
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/list_item_category.xml
@@ -0,0 +1,33 @@
+<?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="wrap_content">
+
+ <Button
+ android:id="@+id/button"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Snacks" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/list_item_history.xml
b/merchant-terminal/src/main/res/layout/list_item_history.xml
new file mode 100644
index 0000000..fe485ba
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/list_item_history.xml
@@ -0,0 +1,97 @@
+<?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="wrap_content"
+ android:padding="16dp">
+
+ <TextView
+ android:id="@+id/orderSummaryView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ app:layout_constraintEnd_toStartOf="@+id/orderAmountView"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="One Cappuccino or another name that can be so long
that it spans more than one line" />
+
+ <TextView
+ android:id="@+id/orderAmountView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="16dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ app:layout_constraintBottom_toBottomOf="@+id/orderSummaryView"
+ app:layout_constraintEnd_toStartOf="@+id/refundButton"
+ app:layout_constraintStart_toEndOf="@+id/orderSummaryView"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="23.42 TESTKUDOS" />
+
+ <TextView
+ android:id="@+id/orderIdView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/history_ref_no"
+ android:textAllCaps="false"
+ android:textSize="20sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/orderTimeView"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/orderSummaryView" />
+
+ <TextView
+ android:id="@+id/orderTimeView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:textSize="20sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/refundButton"
+ app:layout_constraintStart_toEndOf="@+id/orderIdView"
+ app:layout_constraintTop_toBottomOf="@+id/orderAmountView"
+ app:layout_constraintVertical_bias="1.0"
+ tools:text="3 hrs. ago" />
+
+ <ImageButton
+ android:id="@+id/refundButton"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:backgroundTint="?colorPrimary"
+ android:contentDescription="@string/history_refund"
+ android:tint="?attr/colorOnPrimary"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/ic_cash_refund" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/list_item_order.xml
b/merchant-terminal/src/main/res/layout/list_item_order.xml
new file mode 100644
index 0000000..f88364d
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/list_item_order.xml
@@ -0,0 +1,61 @@
+<?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="wrap_content"
+ android:background="@drawable/selectable_background"
+ android:minHeight="48dp"
+ android:padding="8dp">
+
+ <TextView
+ android:id="@+id/quantity"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ android:minWidth="24dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/name"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="31" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/price"
+ app:layout_constraintStart_toEndOf="@+id/quantity"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="An order product item that in some cases could have a
very long name" />
+
+ <TextView
+ android:id="@+id/price"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/name"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="23.42" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/merchant-terminal/src/main/res/layout/list_item_product.xml
b/merchant-terminal/src/main/res/layout/list_item_product.xml
new file mode 100644
index 0000000..1037bef
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/list_item_product.xml
@@ -0,0 +1,56 @@
+<?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/>
+ -->
+
+<com.google.android.material.card.MaterialCardView
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="wrap_content"
+ android:layout_margin="4dp"
+ android:clickable="true"
+ android:focusable="true"
+ app:cardUseCompatPadding="true">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="8dp">
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textColor="?android:textColorPrimary"
+ android:textStyle="bold"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Steak and two Eggs" />
+
+ <TextView
+ android:id="@+id/price"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:textColor="?android:textColorSecondary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/name"
+ tools:text="7.95" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.google.android.material.card.MaterialCardView>
\ No newline at end of file
diff --git a/merchant-terminal/src/main/res/layout/nav_header_main.xml
b/merchant-terminal/src/main/res/layout/nav_header_main.xml
new file mode 100644
index 0000000..14bbd51
--- /dev/null
+++ b/merchant-terminal/src/main/res/layout/nav_header_main.xml
@@ -0,0 +1,55 @@
+<?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="@dimen/nav_header_height"
+ android:background="@drawable/side_nav_bar"
+ android:gravity="bottom"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:theme="@style/AppTheme">
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/nav_header_vertical_spacing"
+ app:srcCompat="@mipmap/ic_taler_logo_round"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/nav_header_vertical_spacing"
+ android:text="@string/project_name"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ android:textColor="#FFF" />
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/app_name_short"
+ android:textColor="#FFF" />
+
+</LinearLayout>
diff --git a/merchant-terminal/src/main/res/menu/activity_main_drawer.xml
b/merchant-terminal/src/main/res/menu/activity_main_drawer.xml
new file mode 100644
index 0000000..1303605
--- /dev/null
+++ b/merchant-terminal/src/main/res/menu/activity_main_drawer.xml
@@ -0,0 +1,36 @@
+<?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/>
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:showIn="navigation_view">
+
+ <group android:checkableBehavior="single">
+ <item
+ android:id="@+id/nav_order"
+ android:icon="@drawable/ic_move_money_24dp"
+ android:title="@string/menu_order" />
+ <item
+ android:id="@+id/nav_history"
+ android:icon="@drawable/ic_history_black_24dp"
+ android:title="@string/menu_history" />
+ <item
+ android:id="@+id/nav_settings"
+ android:icon="@drawable/ic_menu_manage"
+ android:title="@string/menu_settings" />
+ </group>
+</menu>
diff --git a/merchant-terminal/src/main/res/mipmap-anydpi-v26/ic_taler_logo.xml
b/merchant-terminal/src/main/res/mipmap-anydpi-v26/ic_taler_logo.xml
new file mode 100644
index 0000000..c4a603d
--- /dev/null
+++ b/merchant-terminal/src/main/res/mipmap-anydpi-v26/ic_taler_logo.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git
a/merchant-terminal/src/main/res/mipmap-anydpi-v26/ic_taler_logo_round.xml
b/merchant-terminal/src/main/res/mipmap-anydpi-v26/ic_taler_logo_round.xml
new file mode 100644
index 0000000..c4a603d
--- /dev/null
+++ b/merchant-terminal/src/main/res/mipmap-anydpi-v26/ic_taler_logo_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git
a/merchant-terminal/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
b/merchant-terminal/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..75273ec
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/merchant-terminal/src/main/res/mipmap-hdpi/ic_taler_logo.png
b/merchant-terminal/src/main/res/mipmap-hdpi/ic_taler_logo.png
new file mode 100644
index 0000000..eaecede
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-hdpi/ic_taler_logo.png differ
diff --git a/merchant-terminal/src/main/res/mipmap-hdpi/ic_taler_logo_round.png
b/merchant-terminal/src/main/res/mipmap-hdpi/ic_taler_logo_round.png
new file mode 100644
index 0000000..caa2a3e
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-hdpi/ic_taler_logo_round.png differ
diff --git
a/merchant-terminal/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
b/merchant-terminal/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..a450287
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/merchant-terminal/src/main/res/mipmap-mdpi/ic_taler_logo.png
b/merchant-terminal/src/main/res/mipmap-mdpi/ic_taler_logo.png
new file mode 100644
index 0000000..e1f7374
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-mdpi/ic_taler_logo.png differ
diff --git a/merchant-terminal/src/main/res/mipmap-mdpi/ic_taler_logo_round.png
b/merchant-terminal/src/main/res/mipmap-mdpi/ic_taler_logo_round.png
new file mode 100644
index 0000000..e92d2d3
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-mdpi/ic_taler_logo_round.png differ
diff --git
a/merchant-terminal/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
b/merchant-terminal/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..a5e875c
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/merchant-terminal/src/main/res/mipmap-xhdpi/ic_taler_logo.png
b/merchant-terminal/src/main/res/mipmap-xhdpi/ic_taler_logo.png
new file mode 100644
index 0000000..5ca4409
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-xhdpi/ic_taler_logo.png differ
diff --git
a/merchant-terminal/src/main/res/mipmap-xhdpi/ic_taler_logo_round.png
b/merchant-terminal/src/main/res/mipmap-xhdpi/ic_taler_logo_round.png
new file mode 100644
index 0000000..12b9056
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-xhdpi/ic_taler_logo_round.png differ
diff --git
a/merchant-terminal/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
b/merchant-terminal/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..e9d1fc9
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/merchant-terminal/src/main/res/mipmap-xxhdpi/ic_taler_logo.png
b/merchant-terminal/src/main/res/mipmap-xxhdpi/ic_taler_logo.png
new file mode 100644
index 0000000..a786efa
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-xxhdpi/ic_taler_logo.png differ
diff --git
a/merchant-terminal/src/main/res/mipmap-xxhdpi/ic_taler_logo_round.png
b/merchant-terminal/src/main/res/mipmap-xxhdpi/ic_taler_logo_round.png
new file mode 100644
index 0000000..b22a84e
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-xxhdpi/ic_taler_logo_round.png differ
diff --git
a/merchant-terminal/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
b/merchant-terminal/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..f8037d1
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
differ
diff --git a/merchant-terminal/src/main/res/mipmap-xxxhdpi/ic_taler_logo.png
b/merchant-terminal/src/main/res/mipmap-xxxhdpi/ic_taler_logo.png
new file mode 100644
index 0000000..0e9df6a
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-xxxhdpi/ic_taler_logo.png differ
diff --git
a/merchant-terminal/src/main/res/mipmap-xxxhdpi/ic_taler_logo_round.png
b/merchant-terminal/src/main/res/mipmap-xxxhdpi/ic_taler_logo_round.png
new file mode 100644
index 0000000..6bef9bd
Binary files /dev/null and
b/merchant-terminal/src/main/res/mipmap-xxxhdpi/ic_taler_logo_round.png differ
diff --git a/merchant-terminal/src/main/res/navigation/nav_graph.xml
b/merchant-terminal/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..2e337f2
--- /dev/null
+++ b/merchant-terminal/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,137 @@
+<?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/>
+ -->
+
+<navigation 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:id="@+id/nav_graph"
+ app:startDestination="@+id/nav_order"
+ tools:ignore="UnusedNavigation">
+
+ <fragment
+ android:id="@+id/nav_order"
+ android:name="net.taler.merchantpos.order.OrderFragment"
+ android:label=""
+ tools:layout="@layout/fragment_order">
+ <action
+ android:id="@+id/action_order_to_merchantSettings"
+ app:destination="@+id/nav_settings"
+ app:launchSingleTop="true"
+ app:popUpTo="@+id/nav_graph"
+ app:popUpToInclusive="true" />
+ <action
+ android:id="@+id/action_order_self"
+ app:destination="@+id/nav_order"
+ app:popUpTo="@+id/nav_graph" />
+ <action
+ android:id="@+id/action_order_to_processPayment"
+ app:destination="@+id/processPayment" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/processPayment"
+ android:name="net.taler.merchantpos.payment.ProcessPaymentFragment"
+ android:label="@string/payment_process_label"
+ tools:layout="@layout/fragment_process_payment">
+ <action
+ android:id="@+id/action_processPayment_to_paymentSuccess"
+ app:destination="@+id/paymentSuccess"
+ app:popUpTo="@id/nav_order" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/nav_history"
+
android:name="net.taler.merchantpos.history.MerchantHistoryFragment"
+ android:label="@string/history_label"
+ tools:layout="@layout/fragment_merchant_history">
+ <action
+ android:id="@+id/action_nav_history_to_refundFragment"
+ app:destination="@id/refundFragment" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/refundFragment"
+ android:name="net.taler.merchantpos.history.RefundFragment"
+ android:label="@string/history_refund"
+ tools:layout="@layout/fragment_refund">
+ <action
+ android:id="@+id/action_refundFragment_to_refundUriFragment"
+ app:destination="@id/refundUriFragment" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/refundUriFragment"
+ android:name="net.taler.merchantpos.history.RefundUriFragment"
+ android:label="@string/history_refund"
+ tools:layout="@layout/fragment_refund_uri" />
+
+ <fragment
+ android:id="@+id/nav_settings"
+ android:name="net.taler.merchantpos.config.MerchantConfigFragment"
+ android:label="@string/config_label"
+ tools:layout="@layout/fragment_merchant_config">
+ <action
+ android:id="@+id/action_settings_to_order"
+ app:destination="@+id/nav_order"
+ app:launchSingleTop="true"
+ app:popUpTo="@+id/nav_graph"
+ app:popUpToInclusive="true" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/configFetcher"
+ android:name="net.taler.merchantpos.config.ConfigFetcherFragment"
+ android:label="@string/config_fetching_label"
+ tools:layout="@layout/fragment_config_fetcher">
+ <action
+ android:id="@+id/action_configFetcher_to_merchantSettings"
+ app:destination="@+id/nav_settings"
+ app:launchSingleTop="true"
+ app:popUpTo="@+id/nav_graph"
+ app:popUpToInclusive="true" />
+ <action
+ android:id="@+id/action_configFetcher_to_order"
+ app:destination="@+id/nav_order"
+ app:launchSingleTop="true"
+ app:popUpTo="@+id/nav_graph"
+ app:popUpToInclusive="true" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/paymentSuccess"
+ android:name="net.taler.merchantpos.payment.PaymentSuccessFragment"
+ android:label="@string/payment_received"
+ tools:layout="@layout/fragment_payment_success" />
+
+ <action
+ android:id="@+id/action_global_order"
+ app:destination="@+id/nav_order"
+ app:launchSingleTop="true"
+ app:popUpTo="@+id/nav_graph" />
+ <action
+ android:id="@+id/action_global_merchantHistory"
+ app:destination="@+id/nav_history"
+ app:launchSingleTop="true" />
+ <action
+ android:id="@+id/action_global_merchantSettings"
+ app:destination="@+id/nav_settings"
+ app:launchSingleTop="true" />
+ <action
+ android:id="@+id/action_global_configFetcher"
+ app:destination="@+id/configFetcher"
+ app:launchSingleTop="true" />
+
+</navigation>
diff --git a/merchant-terminal/src/main/res/values-night/colors.xml
b/merchant-terminal/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..10bdbb9
--- /dev/null
+++ b/merchant-terminal/src/main/res/values-night/colors.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="highlightedBackground">#2E2E2E</color>
+ <color name="selectedBackground">#363636</color>
+</resources>
diff --git a/merchant-terminal/src/main/res/values/colors.xml
b/merchant-terminal/src/main/res/values/colors.xml
new file mode 100644
index 0000000..bf0c849
--- /dev/null
+++ b/merchant-terminal/src/main/res/values/colors.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#795548</color>
+ <color name="colorPrimaryDark">#5D4037</color>
+ <color name="colorAccent">#FFEB3B</color>
+
+ <color name="highlightedBackground">#E4E4E4</color>
+ <color name="selectedBackground">#DADADA</color>
+ <color name="bottomButtons">#9E9D24</color>
+
+ <color name="green">#388E3C</color>
+ <color name="red">#C62828</color>
+
+</resources>
diff --git a/merchant-terminal/src/main/res/values/dimens.xml
b/merchant-terminal/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..eedc3c6
--- /dev/null
+++ b/merchant-terminal/src/main/res/values/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+ <dimen name="nav_header_vertical_spacing">8dp</dimen>
+ <dimen name="nav_header_height">176dp</dimen>
+</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
new file mode 100644
index 0000000..77c7e03
--- /dev/null
+++ b/merchant-terminal/src/main/res/values/strings.xml
@@ -0,0 +1,68 @@
+<resources>
+ <string name="app_name">Taler Merchant PoS Terminal</string>
+ <string name="app_name_short">Merchant Terminal</string>
+ <string name="project_name">GNU Taler</string>
+
+ <string name="menu_order">Orders</string>
+ <string name="menu_history">History</string>
+ <string name="menu_settings">Settings</string>
+
+ <string name="order_label_title">Order #%s</string>
+ <!-- The first placeholder is the amount and the second the currency -->
+ <string name="order_total">Total: %1$.2f %2$s</string>
+ <string name="order_restart">Restart</string>
+ <string name="order_undo">Undo</string>
+ <string name="order_previous">Prev</string>
+ <string name="order_next">Next</string>
+ <string name="order_complete">Complete</string>
+
+ <string name="config_label">Merchant Settings</string>
+ <string name="config_url">Configuration URL</string>
+ <string name="config_username">Username</string>
+ <string name="config_password">Password</string>
+ <string name="config_ok">Fetch Configuration</string>
+ <string name="config_auth_error">Error: Invalid username or
password</string>
+ <string name="config_error_network">Error: Could not connect to
configuration server</string>
+ <string name="config_error_category">Error: No valid product category
found</string>
+ <string name="config_error_malformed">Error: The configuration JSON is
malformed</string>
+ <string name="config_error_currency">Error: Product %1$s has currency
%2$s, but %3$s expected</string>
+ <string name="config_error_product_category_id">Error: Product %1$s
references unknown category ID %2$d</string>
+ <string name="config_error_product_zero">Error: No valid products
found</string>
+ <string name="config_error_unknown">Error: Invalid Configuration</string>
+ <string name="config_fetching">Fetching Configuration…</string>
+ <string name="config_save_password">Remember Password</string>
+ <string name="config_forget_password">Forget</string>
+ <string name="config_changed">Changed to new merchant using %s</string>
+ <string name="config_fetching_label">Fetching Configuration</string>
+ <string name="config_docs">Please refer to <a
href="https://docs.taler.net/taler-merchant-pos-terminal.html#apis-and-data-formats">the
documentation</a> for the configuration format.</string>
+
+ <string name="payment_intro_nfc">Please scan QR Code or use NFC to
pay</string>
+ <string name="payment_intro">Please scan QR Code to pay</string>
+ <string name="payment_cancel">Cancel Payment</string>
+ <string name="payment_received">Payment received</string>
+ <string name="payment_back_button">Continue</string>
+ <string name="payment_order_ref">Order Reference: %s</string>
+ <string name="payment_process_label">Customer Payment Required</string>
+ <string name="payment_canceled">Payment Canceled</string>
+
+ <string name="history_label">Payment History</string>
+ <string name="history_received_at">Received at</string>
+ <string name="history_ref_no">Ref. No: %s</string>
+ <string name="history_refund">Refund Order</string>
+ <string name="refund_amount">Amount</string>
+ <string name="refund_reason">Refund reason</string>
+ <string name="refund_abort">Abort</string>
+ <string name="refund_confirm">Give Refund</string>
+ <string name="refund_error_max_amount">Greater than order amount of
%s</string>
+ <string name="refund_error_zero">Needs to be positive amount</string>
+ <string name="refund_error_backend">Error processing refund</string>
+ <string name="refund_error_deadline">Refund deadline has passed</string>
+ <string name="refund_intro_nfc">Please scan QR Code or use NFC to give
refund</string>
+ <string name="refund_intro">Please scan QR Code to give refund</string>
+ <string name="refund_order_ref">Order Reference: %1$s\n\n%2$s</string>
+
+ <string name="error_network">Network Error</string>
+
+ <string name="toast_back_to_exit">Click BACK again to exit</string>
+
+</resources>
diff --git a/merchant-terminal/src/main/res/values/styles.xml
b/merchant-terminal/src/main/res/values/styles.xml
new file mode 100644
index 0000000..4445a01
--- /dev/null
+++ b/merchant-terminal/src/main/res/values/styles.xml
@@ -0,0 +1,21 @@
+<resources>
+ <!-- Base application theme. -->
+ <style name="AppTheme"
parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorOnPrimary">@android:color/white</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</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.AppCompat.Dark.ActionBar" />
+
+ <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"
/>
+
+</resources>
diff --git a/merchant-terminal/src/main/res/xml/backup_descriptor.xml
b/merchant-terminal/src/main/res/xml/backup_descriptor.xml
new file mode 100644
index 0000000..6fd6103
--- /dev/null
+++ b/merchant-terminal/src/main/res/xml/backup_descriptor.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<full-backup-content>
+ <!-- Exclude specific shared preferences that contain GCM registration Id
-->
+</full-backup-content>
diff --git
a/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt
b/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt
new file mode 100644
index 0000000..cdb928a
--- /dev/null
+++
b/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.merchantpos.order
+
+import android.app.Application
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import kotlinx.coroutines.runBlocking
+import net.taler.merchantpos.R
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class OrderManagerTest {
+
+ private val mapper = ObjectMapper()
+ .registerModule(KotlinModule())
+ .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ private val app: Application = getApplicationContext()
+ private val orderManager = OrderManager(app, mapper)
+
+ @Test
+ fun `config test missing categories`() = runBlocking {
+ val json = JSONObject(
+ """
+ { "categories": [] }
+ """.trimIndent()
+ )
+ val result = orderManager.onConfigurationReceived(json, "KUDOS")
+ assertEquals(app.getString(R.string.config_error_category), result)
+ }
+
+ @Test
+ fun `config test currency mismatch`() = runBlocking {
+ val json = JSONObject(
+ """{
+ "categories": [
+ {
+ "id": 1,
+ "name": "Snacks"
+ }
+ ],
+ "products": [
+ {
+ "product_id": "631361561",
+ "description": "Chips",
+ "price": "WRONGCURRENCY:1.00",
+ "categories": [ 1 ],
+ "delivery_location": "cafeteria"
+ }
+ ]
+ }""".trimIndent()
+ )
+ val result = orderManager.onConfigurationReceived(json, "KUDOS")
+ val expectedStr = app.getString(
+ R.string.config_error_currency, "Chips", "WRONGCURRENCY", "KUDOS"
+ )
+ assertEquals(expectedStr, result)
+ }
+
+ @Test
+ fun `config test unknown category ID`() = runBlocking {
+ val json = JSONObject(
+ """{
+ "categories": [
+ {
+ "id": 1,
+ "name": "Snacks"
+ }
+ ],
+ "products": [
+ {
+ "product_id": "631361561",
+ "description": "Chips",
+ "price": "KUDOS:1.00",
+ "categories": [ 2 ]
+ }
+ ]
+ }""".trimIndent()
+ )
+ val result = orderManager.onConfigurationReceived(json, "KUDOS")
+ val expectedStr = app.getString(
+ R.string.config_error_product_category_id, "Chips", 2
+ )
+ assertEquals(expectedStr, result)
+ }
+
+ @Test
+ fun `config test no products`() = runBlocking {
+ val json = JSONObject(
+ """{
+ "categories": [
+ {
+ "id": 1,
+ "name": "Snacks"
+ }
+ ],
+ "products": []
+ }""".trimIndent()
+ )
+ val result = orderManager.onConfigurationReceived(json, "KUDOS")
+ val expectedStr = app.getString(R.string.config_error_product_zero)
+ assertEquals(expectedStr, result)
+ }
+
+ @Test
+ fun `config test valid config gets accepted`() = runBlocking {
+ val json = JSONObject(
+ """{
+ "categories": [
+ {
+ "id": 1,
+ "name": "Snacks"
+ }
+ ],
+ "products": [
+ {
+ "product_id": "631361561",
+ "description": "Chips",
+ "price": "KUDOS:1.00",
+ "categories": [ 1 ]
+ }
+ ]
+ }""".trimIndent()
+ )
+ val result = orderManager.onConfigurationReceived(json, "KUDOS")
+ assertNull(result)
+ }
+
+}
diff --git a/nightly-stats.patch b/nightly-stats.patch
new file mode 100644
index 0000000..689f46a
--- /dev/null
+++ b/nightly-stats.patch
@@ -0,0 +1,38 @@
+diff --git a/fdroidserver/nightly.py b/fdroidserver/nightly.py
+index 0a3a8012..ae3aa0e3 100644
+--- a/fdroidserver/nightly.py
++++ b/fdroidserver/nightly.py
+@@ -170,6 +170,7 @@ def main():
+ git_mirror_path = os.path.join(repo_basedir, 'git-mirror')
+ git_mirror_repodir = os.path.join(git_mirror_path, 'fdroid', 'repo')
+ git_mirror_metadatadir = os.path.join(git_mirror_path, 'fdroid',
'metadata')
++ git_mirror_statsdir = os.path.join(git_mirror_path, 'fdroid', 'stats')
+ if not os.path.isdir(git_mirror_repodir):
+ logging.debug(_('cloning {url}').format(url=clone_url))
+ try:
+@@ -217,6 +218,8 @@ Last updated: {date}'''.format(repo_git_base=repo_git_base,
+ common.local_rsync(options, git_mirror_repodir + '/', 'repo/')
+ if os.path.isdir(git_mirror_metadatadir):
+ common.local_rsync(options, git_mirror_metadatadir + '/',
'metadata/')
++ if os.path.isdir(git_mirror_statsdir):
++ common.local_rsync(options, git_mirror_statsdir + '/', 'stats/')
+
+ ssh_private_key_file = _ssh_key_from_debug_keystore()
+ # this is needed for GitPython to find the SSH key
+@@ -246,7 +249,7 @@ Last updated: {date}'''.format(repo_git_base=repo_git_base,
+ config += "keydname = '%s'\n" % DISTINGUISHED_NAME
+ config += "make_current_version_link = False\n"
+ config += "accepted_formats = ('txt', 'yml')\n"
+- # TODO add update_stats = True
++ config += "update_stats = True\n"
+ with open('config.py', 'w') as fp:
+ fp.write(config)
+ os.chmod('config.py', 0o600)
+@@ -293,6 +296,7 @@ Last updated: {date}'''.format(repo_git_base=repo_git_base,
+ subprocess.check_call(['fdroid', 'update', '--rename-apks',
'--create-metadata', '--verbose'],
+ cwd=repo_basedir)
+ common.local_rsync(options, repo_basedir + '/metadata/',
git_mirror_metadatadir + '/')
++ common.local_rsync(options, repo_basedir + '/stats/',
git_mirror_statsdir + '/')
+ mirror_git_repo.git.add(all=True)
+ mirror_git_repo.index.commit("update app metadata")
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..a1882de
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,17 @@
+/*
+ * 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/>
+ */
+
+include ':akono', ':cashier', ':merchant-terminal', ':wallet'
diff --git a/wallet/.gitignore b/wallet/.gitignore
new file mode 100644
index 0000000..741e19c
--- /dev/null
+++ b/wallet/.gitignore
@@ -0,0 +1,2 @@
+/build
+/src/main/assets
diff --git a/wallet/.gitlab-ci.yml b/wallet/.gitlab-ci.yml
new file mode 100644
index 0000000..4c1f9a8
--- /dev/null
+++ b/wallet/.gitlab-ci.yml
@@ -0,0 +1,42 @@
+.binary_deps:
+ only:
+ changes:
+ - "wallet"
+ before_script:
+ - wget
"https://git.taler.net/wallet-android.git/plain/akono.aar?h=binary-deps" -O
akono/akono.aar
+ - mkdir -p app/src/main/assets
+ - wget
"https://git.taler.net/wallet-android.git/plain/taler-wallet-android.js?h=binary-deps"
-O app/src/main/assets/taler-wallet-android.js
+
+wallet_test:
+ stage: test
+ extends: .binary_deps
+ script: ./gradlew :wallet:lint :wallet:assembleRelease
+
+wallet_deploy_nightly:
+ stage: deploy
+ extends: .binary_deps
+ only:
+ refs:
+ - master
+ script:
+ # Ensure that key exists
+ - test -z "$DEBUG_KEYSTORE" && exit 0
+ # Rename nightly app
+ - sed -i
+ 's,<string name="app_name">.*</string>,<string name="app_name">Taler
Wallet Nightly</string>,'
+ wallet/src/main/res/values*/strings.xml
+ # Set time-based version code
+ - export versionCode=$(date '+%s')
+ - sed -i "s,^\(\s*versionCode\) *[0-9].*,\1 $versionCode,"
wallet/build.gradle
+ # Add commit to version name
+ - export versionName=$(git rev-parse --short=7 HEAD)
+ - sed -i "s,^\(\s*versionName\ *\"[0-9].*\)\",\1 ($versionName)\","
wallet/build.gradle
+ # Set nightly application ID
+ - sed -i "s,^\(\s*applicationId\) \"*[a-z\.].*\",\1
\"net.taler.wallet.nightly\"," wallet/build.gradle
+ # Build the APK
+ - ./gradlew :wallet:assembleDebug
+ # START only needed while patch not accepted/released upstream
+ - apt update && apt install patch
+ - patch /usr/lib/python3/dist-packages/fdroidserver/nightly.py
nightly-stats.patch
+ # END
+ - CI_PROJECT_URL="https://gitlab.com/gnu-taler/fdroid-repo"
CI_PROJECT_PATH="gnu-taler/fdroid-repo" fdroid nightly -v
diff --git a/wallet/README.md b/wallet/README.md
new file mode 100644
index 0000000..63b128b
--- /dev/null
+++ b/wallet/README.md
@@ -0,0 +1,40 @@
+GNU Taler Wallet
+================
+
+This package implements a GNU Taler wallet for Android.
+It is currently a UI for the wallet writen in TypeScript.
+
+
+Building
+========
+
+Currently, building the wallet for Android requires manually copying two
+dependencies:
+
+`akono.aar` -> `../akono/akono.aar`
+`taler-wallet-android.js` -> `src/main/assets/taler-wallet-android.js`
+
+After that, the Android wallet can be built with Gradle:
+
+ $ ./gradlew build
+
+
+Obtaining Dependencies
+======================
+
+There are two ways of obtaining the dependencies. The easiest one is
+to use the pre-built versions, which are stored in the "binary-deps"
+branch of this repository.
+
+An easy way to access them is using a git worktree:
+
+ $ git fetch origin binary-deps
+ $ git worktree add binary-deps binary-deps
+ $ cp binary-deps/akono.aar ../akono/akono.aar
+ $ cp binary-deps/taler-wallet-android.js
src/main/assets/taler-wallet-android.js
+ $ git worktree remove binary-deps
+
+Alternatively, you can build them yourself from the respective repositories:
+
+ * git://git.taler.net/akono.git
+ * git://git.taler.net/wallet-core.git
diff --git a/wallet/build.gradle b/wallet/build.gradle
new file mode 100644
index 0000000..c31e392
--- /dev/null
+++ b/wallet/build.gradle
@@ -0,0 +1,81 @@
+/*
+ * 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/>
+ */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+ defaultConfig {
+ applicationId "net.taler.wallet"
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 6
+ versionName "0.6.0pre8"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles
getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+ implementation project(":akono")
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.2.0'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+
+ // Navigation Library
+ implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
+ implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
+
+ // ViewModel and LiveData
+ def lifecycle_version = "2.2.0"
+ implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
+ implementation
"androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
+ implementation
"androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
+
+ // QR codes
+ implementation 'com.google.zxing:core:3.4.0'
+ implementation 'com.journeyapps:zxing-android-embedded:3.2.0@aar'
+
+ // Nicer ProgressBar
+ implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1'
+
+ // JSON parsing and serialization
+ implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2'
+
+ testImplementation 'junit:junit:4.13'
+ androidTestImplementation 'androidx.test:runner:1.2.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+}
diff --git a/wallet/proguard-rules.pro b/wallet/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/wallet/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git
a/wallet/src/androidTest/java/net/taler/wallet/ExampleInstrumentedTest.kt
b/wallet/src/androidTest/java/net/taler/wallet/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..5f0c423
--- /dev/null
+++ b/wallet/src/androidTest/java/net/taler/wallet/ExampleInstrumentedTest.kt
@@ -0,0 +1,38 @@
+/*
+ * 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
+
+import androidx.test.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getTargetContext()
+ assertEquals("net.taler.wallet", appContext.packageName)
+ }
+}
diff --git a/wallet/src/main/AndroidManifest.xml
b/wallet/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a61483d
--- /dev/null
+++ b/wallet/src/main/AndroidManifest.xml
@@ -0,0 +1,81 @@
+<?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/>
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="net.taler.wallet">
+
+ <uses-permission android:name="android.permission.NFC" />
+
+ <uses-feature
+ android:name="android.hardware.telephony"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.nfc.hce"
+ android:required="false" />
+
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_descriptor"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme"
+ tools:ignore="GoogleAppIndexingWarning">
+
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme.NoActionBar">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+
+ <data android:scheme="taler" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name="com.journeyapps.barcodescanner.CaptureActivity"
+ android:screenOrientation="fullSensor"
+ tools:replace="screenOrientation" />
+
+ <service
+ android:name=".HostCardEmulatorService"
+ android:exported="true"
+ android:permission="android.permission.BIND_NFC_SERVICE">
+ <intent-filter>
+ <action
android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.nfc.cardemulation.host_apdu_service"
+ android:resource="@xml/apduservice" />
+ </service>
+
+ <service
+ android:name=".backend.WalletBackendService"
+ android:process=":WalletBackendService" />
+ </application>
+
+</manifest>
diff --git a/wallet/src/main/ic_launcher-web.png
b/wallet/src/main/ic_launcher-web.png
new file mode 100644
index 0000000..f0f6be7
Binary files /dev/null and b/wallet/src/main/ic_launcher-web.png differ
diff --git a/wallet/src/main/java/net/taler/wallet/Amount.kt
b/wallet/src/main/java/net/taler/wallet/Amount.kt
new file mode 100644
index 0000000..a19e9bc
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/Amount.kt
@@ -0,0 +1,141 @@
+/*
+ * 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/>
+ */
+
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
+
+package net.taler.wallet
+
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import org.json.JSONObject
+import kotlin.math.round
+
+private const val FRACTIONAL_BASE = 1e8
+
+@JsonDeserialize(using = AmountDeserializer::class)
+data class Amount(val currency: String, val amount: String) {
+ fun isZero(): Boolean {
+ return amount.toDouble() == 0.0
+ }
+
+ companion object {
+ fun fromJson(jsonAmount: JSONObject): Amount {
+ val amountCurrency = jsonAmount.getString("currency")
+ val amountValue = jsonAmount.getString("value")
+ val amountFraction = jsonAmount.getString("fraction")
+ val amountIntValue = Integer.parseInt(amountValue)
+ val amountIntFraction = Integer.parseInt(amountFraction)
+ return Amount(
+ amountCurrency,
+ (amountIntValue + amountIntFraction /
FRACTIONAL_BASE).toString()
+ )
+ }
+
+ fun fromString(strAmount: String): Amount {
+ val components = strAmount.split(":")
+ return Amount(components[0], components[1])
+ }
+ }
+
+ override fun toString(): String {
+ return String.format("%.2f $currency", amount.toDouble())
+ }
+}
+
+class AmountDeserializer : StdDeserializer<Amount>(Amount::class.java) {
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext):
Amount {
+ val node = p.codec.readValue(p, String::class.java)
+ return Amount.fromString(node)
+ }
+}
+
+class ParsedAmount(
+ /**
+ * name of the currency using either a three-character ISO 4217 currency
code,
+ * or a regional currency identifier starting with a "*" followed by at
most 10 characters.
+ * ISO 4217 exponents in the name are not supported,
+ * although the "fraction" is corresponds to an ISO 4217 exponent of 6.
+ */
+ val currency: String,
+
+ /**
+ * unsigned 32 bit value in the currency,
+ * note that "1" here would correspond to 1 EUR or 1 USD, depending on
currency, not 1 cent.
+ */
+ val value: UInt,
+
+ /**
+ * unsigned 32 bit fractional value to be added to value
+ * representing an additional currency fraction,
+ * in units of one millionth (1e-6) of the base currency value.
+ * For example, a fraction of 500,000 would correspond to 50 cents.
+ */
+ val fraction: Double
+) {
+ companion object {
+ fun parseAmount(str: String): ParsedAmount {
+ val split = str.split(":")
+ check(split.size == 2)
+ val currency = split[0]
+ val valueSplit = split[1].split(".")
+ val value = valueSplit[0].toUInt()
+ val fraction: Double = if (valueSplit.size > 1) {
+ round("0.${valueSplit[1]}".toDouble() * FRACTIONAL_BASE)
+ } else 0.0
+ return ParsedAmount(currency, value, fraction)
+ }
+ }
+
+ operator fun minus(other: ParsedAmount): ParsedAmount {
+ check(currency == other.currency) { "Can only subtract from same
currency" }
+ var resultValue = value
+ var resultFraction = fraction
+ if (resultFraction < other.fraction) {
+ if (resultValue < 1u) {
+ return ParsedAmount(currency, 0u, 0.0)
+ }
+ resultValue--
+ resultFraction += FRACTIONAL_BASE
+ }
+ check(resultFraction >= other.fraction)
+ resultFraction -= other.fraction
+ if (resultValue < other.value) {
+ return ParsedAmount(currency, 0u, 0.0)
+ }
+ resultValue -= other.value
+ return ParsedAmount(currency, resultValue, resultFraction)
+ }
+
+ fun isZero(): Boolean {
+ return value == 0u && fraction == 0.0
+ }
+
+ @Suppress("unused")
+ fun toJSONString(): String {
+ return "$currency:${getValueString()}"
+ }
+
+ override fun toString(): String {
+ return "${getValueString()} $currency"
+ }
+
+ private fun getValueString(): String {
+ return "$value${(fraction / FRACTIONAL_BASE).toString().substring(1)}"
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/BalanceFragment.kt
b/wallet/src/main/java/net/taler/wallet/BalanceFragment.kt
new file mode 100644
index 0000000..84a1b3c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/BalanceFragment.kt
@@ -0,0 +1,198 @@
+/*
+ * 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
+
+import android.os.Bundle
+import android.transition.TransitionManager.beginDelayedTransition
+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.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.google.zxing.integration.android.IntentIntegrator
+import com.google.zxing.integration.android.IntentIntegrator.QR_CODE_TYPES
+import kotlinx.android.synthetic.main.fragment_show_balance.*
+import net.taler.wallet.BalanceAdapter.BalanceViewHolder
+
+class BalanceFragment : Fragment() {
+
+ private val model: WalletViewModel by activityViewModels()
+ private val withdrawManager by lazy { model.withdrawManager }
+
+ private var reloadBalanceMenuItem: MenuItem? = null
+ private val balancesAdapter = BalanceAdapter()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_show_balance, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ balancesList.apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = balancesAdapter
+ addItemDecoration(DividerItemDecoration(context, VERTICAL))
+ }
+
+ model.balances.observe(viewLifecycleOwner, Observer {
+ onBalancesChanged(it)
+ })
+
+ model.devMode.observe(viewLifecycleOwner, Observer { enabled ->
+ delayedTransition()
+ testWithdrawButton.visibility = if (enabled) VISIBLE else GONE
+ reloadBalanceMenuItem?.isVisible = enabled
+ })
+ testWithdrawButton.setOnClickListener {
+ withdrawManager.withdrawTestkudos()
+ }
+ withdrawManager.testWithdrawalInProgress.observe(viewLifecycleOwner,
Observer { loading ->
+ Log.v("taler-wallet", "observing balance loading $loading in show
balance")
+ testWithdrawButton.isEnabled = !loading
+ model.showProgressBar.value = loading
+ })
+
+ scanButton.setOnClickListener {
+ IntentIntegrator(activity).apply {
+ setPrompt("")
+ setBeepEnabled(true)
+ setOrientationLocked(false)
+ }.initiateScan(QR_CODE_TYPES)
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ model.loadBalances()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.reload_balance -> {
+ model.loadBalances()
+ true
+ }
+ R.id.developer_mode -> {
+ item.isChecked = !item.isChecked
+ model.devMode.value = item.isChecked
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.balance, menu)
+ menu.findItem(R.id.developer_mode).isChecked = model.devMode.value!!
+ reloadBalanceMenuItem = menu.findItem(R.id.reload_balance).apply {
+ isVisible = model.devMode.value!!
+ }
+ super.onCreateOptionsMenu(menu, inflater)
+ }
+
+ private fun onBalancesChanged(balances: List<BalanceItem>) {
+ delayedTransition()
+ if (balances.isEmpty()) {
+ balancesEmptyState.visibility = VISIBLE
+ balancesList.visibility = GONE
+ } else {
+ balancesAdapter.setItems(balances)
+ balancesEmptyState.visibility = GONE
+ balancesList.visibility = VISIBLE
+ }
+ }
+
+ private fun delayedTransition() {
+ beginDelayedTransition(view as ViewGroup)
+ }
+
+}
+
+class BalanceAdapter : Adapter<BalanceViewHolder>() {
+
+ private var items = emptyList<BalanceItem>()
+
+ init {
+ setHasStableIds(false)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
BalanceViewHolder {
+ val v =
+
LayoutInflater.from(parent.context).inflate(R.layout.list_item_balance, parent,
false)
+ return BalanceViewHolder(v)
+ }
+
+ override fun getItemCount() = items.size
+
+ override fun onBindViewHolder(holder: BalanceViewHolder, position: Int) {
+ val item = items[position]
+ holder.bind(item)
+ }
+
+ fun setItems(items: List<BalanceItem>) {
+ this.items = items
+ this.notifyDataSetChanged()
+ }
+
+ class BalanceViewHolder(private val v: View) : ViewHolder(v) {
+ private val currencyView: TextView =
v.findViewById(R.id.balance_currency)
+ private val amountView: TextView = v.findViewById(R.id.balance_amount)
+ private val balanceInboundAmount: TextView =
v.findViewById(R.id.balanceInboundAmount)
+ private val balanceInboundLabel: TextView =
v.findViewById(R.id.balanceInboundLabel)
+
+ fun bind(item: BalanceItem) {
+ currencyView.text = item.available.currency
+ amountView.text = item.available.amount
+
+ val amountIncoming = item.pendingIncoming
+ if (amountIncoming.isZero()) {
+ balanceInboundAmount.visibility = GONE
+ balanceInboundLabel.visibility = GONE
+ } else {
+ balanceInboundAmount.visibility = VISIBLE
+ balanceInboundLabel.visibility = VISIBLE
+ balanceInboundAmount.text = v.context.getString(
+ R.string.balances_inbound_amount,
+ amountIncoming.amount,
+ amountIncoming.currency
+ )
+ }
+ }
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/HostCardEmulatorService.kt
b/wallet/src/main/java/net/taler/wallet/HostCardEmulatorService.kt
new file mode 100644
index 0000000..93f1d3f
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/HostCardEmulatorService.kt
@@ -0,0 +1,187 @@
+/*
+ * 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
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.Uri
+import android.nfc.cardemulation.HostApduService
+import android.os.Bundle
+import android.util.Log
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.util.concurrent.ConcurrentLinkedDeque
+
+fun makeApduSuccessResponse(payload: ByteArray): ByteArray {
+ val stream = ByteArrayOutputStream()
+ stream.write(payload)
+ stream.write(0x90)
+ stream.write(0x00)
+ return stream.toByteArray()
+}
+
+
+fun makeApduFailureResponse(): ByteArray {
+ val stream = ByteArrayOutputStream()
+ stream.write(0x6F)
+ stream.write(0x00)
+ return stream.toByteArray()
+}
+
+
+fun readApduBodySize(stream: ByteArrayInputStream): Int {
+ val b0 = stream.read()
+ if (b0 == -1) {
+ return 0
+ }
+ if (b0 != 0) {
+ return b0
+ }
+ val b1 = stream.read()
+ val b2 = stream.read()
+
+ return (b1 shl 8) and b2
+}
+
+
+class HostCardEmulatorService: HostApduService() {
+
+ val queuedRequests: ConcurrentLinkedDeque<String> = ConcurrentLinkedDeque()
+ private lateinit var receiver: BroadcastReceiver
+
+ override fun onCreate() {
+ super.onCreate()
+ receiver = object : BroadcastReceiver() {
+ override fun onReceive(p0: Context?, p1: Intent?) {
+ queuedRequests.addLast(p1!!.getStringExtra("tunnelMessage"))
+ }
+ }
+ IntentFilter(HTTP_TUNNEL_REQUEST).also { filter ->
+ registerReceiver(receiver, filter)
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ unregisterReceiver(receiver)
+ }
+
+ override fun onDeactivated(reason: Int) {
+ Log.d(TAG, "Deactivated: $reason")
+ Intent().also { intent ->
+ intent.action = MERCHANT_NFC_DISCONNECTED
+ sendBroadcast(intent)
+ }
+ }
+
+ override fun processCommandApdu(commandApdu: ByteArray?,
+ extras: Bundle?): ByteArray {
+
+ Log.d(TAG, "Processing command APDU")
+
+ if (commandApdu == null) {
+ Log.d(TAG, "APDU is null")
+ return makeApduFailureResponse()
+ }
+
+ val stream = ByteArrayInputStream(commandApdu)
+
+ val command = stream.read()
+
+ if (command != 0) {
+ Log.d(TAG, "APDU has invalid command")
+ return makeApduFailureResponse()
+ }
+
+ val instruction = stream.read()
+
+ // Read instruction parameters, currently ignored.
+ stream.read()
+ stream.read()
+
+ if (instruction == SELECT_INS) {
+ // FIXME: validate body!
+ return makeApduSuccessResponse(ByteArray(0))
+ }
+
+ if (instruction == GET_INS) {
+ val req = queuedRequests.poll()
+ return if (req != null) {
+ Log.v(TAG,"sending tunnel request")
+ makeApduSuccessResponse(req.toByteArray(Charsets.UTF_8))
+ } else {
+ makeApduSuccessResponse(ByteArray(0))
+ }
+ }
+
+ if (instruction == PUT_INS) {
+ val bodySize = readApduBodySize(stream)
+ val talerInstr = stream.read()
+ val bodyBytes = stream.readBytes()
+ if (1 + bodyBytes.size != bodySize) {
+ Log.w(TAG, "mismatched body size ($bodySize vs
${bodyBytes.size}")
+ }
+
+ when (talerInstr) {
+ 1 -> {
+ val url = String(bodyBytes, Charsets.UTF_8)
+ Log.v(TAG, "got URL: '$url'")
+
+ Intent(this, MainActivity::class.java).also { intent ->
+ intent.data = Uri.parse(url)
+ intent.action = Intent.ACTION_VIEW
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
+ }
+ }
+ 2 -> {
+ Log.v(TAG, "got http response:
${bodyBytes.toString(Charsets.UTF_8)}")
+
+ Intent().also { intent ->
+ intent.action = HTTP_TUNNEL_RESPONSE
+ intent.putExtra("response",
bodyBytes.toString(Charsets.UTF_8))
+ sendBroadcast(intent)
+ }
+ }
+ else -> {
+ Log.v(TAG, "taler instruction $talerInstr unknown")
+ }
+ }
+
+ return makeApduSuccessResponse(ByteArray(0))
+ }
+
+ return makeApduFailureResponse()
+ }
+
+ companion object {
+ const val TAG = "taler-wallet-hce"
+ const val SELECT_INS = 0xA4
+ const val PUT_INS = 0xDA
+ const val GET_INS = 0xCA
+
+ const val TRIGGER_PAYMENT_ACTION = "net.taler.TRIGGER_PAYMENT_ACTION"
+
+ const val MERCHANT_NFC_CONNECTED = "net.taler.MERCHANT_NFC_CONNECTED"
+ const val MERCHANT_NFC_DISCONNECTED =
"net.taler.MERCHANT_NFC_DISCONNECTED"
+
+ const val HTTP_TUNNEL_RESPONSE = "net.taler.HTTP_TUNNEL_RESPONSE"
+ const val HTTP_TUNNEL_REQUEST = "net.taler.HTTP_TUNNEL_REQUEST"
+ }
+}
\ 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
new file mode 100644
index 0000000..c2f20f7
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
@@ -0,0 +1,209 @@
+/*
+ * 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
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import android.content.IntentFilter
+import android.os.Bundle
+import android.util.Log
+import android.view.MenuItem
+import android.view.View.GONE
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.widget.TextView
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.GravityCompat.START
+import androidx.lifecycle.Observer
+import androidx.navigation.NavController
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.setupWithNavController
+import
com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import com.google.zxing.integration.android.IntentIntegrator
+import
com.google.zxing.integration.android.IntentIntegrator.parseActivityResult
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.app_bar_main.*
+import net.taler.wallet.BuildConfig.VERSION_CODE
+import net.taler.wallet.BuildConfig.VERSION_NAME
+import net.taler.wallet.HostCardEmulatorService.Companion.HTTP_TUNNEL_RESPONSE
+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 java.util.Locale.ROOT
+
+class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
+ ResetDialogEventListener {
+
+ private val model: WalletViewModel by viewModels()
+
+ private lateinit var nav: NavController
+
+ @SuppressLint("SetTextI18n")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as
NavHostFragment
+ nav = navHostFragment.navController
+ nav_view.setupWithNavController(nav)
+ nav_view.setNavigationItemSelectedListener(this)
+ if (savedInstanceState == null) {
+ nav_view.menu.getItem(0).isChecked = true
+ }
+
+ setSupportActionBar(toolbar)
+ val appBarConfiguration = AppBarConfiguration(
+ setOf(R.id.showBalance, R.id.settings, R.id.walletHistory,
R.id.nav_pending_operations), drawer_layout
+ )
+ toolbar.setupWithNavController(nav, appBarConfiguration)
+
+ model.showProgressBar.observe(this, Observer { show ->
+ progress_bar.visibility = if (show) VISIBLE else INVISIBLE
+ })
+
+ val versionView: TextView =
nav_view.getHeaderView(0).findViewById(R.id.versionView)
+ model.devMode.observe(this, Observer { enabled ->
+ nav_view.menu.findItem(R.id.nav_pending_operations).isVisible =
enabled
+ if (enabled) {
+ @SuppressLint("SetTextI18n")
+ versionView.text = "$VERSION_NAME ($VERSION_CODE)"
+ versionView.visibility = VISIBLE
+ } else versionView.visibility = GONE
+ })
+
+ if (intent.action == ACTION_VIEW) intent.dataString?.let { uri ->
+ handleTalerUri(uri, "intent")
+ }
+
+ //model.startTunnel()
+
+ registerReceiver(triggerPaymentReceiver,
IntentFilter(TRIGGER_PAYMENT_ACTION))
+ registerReceiver(nfcConnectedReceiver,
IntentFilter(MERCHANT_NFC_CONNECTED))
+ registerReceiver(nfcDisconnectedReceiver,
IntentFilter(MERCHANT_NFC_DISCONNECTED))
+ registerReceiver(tunnelResponseReceiver,
IntentFilter(HTTP_TUNNEL_RESPONSE))
+ }
+
+ override fun onBackPressed() {
+ if (drawer_layout.isDrawerOpen(START)) drawer_layout.closeDrawer(START)
+ else super.onBackPressed()
+ }
+
+ override fun onNavigationItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.nav_home -> nav.navigate(R.id.showBalance)
+ R.id.nav_settings -> nav.navigate(R.id.settings)
+ R.id.nav_history -> nav.navigate(R.id.walletHistory)
+ R.id.nav_pending_operations ->
nav.navigate(R.id.nav_pending_operations)
+ }
+ drawer_layout.closeDrawer(START)
+ return true
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data:
Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == IntentIntegrator.REQUEST_CODE) {
+ parseActivityResult(requestCode, resultCode, data)?.contents?.let
{ contents ->
+ handleTalerUri(contents, "QR code")
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ unregisterReceiver(triggerPaymentReceiver)
+ unregisterReceiver(nfcConnectedReceiver)
+ unregisterReceiver(nfcDisconnectedReceiver)
+ unregisterReceiver(tunnelResponseReceiver)
+ super.onDestroy()
+ }
+
+ private fun handleTalerUri(url: String, from: String) {
+ when {
+ url.toLowerCase(ROOT).startsWith("taler://pay/") -> {
+ Log.v(TAG, "navigating!")
+ nav.navigate(R.id.action_showBalance_to_promptPayment)
+ model.paymentManager.preparePay(url)
+ }
+ url.toLowerCase(ROOT).startsWith("taler://withdraw/") -> {
+ Log.v(TAG, "navigating!")
+ nav.navigate(R.id.action_showBalance_to_promptWithdraw)
+ model.withdrawManager.getWithdrawalInfo(url)
+ }
+ url.toLowerCase(ROOT).startsWith("taler://refund/") -> {
+ // TODO implement refunds
+ Snackbar.make(nav_view, "Refunds are not yet implemented",
LENGTH_SHORT).show()
+ }
+ else -> {
+ Snackbar.make(
+ nav_view,
+ "URL from $from doesn't contain a supported Taler Uri.",
+ LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+
+ private val triggerPaymentReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (nav.currentDestination?.id == R.id.promptPayment) return
+ intent.extras?.getString("contractUrl")?.let { url ->
+ nav.navigate(R.id.action_global_promptPayment)
+ model.paymentManager.preparePay(url)
+ }
+ }
+ }
+
+ private val nfcConnectedReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.v(TAG, "got MERCHANT_NFC_CONNECTED")
+ //model.startTunnel()
+ }
+ }
+
+ private val nfcDisconnectedReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.v(TAG, "got MERCHANT_NFC_DISCONNECTED")
+ //model.stopTunnel()
+ }
+ }
+
+ private val tunnelResponseReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.v("taler-tunnel", "got HTTP_TUNNEL_RESPONSE")
+ intent.getStringExtra("response")?.let {
+ model.tunnelResponse(it)
+ }
+ }
+ }
+
+ override fun onResetConfirmed() {
+ model.dangerouslyReset()
+ Snackbar.make(nav_view, "Wallet has been reset", LENGTH_SHORT).show()
+ }
+
+ override fun onResetCancelled() {
+ Snackbar.make(nav_view, "Reset cancelled", LENGTH_SHORT).show()
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/Settings.kt
b/wallet/src/main/java/net/taler/wallet/Settings.kt
new file mode 100644
index 0000000..6d10412
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/Settings.kt
@@ -0,0 +1,140 @@
+/*
+ * 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
+
+import android.app.Dialog
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_CREATE_DOCUMENT
+import android.content.Intent.ACTION_OPEN_DOCUMENT
+import android.content.Intent.CATEGORY_OPENABLE
+import android.content.Intent.EXTRA_TITLE
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import kotlinx.android.synthetic.main.fragment_settings.*
+
+
+interface ResetDialogEventListener {
+ fun onResetConfirmed()
+ fun onResetCancelled()
+}
+
+
+class ResetDialogFragment : DialogFragment() {
+ private lateinit var listener: ResetDialogEventListener
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return activity?.let {
+ // Use the Builder class for convenient dialog construction
+ val builder = AlertDialog.Builder(it)
+ builder.setMessage("Do you really want to reset the wallet and
lose all coins and purchases? Consider making a backup first.")
+ .setPositiveButton("Reset") { _, _ ->
+ listener.onResetConfirmed()
+ }
+ .setNegativeButton("Cancel") { _, _ ->
+ listener.onResetCancelled()
+ }
+ // Create the AlertDialog object and return it
+ builder.create()
+ } ?: throw IllegalStateException("Activity cannot be null")
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ // Verify that the host activity implements the callback interface
+ try {
+ // Instantiate the NoticeDialogListener so we can send events to
the host
+ listener = context as ResetDialogEventListener
+ } catch (e: ClassCastException) {
+ // The activity doesn't implement the interface, throw exception
+ throw ClassCastException((context.toString() +
+ " must implement ResetDialogEventListener"))
+ }
+ }
+}
+
+class Settings : Fragment() {
+
+ companion object {
+ private const val TAG = "taler-wallet"
+ private const val CREATE_FILE = 1
+ private const val PICK_FILE = 2
+ }
+
+ private val model: WalletViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_settings, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ model.devMode.observe(viewLifecycleOwner, Observer { enabled ->
+ val visibility = if (enabled) VISIBLE else GONE
+ devSettingsTitle.visibility = visibility
+ button_reset_wallet_dangerously.visibility = visibility
+ })
+
+ textView4.text = BuildConfig.VERSION_NAME
+ button_reset_wallet_dangerously.setOnClickListener {
+ val d = ResetDialogFragment()
+ d.show(parentFragmentManager, "walletResetDialog")
+ }
+ button_backup_export.setOnClickListener {
+ val intent = Intent(ACTION_CREATE_DOCUMENT).apply {
+ addCategory(CATEGORY_OPENABLE)
+ type = "application/json"
+ putExtra(EXTRA_TITLE, "taler-wallet-backup.json")
+
+ // Optionally, specify a URI for the directory that should be
opened in
+ // the system file picker before your app creates the document.
+ //putExtra(DocumentsContract.EXTRA_INITIAL_URI,
pickerInitialUri)
+ }
+ startActivityForResult(intent, CREATE_FILE)
+ }
+ button_backup_import.setOnClickListener {
+ val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
+ addCategory(CATEGORY_OPENABLE)
+ type = "application/json"
+
+ //putExtra(DocumentsContract.EXTRA_INITIAL_URI,
pickerInitialUri)
+ }
+ startActivityForResult(intent, PICK_FILE)
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data:
Intent?) {
+ if (data == null) return
+ when (requestCode) {
+ CREATE_FILE -> Log.i(TAG, "got createFile result with URL
${data.data}")
+ PICK_FILE -> Log.i(TAG, "got pickFile result with URL
${data.data}")
+ }
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/Utils.kt
b/wallet/src/main/java/net/taler/wallet/Utils.kt
new file mode 100644
index 0000000..fb0b3ae
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/Utils.kt
@@ -0,0 +1,40 @@
+/*
+ * 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
+
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+
+fun View.fadeIn(endAction: () -> Unit = {}) {
+ if (visibility == VISIBLE) return
+ alpha = 0f
+ visibility = VISIBLE
+ animate().alpha(1f).withEndAction {
+ if (context != null) endAction.invoke()
+ }.start()
+}
+
+fun View.fadeOut(endAction: () -> Unit = {}) {
+ if (visibility == INVISIBLE) return
+ animate().alpha(0f).withEndAction {
+ if (context == null) return@withEndAction
+ visibility = INVISIBLE
+ alpha = 1f
+ endAction.invoke()
+ }.start()
+}
diff --git a/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt
b/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt
new file mode 100644
index 0000000..14a800f
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt
@@ -0,0 +1,124 @@
+/*
+ * 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
+
+import android.app.Application
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.distinctUntilChanged
+import
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.history.HistoryManager
+import net.taler.wallet.payment.PaymentManager
+import net.taler.wallet.pending.PendingOperationsManager
+import net.taler.wallet.withdraw.WithdrawManager
+import org.json.JSONObject
+
+const val TAG = "taler-wallet"
+
+data class BalanceItem(val available: Amount, val pendingIncoming: Amount)
+
+class WalletViewModel(val app: Application) : AndroidViewModel(app) {
+
+ private val mBalances = MutableLiveData<List<BalanceItem>>()
+ val balances: LiveData<List<BalanceItem>> =
mBalances.distinctUntilChanged()
+
+ val devMode = MutableLiveData(BuildConfig.DEBUG)
+ val showProgressBar = MutableLiveData<Boolean>()
+
+ private var activeGetBalance = 0
+
+ private val walletBackendApi = WalletBackendApi(app, {
+ activeGetBalance = 0
+ loadBalances()
+ pendingOperationsManager.getPending()
+ }) {
+ Log.i(TAG, "Received notification from wallet-core")
+ loadBalances()
+ pendingOperationsManager.getPending()
+ }
+
+ private val mapper = ObjectMapper()
+ .registerModule(KotlinModule())
+ .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ val withdrawManager = WithdrawManager(walletBackendApi)
+ val paymentManager = PaymentManager(walletBackendApi, mapper)
+ val pendingOperationsManager: PendingOperationsManager =
+ PendingOperationsManager(walletBackendApi)
+ val historyManager = HistoryManager(walletBackendApi, mapper)
+
+ override fun onCleared() {
+ walletBackendApi.destroy()
+ super.onCleared()
+ }
+
+ @UiThread
+ fun loadBalances() {
+ if (activeGetBalance > 0) {
+ return
+ }
+ activeGetBalance++
+ showProgressBar.value = true
+ walletBackendApi.sendRequest("getBalances", null) { isError, result ->
+ activeGetBalance--
+ if (isError) {
+ return@sendRequest
+ }
+ val balanceList = mutableListOf<BalanceItem>()
+ val byCurrency = result.getJSONObject("byCurrency")
+ val currencyList = byCurrency.keys().asSequence().toList().sorted()
+ for (currency in currencyList) {
+ val jsonAmount = byCurrency.getJSONObject(currency)
+ .getJSONObject("available")
+ val amount = Amount.fromJson(jsonAmount)
+ val jsonAmountIncoming = byCurrency.getJSONObject(currency)
+ .getJSONObject("pendingIncoming")
+ val amountIncoming = Amount.fromJson(jsonAmountIncoming)
+ balanceList.add(BalanceItem(amount, amountIncoming))
+ }
+ mBalances.postValue(balanceList)
+ showProgressBar.postValue(false)
+ }
+ }
+
+ @UiThread
+ fun dangerouslyReset() {
+ walletBackendApi.sendRequest("reset", null)
+ withdrawManager.testWithdrawalInProgress.value = false
+ mBalances.value = emptyList()
+ }
+
+ fun startTunnel() {
+ walletBackendApi.sendRequest("startTunnel", null)
+ }
+
+ fun stopTunnel() {
+ walletBackendApi.sendRequest("stopTunnel", null)
+ }
+
+ fun tunnelResponse(resp: String) {
+ val respJson = JSONObject(resp)
+ walletBackendApi.sendRequest("tunnelResponse", respJson)
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
new file mode 100644
index 0000000..d447287
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.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 android.util.SparseArray
+import org.json.JSONObject
+import java.lang.ref.WeakReference
+import java.util.*
+
+class WalletBackendApi(
+ private val app: Application,
+ private val onConnected: (() -> Unit),
+ private val notificationHandler: (() -> Unit)
+) {
+
+ private var walletBackendMessenger: Messenger? = null
+ private val queuedMessages = LinkedList<Message>()
+ private val handlers = SparseArray<(isError: Boolean, message: JSONObject)
-> Unit>()
+ private var nextRequestID = 1
+
+ private val walletBackendConn = object : ServiceConnection {
+ override fun onServiceDisconnected(p0: ComponentName?) {
+ Log.w(TAG, "wallet backend service disconnected (crash?)")
+ walletBackendMessenger = null
+ }
+
+ 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,
WalletBackendService.MSG_SUBSCRIBE_NOTIFY)
+ msg.replyTo = incomingMessenger
+ bm.send(msg)
+ onConnected.invoke()
+ }
+ }
+
+ private class IncomingHandler(strongApi: WalletBackendApi) : Handler() {
+ private val weakApi = WeakReference(strongApi)
+ override fun handleMessage(msg: Message) {
+ val api = weakApi.get() ?: return
+ when (msg.what) {
+ WalletBackendService.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.get(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)
+ }
+ WalletBackendService.MSG_NOTIFY -> {
+ api.notificationHandler.invoke()
+ }
+ }
+ }
+ }
+
+ private val incomingMessenger = Messenger(IncomingHandler(this))
+
+ init {
+ Intent(app, WalletBackendService::class.java).also { intent ->
+ app.bindService(intent, walletBackendConn,
Context.BIND_AUTO_CREATE)
+ }
+ }
+
+ private fun pumpQueue(bm: Messenger) {
+ while (true) {
+ val msg = queuedMessages.pollFirst() ?: return
+ bm.send(msg)
+ }
+ }
+
+
+ fun sendRequest(
+ operation: String,
+ args: JSONObject?,
+ onResponse: (isError: Boolean, message: JSONObject) -> Unit = { _, _
-> }
+ ) {
+ val requestID = nextRequestID++
+ Log.i(TAG, "sending request for operation $operation ($requestID)")
+ val msg = Message.obtain(null, WalletBackendService.MSG_COMMAND)
+ handlers.put(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)
+ }
+ }
+
+ fun destroy() {
+ // FIXME: implement this!
+ }
+
+ companion object {
+ const val TAG = "WalletBackendApi"
+ }
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
new file mode 100644
index 0000000..0b71774
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
@@ -0,0 +1,239 @@
+/*
+ * 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 akono.AkonoJni
+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.wallet.HostCardEmulatorService
+import org.json.JSONObject
+import java.lang.ref.WeakReference
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.system.exitProcess
+
+private const val TAG = "taler-wallet-backend"
+
+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-android.js").use {
+ it.readBytes().toString(Charsets.UTF_8)
+ }
+
+
+ Log.i(TAG, "onCreate in wallet backend service")
+ akono = AkonoJni()
+ akono.putModuleCode("taler-wallet-android", talerWalletAndroidCode)
+ akono.setMessageHandler(object : AkonoJni.MessageHandler {
+ override fun handleMessage(message: String) {
+ this@WalletBackendService.handleAkonoMessage(message)
+ }
+ })
+ akono.evalNodeCode("console.log('hello world from taler
wallet-android')")
+ //akono.evalNodeCode("require('source-map-support').install();")
+ akono.evalNodeCode("require('akono');")
+ akono.evalNodeCode("tw = require('taler-wallet-android');")
+ akono.evalNodeCode("tw.installAndroidWalletListener();")
+ sendInitMessage()
+ initialized = true
+ super.onCreate()
+ }
+
+ private fun sendInitMessage() {
+ val msg = JSONObject()
+ msg.put("operation", "init")
+ val args = JSONObject()
+ msg.put("args", args)
+ args.put("persistentStoragePath",
"${application.filesDir}/talerwalletdb-v30.json")
+ 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
+ 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() {
+ var rm: LinkedList<Messenger>? = null
+ for (s in subscribers) {
+ val m = Message.obtain(null, MSG_NOTIFY)
+ 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) {
+ Log.v(TAG, "got back message: $messageStr")
+ val message = JSONObject(messageStr)
+ when (message.getString("type")) {
+ "notification" -> {
+ sendNotify()
+ }
+ "tunnelHttp" -> {
+ Log.v(TAG, "got http tunnel request!")
+ Intent().also { intent ->
+ intent.action = HostCardEmulatorService.HTTP_TUNNEL_REQUEST
+ intent.putExtra("tunnelMessage", messageStr)
+ application.sendBroadcast(intent)
+ }
+ }
+ "response" -> {
+ when (val operation = message.getString("operation")) {
+ "init" -> {
+ Log.v(TAG, "got response for init operation")
+ sendNotify()
+ }
+ "reset" -> {
+ exitProcess(1)
+ }
+ else -> {
+ val id = message.getInt("id")
+ Log.v(TAG, "got response for operation $operation")
+ val rd = requests[id]
+ if (rd == null) {
+ Log.e(TAG, "wallet returned unknown request ID
($id)")
+ return
+ }
+ val m = Message.obtain(null, MSG_REPLY)
+ val b = m.data
+ if (message.has("result")) {
+ val respJson = message.getJSONObject("result")
+ b.putString("response", respJson.toString(2))
+ } else {
+ b.putString("response", "{}")
+ }
+ b.putBoolean("isError", message.getBoolean("isError"))
+ b.putInt("requestID", rd.clientRequestID)
+ b.putString("operation", operation)
+ rd.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/crypto/Encoding.kt
b/wallet/src/main/java/net/taler/wallet/crypto/Encoding.kt
new file mode 100644
index 0000000..25a59be
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/crypto/Encoding.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.crypto
+
+import java.io.ByteArrayOutputStream
+
+class EncodingException : Exception("Invalid encoding")
+
+
+object Base32Crockford {
+
+ private fun ByteArray.getIntAt(index: Int): Int {
+ val x = this[index].toInt()
+ return if (x >= 0) x else (x + 256)
+ }
+
+ private var encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
+
+ fun encode(data: ByteArray): String {
+ val sb = StringBuilder()
+ val size = data.size
+ var bitBuf = 0
+ var numBits = 0
+ var pos = 0
+ while (pos < size || numBits > 0) {
+ if (pos < size && numBits < 5) {
+ val d = data.getIntAt(pos++)
+ bitBuf = (bitBuf shl 8) or d
+ numBits += 8
+ }
+ if (numBits < 5) {
+ // zero-padding
+ bitBuf = bitBuf shl (5 - numBits)
+ numBits = 5
+ }
+ val v = bitBuf.ushr(numBits - 5) and 31
+ sb.append(encTable[v])
+ numBits -= 5
+ }
+ return sb.toString()
+ }
+
+ fun decode(encoded: String, out: ByteArrayOutputStream) {
+ val size = encoded.length
+ var bitpos = 0
+ var bitbuf = 0
+ var readPosition = 0
+
+ while (readPosition < size || bitpos > 0) {
+ //println("at position $readPosition with bitpos $bitpos")
+ if (readPosition < size) {
+ val v = getValue(encoded[readPosition++])
+ bitbuf = (bitbuf shl 5) or v
+ bitpos += 5
+ }
+ while (bitpos >= 8) {
+ val d = (bitbuf ushr (bitpos - 8)) and 0xFF
+ out.write(d)
+ bitpos -= 8
+ }
+ if (readPosition == size && bitpos > 0) {
+ bitbuf = (bitbuf shl (8 - bitpos)) and 0xFF
+ bitpos = if (bitbuf == 0) 0 else 8
+ }
+ }
+ }
+
+ fun decode(encoded: String): ByteArray {
+ val out = ByteArrayOutputStream()
+ decode(encoded, out)
+ return out.toByteArray()
+ }
+
+ private fun getValue(chr: Char): Int {
+ var a = chr
+ when (a) {
+ 'O', 'o' -> a = '0'
+ 'i', 'I', 'l', 'L' -> a = '1'
+ 'u', 'U' -> a = 'V'
+ }
+ if (a in '0'..'9')
+ return a - '0'
+ if (a in 'a'..'z')
+ a = Character.toUpperCase(a)
+ var dec = 0
+ if (a in 'A'..'Z') {
+ if ('I' < a) dec++
+ if ('L' < a) dec++
+ if ('O' < a) dec++
+ if ('U' < a) dec++
+ return a - 'A' + 10 - dec
+ }
+ throw EncodingException()
+ }
+
+ /**
+ * Compute the length of the resulting string when encoding data of the
given size
+ * in bytes.
+ *
+ * @param dataSize size of the data to encode in bytes
+ * @return size of the string that would result from encoding
+ */
+ @Suppress("unused")
+ fun calculateEncodedStringLength(dataSize: Int): Int {
+ return (dataSize * 8 + 4) / 5
+ }
+
+ /**
+ * Compute the length of the resulting data in bytes when decoding a
(valid) string of the
+ * given size.
+ *
+ * @param stringSize size of the string to decode
+ * @return size of the resulting data in bytes
+ */
+ @Suppress("unused")
+ fun calculateDecodedDataLength(stringSize: Int): Int {
+ return stringSize * 5 / 8
+ }
+}
+
diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
new file mode 100644
index 0000000..9e5c99d
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
@@ -0,0 +1,452 @@
+/*
+ * 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.history
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonSubTypes.Type
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
+import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
+import com.fasterxml.jackson.annotation.JsonTypeName
+import net.taler.wallet.ParsedAmount.Companion.parseAmount
+import net.taler.wallet.R
+import org.json.JSONObject
+
+enum class ReserveType {
+ /**
+ * Manually created.
+ */
+ @JsonProperty("manual")
+ MANUAL,
+ /**
+ * Withdrawn from a bank that has "tight" Taler integration
+ */
+ @JsonProperty("taler-bank-withdraw")
+ @Suppress("unused")
+ TALER_BANK_WITHDRAW,
+}
+
+@JsonInclude(NON_EMPTY)
+class ReserveCreationDetail(val type: ReserveType, val bankUrl: String?)
+
+enum class RefreshReason {
+ @JsonProperty("manual")
+ @Suppress("unused")
+ MANUAL,
+ @JsonProperty("pay")
+ PAY,
+ @JsonProperty("refund")
+ @Suppress("unused")
+ REFUND,
+ @JsonProperty("abort-pay")
+ @Suppress("unused")
+ ABORT_PAY,
+ @JsonProperty("recoup")
+ @Suppress("unused")
+ RECOUP,
+ @JsonProperty("backup-restored")
+ @Suppress("unused")
+ BACKUP_RESTORED
+}
+
+
+@JsonInclude(NON_EMPTY)
+class Timestamp(
+ @JsonProperty("t_ms")
+ val ms: Long
+)
+
+@JsonInclude(NON_EMPTY)
+class ReserveShortInfo(
+ /**
+ * The exchange that the reserve will be at.
+ */
+ val exchangeBaseUrl: String,
+ /**
+ * Key to query more details
+ */
+ val reservePub: String,
+ /**
+ * Detail about how the reserve has been created.
+ */
+ val reserveCreationDetail: ReserveCreationDetail
+)
+
+typealias History = ArrayList<HistoryEvent>
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type",
+ defaultImpl = HistoryUnknownEvent::class
+)
+/** missing:
+AuditorComplaintSent = "auditor-complained-sent",
+AuditorComplaintProcessed = "auditor-complaint-processed",
+AuditorTrustAdded = "auditor-trust-added",
+AuditorTrustRemoved = "auditor-trust-removed",
+ExchangeTermsAccepted = "exchange-terms-accepted",
+ExchangePolicyChanged = "exchange-policy-changed",
+ExchangeTrustAdded = "exchange-trust-added",
+ExchangeTrustRemoved = "exchange-trust-removed",
+FundsDepositedToSelf = "funds-deposited-to-self",
+FundsRecouped = "funds-recouped",
+ReserveCreated = "reserve-created",
+ */
+@JsonSubTypes(
+ Type(value = ExchangeAddedEvent::class, name = "exchange-added"),
+ Type(value = ExchangeUpdatedEvent::class, name = "exchange-updated"),
+ Type(value = ReserveBalanceUpdatedEvent::class, name =
"reserve-balance-updated"),
+ Type(value = HistoryWithdrawnEvent::class, name = "withdrawn"),
+ Type(value = HistoryOrderAcceptedEvent::class, name = "order-accepted"),
+ Type(value = HistoryOrderRefusedEvent::class, name = "order-refused"),
+ Type(value = HistoryOrderRedirectedEvent::class, name =
"order-redirected"),
+ Type(value = HistoryPaymentSentEvent::class, name = "payment-sent"),
+ Type(value = HistoryPaymentAbortedEvent::class, name = "payment-aborted"),
+ Type(value = HistoryTipAcceptedEvent::class, name = "tip-accepted"),
+ Type(value = HistoryTipDeclinedEvent::class, name = "tip-declined"),
+ Type(value = HistoryRefundedEvent::class, name = "refund"),
+ Type(value = HistoryRefreshedEvent::class, name = "refreshed")
+)
+@JsonIgnoreProperties(
+ value = [
+ "eventId"
+ ]
+)
+abstract class HistoryEvent(
+ val timestamp: Timestamp,
+ @get:LayoutRes
+ open val layout: Int = R.layout.history_row,
+ @get:StringRes
+ open val title: Int = 0,
+ @get:DrawableRes
+ open val icon: Int = R.drawable.ic_account_balance,
+ open val showToUser: Boolean = false
+) {
+ open lateinit var json: JSONObject
+}
+
+
+class HistoryUnknownEvent(timestamp: Timestamp) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_unknown
+}
+
+@JsonTypeName("exchange-added")
+class ExchangeAddedEvent(
+ timestamp: Timestamp,
+ val exchangeBaseUrl: String,
+ val builtIn: Boolean
+) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_exchange_added
+}
+
+@JsonTypeName("exchange-updated")
+class ExchangeUpdatedEvent(
+ timestamp: Timestamp,
+ val exchangeBaseUrl: String
+) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_exchange_updated
+}
+
+
+@JsonTypeName("reserve-balance-updated")
+class ReserveBalanceUpdatedEvent(
+ timestamp: Timestamp,
+ val newHistoryTransactions: List<ReserveTransaction>,
+ /**
+ * Condensed information about the reserve.
+ */
+ val reserveShortInfo: ReserveShortInfo,
+ /**
+ * Amount currently left in the reserve.
+ */
+ val amountReserveBalance: String,
+ /**
+ * Amount we expected to be in the reserve at that time,
+ * considering ongoing withdrawals from that reserve.
+ */
+ val amountExpected: String
+) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_reserve_balance_updated
+}
+
+@JsonTypeName("withdrawn")
+class HistoryWithdrawnEvent(
+ timestamp: Timestamp,
+ /**
+ * Exchange that was withdrawn from.
+ */
+ val exchangeBaseUrl: String,
+ /**
+ * Unique identifier for the withdrawal session, can be used to
+ * query more detailed information from the wallet.
+ */
+ val withdrawSessionId: String,
+ val withdrawalSource: WithdrawalSource,
+ /**
+ * Amount that has been subtracted from the reserve's balance
+ * for this withdrawal.
+ */
+ val amountWithdrawnRaw: String,
+ /**
+ * Amount that actually was added to the wallet's balance.
+ */
+ val amountWithdrawnEffective: String
+) : HistoryEvent(timestamp) {
+ override val layout = R.layout.history_receive
+ override val title = R.string.history_event_withdrawn
+ override val icon = R.drawable.history_withdrawn
+ override val showToUser = true
+}
+
+@JsonTypeName("order-accepted")
+class HistoryOrderAcceptedEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order.
+ */
+ val orderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.ic_add_circle
+ override val title = R.string.history_event_order_accepted
+}
+
+@JsonTypeName("order-refused")
+class HistoryOrderRefusedEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order.
+ */
+ val orderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.ic_cancel
+ override val title = R.string.history_event_order_refused
+}
+
+@JsonTypeName("payment-sent")
+class HistoryPaymentSentEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order that we already paid for.
+ */
+ val orderShortInfo: OrderShortInfo,
+ /**
+ * Set to true if the payment has been previously sent
+ * to the merchant successfully, possibly with a different session ID.
+ */
+ val replay: Boolean,
+ /**
+ * Number of coins that were involved in the payment.
+ */
+ val numCoins: Int,
+ /**
+ * Amount that was paid, including deposit and wire fees.
+ */
+ val amountPaidWithFees: String,
+ /**
+ * Session ID that the payment was (re-)submitted under.
+ */
+ val sessionId: String?
+) : HistoryEvent(timestamp) {
+ override val layout = R.layout.history_payment
+ override val title = R.string.history_event_payment_sent
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val showToUser = true
+}
+
+@JsonTypeName("payment-aborted")
+class HistoryPaymentAbortedEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order that we already paid for.
+ */
+ val orderShortInfo: OrderShortInfo,
+ /**
+ * Amount that was lost due to refund and refreshing fees.
+ */
+ val amountLost: String
+) : HistoryEvent(timestamp) {
+ override val layout = R.layout.history_payment
+ override val title = R.string.history_event_payment_aborted
+ override val icon = R.drawable.history_payment_aborted
+ override val showToUser = true
+}
+
+@JsonTypeName("refreshed")
+class HistoryRefreshedEvent(
+ timestamp: Timestamp,
+ /**
+ * Amount that is now available again because it has
+ * been refreshed.
+ */
+ val amountRefreshedEffective: String,
+ /**
+ * Amount that we spent for refreshing.
+ */
+ val amountRefreshedRaw: String,
+ /**
+ * Why was the refreshing done?
+ */
+ val refreshReason: RefreshReason,
+ val numInputCoins: Int,
+ val numRefreshedInputCoins: Int,
+ val numOutputCoins: Int,
+ /**
+ * Identifier for a refresh group, contains one or
+ * more refresh session IDs.
+ */
+ val refreshGroupId: String
+) : HistoryEvent(timestamp) {
+ override val layout = R.layout.history_payment
+ override val icon = R.drawable.history_refresh
+ override val title = R.string.history_event_refreshed
+ override val showToUser =
+ !(parseAmount(amountRefreshedRaw) -
parseAmount(amountRefreshedEffective)).isZero()
+}
+
+@JsonTypeName("order-redirected")
+class HistoryOrderRedirectedEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the new order that contains a
+ * product (identified by the fulfillment URL) that we've already paid for.
+ */
+ val newOrderShortInfo: OrderShortInfo,
+ /**
+ * Condensed info about the order that we already paid for.
+ */
+ val alreadyPaidOrderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.ic_directions
+ override val title = R.string.history_event_order_redirected
+}
+
+@JsonTypeName("tip-accepted")
+class HistoryTipAcceptedEvent(
+ timestamp: Timestamp,
+ /**
+ * Unique identifier for the tip to query more information.
+ */
+ val tipId: String,
+ /**
+ * Raw amount of the tip, without extra fees that apply.
+ */
+ val tipRaw: String
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.history_tip_accepted
+ override val title = R.string.history_event_tip_accepted
+ override val layout = R.layout.history_receive
+ override val showToUser = true
+}
+
+@JsonTypeName("tip-declined")
+class HistoryTipDeclinedEvent(
+ timestamp: Timestamp,
+ /**
+ * Unique identifier for the tip to query more information.
+ */
+ val tipId: String,
+ /**
+ * Raw amount of the tip, without extra fees that apply.
+ */
+ val tipAmount: String
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.history_tip_declined
+ override val title = R.string.history_event_tip_declined
+ override val layout = R.layout.history_receive
+ override val showToUser = true
+}
+
+@JsonTypeName("refund")
+class HistoryRefundedEvent(
+ timestamp: Timestamp,
+ val orderShortInfo: OrderShortInfo,
+ /**
+ * Unique identifier for this refund.
+ * (Identifies multiple refund permissions that were obtained at once.)
+ */
+ val refundGroupId: String,
+ /**
+ * Part of the refund that couldn't be applied because
+ * the refund permissions were expired.
+ */
+ val amountRefundedInvalid: String,
+ /**
+ * Amount that has been refunded by the merchant.
+ */
+ val amountRefundedRaw: String,
+ /**
+ * Amount will be added to the wallet's balance after fees and refreshing.
+ */
+ val amountRefundedEffective: String
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.history_refund
+ override val title = R.string.history_event_refund
+ override val layout = R.layout.history_receive
+ override val showToUser = true
+}
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type"
+)
+@JsonSubTypes(
+ Type(value = WithdrawalSourceReserve::class, name = "reserve")
+)
+abstract class WithdrawalSource
+
+@Suppress("unused")
+@JsonTypeName("tip")
+class WithdrawalSourceTip(
+ val tipId: String
+) : WithdrawalSource()
+
+@JsonTypeName("reserve")
+class WithdrawalSourceReserve(
+ val reservePub: String
+) : WithdrawalSource()
+
+data class OrderShortInfo(
+ /**
+ * Wallet-internal identifier of the proposal.
+ */
+ val proposalId: String,
+ /**
+ * Order ID, uniquely identifies the order within a merchant instance.
+ */
+ val orderId: String,
+ /**
+ * Base URL of the merchant.
+ */
+ val merchantBaseUrl: String,
+ /**
+ * Amount that must be paid for the contract.
+ */
+ val amount: String,
+ /**
+ * Summary of the proposal, given by the merchant.
+ */
+ val summary: String
+)
diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt
b/wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt
new file mode 100644
index 0000000..c350daa
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.history
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.switchMap
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
+import net.taler.wallet.backend.WalletBackendApi
+
+@Suppress("EXPERIMENTAL_API_USAGE")
+class HistoryManager(
+ private val walletBackendApi: WalletBackendApi,
+ private val mapper: ObjectMapper
+) {
+
+ private val mProgress = MutableLiveData<Boolean>()
+ val progress: LiveData<Boolean> = mProgress
+
+ val showAll = MutableLiveData<Boolean>()
+
+ val history: LiveData<History> = showAll.switchMap { showAll ->
+ loadHistory(showAll)
+ .onStart { mProgress.postValue(true) }
+ .onCompletion { mProgress.postValue(false) }
+ .asLiveData(Dispatchers.IO)
+ }
+
+ private fun loadHistory(showAll: Boolean) = callbackFlow {
+ walletBackendApi.sendRequest("getHistory", null) { isError, result ->
+ if (isError) {
+ // TODO show error message in [WalletHistory] fragment
+ close()
+ return@sendRequest
+ }
+ val history = History()
+ val json = result.getJSONArray("history")
+ for (i in 0 until json.length()) {
+ val event: HistoryEvent = mapper.readValue(json.getString(i))
+ event.json = json.getJSONObject(i)
+ history.add(event)
+ }
+ history.reverse() // show latest first
+ offer(if (showAll) history else history.filter { it.showToUser }
as History)
+ close()
+ }
+ awaitClose()
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
new file mode 100644
index 0000000..f51dba9
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.history
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import kotlinx.android.synthetic.main.fragment_json.*
+import net.taler.wallet.R
+
+class JsonDialogFragment : DialogFragment() {
+
+ companion object {
+ fun new(json: String): JsonDialogFragment {
+ return JsonDialogFragment().apply {
+ arguments = Bundle().apply { putString("json", json) }
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_json, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val json = arguments!!.getString("json")
+ jsonView.text = json
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
new file mode 100644
index 0000000..45c539c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.history
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
+import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
+import com.fasterxml.jackson.annotation.JsonTypeName
+
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type"
+)
+@JsonSubTypes(
+ JsonSubTypes.Type(value = ReserveDepositTransaction::class, name =
"DEPOSIT")
+)
+abstract class ReserveTransaction
+
+
+@JsonTypeName("DEPOSIT")
+class ReserveDepositTransaction(
+ /**
+ * Amount withdrawn.
+ */
+ val amount: String,
+ /**
+ * Sender account payto://-URL
+ */
+ @JsonProperty("sender_account_url")
+ val senderAccountUrl: String,
+ /**
+ * Transfer details uniquely identifying the transfer.
+ */
+ @JsonProperty("wire_reference")
+ val wireReference: String,
+ /**
+ * Timestamp of the incoming wire transfer.
+ */
+ val timestamp: Timestamp
+) : ReserveTransaction()
diff --git
a/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
new file mode 100644
index 0000000..71bdebc
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.history
+
+import android.annotation.SuppressLint
+import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
+import android.text.format.DateUtils.DAY_IN_MILLIS
+import android.text.format.DateUtils.FORMAT_ABBREV_MONTH
+import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
+import android.text.format.DateUtils.FORMAT_NO_YEAR
+import android.text.format.DateUtils.FORMAT_SHOW_DATE
+import android.text.format.DateUtils.FORMAT_SHOW_TIME
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.formatDateTime
+import android.text.format.DateUtils.getRelativeTimeSpanString
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.CallSuper
+import androidx.core.net.toUri
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.wallet.BuildConfig
+import net.taler.wallet.ParsedAmount
+import net.taler.wallet.ParsedAmount.Companion.parseAmount
+import net.taler.wallet.R
+
+
+internal class WalletHistoryAdapter(
+ private val listener: OnEventClickListener,
+ private var history: History = History()
+) : Adapter<WalletHistoryAdapter.HistoryEventViewHolder>() {
+
+ init {
+ setHasStableIds(false)
+ }
+
+ override fun getItemViewType(position: Int): Int = history[position].layout
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
HistoryEventViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(viewType,
parent, false)
+ return when (viewType) {
+ R.layout.history_receive -> HistoryReceiveViewHolder(view)
+ R.layout.history_payment -> HistoryPaymentViewHolder(view)
+ else -> GenericHistoryEventViewHolder(view)
+ }
+ }
+
+ override fun getItemCount(): Int = history.size
+
+ override fun onBindViewHolder(holder: HistoryEventViewHolder, position:
Int) {
+ val event = history[position]
+ holder.bind(event)
+ }
+
+ fun update(updatedHistory: History) {
+ this.history = updatedHistory
+ this.notifyDataSetChanged()
+ }
+
+ internal abstract inner class HistoryEventViewHolder(protected val v:
View) : ViewHolder(v) {
+
+ private val icon: ImageView = v.findViewById(R.id.icon)
+ protected val title: TextView = v.findViewById(R.id.title)
+ private val time: TextView = v.findViewById(R.id.time)
+
+ @CallSuper
+ open fun bind(event: HistoryEvent) {
+ if (BuildConfig.DEBUG) { // doesn't produce recycling issues, no
need to cover all cases
+ v.setOnClickListener { listener.onEventClicked(event) }
+ } else {
+ v.background = null
+ }
+ icon.setImageResource(event.icon)
+ if (event.title == 0) title.text = event::class.java.simpleName
+ else title.setText(event.title)
+ time.text = getRelativeTime(event.timestamp.ms)
+ }
+
+ private fun getRelativeTime(timestamp: Long): CharSequence {
+ val now = System.currentTimeMillis()
+ return if (now - timestamp > DAY_IN_MILLIS * 2) {
+ formatDateTime(
+ v.context,
+ timestamp,
+ FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or
FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR
+ )
+ } else {
+ getRelativeTimeSpanString(timestamp, now, MINUTE_IN_MILLIS,
FORMAT_ABBREV_RELATIVE)
+ }
+ }
+
+ }
+
+ internal inner class GenericHistoryEventViewHolder(v: View) :
HistoryEventViewHolder(v) {
+
+ private val info: TextView = v.findViewById(R.id.info)
+
+ override fun bind(event: HistoryEvent) {
+ super.bind(event)
+ info.text = when (event) {
+ is ExchangeAddedEvent -> event.exchangeBaseUrl
+ is ExchangeUpdatedEvent -> event.exchangeBaseUrl
+ is ReserveBalanceUpdatedEvent ->
parseAmount(event.amountReserveBalance).toString()
+ is HistoryPaymentSentEvent -> event.orderShortInfo.summary
+ is HistoryOrderAcceptedEvent -> event.orderShortInfo.summary
+ is HistoryOrderRefusedEvent -> event.orderShortInfo.summary
+ is HistoryOrderRedirectedEvent ->
event.newOrderShortInfo.summary
+ else -> ""
+ }
+ }
+
+ }
+
+ internal inner class HistoryReceiveViewHolder(v: View) :
HistoryEventViewHolder(v) {
+
+ private val summary: TextView = v.findViewById(R.id.summary)
+ private val amountWithdrawn: TextView =
v.findViewById(R.id.amountWithdrawn)
+ private val feeLabel: TextView = v.findViewById(R.id.feeLabel)
+ private val fee: TextView = v.findViewById(R.id.fee)
+
+ override fun bind(event: HistoryEvent) {
+ super.bind(event)
+ when (event) {
+ is HistoryWithdrawnEvent -> bind(event)
+ is HistoryRefundedEvent -> bind(event)
+ is HistoryTipAcceptedEvent -> bind(event)
+ is HistoryTipDeclinedEvent -> bind(event)
+ }
+ }
+
+ private fun bind(event: HistoryWithdrawnEvent) {
+ title.text = getHostname(event.exchangeBaseUrl)
+ summary.setText(event.title)
+
+ val parsedEffective = parseAmount(event.amountWithdrawnEffective)
+ val parsedRaw = parseAmount(event.amountWithdrawnRaw)
+ showAmounts(parsedEffective, parsedRaw)
+ }
+
+ private fun bind(event: HistoryRefundedEvent) {
+ title.text = event.orderShortInfo.summary
+ summary.setText(event.title)
+
+ val parsedEffective = parseAmount(event.amountRefundedEffective)
+ val parsedRaw = parseAmount(event.amountRefundedRaw)
+ showAmounts(parsedEffective, parsedRaw)
+ }
+
+ private fun bind(event: HistoryTipAcceptedEvent) {
+ title.setText(event.title)
+ summary.text = null
+ val amount = parseAmount(event.tipRaw)
+ showAmounts(amount, amount)
+ }
+
+ private fun bind(event: HistoryTipDeclinedEvent) {
+ title.setText(event.title)
+ summary.text = null
+ val amount = parseAmount(event.tipAmount)
+ showAmounts(amount, amount)
+ amountWithdrawn.paintFlags = amountWithdrawn.paintFlags or
STRIKE_THRU_TEXT_FLAG
+ }
+
+ private fun showAmounts(effective: ParsedAmount, raw: ParsedAmount) {
+ @SuppressLint("SetTextI18n")
+ amountWithdrawn.text = "+$raw"
+ val calculatedFee = raw - effective
+ if (calculatedFee.isZero()) {
+ fee.visibility = GONE
+ feeLabel.visibility = GONE
+ } else {
+ @SuppressLint("SetTextI18n")
+ fee.text = "-$calculatedFee"
+ fee.visibility = VISIBLE
+ feeLabel.visibility = VISIBLE
+ }
+ amountWithdrawn.paintFlags = fee.paintFlags
+ }
+
+ private fun getHostname(url: String): String {
+ return url.toUri().host!!
+ }
+
+ }
+
+ internal inner class HistoryPaymentViewHolder(v: View) :
HistoryEventViewHolder(v) {
+
+ private val summary: TextView = v.findViewById(R.id.summary)
+ private val amountPaidWithFees: TextView =
v.findViewById(R.id.amountPaidWithFees)
+
+ override fun bind(event: HistoryEvent) {
+ super.bind(event)
+ summary.setText(event.title)
+ when (event) {
+ is HistoryPaymentSentEvent -> bind(event)
+ is HistoryPaymentAbortedEvent -> bind(event)
+ is HistoryRefreshedEvent -> bind(event)
+ }
+ }
+
+ private fun bind(event: HistoryPaymentSentEvent) {
+ title.text = event.orderShortInfo.summary
+ @SuppressLint("SetTextI18n")
+ amountPaidWithFees.text =
"-${parseAmount(event.amountPaidWithFees)}"
+ }
+
+ private fun bind(event: HistoryPaymentAbortedEvent) {
+ title.text = event.orderShortInfo.summary
+ @SuppressLint("SetTextI18n")
+ amountPaidWithFees.text = "-${parseAmount(event.amountLost)}"
+ }
+
+ private fun bind(event: HistoryRefreshedEvent) {
+ title.text = ""
+ val fee =
+ parseAmount(event.amountRefreshedRaw) -
parseAmount(event.amountRefreshedEffective)
+ @SuppressLint("SetTextI18n")
+ if (fee.isZero()) amountPaidWithFees.text = null
+ else amountPaidWithFees.text = "-$fee"
+ }
+
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/history/WalletHistoryFragment.kt
b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryFragment.kt
new file mode 100644
index 0000000..4f8ab82
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryFragment.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.history
+
+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.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+import kotlinx.android.synthetic.main.fragment_show_balance.*
+import kotlinx.android.synthetic.main.fragment_show_history.*
+import net.taler.wallet.R
+import net.taler.wallet.WalletViewModel
+
+interface OnEventClickListener {
+ fun onEventClicked(event: HistoryEvent)
+}
+
+class WalletHistoryFragment : Fragment(), OnEventClickListener {
+
+ private val model: WalletViewModel by activityViewModels()
+ private val historyManager by lazy { model.historyManager }
+ private lateinit var showAllItem: MenuItem
+ private var reloadHistoryItem: MenuItem? = null
+ private val historyAdapter = WalletHistoryAdapter(this)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_show_history, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ historyList.apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = historyAdapter
+ addItemDecoration(DividerItemDecoration(context, VERTICAL))
+ }
+
+ model.devMode.observe(viewLifecycleOwner, Observer { enabled ->
+ reloadHistoryItem?.isVisible = enabled
+ })
+ historyManager.progress.observe(viewLifecycleOwner, Observer { show ->
+ historyProgressBar.visibility = if (show) VISIBLE else INVISIBLE
+ })
+ historyManager.history.observe(viewLifecycleOwner, Observer { history
->
+ historyEmptyState.visibility = if (history.isEmpty()) VISIBLE else
INVISIBLE
+ historyAdapter.update(history)
+ })
+
+ // kicks off initial load, needs to be adapted if showAll state is
ever saved
+ if (savedInstanceState == null) historyManager.showAll.value =
model.devMode.value
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.history, menu)
+ showAllItem = menu.findItem(R.id.show_all_history)
+ showAllItem.isChecked = historyManager.showAll.value == true
+ reloadHistoryItem = menu.findItem(R.id.reload_history).apply {
+ isVisible = model.devMode.value!!
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.show_all_history -> {
+ item.isChecked = !item.isChecked
+ historyManager.showAll.value = item.isChecked
+ true
+ }
+ R.id.reload_history -> {
+ historyManager.showAll.value = showAllItem.isChecked
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onEventClicked(event: HistoryEvent) {
+ if (model.devMode.value != true) return
+ JsonDialogFragment.new(event.json.toString(4))
+ .show(parentFragmentManager, null)
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt
b/wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt
new file mode 100644
index 0000000..33e3a1d
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt
@@ -0,0 +1,47 @@
+/*
+ * 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 kotlinx.android.synthetic.main.fragment_already_paid.*
+import net.taler.wallet.R
+
+/**
+ * Display the message that the user already paid for the order
+ * that the merchant is proposing.
+ */
+class AlreadyPaidFragment : Fragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_already_paid, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ backButton.setOnClickListener {
+ findNavController().navigateUp()
+ }
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/ContractTerms.kt
b/wallet/src/main/java/net/taler/wallet/payment/ContractTerms.kt
new file mode 100644
index 0000000..da91dea
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/ContractTerms.kt
@@ -0,0 +1,56 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties
+import com.fasterxml.jackson.annotation.JsonProperty
+import net.taler.wallet.Amount
+
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class ContractTerms(
+ val summary: String,
+ val products: List<ContractProduct>,
+ val amount: Amount
+)
+
+interface Product {
+ val id: String?
+ val description: String
+ val price: Amount
+ val location: String?
+ val image: String?
+}
+
+@JsonIgnoreProperties("totalPrice")
+data class ContractProduct(
+ @JsonProperty("product_id")
+ override val id: String?,
+ override val description: String,
+ override val price: Amount,
+ @JsonProperty("delivery_location")
+ override val location: String?,
+ override val image: String?,
+ val quantity: Int
+) : Product {
+
+ val totalPrice: Amount by lazy {
+ val amount = price.amount.toDouble() * quantity
+ Amount(price.currency, amount.toString())
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
new file mode 100644
index 0000000..ee0edaf
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import net.taler.wallet.Amount
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.WalletBackendApi
+import org.json.JSONObject
+import java.net.MalformedURLException
+
+val REGEX_PRODUCT_IMAGE =
Regex("^data:image/(jpeg|png);base64,([A-Za-z0-9+/=]+)$")
+
+class PaymentManager(
+ private val walletBackendApi: WalletBackendApi,
+ private val mapper: ObjectMapper
+) {
+
+ private val mPayStatus = MutableLiveData<PayStatus>(PayStatus.None)
+ internal val payStatus: LiveData<PayStatus> = mPayStatus
+
+ private val mDetailsShown = MutableLiveData<Boolean>()
+ internal val detailsShown: LiveData<Boolean> = mDetailsShown
+
+ private var currentPayRequestId = 0
+
+ @UiThread
+ fun preparePay(url: String) {
+ mPayStatus.value = PayStatus.Loading
+ mDetailsShown.value = false
+
+ val args = JSONObject(mapOf("url" to url))
+
+ currentPayRequestId += 1
+ val payRequestId = currentPayRequestId
+
+ walletBackendApi.sendRequest("preparePay", args) { isError, result ->
+ when {
+ isError -> {
+ Log.v(TAG, "got preparePay error result")
+ mPayStatus.value = PayStatus.Error(result.toString())
+ }
+ payRequestId != this.currentPayRequestId -> {
+ Log.v(TAG, "preparePay result was for old request")
+ }
+ else -> {
+ val status = result.getString("status")
+ try {
+ mPayStatus.postValue(getPayStatusUpdate(status,
result))
+ } catch (e: Exception) {
+ Log.e(TAG, "Error getting PayStatusUpdate", e)
+ mPayStatus.postValue(PayStatus.Error(e.message ?:
"unknown error"))
+ }
+ }
+ }
+ }
+ }
+
+ private fun getPayStatusUpdate(status: String, json: JSONObject) = when
(status) {
+ "payment-possible" -> PayStatus.Prepared(
+ contractTerms = getContractTerms(json),
+ proposalId = json.getString("proposalId"),
+ totalFees = Amount.fromJson(json.getJSONObject("totalFees"))
+ )
+ "paid" -> PayStatus.AlreadyPaid(getContractTerms(json))
+ "insufficient-balance" ->
PayStatus.InsufficientBalance(getContractTerms(json))
+ "error" -> PayStatus.Error("got some error")
+ else -> PayStatus.Error("unknown status")
+ }
+
+ private fun getContractTerms(json: JSONObject): ContractTerms {
+ val terms: ContractTerms =
mapper.readValue(json.getString("contractTermsRaw"))
+ // validate product images
+ terms.products.forEach { product ->
+ product.image?.let { image ->
+ if (REGEX_PRODUCT_IMAGE.matchEntire(image) == null) {
+ throw MalformedURLException("Invalid image data URL for
${product.description}")
+ }
+ }
+ }
+ return terms
+ }
+
+ @UiThread
+ fun toggleDetailsShown() {
+ val oldValue = mDetailsShown.value ?: false
+ mDetailsShown.value = !oldValue
+ }
+
+ fun confirmPay(proposalId: String) {
+ val args = JSONObject(mapOf("proposalId" to proposalId))
+
+ walletBackendApi.sendRequest("confirmPay", args) { _, _ ->
+ mPayStatus.postValue(PayStatus.Success)
+ }
+ }
+
+ @UiThread
+ fun abortPay() {
+ val ps = payStatus.value
+ if (ps is PayStatus.Prepared) {
+ abortProposal(ps.proposalId)
+ }
+ resetPayStatus()
+ }
+
+ internal fun abortProposal(proposalId: String) {
+ val args = JSONObject(mapOf("proposalId" to proposalId))
+
+ Log.i(TAG, "aborting proposal")
+
+ walletBackendApi.sendRequest("abortProposal", args) { isError, _ ->
+ if (isError) {
+ Log.e(TAG, "received error response to abortProposal")
+ return@sendRequest
+ }
+ mPayStatus.postValue(PayStatus.None)
+ }
+ }
+
+ @UiThread
+ fun resetPayStatus() {
+ mPayStatus.value = PayStatus.None
+ }
+
+}
+
+sealed class PayStatus {
+ object None : PayStatus()
+ object Loading : PayStatus()
+ data class Prepared(
+ val contractTerms: ContractTerms,
+ val proposalId: String,
+ val totalFees: Amount
+ ) : PayStatus()
+
+ data class InsufficientBalance(val contractTerms: ContractTerms) :
PayStatus()
+ data class AlreadyPaid(val contractTerms: ContractTerms) : PayStatus()
+ data class Error(val error: String) : PayStatus()
+ object Success : PayStatus()
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt
b/wallet/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt
new file mode 100644
index 0000000..2084c45
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt
@@ -0,0 +1,49 @@
+/*
+ * 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 kotlinx.android.synthetic.main.fragment_payment_successful.*
+import net.taler.wallet.R
+import net.taler.wallet.fadeIn
+
+/**
+ * Fragment that shows the success message for a payment.
+ */
+class PaymentSuccessfulFragment : Fragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_payment_successful,
container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ successImageView.fadeIn()
+ successTextView.fadeIn()
+ backButton.setOnClickListener {
+ findNavController().navigateUp()
+ }
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
b/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
new file mode 100644
index 0000000..4b1b062
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.graphics.Bitmap
+import android.graphics.BitmapFactory.decodeByteArray
+import android.util.Base64
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.wallet.R
+import net.taler.wallet.payment.ProductAdapter.ProductViewHolder
+
+internal interface ProductImageClickListener {
+ fun onImageClick(image: Bitmap)
+}
+
+internal class ProductAdapter(private val listener: ProductImageClickListener)
:
+ RecyclerView.Adapter<ProductViewHolder>() {
+
+ private val items = ArrayList<ContractProduct>()
+
+ override fun getItemCount() = items.size
+
+ override fun getItemViewType(position: Int): Int {
+ return if (itemCount == 1) 1 else 0
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ProductViewHolder {
+ val res =
+ if (viewType == 1) R.layout.list_item_product_single else
R.layout.list_item_product
+ val view = LayoutInflater.from(parent.context).inflate(res, parent,
false)
+ return ProductViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
+ holder.bind(items[position])
+ }
+
+ fun setItems(items: List<ContractProduct>) {
+ this.items.clear()
+ this.items.addAll(items)
+ notifyDataSetChanged()
+ }
+
+ internal inner class ProductViewHolder(v: View) : ViewHolder(v) {
+ private val quantity: TextView = v.findViewById(R.id.quantity)
+ private val image: ImageView = v.findViewById(R.id.image)
+ private val name: TextView = v.findViewById(R.id.name)
+ private val price: TextView = v.findViewById(R.id.price)
+
+ fun bind(product: ContractProduct) {
+ quantity.text = product.quantity.toString()
+ if (product.image == 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)
+ if (itemCount > 1) image.setOnClickListener {
+ listener.onImageClick(bitmap)
+ }
+ }
+ name.text = product.description
+ price.text = product.totalPrice.toString()
+ }
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/payment/ProductImageFragment.kt
b/wallet/src/main/java/net/taler/wallet/payment/ProductImageFragment.kt
new file mode 100644
index 0000000..02414a6
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/ProductImageFragment.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.graphics.Bitmap
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import kotlinx.android.synthetic.main.fragment_product_image.*
+import net.taler.wallet.R
+
+class ProductImageFragment private constructor() : DialogFragment() {
+
+ companion object {
+ private const val IMAGE = "image"
+
+ fun new(image: Bitmap) = ProductImageFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable(IMAGE, image)
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_product_image, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val bitmap = arguments!!.getParcelable<Bitmap>(IMAGE)
+ productImageView.setImageBitmap(bitmap)
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
new file mode 100644
index 0000000..44dcf26
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
@@ -0,0 +1,168 @@
+/*
+ * 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.annotation.SuppressLint
+import android.graphics.Bitmap
+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.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.lifecycle.observe
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.transition.TransitionManager.beginDelayedTransition
+import kotlinx.android.synthetic.main.payment_bottom_bar.*
+import kotlinx.android.synthetic.main.payment_details.*
+import net.taler.wallet.Amount
+import net.taler.wallet.R
+import net.taler.wallet.WalletViewModel
+import net.taler.wallet.fadeIn
+import net.taler.wallet.fadeOut
+
+/**
+ * Show a payment and ask the user to accept/decline.
+ */
+class PromptPaymentFragment : Fragment(), ProductImageClickListener {
+
+ private val model: WalletViewModel by activityViewModels()
+ private val paymentManager by lazy { model.paymentManager }
+ private val adapter = ProductAdapter(this)
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_prompt_payment, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ paymentManager.payStatus.observe(viewLifecycleOwner,
this::onPaymentStatusChanged)
+ paymentManager.detailsShown.observe(viewLifecycleOwner, Observer {
shown ->
+ beginDelayedTransition(view as ViewGroup)
+ val res = if (shown) R.string.payment_hide_details else
R.string.payment_show_details
+ detailsButton.setText(res)
+ productsList.visibility = if (shown) VISIBLE else GONE
+ })
+
+ detailsButton.setOnClickListener {
+ paymentManager.toggleDetailsShown()
+ }
+ productsList.apply {
+ adapter = this@PromptPaymentFragment.adapter
+ layoutManager = LinearLayoutManager(requireContext())
+ }
+
+ abortButton.setOnClickListener {
+ paymentManager.abortPay()
+ findNavController().navigateUp()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!requireActivity().isChangingConfigurations) {
+ paymentManager.abortPay()
+ }
+ }
+
+ private fun showLoading(show: Boolean) {
+ model.showProgressBar.value = show
+ if (show) {
+ progressBar.fadeIn()
+ } else {
+ progressBar.fadeOut()
+ }
+ }
+
+ private fun onPaymentStatusChanged(payStatus: PayStatus) {
+ when (payStatus) {
+ is PayStatus.Prepared -> {
+ showLoading(false)
+ showOrder(payStatus.contractTerms, payStatus.totalFees)
+ confirmButton.isEnabled = true
+ confirmButton.setOnClickListener {
+ model.showProgressBar.value = true
+ paymentManager.confirmPay(payStatus.proposalId)
+ confirmButton.fadeOut()
+ confirmProgressBar.fadeIn()
+ }
+ }
+ is PayStatus.InsufficientBalance -> {
+ showLoading(false)
+ showOrder(payStatus.contractTerms, null)
+ errorView.setText(R.string.payment_balance_insufficient)
+ errorView.fadeIn()
+ }
+ is PayStatus.Success -> {
+ showLoading(false)
+ paymentManager.resetPayStatus()
+
findNavController().navigate(R.id.action_promptPayment_to_paymentSuccessful)
+ }
+ is PayStatus.AlreadyPaid -> {
+ showLoading(false)
+ paymentManager.resetPayStatus()
+
findNavController().navigate(R.id.action_promptPayment_to_alreadyPaid)
+ }
+ is PayStatus.Error -> {
+ showLoading(false)
+ errorView.text = getString(R.string.payment_error,
payStatus.error)
+ errorView.fadeIn()
+ }
+ is PayStatus.None -> {
+ // No payment active.
+ showLoading(false)
+ }
+ is PayStatus.Loading -> {
+ // Wait until loaded ...
+ showLoading(true)
+ }
+ }
+ }
+
+ private fun showOrder(contractTerms: ContractTerms, totalFees: Amount?) {
+ orderView.text = contractTerms.summary
+ adapter.setItems(contractTerms.products)
+ if (contractTerms.products.size == 1)
paymentManager.toggleDetailsShown()
+ val amount = contractTerms.amount
+ @SuppressLint("SetTextI18n")
+ totalView.text = "${amount.amount} ${amount.currency}"
+ if (totalFees != null && !totalFees.isZero()) {
+ val fee = "${totalFees.amount} ${totalFees.currency}"
+ feeView.text = getString(R.string.payment_fee, fee)
+ feeView.fadeIn()
+ } else {
+ feeView.visibility = GONE
+ }
+ orderLabelView.fadeIn()
+ orderView.fadeIn()
+ if (contractTerms.products.size > 1) detailsButton.fadeIn()
+ totalLabelView.fadeIn()
+ totalView.fadeIn()
+ }
+
+ override fun onImageClick(image: Bitmap) {
+ val f = ProductImageFragment.new(image)
+ f.show(parentFragmentManager, "image")
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
new file mode 100644
index 0000000..946e5ba
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import kotlinx.android.synthetic.main.fragment_pending_operations.*
+import net.taler.wallet.R
+import net.taler.wallet.TAG
+import net.taler.wallet.WalletViewModel
+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: WalletViewModel by activityViewModels()
+ private val pendingOperationsManager by lazy {
model.pendingOperationsManager }
+
+ 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? {
+ return inflater.inflate(R.layout.fragment_pending_operations,
container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ list_pending.apply {
+ val myLayoutManager = LinearLayoutManager(requireContext())
+ val myItemDecoration =
+ DividerItemDecoration(requireContext(),
myLayoutManager.orientation)
+ layoutManager = myLayoutManager
+ adapter = pendingAdapter
+ addItemDecoration(myItemDecoration)
+ }
+
+ pendingOperationsManager.pendingOperations.observe(viewLifecycleOwner,
Observer {
+ updatePending(it)
+ })
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.retry_pending -> {
+ pendingOperationsManager.retryPendingNow()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ 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) {
+ Snackbar.make(view!!, "No detail view for $type implemented yet.",
LENGTH_SHORT).show()
+ }
+
+ 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.pending_row,
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
new file mode 100644
index 0000000..2125dbc
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
@@ -0,0 +1,64 @@
+/*
+ * 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)
{
+
+ private var activeGetPending = 0
+
+ val pendingOperations = MutableLiveData<List<PendingOperationInfo>>()
+
+ internal fun getPending() {
+ if (activeGetPending > 0) {
+ return
+ }
+ activeGetPending++
+ walletBackendApi.sendRequest("getPendingOperations", null) { isError,
result ->
+ activeGetPending--
+ if (isError) {
+ Log.i(TAG, "got getPending error result")
+ 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", null)
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ErrorFragment.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/ErrorFragment.kt
new file mode 100644
index 0000000..f0f6610
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/ErrorFragment.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.withdraw
+
+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.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_error.*
+import net.taler.wallet.R
+import net.taler.wallet.WalletViewModel
+
+class ErrorFragment : Fragment() {
+
+ private val model: WalletViewModel by activityViewModels()
+ private val withdrawManager by lazy { model.withdrawManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_error, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ errorTitle.setText(R.string.withdraw_error_title)
+ errorMessage.setText(R.string.withdraw_error_message)
+
+ // show dev error message if dev mode is on
+ val status = withdrawManager.withdrawStatus.value
+ if (model.devMode.value == true && status is WithdrawStatus.Error) {
+ errorDevMessage.visibility = VISIBLE
+ errorDevMessage.text = status.message
+ } else {
+ errorDevMessage.visibility = GONE
+ }
+
+ backButton.setOnClickListener {
+ findNavController().navigateUp()
+ }
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
new file mode 100644
index 0000000..454816b
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.withdraw
+
+import android.annotation.SuppressLint
+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.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_prompt_withdraw.*
+import net.taler.wallet.R
+import net.taler.wallet.WalletViewModel
+import net.taler.wallet.fadeIn
+import net.taler.wallet.fadeOut
+import net.taler.wallet.withdraw.WithdrawStatus.Loading
+import net.taler.wallet.withdraw.WithdrawStatus.TermsOfServiceReviewRequired
+import net.taler.wallet.withdraw.WithdrawStatus.Withdrawing
+
+class PromptWithdrawFragment : Fragment() {
+
+ private val model: WalletViewModel by activityViewModels()
+ private val withdrawManager by lazy { model.withdrawManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_prompt_withdraw, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ button_cancel_withdraw.setOnClickListener {
+ withdrawManager.cancelCurrentWithdraw()
+ findNavController().navigateUp()
+ }
+
+ button_confirm_withdraw.setOnClickListener {
+ val status = withdrawManager.withdrawStatus.value
+ if (status !is WithdrawStatus.ReceivedDetails) throw
AssertionError()
+ it.fadeOut()
+ confirmProgressBar.fadeIn()
+ withdrawManager.acceptWithdrawal(status.talerWithdrawUri,
status.suggestedExchange)
+ }
+
+ withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer {
+ showWithdrawStatus(it)
+ })
+ }
+
+ private fun showWithdrawStatus(status: WithdrawStatus?) = when (status) {
+ is WithdrawStatus.ReceivedDetails -> {
+ model.showProgressBar.value = false
+ progressBar.fadeOut()
+
+ introView.fadeIn()
+ @SuppressLint("SetTextI18n")
+ withdrawAmountView.text = "${status.amount.amount}
${status.amount.currency}"
+ withdrawAmountView.fadeIn()
+ feeView.fadeIn()
+
+ exchangeIntroView.fadeIn()
+ withdrawExchangeUrl.text = status.suggestedExchange
+ withdrawExchangeUrl.fadeIn()
+
+ button_confirm_withdraw.isEnabled = true
+ }
+ is WithdrawStatus.Success -> {
+ model.showProgressBar.value = false
+ withdrawManager.withdrawStatus.value = null
+
findNavController().navigate(R.id.action_promptWithdraw_to_withdrawSuccessful)
+ }
+ is Loading -> {
+ model.showProgressBar.value = true
+ }
+ is Withdrawing -> {
+ model.showProgressBar.value = true
+ }
+ is TermsOfServiceReviewRequired -> {
+ model.showProgressBar.value = false
+
findNavController().navigate(R.id.action_promptWithdraw_to_reviewExchangeTOS)
+ }
+ is WithdrawStatus.Error -> {
+ model.showProgressBar.value = false
+
findNavController().navigate(R.id.action_promptWithdraw_to_errorFragment)
+ }
+ null -> model.showProgressBar.value = false
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt
new file mode 100644
index 0000000..cd01a33
--- /dev/null
+++
b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.withdraw
+
+
+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.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_review_exchange_tos.*
+import net.taler.wallet.R
+import net.taler.wallet.WalletViewModel
+import net.taler.wallet.fadeIn
+import net.taler.wallet.fadeOut
+
+class ReviewExchangeTosFragment : Fragment() {
+
+ private val model: WalletViewModel by activityViewModels()
+ private val withdrawManager by lazy { model.withdrawManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_review_exchange_tos,
container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ acceptTosCheckBox.isChecked = false
+ acceptTosCheckBox.setOnCheckedChangeListener { _, isChecked ->
+ acceptTosButton.isEnabled = isChecked
+ }
+ abortTosButton.setOnClickListener {
+ withdrawManager.cancelCurrentWithdraw()
+ findNavController().navigateUp()
+ }
+ acceptTosButton.setOnClickListener {
+ withdrawManager.acceptCurrentTermsOfService()
+ }
+ withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer {
+ when (it) {
+ is WithdrawStatus.TermsOfServiceReviewRequired -> {
+ tosTextView.text = it.tosText
+ tosTextView.fadeIn()
+ acceptTosCheckBox.fadeIn()
+ progressBar.fadeOut()
+ }
+ is WithdrawStatus.Loading -> {
+
findNavController().navigate(R.id.action_reviewExchangeTOS_to_promptWithdraw)
+ }
+ is WithdrawStatus.ReceivedDetails -> {
+
findNavController().navigate(R.id.action_reviewExchangeTOS_to_promptWithdraw)
+ }
+ else -> {
+ }
+ }
+ })
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
new file mode 100644
index 0000000..e3af757
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
@@ -0,0 +1,209 @@
+/*
+ * 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.withdraw
+
+import android.util.Log
+import androidx.lifecycle.MutableLiveData
+import net.taler.wallet.Amount
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.WalletBackendApi
+import org.json.JSONObject
+
+sealed class WithdrawStatus {
+ data class Loading(val talerWithdrawUri: String) : WithdrawStatus()
+ data class TermsOfServiceReviewRequired(
+ val talerWithdrawUri: String,
+ val exchangeBaseUrl: String,
+ val tosText: String,
+ val tosEtag: String,
+ val amount: Amount,
+ val suggestedExchange: String
+ ) : WithdrawStatus()
+
+ data class ReceivedDetails(
+ val talerWithdrawUri: String,
+ val amount: Amount,
+ val suggestedExchange: String
+ ) : WithdrawStatus()
+
+ data class Withdrawing(val talerWithdrawUri: String) : WithdrawStatus()
+
+ object Success : WithdrawStatus()
+ data class Error(val message: String?) : WithdrawStatus()
+}
+
+class WithdrawManager(private val walletBackendApi: WalletBackendApi) {
+
+ val withdrawStatus = MutableLiveData<WithdrawStatus>()
+ val testWithdrawalInProgress = MutableLiveData(false)
+
+ private var currentWithdrawRequestId = 0
+
+ fun withdrawTestkudos() {
+ testWithdrawalInProgress.value = true
+
+ walletBackendApi.sendRequest("withdrawTestkudos", null) { _, _ ->
+ testWithdrawalInProgress.postValue(false)
+ }
+ }
+
+ fun getWithdrawalInfo(talerWithdrawUri: String) {
+ val args = JSONObject()
+ args.put("talerWithdrawUri", talerWithdrawUri)
+
+ withdrawStatus.value = WithdrawStatus.Loading(talerWithdrawUri)
+
+ this.currentWithdrawRequestId++
+ val myWithdrawRequestId = this.currentWithdrawRequestId
+
+ walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) {
isError, result ->
+ if (isError) {
+ Log.e(TAG, "Error getWithdrawDetailsForUri
${result.toString(4)}")
+ val message = if (result.has("message"))
result.getString("message") else null
+ withdrawStatus.postValue(WithdrawStatus.Error(message))
+ return@sendRequest
+ }
+ if (myWithdrawRequestId != this.currentWithdrawRequestId) {
+ val mismatch = "$myWithdrawRequestId !=
${this.currentWithdrawRequestId}"
+ Log.w(TAG, "Got withdraw result for different request id
$mismatch")
+ return@sendRequest
+ }
+ Log.v(TAG, "got getWithdrawDetailsForUri result")
+ val status = withdrawStatus.value
+ if (status !is WithdrawStatus.Loading) {
+ Log.v(TAG, "ignoring withdrawal info result, not loading.")
+ return@sendRequest
+ }
+ val wi = result.getJSONObject("bankWithdrawDetails")
+ val suggestedExchange = wi.getString("suggestedExchange")
+ // We just use the suggested exchange, in the future there will be
+ // a selection dialog.
+ getWithdrawalInfoWithExchange(talerWithdrawUri, suggestedExchange)
+ }
+ }
+
+ private fun getWithdrawalInfoWithExchange(talerWithdrawUri: String,
selectedExchange: String) {
+ val args = JSONObject()
+ args.put("talerWithdrawUri", talerWithdrawUri)
+ args.put("selectedExchange", selectedExchange)
+
+ this.currentWithdrawRequestId++
+ val myWithdrawRequestId = this.currentWithdrawRequestId
+
+ walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) {
isError, result ->
+ if (isError) {
+ Log.e(TAG, "Error getWithdrawDetailsForUri
${result.toString(4)}")
+ val message = if (result.has("message"))
result.getString("message") else null
+ withdrawStatus.postValue(WithdrawStatus.Error(message))
+ return@sendRequest
+ }
+ if (myWithdrawRequestId != this.currentWithdrawRequestId) {
+ val mismatch = "$myWithdrawRequestId !=
${this.currentWithdrawRequestId}"
+ Log.w(TAG, "Got withdraw result for different request id
$mismatch")
+ return@sendRequest
+ }
+ Log.v(TAG, "got getWithdrawDetailsForUri result (with exchange
details)")
+ val status = withdrawStatus.value
+ if (status !is WithdrawStatus.Loading) {
+ Log.v(TAG, "ignoring withdrawal info result, not loading.")
+ return@sendRequest
+ }
+ val wi = result.getJSONObject("bankWithdrawDetails")
+ val suggestedExchange = wi.getString("suggestedExchange")
+ val amount = Amount.fromJson(wi.getJSONObject("amount"))
+
+ val ei = result.getJSONObject("exchangeWithdrawDetails")
+ val termsOfServiceAccepted =
ei.getBoolean("termsOfServiceAccepted")
+
+ if (!termsOfServiceAccepted) {
+ val exchange = ei.getJSONObject("exchangeInfo")
+ val tosText = exchange.getString("termsOfServiceText")
+ val tosEtag = exchange.optString("termsOfServiceLastEtag",
"undefined")
+ withdrawStatus.postValue(
+ WithdrawStatus.TermsOfServiceReviewRequired(
+ status.talerWithdrawUri,
+ selectedExchange,
+ tosText,
+ tosEtag,
+ amount,
+ suggestedExchange
+ )
+ )
+ } else {
+ withdrawStatus.postValue(
+ WithdrawStatus.ReceivedDetails(
+ status.talerWithdrawUri,
+ amount,
+ suggestedExchange
+ )
+ )
+ }
+ }
+ }
+
+ fun acceptWithdrawal(talerWithdrawUri: String, selectedExchange: String) {
+ val args = JSONObject()
+ args.put("talerWithdrawUri", talerWithdrawUri)
+ args.put("selectedExchange", selectedExchange)
+
+ withdrawStatus.value = WithdrawStatus.Withdrawing(talerWithdrawUri)
+
+ walletBackendApi.sendRequest("acceptWithdrawal", args) { isError, _ ->
+ if (isError) {
+ Log.v(TAG, "got acceptWithdrawal error result")
+ return@sendRequest
+ }
+ Log.v(TAG, "got acceptWithdrawal result")
+ val status = withdrawStatus.value
+ if (status !is WithdrawStatus.Withdrawing) {
+ Log.v(TAG, "ignoring acceptWithdrawal result, invalid state")
+ }
+ withdrawStatus.postValue(WithdrawStatus.Success)
+ }
+ }
+
+ /**
+ * Accept the currently displayed terms of service.
+ */
+ fun acceptCurrentTermsOfService() {
+ when (val s = withdrawStatus.value) {
+ is WithdrawStatus.TermsOfServiceReviewRequired -> {
+ val args = JSONObject()
+ args.put("exchangeBaseUrl", s.exchangeBaseUrl)
+ args.put("etag", s.tosEtag)
+ walletBackendApi.sendRequest("acceptExchangeTermsOfService",
args) { isError, _ ->
+ if (isError) {
+ return@sendRequest
+ }
+ withdrawStatus.postValue(
+ WithdrawStatus.ReceivedDetails(
+ s.talerWithdrawUri,
+ s.amount,
+ s.suggestedExchange
+ )
+ )
+ }
+ }
+ }
+ }
+
+ fun cancelCurrentWithdraw() {
+ currentWithdrawRequestId++
+ withdrawStatus.value = null
+ }
+
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawSuccessfulFragment.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawSuccessfulFragment.kt
new file mode 100644
index 0000000..5daeff1
--- /dev/null
+++
b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawSuccessfulFragment.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.withdraw
+
+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 kotlinx.android.synthetic.main.fragment_withdraw_successful.*
+import net.taler.wallet.R
+
+class WithdrawSuccessfulFragment : Fragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_withdraw_successful,
container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ backButton.setOnClickListener {
+ findNavController().navigateUp()
+ }
+ }
+
+}
diff --git a/wallet/src/main/res/drawable/history_payment_aborted.xml
b/wallet/src/main/res/drawable/history_payment_aborted.xml
new file mode 100644
index 0000000..03cd7b2
--- /dev/null
+++ b/wallet/src/main/res/drawable/history_payment_aborted.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<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="#000"
+ android:pathData="M15.46 18.12L16.88 19.54L19 17.41L21.12
19.54L22.54 18.12L20.41 16L22.54 13.88L21.12 12.46L19 14.59L16.88 12.46L15.46
13.88L17.59 16M14.97 11.62C14.86 10.28 13.58 8.97 12 9C10.3 9.04 9 10.3 9 12C9
13.7 10.3 14.94 12 15C12.39 15 12.77 14.92 13.14 14.77C13.41 13.67 13.86 12.63
14.97 11.62M13 16H7C7 14.9 6.1 14 5 14V10C6.1 10 7 9.1 7 8H17C17 9.1 17.9 10 19
10V10.05C19.67 10.06 20.34 10.18 21 10.4V6H3V18H13.32C13.1 17.33 13 16.66 13
16Z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/history_refresh.xml
b/wallet/src/main/res/drawable/history_refresh.xml
new file mode 100644
index 0000000..f5d8972
--- /dev/null
+++ b/wallet/src/main/res/drawable/history_refresh.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ 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/>
+ -->
+
+<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="#FF000000"
+ android:pathData="M14.97,11.62C14.86,10.28 13.58,8.97
12,9c-1.7,0.04 -3,1.3 -3,3 0,1.7 1.3,2.94 3,3 0.39,0 0.77,-0.08 1.14,-0.23
0.27,-1.1 0.72,-2.14 1.83,-3.15M13,16H7C7,14.9 6.1,14 5,14V10C6.1,10 7,9.1
7,8h10c0,1.1 0.9,2 2,2v0.05c0.67,0.01 1.34,0.13 2,0.35V6H3V18H13.32C13.1,17.33
13,16.66 13,16Z" />
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19,12 L16.75,14.25 19,16.5V15c1.38,0 2.5,1.12
2.5,2.5 0,0.4 -0.09,0.78 -0.26,1.12l1.09,1.09C22.75,19.08 23,18.32
23,17.5c0,-2.21 -1.79,-4 -4,-4V12m-3.33,3.29C15.25,15.92 15,16.68
15,17.5c0,2.21 1.79,4 4,4V23L21.25,20.75 19,18.5V20c-1.38,0 -2.5,-1.12
-2.5,-2.5 0,-0.4 0.09,-0.78 0.26,-1.12z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/history_refund.xml
b/wallet/src/main/res/drawable/history_refund.xml
new file mode 100644
index 0000000..60872a9
--- /dev/null
+++ b/wallet/src/main/res/drawable/history_refund.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<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="#000"
+ android:pathData="M3,11H21V23H3V11M12,15A2,2 0 0,1 14,17A2,2 0 0,1
12,19A2,2 0 0,1 10,17A2,2 0 0,1 12,15M7,13A2,2 0 0,1 5,15V19A2,2 0 0,1
7,21H17A2,2 0 0,1 19,19V15A2,2 0 0,1
17,13H7M17,5V10H15.5V6.5H9.88L12.3,8.93L11.24,10L7,5.75L11.24,1.5L12.3,2.57L9.88,5H17Z"
/>
+</vector>
diff --git a/wallet/src/main/res/drawable/history_tip_accepted.xml
b/wallet/src/main/res/drawable/history_tip_accepted.xml
new file mode 100644
index 0000000..794d1bf
--- /dev/null
+++ b/wallet/src/main/res/drawable/history_tip_accepted.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<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="#000"
+ android:pathData="M18,21L15,18L18,15V17H22V19H18V21M10,4A4,4 0 0,1
14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,14C11.15,14 12.25,14.12
13.24,14.34C12.46,15.35 12,16.62 12,18C12,18.7 12.12,19.37
12.34,20H2V18C2,15.79 5.58,14 10,14Z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/history_tip_declined.xml
b/wallet/src/main/res/drawable/history_tip_declined.xml
new file mode 100644
index 0000000..4838ee4
--- /dev/null
+++ b/wallet/src/main/res/drawable/history_tip_declined.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<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="#000"
+ android:pathData="M10 4A4 4 0 0 0 6 8A4 4 0 0 0 10 12A4 4 0 0 0 14
8A4 4 0 0 0 10 4M17.5 13C15 13 13 15 13 17.5C13 20 15 22 17.5 22C20 22 22 20 22
17.5C22 15 20 13 17.5 13M10 14C5.58 14 2 15.79 2 18V20H11.5A6.5 6.5 0 0 1 11
17.5A6.5 6.5 0 0 1 11.95 14.14C11.32 14.06 10.68 14 10 14M17.5 14.5C19.16 14.5
20.5 15.84 20.5 17.5C20.5 18.06 20.35 18.58 20.08 19L16 14.92C16.42 14.65 16.94
14.5 17.5 14.5M14.92 16L19 20.08C18.58 20.35 18.06 20.5 17.5 20.5C15.84 20.5
14.5 19.16 14.5 17.5 [...]
+</vector>
diff --git a/wallet/src/main/res/drawable/history_withdrawn.xml
b/wallet/src/main/res/drawable/history_withdrawn.xml
new file mode 100644
index 0000000..f524474
--- /dev/null
+++ b/wallet/src/main/res/drawable/history_withdrawn.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<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="#000"
+ android:pathData="M3 0V3H0V5H3V8H5V5H8V3H5V0H3M9 3V6H6V9H3V19C3
20.1 3.89 21 5 21H19C20.11 21 21 20.11 21 19V18H12C10.9 18 10 17.11 10 16V8C10
6.9 10.89 6 12 6H21V5C21 3.9 20.11 3 19 3H9M12 8V16H22V8H12M16 10.5C16.83 10.5
17.5 11.17 17.5 12C17.5 12.83 16.83 13.5 16 13.5C15.17 13.5 14.5 12.83 14.5
12C14.5 11.17 15.17 10.5 16 10.5Z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_account_balance.xml
b/wallet/src/main/res/drawable/ic_account_balance.xml
new file mode 100644
index 0000000..e9f51a2
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_account_balance.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+
android:pathData="M4,10v7h3v-7L4,10zM10,10v7h3v-7h-3zM2,22h19v-3L2,19v3zM16,10v7h3v-7h-3zM11.5,1L2,6v2h19L21,6l-9.5,-5z"
/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_account_balance_wallet.xml
b/wallet/src/main/res/drawable/ic_account_balance_wallet.xml
new file mode 100644
index 0000000..514b118
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_account_balance_wallet.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M21,18v1c0,1.1 -0.9,2 -2,2L5,21c-1.11,0 -2,-0.9
-2,-2L3,5c0,-1.1 0.89,-2 2,-2h14c1.1,0 2,0.9 2,2v1h-9c-1.11,0 -2,0.9
-2,2v8c0,1.1 0.89,2 2,2h9zM12,16h10L22,8L12,8v8zM16,13.5c-0.83,0 -1.5,-0.67
-1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_add_circle.xml
b/wallet/src/main/res/drawable/ic_add_circle.xml
new file mode 100644
index 0000000..c32faa6
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_add_circle.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48
10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_cancel.xml
b/wallet/src/main/res/drawable/ic_cancel.xml
new file mode 100644
index 0000000..6dc55cf
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_cancel.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47
10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41
8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_cash_usd_outline.xml
b/wallet/src/main/res/drawable/ic_cash_usd_outline.xml
new file mode 100644
index 0000000..428a466
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_cash_usd_outline.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<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="#000"
+ android:pathData="M20,18H4V6H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0
0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4M11,17H13V16H14A1,1 0 0,0
15,15V12A1,1 0 0,0 14,11H11V10H15V8H13V7H11V8H10A1,1 0 0,0 9,9V12A1,1 0 0,0
10,13H13V14H9V16H11V17Z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_check_circle.xml
b/wallet/src/main/res/drawable/ic_check_circle.xml
new file mode 100644
index 0000000..c299cc3
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_check_circle.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:alpha="0.56"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/green"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48
10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_directions.xml
b/wallet/src/main/res/drawable/ic_directions.xml
new file mode 100644
index 0000000..7fc7fe7
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_directions.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M21.71,11.29l-9,-9c-0.39,-0.39 -1.02,-0.39
-1.41,0l-9,9c-0.39,0.39 -0.39,1.02 0,1.41l9,9c0.39,0.39 1.02,0.39
1.41,0l9,-9c0.39,-0.38 0.39,-1.01 0,-1.41zM14,14.5V12h-4v3H8v-4c0,-0.55 0.45,-1
1,-1h5V7.5l3.5,3.5 -3.5,3.5z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_error.xml
b/wallet/src/main/res/drawable/ic_error.xml
new file mode 100644
index 0000000..1f705af
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_error.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48
10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_history_black_24dp.xml
b/wallet/src/main/res/drawable/ic_history_black_24dp.xml
new file mode 100644
index 0000000..4404ee4
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_history_black_24dp.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89
0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0
-3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03
9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_home_black_24dp.xml
b/wallet/src/main/res/drawable/ic_home_black_24dp.xml
new file mode 100644
index 0000000..ed8aa1e
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_home_black_24dp.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_launcher_foreground.xml
b/wallet/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..028c873
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,68 @@
+<!--
+ ~ 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="237.28813"
+ android:viewportHeight="237.2881">
+ <group
+ android:translateX="48.64407"
+ android:translateY="48.644062">
+ <path
+ android:fillAlpha="1"
+ android:fillColor="#ffffff"
+
android:pathData="m31.669,82.748h-4.702v-19.684h-6.041v-4.112h16.783v4.112L31.669,63.064Z"
+ android:strokeWidth="0.81604069"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillAlpha="1"
+ android:fillColor="#ffffff"
+ android:pathData="m50.301,74.364q-2.614,0 -3.65,0.669
-1.036,0.669 -1.036,2.295 0,1.211 0.717,1.929 0.717,0.717 1.944,0.717 1.849,0
2.869,-1.387 1.02,-1.403
1.02,-3.905v-0.319zM56.804,72.563v10.185h-4.638v-1.992q-0.845,1.179
-2.168,1.817 -1.323,0.638 -2.917,0.638 -3.044,0 -4.75,-1.61 -1.689,-1.61
-1.689,-4.495 0,-3.124 2.024,-4.606 2.024,-1.498
6.264,-1.498h3.235v-0.781q0,-1.132 -0.829,-1.705 -0.813,-0.59 -2.407,-0.59
-1.674,0 -3.251,0.43 -1.562,0.414 -3.267,1.339v-3.985q [...]
+ android:strokeWidth="0.81604069"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillAlpha="1"
+ android:fillColor="#ffffff"
+
android:pathData="m64.964,75.305v-13.771h-4.734v-3.586h9.404v17.357q0,2.104
0.653,2.98 0.653,0.877 2.215,0.877h3.73v3.586h-5.037q-3.331,0 -4.781,-1.721
-1.45,-1.721 -1.45,-5.722z"
+ android:strokeWidth="0.81604069"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillAlpha="1"
+ android:fillColor="#ffffff"
+ android:pathData="m96.012,81.871q-1.626,0.669 -3.315,1.004
-1.689,0.335 -3.57,0.335 -4.479,0 -6.853,-2.391 -2.359,-2.407 -2.359,-6.917
0,-4.367 2.279,-6.901 2.279,-2.534 6.216,-2.534 3.969,0 6.152,2.359 2.199,2.343
2.199,6.614v1.897h-12.097q0.016,2.104 1.243,3.14 1.227,1.036 3.666,1.036 1.61,0
3.172,-0.462 1.562,-0.462 3.267,-1.466zM92.059,71.83q-0.032,-1.849
-0.956,-2.789 -0.908,-0.956 -2.694,-0.956 -1.61,0 -2.566,0.988 -0.956,0.972
-1.132,2.773z"
+ android:strokeWidth="0.81604069"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillAlpha="1"
+ android:fillColor="#ffffff"
+ android:pathData="m116.445,69.822q-0.765,-0.701 -1.801,-1.052
-1.02,-0.351 -2.247,-0.351 -1.482,0 -2.598,0.526 -1.1,0.51 -1.705,1.498
-0.383,0.606 -0.542,1.466 -0.143,0.861
-0.143,2.614v8.224h-4.67v-17.851h4.67v2.773q0.685,-1.53 2.104,-2.359
1.419,-0.845 3.315,-0.845 0.956,0 1.865,0.239 0.924,0.223 1.753,0.669z"
+ android:strokeWidth="0.81604069"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillAlpha="1"
+ android:fillColor="#ae1010"
+ android:pathData="M25.843,97.583L16.433,97.583L0,70.865
16.433,44.111l9.409,0l-16.522,26.754z"
+ android:strokeWidth="2.03518677"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillAlpha="1"
+ android:fillColor="#ae1010"
+ android:pathData="m109.483,97.667 l17.087,-27.134
-17.041,-27.171l9.712,0l17.041,27.171 -17.041,27.134z"
+ android:strokeWidth="2.08855891"
+ android:strokeColor="#00000000" />
+ </group>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_scan_qr.xml
b/wallet/src/main/res/drawable/ic_scan_qr.xml
new file mode 100644
index 0000000..2ca8a69
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_scan_qr.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorOnPrimarySurface"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#000"
+
android:pathData="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2
0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1
0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_settings.xml
b/wallet/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 0000000..7cadd58
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,9 @@
+<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="#FF000000"
+ android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0
-0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6
-0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5
-0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4
0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8
2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6
0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5, [...]
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_sync.xml
b/wallet/src/main/res/drawable/ic_sync.xml
new file mode 100644
index 0000000..78593fc
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_sync.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01
-0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8
-8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97
0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" />
+</vector>
diff --git a/wallet/src/main/res/drawable/pending_border.xml
b/wallet/src/main/res/drawable/pending_border.xml
new file mode 100644
index 0000000..bb50fea
--- /dev/null
+++ b/wallet/src/main/res/drawable/pending_border.xml
@@ -0,0 +1,37 @@
+<?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/>
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+
+ <!-- View background color -->
+ <solid
+ android:color="@android:color/transparent" >
+ </solid>
+
+ <!-- View border color and width -->
+ <stroke
+ android:width="1dp"
+ android:color="@color/colorPrimary" >
+ </stroke>
+
+ <!-- The radius makes the corners rounded -->
+ <corners
+ android:radius="2dp" >
+ </corners>
+
+</shape>
\ No newline at end of file
diff --git a/wallet/src/main/res/drawable/side_nav_bar.xml
b/wallet/src/main/res/drawable/side_nav_bar.xml
new file mode 100644
index 0000000..6be80a8
--- /dev/null
+++ b/wallet/src/main/res/drawable/side_nav_bar.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ 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/>
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient
+ android:angle="135"
+ android:startColor="@color/colorPrimary"
+ android:endColor="@color/colorPrimaryDark"
+ android:type="linear"/>
+</shape>
\ No newline at end of file
diff --git a/wallet/src/main/res/layout-w550dp/payment_bottom_bar.xml
b/wallet/src/main/res/layout-w550dp/payment_bottom_bar.xml
new file mode 100644
index 0000000..d9e2f59
--- /dev/null
+++ b/wallet/src/main/res/layout-w550dp/payment_bottom_bar.xml
@@ -0,0 +1,123 @@
+<?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/>
+ -->
+
+<com.google.android.material.card.MaterialCardView
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:id="@+id/bottomView"
+ style="@style/BottomCard"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ tools:showIn="@layout/fragment_prompt_payment">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/totalLabelView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:text="@string/payment_label_amount_total"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/totalView"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toEndOf="@+id/abortButton"
+ app:layout_constraintTop_toTopOf="@+id/totalView"
+ app:layout_constraintVertical_bias="0.0"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/totalView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textStyle="bold"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/feeView"
+ app:layout_constraintEnd_toStartOf="@+id/confirmButton"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toEndOf="@+id/totalLabelView"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_goneMarginBottom="8dp"
+ tools:text="10 TESTKUDOS"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/feeView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/confirmButton"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/totalView"
+ tools:text="@string/payment_fee"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/abortButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:backgroundTint="@color/red"
+ android:text="@string/payment_button_abort"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/confirmButton"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <Button
+ android:id="@+id/confirmButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:backgroundTint="@color/green"
+ android:enabled="false"
+ android:text="@string/payment_button_confirm"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/abortButton"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:enabled="true" />
+
+ <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/confirmButton"
+ app:layout_constraintEnd_toEndOf="@+id/confirmButton"
+ app:layout_constraintStart_toStartOf="@+id/confirmButton"
+ app:layout_constraintTop_toTopOf="@+id/confirmButton"
+ tools:visibility="visible" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/wallet/src/main/res/layout/activity_main.xml
b/wallet/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..0612306
--- /dev/null
+++ b/wallet/src/main/res/layout/activity_main.xml
@@ -0,0 +1,40 @@
+<?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.drawerlayout.widget.DrawerLayout
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:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:openDrawer="start">
+
+ <include
+ layout="@layout/app_bar_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <com.google.android.material.navigation.NavigationView
+ android:id="@+id/nav_view"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:fitsSystemWindows="false"
+ app:headerLayout="@layout/nav_header_main"
+ app:menu="@menu/activity_main_drawer" />
+
+</androidx.drawerlayout.widget.DrawerLayout>
diff --git a/wallet/src/main/res/layout/app_bar_main.xml
b/wallet/src/main/res/layout/app_bar_main.xml
new file mode 100644
index 0000000..d976be8
--- /dev/null
+++ b/wallet/src/main/res/layout/app_bar_main.xml
@@ -0,0 +1,75 @@
+<?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.coordinatorlayout.widget.CoordinatorLayout
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=".MainActivity">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:theme="@style/AppTheme.AppBarOverlay">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/relativeLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar"
+ style="@style/AppTheme.Toolbar"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <me.zhanghai.android.materialprogressbar.MaterialProgressBar
+ android:id="@+id/progress_bar"
+
style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal"
+ 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_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:mpb_progressStyle="horizontal"
+ app:mpb_useIntrinsicPadding="false"
+ tools:visibility="visible" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/nav_host_fragment"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:defaultNavHost="true"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/nav_graph" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/wallet/src/main/res/layout/fragment_already_paid.xml
b/wallet/src/main/res/layout/fragment_already_paid.xml
new file mode 100644
index 0000000..d36fe69
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_already_paid.xml
@@ -0,0 +1,52 @@
+<?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.PaymentSuccessfulFragment">
+
+ <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
new file mode 100644
index 0000000..3d977dd
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_error.xml
@@ -0,0 +1,97 @@
+<?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=".withdraw.ErrorFragment">
+
+ <ImageView
+ android:id="@+id/errorImageView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="16dp"
+ android:alpha="0.56"
+ android:src="@drawable/ic_error"
+ android:tint="@color/red"
+ app:layout_constraintBottom_toTopOf="@+id/errorTitle"
+ app:layout_constraintDimensionRatio="1: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"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:id="@+id/errorTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:gravity="center_horizontal|top"
+ android:minHeight="64dp"
+ android:textColor="@color/red"
+ app:autoSizeMaxTextSize="40sp"
+ app:autoSizeTextType="uniform"
+ app:layout_constraintBottom_toTopOf="@+id/errorMessage"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/errorImageView"
+ tools:text="@string/withdraw_error_title" />
+
+ <TextView
+ android:id="@+id/errorMessage"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ app:layout_constraintBottom_toTopOf="@+id/errorDevMessage"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/errorTitle"
+ tools:text="@string/withdraw_error_message" />
+
+ <TextView
+ android:id="@+id/errorDevMessage"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:fontFamily="monospace"
+ android:gravity="center"
+ android:textColor="@color/red"
+ android:textIsSelectable="true"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/backButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/errorMessage"
+ tools:text="Error: Fetching keys failed: unexpected status for
keys: 502"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/backButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/button_back"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/fragment_json.xml
b/wallet/src/main/res/layout/fragment_json.xml
new file mode 100644
index 0000000..1e0c047
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_json.xml
@@ -0,0 +1,41 @@
+<?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="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/jsonView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="8dp"
+ android:fontFamily="monospace"
+ android:textSize="12sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="[JSON]" />
+
+</ScrollView>
diff --git a/wallet/src/main/res/layout/fragment_payment_successful.xml
b/wallet/src/main/res/layout/fragment_payment_successful.xml
new file mode 100644
index 0000000..cf9e5e8
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_payment_successful.xml
@@ -0,0 +1,63 @@
+<?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:id="@+id/frameLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="16dp"
+ tools:context=".payment.PaymentSuccessfulFragment">
+
+ <ImageView
+ android:id="@+id/successImageView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="32dp"
+ android:src="@drawable/ic_check_circle"
+ app:layout_constraintBottom_toTopOf="@+id/successTextView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="ContentDescription" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/successTextView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/payment_successful"
+ android:textAlignment="center"
+ android:textColor="@color/green"
+ app:autoSizeMaxTextSize="48sp"
+ app:autoSizeMinTextSize="10sp"
+ app:autoSizeTextType="uniform"
+ app:layout_constraintBottom_toTopOf="@+id/backButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/successImageView" />
+
+ <Button
+ android:id="@+id/backButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/payment_back_button"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/fragment_pending_operations.xml
b/wallet/src/main/res/layout/fragment_pending_operations.xml
new file mode 100644
index 0000000..26c1be1
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_pending_operations.xml
@@ -0,0 +1,34 @@
+<?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/pending_row" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/fragment_product_image.xml
b/wallet/src/main/res/layout/fragment_product_image.xml
new file mode 100644
index 0000000..9f65d4d
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_product_image.xml
@@ -0,0 +1,24 @@
+<?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/>
+ -->
+
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/productImageView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:ignore="ContentDescription">
+
+</ImageView>
\ No newline at end of file
diff --git a/wallet/src/main/res/layout/fragment_prompt_payment.xml
b/wallet/src/main/res/layout/fragment_prompt_payment.xml
new file mode 100644
index 0000000..26cbeb6
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_prompt_payment.xml
@@ -0,0 +1,44 @@
+<?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=".payment.PromptPaymentFragment">
+
+ <include
+ android:id="@+id/scrollView"
+ layout="@layout/payment_details"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toTopOf="@+id/bottomView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <include
+ android:id="@+id/bottomView"
+ layout="@layout/payment_bottom_bar"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/scrollView" />
+
+</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
new file mode 100644
index 0000000..1114c17
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml
@@ -0,0 +1,171 @@
+<?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=".withdraw.PromptWithdrawFragment">
+
+ <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/withdraw_do_you_want"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/withdrawAmountView"
+ 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/withdrawAmountView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/feeView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:text="10.00 TESTKUDOS"
+ 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:text="@string/withdraw_fees"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/exchangeIntroView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/withdrawAmountView"
+ 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/withdraw_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="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:textSize="25sp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/withdrawCard"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/exchangeIntroView"
+ tools:text="(exchange base url)"
+ 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_toTopOf="@+id/withdrawCard"
+ 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"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="8dp">
+
+ <Button
+ android:id="@+id/button_cancel_withdraw"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:backgroundTint="@color/red"
+ android:text="@string/button_cancel"
+ app:layout_constraintBottom_toBottomOf="parent"
+
app:layout_constraintEnd_toStartOf="@+id/button_confirm_withdraw"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <Button
+ android:id="@+id/button_confirm_withdraw"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:backgroundTint="@color/green"
+ android:enabled="false"
+ android:text="@string/withdraw_button_confirm"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+
app:layout_constraintStart_toEndOf="@+id/button_cancel_withdraw" />
+
+ <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/button_confirm_withdraw"
+
app:layout_constraintEnd_toEndOf="@+id/button_confirm_withdraw"
+
app:layout_constraintStart_toStartOf="@+id/button_confirm_withdraw"
+
app:layout_constraintTop_toTopOf="@+id/button_confirm_withdraw"
+ 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_review_exchange_tos.xml
b/wallet/src/main/res/layout/fragment_review_exchange_tos.xml
new file mode 100644
index 0000000..61a61f1
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_review_exchange_tos.xml
@@ -0,0 +1,105 @@
+<?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=".withdraw.ReviewExchangeTosFragment">
+
+ <ScrollView
+ android:id="@+id/tosScrollView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toTopOf="@+id/buttonCard"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <TextView
+ android:id="@+id/tosTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:visibility="invisible"
+ tools:text="@tools:sample/lorem/random"
+ tools:visibility="visible" />
+
+ </ScrollView>
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="@+id/tosScrollView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/buttonCard"
+ style="@style/BottomCard"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="8dp">
+
+ <CheckBox
+ android:id="@+id/acceptTosCheckBox"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/exchange_tos_accept"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/acceptTosButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/abortTosButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:backgroundTint="@color/red"
+ android:text="@string/button_cancel"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/acceptTosButton"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <Button
+ android:id="@+id/acceptTosButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:backgroundTint="@color/green"
+ android:enabled="false"
+ android:text="@string/exchange_tos_button_continue"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/abortTosButton" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ </com.google.android.material.card.MaterialCardView>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/fragment_settings.xml
b/wallet/src/main/res/layout/fragment_settings.xml
new file mode 100644
index 0000000..2fa0fcc
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_settings.xml
@@ -0,0 +1,104 @@
+<?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:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="10dp"
+ android:orientation="vertical"
+ tools:context=".Settings">
+
+
+ <TextView
+ android:id="@+id/editText2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:text="@string/settings_version"
+ android:textSize="18sp" />
+
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView5"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/app_name" />
+
+ <TextView
+ android:id="@+id/textView4"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ tools:text="0.6.0pre8" />
+
+ </LinearLayout>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="15dp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:text="@string/settings_backups"
+ android:textSize="18sp"
+ android:visibility="gone" />
+
+ <Button
+ android:id="@+id/button_backup_export"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/settings_export_to_file"
+ android:visibility="gone" />
+
+ <Button
+ android:id="@+id/button_backup_import"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/settings_import_from_file"
+ android:visibility="gone" />
+
+
+ <TextView
+ android:id="@+id/devSettingsTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:text="@string/settings_developer"
+ android:textSize="18sp" />
+
+ <!--
+ <Button
+ android:text="Withdraw TESTKUDOS"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/button_withdraw_testkudos"/>-->
+
+ <Button
+ android:id="@+id/button_reset_wallet_dangerously"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/settings_reset" />
+
+</LinearLayout>
diff --git a/wallet/src/main/res/layout/fragment_show_balance.xml
b/wallet/src/main/res/layout/fragment_show_balance.xml
new file mode 100644
index 0000000..5bc6ee8
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_show_balance.xml
@@ -0,0 +1,91 @@
+<?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/balancesList"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/scanButton"
+ 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"
+ tools:layout_height="200dp"
+ tools:listitem="@layout/list_item_balance"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/balancesEmptyState"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:autoLink="web"
+ android:gravity="center"
+ android:padding="16dp"
+ android:text="@string/balances_empty_state"
+ android:textSize="18sp"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/scanButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:visibility="gone" />
+
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/barrier"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:barrierAllowsGoneWidgets="false"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="balancesList, balancesEmptyState" />
+
+ <Button
+ android:id="@+id/scanButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:drawableLeft="@drawable/ic_scan_qr"
+ android:padding="16dp"
+ android:text="@string/button_scan_qr_code"
+ app:layout_constraintBottom_toTopOf="@+id/testWithdrawButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/barrier"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:ignore="RtlHardcoded" />
+
+ <Button
+ android:id="@+id/testWithdrawButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:padding="16dp"
+ android:text="@string/withdraw_button_testkudos"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/scanButton"
+ tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/fragment_show_history.xml
b/wallet/src/main/res/layout/fragment_show_history.xml
new file mode 100644
index 0000000..3e84b0f
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_show_history.xml
@@ -0,0 +1,47 @@
+<?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/>
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/historyList"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+ <TextView
+ android:id="@+id/historyEmptyState"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/history_empty"
+ android:visibility="invisible"
+ tools:visibility="visible" />
+
+ <ProgressBar
+ android:id="@+id/historyProgressBar"
+ style="?android:progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="invisible"
+ tools:visibility="visible" />
+
+</FrameLayout>
diff --git a/wallet/src/main/res/layout/fragment_withdraw_successful.xml
b/wallet/src/main/res/layout/fragment_withdraw_successful.xml
new file mode 100644
index 0000000..2b7c308
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_withdraw_successful.xml
@@ -0,0 +1,63 @@
+<?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=".withdraw.WithdrawSuccessfulFragment">
+
+ <TextView
+ android:id="@+id/withdrawHeadlineView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="16dp"
+ android:gravity="center_horizontal|bottom"
+ android:text="@string/withdraw_accepted"
+ android:textColor="@color/green"
+ app:autoSizeMaxTextSize="40sp"
+ app:autoSizeTextType="uniform"
+ app:layout_constraintBottom_toTopOf="@+id/withdrawInfoView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/withdrawInfoView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="16dp"
+ android:text="@string/withdraw_success_info"
+ android:textAlignment="center"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ app:layout_constraintBottom_toTopOf="@+id/backButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/withdrawHeadlineView" />
+
+ <Button
+ android:id="@+id/backButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/button_continue"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/withdrawInfoView" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/history_payment.xml
b/wallet/src/main/res/layout/history_payment.xml
new file mode 100644
index 0000000..dd135e7
--- /dev/null
+++ b/wallet/src/main/res/layout/history_payment.xml
@@ -0,0 +1,87 @@
+<?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="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
+ android:background="?attr/selectableItemBackground">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/history_withdrawn"
+ app:tint="?android:colorControlNormal"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:id="@+id/title"
+ style="@style/HistoryTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ app:layout_constraintEnd_toStartOf="@+id/amountPaidWithFees"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Lots of books with very long titles" />
+
+ <TextView
+ android:id="@+id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/amountPaidWithFees"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="@string/history_event_payment_sent" />
+
+ <TextView
+ android:id="@+id/amountPaidWithFees"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/red"
+ android:textSize="16sp"
+ app:layout_constraintBottom_toTopOf="@+id/time"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="0.2 TESTKUDOS" />
+
+ <TextView
+ android:id="@+id/time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ tools:text="23 min ago" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/history_receive.xml
b/wallet/src/main/res/layout/history_receive.xml
new file mode 100644
index 0000000..1f76376
--- /dev/null
+++ b/wallet/src/main/res/layout/history_receive.xml
@@ -0,0 +1,105 @@
+<?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="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
+ android:background="?attr/selectableItemBackground">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/history_withdrawn"
+ app:tint="?android:colorControlNormal"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:id="@+id/title"
+ style="@style/HistoryTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:text="@string/history_event_withdrawn"
+ app:layout_constraintEnd_toStartOf="@+id/amountWithdrawn"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/summary"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="8dp"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/feeLabel"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ tools:text="http://taler.quite-long-exchange.url" />
+
+ <TextView
+ android:id="@+id/feeLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="2dp"
+ android:text="@string/history_fee_label"
+ app:layout_constraintEnd_toStartOf="@+id/fee"
+ app:layout_constraintTop_toTopOf="@+id/fee" />
+
+ <TextView
+ android:id="@+id/fee"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/red"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/amountWithdrawn"
+ tools:text="0.2 TESTKUDOS" />
+
+ <TextView
+ android:id="@+id/amountWithdrawn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/green"
+ android:textSize="16sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="10 TESTKUDOS" />
+
+ <TextView
+ android:id="@+id/time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:textSize="14sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/fee"
+ tools:text="23 min. ago" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/history_row.xml
b/wallet/src/main/res/layout/history_row.xml
new file mode 100644
index 0000000..8f0db1f
--- /dev/null
+++ b/wallet/src/main/res/layout/history_row.xml
@@ -0,0 +1,73 @@
+<?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="wrap_content"
+ android:layout_margin="15dp"
+ android:background="?attr/selectableItemBackground">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/ic_account_balance"
+ app:tint="?android:colorControlNormal"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:id="@+id/title"
+ style="@style/HistoryTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:text="My History Event" />
+
+ <TextView
+ android:id="@+id/info"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/time"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ tools:text="TextView" />
+
+ <TextView
+ android:id="@+id/time"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:gravity="end"
+ android:textSize="14sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ tools:text="3 days ago" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/wallet/src/main/res/layout/list_item_balance.xml
b/wallet/src/main/res/layout/list_item_balance.xml
new file mode 100644
index 0000000..f9c37b7
--- /dev/null
+++ b/wallet/src/main/res/layout/list_item_balance.xml
@@ -0,0 +1,77 @@
+<?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="wrap_content"
+ android:padding="16dp">
+
+ <TextView
+ android:id="@+id/balance_amount"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:textSize="40sp"
+ app:layout_constraintEnd_toStartOf="@+id/balance_currency"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="100.50" />
+
+ <TextView
+ android:id="@+id/balance_currency"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ app:layout_constraintBottom_toBottomOf="@+id/balance_amount"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/balance_amount"
+ app:layout_constraintTop_toTopOf="@+id/balance_amount"
+ tools:text="TESTKUDOS" />
+
+ <TextView
+ android:id="@+id/balanceInboundAmount"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/green"
+ android:textSize="20sp"
+ 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/balance_amount"
+ tools:text="+10 TESTKUDOS"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/balanceInboundLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:text="@string/balances_inbound_label"
+ android:textColor="@color/green"
+ app:layout_constraintBottom_toBottomOf="@+id/balanceInboundAmount"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/balanceInboundAmount"
+ app:layout_constraintTop_toTopOf="@+id/balanceInboundAmount"
+ tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/list_item_product.xml
b/wallet/src/main/res/layout/list_item_product.xml
new file mode 100644
index 0000000..fe6ba23
--- /dev/null
+++ b/wallet/src/main/res/layout/list_item_product.xml
@@ -0,0 +1,75 @@
+<?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="wrap_content"
+ android:padding="8dp">
+
+ <TextView
+ android:id="@+id/quantity"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ android:minWidth="24dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="31" />
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintDimensionRatio="H,4:3"
+ app:layout_constraintEnd_toStartOf="@+id/name"
+ app:layout_constraintStart_toEndOf="@+id/quantity"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_max="64dp"
+ tools:ignore="ContentDescription"
+ tools:srcCompat="@tools:sample/avatars"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/price"
+ app:layout_constraintStart_toEndOf="@+id/image"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="A product item that in some cases could have a very
long name" />
+
+ <TextView
+ android:id="@+id/price"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="23.42" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/list_item_product_single.xml
b/wallet/src/main/res/layout/list_item_product_single.xml
new file mode 100644
index 0000000..a08f1f8
--- /dev/null
+++ b/wallet/src/main/res/layout/list_item_product_single.xml
@@ -0,0 +1,78 @@
+<?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="wrap_content"
+ android:padding="8dp">
+
+ <TextView
+ android:id="@+id/quantity"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:layout_constraintEnd_toStartOf="@+id/name"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="31" />
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/name"
+ tools:ignore="ContentDescription"
+ tools:srcCompat="@tools:sample/avatars"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:visibility="gone"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toTopOf="@+id/image"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/price"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/quantity"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_goneMarginEnd="0dp"
+ tools:text="A product item that can have a very long name that
wraps over two lines" />
+
+ <TextView
+ android:id="@+id/price"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="23.42" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/nav_header_main.xml
b/wallet/src/main/res/layout/nav_header_main.xml
new file mode 100644
index 0000000..5574c1f
--- /dev/null
+++ b/wallet/src/main/res/layout/nav_header_main.xml
@@ -0,0 +1,73 @@
+<?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="wrap_content"
+ android:background="@drawable/side_nav_bar"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark">
+
+ <ImageView
+ android:id="@+id/talerLogoView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:contentDescription="@string/nav_header_desc"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@mipmap/ic_launcher_round" />
+
+ <TextView
+ android:id="@+id/gnuView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:text="@string/nav_header_title"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/talerLogoView" />
+
+ <TextView
+ android:id="@+id/walletView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/nav_header_subtitle"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/gnuView" />
+
+ <TextView
+ android:id="@+id/versionView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="16dp"
+ app:layout_constraintBottom_toBottomOf="@+id/walletView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toEndOf="@+id/walletView"
+ app:layout_constraintTop_toTopOf="@+id/walletView"
+ tools:text="0.6.9-pre15" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/payment_bottom_bar.xml
b/wallet/src/main/res/layout/payment_bottom_bar.xml
new file mode 100644
index 0000000..8fdf0f8
--- /dev/null
+++ b/wallet/src/main/res/layout/payment_bottom_bar.xml
@@ -0,0 +1,123 @@
+<?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/>
+ -->
+
+<com.google.android.material.card.MaterialCardView
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"
+ style="@style/BottomCard"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ tools:showIn="@layout/fragment_prompt_payment">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/totalLabelView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:text="@string/payment_label_amount_total"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/abortButton"
+ app:layout_constraintEnd_toStartOf="@+id/totalView"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/totalView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textStyle="bold"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/feeView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toEndOf="@+id/totalLabelView"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="10 TESTKUDOS"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/feeView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/confirmButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/totalView"
+ tools:text="@string/payment_fee"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/abortButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:backgroundTint="@color/red"
+ android:text="@string/payment_button_abort"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/confirmButton"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <Button
+ android:id="@+id/confirmButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:backgroundTint="@color/green"
+ android:enabled="false"
+ android:text="@string/payment_button_confirm"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/abortButton"
+ app:layout_constraintTop_toBottomOf="@+id/feeView"
+ tools:enabled="true" />
+
+ <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/confirmButton"
+ app:layout_constraintEnd_toEndOf="@+id/confirmButton"
+ app:layout_constraintStart_toStartOf="@+id/confirmButton"
+ app:layout_constraintTop_toTopOf="@+id/confirmButton"
+ tools:visibility="visible" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/wallet/src/main/res/layout/payment_details.xml
b/wallet/src/main/res/layout/payment_details.xml
new file mode 100644
index 0000000..60d1d73
--- /dev/null
+++ b/wallet/src/main/res/layout/payment_details.xml
@@ -0,0 +1,119 @@
+<?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="0dp"
+ android:layout_height="0dp"
+ android:fillViewport="true"
+ tools:showIn="@layout/fragment_prompt_payment">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/errorView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/activity_horizontal_margin"
+ android:textAlignment="center"
+ android:textColor="@android:color/holo_red_dark"
+ android:textSize="22sp"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/orderLabelView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:text="@string/payment_balance_insufficient"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/orderLabelView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/activity_horizontal_margin"
+ android:layout_marginTop="@dimen/activity_horizontal_margin"
+ android:layout_marginEnd="@dimen/activity_horizontal_margin"
+ android:text="@string/payment_label_order_summary"
+ android:textAlignment="center"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/orderView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/errorView"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/orderView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/activity_horizontal_margin"
+ android:layout_marginTop="16dp"
+ android:textAlignment="center"
+
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+ android:textSize="25sp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/detailsButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/orderLabelView"
+ tools:text="2 x Cappuccino, 1 x Hot Meals, 1 x Dessert"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/detailsButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/payment_show_details"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/productsList"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/orderView"
+ tools:visibility="visible" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/productsList"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/activity_horizontal_margin"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/detailsButton"
+ tools:listitem="@layout/list_item_product"
+ tools:visibility="visible" />
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:indeterminate="false"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:visibility="visible" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</ScrollView>
diff --git a/wallet/src/main/res/layout/pending_row.xml
b/wallet/src/main/res/layout/pending_row.xml
new file mode 100644
index 0000000..3505398
--- /dev/null
+++ b/wallet/src/main/res/layout/pending_row.xml
@@ -0,0 +1,48 @@
+<?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:padding="3dp"
+ android:background="@drawable/pending_border"
+ android:orientation="vertical">
+
+ <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/menu/activity_main_drawer.xml
b/wallet/src/main/res/menu/activity_main_drawer.xml
new file mode 100644
index 0000000..5eee6cc
--- /dev/null
+++ b/wallet/src/main/res/menu/activity_main_drawer.xml
@@ -0,0 +1,41 @@
+<?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/>
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:showIn="@layout/activity_main">
+
+ <group android:checkableBehavior="single">
+ <item
+ android:id="@+id/nav_home"
+ android:icon="@drawable/ic_account_balance_wallet"
+ android:title="@string/balances_title"
+ tools:checked="true" />
+ <item
+ android:id="@+id/nav_history"
+ android:icon="@drawable/ic_history_black_24dp"
+ android:title="@string/menu_history" />
+ <item
+ android:id="@+id/nav_settings"
+ android:icon="@drawable/ic_settings"
+ android:title="@string/menu_settings" />
+ <item
+ android:id="@+id/nav_pending_operations"
+ android:icon="@drawable/ic_sync"
+ android:title="@string/pending_operations_title" />
+ </group>
+
+</menu>
diff --git a/wallet/src/main/res/menu/balance.xml
b/wallet/src/main/res/menu/balance.xml
new file mode 100644
index 0000000..7ac3a9f
--- /dev/null
+++ b/wallet/src/main/res/menu/balance.xml
@@ -0,0 +1,28 @@
+<?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/>
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/reload_balance"
+ android:title="@string/menu_balance_reload"
+ app:showAsAction="never" />
+ <item
+ android:id="@+id/developer_mode"
+ android:checkable="true"
+ android:title="@string/menu_developer_mode"
+ app:showAsAction="never" />
+</menu>
diff --git a/wallet/src/main/res/menu/history.xml
b/wallet/src/main/res/menu/history.xml
new file mode 100644
index 0000000..755323b
--- /dev/null
+++ b/wallet/src/main/res/menu/history.xml
@@ -0,0 +1,31 @@
+<?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/>
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/show_all_history"
+ android:checkable="true"
+ android:checked="false"
+ android:title="@string/history_show_all"
+ app:showAsAction="never" />
+ <item
+ android:id="@+id/reload_history"
+ android:orderInCategory="100"
+ android:title="@string/history_reload"
+ app:showAsAction="never" />
+</menu>
diff --git a/wallet/src/main/res/menu/pending_operations.xml
b/wallet/src/main/res/menu/pending_operations.xml
new file mode 100644
index 0000000..980ea66
--- /dev/null
+++ b/wallet/src/main/res/menu/pending_operations.xml
@@ -0,0 +1,24 @@
+<?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/>
+ -->
+
+<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"
+ app:showAsAction="never" />
+</menu>
diff --git a/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..7acad4e
--- /dev/null
+++ b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+<?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/>
+ -->
+
+<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"/>
+</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
new file mode 100644
index 0000000..7acad4e
--- /dev/null
+++ b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,21 @@
+<?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/>
+ -->
+
+<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"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/wallet/src/main/res/mipmap-hdpi/ic_launcher.png
b/wallet/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..2bfb696
Binary files /dev/null and b/wallet/src/main/res/mipmap-hdpi/ic_launcher.png
differ
diff --git a/wallet/src/main/res/mipmap-hdpi/ic_launcher_round.png
b/wallet/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..c7ae940
Binary files /dev/null and
b/wallet/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/wallet/src/main/res/mipmap-mdpi/ic_launcher.png
b/wallet/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c104056
Binary files /dev/null and b/wallet/src/main/res/mipmap-mdpi/ic_launcher.png
differ
diff --git a/wallet/src/main/res/mipmap-mdpi/ic_launcher_round.png
b/wallet/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1130914
Binary files /dev/null and
b/wallet/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/wallet/src/main/res/mipmap-xhdpi/ic_launcher.png
b/wallet/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..7144a11
Binary files /dev/null and b/wallet/src/main/res/mipmap-xhdpi/ic_launcher.png
differ
diff --git a/wallet/src/main/res/mipmap-xhdpi/ic_launcher_round.png
b/wallet/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..d63ccd3
Binary files /dev/null and
b/wallet/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/wallet/src/main/res/mipmap-xxhdpi/ic_launcher.png
b/wallet/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..bc3155e
Binary files /dev/null and b/wallet/src/main/res/mipmap-xxhdpi/ic_launcher.png
differ
diff --git a/wallet/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
b/wallet/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..028fe60
Binary files /dev/null and
b/wallet/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/wallet/src/main/res/mipmap-xxxhdpi/ic_launcher.png
b/wallet/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ccc81eb
Binary files /dev/null and b/wallet/src/main/res/mipmap-xxxhdpi/ic_launcher.png
differ
diff --git a/wallet/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
b/wallet/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da3ce45
Binary files /dev/null and
b/wallet/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/wallet/src/main/res/navigation/nav_graph.xml
b/wallet/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..549ca01
--- /dev/null
+++ b/wallet/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,125 @@
+<?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/>
+ -->
+
+<navigation 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:id="@+id/nav_graph"
+ app:startDestination="@id/showBalance"
+ tools:ignore="UnusedNavigation">
+
+ <fragment
+ android:id="@+id/showBalance"
+ android:name="net.taler.wallet.BalanceFragment"
+ android:label="@string/balances_title"
+ tools:layout="@layout/fragment_show_balance">
+ <action
+ android:id="@+id/action_showBalance_to_promptPayment"
+ app:destination="@id/promptPayment" />
+ <action
+ android:id="@+id/action_showBalance_to_promptWithdraw"
+ app:destination="@id/promptWithdraw" />
+ </fragment>
+ <fragment
+ android:id="@+id/promptPayment"
+ android:name="net.taler.wallet.payment.PromptPaymentFragment"
+ android:label="Review Payment"
+ tools:layout="@layout/fragment_prompt_payment">
+ <action
+ android:id="@+id/action_promptPayment_to_paymentSuccessful"
+ app:destination="@id/paymentSuccessful"
+ app:popUpTo="@id/showBalance" />
+ <action
+ android:id="@+id/action_promptPayment_to_alreadyPaid"
+ app:destination="@id/alreadyPaid"
+ app:popUpTo="@id/showBalance" />
+ </fragment>
+ <fragment
+ android:id="@+id/paymentSuccessful"
+ android:name="net.taler.wallet.payment.PaymentSuccessfulFragment"
+ android:label="Payment Successful"
+ tools:layout="@layout/fragment_payment_successful" />
+ <fragment
+ android:id="@+id/settings"
+ android:name="net.taler.wallet.Settings"
+ android:label="Settings"
+ tools:layout="@layout/fragment_settings" />
+ <fragment
+ android:id="@+id/walletHistory"
+ android:name="net.taler.wallet.history.WalletHistoryFragment"
+ android:label="@string/history_title"
+ tools:layout="@layout/fragment_show_history" />
+ <fragment
+ android:id="@+id/alreadyPaid"
+ android:name="net.taler.wallet.payment.AlreadyPaidFragment"
+ android:label="Already Paid"
+ tools:layout="@layout/fragment_already_paid" />
+
+ <fragment
+ android:id="@+id/promptWithdraw"
+ android:name="net.taler.wallet.withdraw.PromptWithdrawFragment"
+ android:label="@string/nav_prompt_withdraw"
+ tools:layout="@layout/fragment_prompt_withdraw">
+ <action
+ android:id="@+id/action_promptWithdraw_to_withdrawSuccessful"
+ app:destination="@id/withdrawSuccessful"
+ app:popUpTo="@id/showBalance" />
+ <action
+ android:id="@+id/action_promptWithdraw_to_reviewExchangeTOS"
+ app:destination="@id/reviewExchangeTOS"
+ app:popUpTo="@id/showBalance" />
+ <action
+ android:id="@+id/action_promptWithdraw_to_errorFragment"
+ app:destination="@id/errorFragment"
+ app:popUpTo="@id/showBalance" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/withdrawSuccessful"
+ android:name="net.taler.wallet.withdraw.WithdrawSuccessfulFragment"
+ android:label="Withdrawal Confirmed"
+ tools:layout="@layout/fragment_withdraw_successful" />
+ <fragment
+ android:id="@+id/reviewExchangeTOS"
+ android:name="net.taler.wallet.withdraw.ReviewExchangeTosFragment"
+ android:label="@string/nav_exchange_tos"
+ tools:layout="@layout/fragment_review_exchange_tos">
+ <action
+ android:id="@+id/action_reviewExchangeTOS_to_promptWithdraw"
+ app:destination="@id/promptWithdraw"
+ app:popUpTo="@id/showBalance" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/nav_pending_operations"
+ android:name="net.taler.wallet.pending.PendingOperationsFragment"
+ android:label="Pending Operations"
+ tools:layout="@layout/fragment_pending_operations" />
+ <fragment
+ android:id="@+id/errorFragment"
+ android:name="net.taler.wallet.withdraw.ErrorFragment"
+ android:label="@string/nav_error"
+ tools:layout="@layout/fragment_error" />
+
+ <action
+ android:id="@+id/action_global_promptPayment"
+ app:destination="@id/promptPayment" />
+
+ <action
+ android:id="@+id/action_global_pending_operations"
+ app:destination="@id/nav_pending_operations" />
+
+</navigation>
\ No newline at end of file
diff --git a/wallet/src/main/res/values/colors.xml
b/wallet/src/main/res/values/colors.xml
new file mode 100644
index 0000000..2d1f0d7
--- /dev/null
+++ b/wallet/src/main/res/values/colors.xml
@@ -0,0 +1,25 @@
+<?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>
+ <color name="colorPrimary">#283593</color>
+ <color name="colorPrimaryDark">#1A237E</color>
+ <color name="colorAccent">#AE1010</color>
+
+ <color name="red">#C62828</color>
+ <color name="green">#558B2F</color>
+</resources>
diff --git a/wallet/src/main/res/values/dimens.xml
b/wallet/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..2bbc14d
--- /dev/null
+++ b/wallet/src/main/res/values/dimens.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ 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>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+ <dimen name="nav_header_vertical_spacing">8dp</dimen>
+ <dimen name="nav_header_height">176dp</dimen>
+ <dimen name="fab_margin">16dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/wallet/src/main/res/values/ic_launcher_background.xml
b/wallet/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..758b965
--- /dev/null
+++ b/wallet/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,20 @@
+<?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>
+ <color name="ic_launcher_background">#000000</color>
+</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
new file mode 100644
index 0000000..8981e04
--- /dev/null
+++ b/wallet/src/main/res/values/strings.xml
@@ -0,0 +1,105 @@
+<!--
+ ~ 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>
+ <string name="app_name">Taler Wallet</string>
+
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="nav_header_subtitle">Wallet</string>
+ <string name="nav_header_desc">Navigation header</string>
+
+ <string name="nav_prompt_withdraw">Withdraw Digital Cash</string>
+ <string name="nav_exchange_tos">Exchange\'s Terms of Service</string>
+ <string name="nav_error">Error</string>
+
+ <string name="button_back">Go Back</string>
+ <string name="button_cancel">Cancel</string>
+ <string name="button_continue">Continue</string>
+ <string name="button_scan_qr_code">Scan Taler QR Code</string>
+
+ <string name="menu_history">History</string>
+ <string name="menu_settings">Settings</string>
+ <string name="menu_balance_reload">Reload balances</string>
+ <string name="menu_developer_mode">Developer Mode</string>
+ <string name="menu_retry_pending_operations">Retry Pending
Operations</string>
+
+ <string name="servicedesc">my service</string>
+ <string name="aiddescription">my aid</string>
+
+ <string name="balances_title">Balances</string>
+ <string name="balances_inbound_amount">+%1s %2s</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="history_title">History</string>
+ <string name="history_fee_label">Fee:</string>
+ <string name="history_show_all">Show All</string>
+ <string name="history_reload">Reload History</string>
+ <string name="history_empty">The wallet history is empty</string>
+
+ <!-- HistoryEvents -->
+ <string name="history_event_exchange_added">Exchange Added</string>
+ <string name="history_event_exchange_updated">Exchange Updated</string>
+ <string name="history_event_reserve_balance_updated">Reserve Balance
Updated</string>
+ <string name="history_event_payment_sent">Payment</string>
+ <string name="history_event_payment_aborted">Payment Aborted</string>
+ <string name="history_event_withdrawn">Withdraw</string>
+ <string name="history_event_order_accepted">Purchase Confirmed</string>
+ <string name="history_event_order_refused">Purchase Cancelled</string>
+ <string name="history_event_tip_accepted">Tip Accepted</string>
+ <string name="history_event_tip_declined">Tip Declined</string>
+ <string name="history_event_order_redirected">Purchase Redirected</string>
+ <string name="history_event_refund">Refund</string>
+ <string name="history_event_refreshed">Obtained change</string>
+ <string name="history_event_unknown">Unknown Event</string>
+
+ <string name="payment_fee">+%s payment fee</string>
+ <string name="payment_button_confirm">Confirm Payment</string>
+ <string name="payment_button_abort">Abort</string>
+ <string name="payment_label_amount_total">Total Amount:</string>
+ <string name="payment_label_order_summary">Order</string>
+ <string name="payment_error">Error: %s</string>
+ <string name="payment_balance_insufficient">Balance insufficient!</string>
+ <string name="payment_show_details">Show Details</string>
+ <string name="payment_hide_details">Hide Details</string>
+ <string name="payment_successful">Payment was successful</string>
+ <string name="payment_back_button">OK</string>
+ <string name="payment_already_paid">You\'ve already paid for this
order.</string>
+
+ <string name="withdraw_accepted">Withdrawal accepted</string>
+ <string name="withdraw_success_info">The wire transfer now needs to be
confirmed with the bank. Once the wire transfer is complete, the digital cash
will automatically show in this wallet.</string>
+ <string name="withdraw_do_you_want">Do you want to withdraw</string>
+ <string name="withdraw_fees">(minus exchange fees not shown in this
prototype)</string>
+ <string name="withdraw_exchange">Using the exchange provider</string>
+ <string name="withdraw_button_testkudos">Withdraw TESTKUDOS</string>
+ <string name="withdraw_button_confirm">Confirm Withdraw</string>
+ <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="pending_operations_title">Pending Operations</string>
+ <string name="pending_operations_refuse">Refuse Proposal</string>
+ <string name="pending_operations_no_action">(no action)</string>
+
+ <string name="settings_version">Version Information</string>
+ <string name="exchange_tos_accept">Accept Terms of Service</string>
+ <string name="exchange_tos_button_continue">Continue</string>
+ <string name="settings_backups">Backups</string>
+ <string name="settings_export_to_file">Export wallet to file</string>
+ <string name="settings_import_from_file">Import from file</string>
+ <string name="settings_developer">Developer Settings (use with
caution!)</string>
+ <string name="settings_reset">Reset Wallet (dangerous!)</string>
+
+</resources>
diff --git a/wallet/src/main/res/values/styles.xml
b/wallet/src/main/res/values/styles.xml
new file mode 100644
index 0000000..83f3e3a
--- /dev/null
+++ b/wallet/src/main/res/values/styles.xml
@@ -0,0 +1,46 @@
+<!--
+ ~ 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>
+
+ <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>
+
+ <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.Toolbar"
parent="Widget.MaterialComponents.Toolbar.Primary" />
+
+ <style name="HistoryTitle">
+ <item name="android:textSize">17sp</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="BottomCard">
+ <item name="cardCornerRadius">0dp</item>
+ <item name="cardElevation">8dp</item>
+ </style>
+
+</resources>
diff --git a/wallet/src/main/res/xml/apduservice.xml
b/wallet/src/main/res/xml/apduservice.xml
new file mode 100644
index 0000000..fde348c
--- /dev/null
+++ b/wallet/src/main/res/xml/apduservice.xml
@@ -0,0 +1,25 @@
+<?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/>
+ -->
+
+<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
+ android:description="@string/servicedesc"
+ android:requireDeviceUnlock="true">
+ <aid-group android:description="@string/aiddescription"
+ android:category="other">
+ <aid-filter android:name="F00054414C4552"/>
+ </aid-group>
+</host-apdu-service>
\ No newline at end of file
diff --git a/wallet/src/main/res/xml/backup_descriptor.xml
b/wallet/src/main/res/xml/backup_descriptor.xml
new file mode 100644
index 0000000..731d404
--- /dev/null
+++ b/wallet/src/main/res/xml/backup_descriptor.xml
@@ -0,0 +1,20 @@
+<?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/>
+ -->
+
+<full-backup-content>
+ <!-- Exclude specific shared preferences that contain GCM registration Id
-->
+</full-backup-content>
diff --git a/wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt
b/wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt
new file mode 100644
index 0000000..de74f68
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt
@@ -0,0 +1,33 @@
+/*
+ * 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
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine
(host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git
a/wallet/src/test/java/net/taler/wallet/crypto/Base32CrockfordTest.kt
b/wallet/src/test/java/net/taler/wallet/crypto/Base32CrockfordTest.kt
new file mode 100644
index 0000000..7c8cb4c
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/crypto/Base32CrockfordTest.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.crypto
+
+import org.junit.Test
+
+class Base32CrockfordTest {
+ @Test
+ fun testBasic() {
+ val inputStr = "Hello, World"
+ val data = inputStr.toByteArray(Charsets.UTF_8)
+ val enc = Base32Crockford.encode(data)
+ println(enc)
+ val dec = Base32Crockford.decode(enc)
+ val recoveredInputStr = dec.toString(Charsets.UTF_8)
+ println(recoveredInputStr)
+
+ val foo =
Base32Crockford.decode("51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30H2365338E9G6RT4AH1N6H13EGHR70RK6H1S6X2M4CSP8CSK8E1G88VKJH25610KGCHR8RWM4DJ47123CH9K89334D1S8N24ACJ48CR3EH256MR3AH1R711KCE9N6S134GSN6RW46D1H6CV3CDHJ6D0KEDHR6D24CD248MWKADHJ6WT34D25712KCD2474V46EA18H2M4GHM6WTK2E216S14CD238GSK0G9G692KCDHM6RW34CT16MV3CG9P60S34C1G70SMCHHQ8CVKJG9K6CVK6GHK70R46HJ26CR4AE9M8523ADHS8RR3EE1R74S32CHP6N1K0GT38D1M6C1R84TM2E9N8MSK2C1J71248E9H6H1MCD9J70VK4GSG6124CCHR6RS4ADSH8N0M4H1G84
[...]
+ println(foo.toString(Charsets.UTF_8))
+ }
+}
diff --git a/wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt
b/wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt
new file mode 100644
index 0000000..ba18dfb
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt
@@ -0,0 +1,459 @@
+/*
+ * 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.history
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import com.fasterxml.jackson.module.kotlin.readValue
+import net.taler.wallet.history.RefreshReason.PAY
+import net.taler.wallet.history.ReserveType.MANUAL
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import kotlin.random.Random
+
+class HistoryEventTest {
+
+ private val mapper = ObjectMapper().registerModule(KotlinModule())
+
+ private val timestamp = Random.nextLong()
+ private val exchangeBaseUrl = "https://exchange.test.taler.net/"
+ private val orderShortInfo = OrderShortInfo(
+ proposalId = "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ orderId = "2019.364-01RAQ68DQ7AWR",
+ merchantBaseUrl =
"https://backend.demo.taler.net/public/instances/FSF/",
+ amount = "KUDOS:0.5",
+ summary = "Essay: Foreword"
+ )
+
+ @Test
+ fun `test ExchangeAddedEvent`() {
+ val builtIn = Random.nextBoolean()
+ val json = """{
+ "type": "exchange-added",
+ "builtIn": $builtIn,
+ "eventId":
"exchange-added;https%3A%2F%2Fexchange.test.taler.net%2F",
+ "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+ "timestamp": {
+ "t_ms": $timestamp
+ }
+ }""".trimIndent()
+ val event: ExchangeAddedEvent = mapper.readValue(json)
+
+ assertEquals(builtIn, event.builtIn)
+ assertEquals(exchangeBaseUrl, event.exchangeBaseUrl)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test ExchangeUpdatedEvent`() {
+ val json = """{
+ "type": "exchange-updated",
+ "eventId":
"exchange-updated;https%3A%2F%2Fexchange.test.taler.net%2F",
+ "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+ "timestamp": {
+ "t_ms": $timestamp
+ }
+ }""".trimIndent()
+ val event: ExchangeUpdatedEvent = mapper.readValue(json)
+
+ assertEquals(exchangeBaseUrl, event.exchangeBaseUrl)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test ReserveShortInfo`() {
+ val json = """{
+ "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+ "reserveCreationDetail": {
+ "type": "manual"
+ },
+ "reservePub":
"BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G"
+ }""".trimIndent()
+ val info: ReserveShortInfo = mapper.readValue(json)
+
+ assertEquals(exchangeBaseUrl, info.exchangeBaseUrl)
+ assertEquals(MANUAL, info.reserveCreationDetail.type)
+ assertEquals("BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G",
info.reservePub)
+ }
+
+ @Test
+ fun `test ReserveBalanceUpdatedEvent`() {
+ val json = """{
+ "type": "reserve-balance-updated",
+ "eventId":
"reserve-balance-updated;K0H10Q6HB9WH0CKHQQMNH5C6GA7A9AR1E2XSS9G1KG3ZXMBVT26G",
+ "amountExpected": "TESTKUDOS:23",
+ "amountReserveBalance": "TESTKUDOS:10",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "newHistoryTransactions": [
+ {
+ "amount": "TESTKUDOS:10",
+ "sender_account_url":
"payto:\/\/x-taler-bank\/bank.test.taler.net\/894",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "wire_reference": "00000000004TR",
+ "type": "DEPOSIT"
+ }
+ ],
+ "reserveShortInfo": {
+ "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+ "reserveCreationDetail": {
+ "type": "manual"
+ },
+ "reservePub":
"BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G"
+ }
+ }""".trimIndent()
+ val event: ReserveBalanceUpdatedEvent = mapper.readValue(json)
+
+ assertEquals(timestamp, event.timestamp.ms)
+ assertEquals("TESTKUDOS:23", event.amountExpected)
+ assertEquals("TESTKUDOS:10", event.amountReserveBalance)
+ assertEquals(1, event.newHistoryTransactions.size)
+ assertTrue(event.newHistoryTransactions[0] is
ReserveDepositTransaction)
+ assertEquals(exchangeBaseUrl, event.reserveShortInfo.exchangeBaseUrl)
+ }
+
+ @Test
+ fun `test HistoryWithdrawnEvent`() {
+ val json = """{
+ "type": "withdrawn",
+ "withdrawSessionId":
"974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0",
+ "eventId":
"withdrawn;974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0",
+ "amountWithdrawnEffective": "TESTKUDOS:9.8",
+ "amountWithdrawnRaw": "TESTKUDOS:10",
+ "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "withdrawalSource": {
+ "type": "reserve",
+ "reservePub":
"BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G"
+ }
+ }""".trimIndent()
+ val event: HistoryWithdrawnEvent = mapper.readValue(json)
+
+ assertEquals(
+ "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0",
+ event.withdrawSessionId
+ )
+ assertEquals("TESTKUDOS:9.8", event.amountWithdrawnEffective)
+ assertEquals("TESTKUDOS:10", event.amountWithdrawnRaw)
+ assertTrue(event.withdrawalSource is WithdrawalSourceReserve)
+ assertEquals(
+ "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G",
+ (event.withdrawalSource as WithdrawalSourceReserve).reservePub
+ )
+ assertEquals(exchangeBaseUrl, event.exchangeBaseUrl)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test OrderShortInfo`() {
+ val json = """{
+ "amount": "KUDOS:0.5",
+ "orderId": "2019.364-01RAQ68DQ7AWR",
+ "merchantBaseUrl":
"https:\/\/backend.demo.taler.net\/public\/instances\/FSF\/",
+ "proposalId":
"EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ "summary": "Essay: Foreword"
+ }""".trimIndent()
+ val info: OrderShortInfo = mapper.readValue(json)
+
+ assertEquals("KUDOS:0.5", info.amount)
+ assertEquals("2019.364-01RAQ68DQ7AWR", info.orderId)
+ assertEquals("Essay: Foreword", info.summary)
+ }
+
+ @Test
+ fun `test HistoryOrderAcceptedEvent`() {
+ val json = """{
+ "type": "order-accepted",
+ "eventId":
"order-accepted;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "timestamp": {
+ "t_ms": $timestamp
+ }
+ }""".trimIndent()
+ val event: HistoryOrderAcceptedEvent = mapper.readValue(json)
+
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryOrderRefusedEvent`() {
+ val json = """{
+ "type": "order-refused",
+ "eventId":
"order-refused;9RJGAYXKWX0Y3V37H66606SXSA7V2CV255EBFS4G1JSH6W1EG7F0",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "timestamp": {
+ "t_ms": $timestamp
+ }
+ }""".trimIndent()
+ val event: HistoryOrderRefusedEvent = mapper.readValue(json)
+
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryPaymentSentEvent`() {
+ val json = """{
+ "type": "payment-sent",
+ "eventId":
"payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "replay": false,
+ "sessionId": "e4f436c4-3c5c-4aee-81d2-26e425c09520",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "numCoins": 6,
+ "amountPaidWithFees": "KUDOS:0.6"
+ }""".trimIndent()
+ val event: HistoryPaymentSentEvent = mapper.readValue(json)
+
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals(false, event.replay)
+ assertEquals(6, event.numCoins)
+ assertEquals("KUDOS:0.6", event.amountPaidWithFees)
+ assertEquals("e4f436c4-3c5c-4aee-81d2-26e425c09520", event.sessionId)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryPaymentSentEvent without sessionId`() {
+ val json = """{
+ "type": "payment-sent",
+ "eventId":
"payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "replay": true,
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "numCoins": 6,
+ "amountPaidWithFees": "KUDOS:0.6"
+ }""".trimIndent()
+ val event: HistoryPaymentSentEvent = mapper.readValue(json)
+
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals(true, event.replay)
+ assertEquals(6, event.numCoins)
+ assertEquals("KUDOS:0.6", event.amountPaidWithFees)
+ assertEquals(null, event.sessionId)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryPaymentAbortedEvent`() {
+ val json = """{
+ "type": "payment-aborted",
+ "eventId":
"payment-sent;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "amountLost": "KUDOS:0.1"
+ }""".trimIndent()
+ val event: HistoryPaymentAbortedEvent = mapper.readValue(json)
+
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals("KUDOS:0.1", event.amountLost)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryTipAcceptedEvent`() {
+ val json = """{
+ "type": "tip-accepted",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "eventId":
"tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+ "tipId":
"tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+ "tipRaw": "KUDOS:4"
+ }""".trimIndent()
+ val event: HistoryTipAcceptedEvent = mapper.readValue(json)
+
+
assertEquals("tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
event.tipId)
+ assertEquals("KUDOS:4", event.tipRaw)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryTipDeclinedEvent`() {
+ val json = """{
+ "type": "tip-declined",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "eventId":
"tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+ "tipId":
"tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+ "tipAmount": "KUDOS:4"
+ }""".trimIndent()
+ val event: HistoryTipDeclinedEvent = mapper.readValue(json)
+
+
assertEquals("tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
event.tipId)
+ assertEquals("KUDOS:4", event.tipAmount)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryRefundedEvent`() {
+ val json = """{
+ "type": "refund",
+ "eventId":
"refund;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+ "refundGroupId": "refund;998724",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "amountRefundedRaw": "KUDOS:1.0",
+ "amountRefundedInvalid": "KUDOS:0.5",
+ "amountRefundedEffective": "KUDOS:0.4"
+ }""".trimIndent()
+ val event: HistoryRefundedEvent = mapper.readValue(json)
+
+ assertEquals("refund;998724", event.refundGroupId)
+ assertEquals("KUDOS:1.0", event.amountRefundedRaw)
+ assertEquals("KUDOS:0.5", event.amountRefundedInvalid)
+ assertEquals("KUDOS:0.4", event.amountRefundedEffective)
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryRefreshedEvent`() {
+ val json = """{
+ "type": "refreshed",
+ "refreshGroupId":
"8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640",
+ "eventId":
"refreshed;8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "refreshReason": "pay",
+ "amountRefreshedEffective": "KUDOS:0",
+ "amountRefreshedRaw": "KUDOS:1",
+ "numInputCoins": 6,
+ "numOutputCoins": 0,
+ "numRefreshedInputCoins": 1
+ }""".trimIndent()
+ val event: HistoryRefreshedEvent = mapper.readValue(json)
+
+ assertEquals("KUDOS:0", event.amountRefreshedEffective)
+ assertEquals("KUDOS:1", event.amountRefreshedRaw)
+ assertEquals(6, event.numInputCoins)
+ assertEquals(0, event.numOutputCoins)
+ assertEquals(1, event.numRefreshedInputCoins)
+ assertEquals("8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640",
event.refreshGroupId)
+ assertEquals(PAY, event.refreshReason)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryOrderRedirectedEvent`() {
+ val json = """{
+ "type": "order-redirected",
+ "eventId":
"order-redirected;621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G",
+ "alreadyPaidOrderShortInfo": {
+ "amount": "KUDOS:0.5",
+ "orderId": "2019.354-01P25CD66P8NG",
+ "merchantBaseUrl":
"https://backend.demo.taler.net/public/instances/FSF/",
+ "proposalId":
"898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+ "summary": "Essay: 1. The Free Software Definition"
+ },
+ "newOrderShortInfo": {
+ "amount": "KUDOS:0.5",
+ "orderId": "2019.364-01M4QH6KPMJY4",
+ "merchantBaseUrl":
"https://backend.demo.taler.net/public/instances/FSF/",
+ "proposalId":
"621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G",
+ "summary": "Essay: 1. The Free Software Definition"
+ },
+ "timestamp": {
+ "t_ms": $timestamp
+ }
+ }""".trimIndent()
+ val event: HistoryOrderRedirectedEvent = mapper.readValue(json)
+
+ assertEquals("898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
event.alreadyPaidOrderShortInfo.proposalId)
+ assertEquals("https://backend.demo.taler.net/public/instances/FSF/",
event.alreadyPaidOrderShortInfo.merchantBaseUrl)
+ assertEquals("2019.354-01P25CD66P8NG",
event.alreadyPaidOrderShortInfo.orderId)
+ assertEquals("KUDOS:0.5", event.alreadyPaidOrderShortInfo.amount)
+ assertEquals("Essay: 1. The Free Software Definition",
event.alreadyPaidOrderShortInfo.summary)
+
+ assertEquals("621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G",
event.newOrderShortInfo.proposalId)
+ assertEquals("https://backend.demo.taler.net/public/instances/FSF/",
event.newOrderShortInfo.merchantBaseUrl)
+ assertEquals("2019.364-01M4QH6KPMJY4", event.newOrderShortInfo.orderId)
+ assertEquals("KUDOS:0.5", event.newOrderShortInfo.amount)
+ assertEquals("Essay: 1. The Free Software Definition",
event.newOrderShortInfo.summary)
+
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryUnknownEvent`() {
+ val json = """{
+ "type": "does not exist",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "eventId":
"does-not-exist;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0"
+ }""".trimIndent()
+ val event: HistoryEvent = mapper.readValue(json)
+
+ assertEquals(HistoryUnknownEvent::class.java, event.javaClass)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+}
diff --git
a/wallet/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt
b/wallet/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt
new file mode 100644
index 0000000..d3d66f5
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.history
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import com.fasterxml.jackson.module.kotlin.readValue
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import kotlin.random.Random
+
+class ReserveTransactionTest {
+
+ private val mapper = ObjectMapper().registerModule(KotlinModule())
+
+ private val timestamp = Random.nextLong()
+
+ @Test
+ fun `test ExchangeAddedEvent`() {
+ val senderAccountUrl = "payto://x-taler-bank/bank.test.taler.net/894"
+ val json = """{
+ "amount": "TESTKUDOS:10",
+ "sender_account_url":
"payto:\/\/x-taler-bank\/bank.test.taler.net\/894",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "wire_reference": "00000000004TR",
+ "type": "DEPOSIT"
+ }""".trimIndent()
+ val transaction: ReserveDepositTransaction = mapper.readValue(json)
+
+ assertEquals("TESTKUDOS:10", transaction.amount)
+ assertEquals(senderAccountUrl, transaction.senderAccountUrl)
+ assertEquals("00000000004TR", transaction.wireReference)
+ assertEquals(timestamp, transaction.timestamp.ms)
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
address@hidden.