aboutsummaryrefslogtreecommitdiff
path: root/wallet
diff options
context:
space:
mode:
Diffstat (limited to 'wallet')
-rw-r--r--wallet/.gitignore2
-rw-r--r--wallet/.gitlab-ci.yml42
-rw-r--r--wallet/README.md40
-rw-r--r--wallet/build.gradle81
-rw-r--r--wallet/proguard-rules.pro21
-rw-r--r--wallet/src/androidTest/java/net/taler/wallet/ExampleInstrumentedTest.kt38
-rw-r--r--wallet/src/main/AndroidManifest.xml81
-rw-r--r--wallet/src/main/ic_launcher-web.pngbin0 -> 14129 bytes
-rw-r--r--wallet/src/main/java/net/taler/wallet/Amount.kt141
-rw-r--r--wallet/src/main/java/net/taler/wallet/BalanceFragment.kt198
-rw-r--r--wallet/src/main/java/net/taler/wallet/HostCardEmulatorService.kt187
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainActivity.kt209
-rw-r--r--wallet/src/main/java/net/taler/wallet/Settings.kt140
-rw-r--r--wallet/src/main/java/net/taler/wallet/Utils.kt40
-rw-r--r--wallet/src/main/java/net/taler/wallet/WalletViewModel.kt124
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt141
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt239
-rw-r--r--wallet/src/main/java/net/taler/wallet/crypto/Encoding.kt134
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt452
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt71
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt50
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt58
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt243
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/WalletHistoryFragment.kt115
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt47
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/ContractTerms.kt56
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt160
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt49
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt92
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/ProductImageFragment.kt52
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt168
-rw-r--r--wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt180
-rw-r--r--wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt64
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/ErrorFragment.kt64
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt109
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt80
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt209
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/WithdrawSuccessfulFragment.kt44
-rw-r--r--wallet/src/main/res/drawable/history_payment_aborted.xml25
-rw-r--r--wallet/src/main/res/drawable/history_refresh.xml28
-rw-r--r--wallet/src/main/res/drawable/history_refund.xml25
-rw-r--r--wallet/src/main/res/drawable/history_tip_accepted.xml25
-rw-r--r--wallet/src/main/res/drawable/history_tip_declined.xml25
-rw-r--r--wallet/src/main/res/drawable/history_withdrawn.xml25
-rw-r--r--wallet/src/main/res/drawable/ic_account_balance.xml25
-rw-r--r--wallet/src/main/res/drawable/ic_account_balance_wallet.xml9
-rw-r--r--wallet/src/main/res/drawable/ic_add_circle.xml25
-rw-r--r--wallet/src/main/res/drawable/ic_cancel.xml25
-rw-r--r--wallet/src/main/res/drawable/ic_cash_usd_outline.xml25
-rw-r--r--wallet/src/main/res/drawable/ic_check_circle.xml26
-rw-r--r--wallet/src/main/res/drawable/ic_directions.xml25
-rw-r--r--wallet/src/main/res/drawable/ic_error.xml25
-rw-r--r--wallet/src/main/res/drawable/ic_history_black_24dp.xml25
-rw-r--r--wallet/src/main/res/drawable/ic_home_black_24dp.xml25
-rw-r--r--wallet/src/main/res/drawable/ic_launcher_foreground.xml68
-rw-r--r--wallet/src/main/res/drawable/ic_scan_qr.xml10
-rw-r--r--wallet/src/main/res/drawable/ic_settings.xml9
-rw-r--r--wallet/src/main/res/drawable/ic_sync.xml9
-rw-r--r--wallet/src/main/res/drawable/pending_border.xml37
-rw-r--r--wallet/src/main/res/drawable/side_nav_bar.xml24
-rw-r--r--wallet/src/main/res/layout-w550dp/payment_bottom_bar.xml123
-rw-r--r--wallet/src/main/res/layout/activity_main.xml40
-rw-r--r--wallet/src/main/res/layout/app_bar_main.xml75
-rw-r--r--wallet/src/main/res/layout/fragment_already_paid.xml52
-rw-r--r--wallet/src/main/res/layout/fragment_error.xml97
-rw-r--r--wallet/src/main/res/layout/fragment_json.xml41
-rw-r--r--wallet/src/main/res/layout/fragment_payment_successful.xml63
-rw-r--r--wallet/src/main/res/layout/fragment_pending_operations.xml34
-rw-r--r--wallet/src/main/res/layout/fragment_product_image.xml24
-rw-r--r--wallet/src/main/res/layout/fragment_prompt_payment.xml44
-rw-r--r--wallet/src/main/res/layout/fragment_prompt_withdraw.xml171
-rw-r--r--wallet/src/main/res/layout/fragment_review_exchange_tos.xml105
-rw-r--r--wallet/src/main/res/layout/fragment_settings.xml104
-rw-r--r--wallet/src/main/res/layout/fragment_show_balance.xml91
-rw-r--r--wallet/src/main/res/layout/fragment_show_history.xml47
-rw-r--r--wallet/src/main/res/layout/fragment_withdraw_successful.xml63
-rw-r--r--wallet/src/main/res/layout/history_payment.xml87
-rw-r--r--wallet/src/main/res/layout/history_receive.xml105
-rw-r--r--wallet/src/main/res/layout/history_row.xml73
-rw-r--r--wallet/src/main/res/layout/list_item_balance.xml77
-rw-r--r--wallet/src/main/res/layout/list_item_product.xml75
-rw-r--r--wallet/src/main/res/layout/list_item_product_single.xml78
-rw-r--r--wallet/src/main/res/layout/nav_header_main.xml73
-rw-r--r--wallet/src/main/res/layout/payment_bottom_bar.xml123
-rw-r--r--wallet/src/main/res/layout/payment_details.xml119
-rw-r--r--wallet/src/main/res/layout/pending_row.xml48
-rw-r--r--wallet/src/main/res/menu/activity_main_drawer.xml41
-rw-r--r--wallet/src/main/res/menu/balance.xml28
-rw-r--r--wallet/src/main/res/menu/history.xml31
-rw-r--r--wallet/src/main/res/menu/pending_operations.xml24
-rw-r--r--wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml21
-rw-r--r--wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml21
-rw-r--r--wallet/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 1611 bytes
-rw-r--r--wallet/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 2898 bytes
-rw-r--r--wallet/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 1101 bytes
-rw-r--r--wallet/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 1836 bytes
-rw-r--r--wallet/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 2314 bytes
-rw-r--r--wallet/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 4158 bytes
-rw-r--r--wallet/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 3405 bytes
-rw-r--r--wallet/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 6328 bytes
-rw-r--r--wallet/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 4592 bytes
-rw-r--r--wallet/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 8828 bytes
-rw-r--r--wallet/src/main/res/navigation/nav_graph.xml125
-rw-r--r--wallet/src/main/res/values/colors.xml25
-rw-r--r--wallet/src/main/res/values/dimens.xml24
-rw-r--r--wallet/src/main/res/values/ic_launcher_background.xml20
-rw-r--r--wallet/src/main/res/values/strings.xml105
-rw-r--r--wallet/src/main/res/values/styles.xml46
-rw-r--r--wallet/src/main/res/xml/apduservice.xml25
-rw-r--r--wallet/src/main/res/xml/backup_descriptor.xml20
-rw-r--r--wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt33
-rw-r--r--wallet/src/test/java/net/taler/wallet/crypto/Base32CrockfordTest.kt35
-rw-r--r--wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt459
-rw-r--r--wallet/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt52
114 files changed, 7933 insertions, 0 deletions
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
--- /dev/null
+++ b/wallet/src/main/ic_launcher-web.png
Binary files 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.5C14.5 16.94 14.65 16.42 14.92 16Z" />
+</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.985q1.546,-0.638 3.14,-0.94 1.594,-0.303 3.379,-0.303 4.351,0 6.104,1.769 1.769,1.769 1.769,6.328z"
+ 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,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z" />
+</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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files 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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files 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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files 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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files 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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files 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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files 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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files 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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files 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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files 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
--- /dev/null
+++ b/wallet/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files 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("51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30H2365338E9G6RT4AH1N6H13EGHR70RK6H1S6X2M4CSP8CSK8E1G88VKJH25610KGCHR8RWM4DJ47123CH9K89334D1S8N24ACJ48CR3EH256MR3AH1R711KCE9N6S134GSN6RW46D1H6CV3CDHJ6D0KEDHR6D24CD248MWKADHJ6WT34D25712KCD2474V46EA18H2M4GHM6WTK2E216S14CD238GSK0G9G692KCDHM6RW34CT16MV3CG9P60S34C1G70SMCHHQ8CVKJG9K6CVK6GHK70R46HJ26CR4AE9M8523ADHS8RR3EE1R74S32CHP6N1K0GT38D1M6C1R84TM2E9N8MSK2C1J71248E9H6H1MCD9J70VK4GSG6124CCHR6RS4ADSH8N0M4H1G84R4CD1G8D24AG9N6RR48DT1712K6GJ26X232DT36N0K4C9M8H236HJ48N2K4G9H8GVM8E1P8GSM6E9K891K4CSN65348C26611M8DHJ8S1M6H9G8H338CHS6GV3CD9K64S3GCHR8H2M6GJ58MT3EHA26S232GSJ6GTMAGA570W44DA2852KEDSR8MTKEGA460T3CCT18MR48CHK6WWKEGJ460WK4EA568VM6GSJ70T32CA461234DJ66RS34DHM6D242CT46MV3JDA584S4ADSM6S1MAE1P6GTKEGA68N1M8E216WRMAGHM6RR4ADSJ8MR3EDJ2690KAD9H6H346D9R88RKECSN8RRKJC1N74W34DSQ60W48DSJ8S1K0DSH8D1M4E1J6H1M2D1S8S33CG9R6RSMCH9K4CMGM81051JJ08SG64R30C1H4CMGM81054520A8A00")
+ 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)
+ }
+
+}