summaryrefslogtreecommitdiff
path: root/wallet/src
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src')
-rw-r--r--wallet/src/main/AndroidManifest.xml38
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainActivity.kt262
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainFragment.kt41
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainViewModel.kt184
-rw-r--r--wallet/src/main/java/net/taler/wallet/NetworkManager.kt64
-rw-r--r--wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt201
-rw-r--r--wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt196
-rw-r--r--wallet/src/main/java/net/taler/wallet/UriInputFragment.kt12
-rw-r--r--wallet/src/main/java/net/taler/wallet/Utils.kt65
-rw-r--r--wallet/src/main/java/net/taler/wallet/WalletApp.kt (renamed from wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt)17
-rw-r--r--wallet/src/main/java/net/taler/wallet/accounts/AccountManager.kt54
-rw-r--r--wallet/src/main/java/net/taler/wallet/accounts/KnownBankAccounts.kt113
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt61
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt94
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt (renamed from wallet/src/androidTest/java/net/taler/wallet/ExampleInstrumentedTest.kt)43
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt44
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt3897
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt203
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt252
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt90
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt40
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt137
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/Balances.kt57
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt50
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt226
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt70
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt153
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt46
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt67
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/Utils.kt64
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt117
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt169
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt49
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/MakeBitcoinDepositComposable.kt159
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt192
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt242
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt119
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt3
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt31
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt10
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt74
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt109
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt32
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt111
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt48
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt49
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt187
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt115
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt177
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt96
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt29
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt26
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt57
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt174
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt128
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt270
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt73
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt73
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt60
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt280
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt107
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt218
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt122
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt63
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt217
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt105
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt90
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt90
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt152
-rw-r--r--wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt185
-rw-r--r--wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt57
-rw-r--r--wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt17
-rw-r--r--wallet/src/main/java/net/taler/wallet/refund/RefundPaymentInfo.kt39
-rw-r--r--wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt123
-rw-r--r--wallet/src/main/java/net/taler/wallet/settings/BackupSettingsFragment.kt43
-rw-r--r--wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt104
-rw-r--r--wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt171
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt116
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/ErrorTransactionComposable.kt116
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt120
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt48
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt164
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionDummyFragment.kt64
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionLinkComposable.kt86
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt178
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt47
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt135
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt106
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt47
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt180
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt93
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt390
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt97
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt113
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt139
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt15
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt154
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt267
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt (renamed from wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt)33
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt95
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt314
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt112
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt91
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt89
-rw-r--r--wallet/src/main/res/drawable/ic_archive.xml21
-rw-r--r--wallet/src/main/res/drawable/ic_baseline_account_tree.xml10
-rw-r--r--wallet/src/main/res/drawable/ic_baseline_backup.xml10
-rw-r--r--wallet/src/main/res/drawable/ic_baseline_vpn_key.xml10
-rw-r--r--wallet/src/main/res/drawable/ic_fail.xml5
-rw-r--r--wallet/src/main/res/drawable/ic_resume.xml5
-rw-r--r--wallet/src/main/res/drawable/ic_retry.xml5
-rw-r--r--wallet/src/main/res/drawable/ic_suspend.xml5
-rw-r--r--wallet/src/main/res/drawable/ic_unarchive.xml26
-rw-r--r--wallet/src/main/res/layout/app_content_main.xml28
-rw-r--r--wallet/src/main/res/layout/dialog_exchange_add.xml2
-rw-r--r--wallet/src/main/res/layout/fragment_already_paid.xml52
-rw-r--r--wallet/src/main/res/layout/fragment_error.xml10
-rw-r--r--wallet/src/main/res/layout/fragment_exchange_fees.xml4
-rw-r--r--wallet/src/main/res/layout/fragment_main.xml5
-rw-r--r--wallet/src/main/res/layout/fragment_manual_withdraw.xml180
-rw-r--r--wallet/src/main/res/layout/fragment_pending_operations.xml34
-rw-r--r--wallet/src/main/res/layout/fragment_prompt_withdraw.xml104
-rw-r--r--wallet/src/main/res/layout/fragment_review_exchange_tos.xml2
-rw-r--r--wallet/src/main/res/layout/fragment_transaction_payment.xml126
-rw-r--r--wallet/src/main/res/layout/fragment_transaction_withdrawal.xml128
-rw-r--r--wallet/src/main/res/layout/fragment_transactions.xml103
-rw-r--r--wallet/src/main/res/layout/fragment_uri_input.xml10
-rw-r--r--wallet/src/main/res/layout/list_item_age.xml26
-rw-r--r--wallet/src/main/res/layout/list_item_balance.xml29
-rw-r--r--wallet/src/main/res/layout/list_item_pending_operation.xml47
-rw-r--r--wallet/src/main/res/layout/list_item_transaction.xml1
-rw-r--r--wallet/src/main/res/layout/nav_header_main.xml2
-rw-r--r--wallet/src/main/res/menu/activity_main_drawer.xml5
-rw-r--r--wallet/src/main/res/menu/exchange.xml6
-rw-r--r--wallet/src/main/res/menu/exchange_list.xml (renamed from wallet/src/main/res/menu/pending_operations.xml)9
-rw-r--r--wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml1
-rw-r--r--wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml1
-rw-r--r--wallet/src/main/res/navigation/nav_graph.xml253
-rw-r--r--wallet/src/main/res/values-ar/strings.xml147
-rw-r--r--wallet/src/main/res/values-ca/strings.xml265
-rw-r--r--wallet/src/main/res/values-de/strings.xml266
-rw-r--r--wallet/src/main/res/values-es/strings.xml295
-rw-r--r--wallet/src/main/res/values-fi/strings.xml288
-rw-r--r--wallet/src/main/res/values-fr/strings.xml164
-rw-r--r--wallet/src/main/res/values-it/strings.xml242
-rw-r--r--wallet/src/main/res/values-ko/strings.xml142
-rw-r--r--wallet/src/main/res/values-night/styles.xml28
-rw-r--r--wallet/src/main/res/values-nl/strings.xml (renamed from wallet/src/main/res/menu/transactions_detail.xml)9
-rw-r--r--wallet/src/main/res/values-pt/strings.xml142
-rw-r--r--wallet/src/main/res/values-ru/strings.xml145
-rw-r--r--wallet/src/main/res/values-tr/strings.xml160
-rw-r--r--wallet/src/main/res/values-v27/styles.xml26
-rw-r--r--wallet/src/main/res/values/colors.xml63
-rw-r--r--wallet/src/main/res/values/defaults.xml2
-rw-r--r--wallet/src/main/res/values/strings.xml151
-rw-r--r--wallet/src/main/res/values/styles.xml85
-rw-r--r--wallet/src/main/res/xml/network_security_config.xml6
-rw-r--r--wallet/src/main/res/xml/settings_backup.xml58
-rw-r--r--wallet/src/main/res/xml/settings_main.xml32
-rw-r--r--wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt5
-rw-r--r--wallet/src/test/java/net/taler/wallet/payment/PaymentResponsesTest.kt9
-rw-r--r--wallet/src/test/java/net/taler/wallet/withdraw/WithdrawManagerKtTest.kt51
164 files changed, 17096 insertions, 2372 deletions
diff --git a/wallet/src/main/AndroidManifest.xml b/wallet/src/main/AndroidManifest.xml
index b011583..b8c05c6 100644
--- a/wallet/src/main/AndroidManifest.xml
+++ b/wallet/src/main/AndroidManifest.xml
@@ -15,12 +15,15 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- package="net.taler.wallet">
+ xmlns:tools="http://schemas.android.com/tools">
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission
+ android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.NFC" />
<uses-feature
@@ -31,21 +34,24 @@
android:required="false" />
<application
+ android:name=".WalletApp"
android:allowBackup="true"
android:extractNativeLibs="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
- tools:ignore="GoogleAppIndexingWarning"
- tools:targetApi="m">
+ tools:ignore="GoogleAppIndexingWarning">
<activity
android:name=".MainActivity"
- android:label="@string/app_name"
- android:theme="@style/AppTheme.NoActionBar">
+ android:exported="true"
+ android:launchMode="singleInstance"
+ android:theme="@style/AppTheme.NoActionBar"
+ android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -57,6 +63,14 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="taler" />
+ <data
+ android:scheme="TALER"
+ tools:ignore="AppLinkUrlError" />
+ <data android:scheme="ext+taler" />
+ <data
+ android:scheme="EXT+TALER"
+ tools:ignore="AppLinkUrlError" />
+ <data android:scheme="payto" />
</intent-filter>
</activity>
@@ -77,10 +91,14 @@
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice" />
</service>
-
- <service
- android:name=".backend.WalletBackendService"
- android:process=":WalletBackendService" />
</application>
+ <queries>
+ <intent>
+ <data
+ android:host="iban"
+ android:scheme="payto" />
+ </intent>
+ </queries>
+
</manifest>
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
index 1dafce0..5dfd920 100644
--- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
@@ -32,8 +32,12 @@ import android.view.View.VISIBLE
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat.START
+import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
@@ -44,8 +48,15 @@ import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCal
import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener
import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
import com.google.android.material.snackbar.Snackbar
-import com.google.zxing.integration.android.IntentIntegrator
-import com.google.zxing.integration.android.IntentIntegrator.parseActivityResult
+import com.google.zxing.client.android.Intents.Scan.MIXED_SCAN
+import com.google.zxing.client.android.Intents.Scan.SCAN_TYPE
+import com.journeyapps.barcodescanner.ScanContract
+import com.journeyapps.barcodescanner.ScanOptions
+import com.journeyapps.barcodescanner.ScanOptions.QR_CODE
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import net.taler.common.EventObserver
import net.taler.common.isOnline
import net.taler.common.showError
import net.taler.wallet.BuildConfig.VERSION_CODE
@@ -56,6 +67,9 @@ import net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_DISCONNEC
import net.taler.wallet.HostCardEmulatorService.Companion.TRIGGER_PAYMENT_ACTION
import net.taler.wallet.databinding.ActivityMainBinding
import net.taler.wallet.refund.RefundStatus
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URL
import java.util.Locale.ROOT
class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
@@ -66,6 +80,11 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
private lateinit var ui: ActivityMainBinding
private lateinit var nav: NavController
+ private val barcodeLauncher = registerForActivityResult(ScanContract()) { result ->
+ if (result == null || result.contents == null) return@registerForActivityResult
+ handleTalerUri(result.contents, "QR code")
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ui = ActivityMainBinding.inflate(layoutInflater)
@@ -82,24 +101,23 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
setSupportActionBar(ui.content.toolbar)
val appBarConfiguration = AppBarConfiguration(
- setOf(R.id.nav_main, R.id.nav_settings, R.id.nav_pending_operations),
+ setOf(R.id.nav_main, R.id.nav_settings),
ui.drawerLayout
)
ui.content.toolbar.setupWithNavController(nav, appBarConfiguration)
- model.showProgressBar.observe(this, { show ->
+ model.showProgressBar.observe(this) { show ->
ui.content.progressBar.visibility = if (show) VISIBLE else INVISIBLE
- })
+ }
val versionView: TextView = ui.navView.getHeaderView(0).findViewById(R.id.versionView)
- model.devMode.observe(this, { enabled ->
- ui.navView.menu.findItem(R.id.nav_dev).isVisible = enabled
- if (enabled) {
- @SuppressLint("SetTextI18n")
- versionView.text = "$VERSION_NAME ($VERSION_CODE)"
- versionView.visibility = VISIBLE
- } else versionView.visibility = GONE
- })
+ @SuppressLint("SetTextI18n")
+ versionView.text = "$VERSION_NAME ($VERSION_CODE)"
+
+ // Uncomment if any dev options are added in the future
+ // model.devMode.observe(this) { enabled ->
+ // ui.navView.menu.findItem(R.id.nav_dev).isVisible = enabled
+ // }
if (intent.action == ACTION_VIEW) intent.dataString?.let { uri ->
handleTalerUri(uri, "intent")
@@ -111,32 +129,45 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
registerReceiver(nfcConnectedReceiver, IntentFilter(MERCHANT_NFC_CONNECTED))
registerReceiver(nfcDisconnectedReceiver, IntentFilter(MERCHANT_NFC_DISCONNECTED))
registerReceiver(tunnelResponseReceiver, IntentFilter(HTTP_TUNNEL_RESPONSE))
+
+ model.scanCodeEvent.observe(this, EventObserver {
+ val scanOptions = ScanOptions().apply {
+ setPrompt("")
+ setBeepEnabled(true)
+ setOrientationLocked(false)
+ setDesiredBarcodeFormats(QR_CODE)
+ addExtra(SCAN_TYPE, MIXED_SCAN)
+ }
+ if (it) barcodeLauncher.launch(scanOptions)
+ })
+
+ model.networkManager.networkStatus.observe(this) { online ->
+ ui.content.offlineBanner.visibility = if (online) GONE else VISIBLE
+ }
}
+ @Deprecated("Deprecated in Java")
override fun onBackPressed() {
if (ui.drawerLayout.isDrawerOpen(START)) ui.drawerLayout.closeDrawer(START)
else super.onBackPressed()
}
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ if (intent?.action == ACTION_VIEW) intent.dataString?.let { uri ->
+ handleTalerUri(uri, "intent")
+ }
+ }
+
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_home -> nav.navigate(R.id.nav_main)
R.id.nav_settings -> nav.navigate(R.id.nav_settings)
- R.id.nav_pending_operations -> nav.navigate(R.id.nav_pending_operations)
}
ui.drawerLayout.closeDrawer(START)
return true
}
- override fun 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)
@@ -145,29 +176,157 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
super.onDestroy()
}
+ private fun getTalerAction(
+ uri: Uri,
+ maxRedirects: Int,
+ actionFound: MutableLiveData<String>,
+ ): MutableLiveData<String> {
+ val scheme = uri.scheme ?: return actionFound
+
+ if (scheme == "http" || scheme == "https") {
+ model.viewModelScope.launch(Dispatchers.IO) {
+ val conn = URL(uri.toString()).openConnection() as HttpURLConnection
+ Log.v(TAG, "prepare query: $uri")
+ conn.setRequestProperty("Accept", "text/html")
+ conn.connectTimeout = 5000
+ conn.requestMethod = "HEAD"
+ try {
+ conn.connect()
+ } catch (e: IOException) {
+ Log.e(TAG, "Error connecting to $uri ", e)
+ showError(R.string.error_broken_uri, "$uri")
+ return@launch
+ }
+ val status = conn.responseCode
+
+ if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_PAYMENT_REQUIRED) {
+ val talerHeader = conn.headerFields["Taler"]
+ if (talerHeader != null && talerHeader[0] != null) {
+ Log.v(TAG, "taler header: ${talerHeader[0]}")
+ val talerHeaderUri = Uri.parse(talerHeader[0])
+ getTalerAction(talerHeaderUri, 0, actionFound)
+ }
+ } else if (status == HttpURLConnection.HTTP_MOVED_TEMP
+ || status == HttpURLConnection.HTTP_MOVED_PERM
+ || status == HttpURLConnection.HTTP_SEE_OTHER
+ ) {
+ val location = conn.headerFields["Location"]
+ if (location != null && location[0] != null) {
+ Log.v(TAG, "location redirect: ${location[0]}")
+ val locUri = Uri.parse(location[0])
+ getTalerAction(locUri, maxRedirects - 1, actionFound)
+ }
+ } else {
+ showError(R.string.error_broken_uri, "$uri")
+ }
+ }
+ } else {
+ actionFound.postValue(uri.toString())
+ }
+
+ return actionFound
+ }
+
private fun handleTalerUri(url: String, from: String) {
val uri = Uri.parse(url)
if (uri.fragment != null && !isOnline()) {
connectToWifi(this, uri.fragment!!)
}
- when {
- url.toLowerCase(ROOT).startsWith("taler://pay/") -> {
- Log.v(TAG, "navigating!")
- nav.navigate(R.id.action_nav_main_to_promptPayment)
- model.paymentManager.preparePay(url)
- }
- url.toLowerCase(ROOT).startsWith("taler://withdraw/") -> {
- Log.v(TAG, "navigating!")
- // there's more than one entry point, so use global action
- nav.navigate(R.id.action_global_promptWithdraw)
- model.withdrawManager.getWithdrawalDetails(url)
- }
- url.toLowerCase(ROOT).startsWith("taler://refund/") -> {
- model.showProgressBar.value = true
- model.refundManager.refund(url).observe(this, Observer(::onRefundResponse))
+
+ getTalerAction(uri, 3, MutableLiveData<String>()).observe(this) { u ->
+ Log.v(TAG, "found action $u")
+
+ if (u.startsWith("payto://", ignoreCase = true)) {
+ Log.v(TAG, "navigating with paytoUri!")
+ val bundle = bundleOf("uri" to u)
+ nav.navigate(R.id.action_nav_payto_uri, bundle)
+ return@observe
}
- else -> {
- showError(R.string.error_unsupported_uri, "From: $from\nURI: $url")
+
+ val normalizedURL = u.lowercase(ROOT)
+ var ext = false
+ val action = normalizedURL.substring(
+ if (normalizedURL.startsWith("taler://", ignoreCase = true)) {
+ "taler://".length
+ } else if (normalizedURL.startsWith("ext+taler://", ignoreCase = true)) {
+ ext = true
+ "ext+taler://".length
+ } else if (normalizedURL.startsWith("taler+http://", ignoreCase = true) &&
+ model.devMode.value == true
+ ) {
+ "taler+http://".length
+ } else {
+ normalizedURL.length
+ }
+ )
+
+ // Remove ext+ scheme prefix if present
+ val u2 = if (ext) {
+ "taler://" + u.substring("ext+taler://".length)
+ } else u
+
+ when {
+ action.startsWith("pay/", ignoreCase = true) -> {
+ Log.v(TAG, "navigating!")
+ nav.navigate(R.id.action_global_promptPayment)
+ model.paymentManager.preparePay(u2)
+ }
+ action.startsWith("withdraw/", ignoreCase = true) -> {
+ Log.v(TAG, "navigating!")
+ // there's more than one entry point, so use global action
+ nav.navigate(R.id.action_global_promptWithdraw)
+ model.withdrawManager.getWithdrawalDetails(u2)
+ }
+
+ action.startsWith("withdraw-exchange/", ignoreCase = true) -> {
+ model.showProgressBar.value = true
+ lifecycleScope.launch(Dispatchers.IO) {
+ val response = model.withdrawManager.prepareManualWithdrawal(u2)
+ if (response == null) withContext(Dispatchers.Main) {
+ model.showProgressBar.value = false
+ nav.navigate(R.id.errorFragment)
+ } else {
+ val exchange =
+ model.exchangeManager.findExchangeByUrl(response.exchangeBaseUrl)
+ if (exchange == null) withContext(Dispatchers.Main) {
+ model.showProgressBar.value = false
+ showError(R.string.exchange_add_error)
+ } else {
+ model.exchangeManager.withdrawalExchange = exchange
+ withContext(Dispatchers.Main) {
+ model.showProgressBar.value = false
+ val args = Bundle().apply {
+ if (response.amount != null) {
+ putString("amount", response.amount.toJSONString())
+ }
+ }
+ // there's more than one entry point, so use global action
+ nav.navigate(R.id.action_global_manual_withdrawal, args)
+ }
+ }
+ }
+ }
+ }
+
+ action.startsWith("refund/", ignoreCase = true) -> {
+ model.showProgressBar.value = true
+ model.refundManager.refund(u2).observe(this, Observer(::onRefundResponse))
+ }
+ action.startsWith("pay-pull/", ignoreCase = true) -> {
+ nav.navigate(R.id.action_global_prompt_pull_payment)
+ model.peerManager.preparePeerPullDebit(u2)
+ }
+ action.startsWith("pay-push/", ignoreCase = true) -> {
+ nav.navigate(R.id.action_global_prompt_push_payment)
+ model.peerManager.preparePeerPushCredit(u2)
+ }
+ action.startsWith("pay-template/", ignoreCase = true) -> {
+ val bundle = bundleOf("uri" to u2)
+ nav.navigate(R.id.action_global_prompt_pay_template, bundle)
+ }
+ else -> {
+ showError(R.string.error_unsupported_uri, "From: $from\nURI: $u2")
+ }
}
}
}
@@ -176,13 +335,23 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
model.showProgressBar.value = false
when (status) {
is RefundStatus.Error -> {
- showError(R.string.refund_error, status.msg)
+ if (model.devMode.value == true) {
+ showError(status.error)
+ } else {
+ showError(R.string.refund_error, status.error.userFacingMsg)
+ }
}
is RefundStatus.Success -> {
- val amount = status.response.amountRefundGranted
- model.showTransactions(amount.currency)
- val str = getString(R.string.refund_success, amount.amountStr)
- Snackbar.make(ui.navView, str, LENGTH_LONG).show()
+ lifecycleScope.launch {
+ val transactionId = status.response.transactionId
+ val transaction = model.transactionManager.getTransactionById(transactionId)
+ if (transaction != null) {
+ // TODO: currency what? scopes are the cool thing now
+ // val currency = transaction.amountRaw.currency
+ // model.showTransactions(currency)
+ Snackbar.make(ui.navView, getString(R.string.refund_success), LENGTH_LONG).show()
+ }
+ }
}
}
}
@@ -222,10 +391,9 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
- pref: Preference
+ pref: Preference,
): Boolean {
when (pref.key) {
- "pref_backup" -> nav.navigate(R.id.action_nav_settings_to_nav_settings_backup)
"pref_exchanges" -> nav.navigate(R.id.action_nav_settings_to_nav_settings_exchanges)
}
return true
diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt
index 1479bc0..9fa9838 100644
--- a/wallet/src/main/java/net/taler/wallet/MainFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainFragment.kt
@@ -24,45 +24,46 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import net.taler.common.EventObserver
-import net.taler.wallet.CurrencyMode.MULTI
-import net.taler.wallet.CurrencyMode.SINGLE
-import net.taler.wallet.balances.BalanceItem
+import net.taler.wallet.ScopeMode.MULTI
+import net.taler.wallet.ScopeMode.SINGLE
+import net.taler.wallet.balances.BalanceState
+import net.taler.wallet.balances.BalanceState.Success
import net.taler.wallet.balances.BalancesFragment
import net.taler.wallet.databinding.FragmentMainBinding
import net.taler.wallet.transactions.TransactionsFragment
-enum class CurrencyMode { SINGLE, MULTI }
+enum class ScopeMode { SINGLE, MULTI }
class MainFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
- private var currencyMode: CurrencyMode? = null
+ private var scopeMode: ScopeMode? = null
private lateinit var ui: FragmentMainBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
+ savedInstanceState: Bundle?,
+ ): View {
ui = FragmentMainBinding.inflate(inflater, container, false)
return ui.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- model.balances.observe(viewLifecycleOwner, {
+ model.balanceManager.state.observe(viewLifecycleOwner) {
onBalancesChanged(it)
- })
- model.transactionsEvent.observe(viewLifecycleOwner, EventObserver { currency ->
- // we only need to navigate to a dedicated list, when in multi-currency mode
- if (currencyMode == MULTI) {
- model.transactionManager.selectedCurrency = currency
+ }
+ model.transactionsEvent.observe(viewLifecycleOwner, EventObserver { scopeInfo ->
+ // we only need to navigate to a dedicated list, when in multi-scope mode
+ if (scopeMode == MULTI) {
+ model.transactionManager.selectedScope = scopeInfo
findNavController().navigate(R.id.action_nav_main_to_nav_transactions)
}
})
ui.mainFab.setOnClickListener {
- scanQrCode(requireActivity())
+ model.scanCode()
}
ui.mainFab.setOnLongClickListener {
findNavController().navigate(R.id.action_nav_main_to_nav_uri_input)
@@ -72,19 +73,21 @@ class MainFragment : Fragment() {
override fun onStart() {
super.onStart()
- model.loadBalances()
+ model.balanceManager.loadBalances()
}
- private fun onBalancesChanged(balances: List<BalanceItem>) {
+ private fun onBalancesChanged(state: BalanceState) {
+ if (state !is Success) return
+ val balances = state.balances
val mode = if (balances.size == 1) SINGLE else MULTI
- if (currencyMode != mode) {
+ if (scopeMode != mode) {
val f = if (mode == SINGLE) {
- model.transactionManager.selectedCurrency = balances[0].available.currency
+ model.transactionManager.selectedScope = balances[0].scopeInfo
TransactionsFragment()
} else {
BalancesFragment()
}
- currencyMode = mode
+ scopeMode = mode
childFragmentManager.beginTransaction()
.replace(R.id.mainFragmentContainer, f, mode.name)
.commitNow()
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
index 7bb6ad9..5903446 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -22,131 +22,175 @@ import androidx.annotation.UiThread
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.Job
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import net.taler.common.Amount
+import net.taler.common.AmountParserException
import net.taler.common.Event
-import net.taler.common.assertUiThread
import net.taler.common.toEvent
+import net.taler.wallet.accounts.AccountManager
+import net.taler.wallet.backend.NotificationPayload
+import net.taler.wallet.backend.NotificationReceiver
+import net.taler.wallet.backend.VersionReceiver
import net.taler.wallet.backend.WalletBackendApi
-import net.taler.wallet.balances.BalanceItem
-import net.taler.wallet.balances.BalanceResponse
+import net.taler.wallet.backend.WalletCoreVersion
+import net.taler.wallet.balances.BalanceManager
+import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.deposit.DepositManager
import net.taler.wallet.exchanges.ExchangeManager
import net.taler.wallet.payment.PaymentManager
-import net.taler.wallet.pending.PendingOperationsManager
+import net.taler.wallet.peer.PeerManager
import net.taler.wallet.refund.RefundManager
+import net.taler.wallet.settings.SettingsManager
import net.taler.wallet.transactions.TransactionManager
import net.taler.wallet.withdraw.WithdrawManager
import org.json.JSONObject
-import java.util.concurrent.TimeUnit.DAYS
-import java.util.concurrent.TimeUnit.MINUTES
-import kotlin.random.Random
const val TAG = "taler-wallet"
private val transactionNotifications = listOf(
- "proposal-accepted",
- "refresh-revealed",
- "withdraw-group-finished"
+ "transaction-state-transition",
)
-class MainViewModel(val app: Application) : AndroidViewModel(app) {
-
- private val mBalances = MutableLiveData<List<BalanceItem>>()
- val balances: LiveData<List<BalanceItem>> = mBalances.distinctUntilChanged()
+class MainViewModel(
+ app: Application,
+) : AndroidViewModel(app), VersionReceiver, NotificationReceiver {
val devMode = MutableLiveData(BuildConfig.DEBUG)
val showProgressBar = MutableLiveData<Boolean>()
+ var walletVersion: String? = null
+ private set
+ var walletVersionHash: String? = null
+ private set
var exchangeVersion: String? = null
private set
var merchantVersion: String? = null
private set
- private val api = WalletBackendApi(app) { payload ->
- if (payload.optString("operation") == "init") {
- val result = payload.getJSONObject("result")
- val versions = result.getJSONObject("supported_protocol_versions")
- exchangeVersion = versions.getString("exchange")
- merchantVersion = versions.getString("merchant")
- } else if (payload.getString("type") != "waiting-for-retry") { // ignore ping
- Log.i(TAG, "Received notification from wallet-core: ${payload.toString(2)}")
- loadBalances()
- if (payload.optString("type") in transactionNotifications) {
- assertUiThread()
- // TODO notification API should give us a currency to update
- // update currently selected transaction list
- transactionManager.loadTransactions()
- }
- // refresh pending ops and history with each notification
- if (devMode.value == true) {
- pendingOperationsManager.getPending()
- }
- }
- }
+ private val api = WalletBackendApi(app, this, this)
+ val networkManager = NetworkManager(app.applicationContext)
val withdrawManager = WithdrawManager(api, viewModelScope)
val paymentManager = PaymentManager(api, viewModelScope)
- val pendingOperationsManager: PendingOperationsManager = PendingOperationsManager(api)
val transactionManager: TransactionManager = TransactionManager(api, viewModelScope)
val refundManager = RefundManager(api, viewModelScope)
+ val balanceManager = BalanceManager(api, viewModelScope)
val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope)
+ val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope)
+ val settingsManager: SettingsManager = SettingsManager(app.applicationContext, api, viewModelScope)
+ val accountManager: AccountManager = AccountManager(api, viewModelScope)
+ val depositManager: DepositManager = DepositManager(api, viewModelScope)
+
+ private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>()
+ val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent
+
+ private val mScanCodeEvent = MutableLiveData<Event<Boolean>>()
+ val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent
+
+ override fun onVersionReceived(versionInfo: WalletCoreVersion) {
+ walletVersion = versionInfo.implementationSemver
+ walletVersionHash = versionInfo.implementationGitHash
+ exchangeVersion = versionInfo.exchange
+ merchantVersion = versionInfo.merchant
+ }
- private val mTransactionsEvent = MutableLiveData<Event<String>>()
- val transactionsEvent: LiveData<Event<String>> = mTransactionsEvent
+ override fun onNotificationReceived(payload: NotificationPayload) {
+ if (payload.type == "waiting-for-retry") return // ignore ping)
+ Log.i(TAG, "Received notification from wallet-core: $payload")
- private val mLastBackup = MutableLiveData(
- // fake backup time until we actually do backup
- System.currentTimeMillis() -
- Random.nextLong(MINUTES.toMillis(5), DAYS.toMillis(2))
- )
- val lastBackup: LiveData<Long> = mLastBackup
+ // Only update balances when we're told they changed
+ if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) {
+ balanceManager.loadBalances()
+ }
- override fun onCleared() {
- api.destroy()
- super.onCleared()
+ if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) {
+ // TODO notification API should give us a currency to update
+ // update currently selected transaction list
+ transactionManager.loadTransactions()
+ }
}
+ /**
+ * Navigates to the given scope info's transaction list, when [MainFragment] is shown.
+ */
@UiThread
- fun loadBalances(): Job = viewModelScope.launch {
- showProgressBar.value = true
- val response = api.request("getBalances", BalanceResponse.serializer())
- showProgressBar.value = false
- response.onError {
- // TODO expose in UI
- Log.e(TAG, "Error retrieving balances: $it")
- }
- response.onSuccess {
- mBalances.value = it.balances
+ fun showTransactions(scopeInfo: ScopeInfo) {
+ mTransactionsEvent.value = scopeInfo.toEvent()
+ }
+
+ @UiThread
+ fun getCurrencies() = balanceManager.balances.value?.map { balanceItem ->
+ balanceItem.currency
+ } ?: emptyList()
+
+ @UiThread
+ fun createAmount(amountText: String, currency: String): AmountResult {
+ val amount = try {
+ Amount.fromString(currency, amountText)
+ } catch (e: AmountParserException) {
+ return AmountResult.InvalidAmount
}
+ if (hasSufficientBalance(amount)) return AmountResult.Success(amount)
+ return AmountResult.InsufficientBalance
}
- /**
- * Navigates to the given currency's transaction list, when [MainFragment] is shown.
- */
@UiThread
- fun showTransactions(currency: String) {
- mTransactionsEvent.value = currency.toEvent()
+ fun hasSufficientBalance(amount: Amount): Boolean {
+ balanceManager.balances.value?.forEach { balanceItem ->
+ if (balanceItem.currency == amount.currency) {
+ return balanceItem.available >= amount
+ }
+ }
+ return false
}
@UiThread
fun dangerouslyReset() {
- api.sendRequest("reset")
withdrawManager.testWithdrawalStatus.value = null
- mBalances.value = emptyList()
+ balanceManager.resetBalances()
}
fun startTunnel() {
- api.sendRequest("startTunnel")
+ viewModelScope.launch {
+ api.sendRequest("startTunnel")
+ }
}
fun stopTunnel() {
- api.sendRequest("stopTunnel")
+ viewModelScope.launch {
+ api.sendRequest("stopTunnel")
+ }
}
fun tunnelResponse(resp: String) {
- val respJson = JSONObject(resp)
- api.sendRequest("tunnelResponse", respJson)
+ viewModelScope.launch {
+ api.sendRequest("tunnelResponse", JSONObject(resp))
+ }
}
+ @UiThread
+ fun scanCode() {
+ mScanCodeEvent.value = true.toEvent()
+ }
+
+ fun runIntegrationTest() {
+ viewModelScope.launch {
+ api.request<Unit>("runIntegrationTestV2") {
+ put("amountToWithdraw", "KUDOS:42")
+ put("amountToSpend", "KUDOS:23")
+ put("corebankApiBaseUrl", "https://bank.demo.taler.net/")
+ put("exchangeBaseUrl", "https://exchange.demo.taler.net/")
+ put("merchantBaseUrl", "https://backend.demo.taler.net/")
+ put("merchantAuthToken", "secret-token:sandbox")
+ }
+ }
+ }
+
+}
+
+sealed class AmountResult {
+ class Success(val amount: Amount) : AmountResult()
+ object InsufficientBalance : AmountResult()
+ object InvalidAmount : AmountResult()
}
diff --git a/wallet/src/main/java/net/taler/wallet/NetworkManager.kt b/wallet/src/main/java/net/taler/wallet/NetworkManager.kt
new file mode 100644
index 0000000..a45ad48
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/NetworkManager.kt
@@ -0,0 +1,64 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet
+
+import android.content.Context
+import android.content.Context.CONNECTIVITY_SERVICE
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+
+class NetworkManager(context: Context) : ConnectivityManager.NetworkCallback() {
+ private val connectivityManager: ConnectivityManager
+
+ private val _networkStatus: MutableLiveData<Boolean>
+ val networkStatus: LiveData<Boolean>
+
+ init {
+ // careful, the order below is important, should probably get simplified
+ connectivityManager = context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ _networkStatus = MutableLiveData(getCurrentStatus())
+ networkStatus = _networkStatus
+ connectivityManager.registerDefaultNetworkCallback(this)
+ }
+
+ @UiThread
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+ super.onCapabilitiesChanged(network, networkCapabilities)
+ _networkStatus.postValue(networkCapabilities.isOnline())
+ }
+
+ override fun onLost(network: Network) {
+ super.onLost(network)
+ _networkStatus.postValue(getCurrentStatus())
+ }
+
+ private fun getCurrentStatus(): Boolean {
+ return connectivityManager.activeNetwork?.let { network ->
+ connectivityManager.getNetworkCapabilities(network)?.isOnline()
+ } ?: false
+ }
+
+ private fun NetworkCapabilities.isOnline(): Boolean {
+ return hasCapability(NET_CAPABILITY_INTERNET) && hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
new file mode 100644
index 0000000..9a83bc8
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
@@ -0,0 +1,201 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import android.widget.Toast.LENGTH_LONG
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.compose.AmountInputField
+import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.exchanges.ExchangeItem
+
+class ReceiveFundsFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val exchangeManager get() = model.exchangeManager
+ private val withdrawManager get() = model.withdrawManager
+ private val balanceManager get() = model.balanceManager
+ private val peerManager get() = model.peerManager
+ private val scopeInfo get() = model.transactionManager.selectedScope ?: error("No scope selected")
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ ReceiveFundsIntro(
+ scopeInfo.currency,
+ balanceManager.getSpecForScopeInfo(scopeInfo),
+ this@ReceiveFundsFragment::onManualWithdraw,
+ this@ReceiveFundsFragment::onPeerPull,
+ )
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(getString(R.string.transactions_receive_funds_title, scopeInfo.currency))
+ }
+
+ private fun onManualWithdraw(amount: Amount) {
+ // TODO give some UI feedback while we wait for exchanges to load (quick enough for now)
+ lifecycleScope.launchWhenResumed {
+ // we need to set the exchange first, we want to withdraw from
+ exchangeManager.findExchangeForCurrency(amount.currency).collect { exchange ->
+ onExchangeRetrieved(exchange, amount)
+ }
+ }
+ }
+
+ private fun onExchangeRetrieved(exchange: ExchangeItem?, amount: Amount) {
+ if (exchange == null) {
+ Toast.makeText(requireContext(), "No exchange available", LENGTH_LONG).show()
+ return
+ }
+
+ // now that we have the exchange, we can navigate
+ exchangeManager.withdrawalExchange = exchange
+ withdrawManager.getWithdrawalDetails(exchange.exchangeBaseUrl, amount)
+ findNavController().navigate(R.id.action_receiveFunds_to_nav_prompt_withdraw)
+ }
+
+ private fun onPeerPull(amount: Amount) {
+ val bundle = bundleOf("amount" to amount.toJSONString())
+ peerManager.checkPeerPullCredit(amount)
+ findNavController().navigate(R.id.action_receiveFunds_to_nav_peer_pull, bundle)
+ }
+}
+
+@Composable
+private fun ReceiveFundsIntro(
+ currency: String,
+ spec: CurrencySpecification?,
+ onManualWithdraw: (Amount) -> Unit,
+ onPeerPull: (Amount) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ ) {
+ var text by rememberSaveable { mutableStateOf("0") }
+ var isError by rememberSaveable { mutableStateOf(false) }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(16.dp),
+ ) {
+ AmountInputField(
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 16.dp),
+ value = text,
+ onValueChange = { input ->
+ isError = false
+ text = input
+ },
+ label = { Text(stringResource(R.string.receive_amount)) },
+ supportingText = {
+ if (isError) Text(stringResource(R.string.receive_amount_invalid))
+ },
+ isError = isError,
+ numberOfDecimals = spec?.numFractionalInputDigits ?: DEFAULT_INPUT_DECIMALS,
+ )
+ Text(
+ modifier = Modifier,
+ text = spec?.symbol ?: currency,
+ softWrap = false,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ text = stringResource(R.string.receive_intro),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ Row(modifier = Modifier.padding(16.dp)) {
+ Button(
+ modifier = Modifier
+ .padding(end = 16.dp)
+ .height(IntrinsicSize.Max)
+ .weight(1f),
+ onClick = {
+ val amount = getAmount(currency, text)
+ if (amount == null) isError = true
+ else onManualWithdraw(amount)
+ }) {
+ Text(text = stringResource(R.string.receive_withdraw))
+ }
+ Button(
+ modifier = Modifier
+ .weight(1f)
+ .height(IntrinsicSize.Max),
+ onClick = {
+ val amount = getAmount(currency, text)
+ if (amount == null || amount.isZero()) isError = true
+ else onPeerPull(amount)
+ },
+ ) {
+ Text(text = stringResource(R.string.receive_peer))
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewReceiveFundsIntro() {
+ Surface {
+ ReceiveFundsIntro("TESTKUDOS", null, {}) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
new file mode 100644
index 0000000..2581979
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
@@ -0,0 +1,196 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.compose.AmountInputField
+import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS
+import net.taler.wallet.compose.TalerSurface
+
+class SendFundsFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val balanceManager get() = model.balanceManager
+ private val peerManager get() = model.peerManager
+ private val scopeInfo get() = model.transactionManager.selectedScope ?: error("No scope selected")
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ SendFundsIntro(
+ currency = scopeInfo.currency,
+ spec = balanceManager.getSpecForScopeInfo(scopeInfo),
+ hasSufficientBalance = model::hasSufficientBalance,
+ onDeposit = this@SendFundsFragment::onDeposit,
+ onPeerPush = this@SendFundsFragment::onPeerPush,
+ )
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(getString(R.string.transactions_send_funds_title, scopeInfo.currency))
+ }
+
+ private fun onDeposit(amount: Amount) {
+ val bundle = bundleOf("amount" to amount.toJSONString())
+ findNavController().navigate(R.id.action_sendFunds_to_nav_deposit, bundle)
+ }
+
+ private fun onPeerPush(amount: Amount) {
+ val bundle = bundleOf("amount" to amount.toJSONString())
+ peerManager.checkPeerPushDebit(amount)
+ findNavController().navigate(R.id.action_sendFunds_to_nav_peer_push, bundle)
+ }
+}
+
+@Composable
+private fun SendFundsIntro(
+ currency: String,
+ spec: CurrencySpecification?,
+ hasSufficientBalance: (Amount) -> Boolean,
+ onDeposit: (Amount) -> Unit,
+ onPeerPush: (Amount) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ ) {
+ var text by rememberSaveable { mutableStateOf("0") }
+ var isError by rememberSaveable { mutableStateOf(false) }
+ var insufficientBalance by rememberSaveable { mutableStateOf(false) }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(16.dp),
+ ) {
+ AmountInputField(
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 16.dp),
+ value = text,
+ onValueChange = { input ->
+ isError = false
+ insufficientBalance = false
+ text = input
+ },
+ label = { Text(stringResource(R.string.send_amount)) },
+ supportingText = {
+ if (isError) Text(stringResource(R.string.receive_amount_invalid))
+ else if (insufficientBalance) {
+ Text(stringResource(R.string.payment_balance_insufficient))
+ }
+ },
+ isError = isError || insufficientBalance,
+ numberOfDecimals = spec?.numFractionalInputDigits ?: DEFAULT_INPUT_DECIMALS,
+ )
+ Text(
+ modifier = Modifier,
+ text = spec?.symbol ?: currency,
+ softWrap = false,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ text = stringResource(R.string.send_intro),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ Row(modifier = Modifier.padding(16.dp)) {
+ fun onClickButton(block: (Amount) -> Unit) {
+ val amount = getAmount(currency, text)
+ if (amount == null || amount.isZero()) isError = true
+ else if (!hasSufficientBalance(amount)) insufficientBalance = true
+ else block(amount)
+ }
+ Button(
+ modifier = Modifier
+ .padding(end = 16.dp)
+ .height(IntrinsicSize.Max)
+ .weight(1f),
+ onClick = {
+ onClickButton { amount -> onDeposit(amount) }
+ }) {
+ Text(text = if (currency == CURRENCY_BTC) {
+ stringResource(R.string.send_deposit_bitcoin)
+ } else {
+ stringResource(R.string.send_deposit)
+ })
+ }
+ Button(
+ modifier = Modifier
+ .height(IntrinsicSize.Max)
+ .weight(1f),
+ onClick = {
+ onClickButton { amount -> onPeerPush(amount) }
+ },
+ ) {
+ Text(text = if (currency == CURRENCY_BTC) {
+ stringResource(R.string.send_peer_bitcoin)
+ } else {
+ stringResource(R.string.send_peer)
+ })
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewSendFundsIntro() {
+ Surface {
+ SendFundsIntro("TESTKUDOS", null, { true }, {}) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/UriInputFragment.kt b/wallet/src/main/java/net/taler/wallet/UriInputFragment.kt
index f4a5f23..63a46a4 100644
--- a/wallet/src/main/java/net/taler/wallet/UriInputFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/UriInputFragment.kt
@@ -17,9 +17,6 @@
package net.taler.wallet
import android.content.ClipboardManager
-import android.content.Intent
-import android.content.Intent.ACTION_VIEW
-import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -38,7 +35,7 @@ class UriInputFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View? {
+ ): View {
ui = FragmentUriInputBinding.inflate(inflater, container, false)
return ui.root
}
@@ -59,10 +56,11 @@ class UriInputFragment : Fragment() {
}
}
ui.okButton.setOnClickListener {
- if (ui.uriView.text?.startsWith("taler://") == true) {
+ val trimmedText = ui.uriView.text?.trim()
+ if (trimmedText?.startsWith("taler://", ignoreCase = true) == true ||
+ trimmedText?.startsWith("payto://", ignoreCase = true) == true) {
ui.uriLayout.error = null
- val i = Intent(ACTION_VIEW, Uri.parse(ui.uriView.text.toString()))
- startActivity(i)
+ launchInAppBrowser(requireContext(), trimmedText.toString())
} else {
ui.uriLayout.error = getString(R.string.uri_invalid)
}
diff --git a/wallet/src/main/java/net/taler/wallet/Utils.kt b/wallet/src/main/java/net/taler/wallet/Utils.kt
index 388bf61..8b34531 100644
--- a/wallet/src/main/java/net/taler/wallet/Utils.kt
+++ b/wallet/src/main/java/net/taler/wallet/Utils.kt
@@ -16,29 +16,33 @@
package net.taler.wallet
-import android.app.Activity
import android.content.Context
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.NetworkCapabilities
import android.net.NetworkRequest
+import android.net.Uri
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSpecifier
import android.os.Build.VERSION.SDK_INT
+import android.util.TypedValue
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.annotation.RequiresApi
+import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.getSystemService
-import com.google.zxing.integration.android.IntentIntegrator
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.common.Amount
+import net.taler.common.AmountParserException
+import net.taler.common.showError
+import net.taler.common.startActivitySafe
+import net.taler.wallet.backend.TalerErrorInfo
-fun scanQrCode(activity: Activity) {
- IntentIntegrator(activity).apply {
- setPrompt("")
- setBeepEnabled(true)
- setOrientationLocked(false)
- }.initiateScan(listOf(IntentIntegrator.QR_CODE))
-}
+const val CURRENCY_BTC = "BITCOINBTC"
fun connectToWifi(context: Context, ssid: String) {
if (SDK_INT >= 29) {
@@ -92,5 +96,46 @@ private fun connectToWifiDeprecated(context: Context, ssid: String) {
}
fun cleanExchange(exchange: String) = exchange.let {
- if (it.startsWith("https://")) it.substring(8) else it
+ if (it.startsWith("https://", ignoreCase = true)) it.substring(8) else it
}.trimEnd('/')
+
+fun getAmount(currency: String, text: String): Amount? {
+ return try {
+ Amount.fromString(currency, text)
+ } catch (e: AmountParserException) {
+ null
+ }
+}
+
+fun Context.getAttrColor(attr: Int): Int {
+ val value = TypedValue()
+ theme.resolveAttribute(attr, value, true)
+ return value.data
+}
+
+fun launchInAppBrowser(context: Context, url: String) {
+ val builder = CustomTabsIntent.Builder()
+ val intent = builder.build().intent
+ intent.data = Uri.parse(url)
+ context.startActivitySafe(intent)
+}
+
+fun Fragment.showError(error: TalerErrorInfo) {
+ @Suppress("OPT_IN_USAGE")
+ val json = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+ val message = json.encodeToString(error)
+ showError(message)
+}
+
+fun FragmentActivity.showError(error: TalerErrorInfo) {
+ @Suppress("OPT_IN_USAGE")
+ val json = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+ val message = json.encodeToString(error)
+ showError(message)
+}
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt b/wallet/src/main/java/net/taler/wallet/WalletApp.kt
index d1a111f..1384f76 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt
+++ b/wallet/src/main/java/net/taler/wallet/WalletApp.kt
@@ -1,6 +1,6 @@
/*
* This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
+ * (C) 2023 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
@@ -14,11 +14,14 @@
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-package net.taler.wallet.balances
+package net.taler.wallet
-import kotlinx.serialization.Serializable
+import android.app.Application
+import com.google.android.material.color.DynamicColors
-@Serializable
-data class BalanceResponse(
- val balances: List<BalanceItem>
-)
+class WalletApp : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ DynamicColors.applyToActivitiesIfAvailable(this)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/accounts/AccountManager.kt b/wallet/src/main/java/net/taler/wallet/accounts/AccountManager.kt
new file mode 100644
index 0000000..714683c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/accounts/AccountManager.kt
@@ -0,0 +1,54 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received 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.accounts
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import net.taler.wallet.backend.WalletBackendApi
+
+class AccountManager(
+ private val api: WalletBackendApi,
+ private val scope: CoroutineScope,
+) {
+
+ fun listKnownBankAccounts() {
+ scope.launch {
+ val response = api.request("listKnownBankAccounts", KnownBankAccounts.serializer())
+ response.onError {
+ throw AssertionError("Wallet core failed to return known bank accounts!")
+ }.onSuccess { knownBankAccounts ->
+
+ }
+ }
+ }
+
+ fun addKnownBankAccount(paytoUri: String, alias: String, currency: String) {
+ scope.launch {
+ val response = api.request<Unit>("addKnownBankAccounts") {
+ put("payto", paytoUri)
+ put("alias", alias)
+ put("currency", currency)
+ }
+ response.onError {
+ throw AssertionError("Wallet core failed to add known bank account!")
+ }.onSuccess {
+
+ }
+ }
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/accounts/KnownBankAccounts.kt b/wallet/src/main/java/net/taler/wallet/accounts/KnownBankAccounts.kt
new file mode 100644
index 0000000..de8676c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/accounts/KnownBankAccounts.kt
@@ -0,0 +1,113 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.accounts
+
+import android.net.Uri
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonClassDiscriminator
+
+@Serializable
+data class KnownBankAccounts(
+ val accounts: List<KnownBankAccountsInfo>,
+)
+
+@Serializable
+data class KnownBankAccountsInfo(
+ val uri: PaytoUri,
+ @SerialName("kyc_completed")
+ val kycCompleted: Boolean,
+ val currency: String,
+ val alias: String,
+)
+
+@Serializable
+@OptIn(ExperimentalSerializationApi::class)
+@JsonClassDiscriminator("targetType")
+sealed class PaytoUri(
+ val isKnown: Boolean,
+ val targetType: String,
+) {
+ abstract val targetPath: String
+ abstract val params: Map<String, String>
+}
+
+@Serializable
+@SerialName("iban")
+class PaytoUriIban(
+ val iban: String,
+ val bic: String? = "SANDBOXX",
+ override val targetPath: String,
+ override val params: Map<String, String>,
+) : PaytoUri(
+ isKnown = true,
+ targetType = "iban",
+) {
+ val paytoUri: String
+ get() = Uri.Builder()
+ .scheme("payto")
+ .authority(targetType)
+ .apply { if (bic != null) appendPath(bic) }
+ .appendPath(iban)
+ .apply {
+ params.forEach { (key, value) ->
+ appendQueryParameter(key, value)
+ }
+ }
+ .build().toString()
+}
+
+@Serializable
+@SerialName("x-taler-bank")
+class PaytoUriTalerBank(
+ val host: String,
+ val account: String,
+ override val targetPath: String,
+ override val params: Map<String, String>,
+) : PaytoUri(
+ isKnown = true,
+ targetType = "x-taler-bank",
+)
+
+@Serializable
+@SerialName("bitcoin")
+class PaytoUriBitcoin(
+ @SerialName("segwitAddrs")
+ val segwitAddresses: List<String>,
+ override val targetPath: String,
+ override val params: Map<String, String> = emptyMap(),
+) : PaytoUri(
+ isKnown = true,
+ targetType = "bitcoin",
+) {
+ val paytoUri: String
+ get() = Uri.Builder()
+ .scheme("payto")
+ .authority(targetType)
+ .apply {
+ segwitAddresses.forEach { address ->
+ appendPath(address)
+ }
+ }
+ .apply {
+ params.forEach { (key, value) ->
+ appendQueryParameter(key, value)
+ }
+ }
+ .build().toString()
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
new file mode 100644
index 0000000..46eb2f0
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
@@ -0,0 +1,61 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.backend
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonObject
+
+@Serializable
+sealed class ApiMessage {
+
+ @Serializable
+ @SerialName("notification")
+ data class Notification(
+ val payload: NotificationPayload,
+ ) : ApiMessage()
+
+}
+
+@Serializable
+data class NotificationPayload(
+ val type: String,
+ val id: String? = null,
+)
+
+@Serializable
+sealed class ApiResponse : ApiMessage() {
+
+ abstract val id: Int
+ abstract val operation: String
+
+ @Serializable
+ @SerialName("response")
+ data class Response(
+ override val id: Int,
+ override val operation: String,
+ val result: JsonObject,
+ ) : ApiResponse()
+
+ @Serializable
+ @SerialName("error")
+ data class Error(
+ override val id: Int,
+ override val operation: String,
+ val error: JsonObject,
+ ) : ApiResponse()
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
new file mode 100644
index 0000000..1339dca
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
@@ -0,0 +1,94 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.backend
+
+import android.util.Log
+import kotlinx.serialization.json.Json
+import net.taler.qtart.TalerWalletCore
+import net.taler.wallet.BuildConfig
+import org.json.JSONObject
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+
+fun interface NotificationReceiver {
+ fun onNotificationReceived(payload: NotificationPayload)
+}
+
+class BackendManager(
+ private val notificationReceiver: NotificationReceiver,
+) {
+
+ companion object {
+ private const val TAG = "BackendManager"
+ private const val TAG_CORE = "taler-wallet-embedded"
+ val json = Json {
+ ignoreUnknownKeys = true
+ }
+ @JvmStatic
+ private val initialized = AtomicBoolean(false)
+ }
+
+ private val walletCore = TalerWalletCore()
+ private val requestManager = RequestManager()
+
+ init {
+ // TODO using Dagger/Hilt and @Singleton would be nice as well
+ if (initialized.getAndSet(true)) error("Already initialized")
+ walletCore.setMessageHandler { onMessageReceived(it) }
+ walletCore.setCurlHttpClient()
+ if (BuildConfig.DEBUG) walletCore.setStdoutHandler {
+ Log.d(TAG_CORE, it)
+ }
+ }
+
+ fun run() {
+ walletCore.run()
+ }
+
+ suspend fun send(operation: String, args: JSONObject? = null): ApiResponse =
+ suspendCoroutine { cont ->
+ requestManager.addRequest(cont) { id ->
+ val request = JSONObject().apply {
+ put("id", id)
+ put("operation", operation)
+ if (args != null) put("args", args)
+ }
+ Log.d(TAG, "sending message:\n${request.toString(2)}")
+ walletCore.sendRequest(request.toString())
+ }
+ }
+
+ private fun onMessageReceived(msg: String) {
+ Log.d(TAG, "message received: $msg")
+ when (val message = json.decodeFromString<ApiMessage>(msg)) {
+ is ApiMessage.Notification -> {
+ notificationReceiver.onNotificationReceived(message.payload)
+ }
+ is ApiResponse -> {
+ val id = message.id
+ val cont = requestManager.getAndRemoveContinuation(id)
+ if (cont == null) {
+ Log.e(TAG, "wallet returned unknown request ID ($id)")
+ } else {
+ cont.resume(message)
+ }
+ }
+ }
+ }
+}
diff --git a/wallet/src/androidTest/java/net/taler/wallet/ExampleInstrumentedTest.kt b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
index 5f0c423..e9f7fcd 100644
--- a/wallet/src/androidTest/java/net/taler/wallet/ExampleInstrumentedTest.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
@@ -1,6 +1,6 @@
/*
* This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
+ * (C) 2023 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
@@ -14,25 +14,28 @@
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-package net.taler.wallet
+package net.taler.wallet.backend
-import androidx.test.InstrumentationRegistry
-import androidx.test.runner.AndroidJUnit4
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
+import kotlinx.serialization.Serializable
-/**
- * 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)
- }
+@Serializable
+data class InitResponse(
+ val versionInfo: WalletCoreVersion,
+)
+
+fun interface VersionReceiver {
+ fun onVersionReceived(versionInfo: WalletCoreVersion)
}
+
+@Serializable
+data class WalletCoreVersion(
+ val implementationSemver: String,
+ val implementationGitHash: String,
+ val version: String,
+ val exchange: String,
+ val merchant: String,
+ val bankIntegrationApiRange: String,
+ val bankConversionApiRange: String,
+ val corebankApiRange: String,
+ val devMode: Boolean,
+)
diff --git a/wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt b/wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt
new file mode 100644
index 0000000..041656e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt
@@ -0,0 +1,44 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.backend
+
+import androidx.annotation.GuardedBy
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.coroutines.Continuation
+
+class RequestManager {
+
+ @GuardedBy("this")
+ private val contMap = ConcurrentHashMap<Int, Continuation<ApiResponse>>()
+
+ @Volatile
+ @GuardedBy("this")
+ private var currentId = 0
+
+ @Synchronized
+ fun addRequest(cont: Continuation<ApiResponse>, block: (Int) -> Unit) {
+ val id = currentId++
+ contMap[id] = cont
+ block(id)
+ }
+
+ @Synchronized
+ fun getAndRemoveContinuation(id: Int): Continuation<ApiResponse>? {
+ return contMap.remove(id)
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt b/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt
new file mode 100644
index 0000000..2242e33
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt
@@ -0,0 +1,3897 @@
+/*
+ This file is part of GNU Taler
+ Copyright (C) 2012-2020 Taler Systems SA
+
+ GNU Taler is free software: you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation, either version 3 of the License,
+ or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+ SPDX-License-Identifier: LGPL3.0-or-later
+
+ Note: the LGPL does not apply to all components of GNU Taler,
+ but it does apply to this file.
+ */
+package net.taler.wallet.backend
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+@Serializable(TalerErrorCodeSerializer::class)
+enum class TalerErrorCode(val code: Int) {
+
+ UNKNOWN(-1),
+
+
+ /**
+ * Special code to indicate success (no error).
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ NONE(0),
+
+
+ /**
+ * A non-integer error code was returned in the JSON response.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ INVALID(1),
+
+
+ /**
+ * An internal failure happened on the client side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_CLIENT_INTERNAL_ERROR(2),
+
+
+ /**
+ * The response we got from the server was not even in JSON format.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_INVALID_RESPONSE(10),
+
+
+ /**
+ * An operation timed out.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_TIMEOUT(11),
+
+
+ /**
+ * The version string given does not follow the expected CURRENT:REVISION:AGE Format.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_VERSION_MALFORMED(12),
+
+
+ /**
+ * The service responded with a reply that was in JSON but did not satsify the protocol. Note that invalid cryptographic signatures should have signature-specific error codes.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_REPLY_MALFORMED(13),
+
+
+ /**
+ * There is an error in the client-side configuration, for example the base URL specified is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_CONFIGURATION_INVALID(14),
+
+
+ /**
+ * The client made a request to a service, but received an error response it does not know how to handle.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_UNEXPECTED_REQUEST_ERROR(15),
+
+
+ /**
+ * The HTTP method used is invalid for this endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_METHOD_INVALID(20),
+
+
+ /**
+ * There is no endpoint defined for the URL provided by the client.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_ENDPOINT_UNKNOWN(21),
+
+
+ /**
+ * The JSON in the client's request was malformed (generic parse error).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_JSON_INVALID(22),
+
+
+ /**
+ * Some of the HTTP headers provided by the client caused the server to not be able to handle the request.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_HTTP_HEADERS_MALFORMED(23),
+
+
+ /**
+ * The payto:// URI provided by the client is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_PAYTO_URI_MALFORMED(24),
+
+
+ /**
+ * A required parameter in the request was missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_PARAMETER_MISSING(25),
+
+
+ /**
+ * A parameter in the request was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_PARAMETER_MALFORMED(26),
+
+
+ /**
+ * The reserve public key given as part of a /reserves/ endpoint was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_RESERVE_PUB_MALFORMED(27),
+
+
+ /**
+ * The currencies involved in the operation do not match.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_CURRENCY_MISMATCH(30),
+
+
+ /**
+ * The URI is longer than the longest URI the HTTP server is willing to parse.
+ * Returned with an HTTP status code of #MHD_HTTP_URI_TOO_LONG (414).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_URI_TOO_LONG(31),
+
+
+ /**
+ * The body is too large to be permissible for the endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_UPLOAD_EXCEEDS_LIMIT(32),
+
+
+ /**
+ * The service failed initialize its connection to the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_DB_SETUP_FAILED(50),
+
+
+ /**
+ * The service encountered an error event to just start the database transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_DB_START_FAILED(51),
+
+
+ /**
+ * The service failed to store information in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_DB_STORE_FAILED(52),
+
+
+ /**
+ * The service failed to fetch information from its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_DB_FETCH_FAILED(53),
+
+
+ /**
+ * The service encountered an error event to commit the database transaction (hard, unrecoverable error).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_DB_COMMIT_FAILED(54),
+
+
+ /**
+ * The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. (This indicates a repeated serialization error; should only happen if some client maliciously tries to create conflicting concurrent transactions.)
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_DB_SOFT_FAILURE(55),
+
+
+ /**
+ * The service's database is inconsistent and violates service-internal invariants.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_DB_INVARIANT_FAILURE(56),
+
+
+ /**
+ * The HTTP server experienced an internal invariant failure (bug).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_INTERNAL_INVARIANT_FAILURE(60),
+
+
+ /**
+ * The service could not compute a cryptographic hash over some JSON value.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_FAILED_COMPUTE_JSON_HASH(61),
+
+
+ /**
+ * The service could not compute an amount.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_FAILED_COMPUTE_AMOUNT(62),
+
+
+ /**
+ * The HTTP server had insufficient memory to parse the request.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_PARSER_OUT_OF_MEMORY(70),
+
+
+ /**
+ * The HTTP server failed to allocate memory.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_ALLOCATION_FAILURE(71),
+
+
+ /**
+ * The HTTP server failed to allocate memory for building JSON reply.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_JSON_ALLOCATION_FAILURE(72),
+
+
+ /**
+ * The HTTP server failed to allocate memory for making a CURL request.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_CURL_ALLOCATION_FAILURE(73),
+
+
+ /**
+ * The backend could not locate a required template to generate an HTML reply.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_FAILED_TO_LOAD_TEMPLATE(74),
+
+
+ /**
+ * The backend could not expand the template to generate an HTML reply.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_FAILED_TO_EXPAND_TEMPLATE(75),
+
+
+ /**
+ * Exchange is badly configured and thus cannot operate.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_BAD_CONFIGURATION(1000),
+
+
+ /**
+ * Operation specified unknown for this endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_OPERATION_UNKNOWN(1001),
+
+
+ /**
+ * The number of segments included in the URI does not match the number of segments expected by the endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS(1002),
+
+
+ /**
+ * The same coin was already used with a different denomination previously.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY(1003),
+
+
+ /**
+ * The public key of given to a "/coins/" endpoint of the exchange was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB(1004),
+
+
+ /**
+ * The exchange is not aware of the denomination key the wallet requested for the operation.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN(1005),
+
+
+ /**
+ * The signature of the denomination key over the coin is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DENOMINATION_SIGNATURE_INVALID(1006),
+
+
+ /**
+ * The exchange failed to perform the operation as it could not find the private keys. This is a problem with the exchange setup, not with the client's request.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KEYS_MISSING(1007),
+
+
+ /**
+ * Validity period of the denomination lies in the future.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE(1008),
+
+
+ /**
+ * Denomination key of the coin is past its expiration time for the requested operation.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_DENOMINATION_EXPIRED(1009),
+
+
+ /**
+ * Denomination key of the coin has been revoked.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_DENOMINATION_REVOKED(1010),
+
+
+ /**
+ * An operation where the exchange interacted with a security module timed out.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_SECMOD_TIMEOUT(1011),
+
+
+ /**
+ * The respective coin did not have sufficient residual value for the operation. The "history" in this response provides the "residual_value" of the coin, which may be less than its "original_value".
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_INSUFFICIENT_FUNDS(1012),
+
+
+ /**
+ * The exchange had an internal error reconstructing the transaction history of the coin that was being processed.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_COIN_HISTORY_COMPUTATION_FAILED(1013),
+
+
+ /**
+ * The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS(1014),
+
+
+ /**
+ * The same coin was already used with a different age hash previously.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH(1015),
+
+
+ /**
+ * The requested operation is not valid for the cipher used by the selected denomination.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION(1016),
+
+
+ /**
+ * The provided arguments for the operation use inconsistent ciphers.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_CIPHER_MISMATCH(1017),
+
+
+ /**
+ * The number of denominations specified in the request exceeds the limit of the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE(1018),
+
+
+ /**
+ * The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_CLOCK_SKEW(1020),
+
+
+ /**
+ * The specified amount for the coin is higher than the value of the denomination of the coin.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE(1021),
+
+
+ /**
+ * The exchange was not properly configured with global fees.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_GLOBAL_FEES_MISSING(1022),
+
+
+ /**
+ * The exchange was not properly configured with wire fees.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_WIRE_FEES_MISSING(1023),
+
+
+ /**
+ * The purse public key was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_PURSE_PUB_MALFORMED(1024),
+
+
+ /**
+ * The purse is unknown.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_PURSE_UNKNOWN(1025),
+
+
+ /**
+ * The purse has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_PURSE_EXPIRED(1026),
+
+
+ /**
+ * The exchange has no information about the "reserve_pub" that was given.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_RESERVE_UNKNOWN(1027),
+
+
+ /**
+ * The exchange is not allowed to proceed with the operation until the client has satisfied a KYC check.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KYC_REQUIRED(1028),
+
+
+ /**
+ * Inconsistency between provided age commitment and attest: either none or both must be provided
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DEPOSIT_COIN_CONFLICTING_ATTEST_VS_AGE_COMMITMENT(1029),
+
+
+ /**
+ * The provided attestation for the minimum age couldn't be verified by the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DEPOSIT_COIN_AGE_ATTESTATION_FAILURE(1030),
+
+
+ /**
+ * The purse was deleted.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_PURSE_DELETED(1031),
+
+
+ /**
+ * The exchange did not find information about the specified transaction in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSITS_GET_NOT_FOUND(1100),
+
+
+ /**
+ * The wire hash of given to a "/deposits/" handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE(1101),
+
+
+ /**
+ * The merchant key of given to a "/deposits/" handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB(1102),
+
+
+ /**
+ * The hash of the contract terms given to a "/deposits/" handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS(1103),
+
+
+ /**
+ * The coin public key of given to a "/deposits/" handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB(1104),
+
+
+ /**
+ * The signature returned by the exchange in a /deposits/ request was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE(1105),
+
+
+ /**
+ * The signature of the merchant is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID(1106),
+
+
+ /**
+ * The provided policy data was not accepted
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED(1107),
+
+
+ /**
+ * The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS(1150),
+
+
+ /**
+ * The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW(1152),
+
+
+ /**
+ * The exchange failed to create the signature using the denomination key.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_SIGNATURE_FAILED(1153),
+
+
+ /**
+ * The signature of the reserve is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID(1154),
+
+
+ /**
+ * When computing the reserve history, we ended up with a negative overall balance, which should be impossible.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVE_HISTORY_ERROR_INSUFFICIENT_FUNDS(1155),
+
+
+ /**
+ * The reserve did not have sufficient funds in it to pay for a full reserve history statement.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GET_RESERVE_HISTORY_ERROR_INSUFFICIENT_BALANCE(1156),
+
+
+ /**
+ * Withdraw period of the coin to be withdrawn is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_DENOMINATION_KEY_LOST(1158),
+
+
+ /**
+ * The client failed to unblind the blind signature.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_UNBLIND_FAILURE(1159),
+
+
+ /**
+ * The client re-used a withdraw nonce, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_NONCE_REUSE(1160),
+
+
+ /**
+ * The batch withdraw included a planchet that was already withdrawn. This is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET(1175),
+
+
+ /**
+ * The signature made by the coin over the deposit permission is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID(1205),
+
+
+ /**
+ * The same coin was already deposited for the same merchant and contract with other details.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT(1206),
+
+
+ /**
+ * The stated value of the coin after the deposit fee is subtracted would be negative.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE(1207),
+
+
+ /**
+ * The stated refund deadline is after the wire deadline.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE(1208),
+
+
+ /**
+ * The stated wire deadline is "never", which makes no sense.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER(1209),
+
+
+ /**
+ * The exchange failed to canonicalize and hash the given wire format. For example, the merchant failed to provide the "salt" or a valid payto:// URI in the wire details. Note that while the exchange will do some basic sanity checking on the wire details, it cannot warrant that the banking system will ultimately be able to route to the specified address, even if this check passed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_JSON(1210),
+
+
+ /**
+ * The hash of the given wire address does not match the wire hash specified in the proposal data.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_CONTRACT_HASH_CONFLICT(1211),
+
+
+ /**
+ * The signature provided by the exchange is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE(1221),
+
+
+ /**
+ * The deposited amount is smaller than the deposit fee, which would result in a negative contribution.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT(1222),
+
+
+ /**
+ * The proof of policy fulfillment was invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_EXTENSIONS_INVALID_FULFILLMENT(1240),
+
+
+ /**
+ * The reserve balance, status or history was requested for a reserve which is not known to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_STATUS_UNKNOWN(1250),
+
+
+ /**
+ * The reserve status was requested with a bad signature.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_STATUS_BAD_SIGNATURE(1251),
+
+
+ /**
+ * The reserve history was requested with a bad signature.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_HISTORY_BAD_SIGNATURE(1252),
+
+
+ /**
+ * The exchange encountered melt fees exceeding the melted coin's contribution.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION(1302),
+
+
+ /**
+ * The signature made with the coin to be melted is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MELT_COIN_SIGNATURE_INVALID(1303),
+
+
+ /**
+ * The denomination of the given coin has past its expiration date and it is also not a valid zombie (that is, was not refreshed with the fresh coin being subjected to recoup).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE(1305),
+
+
+ /**
+ * The signature returned by the exchange in a melt request was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE(1306),
+
+
+ /**
+ * The provided transfer keys do not match up with the original commitment. Information about the original commitment is included in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_COMMITMENT_VIOLATION(1353),
+
+
+ /**
+ * Failed to produce the blinded signatures over the coins to be returned.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_SIGNING_ERROR(1354),
+
+
+ /**
+ * The exchange is unaware of the refresh session specified in the request.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN(1355),
+
+
+ /**
+ * The size of the cut-and-choose dimension of the private transfer keys request does not match #TALER_CNC_KAPPA - 1.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID(1356),
+
+
+ /**
+ * The number of envelopes given does not match the number of denomination keys given.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_MISMATCH(1358),
+
+
+ /**
+ * The exchange encountered a numeric overflow totaling up the cost for the refresh operation.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW(1359),
+
+
+ /**
+ * The exchange's cost calculation shows that the melt amount is below the costs of the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_AMOUNT_INSUFFICIENT(1360),
+
+
+ /**
+ * The signature made with the coin over the link data is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID(1361),
+
+
+ /**
+ * The refresh session hash given to a /refreshes/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_INVALID_RCH(1362),
+
+
+ /**
+ * Operation specified invalid for this endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_OPERATION_INVALID(1363),
+
+
+ /**
+ * The client provided age commitment data, but age restriction is not supported on this server.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_NOT_SUPPORTED(1364),
+
+
+ /**
+ * The client provided invalid age commitment data: missing, not an array, or array of invalid size.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID(1365),
+
+
+ /**
+ * The coin specified in the link request is unknown to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_LINK_COIN_UNKNOWN(1400),
+
+
+ /**
+ * The public key of given to a /transfers/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_TRANSFERS_GET_WTID_MALFORMED(1450),
+
+
+ /**
+ * The exchange did not find information about the specified wire transfer identifier in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND(1451),
+
+
+ /**
+ * The exchange did not find information about the wire transfer fees it charged.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND(1452),
+
+
+ /**
+ * The exchange found a wire fee that was above the total transfer value (and thus could not have been charged).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT(1453),
+
+
+ /**
+ * The wait target of the URL was not in the set of expected values.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSES_INVALID_WAIT_TARGET(1475),
+
+
+ /**
+ * The signature on the purse status returned by the exchange was invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSES_GET_INVALID_SIGNATURE_BY_EXCHANGE(1476),
+
+
+ /**
+ * The exchange knows literally nothing about the coin we were asked to refund. But without a transaction history, we cannot issue a refund. This is kind-of OK, the owner should just refresh it directly without executing the refund.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_COIN_NOT_FOUND(1500),
+
+
+ /**
+ * We could not process the refund request as the coin's transaction history does not permit the requested refund because then refunds would exceed the deposit amount. The "history" in the response proves this.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT(1501),
+
+
+ /**
+ * The exchange knows about the coin we were asked to refund, but not about the specific /deposit operation. Hence, we cannot issue a refund (as we do not know if this merchant public key is authorized to do a refund).
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_DEPOSIT_NOT_FOUND(1502),
+
+
+ /**
+ * The exchange can no longer refund the customer/coin as the money was already transferred (paid out) to the merchant. (It should be past the refund deadline.)
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_MERCHANT_ALREADY_PAID(1503),
+
+
+ /**
+ * The refund fee specified for the request is lower than the refund fee charged by the exchange for the given denomination key of the refunded coin.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_FEE_TOO_LOW(1504),
+
+
+ /**
+ * The refunded amount is smaller than the refund fee, which would result in a negative refund.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_FEE_ABOVE_AMOUNT(1505),
+
+
+ /**
+ * The signature of the merchant is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID(1506),
+
+
+ /**
+ * Merchant backend failed to create the refund confirmation signature.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_MERCHANT_SIGNING_FAILED(1507),
+
+
+ /**
+ * The signature returned by the exchange in a refund request was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE(1508),
+
+
+ /**
+ * The failure proof returned by the exchange is incorrect.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE(1509),
+
+
+ /**
+ * Conflicting refund granted before with different amount but same refund transaction ID.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_INCONSISTENT_AMOUNT(1510),
+
+
+ /**
+ * The given coin signature is invalid for the request.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_SIGNATURE_INVALID(1550),
+
+
+ /**
+ * The exchange could not find the corresponding withdraw operation. The request is denied.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND(1551),
+
+
+ /**
+ * The coin's remaining balance is zero. The request is denied.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_COIN_BALANCE_ZERO(1552),
+
+
+ /**
+ * The exchange failed to reproduce the coin's blinding.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_BLINDING_FAILED(1553),
+
+
+ /**
+ * The coin's remaining balance is zero. The request is denied.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_COIN_BALANCE_NEGATIVE(1554),
+
+
+ /**
+ * The coin's denomination has not been revoked yet.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_NOT_ELIGIBLE(1555),
+
+
+ /**
+ * The given coin signature is invalid for the request.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID(1575),
+
+
+ /**
+ * The exchange could not find the corresponding melt operation. The request is denied.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND(1576),
+
+
+ /**
+ * The exchange failed to reproduce the coin's blinding.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED(1578),
+
+
+ /**
+ * The coin's denomination has not been revoked yet.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE(1580),
+
+
+ /**
+ * This exchange does not allow clients to request /keys for times other than the current (exchange) time.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KEYS_TIMETRAVEL_FORBIDDEN(1600),
+
+
+ /**
+ * A signature in the server's response was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WIRE_SIGNATURE_INVALID(1650),
+
+
+ /**
+ * No bank accounts are enabled for the exchange. The administrator should enable-account using the taler-exchange-offline tool.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED(1651),
+
+
+ /**
+ * The payto:// URI stored in the exchange database for its bank account is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED(1652),
+
+
+ /**
+ * No wire fees are configured for an enabled wire method of the exchange. The administrator must set the wire-fee using the taler-exchange-offline tool.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WIRE_FEES_NOT_CONFIGURED(1653),
+
+
+ /**
+ * This purse was previously created with different meta data.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA(1675),
+
+
+ /**
+ * This purse was previously merged with different meta data.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA(1676),
+
+
+ /**
+ * The reserve has insufficient funds to create another purse.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS(1677),
+
+
+ /**
+ * The purse fee specified for the request is lower than the purse fee charged by the exchange at this time.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW(1678),
+
+
+ /**
+ * The payment request cannot be deleted anymore, as it either already completed or timed out.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DELETE_ALREADY_DECIDED(1679),
+
+
+ /**
+ * The signature affirming the purse deletion is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID(1680),
+
+
+ /**
+ * The exchange failed to talk to the process responsible for its private denomination keys.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE(1700),
+
+
+ /**
+ * The response from the denomination key helper process was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DENOMINATION_HELPER_BUG(1701),
+
+
+ /**
+ * The helper refuses to sign with the key, because it is too early: the validity period has not yet started.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DENOMINATION_HELPER_TOO_EARLY(1702),
+
+
+ /**
+ * The signature of the exchange on the reply was invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID(1725),
+
+
+ /**
+ * The exchange failed to talk to the process responsible for its private signing keys.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE(1750),
+
+
+ /**
+ * The response from the online signing key helper process was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_SIGNKEY_HELPER_BUG(1751),
+
+
+ /**
+ * The helper refuses to sign with the key, because it is too early: the validity period has not yet started.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_SIGNKEY_HELPER_TOO_EARLY(1752),
+
+
+ /**
+ * The purse expiration time is in the past at the time of its creation.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW(1775),
+
+
+ /**
+ * The purse expiration time is set to never, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER(1776),
+
+
+ /**
+ * The signature affirming the merge of the purse is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID(1777),
+
+
+ /**
+ * The signature by the reserve affirming the merge is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID(1778),
+
+
+ /**
+ * The signature by the reserve affirming the open operation is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE(1785),
+
+
+ /**
+ * The signature by the reserve affirming the close operation is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE(1786),
+
+
+ /**
+ * The signature by the reserve affirming the attestion request is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE(1787),
+
+
+ /**
+ * The exchange does not know an origin account to which the remaining reserve balance could be wired to, and the wallet failed to provide one.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT(1788),
+
+
+ /**
+ * The reserve balance is insufficient to pay for the open operation.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS(1789),
+
+
+ /**
+ * The auditor that was supposed to be disabled is unknown to this exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_AUDITOR_NOT_FOUND(1800),
+
+
+ /**
+ * The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected).
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT(1801),
+
+
+ /**
+ * The signature to add or enable the auditor does not validate.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_AUDITOR_ADD_SIGNATURE_INVALID(1802),
+
+
+ /**
+ * The signature to disable the auditor does not validate.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_AUDITOR_DEL_SIGNATURE_INVALID(1803),
+
+
+ /**
+ * The signature to revoke the denomination does not validate.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_DENOMINATION_REVOKE_SIGNATURE_INVALID(1804),
+
+
+ /**
+ * The signature to revoke the online signing key does not validate.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_SIGNKEY_REVOKE_SIGNATURE_INVALID(1805),
+
+
+ /**
+ * The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected).
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT(1806),
+
+
+ /**
+ * The signingkey specified is unknown to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN(1807),
+
+
+ /**
+ * The signature to publish wire account does not validate.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_WIRE_DETAILS_SIGNATURE_INVALID(1808),
+
+
+ /**
+ * The signature to add the wire account does not validate.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID(1809),
+
+
+ /**
+ * The signature to disable the wire account does not validate.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_WIRE_DEL_SIGNATURE_INVALID(1810),
+
+
+ /**
+ * The wire account to be disabled is unknown to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_WIRE_NOT_FOUND(1811),
+
+
+ /**
+ * The signature to affirm wire fees does not validate.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_WIRE_FEE_SIGNATURE_INVALID(1812),
+
+
+ /**
+ * The signature conflicts with a previous signature affirming different fees.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_WIRE_FEE_MISMATCH(1813),
+
+
+ /**
+ * The signature affirming the denomination key is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID(1814),
+
+
+ /**
+ * The signature affirming the signing key is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID(1815),
+
+
+ /**
+ * The signature conflicts with a previous signature affirming different fees.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH(1816),
+
+
+ /**
+ * The signature affirming the fee structure is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID(1817),
+
+
+ /**
+ * The signature affirming the profit drain is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID(1818),
+
+
+ /**
+ * The purse was previously created with different meta data.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA(1850),
+
+
+ /**
+ * The purse was previously created with a different contract.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT_STORED(1851),
+
+
+ /**
+ * A coin signature for a deposit into the purse is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID(1852),
+
+
+ /**
+ * The purse expiration time is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW(1853),
+
+
+ /**
+ * The purse expiration time is "never".
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER(1854),
+
+
+ /**
+ * The purse signature over the purse meta data is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID(1855),
+
+
+ /**
+ * The signature over the encrypted contract is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID(1856),
+
+
+ /**
+ * The signature from the exchange over the confirmation is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID(1857),
+
+
+ /**
+ * The coin was previously deposited with different meta data.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA(1858),
+
+
+ /**
+ * The encrypted contract was previously uploaded with different meta data.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA(1859),
+
+
+ /**
+ * The deposited amount is less than the purse fee.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE(1860),
+
+
+ /**
+ * The signature using the merge key is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE(1876),
+
+
+ /**
+ * The signature using the reserve key is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE(1877),
+
+
+ /**
+ * The targeted purse is not yet full and thus cannot be merged. Retrying the request later may succeed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_NOT_FULL(1878),
+
+
+ /**
+ * The signature from the exchange over the confirmation is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID(1879),
+
+
+ /**
+ * The exchange of the target account is not a partner of this exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN(1880),
+
+
+ /**
+ * The auditor signature over the denomination meta data is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID(1900),
+
+
+ /**
+ * The auditor that was specified is unknown to this exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AUDITORS_AUDITOR_UNKNOWN(1901),
+
+
+ /**
+ * The auditor that was specified is no longer used by this exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AUDITORS_AUDITOR_INACTIVE(1902),
+
+
+ /**
+ * The signature affirming the wallet's KYC request was invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_WALLET_SIGNATURE_INVALID(1925),
+
+
+ /**
+ * The exchange received an unexpected malformed response from its KYC backend.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE(1926),
+
+
+ /**
+ * The backend signaled an unexpected failure.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_PROOF_BACKEND_ERROR(1927),
+
+
+ /**
+ * The backend signaled an authorization failure.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_PROOF_BACKEND_AUTHORIZATION_FAILED(1928),
+
+
+ /**
+ * The exchange is unaware of having made an the authorization request.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN(1929),
+
+
+ /**
+ * The payto-URI hash did not match. Hence the request was denied.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED(1930),
+
+
+ /**
+ * The request used a logic specifier that is not known to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN(1931),
+
+
+ /**
+ * The request requires a logic which is no longer configured at the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_LOGIC_GONE(1932),
+
+
+ /**
+ * The logic plugin had a bug in its interaction with the KYC provider.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_LOGIC_BUG(1933),
+
+
+ /**
+ * The exchange could not process the request with its KYC provider because the provider refused access to the service. This indicates some configuration issue at the Taler exchange operator.
+ * Returned with an HTTP status code of #MHD_HTTP_NETWORK_AUTHENTICATION_REQUIRED (511).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED(1934),
+
+
+ /**
+ * There was a timeout in the interaction between the exchange and the KYC provider. The most likely cause is some networking problem. Trying again later might succeed.
+ * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT(1935),
+
+
+ /**
+ * The KYC provider responded with a status that was completely unexpected by the KYC logic of the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY(1936),
+
+
+ /**
+ * The rate limit of the exchange at the KYC provider has been exceeded. Trying much later might work.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED(1937),
+
+
+ /**
+ * The request to the webhook lacked proper authorization or authentication data.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED(1938),
+
+
+ /**
+ * The exchange does not know a contract under the given contract public key.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_CONTRACTS_UNKNOWN(1950),
+
+
+ /**
+ * The URL does not encode a valid exchange public key in its path.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB(1951),
+
+
+ /**
+ * The returned encrypted contract did not decrypt.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_CONTRACTS_DECRYPTION_FAILED(1952),
+
+
+ /**
+ * The signature on the encrypted contract did not validate.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_CONTRACTS_SIGNATURE_INVALID(1953),
+
+
+ /**
+ * The decrypted contract was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_CONTRACTS_DECODING_FAILED(1954),
+
+
+ /**
+ * A coin signature for a deposit into the purse is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DEPOSIT_COIN_SIGNATURE_INVALID(1975),
+
+
+ /**
+ * It is too late to deposit coins into the purse.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY(1976),
+
+
+ /**
+ * The backend could not find the merchant instance specified in the request.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_INSTANCE_UNKNOWN(2000),
+
+
+ /**
+ * The start and end-times in the wire fee structure leave a hole. This is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE(2001),
+
+
+ /**
+ * The merchant was unable to obtain a valid answer to /wire from the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED(2002),
+
+
+ /**
+ * The proposal is not known to the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_ORDER_UNKNOWN(2005),
+
+
+ /**
+ * The order provided to the backend could not be completed, because a product to be completed via inventory data is not actually in our inventory.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_PRODUCT_UNKNOWN(2006),
+
+
+ /**
+ * The tip ID is unknown. This could happen if the tip has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_TIP_ID_UNKNOWN(2007),
+
+
+ /**
+ * The contract obtained from the merchant backend was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID(2008),
+
+
+ /**
+ * The order we found does not match the provided contract hash.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER(2009),
+
+
+ /**
+ * The exchange failed to provide a valid response to the merchant's /keys request.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE(2010),
+
+
+ /**
+ * The exchange failed to respond to the merchant on time.
+ * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_TIMEOUT(2011),
+
+
+ /**
+ * The merchant failed to talk to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE(2012),
+
+
+ /**
+ * The exchange returned a maformed response.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED(2013),
+
+
+ /**
+ * The exchange returned an unexpected response status.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS(2014),
+
+
+ /**
+ * The merchant refused the request due to lack of authorization.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_UNAUTHORIZED(2015),
+
+
+ /**
+ * The merchant instance specified in the request was deleted.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_INSTANCE_DELETED(2016),
+
+
+ /**
+ * The backend could not find the inbound wire transfer specified in the request.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_TRANSFER_UNKNOWN(2017),
+
+
+ /**
+ * The backend could not find the template(id) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_TEMPLATE_UNKNOWN(2018),
+
+
+ /**
+ * The backend could not find the webhook(id) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_WEBHOOK_UNKNOWN(2019),
+
+
+ /**
+ * The backend could not find the webhook(serial) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_PENDING_WEBHOOK_UNKNOWN(2020),
+
+
+ /**
+ * The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE(2100),
+
+
+ /**
+ * The merchant backend failed to construct the request for tracking to the exchange, thus tracking details are not in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE(2103),
+
+
+ /**
+ * The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE(2104),
+
+
+ /**
+ * The claim token used to authenticate the client is invalid for this order.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GET_ORDERS_ID_INVALID_TOKEN(2105),
+
+
+ /**
+ * The contract terms hash used to authenticate the client is invalid for this order.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH(2106),
+
+
+ /**
+ * The exchange responded saying that funds were insufficient (for example, due to double-spending).
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS(2150),
+
+
+ /**
+ * The denomination key used for payment is not listed among the denomination keys of the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND(2151),
+
+
+ /**
+ * The denomination key used for payment is not audited by an auditor approved by the merchant.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_AUDITOR_FAILURE(2152),
+
+
+ /**
+ * There was an integer overflow totaling up the amounts or deposit fees in the payment.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW(2153),
+
+
+ /**
+ * The deposit fees exceed the total value of the payment.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT(2154),
+
+
+ /**
+ * After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES(2155),
+
+
+ /**
+ * Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT(2156),
+
+
+ /**
+ * The signature over the contract of one of the coins was invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_COIN_SIGNATURE_INVALID(2157),
+
+
+ /**
+ * When we tried to find information about the exchange to issue the deposit, we failed. This usually only happens if the merchant backend is somehow unable to get its own HTTP client logic to work.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED(2158),
+
+
+ /**
+ * The refund deadline in the contract is after the transfer deadline.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE(2159),
+
+
+ /**
+ * The order was already paid (maybe by another wallet).
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID(2160),
+
+
+ /**
+ * The payment is too late, the offer has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED(2161),
+
+
+ /**
+ * The "merchant" field is missing in the proposal data. This is an internal error as the proposal is from the merchant's own database at this point.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING(2162),
+
+
+ /**
+ * Failed to locate merchant's account information matching the wire hash given in the proposal.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN(2163),
+
+
+ /**
+ * The deposit time for the denomination has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED(2165),
+
+
+ /**
+ * The exchange of the deposited coin charges a wire fee that could not be added to the total (total amount too high).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED(2166),
+
+
+ /**
+ * The contract was not fully paid because of refunds. Note that clients MAY treat this as paid if, for example, contracts must be executed despite of refunds.
+ * Returned with an HTTP status code of #MHD_HTTP_PAYMENT_REQUIRED (402).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_REFUNDED(2167),
+
+
+ /**
+ * According to our database, we have refunded more than we were paid (which should not be possible).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS(2168),
+
+
+ /**
+ * Legacy stuff. Remove me with protocol v1.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEAD_QQQ_PAY_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE(2169),
+
+
+ /**
+ * The payment failed at the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED(2170),
+
+
+ /**
+ * The payment required a minimum age but one of the coins (of a denomination with support for age restriction) did not provide any age_commitment.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING(2171),
+
+
+ /**
+ * The payment required a minimum age but one of the coins provided an age_commitment that contained a wrong number of public keys compared to the number of age groups defined in the denomination of the coin.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH(2172),
+
+
+ /**
+ * The payment required a minimum age but one of the coins provided a minimum_age_sig that couldn't be verified with the given age_commitment for that particular minimum age.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED(2173),
+
+
+ /**
+ * The payment required no minimum age but one of the coins (of a denomination with support for age restriction) did not provide the required h_age_commitment.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING(2174),
+
+
+ /**
+ * The contract hash does not match the given order ID.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAID_CONTRACT_HASH_MISMATCH(2200),
+
+
+ /**
+ * The signature of the merchant is not valid for the given contract hash.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID(2201),
+
+
+ /**
+ * The merchant failed to send the exchange the refund request.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED(2251),
+
+
+ /**
+ * The merchant failed to find the exchange to process the lookup.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_LOOKUP_FAILED(2252),
+
+
+ /**
+ * The merchant could not find the contract.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND(2253),
+
+
+ /**
+ * The payment was already completed and thus cannot be aborted anymore.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE(2254),
+
+
+ /**
+ * The hash provided by the wallet does not match the order.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH(2255),
+
+
+ /**
+ * The array of coins cannot be empty.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY(2256),
+
+
+ /**
+ * We could not claim the order because the backend is unaware of it.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND(2300),
+
+
+ /**
+ * We could not claim the order because someone else claimed it first.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED(2301),
+
+
+ /**
+ * The client-side experienced an internal failure.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_CLAIM_CLIENT_INTERNAL_FAILURE(2302),
+
+
+ /**
+ * The backend failed to sign the refund request.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_REFUND_SIGNATURE_FAILED(2350),
+
+
+ /**
+ * The client failed to unblind the signature returned by the merchant.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_TIP_PICKUP_UNBLIND_FAILURE(2400),
+
+
+ /**
+ * The exchange returned a failure code for the withdraw operation.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_TIP_PICKUP_EXCHANGE_ERROR(2403),
+
+
+ /**
+ * The merchant failed to add up the amounts to compute the pick up value.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_TIP_PICKUP_SUMMATION_FAILED(2404),
+
+
+ /**
+ * The tip expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_TIP_PICKUP_HAS_EXPIRED(2405),
+
+
+ /**
+ * The requested withdraw amount exceeds the amount remaining to be picked up.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_TIP_PICKUP_AMOUNT_EXCEEDS_TIP_REMAINING(2406),
+
+
+ /**
+ * The merchant did not find the specified denomination key in the exchange's key set.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_TIP_PICKUP_DENOMINATION_UNKNOWN(2407),
+
+
+ /**
+ * The backend lacks a wire transfer method configuration option for the given instance. Thus, this instance is unavailable (not findable for creating new orders).
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE(2500),
+
+
+ /**
+ * The proposal had no timestamp and the backend failed to obtain the local time. Likely to be an internal error.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME(2501),
+
+
+ /**
+ * The order provided to the backend could not be parsed, some required fields were missing or ill-formed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR(2502),
+
+
+ /**
+ * The backend encountered an error: the proposal already exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS(2503),
+
+
+ /**
+ * The request is invalid: the wire deadline is before the refund deadline.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE(2504),
+
+
+ /**
+ * The request is invalid: a delivery date was given, but it is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST(2505),
+
+
+ /**
+ * The request is invalid: the wire deadline for the order would be "never".
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER(2506),
+
+
+ /**
+ * One of the paths to forget is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT(2510),
+
+
+ /**
+ * One of the paths to forget was not marked as forgettable.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE(2511),
+
+
+ /**
+ * The order provided to the backend could not be deleted, our offer is still valid and awaiting payment.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT(2520),
+
+
+ /**
+ * The order provided to the backend could not be deleted as the order was already paid.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID(2521),
+
+
+ /**
+ * The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it is too big to be paid back. In this second case, the fault stays on the business dept. side.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT(2530),
+
+
+ /**
+ * The frontend gave an unpaid order id to issue the refund to.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID(2531),
+
+
+ /**
+ * The refund delay was set to 0 and thus no refunds are allowed for this order.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT(2532),
+
+
+ /**
+ * The exchange says it does not know this transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TRANSFERS_EXCHANGE_UNKNOWN(2550),
+
+
+ /**
+ * We internally failed to execute the /track/transfer request.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TRANSFERS_REQUEST_ERROR(2551),
+
+
+ /**
+ * The amount transferred differs between what was submitted and what the exchange claimed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS(2552),
+
+
+ /**
+ * The exchange gave conflicting information about a coin which has been wire transferred.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS(2553),
+
+
+ /**
+ * The exchange charged a different wire fee than what it originally advertised, and it is higher.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE(2554),
+
+
+ /**
+ * We did not find the account that the transfer was made to.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND(2555),
+
+
+ /**
+ * The backend could not delete the transfer as the echange already replied to our inquiry about it and we have integrated the result.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED(2556),
+
+
+ /**
+ * The backend was previously informed about a wire transfer with the same ID but a different amount. Multiple wire transfers with the same ID are not allowed. If the new amount is correct, the old transfer should first be deleted.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION(2557),
+
+
+ /**
+ * The merchant backend cannot create an instance under the given identifier as one already exists. Use PATCH to modify the existing entry.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS(2600),
+
+
+ /**
+ * The merchant backend cannot create an instance because the authentication configuration field is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_INSTANCES_BAD_AUTH(2601),
+
+
+ /**
+ * The merchant backend cannot update an instance's authentication settings because the provided authentication settings are malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH(2602),
+
+
+ /**
+ * The merchant backend cannot create an instance under the given identifier, the previous one was deleted but must be purged first.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED(2603),
+
+
+ /**
+ * The merchant backend cannot update an instance under the given identifier, the previous one was deleted but must be purged first.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED(2625),
+
+
+ /**
+ * The product ID exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS(2650),
+
+
+ /**
+ * The update would have reduced the total amount of product lost, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED(2660),
+
+
+ /**
+ * The update would have mean that more stocks were lost than what remains from total inventory after sales, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS(2661),
+
+
+ /**
+ * The update would have reduced the total amount of product in stock, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED(2662),
+
+
+ /**
+ * The update would have reduced the total amount of product sold, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED(2663),
+
+
+ /**
+ * The lock request is for more products than we have left (unlocked) in stock.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS(2670),
+
+
+ /**
+ * The deletion request is for a product that is locked.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_DELETE_PRODUCTS_CONFLICTING_LOCK(2680),
+
+
+ /**
+ * The requested wire method is not supported by the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_RESERVES_UNSUPPORTED_WIRE_METHOD(2700),
+
+
+ /**
+ * The reserve could not be deleted because it is unknown.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_DELETE_RESERVES_NO_SUCH_RESERVE(2710),
+
+
+ /**
+ * The reserve that was used to fund the tips has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_RESERVE_EXPIRED(2750),
+
+
+ /**
+ * The reserve that was used to fund the tips was not found in the DB.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_RESERVE_UNKNOWN(2751),
+
+
+ /**
+ * The backend knows the instance that was supposed to support the tip, and it was configured for tipping. However, the funds remaining are insufficient to cover the tip, and the merchant should top up the reserve.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_INSUFFICIENT_FUNDS(2752),
+
+
+ /**
+ * The backend failed to find a reserve needed to authorize the tip.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_RESERVE_NOT_FOUND(2753),
+
+
+ /**
+ * The merchant backend encountered a failure in computing the deposit total.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE(2800),
+
+
+ /**
+ * The template ID already exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TEMPLATES_CONFLICT_TEMPLATE_EXISTS(2850),
+
+
+ /**
+ * Amount given in the using template and in the template contract. There is a conflict.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT(2860),
+
+
+ /**
+ * Subject given in the using template and in the template contract. There is a conflict.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT(2861),
+
+
+ /**
+ * Amount not given in the using template and in the template contract. There is a conflict.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT(2862),
+
+
+ /**
+ * Subject not given in the using template and in the template contract. There is a conflict.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY(2863),
+
+
+ /**
+ * The webhook ID elready exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_WEBHOOKS_CONFLICT_WEBHOOK_EXISTS(2900),
+
+
+ /**
+ * The webhook serial elready exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_PENDING_WEBHOOKS_CONFLICT_PENDING_WEBHOOK_EXISTS(2910),
+
+
+ /**
+ * The signature from the exchange on the deposit confirmation is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_DEPOSIT_CONFIRMATION_SIGNATURE_INVALID(3100),
+
+
+ /**
+ * The exchange key used for the signature on the deposit confirmation was revoked.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_EXCHANGE_SIGNING_KEY_REVOKED(3101),
+
+
+ /**
+ * Wire transfer attempted with credit and debit party being the same bank account.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_SAME_ACCOUNT(5101),
+
+
+ /**
+ * Wire transfer impossible, due to financial limitation of the party that attempted the payment.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNALLOWED_DEBIT(5102),
+
+
+ /**
+ * Negative numbers are not allowed (as value and/or fraction) to instantiate an amount object.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NEGATIVE_NUMBER_AMOUNT(5103),
+
+
+ /**
+ * A too big number was used (as value and/or fraction) to instantiate an amount object.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NUMBER_TOO_BIG(5104),
+
+
+ /**
+ * Could not login for the requested operation.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_LOGIN_FAILED(5105),
+
+
+ /**
+ * The bank account referenced in the requested operation was not found.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNKNOWN_ACCOUNT(5106),
+
+
+ /**
+ * The transaction referenced in the requested operation (typically a reject operation), was not found.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TRANSACTION_NOT_FOUND(5107),
+
+
+ /**
+ * Bank received a malformed amount string.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_BAD_FORMAT_AMOUNT(5108),
+
+
+ /**
+ * The client does not own the account credited by the transaction which is to be rejected, so it has no rights do reject it.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_REJECT_NO_RIGHTS(5109),
+
+
+ /**
+ * This error code is returned when no known exception types captured the exception.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNMANAGED_EXCEPTION(5110),
+
+
+ /**
+ * This error code is used for all those exceptions that do not really need a specific error code to return to the client. Used for example when a client is trying to register with a unavailable username.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_SOFT_EXCEPTION(5111),
+
+
+ /**
+ * The request UID for a request to transfer funds has already been used, but with different details for the transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TRANSFER_REQUEST_UID_REUSED(5112),
+
+
+ /**
+ * The withdrawal operation already has a reserve selected. The current request conflicts with the existing selection.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT(5113),
+
+
+ /**
+ * The wire transfer subject duplicates an existing reserve public key. But wire transfer subjects must be unique.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_DUPLICATE_RESERVE_PUB_SUBJECT(5114),
+
+
+ /**
+ * The client requested a transaction that is so far in the past, that it has been forgotten by the bank.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ANCIENT_TRANSACTION_GONE(5115),
+
+
+ /**
+ * The client attempted to abort a transaction that was already confirmed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ABORT_CONFIRM_CONFLICT(5116),
+
+
+ /**
+ * The client attempted to confirm a transaction that was already aborted.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_CONFIRM_ABORT_CONFLICT(5117),
+
+
+ /**
+ * The client attempted to register an account with the same name.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_REGISTER_CONFLICT(5118),
+
+
+ /**
+ * The client attempted to confirm a withdrawal operation before the wallet posted the required details.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_POST_WITHDRAWAL_OPERATION_REQUIRED(5119),
+
+
+ /**
+ * The sync service failed find the account in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_ACCOUNT_UNKNOWN(6100),
+
+
+ /**
+ * The SHA-512 hash provided in the If-None-Match header is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_BAD_IF_NONE_MATCH(6101),
+
+
+ /**
+ * The SHA-512 hash provided in the If-Match header is malformed or missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_BAD_IF_MATCH(6102),
+
+
+ /**
+ * The signature provided in the "Sync-Signature" header is malformed or missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_BAD_SYNC_SIGNATURE(6103),
+
+
+ /**
+ * The signature provided in the "Sync-Signature" header does not match the account, old or new Etags.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_INVALID_SIGNATURE(6104),
+
+
+ /**
+ * The "Content-length" field for the upload is not a number.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_MALFORMED_CONTENT_LENGTH(6105),
+
+
+ /**
+ * The "Content-length" field for the upload is too big based on the server's terms of service.
+ * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_EXCESSIVE_CONTENT_LENGTH(6106),
+
+
+ /**
+ * The server is out of memory to handle the upload. Trying again later may succeed.
+ * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH(6107),
+
+
+ /**
+ * The uploaded data does not match the Etag.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_INVALID_UPLOAD(6108),
+
+
+ /**
+ * HTTP server experienced a timeout while awaiting promised payment.
+ * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_PAYMENT_GENERIC_TIMEOUT(6109),
+
+
+ /**
+ * Sync could not setup the payment request with its own backend.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_PAYMENT_CREATE_BACKEND_ERROR(6110),
+
+
+ /**
+ * The sync service failed find the backup to be updated in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_PREVIOUS_BACKUP_UNKNOWN(6111),
+
+
+ /**
+ * The "Content-length" field for the upload is missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_MISSING_CONTENT_LENGTH(6112),
+
+
+ /**
+ * Sync had problems communicating with its payment backend.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_GENERIC_BACKEND_ERROR(6113),
+
+
+ /**
+ * Sync experienced a timeout communicating with its payment backend.
+ * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_GENERIC_BACKEND_TIMEOUT(6114),
+
+
+ /**
+ * The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE(7000),
+
+
+ /**
+ * The wallet encountered an unexpected exception. This is likely a bug in the wallet implementation.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_UNEXPECTED_EXCEPTION(7001),
+
+
+ /**
+ * The wallet received a response from a server, but the response can't be parsed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_RECEIVED_MALFORMED_RESPONSE(7002),
+
+
+ /**
+ * The wallet tried to make a network request, but it received no response.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_NETWORK_ERROR(7003),
+
+
+ /**
+ * The wallet tried to make a network request, but it was throttled.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_HTTP_REQUEST_THROTTLED(7004),
+
+
+ /**
+ * The wallet made a request to a service, but received an error response it does not know how to handle.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_UNEXPECTED_REQUEST_ERROR(7005),
+
+
+ /**
+ * The denominations offered by the exchange are insufficient. Likely the exchange is badly configured or not maintained.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT(7006),
+
+
+ /**
+ * The wallet does not support the operation requested by a client.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CORE_API_OPERATION_UNKNOWN(7007),
+
+
+ /**
+ * The given taler://pay URI is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_INVALID_TALER_PAY_URI(7008),
+
+
+ /**
+ * The signature on a coin by the exchange's denomination key is invalid after unblinding it.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_COIN_SIGNATURE_INVALID(7009),
+
+
+ /**
+ * The exchange does not know about the reserve (yet), and thus withdrawal can't progress.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_WITHDRAW_RESERVE_UNKNOWN_AT_EXCHANGE(7010),
+
+
+ /**
+ * The wallet core service is not available.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CORE_NOT_AVAILABLE(7011),
+
+
+ /**
+ * The bank has aborted a withdrawal operation, and thus a withdrawal can't complete.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK(7012),
+
+
+ /**
+ * An HTTP request made by the wallet timed out.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_HTTP_REQUEST_GENERIC_TIMEOUT(7013),
+
+
+ /**
+ * The order has already been claimed by another wallet.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_ORDER_ALREADY_CLAIMED(7014),
+
+
+ /**
+ * A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_WITHDRAWAL_GROUP_INCOMPLETE(7015),
+
+
+ /**
+ * The signature on a coin by the exchange's denomination key (obtained through the merchant via tipping) is invalid after unblinding it.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_TIPPING_COIN_SIGNATURE_INVALID(7016),
+
+
+ /**
+ * The wallet does not implement a version of the bank integration API that is compatible with the version offered by the bank.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE(7017),
+
+
+ /**
+ * The wallet processed a taler://pay URI, but the merchant base URL in the downloaded contract terms does not match the merchant base URL derived from the URI.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH(7018),
+
+
+ /**
+ * The merchant's signature on the contract terms is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CONTRACT_TERMS_SIGNATURE_INVALID(7019),
+
+
+ /**
+ * The contract terms given by the merchant are malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CONTRACT_TERMS_MALFORMED(7020),
+
+
+ /**
+ * A pending operation failed, and thus the request can't be completed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PENDING_OPERATION_FAILED(7021),
+
+
+ /**
+ * A payment was attempted, but the merchant had an internal server error (5xx).
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PAY_MERCHANT_SERVER_ERROR(7022),
+
+
+ /**
+ * The crypto worker failed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CRYPTO_WORKER_ERROR(7023),
+
+
+ /**
+ * The crypto worker received a bad request.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CRYPTO_WORKER_BAD_REQUEST(7024),
+
+
+ /**
+ * A KYC step is required before withdrawal can proceed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_WITHDRAWAL_KYC_REQUIRED(7025),
+
+
+ /**
+ * The wallet does not have sufficient balance to create a deposit group.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE(7026),
+
+
+ /**
+ * The wallet does not have sufficient balance to create a peer push payment.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE(7027),
+
+
+ /**
+ * The wallet does not have sufficient balance to pay for an invoice.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE(7028),
+
+
+ /**
+ * We encountered a timeout with our payment backend.
+ * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_GENERIC_BACKEND_TIMEOUT(8000),
+
+
+ /**
+ * The backend requested payment, but the request is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST(8001),
+
+
+ /**
+ * The backend got an unexpected reply from the payment processor.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_GENERIC_BACKEND_ERROR(8002),
+
+
+ /**
+ * The "Content-length" field for the upload is missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH(8003),
+
+
+ /**
+ * The "Content-length" field for the upload is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH(8004),
+
+
+ /**
+ * The backend failed to setup an order with the payment processor.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_GENERIC_ORDER_CREATE_BACKEND_ERROR(8005),
+
+
+ /**
+ * The backend was not authorized to check for payment with the payment processor.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED(8006),
+
+
+ /**
+ * The backend could not check payment status with the payment processor.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED(8007),
+
+
+ /**
+ * The Anastasis provider could not be reached.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_GENERIC_PROVIDER_UNREACHABLE(8008),
+
+
+ /**
+ * HTTP server experienced a timeout while awaiting promised payment.
+ * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_PAYMENT_GENERIC_TIMEOUT(8009),
+
+
+ /**
+ * The key share is unknown to the provider.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_UNKNOWN(8108),
+
+
+ /**
+ * The authorization method used for the key share is no longer supported by the provider.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED(8109),
+
+
+ /**
+ * The client needs to respond to the challenge.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED(8110),
+
+
+ /**
+ * The client's response to the challenge was invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_CHALLENGE_FAILED(8111),
+
+
+ /**
+ * The backend is not aware of having issued the provided challenge code. Either this is the wrong code, or it has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_CHALLENGE_UNKNOWN(8112),
+
+
+ /**
+ * The backend failed to initiate the authorization process.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED(8114),
+
+
+ /**
+ * The authorization succeeded, but the key share is no longer available.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_KEY_SHARE_GONE(8115),
+
+
+ /**
+ * The backend forgot the order we asked the client to pay for
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_ORDER_DISAPPEARED(8116),
+
+
+ /**
+ * The backend itself reported a bad exchange interaction.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD(8117),
+
+
+ /**
+ * The backend reported a payment status we did not expect.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS(8118),
+
+
+ /**
+ * The backend failed to setup the order for payment.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR(8119),
+
+
+ /**
+ * The decryption of the key share failed with the provided key.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_DECRYPTION_FAILED(8120),
+
+
+ /**
+ * The request rate is too high. The server is refusing requests to guard against brute-force attacks.
+ * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_RATE_LIMITED(8121),
+
+
+ /**
+ * A request to issue a challenge is not valid for this authentication method.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD(8123),
+
+
+ /**
+ * The backend failed to store the key share because the UUID is already in use.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS(8150),
+
+
+ /**
+ * The backend failed to store the key share because the authorization method is not supported.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED(8151),
+
+
+ /**
+ * The provided phone number is not an acceptable number.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_SMS_PHONE_INVALID(8200),
+
+
+ /**
+ * Failed to run the SMS transmission helper process.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_SMS_HELPER_EXEC_FAILED(8201),
+
+
+ /**
+ * Provider failed to send SMS. Helper terminated with a non-successful result.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_SMS_HELPER_COMMAND_FAILED(8202),
+
+
+ /**
+ * The provided email address is not an acceptable address.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_EMAIL_INVALID(8210),
+
+
+ /**
+ * Failed to run the E-mail transmission helper process.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_EMAIL_HELPER_EXEC_FAILED(8211),
+
+
+ /**
+ * Provider failed to send E-mail. Helper terminated with a non-successful result.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_EMAIL_HELPER_COMMAND_FAILED(8212),
+
+
+ /**
+ * The provided postal address is not an acceptable address.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_POST_INVALID(8220),
+
+
+ /**
+ * Failed to run the mail transmission helper process.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_POST_HELPER_EXEC_FAILED(8221),
+
+
+ /**
+ * Provider failed to send mail. Helper terminated with a non-successful result.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_POST_HELPER_COMMAND_FAILED(8222),
+
+
+ /**
+ * The provided IBAN address is not an acceptable IBAN.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_IBAN_INVALID(8230),
+
+
+ /**
+ * The provider has not yet received the IBAN wire transfer authorizing the disclosure of the key share.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_IBAN_MISSING_TRANSFER(8231),
+
+
+ /**
+ * The backend did not find a TOTP key in the data provided.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TOTP_KEY_MISSING(8240),
+
+
+ /**
+ * The key provided does not satisfy the format restrictions for an Anastasis TOTP key.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_TOTP_KEY_INVALID(8241),
+
+
+ /**
+ * The given if-none-match header is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_POLICY_BAD_IF_NONE_MATCH(8301),
+
+
+ /**
+ * The server is out of memory to handle the upload. Trying again later may succeed.
+ * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH(8304),
+
+
+ /**
+ * The signature provided in the "Anastasis-Policy-Signature" header is malformed or missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_POLICY_BAD_SIGNATURE(8305),
+
+
+ /**
+ * The given if-match header is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_POLICY_BAD_IF_MATCH(8306),
+
+
+ /**
+ * The uploaded data does not match the Etag.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_POLICY_INVALID_UPLOAD(8307),
+
+
+ /**
+ * The provider is unaware of the requested policy.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_POLICY_NOT_FOUND(8350),
+
+
+ /**
+ * The given action is invalid for the current state of the reducer.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_ACTION_INVALID(8400),
+
+
+ /**
+ * The given state of the reducer is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_STATE_INVALID(8401),
+
+
+ /**
+ * The given input to the reducer is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_INPUT_INVALID(8402),
+
+
+ /**
+ * The selected authentication method does not work for the Anastasis provider.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_AUTHENTICATION_METHOD_NOT_SUPPORTED(8403),
+
+
+ /**
+ * The given input and action do not work for the current state.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE(8404),
+
+
+ /**
+ * We experienced an unexpected failure interacting with the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_BACKEND_FAILURE(8405),
+
+
+ /**
+ * The contents of a resource file did not match our expectations.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_RESOURCE_MALFORMED(8406),
+
+
+ /**
+ * A required resource file is missing.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_RESOURCE_MISSING(8407),
+
+
+ /**
+ * An input did not match the regular expression.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_INPUT_REGEX_FAILED(8408),
+
+
+ /**
+ * An input did not match the custom validation logic.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED(8409),
+
+
+ /**
+ * Our attempts to download the recovery document failed with all providers. Most likely the personal information you entered differs from the information you provided during the backup process and you should go back to the previous step. Alternatively, if you used a backup provider that is unknown to this application, you should add that provider manually.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED(8410),
+
+
+ /**
+ * Anastasis provider reported a fatal failure.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED(8411),
+
+
+ /**
+ * Anastasis provider failed to respond to the configuration request.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED(8412),
+
+
+ /**
+ * The policy we downloaded is malformed. Must have been a client error while creating the backup.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_POLICY_MALFORMED(8413),
+
+
+ /**
+ * We failed to obtain the policy, likely due to a network issue.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_NETWORK_FAILED(8414),
+
+
+ /**
+ * The recovered secret did not match the required syntax.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_SECRET_MALFORMED(8415),
+
+
+ /**
+ * The challenge data provided is too large for the available providers.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_CHALLENGE_DATA_TOO_BIG(8416),
+
+
+ /**
+ * The provided core secret is too large for some of the providers.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_SECRET_TOO_BIG(8417),
+
+
+ /**
+ * The provider returned in invalid configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG(8418),
+
+
+ /**
+ * The reducer encountered an internal error, likely a bug that needs to be reported.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_INTERNAL_ERROR(8419),
+
+
+ /**
+ * The reducer already synchronized with all providers.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_PROVIDERS_ALREADY_SYNCED(8420),
+
+
+ /**
+ * A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ LIBEUFIN_NEXUS_GENERIC_ERROR(9000),
+
+
+ /**
+ * An uncaught exception happened in the LibEuFin nexus service.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION(9001),
+
+
+ /**
+ * A generic error happened in the LibEuFin sandbox. See the enclose details JSON for more information.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ LIBEUFIN_SANDBOX_GENERIC_ERROR(9500),
+
+
+ /**
+ * An uncaught exception happened in the LibEuFin sandbox service.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ LIBEUFIN_SANDBOX_UNCAUGHT_EXCEPTION(9501),
+
+
+ /**
+ * This validation method is not supported by the service.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TALDIR_METHOD_NOT_SUPPORTED(9600),
+
+
+ /**
+ * Number of allowed attempts for initiating a challenge exceeded.
+ * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TALDIR_REGISTER_RATE_LIMITED(9601),
+
+
+ /**
+ * End of error code range.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ END(9999);
+}
+
+@OptIn(ExperimentalSerializationApi::class)
+@Serializer(forClass = TalerErrorCode::class)
+object TalerErrorCodeSerializer : KSerializer<TalerErrorCode> {
+
+ override val descriptor =
+ PrimitiveSerialDescriptor("TalerErrorCodeSerializer", PrimitiveKind.INT)
+
+ override fun deserialize(decoder: Decoder): TalerErrorCode {
+ val code = decoder.decodeInt()
+ return enumValues<TalerErrorCode>().firstOrNull {
+ code == it.code
+ } ?: TalerErrorCode.UNKNOWN
+ }
+
+ override fun serialize(encoder: Encoder, value: TalerErrorCode) {
+ error("Not supported")
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
index a2b48b6..4e179bb 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -17,171 +17,104 @@
package net.taler.wallet.backend
import android.app.Application
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
-import android.os.Handler
-import android.os.IBinder
-import android.os.Message
-import android.os.Messenger
-import android.util.Log
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.KSerializer
-import kotlinx.serialization.json.Json
-import net.taler.lib.android.CustomClassDiscriminator
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_COMMAND
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_NOTIFY
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_REPLY
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_SUBSCRIBE_NOTIFY
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.decodeFromJsonElement
+import net.taler.wallet.backend.TalerErrorCode.NONE
import org.json.JSONObject
-import java.lang.ref.WeakReference
-import java.util.LinkedList
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-import kotlin.reflect.full.companionObjectInstance
+import java.io.File
+private const val WALLET_DB = "talerwalletdb.sqlite3"
+
+@OptIn(DelicateCoroutinesApi::class)
class WalletBackendApi(
private val app: Application,
- private val notificationHandler: ((payload: JSONObject) -> Unit)
+ private val versionReceiver: VersionReceiver,
+ notificationReceiver: NotificationReceiver,
) {
- private var walletBackendMessenger: Messenger? = null
- private val queuedMessages = LinkedList<Message>()
- private val handlers = ConcurrentHashMap<Int, (isError: Boolean, message: JSONObject) -> Unit>()
- private var nextRequestID = AtomicInteger(0)
- private val incomingMessenger = Messenger(IncomingHandler(this))
-
- private val walletBackendConn = object : ServiceConnection {
- override fun onServiceDisconnected(p0: ComponentName?) {
- Log.w(TAG, "wallet backend service disconnected (crash?)")
- walletBackendMessenger = null
- }
- override fun onServiceConnected(componentName: ComponentName?, binder: IBinder?) {
- Log.i(TAG, "connected to wallet backend service")
- val bm = Messenger(binder)
- walletBackendMessenger = bm
- pumpQueue(bm)
- val msg = Message.obtain(null, MSG_SUBSCRIBE_NOTIFY)
- msg.replyTo = incomingMessenger
- bm.send(msg)
- }
- }
+ private val backendManager = BackendManager(notificationReceiver)
init {
- Intent(app, WalletBackendService::class.java).also { intent ->
- app.bindService(intent, walletBackendConn, Context.BIND_AUTO_CREATE)
+ GlobalScope.launch(Dispatchers.IO) {
+ backendManager.run()
+ sendInitMessage()
}
}
- private class IncomingHandler(strongApi: WalletBackendApi) : Handler() {
- private val weakApi = WeakReference(strongApi)
- override fun handleMessage(msg: Message) {
- val api = weakApi.get() ?: return
- when (msg.what) {
- MSG_REPLY -> {
- val requestID = msg.data.getInt("requestID", 0)
- val operation = msg.data.getString("operation", "??")
- Log.i(TAG, "got reply for operation $operation ($requestID)")
- val h = api.handlers.remove(requestID)
- if (h == null) {
- Log.e(TAG, "request ID not associated with a handler")
- return
- }
- val response = msg.data.getString("response")
- if (response == null) {
- Log.e(TAG, "response did not contain response payload")
- return
- }
- val isError = msg.data.getBoolean("isError")
- val json = JSONObject(response)
- h(isError, json)
- }
- MSG_NOTIFY -> {
- val payloadStr = msg.data.getString("payload")
- if (payloadStr == null) {
- Log.e(TAG, "Notification had no payload: $msg")
- } else {
- val payload = JSONObject(payloadStr)
- api.notificationHandler.invoke(payload)
- }
- }
- }
+ private suspend fun sendInitMessage() {
+ val db = if (File(app.filesDir, "talerwalletdb.sql").isFile) {
+ // can be removed after a reasonable migration period (2024-02-02)
+ "${app.filesDir}/talerwalletdb.sql"
+ } else {
+ "${app.filesDir}/${WALLET_DB}"
}
- }
-
- private fun pumpQueue(bm: Messenger) {
- while (true) {
- val msg = queuedMessages.pollFirst() ?: return
- bm.send(msg)
+ request("init", InitResponse.serializer()) {
+ put("persistentStoragePath", db)
+ put("logLevel", "INFO")
+ }.onSuccess { response ->
+ versionReceiver.onVersionReceived(response.versionInfo)
+ }.onError { error ->
+ // TODO expose this to the UI as it can happen when using an older DB version
+ error("Error on init message: $error")
}
}
- fun sendRequest(
- operation: String,
- args: JSONObject? = null,
- onResponse: (isError: Boolean, message: JSONObject) -> Unit = { _, _ -> }
- ) {
- val requestID = nextRequestID.incrementAndGet()
- Log.i(TAG, "sending request for operation $operation ($requestID)")
- val msg = Message.obtain(null, MSG_COMMAND)
- handlers[requestID] = onResponse
- msg.replyTo = incomingMessenger
- val data = msg.data
- data.putString("operation", operation)
- data.putInt("requestID", requestID)
- if (args != null) {
- data.putString("args", args.toString())
- }
- val bm = walletBackendMessenger
- if (bm != null) {
- bm.send(msg)
- } else {
- queuedMessages.add(msg)
- }
+ suspend fun sendRequest(operation: String, args: JSONObject? = null): ApiResponse {
+ return backendManager.send(operation, args)
}
suspend inline fun <reified T> request(
operation: String,
serializer: KSerializer<T>? = null,
- noinline args: (JSONObject.() -> JSONObject)? = null
+ noinline args: (JSONObject.() -> JSONObject)? = null,
): WalletResponse<T> = withContext(Dispatchers.Default) {
- suspendCoroutine { cont ->
- val json = Json {
- ignoreUnknownKeys = true
- (T::class.companionObjectInstance as? CustomClassDiscriminator)?.let {
- classDiscriminator = it.discriminator
+ val json = BackendManager.json
+ try {
+ when (val response = sendRequest(operation, args?.invoke(JSONObject()))) {
+ is ApiResponse.Response -> {
+ val t: T = serializer?.let {
+ json.decodeFromJsonElement(serializer, response.result)
+ } ?: Unit as T
+ WalletResponse.Success(t)
}
- }
- sendRequest(operation, args?.invoke(JSONObject())) { isError, message ->
- val response = try {
- if (isError) {
- val error =
- json.decodeFromString(TalerErrorInfo.serializer(), message.toString())
- WalletResponse.Error(error)
- } else {
- val t: T = serializer?.let {
- json.decodeFromString(serializer, message.toString())
- } ?: Unit as T
- WalletResponse.Success(t)
- }
- } catch (e: Exception) {
- val info = TalerErrorInfo(0, "", e.toString(), null)
- WalletResponse.Error(info)
+
+ is ApiResponse.Error -> {
+ val error: TalerErrorInfo = json.decodeFromJsonElement(response.error)
+ WalletResponse.Error(error)
}
- cont.resume(response)
}
+ } catch (e: Exception) {
+ val info = TalerErrorInfo(NONE, "", e.toString())
+ WalletResponse.Error(info)
}
}
- fun destroy() {
- // FIXME: implement this!
- }
+ // Returns raw JSON response instead of serialized object
+ suspend inline fun rawRequest(
+ operation: String,
+ noinline args: (JSONObject.() -> JSONObject)? = null,
+ ): WalletResponse<JsonObject> = withContext(Dispatchers.Default) {
+ val json = BackendManager.json
+ try {
+ when (val response = sendRequest(operation, args?.invoke(JSONObject()))) {
+ is ApiResponse.Response -> {
+ WalletResponse.Success(response.result)
+ }
- companion object {
- const val TAG = "WalletBackendApi"
+ is ApiResponse.Error -> {
+ val error: TalerErrorInfo = json.decodeFromJsonElement(response.error)
+ WalletResponse.Error(error)
+ }
+ }
+ } catch (e: Exception) {
+ val info = TalerErrorInfo(NONE, "", e.toString())
+ WalletResponse.Error(info)
+ }
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
deleted file mode 100644
index ccb995a..0000000
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-
-package net.taler.wallet.backend
-
-import 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.BuildConfig.WALLET_CORE_VERSION
-import net.taler.wallet.HostCardEmulatorService
-import org.json.JSONObject
-import java.lang.ref.WeakReference
-import java.util.LinkedList
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.system.exitProcess
-
-private const val TAG = "taler-wallet-backend"
-
-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-$WALLET_CORE_VERSION.js").use {
- it.readBytes().toString(Charsets.UTF_8)
- }
-
-
- Log.i(TAG, "onCreate in wallet backend service")
- akono = AkonoJni()
- akono.putModuleCode("@gnu-taler/taler-wallet-android", talerWalletAndroidCode)
- akono.setMessageHandler(object : AkonoJni.MessageHandler {
- override fun handleMessage(message: String) {
- this@WalletBackendService.handleAkonoMessage(message)
- }
- })
- //akono.evalNodeCode("require('source-map-support').install();")
- akono.evalNodeCode("require('akono');")
- akono.evalNodeCode("tw = require('@gnu-taler/taler-wallet-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(payload: String) {
- var rm: LinkedList<Messenger>? = null
- for (s in subscribers) {
- val m = Message.obtain(null, MSG_NOTIFY)
- val b = m.data
- b.putString("payload", payload)
- try {
- s.send(m)
- } catch (e: RemoteException) {
- if (rm == null) {
- rm = LinkedList()
- }
- rm.add(s)
- subscribers.remove(s)
- }
- }
- if (rm != null) {
- for (s in rm) {
- subscribers.remove(s)
- }
- }
- }
-
- private fun handleAkonoMessage(messageStr: String) {
- val message = JSONObject(messageStr)
- when (val type = message.getString("type")) {
- "notification" -> {
- val payload = message.getJSONObject("payload")
- if (payload.optString("type") != "waiting-for-retry") {
- Log.v(TAG, "got back notification: ${message.toString(2)}")
- }
- sendNotify(payload.toString())
- }
- "tunnelHttp" -> {
- Log.v(TAG, "got http tunnel request! ${message.toString(2)}")
- Intent().also { intent ->
- intent.action = HostCardEmulatorService.HTTP_TUNNEL_REQUEST
- intent.putExtra("tunnelMessage", messageStr)
- application.sendBroadcast(intent)
- }
- }
- "response" -> {
- when (message.getString("operation")) {
- "init" -> {
- Log.v(TAG, "got response for init operation: ${message.toString(2)}")
- sendNotify(message.toString(2))
- }
- "reset" -> {
- Log.v(TAG, "got back message: ${message.toString(2)}")
- exitProcess(1)
- }
- else -> {
- Log.v(TAG, "got back response: ${message.toString(2)}")
- val payload = message.getJSONObject("result").toString(2)
- handleResponse(false, message, payload)
- }
- }
- }
- "error" -> {
- Log.v(TAG, "got back error: ${message.toString(2)}")
- val payload = message.getJSONObject("error").toString(2)
- handleResponse(true, message, payload)
- }
- else -> throw IllegalArgumentException("Unknown message type: $type")
- }
- }
-
- private fun handleResponse(isError: Boolean, message: JSONObject, payload: String) {
- val id = message.getInt("id")
- val rId = requests[id]
- if (rId == null) {
- Log.e(TAG, "wallet returned unknown request ID ($id)")
- return
- }
- val m = Message.obtain(null, MSG_REPLY)
- val b = m.data
- b.putInt("requestID", rId.clientRequestId)
- b.putBoolean("isError", isError)
- b.putString("response", payload)
- b.putString("operation", message.getString("operation"))
- rId.messenger.send(m)
- }
-
- companion object {
- const val MSG_SUBSCRIBE_NOTIFY = 1
- const val MSG_UNSUBSCRIBE_NOTIFY = 2
- const val MSG_COMMAND = 3
- const val MSG_REPLY = 4
- const val MSG_NOTIFY = 5
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
index ab1ac80..3946457 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
@@ -19,26 +19,31 @@ package net.taler.wallet.backend
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-import kotlinx.serialization.descriptors.PrimitiveKind.STRING
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
-import org.json.JSONObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
@Serializable
sealed class WalletResponse<T> {
@Serializable
@SerialName("response")
data class Success<T>(
- val result: T
+ val result: T,
) : WalletResponse<T>()
@Serializable
@SerialName("error")
data class Error<T>(
- val error: TalerErrorInfo
+ val error: TalerErrorInfo,
) : WalletResponse<T>()
fun onSuccess(block: (result: T) -> Unit): WalletResponse<T> {
@@ -52,53 +57,78 @@ sealed class WalletResponse<T> {
}
}
-@Serializable
+@Serializable(with = TalerErrorInfoSerializer::class)
data class TalerErrorInfo(
// Numeric error code defined defined in the
// GANA gnu-taler-error-codes registry.
- val code: Int,
+ val code: TalerErrorCode,
// English description of the error code.
- val hint: String?,
+ val hint: String? = null,
// English diagnostic message that can give details
// for the instance of the error.
- val message: String?,
+ val message: String? = null,
- // Error details
- @Serializable(JSONObjectDeserializer::class)
- val details: JSONObject?
+ // Error extra details
+ val extra: Map<String, JsonElement> = mapOf(),
) {
val userFacingMsg: String
get() {
return StringBuilder().apply {
- append(code)
- hint?.let { append(" (").append(it).append(")") }
- message?.let { append(" ").append(it) }
- details?.let { details ->
- if (details.length() > 0) {
- append("\n\n")
- details.optJSONObject("errorResponse")?.let { errorResponse ->
- append(errorResponse.optString("code")).append(" ")
- append(errorResponse.optString("hint"))
- } ?: append(details.toString(2))
+ // If there's a hint in errorResponse, use it.
+ if (extra.containsKey("errorResponse")) {
+ val errorResponse = extra["errorResponse"]!!.jsonObject
+ if (errorResponse.containsKey("hint")) {
+ val hint = errorResponse["hint"]!!.jsonPrimitive.content
+ append(hint)
}
+ } else {
+ // Otherwise, use the standard ones
+ hint?.let { append(it) }
+ message?.let { append(" ").append(it) }
}
}.toString()
}
+
+ fun getStringExtra(key: String): String? =
+ extra[key]?.jsonPrimitive?.content
}
-class JSONObjectDeserializer : KSerializer<JSONObject> {
+class TalerErrorInfoSerializer : KSerializer<TalerErrorInfo> {
+ private val stringToJsonElementSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
+
+ override val descriptor: SerialDescriptor
+ get() = stringToJsonElementSerializer.descriptor
+
+ override fun deserialize(decoder: Decoder): TalerErrorInfo {
+ // Decoder -> JsonInput
+ require(decoder is JsonDecoder)
+ val json = decoder.json
+ val filtersMap = decoder.decodeSerializableValue(stringToJsonElementSerializer)
+
+ val code = filtersMap["code"]?.let {
+ json.decodeFromJsonElement(TalerErrorCode.serializer(), it)
+ } ?: TalerErrorCode.UNKNOWN
+ val hint = filtersMap["hint"]?.let {
+ json.decodeFromJsonElement(String.serializer(), it)
+ }
+ val message = filtersMap["message"]?.let {
+ json.decodeFromJsonElement(String.serializer(), it)
+ }
- override val descriptor = PrimitiveSerialDescriptor("JSONObjectDeserializer", STRING)
+ val knownKeys = setOf("code", "hint", "message")
+ val unknownFilters = filtersMap.filter { (key, _) -> !knownKeys.contains(key) }
- override fun deserialize(decoder: Decoder): JSONObject {
- val input = decoder as JsonDecoder
- val tree = input.decodeJsonElement() as JsonObject
- return JSONObject(tree.toString())
+ return TalerErrorInfo(code, hint, message, unknownFilters)
}
- override fun serialize(encoder: Encoder, value: JSONObject) {
- error("not supported")
+ override fun serialize(encoder: Encoder, value: TalerErrorInfo) {
+ encoder.encodeSerializableValue(JsonObject.serializer(), buildJsonObject {
+ put("code", JsonPrimitive(value.code.code))
+ put("hint", JsonPrimitive(value.hint))
+ put("message", JsonPrimitive(value.message))
+ value.extra.forEach { (key, value) -> put(key, value) }
+ })
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
index 09ae353..f40def4 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
@@ -24,20 +24,12 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
-import kotlinx.serialization.Serializable
-import net.taler.lib.common.Amount
import net.taler.wallet.R
import net.taler.wallet.balances.BalanceAdapter.BalanceViewHolder
-
-@Serializable
-data class BalanceItem(
- val available: Amount,
- val pendingIncoming: Amount,
- val pendingOutgoing: Amount
-) {
- val currency: String get() = available.currency
- val hasPending: Boolean get() = !pendingIncoming.isZero() || !pendingOutgoing.isZero()
-}
+import net.taler.wallet.balances.ScopeInfo.Auditor
+import net.taler.wallet.balances.ScopeInfo.Exchange
+import net.taler.wallet.balances.ScopeInfo.Global
+import net.taler.wallet.cleanExchange
class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<BalanceViewHolder>() {
@@ -66,16 +58,15 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan
}
inner class BalanceViewHolder(private val v: View) : RecyclerView.ViewHolder(v) {
- private val currencyView: TextView = v.findViewById(R.id.balanceCurrencyView)
private val amountView: TextView = v.findViewById(R.id.balanceAmountView)
+ private val scopeView: TextView = v.findViewById(R.id.scopeView)
private val balanceInboundAmount: TextView = v.findViewById(R.id.balanceInboundAmount)
private val balanceInboundLabel: TextView = v.findViewById(R.id.balanceInboundLabel)
private val pendingView: TextView = v.findViewById(R.id.pendingView)
fun bind(item: BalanceItem) {
- v.setOnClickListener { listener.onBalanceClick(item.available.currency) }
- currencyView.text = item.currency
- amountView.text = item.available.amountStr
+ v.setOnClickListener { listener.onBalanceClick(item.scopeInfo) }
+ amountView.text = item.available.toString()
val amountIncoming = item.pendingIncoming
if (amountIncoming.isZero()) {
@@ -84,9 +75,22 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan
} else {
balanceInboundAmount.visibility = VISIBLE
balanceInboundLabel.visibility = VISIBLE
- balanceInboundAmount.text =
- v.context.getString(R.string.amount_positive, amountIncoming)
+ balanceInboundAmount.text = v.context.getString(R.string.amount_positive, amountIncoming.toString(showSymbol = false))
}
+
+ val scopeInfo = item.scopeInfo
+ scopeView.visibility = when (scopeInfo) {
+ is Global -> GONE
+ is Exchange -> {
+ scopeView.text = v.context.getString(R.string.balance_scope_exchange, cleanExchange(scopeInfo.url))
+ VISIBLE
+ }
+ is Auditor -> {
+ scopeView.text = v.context.getString(R.string.balance_scope_auditor, cleanExchange(scopeInfo.url))
+ VISIBLE
+ }
+ }
+
pendingView.visibility = if (item.hasPending) VISIBLE else GONE
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt
new file mode 100644
index 0000000..42e67cf
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt
@@ -0,0 +1,137 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.balances
+
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.distinctUntilChanged
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.backend.WalletBackendApi
+import org.json.JSONObject
+
+@Serializable
+data class BalanceResponse(
+ val balances: List<BalanceItem>
+)
+
+@Serializable
+data class GetCurrencySpecificationResponse(
+ val currencySpecification: CurrencySpecification,
+)
+
+sealed class BalanceState {
+ data object None: BalanceState()
+ data object Loading: BalanceState()
+
+ data class Success(
+ val balances: List<BalanceItem>,
+ ): BalanceState()
+
+ data class Error(
+ val error: TalerErrorInfo,
+ ): BalanceState()
+}
+
+class BalanceManager(
+ private val api: WalletBackendApi,
+ private val scope: CoroutineScope,
+) {
+ private val mBalances = MutableLiveData<List<BalanceItem>>(emptyList())
+ val balances: LiveData<List<BalanceItem>> = mBalances
+
+ private val mState = MutableLiveData<BalanceState>(BalanceState.None)
+ val state: LiveData<BalanceState> = mState.distinctUntilChanged()
+
+ private val currencySpecs: MutableMap<ScopeInfo, CurrencySpecification?> = mutableMapOf()
+
+ @UiThread
+ fun loadBalances() {
+ mState.value = BalanceState.Loading
+ scope.launch {
+ val response = api.request("getBalances", BalanceResponse.serializer())
+ response.onError {
+ Log.e(TAG, "Error retrieving balances: $it")
+ mState.postValue(BalanceState.Error(it))
+ }
+ response.onSuccess {
+ mBalances.postValue(it.balances)
+ scope.launch {
+ // Fetch missing currency specs for all balances
+ it.balances.forEach { balance ->
+ if (!currencySpecs.containsKey(balance.scopeInfo)) {
+ currencySpecs[balance.scopeInfo] = getCurrencySpecification(balance.scopeInfo)
+ }
+ }
+
+ mState.postValue(
+ BalanceState.Success(it.balances.map { balance ->
+ val spec = currencySpecs[balance.scopeInfo]
+ balance.copy(
+ available = balance.available.withSpec(spec),
+ pendingIncoming = balance.pendingIncoming.withSpec(spec),
+ pendingOutgoing = balance.pendingOutgoing.withSpec(spec),
+ )
+ }),
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun getCurrencySpecification(scopeInfo: ScopeInfo): CurrencySpecification? {
+ var spec: CurrencySpecification? = null
+ api.request("getCurrencySpecification", GetCurrencySpecificationResponse.serializer()) {
+ val json = Json.encodeToString(scopeInfo)
+ Log.d(TAG, "BalanceManager: $json")
+ put("scope", JSONObject(json))
+ }.onSuccess {
+ spec = it.currencySpecification
+ }.onError {
+ Log.e(TAG, "Error getting currency spec for scope $scopeInfo: $it")
+ }
+
+ return spec
+ }
+
+ @Deprecated("Please find spec via scopeInfo instead", ReplaceWith("getSpecForScopeInfo"))
+ fun getSpecForCurrency(currency: String): CurrencySpecification? {
+ val state = mState.value
+ if (state !is BalanceState.Success) return null
+
+ return state.balances.find { it.currency == currency }?.available?.spec
+ }
+
+ fun getSpecForScopeInfo(scopeInfo: ScopeInfo): CurrencySpecification? {
+ val state = mState.value
+ if (state !is BalanceState.Success) return null
+
+ return state.balances.find { it.scopeInfo == scopeInfo }?.available?.spec
+ }
+
+ fun resetBalances() {
+ mState.value = BalanceState.None
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/balances/Balances.kt b/wallet/src/main/java/net/taler/wallet/balances/Balances.kt
new file mode 100644
index 0000000..dff2ffb
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/balances/Balances.kt
@@ -0,0 +1,57 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.balances
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+
+@Serializable
+data class BalanceItem(
+ val scopeInfo: ScopeInfo,
+ val available: Amount,
+ val pendingIncoming: Amount,
+ val pendingOutgoing: Amount,
+) {
+ val currency: String get() = available.currency
+ val hasPending: Boolean get() = !pendingIncoming.isZero() || !pendingOutgoing.isZero()
+}
+
+@Serializable
+sealed class ScopeInfo {
+ abstract val currency: String
+
+ @Serializable
+ @SerialName("global")
+ data class Global(
+ override val currency: String
+ ): ScopeInfo()
+
+ @Serializable
+ @SerialName("exchange")
+ data class Exchange(
+ override val currency: String,
+ val url: String,
+ ): ScopeInfo()
+
+ @Serializable
+ @SerialName("auditor")
+ data class Auditor(
+ override val currency: String,
+ val url: String,
+ ): ScopeInfo()
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt
index afd9a23..93636ea 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt
@@ -29,11 +29,17 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
import net.taler.common.fadeIn
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
+import net.taler.wallet.balances.BalanceState.Error
+import net.taler.wallet.balances.BalanceState.Loading
+import net.taler.wallet.balances.BalanceState.None
+import net.taler.wallet.balances.BalanceState.Success
import net.taler.wallet.databinding.FragmentBalancesBinding
+import net.taler.wallet.showError
interface BalanceClickListener {
- fun onBalanceClick(currency: String)
+ fun onBalanceClick(scopeInfo: ScopeInfo)
}
class BalancesFragment : Fragment(),
@@ -48,7 +54,7 @@ class BalancesFragment : Fragment(),
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View? {
+ ): View {
ui = FragmentBalancesBinding.inflate(inflater, container, false)
return ui.root
}
@@ -59,25 +65,39 @@ class BalancesFragment : Fragment(),
addItemDecoration(DividerItemDecoration(context, VERTICAL))
}
- model.balances.observe(viewLifecycleOwner, {
+ model.balanceManager.state.observe(viewLifecycleOwner) {
onBalancesChanged(it)
- })
+ }
}
- private fun onBalancesChanged(balances: List<BalanceItem>) {
- beginDelayedTransition(view as ViewGroup)
- if (balances.isEmpty()) {
- ui.mainEmptyState.visibility = VISIBLE
- ui.mainList.visibility = GONE
- } else {
- balancesAdapter.setItems(balances)
- ui.mainEmptyState.visibility = INVISIBLE
- ui.mainList.fadeIn()
+ private fun onBalancesChanged(state: BalanceState) {
+ model.showProgressBar.value = false
+ when (state) {
+ is None -> {}
+ is Loading -> {
+ model.showProgressBar.value = true
+ }
+ is Success -> {
+ beginDelayedTransition(view as ViewGroup)
+ if (state.balances.isEmpty()) {
+ ui.mainEmptyState.visibility = VISIBLE
+ ui.mainList.visibility = GONE
+ } else {
+ balancesAdapter.setItems(state.balances)
+ ui.mainEmptyState.visibility = INVISIBLE
+ ui.mainList.fadeIn()
+ }
+ }
+ is Error -> if (model.devMode.value == true) {
+ showError(state.error)
+ } else {
+ showError(state.error.userFacingMsg)
+ }
}
}
- override fun onBalanceClick(currency: String) {
- model.showTransactions(currency)
+ override fun onBalanceClick(scopeInfo: ScopeInfo) {
+ model.showTransactions(scopeInfo)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt
new file mode 100644
index 0000000..a524d1b
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt
@@ -0,0 +1,226 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.compose
+
+import android.os.Build
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
+import net.taler.common.Amount
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
+import kotlin.math.max
+import kotlin.math.pow
+import kotlin.math.roundToLong
+
+const val DEFAULT_INPUT_DECIMALS = 2
+
+@Composable
+fun AmountInputField(
+ value: String,
+ onValueChange: (value: String) -> Unit,
+ modifier: Modifier = Modifier,
+ label: @Composable (() -> Unit)? = null,
+ supportingText: @Composable (() -> Unit)? = null,
+ isError: Boolean = false,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ decimalFormatSymbols: DecimalFormatSymbols = DecimalFormat().decimalFormatSymbols,
+ numberOfDecimals: Int = DEFAULT_INPUT_DECIMALS,
+) {
+ var amountInput by remember { mutableStateOf(value) }
+
+ // React to external changes
+ val amountValue = remember(amountInput, value) {
+ transformOutput(amountInput).let {
+ if (value != it) transformInput(value, numberOfDecimals) else amountInput
+ }
+ }
+
+ OutlinedTextField(
+ value = amountValue,
+ onValueChange = { input ->
+ if (input.matches("0+".toRegex())) {
+ amountInput = "0"
+ onValueChange("")
+ } else transformOutput(input, numberOfDecimals)?.let { filtered ->
+ if (Amount.isValidAmountStr(filtered) && !input.contains("-")) {
+ amountInput = input.trimStart('0')
+ onValueChange(filtered)
+ }
+ }
+ },
+ modifier = modifier,
+ textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace),
+ label = label,
+ supportingText = supportingText,
+ isError = isError,
+ visualTransformation = AmountInputVisualTransformation(
+ symbols = decimalFormatSymbols,
+ fixedCursorAtTheEnd = true,
+ numberOfDecimals = numberOfDecimals,
+ ),
+ keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.NumberPassword),
+ keyboardActions = keyboardActions,
+ singleLine = true,
+ maxLines = 1,
+ )
+}
+
+// 500 -> 5.0
+private fun transformOutput(
+ input: String,
+ numberOfDecimals: Int = 2,
+) = if (input.isEmpty()) "0" else {
+ input.toLongOrNull()?.let { it / 10.0.pow(numberOfDecimals) }?.toBigDecimal()?.toPlainString()
+}
+
+// 5.0 -> 500
+private fun transformInput(
+ output: String,
+ numberOfDecimals: Int = 2,
+) = if (output.isEmpty()) "0" else {
+ (output.toDouble() * 10.0.pow(numberOfDecimals)).roundToLong().toString()
+}
+
+// Source: https://github.com/banmarkovic/CurrencyAmountInput
+
+private class AmountInputVisualTransformation(
+ private val symbols: DecimalFormatSymbols,
+ private val fixedCursorAtTheEnd: Boolean = true,
+ private val numberOfDecimals: Int = 2,
+): VisualTransformation {
+
+ override fun filter(text: AnnotatedString): TransformedText {
+ val thousandsSeparator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ symbols.monetaryGroupingSeparator
+ } else {
+ symbols.groupingSeparator
+ }
+ val decimalSeparator = symbols.monetaryDecimalSeparator
+ val zero = symbols.zeroDigit
+
+ val inputText = text.text
+
+ val intPart = inputText
+ .dropLast(numberOfDecimals)
+ .reversed()
+ .chunked(3)
+ .joinToString(thousandsSeparator.toString())
+ .reversed()
+ .ifEmpty {
+ zero.toString()
+ }
+
+ val fractionPart = inputText.takeLast(numberOfDecimals).let {
+ if (it.length != numberOfDecimals) {
+ List(numberOfDecimals - it.length) {
+ zero
+ }.joinToString("") + it
+ } else {
+ it
+ }
+ }
+
+ // Hide trailing decimal separator if decimals are 0
+ val formattedNumber = if (numberOfDecimals > 0) {
+ intPart + decimalSeparator + fractionPart
+ } else {
+ intPart
+ }
+
+ val newText = AnnotatedString(
+ text = formattedNumber,
+ spanStyles = text.spanStyles,
+ paragraphStyles = text.paragraphStyles
+ )
+
+ val offsetMapping = if (fixedCursorAtTheEnd) {
+ FixedCursorOffsetMapping(
+ contentLength = inputText.length,
+ formattedContentLength = formattedNumber.length
+ )
+ } else {
+ MovableCursorOffsetMapping(
+ unmaskedText = text.toString(),
+ maskedText = newText.toString(),
+ decimalDigits = numberOfDecimals
+ )
+ }
+
+ return TransformedText(newText, offsetMapping)
+ }
+
+ private class FixedCursorOffsetMapping(
+ private val contentLength: Int,
+ private val formattedContentLength: Int,
+ ) : OffsetMapping {
+ override fun originalToTransformed(offset: Int): Int = formattedContentLength
+ override fun transformedToOriginal(offset: Int): Int = contentLength
+ }
+
+ private class MovableCursorOffsetMapping(
+ private val unmaskedText: String,
+ private val maskedText: String,
+ private val decimalDigits: Int
+ ) : OffsetMapping {
+ override fun originalToTransformed(offset: Int): Int =
+ when {
+ unmaskedText.length <= decimalDigits -> {
+ maskedText.length - (unmaskedText.length - offset)
+ }
+ else -> {
+ offset + offsetMaskCount(offset, maskedText)
+ }
+ }
+
+ override fun transformedToOriginal(offset: Int): Int =
+ when {
+ unmaskedText.length <= decimalDigits -> {
+ max(unmaskedText.length - (maskedText.length - offset), 0)
+ }
+ else -> {
+ offset - maskedText.take(offset).count { !it.isDigit() }
+ }
+ }
+
+ private fun offsetMaskCount(offset: Int, maskedText: String): Int {
+ var maskOffsetCount = 0
+ var dataCount = 0
+ for (maskChar in maskedText) {
+ if (!maskChar.isDigit()) {
+ maskOffsetCount++
+ } else if (++dataCount > offset) {
+ break
+ }
+ }
+ return maskOffsetCount
+ }
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt b/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt
new file mode 100644
index 0000000..47401cf
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt
@@ -0,0 +1,70 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.compose
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Remove
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun NumericInputField(
+ modifier: Modifier = Modifier,
+ value: Long,
+ onValueChange: (Long) -> Unit,
+ readOnly: Boolean = true,
+ label: @Composable () -> Unit,
+ minValue: Long? = 0L,
+ maxValue: Long? = null,
+) {
+ OutlinedTextField(
+ modifier = modifier,
+ value = value.toString(),
+ singleLine = true,
+ readOnly = readOnly,
+ onValueChange = {
+ val dd = it.toLongOrNull() ?: 0
+ onValueChange(dd)
+ },
+ trailingIcon = {
+ Row {
+ IconButton(
+ content = { Icon(Icons.Default.Remove, "add1") },
+ onClick = {
+ if (minValue != null && value - 1 >= minValue) {
+ onValueChange(value - 1)
+ }
+ },
+ )
+ IconButton(
+ content = { Icon(Icons.Default.Add, "add1") },
+ onClick = {
+ if (maxValue != null && value + 1 <= maxValue) {
+ onValueChange(value + 1)
+ }
+ },
+ )
+ }
+ },
+ label = label,
+ )
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
new file mode 100644
index 0000000..4991094
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
@@ -0,0 +1,153 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.compose
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.produceState
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.min
+import androidx.core.content.getSystemService
+import net.taler.common.QrCodeManager
+import net.taler.wallet.R
+
+@Composable
+fun ColumnScope.QrCodeUriComposable(
+ talerUri: String,
+ clipBoardLabel: String,
+ buttonText: String = stringResource(R.string.copy),
+ inBetween: (@Composable ColumnScope.() -> Unit)? = null,
+) {
+ val qrCodeSize = getQrCodeSize()
+ val qrPlaceHolder = if (LocalInspectionMode.current) {
+ QrCodeManager.makeQrCode(talerUri, qrCodeSize.value.toInt()).asImageBitmap()
+ } else null
+ val qrState = produceState(qrPlaceHolder) {
+ value = QrCodeManager.makeQrCode(talerUri, qrCodeSize.value.toInt()).asImageBitmap()
+ }
+ qrState.value?.let { qrCode ->
+ Image(
+ modifier = Modifier
+ .size(qrCodeSize)
+ .align(CenterHorizontally)
+ .padding(vertical = 8.dp),
+ bitmap = qrCode,
+ contentDescription = stringResource(id = R.string.button_scan_qr_code),
+ )
+ }
+ if (inBetween != null) inBetween()
+ val scrollState = rememberScrollState()
+ Box(modifier = Modifier.padding(16.dp)) {
+ Text(
+ modifier = Modifier.horizontalScroll(scrollState),
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.bodyLarge,
+ text = talerUri,
+ )
+ }
+ Row(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ ) {
+ CopyToClipboardButton(
+ label = clipBoardLabel,
+ content = talerUri,
+ buttonText = buttonText,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ )
+ ShareButton(
+ content = talerUri,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ )
+ }
+}
+
+@Composable
+fun getQrCodeSize(): Dp {
+ val configuration = LocalConfiguration.current
+ val screenHeight = configuration.screenHeightDp.dp
+ val screenWidth = configuration.screenWidthDp.dp
+ return min(screenHeight, screenWidth)
+}
+
+@Composable
+fun CopyToClipboardButton(
+ label: String,
+ content: String,
+ modifier: Modifier = Modifier,
+ buttonText: String = stringResource(R.string.copy),
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
+) {
+ val context = LocalContext.current
+ Button(
+ modifier = modifier,
+ colors = colors,
+ onClick = { copyToClipBoard(context, label, content) },
+ ) {
+ Icon(
+ Icons.Default.ContentCopy,
+ buttonText,
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(buttonText)
+ }
+}
+
+fun copyToClipBoard(context: Context, label: String, str: String) {
+ val clipboard = context.getSystemService<ClipboardManager>()
+ val clip = ClipData.newPlainText(label, str)
+ clipboard?.setPrimaryClip(clip)
+}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt b/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt
new file mode 100644
index 0000000..c47f55d
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt
@@ -0,0 +1,46 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.compose
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SuggestionChip
+import androidx.compose.material3.SuggestionChipDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+
+@Composable
+fun <T> SelectionChip(
+ label: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ selected: Boolean,
+ value: T,
+ onSelected: (T) -> Unit,
+) {
+ val theme = MaterialTheme.colorScheme
+ SuggestionChip(
+ label = label,
+ modifier = modifier,
+ onClick = {
+ onSelected(value)
+ },
+ colors = SuggestionChipDefaults.suggestionChipColors(
+ containerColor = if (selected) theme.primaryContainer else Color.Transparent,
+ labelColor = if (selected) theme.onPrimaryContainer else theme.onSurface
+ )
+ )
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt b/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt
new file mode 100644
index 0000000..f3a84dd
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt
@@ -0,0 +1,67 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.compose
+
+import android.content.Intent
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.EXTRA_TEXT
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Share
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.core.content.ContextCompat.startActivity
+import net.taler.wallet.R
+
+@Composable
+fun ShareButton(
+ content: String,
+ modifier: Modifier = Modifier,
+ buttonText: String = stringResource(R.string.share),
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
+) {
+ val context = LocalContext.current
+ Button(
+ modifier = modifier,
+ colors = colors,
+ onClick = {
+ val sendIntent: Intent = Intent().apply {
+ action = ACTION_SEND
+ putExtra(EXTRA_TEXT, content)
+ type = "text/plain"
+ }
+ val shareIntent = Intent.createChooser(sendIntent, null)
+ startActivity(context, shareIntent, null)
+ },
+ ) {
+ Icon(
+ Icons.Default.Share,
+ buttonText,
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(buttonText)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/Utils.kt b/wallet/src/main/java/net/taler/wallet/compose/Utils.kt
new file mode 100644
index 0000000..9a27431
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/Utils.kt
@@ -0,0 +1,64 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.compose
+
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.flowWithLifecycle
+import com.google.accompanist.themeadapter.material3.Mdc3Theme
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+@Composable
+fun <T> rememberFlow(
+ flow: Flow<T>,
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+): Flow<T> = remember(key1 = flow, key2 = lifecycleOwner) {
+ flow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
+}
+
+@Composable
+fun <T : R, R> Flow<T>.collectAsStateLifecycleAware(
+ initial: R,
+ context: CoroutineContext = EmptyCoroutineContext,
+): State<R> {
+ val lifecycleAwareFlow = rememberFlow(flow = this)
+ return lifecycleAwareFlow.collectAsState(initial = initial, context = context)
+}
+
+@Suppress("StateFlowValueCalledInComposition")
+@Composable
+fun <T> StateFlow<T>.collectAsStateLifecycleAware(
+ context: CoroutineContext = EmptyCoroutineContext,
+): State<T> = collectAsStateLifecycleAware(initial = value, context = context)
+
+@Composable
+fun TalerSurface(content: @Composable () -> Unit) {
+ Mdc3Theme {
+ Surface {
+ content()
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt
new file mode 100644
index 0000000..20acee1
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt
@@ -0,0 +1,117 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.deposit
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import net.taler.common.Amount
+import net.taler.common.showError
+import net.taler.wallet.CURRENCY_BTC
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
+
+class DepositFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val depositManager get() = model.depositManager
+ private val balanceManager get() = model.balanceManager
+ private val transactionManager get() = model.transactionManager
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ val amount = arguments?.getString("amount")?.let {
+ Amount.fromJSONString(it)
+ } ?: error("no amount passed")
+ val scopeInfo = transactionManager.selectedScope
+ val spec = scopeInfo?.let { balanceManager.getSpecForScopeInfo(it) }
+ val receiverName = arguments?.getString("receiverName")
+ val iban = arguments?.getString("IBAN")
+ if (receiverName != null && iban != null) {
+ onDepositButtonClicked(amount, receiverName, iban)
+ }
+ return ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val state = depositManager.depositState.collectAsStateLifecycleAware()
+ if (amount.currency == CURRENCY_BTC) MakeBitcoinDepositComposable(
+ state = state.value,
+ amount = amount.withSpec(spec),
+ bitcoinAddress = null,
+ onMakeDeposit = { amount, bitcoinAddress ->
+ depositManager.onDepositButtonClicked(amount, bitcoinAddress)
+ },
+ ) else MakeDepositComposable(
+ state = state.value,
+ amount = amount.withSpec(spec),
+ presetName = receiverName,
+ presetIban = iban,
+ onMakeDeposit = this@DepositFragment::onDepositButtonClicked,
+ )
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ lifecycleScope.launchWhenStarted {
+ depositManager.depositState.collect { state ->
+ if (state is DepositState.Error) {
+ if (model.devMode.value == false) {
+ showError(state.error.userFacingMsg)
+ } else {
+ showError(state.error)
+ }
+ } else if (state is DepositState.Success) {
+ findNavController().navigate(R.id.action_nav_deposit_to_nav_main)
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.send_deposit_title)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!requireActivity().isChangingConfigurations) {
+ depositManager.resetDepositState()
+ }
+ }
+
+ private fun onDepositButtonClicked(
+ amount: Amount,
+ receiverName: String,
+ iban: String,
+ ) {
+ depositManager.onDepositButtonClicked(amount, receiverName, iban)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt
new file mode 100644
index 0000000..0075f95
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt
@@ -0,0 +1,169 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.deposit
+
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.UiThread
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+import net.taler.wallet.TAG
+import net.taler.wallet.accounts.PaytoUriBitcoin
+import net.taler.wallet.accounts.PaytoUriIban
+import net.taler.wallet.backend.WalletBackendApi
+
+class DepositManager(
+ private val api: WalletBackendApi,
+ private val scope: CoroutineScope,
+) {
+
+ private val mDepositState = MutableStateFlow<DepositState>(DepositState.Start)
+ internal val depositState = mDepositState.asStateFlow()
+
+ fun isSupportedPayToUri(uriString: String): Boolean {
+ if (!uriString.startsWith("payto://")) return false
+ val u = Uri.parse(uriString)
+ if (!u.authority.equals("iban", ignoreCase = true)) return false
+ return u.pathSegments.size >= 1
+ }
+
+ @UiThread
+ fun onDepositButtonClicked(amount: Amount, receiverName: String, iban: String) {
+ if (depositState.value is DepositState.FeesChecked) {
+ // fees already checked, so IBAN was validated, can make deposit directly
+ makeIbanDeposit(amount, receiverName, iban)
+ } else {
+ // validate IBAN first
+ mDepositState.value = DepositState.CheckingFees
+ scope.launch {
+ api.request("validateIban", ValidateIbanResponse.serializer()) {
+ put("iban", iban)
+ }.onError {
+ Log.e(TAG, "Error validateIban $it")
+ mDepositState.value = DepositState.Error(it)
+ }.onSuccess { response ->
+ if (response.valid) {
+ // only prepare/make deposit, if IBAN is valid
+ makeIbanDeposit(amount, receiverName, iban)
+ } else {
+ mDepositState.value = DepositState.IbanInvalid
+ }
+ }
+ }
+ }
+ }
+
+ @UiThread
+ private fun makeIbanDeposit(amount: Amount, receiverName: String, iban: String) {
+ val paytoUri: String = PaytoUriIban(
+ iban = iban,
+ bic = null,
+ targetPath = "",
+ params = mapOf("receiver-name" to receiverName),
+ ).paytoUri
+ makeDeposit(amount, paytoUri)
+ }
+
+ @UiThread
+ fun onDepositButtonClicked(amount: Amount, bitcoinAddress: String) {
+ val paytoUri: String = PaytoUriBitcoin(
+ segwitAddresses = listOf(bitcoinAddress),
+ targetPath = bitcoinAddress,
+ ).paytoUri
+ makeDeposit(amount, paytoUri)
+ }
+
+ private fun makeDeposit(amount: Amount, uri: String) {
+ if (depositState.value is DepositState.FeesChecked) makeDeposit(
+ paytoUri = uri,
+ amount = amount,
+ totalDepositCost = depositState.value.totalDepositCost
+ ?: Amount.zero(amount.currency),
+ effectiveDepositAmount = depositState.value.effectiveDepositAmount
+ ?: Amount.zero(amount.currency),
+ ) else {
+ prepareDeposit(uri, amount)
+ }
+ }
+
+ private fun prepareDeposit(paytoUri: String, amount: Amount) {
+ mDepositState.value = DepositState.CheckingFees
+ scope.launch {
+ api.request("prepareDeposit", PrepareDepositResponse.serializer()) {
+ put("depositPaytoUri", paytoUri)
+ put("amount", amount.toJSONString())
+ }.onError {
+ Log.e(TAG, "Error prepareDeposit $it")
+ mDepositState.value = DepositState.Error(it)
+ }.onSuccess {
+ mDepositState.value = DepositState.FeesChecked(
+ totalDepositCost = it.totalDepositCost,
+ effectiveDepositAmount = it.effectiveDepositAmount,
+ )
+ }
+ }
+ }
+
+ private fun makeDeposit(
+ paytoUri: String,
+ amount: Amount,
+ totalDepositCost: Amount,
+ effectiveDepositAmount: Amount,
+ ) {
+ mDepositState.value = DepositState.MakingDeposit(
+ totalDepositCost = totalDepositCost,
+ effectiveDepositAmount = effectiveDepositAmount,
+ )
+ scope.launch {
+ api.request("createDepositGroup", CreateDepositGroupResponse.serializer()) {
+ put("depositPaytoUri", paytoUri)
+ put("amount", amount.toJSONString())
+ }.onError {
+ Log.e(TAG, "Error createDepositGroup $it")
+ mDepositState.value = DepositState.Error(it)
+ }.onSuccess {
+ mDepositState.value = DepositState.Success
+ }
+ }
+ }
+
+ @UiThread
+ fun resetDepositState() {
+ mDepositState.value = DepositState.Start
+ }
+}
+
+@Serializable
+data class ValidateIbanResponse(
+ val valid: Boolean,
+)
+
+@Serializable
+data class PrepareDepositResponse(
+ val totalDepositCost: Amount,
+ val effectiveDepositAmount: Amount,
+)
+
+@Serializable
+data class CreateDepositGroupResponse(
+ val depositGroupId: String,
+ val transactionId: String,
+)
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt
new file mode 100644
index 0000000..168378f
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt
@@ -0,0 +1,49 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.deposit
+
+import net.taler.common.Amount
+import net.taler.wallet.backend.TalerErrorInfo
+
+sealed class DepositState {
+
+ open val showFees: Boolean = false
+ open val totalDepositCost: Amount? = null
+ open val effectiveDepositAmount: Amount? = null
+
+ object Start : DepositState()
+ object CheckingFees : DepositState()
+ object IbanInvalid : DepositState()
+ class FeesChecked(
+ override val totalDepositCost: Amount,
+ override val effectiveDepositAmount: Amount,
+ ) : DepositState() {
+ override val showFees = true
+ }
+
+ class MakingDeposit(
+ override val totalDepositCost: Amount,
+ override val effectiveDepositAmount: Amount,
+ ) : DepositState() {
+ override val showFees = true
+ }
+
+ object Success : DepositState()
+
+ class Error(val error: TalerErrorInfo) : DepositState()
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/MakeBitcoinDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/MakeBitcoinDepositComposable.kt
new file mode 100644
index 0000000..3fa0d98
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/deposit/MakeBitcoinDepositComposable.kt
@@ -0,0 +1,159 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.deposit
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import net.taler.common.Amount
+import net.taler.wallet.CURRENCY_BTC
+import net.taler.wallet.R
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.TransactionAmountComposable
+
+@Composable
+fun MakeBitcoinDepositComposable(
+ state: DepositState,
+ amount: Amount,
+ bitcoinAddress: String? = null,
+ onMakeDeposit: (Amount, String) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ var address by rememberSaveable { mutableStateOf(bitcoinAddress ?: "") }
+ val focusRequester = remember { FocusRequester() }
+ OutlinedTextField(
+ modifier = Modifier
+ .padding(16.dp)
+ .focusRequester(focusRequester),
+ value = address,
+ singleLine = true,
+ enabled = !state.showFees,
+ onValueChange = { input ->
+ address = input
+ },
+ isError = address.isBlank(),
+ label = {
+ Text(
+ stringResource(R.string.send_deposit_bitcoin_address),
+ color = if (address.isBlank()) {
+ MaterialTheme.colorScheme.error
+ } else Color.Unspecified,
+ )
+ }
+ )
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+ val amountTitle = if (state.effectiveDepositAmount == null) {
+ R.string.amount_chosen
+ } else R.string.send_deposit_amount_effective
+ TransactionAmountComposable(
+ label = stringResource(id = amountTitle),
+ amount = state.effectiveDepositAmount ?: amount,
+ amountType = AmountType.Positive,
+ )
+ AnimatedVisibility(visible = state.showFees) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ val totalAmount = state.totalDepositCost ?: amount
+ val effectiveAmount = state.effectiveDepositAmount ?: Amount.zero(amount.currency)
+ val fee = totalAmount - effectiveAmount
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee,
+ amountType = AmountType.Negative,
+ )
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.send_amount),
+ amount = totalAmount,
+ amountType = AmountType.Positive,
+ )
+ }
+ }
+ AnimatedVisibility(visible = state is DepositState.Error) {
+ Text(
+ modifier = Modifier.padding(16.dp),
+ fontSize = 18.sp,
+ color = MaterialTheme.colorScheme.error,
+ text = (state as? DepositState.Error)?.error?.userFacingMsg ?: "",
+ )
+ }
+ val focusManager = LocalFocusManager.current
+ Button(
+ modifier = Modifier.padding(16.dp),
+ enabled = address.isNotBlank(),
+ onClick = {
+ focusManager.clearFocus()
+ // TODO validate bitcoin address
+ onMakeDeposit(amount, address)
+ },
+ ) {
+ Text(text = stringResource(
+ if (state.showFees) R.string.send_deposit_bitcoin_create_button
+ else R.string.send_deposit_check_fees_button
+ ))
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewMakeBitcoinDepositComposable() {
+ Surface {
+ val state = DepositState.FeesChecked(
+ effectiveDepositAmount = Amount.fromString(CURRENCY_BTC, "42.00"),
+ totalDepositCost = Amount.fromString(CURRENCY_BTC, "42.23"),
+ )
+ MakeBitcoinDepositComposable(
+ state = state,
+ amount = Amount.fromString(CURRENCY_BTC, "42.23")) { _, _ ->
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt
new file mode 100644
index 0000000..9333ce1
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt
@@ -0,0 +1,192 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.deposit
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.transactions.AmountType.Negative
+import net.taler.wallet.transactions.AmountType.Positive
+import net.taler.wallet.transactions.TransactionAmountComposable
+
+@Composable
+fun MakeDepositComposable(
+ state: DepositState,
+ amount: Amount,
+ presetName: String? = null,
+ presetIban: String? = null,
+ onMakeDeposit: (Amount, String, String) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ var name by rememberSaveable { mutableStateOf(presetName ?: "") }
+ var iban by rememberSaveable { mutableStateOf(presetIban ?: "") }
+ val focusRequester = remember { FocusRequester() }
+ OutlinedTextField(
+ modifier = Modifier
+ .padding(16.dp)
+ .focusRequester(focusRequester)
+ .fillMaxWidth(),
+ value = name,
+ enabled = !state.showFees,
+ onValueChange = { input ->
+ name = input
+ },
+ singleLine = true,
+ isError = name.isBlank(),
+ label = {
+ Text(
+ stringResource(R.string.send_deposit_name),
+ color = if (name.isBlank()) {
+ MaterialTheme.colorScheme.error
+ } else Color.Unspecified,
+ )
+ }
+ )
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+ val ibanError = state is DepositState.IbanInvalid
+ OutlinedTextField(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ value = iban,
+ singleLine = true,
+ enabled = !state.showFees,
+ onValueChange = { input ->
+ iban = input.uppercase()
+ },
+ isError = ibanError,
+ supportingText = {
+ if (ibanError) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.send_deposit_iban_error),
+ color = MaterialTheme.colorScheme.error
+ )
+ }
+ },
+ label = {
+ Text(
+ text = stringResource(R.string.send_deposit_iban),
+ color = if (ibanError) {
+ MaterialTheme.colorScheme.error
+ } else Color.Unspecified,
+ )
+ }
+ )
+ TransactionAmountComposable(
+ label = stringResource(R.string.amount_chosen),
+ amount = amount,
+ amountType = Positive,
+ )
+ AnimatedVisibility(visible = state.showFees) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ val totalAmount = state.totalDepositCost ?: amount
+ val effectiveAmount = state.effectiveDepositAmount ?: Amount.zero(amount.currency)
+ val fee = totalAmount - effectiveAmount
+
+ TransactionAmountComposable(
+ label = stringResource(R.string.withdraw_fees),
+ amount = fee.withSpec(amount.spec),
+ amountType = if (fee.isZero()) Positive else Negative,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(R.string.send_amount),
+ amount = effectiveAmount.withSpec(amount.spec),
+ amountType = Positive,
+ )
+ }
+ }
+ AnimatedVisibility(visible = state is DepositState.Error) {
+ Text(
+ modifier = Modifier.padding(16.dp),
+ fontSize = 18.sp,
+ color = MaterialTheme.colorScheme.error,
+ text = (state as? DepositState.Error)?.error?.userFacingMsg ?: "",
+ )
+ }
+ val focusManager = LocalFocusManager.current
+ Button(
+ modifier = Modifier.padding(16.dp),
+ enabled = iban.isNotBlank(),
+ onClick = {
+ focusManager.clearFocus()
+ onMakeDeposit(amount, name, iban)
+ },
+ ) {
+ Text(
+ text = stringResource(
+ if (state is DepositState.FeesChecked) R.string.send_deposit_create_button
+ else R.string.send_deposit_check_fees_button
+ )
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewMakeDepositComposable() {
+ Surface {
+ val state = DepositState.FeesChecked(
+ effectiveDepositAmount = Amount.fromString("TESTKUDOS", "42.00"),
+ totalDepositCost = Amount.fromString("TESTKUDOS", "42.23"),
+ )
+ MakeDepositComposable(
+ state = state,
+ amount = Amount.fromString("TESTKUDOS", "42.23")) { _, _, _ ->
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt
new file mode 100644
index 0000000..8961016
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt
@@ -0,0 +1,242 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.deposit
+
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import net.taler.common.Amount
+import net.taler.wallet.AmountResult
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.AmountInputField
+import net.taler.wallet.compose.TalerSurface
+
+class PayToUriFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val depositManager get() = model.depositManager
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ val uri = arguments?.getString("uri") ?: error("no amount passed")
+
+ val currencies = model.getCurrencies()
+ return ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ if (currencies.isEmpty()) Text(
+ text = stringResource(id = R.string.payment_balance_insufficient),
+ color = MaterialTheme.colorScheme.error,
+ ) else if (depositManager.isSupportedPayToUri(uri)) PayToComposable(
+ currencies = currencies,
+ getAmount = model::createAmount,
+ onAmountChosen = { amount ->
+ val u = Uri.parse(uri)
+ val bundle = bundleOf(
+ "amount" to amount.toJSONString(),
+ "receiverName" to u.getQueryParameters("receiver-name")[0],
+ "IBAN" to u.pathSegments.last(),
+ )
+ findNavController().navigate(
+ R.id.action_nav_payto_uri_to_nav_deposit, bundle)
+ },
+ ) else Text(
+ text = stringResource(id = R.string.uri_invalid),
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.send_deposit_title)
+ }
+
+}
+
+@Composable
+private fun PayToComposable(
+ currencies: List<String>,
+ getAmount: (String, String) -> AmountResult,
+ onAmountChosen: (Amount) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp)
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ var amountText by rememberSaveable { mutableStateOf("0") }
+ var amountError by rememberSaveable { mutableStateOf("") }
+ var currency by rememberSaveable { mutableStateOf(currencies[0]) }
+ val focusRequester = remember { FocusRequester() }
+ AmountInputField(
+ modifier = Modifier.focusRequester(focusRequester),
+ value = amountText,
+ onValueChange = { input ->
+ amountError = ""
+ amountText = input
+ },
+ label = { Text(stringResource(R.string.send_amount)) },
+ supportingText = {
+ if (amountError.isNotBlank()) Text(amountError)
+ },
+ isError = amountError.isNotBlank(),
+ )
+ CurrencyDropdown(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(Center),
+ currencies = currencies,
+ onCurrencyChanged = { c -> currency = c },
+ )
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
+ val focusManager = LocalFocusManager.current
+ val errorStrInvalidAmount = stringResource(id = R.string.receive_amount_invalid)
+ val errorStrInsufficientBalance = stringResource(id = R.string.payment_balance_insufficient)
+ Button(
+ modifier = Modifier.padding(16.dp),
+ enabled = amountText.isNotBlank(),
+ onClick = {
+ when (val amountResult = getAmount(amountText, currency)) {
+ is AmountResult.Success -> {
+ focusManager.clearFocus()
+ onAmountChosen(amountResult.amount)
+ }
+ is AmountResult.InvalidAmount -> amountError = errorStrInvalidAmount
+ is AmountResult.InsufficientBalance -> amountError = errorStrInsufficientBalance
+ }
+ },
+ ) {
+ Text(text = stringResource(R.string.send_deposit_check_fees_button))
+ }
+ }
+}
+
+@Composable
+fun CurrencyDropdown(
+ currencies: List<String>,
+ onCurrencyChanged: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ initialCurrency: String? = null,
+ readOnly: Boolean = false,
+) {
+ val initialIndex = currencies.indexOf(initialCurrency).let { if (it < 0) 0 else it }
+ var selectedIndex by remember { mutableStateOf(initialIndex) }
+ var expanded by remember { mutableStateOf(false) }
+ Box(
+ modifier = modifier,
+ ) {
+ OutlinedTextField(
+ modifier = Modifier
+ .clickable(onClick = { if (!readOnly) expanded = true }),
+ value = currencies[selectedIndex],
+ onValueChange = { },
+ readOnly = true,
+ enabled = false,
+ textStyle = LocalTextStyle.current.copy( // show text as if not disabled
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ ),
+ singleLine = true,
+ label = {
+ Text(stringResource(R.string.currency))
+ }
+ )
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ modifier = Modifier,
+ ) {
+ currencies.forEachIndexed { index, s ->
+ DropdownMenuItem(
+ text = {
+ Text(text = s)
+ },
+ onClick = {
+ selectedIndex = index
+ onCurrencyChanged(currencies[index])
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewPayToComposable() {
+ Surface {
+ PayToComposable(
+ currencies = listOf("KUDOS", "TESTKUDOS", "BTCBITCOIN"),
+ getAmount = { _, _ -> AmountResult.InvalidAmount },
+ onAmountChosen = {},
+ )
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt
new file mode 100644
index 0000000..817dfac
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt
@@ -0,0 +1,119 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.deposit
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.ErrorTransactionButton
+import net.taler.wallet.transactions.TransactionAction
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionDeposit
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionState
+import net.taler.wallet.transactions.TransitionsComposable
+
+@Composable
+fun TransactionDepositComposable(
+ t: TransactionDeposit,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_chosen),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+
+ val fee = t.amountEffective - t.amountRaw
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_sent),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
+ }
+}
+
+@Preview
+@Composable
+fun TransactionDepositComposablePreview() {
+ val t = TransactionDeposit(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ depositGroupId = "fooBar",
+ amountRaw = Amount.fromString("TESTKUDOS", "42.1337"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.23"),
+ targetPaytoUri = "https://exchange.example.org/peer/pull/credit",
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
+ )
+ Surface {
+ TransactionDepositComposable(t, true, null) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt
index 5ea763a..fd942b0 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt
@@ -22,6 +22,7 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
@@ -32,7 +33,7 @@ class AddExchangeDialogFragment : DialogFragment() {
private val exchangeManager by lazy { model.exchangeManager }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- return AlertDialog.Builder(requireContext(), R.style.DialogTheme)
+ return MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
.setIcon(R.drawable.ic_account_balance)
.setTitle(R.string.exchange_list_add)
.setView(R.layout.dialog_exchange_add)
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
index e315632..cb294ac 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
@@ -26,23 +26,14 @@ import android.widget.TextView
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
-import kotlinx.serialization.Serializable
import net.taler.wallet.R
-import net.taler.wallet.cleanExchange
import net.taler.wallet.exchanges.ExchangeAdapter.ExchangeItemViewHolder
-@Serializable
-data class ExchangeItem(
- val exchangeBaseUrl: String,
- val currency: String,
- val paytoUris: List<String>
-) {
- val name: String get() = cleanExchange(exchangeBaseUrl)
-}
-
interface ExchangeClickListener {
fun onExchangeSelected(item: ExchangeItem)
fun onManualWithdraw(item: ExchangeItem)
+ fun onPeerReceive(item: ExchangeItem)
+ fun onExchangeDelete(item: ExchangeItem)
}
internal class ExchangeAdapter(
@@ -78,14 +69,20 @@ internal class ExchangeAdapter(
fun bind(item: ExchangeItem) {
urlView.text = item.name
- currencyView.text = context.getString(R.string.exchange_list_currency, item.currency)
+ // If currency is null, it's because we have no data from the exchange...
+ currencyView.text = if (item.currency == null) {
+ context.getString(R.string.exchange_not_contacted)
+ } else {
+ context.getString(R.string.exchange_list_currency, item.currency)
+ }
if (selectOnly) {
itemView.setOnClickListener { listener.onExchangeSelected(item) }
overflowIcon.visibility = GONE
} else {
itemView.setOnClickListener(null)
itemView.isClickable = false
- overflowIcon.visibility = VISIBLE
+ // ...thus, we should prevent the user from interacting with it.
+ overflowIcon.visibility = if (item.currency != null) VISIBLE else GONE
}
overflowIcon.setOnClickListener { openMenu(overflowIcon, item) }
}
@@ -98,6 +95,14 @@ internal class ExchangeAdapter(
listener.onManualWithdraw(item)
true
}
+ R.id.action_receive_peer -> {
+ listener.onPeerReceive(item)
+ true
+ }
+ R.id.action_delete -> {
+ listener.onExchangeDelete(item)
+ true
+ }
else -> false
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt
index 1da9b49..7824d1e 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt
@@ -16,8 +16,9 @@
package net.taler.wallet.exchanges
-import net.taler.lib.common.Amount
-import net.taler.lib.common.Timestamp
+import net.taler.common.Amount
+import net.taler.common.Timestamp
+
data class CoinFee(
val coin: Amount,
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt
index c59fffe..d42ab2c 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt
@@ -22,19 +22,19 @@ import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import android.widget.TextView
-import androidx.core.content.ContextCompat.getColor
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.common.Amount
import net.taler.common.toRelativeTime
import net.taler.common.toShortDate
-import net.taler.lib.common.Amount
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.databinding.FragmentExchangeFeesBinding
import net.taler.wallet.exchanges.CoinFeeAdapter.CoinFeeViewHolder
import net.taler.wallet.exchanges.WireFeeAdapter.WireFeeViewHolder
+import net.taler.wallet.getAttrColor
class ExchangeFeesFragment : Fragment() {
@@ -71,7 +71,7 @@ class ExchangeFeesFragment : Fragment() {
if (amount.isZero()) text = amount.toString()
else {
text = getString(R.string.amount_negative, amount)
- setTextColor(getColor(context, R.color.red))
+ setText(requireContext().getAttrColor(R.attr.colorError))
}
}
@@ -89,7 +89,7 @@ private class CoinFeeAdapter(private val items: List<CoinFee>) : Adapter<CoinFee
holder.bind(items[position])
}
- private class CoinFeeViewHolder(private val v: View) : ViewHolder(v) {
+ class CoinFeeViewHolder(private val v: View) : ViewHolder(v) {
private val res = v.context.resources
private val coinView: TextView = v.findViewById(R.id.coinView)
private val withdrawFeeView: TextView = v.findViewById(R.id.withdrawFeeView)
@@ -127,7 +127,7 @@ private class WireFeeAdapter(private val items: List<WireFee>) : Adapter<WireFee
holder.bind(items[position])
}
- private class WireFeeViewHolder(private val v: View) : ViewHolder(v) {
+ class WireFeeViewHolder(private val v: View) : ViewHolder(v) {
private val validityView: TextView = v.findViewById(R.id.validityView)
private val wireFeeView: TextView = v.findViewById(R.id.wireFeeView)
private val closingFeeView: TextView = v.findViewById(R.id.closingFeeView)
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt
index 9a96b59..5482b5a 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt
@@ -18,26 +18,35 @@ package net.taler.wallet.exchanges
import android.os.Bundle
import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
+import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import net.taler.common.EventObserver
import net.taler.common.fadeIn
import net.taler.common.fadeOut
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.databinding.FragmentExchangeListBinding
+import net.taler.wallet.showError
open class ExchangeListFragment : Fragment(), ExchangeClickListener {
protected val model: MainViewModel by activityViewModels()
private val exchangeManager by lazy { model.exchangeManager }
+ private val transactionManager get() = model.transactionManager
protected lateinit var ui: FragmentExchangeListBinding
protected open val isSelectOnly = false
@@ -53,6 +62,21 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ requireActivity().addMenuProvider(object : MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ if (model.devMode.value == true) {
+ menuInflater.inflate(R.menu.exchange_list, menu)
+ }
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ if (menuItem.itemId == R.id.action_add_dev_exchanges) {
+ exchangeManager.addDevExchanges()
+ }
+ return true
+ }
+ }, viewLifecycleOwner, RESUMED)
+
ui.list.apply {
adapter = exchangeAdapter
addItemDecoration(DividerItemDecoration(context, VERTICAL))
@@ -61,14 +85,30 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
AddExchangeDialogFragment().show(parentFragmentManager, "ADD_EXCHANGE")
}
- exchangeManager.progress.observe(viewLifecycleOwner, { show ->
+ exchangeManager.progress.observe(viewLifecycleOwner) { show ->
if (show) ui.progressBar.fadeIn() else ui.progressBar.fadeOut()
- })
- exchangeManager.exchanges.observe(viewLifecycleOwner, { exchanges ->
+ }
+ exchangeManager.exchanges.observe(viewLifecycleOwner) { exchanges ->
onExchangeUpdate(exchanges)
- })
+ }
exchangeManager.addError.observe(viewLifecycleOwner, EventObserver { error ->
- if (error) onAddExchangeFailed()
+ onAddExchangeFailed()
+ if (model.devMode.value == true) {
+ showError(error)
+ }
+ })
+ exchangeManager.listError.observe(viewLifecycleOwner, EventObserver { error ->
+ onListExchangeFailed()
+ if (model.devMode.value == true) {
+ showError(error)
+ }
+ })
+ exchangeManager.deleteError.observe(viewLifecycleOwner, EventObserver { error ->
+ if (model.devMode.value == true) {
+ showError(error)
+ } else {
+ showError(error.userFacingMsg)
+ }
})
}
@@ -87,6 +127,10 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
Toast.makeText(requireContext(), R.string.exchange_add_error, LENGTH_LONG).show()
}
+ private fun onListExchangeFailed() {
+ Toast.makeText(requireContext(), R.string.exchange_list_error, LENGTH_LONG).show()
+ }
+
override fun onExchangeSelected(item: ExchangeItem) {
throw AssertionError("must not get triggered here")
}
@@ -96,4 +140,24 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
findNavController().navigate(R.id.action_nav_settings_exchanges_to_nav_exchange_manual_withdrawal)
}
+ override fun onPeerReceive(item: ExchangeItem) {
+ transactionManager.selectedScope = item.scopeInfo
+ findNavController().navigate(R.id.action_global_receiveFunds)
+ }
+
+ override fun onExchangeDelete(item: ExchangeItem) {
+ val optionsArray = arrayOf(getString(R.string.exchange_delete_force))
+ val checkedArray = BooleanArray(1) { false }
+
+ MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setTitle(R.string.exchange_delete)
+ .setMultiChoiceItems(optionsArray, checkedArray) { _, which, isChecked ->
+ checkedArray[which] = isChecked
+ }
+ .setNegativeButton(R.string.transactions_delete) { _, _ ->
+ exchangeManager.delete(item.exchangeBaseUrl, checkedArray[0])
+ }
+ .setPositiveButton(R.string.cancel) { _, _ -> }
+ .show()
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
index 8205eb7..eb01cab 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
@@ -17,24 +17,34 @@
package net.taler.wallet.exchanges
import android.util.Log
+import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import net.taler.common.Event
import net.taler.common.toEvent
import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
@Serializable
data class ExchangeListResponse(
- val exchanges: List<ExchangeItem>
+ val exchanges: List<ExchangeItem>,
+)
+
+@Serializable
+data class ExchangeDetailedResponse(
+ val exchange: ExchangeItem,
)
class ExchangeManager(
private val api: WalletBackendApi,
- private val scope: CoroutineScope
+ private val scope: CoroutineScope,
) {
private val mProgress = MutableLiveData<Boolean>()
@@ -43,8 +53,14 @@ class ExchangeManager(
private val mExchanges = MutableLiveData<List<ExchangeItem>>()
val exchanges: LiveData<List<ExchangeItem>> get() = list()
- private val mAddError = MutableLiveData<Event<Boolean>>()
- val addError: LiveData<Event<Boolean>> = mAddError
+ private val mAddError = MutableLiveData<Event<TalerErrorInfo>>()
+ val addError: LiveData<Event<TalerErrorInfo>> = mAddError
+
+ private val mListError = MutableLiveData<Event<TalerErrorInfo>>()
+ val listError: LiveData<Event<TalerErrorInfo>> = mListError
+
+ private val mDeleteError = MutableLiveData<Event<TalerErrorInfo>>()
+ val deleteError: LiveData<Event<TalerErrorInfo>> = mDeleteError
var withdrawalExchange: ExchangeItem? = null
@@ -53,7 +69,8 @@ class ExchangeManager(
scope.launch {
val response = api.request("listExchanges", ExchangeListResponse.serializer())
response.onError {
- throw AssertionError("Wallet core failed to return exchanges!")
+ mProgress.value = false
+ mListError.value = it.toEvent()
}.onSuccess {
Log.d(TAG, "Exchange list: ${it.exchanges}")
mProgress.value = false
@@ -68,9 +85,9 @@ class ExchangeManager(
api.request<Unit>("addExchange") {
put("exchangeBaseUrl", exchangeUrl)
}.onError {
- mProgress.value = false
Log.e(TAG, "Error adding exchange: $it")
- mAddError.value = true.toEvent()
+ mProgress.value = false
+ mAddError.value = it.toEvent()
}.onSuccess {
mProgress.value = false
Log.d(TAG, "Exchange $exchangeUrl added")
@@ -78,4 +95,82 @@ class ExchangeManager(
}
}
+ fun delete(exchangeUrl: String, purge: Boolean = false) = scope.launch {
+ mProgress.value = true
+ api.request<Unit>("deleteExchange") {
+ put("exchangeBaseUrl", exchangeUrl)
+ put("purge", purge)
+ }.onError {
+ Log.e(TAG, "Error deleting exchange: $it")
+ mProgress.value = false
+ mDeleteError.value = it.toEvent()
+ }.onSuccess {
+ mProgress.value = false
+ Log.d(TAG, "Exchange $exchangeUrl deleted")
+ list()
+ }
+ }
+
+ fun findExchangeForCurrency(currency: String): Flow<ExchangeItem?> = flow {
+ emit(findExchange(currency))
+ }
+
+ @WorkerThread
+ suspend fun findExchange(currency: String): ExchangeItem? {
+ var exchange: ExchangeItem? = null
+ api.request(
+ operation = "listExchanges",
+ serializer = ExchangeListResponse.serializer()
+ ).onSuccess { exchangeListResponse ->
+ // just pick the first for now
+ exchange = exchangeListResponse.exchanges.find { it.currency == currency }
+ }
+ return exchange
+ }
+
+ @WorkerThread
+ suspend fun findExchangeByUrl(exchangeUrl: String): ExchangeItem? {
+ var exchange: ExchangeItem? = null
+ api.request("getExchangeDetailedInfo", ExchangeDetailedResponse.serializer()) {
+ put("exchangeBaseUrl", exchangeUrl)
+ }.onError {
+ Log.e(TAG, "Error getExchangeDetailedInfo: $it")
+ }.onSuccess {
+ exchange = it.exchange
+ }
+ return exchange
+ }
+
+ fun addDevExchanges() {
+ scope.launch {
+ listOf(
+ "https://exchange.demo.taler.net/",
+ "https://exchange.test.taler.net/",
+ "https://exchange.head.taler.net/",
+ "https://exchange.taler.ar/",
+ "https://exchange.taler.fdold.eu/",
+ "https://exchange.taler.grothoff.org/",
+ ).forEach { exchangeUrl ->
+ add(exchangeUrl)
+ delay(100)
+ }
+ exchanges.value?.let { exs ->
+ exs.find {
+ it.exchangeBaseUrl.startsWith("https://exchange.taler.fdold.eu")
+ }?.let { fDoldExchange ->
+ api.request<Unit>("addGlobalCurrencyExchange") {
+ put("currency", fDoldExchange.currency)
+ put("exchangeBaseUrl", fDoldExchange.exchangeBaseUrl)
+ put("exchangeMasterPub",
+ "7ER30ZWJEXAG026H5KG9M19NGTFC2DKKFPV79GVXA6DK5DCNSWXG")
+ }.onError {
+ Log.e(TAG, "Error addGlobalCurrencyExchange: $it")
+ }.onSuccess {
+ Log.i(TAG, "fdold is global now!")
+ }
+ }
+ }
+ }
+ }
+
}
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
new file mode 100644
index 0000000..ce0bd82
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
@@ -0,0 +1,32 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.exchanges
+
+import kotlinx.serialization.Serializable
+import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.cleanExchange
+
+@Serializable
+data class ExchangeItem(
+ val exchangeBaseUrl: String,
+ // can be null before exchange info in wallet-core was fully loaded
+ val currency: String? = null,
+ val paytoUris: List<String>,
+ val scopeInfo: ScopeInfo? = null,
+) {
+ val name: String get() = cleanExchange(exchangeBaseUrl)
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt
new file mode 100644
index 0000000..136738b
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt
@@ -0,0 +1,111 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.exchanges
+
+import android.app.Dialog
+import android.os.Bundle
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asFlow
+import com.google.accompanist.themeadapter.material3.Mdc3Theme
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import net.taler.common.Event
+import net.taler.common.toEvent
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+
+class SelectExchangeDialogFragment: DialogFragment() {
+ private var exchangeList = MutableLiveData<List<ExchangeItem>>()
+
+ private var mExchangeSelection = MutableLiveData<Event<ExchangeItem>>()
+ val exchangeSelection: LiveData<Event<ExchangeItem>> = mExchangeSelection
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val view = ComposeView(requireContext()).apply {
+ setContent {
+ val exchanges = exchangeList.asFlow().collectAsStateLifecycleAware(initial = emptyList())
+ SelectExchangeComposable(exchanges.value) {
+ onExchangeSelected(it)
+ }
+ }
+ }
+
+ return MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setIcon(R.drawable.ic_account_balance)
+ .setTitle(R.string.exchange_list_select)
+ .setView(view)
+ .setNegativeButton(R.string.cancel) { _, _ ->
+ dismiss()
+ }
+ .create()
+ }
+
+ fun setExchanges(exchanges: List<ExchangeItem>) {
+ exchangeList.value = exchanges
+ }
+
+ private fun onExchangeSelected(exchange: ExchangeItem) {
+ mExchangeSelection.value = exchange.toEvent()
+ dismiss()
+ }
+}
+
+@Composable
+fun SelectExchangeComposable(
+ exchanges: List<ExchangeItem>,
+ onExchangeSelected: (exchange: ExchangeItem) -> Unit,
+) {
+ Mdc3Theme {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ items(exchanges) {
+ ExchangeItemComposable(it) {
+ onExchangeSelected(it)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ExchangeItemComposable(exchange: ExchangeItem, onSelected: () -> Unit) {
+ ListItem(
+ modifier = Modifier.clickable { onSelected() },
+ headlineContent = { Text(cleanExchange(exchange.exchangeBaseUrl)) },
+ supportingContent = exchange.currency?.let {
+ { Text(stringResource(R.string.exchange_list_currency, it)) }
+ },
+ colors = ListItemDefaults.colors(
+ containerColor = Color.Transparent,
+ )
+ )
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt
deleted file mode 100644
index 61e0db5..0000000
--- a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.exchanges
-
-import androidx.navigation.fragment.findNavController
-import net.taler.common.fadeOut
-
-class SelectExchangeFragment : ExchangeListFragment() {
-
- private val withdrawManager by lazy { model.withdrawManager }
-
- override val isSelectOnly = true
- private val exchangeSelection by lazy {
- requireNotNull(withdrawManager.exchangeSelection.value?.getEvenIfConsumedAlready())
- }
-
- override fun onExchangeUpdate(exchanges: List<ExchangeItem>) {
- ui.progressBar.fadeOut()
- super.onExchangeUpdate(exchanges.filter { exchangeItem ->
- exchangeItem.currency == exchangeSelection.amount.currency
- })
- }
-
- override fun onExchangeSelected(item: ExchangeItem) {
- withdrawManager.getWithdrawalDetails(
- exchangeBaseUrl = item.exchangeBaseUrl,
- amount = exchangeSelection.amount,
- showTosImmediately = true,
- uri = exchangeSelection.talerWithdrawUri,
- )
- findNavController().navigateUp()
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt
deleted file mode 100644
index df2b2b8..0000000
--- a/wallet/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.payment
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.navigation.fragment.findNavController
-import net.taler.wallet.databinding.FragmentAlreadyPaidBinding
-
-/**
- * Display the message that the user already paid for the order
- * that the merchant is proposing.
- */
-class AlreadyPaidFragment : Fragment() {
-
- private lateinit var ui: FragmentAlreadyPaidBinding
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- ui = FragmentAlreadyPaidBinding.inflate(inflater, container, false)
- return ui.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- ui.backButton.setOnClickListener {
- findNavController().navigateUp()
- }
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
new file mode 100644
index 0000000..b6c2fb1
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
@@ -0,0 +1,187 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.payment
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import net.taler.common.Amount
+import net.taler.common.ContractTerms
+import net.taler.wallet.AmountResult
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+
+sealed class AmountFieldStatus {
+ object FixedAmount : AmountFieldStatus()
+ class Default(
+ val amountStr: String? = null,
+ val currency: String? = null,
+ ) : AmountFieldStatus()
+
+ object Invalid : AmountFieldStatus()
+}
+
+@Composable
+fun PayTemplateComposable(
+ defaultSummary: String?,
+ amountStatus: AmountFieldStatus,
+ currencies: List<String>,
+ payStatus: PayStatus,
+ onCreateAmount: (String, String) -> AmountResult,
+ onSubmit: (summary: String?, amount: Amount?) -> Unit,
+ onError: (resId: Int) -> Unit,
+) {
+ // If wallet is empty, there's no way the user can pay something
+ if (amountStatus is AmountFieldStatus.Invalid) {
+ PayTemplateError(stringResource(R.string.receive_amount_invalid))
+ } else if (currencies.isEmpty()) {
+ PayTemplateError(stringResource(R.string.payment_balance_insufficient))
+ } else when (val p = payStatus) {
+ is PayStatus.None -> PayTemplateOrderComposable(
+ currencies = currencies,
+ defaultSummary = defaultSummary,
+ amountStatus = amountStatus,
+ onCreateAmount = onCreateAmount,
+ onError = onError,
+ onSubmit = onSubmit,
+ )
+
+ is PayStatus.Loading -> PayTemplateLoading()
+ is PayStatus.AlreadyPaid -> PayTemplateError(stringResource(R.string.payment_already_paid))
+ is PayStatus.InsufficientBalance -> PayTemplateError(stringResource(R.string.payment_balance_insufficient))
+ is PayStatus.Pending -> {
+ val error = p.error
+ PayTemplateError(if (error != null) {
+ stringResource(R.string.payment_error, error.userFacingMsg)
+ } else {
+ stringResource(R.string.payment_template_error)
+ })
+ }
+ is PayStatus.Prepared -> {} // handled in fragment, will redirect
+ is PayStatus.Success -> {} // handled by other UI flow, no need for content here
+ }
+}
+
+@Composable
+fun PayTemplateError(message: String) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Center,
+ ) {
+ Text(
+ text = message,
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+}
+
+@Composable
+fun PayTemplateLoading() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Center,
+ ) {
+ CircularProgressIndicator()
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateLoadingPreview() {
+ TalerSurface {
+ PayTemplateComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ payStatus = PayStatus.Loading,
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { _ -> },
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateInsufficientBalancePreview() {
+ TalerSurface {
+ PayTemplateComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ payStatus = PayStatus.InsufficientBalance(
+ ContractTerms(
+ "test",
+ amount = Amount.zero("TESTKUDOS"),
+ products = emptyList()
+ ), Amount.zero("TESTKUDOS")
+ ),
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { _ -> },
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateAlreadyPaidPreview() {
+ TalerSurface {
+ PayTemplateComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ payStatus = PayStatus.AlreadyPaid(transactionId = "transactionId"),
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { _ -> },
+ )
+ }
+}
+
+
+@Preview
+@Composable
+fun PayTemplateNoCurrenciesPreview() {
+ TalerSurface {
+ PayTemplateComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ payStatus = PayStatus.None,
+ currencies = emptyList(),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { _ -> },
+ )
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt
new file mode 100644
index 0000000..4eb2c11
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt
@@ -0,0 +1,115 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.payment
+
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.asFlow
+import androidx.navigation.fragment.findNavController
+import net.taler.common.Amount
+import net.taler.common.showError
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
+
+class PayTemplateFragment : Fragment() {
+
+ private val model: MainViewModel by activityViewModels()
+ private lateinit var uriString: String
+ private lateinit var uri: Uri
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ uriString = arguments?.getString("uri") ?: error("no amount passed")
+ uri = Uri.parse(uriString)
+
+ val defaultSummary = uri.getQueryParameter("summary")
+ val defaultAmount = uri.getQueryParameter("amount")
+ val amountFieldStatus = getAmountFieldStatus(defaultAmount)
+
+ val payStatusFlow = model.paymentManager.payStatus.asFlow()
+
+ return ComposeView(requireContext()).apply {
+ setContent {
+ val payStatus = payStatusFlow.collectAsStateLifecycleAware(initial = PayStatus.None)
+ TalerSurface {
+ PayTemplateComposable(
+ currencies = model.getCurrencies(),
+ defaultSummary = defaultSummary,
+ amountStatus = amountFieldStatus,
+ payStatus = payStatus.value,
+ onCreateAmount = model::createAmount,
+ onSubmit = this@PayTemplateFragment::createOrder,
+ onError = { this@PayTemplateFragment.showError(it) },
+ )
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ if (uri.queryParameterNames?.isEmpty() == true) {
+ createOrder(null, null)
+ }
+
+ model.paymentManager.payStatus.observe(viewLifecycleOwner) { payStatus ->
+ when (payStatus) {
+ is PayStatus.Prepared -> {
+ findNavController().navigate(R.id.action_promptPayTemplate_to_promptPayment)
+ }
+
+ is PayStatus.Pending -> if (payStatus.error != null && model.devMode.value == true) {
+ showError(payStatus.error)
+ }
+
+ else -> {}
+ }
+ }
+ }
+
+ private fun getAmountFieldStatus(defaultAmount: String?): AmountFieldStatus {
+ return if (defaultAmount == null) {
+ AmountFieldStatus.FixedAmount
+ } else if (defaultAmount.isBlank()) {
+ AmountFieldStatus.Default()
+ } else {
+ val parts = defaultAmount.split(":")
+ when (parts.size) {
+ 0 -> AmountFieldStatus.Default()
+ 1 -> AmountFieldStatus.Default(currency = parts[0])
+ 2 -> AmountFieldStatus.Default(parts[1], parts[0])
+ else -> AmountFieldStatus.Invalid
+ }
+ }
+ }
+
+ private fun createOrder(summary: String?, amount: Amount?) {
+ model.paymentManager.preparePayForTemplate(uriString, summary, amount)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt
new file mode 100644
index 0000000..d6131c7
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt
@@ -0,0 +1,177 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.payment
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment.Companion.End
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.AmountResult
+import net.taler.wallet.R
+import net.taler.wallet.compose.AmountInputField
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.deposit.CurrencyDropdown
+
+@Composable
+fun PayTemplateOrderComposable(
+ currencies: List<String>, // assumed to have size > 0
+ defaultSummary: String? = null,
+ amountStatus: AmountFieldStatus,
+ onCreateAmount: (String, String) -> AmountResult,
+ onError: (msgRes: Int) -> Unit,
+ onSubmit: (summary: String?, amount: Amount?) -> Unit,
+) {
+ val amountDefault = amountStatus as? AmountFieldStatus.Default
+
+ var summary by remember { mutableStateOf(defaultSummary) }
+ var currency by remember { mutableStateOf(amountDefault?.currency ?: currencies[0]) }
+ var amount by remember { mutableStateOf(amountDefault?.amountStr ?: "0") }
+
+ Column(horizontalAlignment = End) {
+ if (defaultSummary != null) OutlinedTextField(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ value = summary ?: "",
+ isError = summary.isNullOrBlank(),
+ onValueChange = { summary = it },
+ singleLine = true,
+ label = { Text(stringResource(R.string.withdraw_manual_ready_subject)) },
+ )
+ if (amountDefault != null) AmountField(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ amount = amount,
+ currency = currency,
+ currencies = currencies,
+ fixedCurrency = (amountStatus as? AmountFieldStatus.Default)?.currency != null,
+ onAmountChosen = { a, c ->
+ amount = a
+ currency = c
+ },
+ )
+ Button(
+ modifier = Modifier.padding(16.dp),
+ enabled = defaultSummary == null || !summary.isNullOrBlank(),
+ onClick = {
+ when (val res = onCreateAmount(amount, currency)) {
+ is AmountResult.InsufficientBalance -> onError(R.string.payment_balance_insufficient)
+ is AmountResult.InvalidAmount -> onError(R.string.receive_amount_invalid)
+ is AmountResult.Success -> onSubmit(summary, res.amount)
+ }
+ },
+ ) {
+ Text(stringResource(R.string.payment_create_order))
+ }
+ }
+}
+
+@Composable
+private fun AmountField(
+ modifier: Modifier = Modifier,
+ currencies: List<String>,
+ fixedCurrency: Boolean,
+ amount: String,
+ currency: String,
+ onAmountChosen: (amount: String, currency: String) -> Unit,
+) {
+ Row(
+ modifier = modifier,
+ ) {
+ AmountInputField(
+ modifier = Modifier
+ .padding(end = 16.dp)
+ .weight(1f),
+ value = amount,
+ onValueChange = { onAmountChosen(it, currency) },
+ label = { Text(stringResource(R.string.send_amount)) }
+ )
+ CurrencyDropdown(
+ modifier = Modifier.weight(1f),
+ initialCurrency = currency,
+ currencies = currencies,
+ onCurrencyChanged = { onAmountChosen(amount, it) },
+ readOnly = fixedCurrency,
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateDefaultPreview() {
+ TalerSurface {
+ PayTemplateOrderComposable(
+ defaultSummary = "Donation",
+ amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { },
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateFixedAmountPreview() {
+ TalerSurface {
+ PayTemplateOrderComposable(
+ defaultSummary = "default summary",
+ amountStatus = AmountFieldStatus.FixedAmount,
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { },
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PayTemplateBlankSubjectPreview() {
+ TalerSurface {
+ PayTemplateOrderComposable(
+ defaultSummary = "",
+ amountStatus = AmountFieldStatus.FixedAmount,
+ currencies = listOf("KUDOS", "ARS"),
+ onCreateAmount = { text, currency ->
+ AmountResult.Success(amount = Amount.fromString(currency, text))
+ },
+ onSubmit = { _, _ -> },
+ onError = { },
+ )
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
index 34a8023..35cd9e6 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
@@ -22,16 +22,17 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import net.taler.common.Amount
import net.taler.common.ContractTerms
-import net.taler.lib.common.Amount
import net.taler.wallet.TAG
-import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.payment.PayStatus.AlreadyPaid
import net.taler.wallet.payment.PayStatus.InsufficientBalance
import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse
import net.taler.wallet.payment.PreparePayResponse.InsufficientBalanceResponse
import net.taler.wallet.payment.PreparePayResponse.PaymentPossibleResponse
+import org.json.JSONObject
val REGEX_PRODUCT_IMAGE = Regex("^data:image/(jpeg|png);base64,([A-Za-z0-9+/=]+)$")
@@ -40,20 +41,28 @@ sealed class PayStatus {
object Loading : PayStatus()
data class Prepared(
val contractTerms: ContractTerms,
- val proposalId: String,
+ val transactionId: String,
val amountRaw: Amount,
- val amountEffective: Amount
+ val amountEffective: Amount,
) : PayStatus()
data class InsufficientBalance(
val contractTerms: ContractTerms,
- val amountRaw: Amount
+ val amountRaw: Amount,
) : PayStatus()
- // TODO bring user to fulfilment URI
- object AlreadyPaid : PayStatus()
- data class Error(val error: String) : PayStatus()
- data class Success(val currency: String) : PayStatus()
+ data class AlreadyPaid(
+ val transactionId: String,
+ ) : PayStatus()
+
+ data class Pending(
+ val transactionId: String? = null,
+ val error: TalerErrorInfo? = null,
+ ) : PayStatus()
+ data class Success(
+ val transactionId: String,
+ val currency: String,
+ ) : PayStatus()
}
class PaymentManager(
@@ -67,50 +76,65 @@ class PaymentManager(
@UiThread
fun preparePay(url: String) = scope.launch {
mPayStatus.value = PayStatus.Loading
- api.request("preparePay", PreparePayResponse.serializer()) {
+ api.request("preparePayForUri", PreparePayResponse.serializer()) {
put("talerPayUri", url)
}.onError {
- handleError("preparePay", it)
+ handleError("preparePayForUri", it)
}.onSuccess { response ->
mPayStatus.value = when (response) {
is PaymentPossibleResponse -> response.toPayStatusPrepared()
is InsufficientBalanceResponse -> InsufficientBalance(
- response.contractTerms,
- response.amountRaw
+ contractTerms = response.contractTerms,
+ amountRaw = response.amountRaw
+ )
+ is AlreadyConfirmedResponse -> AlreadyPaid(
+ transactionId = response.transactionId,
)
- is AlreadyConfirmedResponse -> AlreadyPaid
}
}
}
- fun confirmPay(proposalId: String, currency: String) = scope.launch {
+ fun confirmPay(transactionId: String, currency: String) = scope.launch {
api.request("confirmPay", ConfirmPayResult.serializer()) {
- put("proposalId", proposalId)
+ put("transactionId", transactionId)
}.onError {
handleError("confirmPay", it)
- }.onSuccess {
- mPayStatus.postValue(PayStatus.Success(currency))
- }
- }
-
- @UiThread
- fun abortPay() {
- val ps = payStatus.value
- if (ps is PayStatus.Prepared) {
- abortProposal(ps.proposalId)
+ }.onSuccess { response ->
+ mPayStatus.postValue(when (response) {
+ is ConfirmPayResult.Done -> PayStatus.Success(
+ transactionId = response.transactionId,
+ currency = currency,
+ )
+ is ConfirmPayResult.Pending -> PayStatus.Pending(
+ transactionId = response.transactionId,
+ error = response.lastError,
+ )
+ })
}
- resetPayStatus()
}
- internal fun abortProposal(proposalId: String) = scope.launch {
- Log.i(TAG, "aborting proposal")
- api.request<Unit>("abortProposal") {
- put("proposalId", proposalId)
+ fun preparePayForTemplate(url: String, summary: String?, amount: Amount?) = scope.launch {
+ mPayStatus.value = PayStatus.Loading
+ api.request("preparePayForTemplate", PreparePayResponse.serializer()) {
+ put("talerPayTemplateUri", url)
+ put("templateParams", JSONObject().apply {
+ summary?.let { put("summary", it) }
+ amount?.let { put("amount", it.toJSONString()) }
+ })
}.onError {
- Log.e(TAG, "received error response to abortProposal")
- handleError("abortProposal", it)
- }.onSuccess {
- mPayStatus.postValue(PayStatus.None)
+ handleError("preparePayForTemplate", it)
+ }.onSuccess { response ->
+ mPayStatus.value = when (response) {
+ is PaymentPossibleResponse -> response.toPayStatusPrepared()
+ is InsufficientBalanceResponse -> InsufficientBalance(
+ contractTerms = response.contractTerms,
+ amountRaw = response.amountRaw,
+ )
+
+ is AlreadyConfirmedResponse -> AlreadyPaid(
+ transactionId = response.transactionId,
+ )
+ }
}
}
@@ -121,7 +145,7 @@ class PaymentManager(
private fun handleError(operation: String, error: TalerErrorInfo) {
Log.e(TAG, "got $operation error result $error")
- mPayStatus.value = PayStatus.Error(error.userFacingMsg)
+ mPayStatus.value = PayStatus.Pending(error = error)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
index 4b908b5..407f55f 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
@@ -16,30 +16,30 @@
package net.taler.wallet.payment
+import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonClassDiscriminator
+import net.taler.common.Amount
import net.taler.common.ContractTerms
-import net.taler.lib.android.CustomClassDiscriminator
-import net.taler.lib.common.Amount
import net.taler.wallet.backend.TalerErrorInfo
+@OptIn(ExperimentalSerializationApi::class)
@Serializable
+@JsonClassDiscriminator("status")
sealed class PreparePayResponse {
- companion object : CustomClassDiscriminator {
- override val discriminator: String = "status"
- }
@Serializable
@SerialName("payment-possible")
data class PaymentPossibleResponse(
- val proposalId: String,
+ val transactionId: String,
val amountRaw: Amount,
val amountEffective: Amount,
val contractTerms: ContractTerms,
) : PreparePayResponse() {
fun toPayStatusPrepared() = PayStatus.Prepared(
contractTerms = contractTerms,
- proposalId = proposalId,
+ transactionId = transactionId,
amountRaw = amountRaw,
amountEffective = amountEffective,
)
@@ -48,7 +48,6 @@ sealed class PreparePayResponse {
@Serializable
@SerialName("insufficient-balance")
data class InsufficientBalanceResponse(
- val proposalId: String,
val amountRaw: Amount,
val contractTerms: ContractTerms,
) : PreparePayResponse()
@@ -56,13 +55,13 @@ sealed class PreparePayResponse {
@Serializable
@SerialName("already-confirmed")
data class AlreadyConfirmedResponse(
- val proposalId: String,
+ val transactionId: String,
/**
* Did the payment succeed?
*/
val paid: Boolean,
val amountRaw: Amount,
- val amountEffective: Amount,
+ val amountEffective: Amount? = null,
val contractTerms: ContractTerms,
) : PreparePayResponse()
}
@@ -71,9 +70,15 @@ sealed class PreparePayResponse {
sealed class ConfirmPayResult {
@Serializable
@SerialName("done")
- data class Done(val contractTerms: ContractTerms) : ConfirmPayResult()
+ data class Done(
+ val transactionId: String,
+ val contractTerms: ContractTerms,
+ ) : ConfirmPayResult()
@Serializable
@SerialName("pending")
- data class Pending(val lastError: TalerErrorInfo) : ConfirmPayResult()
+ data class Pending(
+ val transactionId: String,
+ val lastError: TalerErrorInfo? = null,
+ ) : ConfirmPayResult()
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt b/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
index 87b6387..289f0d7 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
@@ -67,21 +67,25 @@ internal class ProductAdapter(private val listener: ProductImageClickListener) :
fun bind(product: ContractProduct) {
quantity.text = product.quantity.toString()
- if (product.image == null) {
+ val productImage = product.image
+ if (productImage == null) {
image.visibility = GONE
- } else {
- image.visibility = VISIBLE
- // product.image was validated before, so non-null below
- val match = REGEX_PRODUCT_IMAGE.matchEntire(product.image!!)!!
- val decodedString = Base64.decode(match.groups[2]!!.value, Base64.DEFAULT)
- val bitmap = decodeByteArray(decodedString, 0, decodedString.size)
- image.setImageBitmap(bitmap)
- image.setOnClickListener {
- listener.onImageClick(bitmap)
+ } else REGEX_PRODUCT_IMAGE.matchEntire(productImage)?.let { match ->
+ match.groups[2]?.value?.let { group ->
+ image.visibility = VISIBLE
+ val decodedString = Base64.decode(group, Base64.DEFAULT)
+ val bitmap = decodeByteArray(decodedString, 0, decodedString.size)
+ image.setImageBitmap(bitmap)
+ image.setOnClickListener {
+ listener.onImageClick(bitmap)
+ }
}
}
name.text = product.description
- price.text = product.totalPrice.toString()
+ price.visibility = product.totalPrice?.let {
+ price.text = it.toString()
+ VISIBLE
+ } ?: GONE
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
index 664dcc9..31c26a0 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
@@ -18,24 +18,29 @@ package net.taler.wallet.payment
import android.graphics.Bitmap
import android.os.Bundle
+import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.observe
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
+import kotlinx.coroutines.launch
+import net.taler.common.Amount
import net.taler.common.ContractTerms
import net.taler.common.fadeIn
import net.taler.common.fadeOut
-import net.taler.lib.common.Amount
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
+import net.taler.wallet.TAG
import net.taler.wallet.databinding.FragmentPromptPaymentBinding
+import net.taler.wallet.showError
/**
* Show a payment and ask the user to accept/decline.
@@ -44,14 +49,15 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
private val model: MainViewModel by activityViewModels()
private val paymentManager by lazy { model.paymentManager }
+ private val transactionManager by lazy { model.transactionManager }
private lateinit var ui: FragmentPromptPaymentBinding
private val adapter = ProductAdapter(this)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
+ savedInstanceState: Bundle?,
+ ): View {
ui = FragmentPromptPaymentBinding.inflate(inflater, container, false)
return ui.root
}
@@ -68,7 +74,15 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
override fun onDestroy() {
super.onDestroy()
if (!requireActivity().isChangingConfigurations) {
- paymentManager.abortPay()
+ val payStatus = paymentManager.payStatus.value as? PayStatus.Prepared ?: return
+ transactionManager.abortTransaction(payStatus.transactionId) { error ->
+ Log.e(TAG, "Error abortTransaction $error")
+ if (model.devMode.value == false) {
+ showError(error.userFacingMsg)
+ } else {
+ showError(error)
+ }
+ }
}
}
@@ -83,6 +97,7 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
private fun onPaymentStatusChanged(payStatus: PayStatus?) {
when (payStatus) {
+ null -> {}
is PayStatus.Prepared -> {
showLoading(false)
val fees = payStatus.amountEffective - payStatus.amountRaw
@@ -91,8 +106,8 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
ui.bottom.confirmButton.setOnClickListener {
model.showProgressBar.value = true
paymentManager.confirmPay(
- payStatus.proposalId,
- payStatus.contractTerms.amount.currency
+ transactionId = payStatus.transactionId,
+ currency = payStatus.contractTerms.amount.currency,
)
ui.bottom.confirmButton.fadeOut()
ui.bottom.confirmProgressBar.fadeIn()
@@ -107,19 +122,24 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
is PayStatus.Success -> {
showLoading(false)
paymentManager.resetPayStatus()
- findNavController().navigate(R.id.action_promptPayment_to_nav_main)
- model.showTransactions(payStatus.currency)
+ navigateToTransaction(payStatus.transactionId)
Snackbar.make(requireView(), R.string.payment_initiated, LENGTH_LONG).show()
}
is PayStatus.AlreadyPaid -> {
showLoading(false)
paymentManager.resetPayStatus()
- findNavController().navigate(R.id.action_promptPayment_to_alreadyPaid)
+ navigateToTransaction(payStatus.transactionId)
+ Snackbar.make(requireView(), R.string.payment_already_paid, LENGTH_LONG).show()
}
- is PayStatus.Error -> {
+ is PayStatus.Pending -> {
showLoading(false)
- ui.details.errorView.text = getString(R.string.payment_error, payStatus.error)
- ui.details.errorView.fadeIn()
+ paymentManager.resetPayStatus()
+ navigateToTransaction(payStatus.transactionId)
+ if (payStatus.error != null && model.devMode.value == true) {
+ showError(payStatus.error)
+ } else {
+ showError(getString(R.string.payment_pending))
+ }
}
is PayStatus.None -> {
// No payment active.
@@ -132,7 +152,7 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
}
}
- private fun showOrder(contractTerms: ContractTerms, amount:Amount, totalFees: Amount? = null) {
+ private fun showOrder(contractTerms: ContractTerms, amount: Amount, totalFees: Amount? = null) {
ui.details.orderView.text = contractTerms.summary
adapter.setItems(contractTerms.products)
ui.details.productsList.fadeIn()
@@ -154,4 +174,13 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
f.show(parentFragmentManager, "image")
}
+ private fun navigateToTransaction(id: String?) {
+ lifecycleScope.launch {
+ if (id != null && transactionManager.selectTransaction(id)) {
+ findNavController().navigate(R.id.action_promptPayment_to_nav_transactions_detail_payment)
+ } else {
+ findNavController().navigate(R.id.action_promptPayment_to_nav_main)
+ }
+ }
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt
new file mode 100644
index 0000000..0f6d661
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt
@@ -0,0 +1,174 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.payment
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.ContractMerchant
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.ErrorTransactionButton
+import net.taler.wallet.transactions.TransactionAction
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfo
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionLinkComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionPayment
+import net.taler.wallet.transactions.TransactionState
+import net.taler.wallet.transactions.TransitionsComposable
+
+@Composable
+fun TransactionPaymentComposable(
+ t: TransactionPayment,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onFulfill: (url: String) -> Unit,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_order_total),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+
+ val fee = t.amountEffective - t.amountRaw
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_paid),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
+ if (t.posConfirmation != null) {
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.payment_confirmation_code),
+ info = t.posConfirmation,
+ )
+ }
+
+ PurchaseDetails(info = t.info) {
+ onFulfill(t.info.fulfillmentUrl ?: "")
+ }
+
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
+ }
+}
+
+@Composable
+fun PurchaseDetails(
+ info: TransactionInfo,
+ onClick: () -> Unit,
+) {
+ Column(
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ // Summary and fulfillment message
+ val text = if (info.fulfillmentMessage == null) {
+ info.summary
+ } else {
+ "${info.summary}\n\n${info.fulfillmentMessage}"
+ }
+ if (info.fulfillmentUrl != null) {
+ TransactionLinkComposable(
+ label = stringResource(id = R.string.transaction_order),
+ info = text,
+ ) { onClick() }
+ } else {
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.transaction_order),
+ info = text,
+ )
+ }
+ // Order ID
+ Text(
+ stringResource(id = R.string.transaction_order_id, info.orderId),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+}
+
+@Preview
+@Composable
+fun TransactionPaymentComposablePreview() {
+ val t = TransactionPayment(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ info = TransactionInfo(
+ orderId = "123",
+ merchant = ContractMerchant(name = "Taler"),
+ summary = "Some Product that was bought and can have quite a long label",
+ fulfillmentMessage = "This is some fulfillment message",
+ fulfillmentUrl = "https://bank.demo.taler.net/",
+ products = listOf(),
+ ),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.1337"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.23"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ )
+ TalerSurface {
+ TransactionPaymentComposable(t = t, devMode = true, spec = null, onFulfill = {}) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt
new file mode 100644
index 0000000..4393e47
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt
@@ -0,0 +1,128 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.wallet.R
+import net.taler.wallet.compose.NumericInputField
+import net.taler.wallet.compose.SelectionChip
+import net.taler.wallet.compose.TalerSurface
+
+enum class ExpirationOption(val hours: Long) {
+ DAYS_1(24),
+ DAYS_7(24 * 7),
+ DAYS_30(24 * 30),
+ CUSTOM(-1)
+}
+
+@Composable
+fun ExpirationComposable(
+ modifier: Modifier = Modifier,
+ option: ExpirationOption,
+ hours: Long,
+ onOptionChange: (ExpirationOption) -> Unit,
+ onHoursChange: (Long) -> Unit,
+) {
+ val options = listOf(
+ ExpirationOption.DAYS_1 to stringResource(R.string.send_peer_expiration_1d),
+ ExpirationOption.DAYS_7 to stringResource(R.string.send_peer_expiration_7d),
+ ExpirationOption.DAYS_30 to stringResource(R.string.send_peer_expiration_30d),
+ ExpirationOption.CUSTOM to stringResource(R.string.send_peer_expiration_custom),
+ )
+ Column(
+ modifier = modifier,
+ ) {
+ LazyRow(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ items(items = options, key = { it.first }) {
+ SelectionChip(
+ label = { Text(it.second) },
+ modifier = Modifier.padding(horizontal = 4.dp),
+ selected = it.first == option,
+ value = it.first,
+ onSelected = { o ->
+ onOptionChange(o)
+ if (o != ExpirationOption.CUSTOM) {
+ onHoursChange(o.hours)
+ }
+ },
+ )
+ }
+ }
+
+ if (option == ExpirationOption.CUSTOM) {
+ val d = hours / 24L
+ val h = hours - d * 24L
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ NumericInputField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(end = 4.dp),
+ value = d,
+ onValueChange = {
+ onHoursChange(it * 24 + h)
+ },
+ label = { Text(stringResource(R.string.send_peer_expiration_days)) },
+ maxValue = 365,
+ )
+ NumericInputField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(start = 4.dp),
+ value = h,
+ onValueChange = {
+ onHoursChange(d * 24 + it)
+ },
+ label = { Text(stringResource(R.string.send_peer_expiration_hours)) },
+ maxValue = 23,
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun ExpirationComposablePreview() {
+ TalerSurface {
+ var option = ExpirationOption.CUSTOM
+ var hours = 25L
+ ExpirationComposable(
+ option = option,
+ hours = hours,
+ onOptionChange = { option = it }
+ ) { hours = it }
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt
new file mode 100644
index 0000000..1ce0175
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt
@@ -0,0 +1,270 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import android.annotation.SuppressLint
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Alignment.Companion.End
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE
+import net.taler.wallet.backend.TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE
+import net.taler.wallet.backend.TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
+
+data class IncomingData(
+ val isCredit: Boolean,
+ @StringRes val intro: Int,
+ @StringRes val button: Int,
+)
+
+val incomingPush = IncomingData(
+ isCredit = true,
+ intro = R.string.receive_peer_payment_intro,
+ button = R.string.receive_peer_payment_title,
+)
+
+val incomingPull = IncomingData(
+ isCredit = false,
+ intro = R.string.pay_peer_intro,
+ button = R.string.payment_button_confirm,
+)
+
+@Composable
+fun IncomingComposable(
+ state: State<IncomingState>,
+ data: IncomingData,
+ onAccept: (IncomingTerms) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState),
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(16.dp)
+ .align(CenterHorizontally),
+ text = stringResource(id = data.intro),
+ )
+ when (val s = state.value) {
+ IncomingChecking -> PeerPullCheckingComposable()
+ is IncomingAccepting -> PeerPullTermsComposable(s, onAccept, data)
+ is IncomingTerms -> PeerPullTermsComposable(s, onAccept, data)
+ is IncomingError -> PeerPullErrorComposable(s)
+ IncomingAccepted -> {
+ // we navigate away, don't show anything
+ }
+ }
+ }
+}
+
+@Composable
+fun ColumnScope.PeerPullCheckingComposable() {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .align(CenterHorizontally)
+ .fillMaxSize(0.75f),
+ )
+}
+
+@Composable
+fun ColumnScope.PeerPullTermsComposable(
+ terms: IncomingTerms,
+ onAccept: (IncomingTerms) -> Unit,
+ data: IncomingData,
+) {
+ Text(
+ modifier = Modifier
+ .padding(16.dp)
+ .align(CenterHorizontally),
+ text = terms.contractTerms.summary,
+ style = MaterialTheme.typography.headlineSmall,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Card(modifier = Modifier.fillMaxWidth()) {
+ Column(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.align(End),
+ ) {
+ Text(
+ text = stringResource(id = R.string.payment_label_amount_total),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ Text(
+ modifier = Modifier.padding(start = 8.dp),
+ text = terms.contractTerms.amount.toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+ // this gets used for credit and debit, so fee calculation differs
+ val fee = if (data.isCredit) {
+ terms.amountRaw - terms.amountEffective
+ } else {
+ terms.amountEffective - terms.amountRaw
+ }
+ val feeStr = if (data.isCredit) {
+ stringResource(R.string.amount_negative, fee)
+ } else {
+ stringResource(R.string.amount_positive, fee)
+ }
+ if (!fee.isZero()) Text(
+ modifier = Modifier.align(End),
+ text = feeStr,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.error,
+ )
+ if (terms is IncomingAccepting) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(end = 64.dp)
+ .align(End),
+ )
+ } else {
+ Button(
+ modifier = Modifier
+ .align(End)
+ .padding(top = 8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = colorResource(R.color.green),
+ contentColor = Color.White,
+ ),
+ onClick = { onAccept(terms) },
+ ) {
+ Text(
+ text = stringResource(id = data.button),
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ColumnScope.PeerPullErrorComposable(s: IncomingError) {
+ val message = when (s.info.code) {
+ WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE -> stringResource(R.string.payment_balance_insufficient)
+ WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE -> stringResource(R.string.payment_balance_insufficient)
+ else -> s.info.userFacingMsg
+ }
+
+ Text(
+ modifier = Modifier
+ .align(CenterHorizontally)
+ .padding(horizontal = 32.dp),
+ text = message,
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.error,
+ )
+}
+
+@Preview
+@Composable
+fun PeerPullCheckingPreview() {
+ Surface {
+ @SuppressLint("UnrememberedMutableState")
+ val s = mutableStateOf(IncomingChecking)
+ IncomingComposable(s, incomingPush) {}
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullTermsPreview() {
+ Surface {
+ val terms = IncomingTerms(
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.423"),
+ contractTerms = PeerContractTerms(
+ summary = "This is a long test summary that can be more than one line long for sure",
+ amount = Amount.fromString("TESTKUDOS", "23.42"),
+ ),
+ id = "ID123",
+ )
+
+ @SuppressLint("UnrememberedMutableState")
+ val s = mutableStateOf(terms)
+ IncomingComposable(s, incomingPush) {}
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullAcceptingPreview() {
+ Surface {
+ val terms = IncomingTerms(
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.123"),
+ contractTerms = PeerContractTerms(
+ summary = "This is a long test summary that can be more than one line long for sure",
+ amount = Amount.fromString("TESTKUDOS", "23.42"),
+ ),
+ id = "ID123",
+ )
+
+ @SuppressLint("UnrememberedMutableState")
+ val s = mutableStateOf(IncomingAccepting(terms))
+ IncomingComposable(s, incomingPush) {}
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullPayErrorPreview() {
+ Surface {
+ @SuppressLint("UnrememberedMutableState")
+ val s = mutableStateOf(
+ IncomingError(
+ info = TalerErrorInfo(WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "msg"),
+ )
+ )
+ IncomingComposable(s, incomingPush) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt
new file mode 100644
index 0000000..df71c72
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt
@@ -0,0 +1,73 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import net.taler.common.showError
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
+
+class IncomingPullPaymentFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val peerManager get() = model.peerManager
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ lifecycleScope.launchWhenResumed {
+ peerManager.incomingPullState.collect {
+ if (it is IncomingAccepted) {
+ findNavController().navigate(R.id.action_promptPullPayment_to_nav_main)
+ } else if (it is IncomingError) {
+ if (model.devMode.value == true) {
+ showError(it.info)
+ } else {
+ showError(it.info.userFacingMsg)
+ }
+ }
+ }
+ }
+ return ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val state = peerManager.incomingPullState.collectAsStateLifecycleAware()
+ IncomingComposable(state, incomingPull) { terms ->
+ peerManager.confirmPeerPullDebit(terms)
+ }
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.pay_peer_title)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt
new file mode 100644
index 0000000..ced2b82
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt
@@ -0,0 +1,73 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import net.taler.common.showError
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
+
+class IncomingPushPaymentFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val peerManager get() = model.peerManager
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ lifecycleScope.launchWhenResumed {
+ peerManager.incomingPushState.collect {
+ if (it is IncomingAccepted) {
+ findNavController().navigate(R.id.action_promptPushPayment_to_nav_main)
+ } else if (it is IncomingError) {
+ if (model.devMode.value == true) {
+ showError(it.info)
+ } else {
+ showError(it.info.userFacingMsg)
+ }
+ }
+ }
+ }
+ return ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val state = peerManager.incomingPushState.collectAsStateLifecycleAware()
+ IncomingComposable(state, incomingPush) { terms ->
+ peerManager.confirmPeerPushCredit(terms)
+ }
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.receive_peer_payment_title)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt
new file mode 100644
index 0000000..cd5b5dd
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt
@@ -0,0 +1,60 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+import net.taler.wallet.backend.TalerErrorInfo
+
+sealed class IncomingState
+object IncomingChecking : IncomingState()
+open class IncomingTerms(
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+ val contractTerms: PeerContractTerms,
+ val id: String,
+) : IncomingState()
+
+class IncomingAccepting(s: IncomingTerms) :
+ IncomingTerms(s.amountRaw, s.amountEffective, s.contractTerms, s.id)
+
+object IncomingAccepted : IncomingState()
+data class IncomingError(
+ val info: TalerErrorInfo,
+) : IncomingState()
+
+@Serializable
+data class PeerContractTerms(
+ val summary: String,
+ val amount: Amount,
+)
+
+@Serializable
+data class PreparePeerPullDebitResponse(
+ val contractTerms: PeerContractTerms,
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+ val transactionId: String,
+)
+
+@Serializable
+data class PreparePeerPushCreditResponse(
+ val contractTerms: PeerContractTerms,
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+ val transactionId: String,
+)
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt
new file mode 100644
index 0000000..90b520e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt
@@ -0,0 +1,280 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.serialization.json.JsonPrimitive
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import kotlin.random.Random
+
+@Composable
+fun OutgoingPullComposable(
+ amount: Amount,
+ state: OutgoingState,
+ onCreateInvoice: (amount: Amount, subject: String, hours: Long, exchange: ExchangeItem) -> Unit,
+ onClose: () -> Unit,
+) {
+ when(state) {
+ is OutgoingChecking, is OutgoingCreating, is OutgoingResponse -> PeerCreatingComposable()
+ is OutgoingIntro, is OutgoingChecked -> OutgoingPullIntroComposable(
+ amount = amount,
+ state = state,
+ onCreateInvoice = onCreateInvoice,
+ )
+ is OutgoingError -> PeerErrorComposable(state, onClose)
+ }
+}
+
+@Composable
+fun PeerCreatingComposable() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(32.dp)
+ .align(Center),
+ )
+ }
+}
+
+@Composable
+fun OutgoingPullIntroComposable(
+ amount: Amount,
+ state: OutgoingState,
+ onCreateInvoice: (amount: Amount, subject: String, hours: Long, exchange: ExchangeItem) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ var subject by rememberSaveable { mutableStateOf("") }
+ val focusRequester = remember { FocusRequester() }
+
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(focusRequester),
+ singleLine = true,
+ value = subject,
+ onValueChange = { input ->
+ if (input.length <= MAX_LENGTH_SUBJECT)
+ subject = input.replace('\n', ' ')
+ },
+ isError = subject.isBlank(),
+ label = {
+ Text(
+ stringResource(R.string.send_peer_purpose),
+ color = if (subject.isBlank()) {
+ MaterialTheme.colorScheme.error
+ } else Color.Unspecified,
+ )
+ }
+ )
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 5.dp),
+ color = if (subject.isBlank()) MaterialTheme.colorScheme.error else Color.Unspecified,
+ text = stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT),
+ textAlign = TextAlign.End,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_chosen),
+ amount = amount,
+ amountType = AmountType.Positive,
+ )
+
+ if (state is OutgoingChecked) {
+ val fee = state.amountRaw - state.amountEffective
+ if (!fee.isZero()) TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(amount.spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ val exchangeItem = (state as? OutgoingChecked)?.exchangeItem
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.withdraw_exchange),
+ info = if (exchangeItem == null) "" else cleanExchange(exchangeItem.exchangeBaseUrl),
+ )
+
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = stringResource(R.string.send_peer_expiration_period),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+
+ var option by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY) }
+ var hours by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY.hours) }
+ ExpirationComposable(
+ modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
+ option = option,
+ hours = hours,
+ onOptionChange = { option = it }
+ ) { hours = it }
+
+ Button(
+ modifier = Modifier.padding(16.dp),
+ enabled = subject.isNotBlank() && state is OutgoingChecked,
+ onClick = {
+ onCreateInvoice(
+ amount,
+ subject,
+ hours,
+ exchangeItem ?: error("clickable without exchange")
+ )
+ },
+ ) {
+ Text(text = stringResource(R.string.receive_peer_create_button))
+ }
+ }
+}
+
+@Composable
+fun PeerErrorComposable(state: OutgoingError, onClose: () -> Unit) {
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ Text(
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodyLarge,
+ text = state.info.userFacingMsg,
+ )
+
+ Button(
+ modifier = Modifier.padding(16.dp),
+ onClick = onClose,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error,
+ contentColor = MaterialTheme.colorScheme.onError,
+ ),
+ ) {
+ Text(text = stringResource(R.string.close))
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullComposableCreatingPreview() {
+ TalerSurface {
+ OutgoingPullComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = OutgoingCreating,
+ onCreateInvoice = { _, _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullComposableCheckingPreview() {
+ TalerSurface {
+ OutgoingPullComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = if (Random.nextBoolean()) OutgoingIntro else OutgoingChecking,
+ onCreateInvoice = { _, _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullComposableCheckedPreview() {
+ TalerSurface {
+ val amountRaw = Amount.fromString("TESTKUDOS", "42.42")
+ val amountEffective = Amount.fromString("TESTKUDOS", "42.23")
+ val exchangeItem = ExchangeItem("https://example.org", "TESTKUDOS", emptyList())
+ OutgoingPullComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = OutgoingChecked(amountRaw, amountEffective, exchangeItem),
+ onCreateInvoice = { _, _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullComposableErrorPreview() {
+ TalerSurface {
+ val json = mapOf("foo" to JsonPrimitive("bar"))
+ val state = OutgoingError(TalerErrorInfo(TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "message", json))
+ OutgoingPullComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = state,
+ onCreateInvoice = { _, _, _, _ -> },
+ onClose = {},
+ )
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
new file mode 100644
index 0000000..8f2fb96
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
@@ -0,0 +1,107 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.findNavController
+import kotlinx.coroutines.launch
+import net.taler.common.Amount
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.showError
+
+class OutgoingPullFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val peerManager get() = model.peerManager
+ private val transactionManager get() = model.transactionManager
+ private val balanceManager get() = model.balanceManager
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ val amount = arguments?.getString("amount")?.let {
+ Amount.fromJSONString(it)
+ } ?: error("no amount passed")
+ val scopeInfo = transactionManager.selectedScope
+ val spec = scopeInfo?.let { balanceManager.getSpecForScopeInfo(it) }
+
+ return ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val state = peerManager.pullState.collectAsStateLifecycleAware().value
+ OutgoingPullComposable(
+ amount = amount.withSpec(spec),
+ state = state,
+ onCreateInvoice = this@OutgoingPullFragment::onCreateInvoice,
+ onClose = {
+ findNavController().navigate(R.id.action_nav_peer_pull_to_nav_main)
+ }
+ )
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ peerManager.pullState.collect {
+ if (it is OutgoingResponse) {
+ if (transactionManager.selectTransaction(it.transactionId)) {
+ findNavController().navigate(R.id.action_nav_peer_pull_to_nav_transactions_detail_peer)
+ } else {
+ findNavController().navigate(R.id.action_nav_peer_pull_to_nav_main)
+ }
+ }
+
+ if (it is OutgoingError && model.devMode.value == true) {
+ showError(it.info)
+ }
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.receive_peer_title)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!requireActivity().isChangingConfigurations) peerManager.resetPullPayment()
+ }
+
+ private fun onCreateInvoice(amount: Amount, summary: String, hours: Long, exchange: ExchangeItem) {
+ peerManager.initiatePeerPullCredit(amount, summary, hours, exchange)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt
new file mode 100644
index 0000000..d39fdc8
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt
@@ -0,0 +1,218 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.serialization.json.JsonPrimitive
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.TalerSurface
+import kotlin.random.Random
+
+@Composable
+fun OutgoingPushComposable(
+ state: OutgoingState,
+ amount: Amount,
+ onSend: (amount: Amount, summary: String, hours: Long) -> Unit,
+ onClose: () -> Unit,
+) {
+ when(state) {
+ is OutgoingChecking, is OutgoingCreating, is OutgoingResponse -> PeerCreatingComposable()
+ is OutgoingIntro, is OutgoingChecked -> OutgoingPushIntroComposable(
+ amount = amount,
+ state = state,
+ onSend = onSend,
+ )
+ is OutgoingError -> PeerErrorComposable(state, onClose)
+ }
+}
+
+@Composable
+fun OutgoingPushIntroComposable(
+ state: OutgoingState,
+ amount: Amount,
+ onSend: (amount: Amount, summary: String, hours: Long) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier.padding(vertical = 16.dp),
+ text = amount.toString(),
+ softWrap = false,
+ style = MaterialTheme.typography.titleLarge,
+ )
+
+ if (state is OutgoingChecked) {
+ val fee = state.amountEffective - state.amountRaw
+ Text(
+ modifier = Modifier.padding(vertical = 16.dp),
+ text = stringResource(id = R.string.payment_fee, fee.withSpec(amount.spec)),
+ softWrap = false,
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+
+ var subject by rememberSaveable { mutableStateOf("") }
+ val focusRequester = remember { FocusRequester() }
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(focusRequester),
+ singleLine = true,
+ value = subject,
+ onValueChange = { input ->
+ if (input.length <= MAX_LENGTH_SUBJECT)
+ subject = input.replace('\n', ' ')
+ },
+ isError = subject.isBlank(),
+ label = {
+ Text(
+ stringResource(R.string.send_peer_purpose),
+ color = if (subject.isBlank()) {
+ MaterialTheme.colorScheme.error
+ } else Color.Unspecified,
+ )
+ }
+ )
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 5.dp),
+ color = if (subject.isBlank()) MaterialTheme.colorScheme.error else Color.Unspecified,
+ text = stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT),
+ textAlign = TextAlign.End,
+ )
+
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = stringResource(R.string.send_peer_expiration_period),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+
+ var option by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY) }
+ var hours by rememberSaveable { mutableLongStateOf(DEFAULT_EXPIRY.hours) }
+ ExpirationComposable(
+ modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
+ option = option,
+ hours = hours,
+ onOptionChange = { option = it }
+ ) { hours = it }
+
+ Button(
+ enabled = state is OutgoingChecked && subject.isNotBlank(),
+ onClick = { onSend(amount, subject, hours) },
+ ) {
+ Text(text = stringResource(R.string.send_peer_create_button))
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushComposableCreatingPreview() {
+ TalerSurface {
+ OutgoingPushComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = OutgoingCreating,
+ onSend = { _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushComposableCheckingPreview() {
+ TalerSurface {
+ val state = if (Random.nextBoolean()) OutgoingIntro else OutgoingChecking
+ OutgoingPushComposable(
+ state = state,
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ onSend = { _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushComposableCheckedPreview() {
+ TalerSurface {
+ val amountEffective = Amount.fromString("TESTKUDOS", "42.42")
+ val amountRaw = Amount.fromString("TESTKUDOS", "42.23")
+ val state = OutgoingChecked(amountRaw, amountEffective)
+ OutgoingPushComposable(
+ state = state,
+ amount = amountEffective,
+ onSend = { _, _, _ -> },
+ onClose = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushComposableErrorPreview() {
+ TalerSurface {
+ val json = mapOf("foo" to JsonPrimitive("bar"))
+ val state = OutgoingError(TalerErrorInfo(TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, "hint", "message", json))
+ OutgoingPushComposable(
+ amount = Amount.fromString("TESTKUDOS", "42.23"),
+ state = state,
+ onSend = { _, _, _ -> },
+ onClose = {},
+ )
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
new file mode 100644
index 0000000..01fb566
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
@@ -0,0 +1,122 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.findNavController
+import androidx.navigation.fragment.findNavController
+import kotlinx.coroutines.launch
+import net.taler.common.Amount
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
+
+class OutgoingPushFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val peerManager get() = model.peerManager
+ private val transactionManager get() = model.transactionManager
+ private val balanceManager get() = model.balanceManager
+
+ // hacky way to change back action until we have navigation for compose
+ private val backPressedCallback = object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ findNavController().navigate(R.id.action_nav_peer_push_to_nav_main)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ val amount = arguments?.getString("amount")?.let {
+ Amount.fromJSONString(it)
+ } ?: error("no amount passed")
+ val scopeInfo = transactionManager.selectedScope
+ val spec = scopeInfo?.let { balanceManager.getSpecForScopeInfo(it) }
+
+ requireActivity().onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner, backPressedCallback
+ )
+
+ return ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val state = peerManager.pushState.collectAsStateLifecycleAware().value
+ OutgoingPushComposable(
+ amount = amount.withSpec(spec),
+ state = state,
+ onSend = this@OutgoingPushFragment::onSend,
+ onClose = {
+ findNavController().navigate(R.id.action_nav_peer_pull_to_nav_main)
+ }
+ )
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ peerManager.pushState.collect {
+ if (it is OutgoingResponse) {
+ if (transactionManager.selectTransaction(it.transactionId)) {
+ findNavController().navigate(R.id.action_nav_peer_push_to_nav_transactions_detail_peer)
+ } else {
+ findNavController().navigate(R.id.action_nav_peer_push_to_nav_main)
+ }
+ }
+
+ if (it is OutgoingError && model.devMode.value == true) {
+ showError(it.info)
+ }
+
+ // Disable back navigation when tx is being created
+ backPressedCallback.isEnabled = it !is OutgoingCreating
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.send_peer_title)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!requireActivity().isChangingConfigurations) peerManager.resetPushPayment()
+ }
+
+ private fun onSend(amount: Amount, summary: String, hours: Long) {
+ peerManager.initiatePeerPushDebit(amount, summary, hours)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt
new file mode 100644
index 0000000..05da294
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt
@@ -0,0 +1,63 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.exchanges.ExchangeItem
+
+sealed class OutgoingState
+object OutgoingIntro : OutgoingState()
+object OutgoingChecking : OutgoingState()
+data class OutgoingChecked(
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+ val exchangeItem: ExchangeItem? = null,
+) : OutgoingState()
+object OutgoingCreating : OutgoingState()
+data class OutgoingResponse(
+ val transactionId: String,
+) : OutgoingState()
+
+data class OutgoingError(
+ val info: TalerErrorInfo,
+) : OutgoingState()
+
+@Serializable
+data class CheckPeerPullCreditResponse(
+ val exchangeBaseUrl: String,
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+)
+
+@Serializable
+data class InitiatePeerPullPaymentResponse(
+ val transactionId: String,
+)
+
+@Serializable
+data class CheckPeerPushDebitResponse(
+ val amountRaw: Amount,
+ val amountEffective: Amount,
+)
+
+@Serializable
+data class InitiatePeerPushDebitResponse(
+ val exchangeBaseUrl: String,
+ val transactionId: String,
+)
diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
new file mode 100644
index 0000000..5bd2b0b
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
@@ -0,0 +1,217 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import android.util.Log
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.common.Amount
+import net.taler.common.Timestamp
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorCode.UNKNOWN
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.exchanges.ExchangeManager
+import org.json.JSONObject
+import java.util.concurrent.TimeUnit.HOURS
+
+const val MAX_LENGTH_SUBJECT = 100
+val DEFAULT_EXPIRY = ExpirationOption.DAYS_1
+
+class PeerManager(
+ private val api: WalletBackendApi,
+ private val exchangeManager: ExchangeManager,
+ private val scope: CoroutineScope,
+) {
+
+ private val _outgoingPullState = MutableStateFlow<OutgoingState>(OutgoingIntro)
+ val pullState: StateFlow<OutgoingState> = _outgoingPullState
+
+ private val _outgoingPushState = MutableStateFlow<OutgoingState>(OutgoingIntro)
+ val pushState: StateFlow<OutgoingState> = _outgoingPushState
+
+ private val _incomingPullState = MutableStateFlow<IncomingState>(IncomingChecking)
+ val incomingPullState: StateFlow<IncomingState> = _incomingPullState
+
+ private val _incomingPushState = MutableStateFlow<IncomingState>(IncomingChecking)
+ val incomingPushState: StateFlow<IncomingState> = _incomingPushState
+
+ fun checkPeerPullCredit(amount: Amount) {
+ _outgoingPullState.value = OutgoingChecking
+ scope.launch(Dispatchers.IO) {
+ val exchangeItem = exchangeManager.findExchange(amount.currency)
+ if (exchangeItem == null) {
+ _outgoingPullState.value = OutgoingError(
+ TalerErrorInfo(UNKNOWN, "No exchange found for ${amount.currency}")
+ )
+ return@launch
+ }
+ api.request("checkPeerPullCredit", CheckPeerPullCreditResponse.serializer()) {
+ put("exchangeBaseUrl", exchangeItem.exchangeBaseUrl)
+ put("amount", amount.toJSONString())
+ }.onSuccess {
+ _outgoingPullState.value = OutgoingChecked(
+ amountRaw = it.amountRaw,
+ amountEffective = it.amountEffective,
+ exchangeItem = exchangeItem,
+ )
+ }.onError { error ->
+ Log.e(TAG, "got checkPeerPullCredit error result $error")
+ _outgoingPullState.value = OutgoingError(error)
+ }
+ }
+ }
+
+ fun initiatePeerPullCredit(amount: Amount, summary: String, expirationHours: Long, exchange: ExchangeItem) {
+ _outgoingPullState.value = OutgoingCreating
+ scope.launch(Dispatchers.IO) {
+ val expiry = Timestamp.fromMillis(System.currentTimeMillis() + HOURS.toMillis(expirationHours))
+ api.request("initiatePeerPullCredit", InitiatePeerPullPaymentResponse.serializer()) {
+ put("exchangeBaseUrl", exchange.exchangeBaseUrl)
+ put("partialContractTerms", JSONObject().apply {
+ put("amount", amount.toJSONString())
+ put("summary", summary)
+ put("purse_expiration", JSONObject(Json.encodeToString(expiry)))
+ })
+ }.onSuccess {
+ _outgoingPullState.value = OutgoingResponse(it.transactionId)
+ }.onError { error ->
+ Log.e(TAG, "got initiatePeerPullCredit error result $error")
+ _outgoingPullState.value = OutgoingError(error)
+ }
+ }
+ }
+
+ fun resetPullPayment() {
+ _outgoingPullState.value = OutgoingIntro
+ }
+
+ fun checkPeerPushDebit(amount: Amount) {
+ _outgoingPushState.value = OutgoingChecking
+ scope.launch(Dispatchers.IO) {
+ api.request("checkPeerPushDebit", CheckPeerPushDebitResponse.serializer()) {
+ put("amount", amount.toJSONString())
+ }.onSuccess { response ->
+ _outgoingPushState.value = OutgoingChecked(
+ amountRaw = response.amountRaw,
+ amountEffective = response.amountEffective,
+ // FIXME add exchangeItem once available in API
+ )
+ }.onError { error ->
+ Log.e(TAG, "got checkPeerPushDebit error result $error")
+ _outgoingPushState.value = OutgoingError(error)
+ }
+ }
+ }
+
+ fun initiatePeerPushDebit(amount: Amount, summary: String, expirationHours: Long) {
+ _outgoingPushState.value = OutgoingCreating
+ scope.launch(Dispatchers.IO) {
+ val expiry = Timestamp.fromMillis(System.currentTimeMillis() + HOURS.toMillis(expirationHours))
+ api.request("initiatePeerPushDebit", InitiatePeerPushDebitResponse.serializer()) {
+ put("amount", amount.toJSONString())
+ put("partialContractTerms", JSONObject().apply {
+ put("amount", amount.toJSONString())
+ put("summary", summary)
+ put("purse_expiration", JSONObject(Json.encodeToString(expiry)))
+ })
+ }.onSuccess { response ->
+ _outgoingPushState.value = OutgoingResponse(response.transactionId)
+ }.onError { error ->
+ Log.e(TAG, "got initiatePeerPushDebit error result $error")
+ _outgoingPushState.value = OutgoingError(error)
+ }
+ }
+ }
+
+ fun resetPushPayment() {
+ _outgoingPushState.value = OutgoingIntro
+ }
+
+ fun preparePeerPullDebit(talerUri: String) {
+ _incomingPullState.value = IncomingChecking
+ scope.launch(Dispatchers.IO) {
+ api.request("preparePeerPullDebit", PreparePeerPullDebitResponse.serializer()) {
+ put("talerUri", talerUri)
+ }.onSuccess { response ->
+ _incomingPullState.value = IncomingTerms(
+ amountRaw = response.amountRaw,
+ amountEffective = response.amountEffective,
+ contractTerms = response.contractTerms,
+ id = response.transactionId,
+ )
+ }.onError { error ->
+ Log.e(TAG, "got preparePeerPullDebit error result $error")
+ _incomingPullState.value = IncomingError(error)
+ }
+ }
+ }
+
+ fun confirmPeerPullDebit(terms: IncomingTerms) {
+ _incomingPullState.value = IncomingAccepting(terms)
+ scope.launch(Dispatchers.IO) {
+ api.request<Unit>("confirmPeerPullDebit") {
+ put("transactionId", terms.id)
+ }.onSuccess {
+ _incomingPullState.value = IncomingAccepted
+ }.onError { error ->
+ Log.e(TAG, "got confirmPeerPullDebit error result $error")
+ _incomingPullState.value = IncomingError(error)
+ }
+ }
+ }
+
+ fun preparePeerPushCredit(talerUri: String) {
+ _incomingPushState.value = IncomingChecking
+ scope.launch(Dispatchers.IO) {
+ api.request("preparePeerPushCredit", PreparePeerPushCreditResponse.serializer()) {
+ put("talerUri", talerUri)
+ }.onSuccess { response ->
+ _incomingPushState.value = IncomingTerms(
+ amountRaw = response.amountRaw,
+ amountEffective = response.amountEffective,
+ contractTerms = response.contractTerms,
+ id = response.transactionId,
+ )
+ }.onError { error ->
+ Log.e(TAG, "got preparePeerPushCredit error result $error")
+ _incomingPushState.value = IncomingError(error)
+ }
+ }
+ }
+
+ fun confirmPeerPushCredit(terms: IncomingTerms) {
+ _incomingPushState.value = IncomingAccepting(terms)
+ scope.launch(Dispatchers.IO) {
+ api.request<Unit>("confirmPeerPushCredit") {
+ put("transactionId", terms.id)
+ }.onSuccess {
+ _incomingPushState.value = IncomingAccepted
+ }.onError { error ->
+ Log.e(TAG, "got confirmPeerPushCredit error result $error")
+ _incomingPushState.value = IncomingError(error)
+ }
+ }
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
new file mode 100644
index 0000000..3b15b6f
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
@@ -0,0 +1,105 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionMinorState.CreatePurse
+import net.taler.wallet.transactions.TransactionMinorState.Ready
+import net.taler.wallet.transactions.TransactionPeerComposable
+import net.taler.wallet.transactions.TransactionPeerPullCredit
+import net.taler.wallet.transactions.TransactionState
+
+@Composable
+fun ColumnScope.TransactionPeerPullCreditComposable(t: TransactionPeerPullCredit, spec: CurrencySpecification?) {
+ if (t.error == null) PeerQrCode(
+ state = t.txState,
+ talerUri = t.talerUri,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.receive_peer_amount_invoiced),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+
+ val fee = t.amountRaw - t.amountEffective
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_received),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Positive,
+ )
+
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.send_peer_purpose),
+ info = t.info.summary ?: "",
+ )
+}
+
+@Preview
+@Composable
+fun TransactionPeerPullCreditPreview(loading: Boolean = false) {
+ val t = TransactionPeerPullCredit(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending, if (loading) CreatePurse else Ready),
+ txActions = listOf(Retry, Suspend, Abort),
+ exchangeBaseUrl = "https://exchange.example.org/",
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ info = PeerInfoShort(
+ expiration = Timestamp.fromMillis(System.currentTimeMillis() + 60 * 60 * 1000),
+ summary = "test invoice",
+ ),
+ talerUri = "https://exchange.example.org/peer/pull/credit",
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
+ )
+ Surface {
+ TransactionPeerComposable(t, true, null) {}
+ }
+}
+
+@Preview
+@Composable
+fun TransactionPeerPullCreditLoadingPreview() {
+ TransactionPeerPullCreditPreview(loading = true)
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt
new file mode 100644
index 0000000..dadff4a
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt
@@ -0,0 +1,90 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionPeerComposable
+import net.taler.wallet.transactions.TransactionPeerPullDebit
+import net.taler.wallet.transactions.TransactionState
+
+@Composable
+fun TransactionPeerPullDebitComposable(t: TransactionPeerPullDebit, spec: CurrencySpecification?) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_order_total),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+
+ val fee = t.amountEffective - t.amountRaw
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_paid),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.send_peer_purpose),
+ info = t.info.summary ?: "",
+ )
+}
+
+@Preview
+@Composable
+fun TransactionPeerPullDebitPreview() {
+ val t = TransactionPeerPullDebit(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ exchangeBaseUrl = "https://exchange.example.org/",
+ amountRaw = Amount.fromString("TESTKUDOS", "42.1337"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.23"),
+ info = PeerInfoShort(
+ expiration = Timestamp.fromMillis(System.currentTimeMillis() + 60 * 60 * 1000),
+ summary = "test invoice",
+ ),
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
+ )
+ Surface {
+ TransactionPeerComposable(t, true, null) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt
new file mode 100644
index 0000000..dbf0fb9
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt
@@ -0,0 +1,90 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionPeerComposable
+import net.taler.wallet.transactions.TransactionPeerPushCredit
+import net.taler.wallet.transactions.TransactionState
+
+@Composable
+fun TransactionPeerPushCreditComposable(t: TransactionPeerPushCredit, spec: CurrencySpecification?) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_sent),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+
+ val fee = t.amountRaw - t.amountEffective
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_received),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Positive,
+ )
+
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.send_peer_purpose),
+ info = t.info.summary ?: "",
+ )
+}
+
+@Preview
+@Composable
+fun TransactionPeerPushCreditPreview() {
+ val t = TransactionPeerPushCredit(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ exchangeBaseUrl = "https://exchange.example.org/",
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ info = PeerInfoShort(
+ expiration = Timestamp.fromMillis(System.currentTimeMillis() + 60 * 60 * 1000),
+ summary = "test invoice",
+ ),
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
+ )
+ Surface {
+ TransactionPeerComposable(t, true, null) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
new file mode 100644
index 0000000..e592c3e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
@@ -0,0 +1,152 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.peer
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.QrCodeUriComposable
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.getQrCodeSize
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionMinorState.CreatePurse
+import net.taler.wallet.transactions.TransactionMinorState.Ready
+import net.taler.wallet.transactions.TransactionPeerComposable
+import net.taler.wallet.transactions.TransactionPeerPushDebit
+import net.taler.wallet.transactions.TransactionState
+
+@Composable
+fun ColumnScope.TransactionPeerPushDebitComposable(t: TransactionPeerPushDebit, spec: CurrencySpecification?) {
+ if (t.error == null) PeerQrCode(
+ state = t.txState,
+ talerUri = t.talerUri,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_order_total),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+
+ val fee = t.amountEffective - t.amountRaw
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_paid),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.send_peer_purpose),
+ info = t.info.summary ?: "",
+ )
+}
+
+@Composable
+fun ColumnScope.PeerQrCode(state: TransactionState, talerUri: String?) {
+ if (state == TransactionState(Pending)) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ style = MaterialTheme.typography.titleLarge,
+ text = stringResource(id = R.string.send_peer_payment_instruction),
+ textAlign = TextAlign.Center,
+ )
+
+ if (state.minor == Ready && talerUri != null) {
+ QrCodeUriComposable(
+ talerUri = talerUri,
+ clipBoardLabel = "Push payment",
+ buttonText = stringResource(id = R.string.copy),
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ style = MaterialTheme.typography.bodyLarge,
+ text = stringResource(id = R.string.receive_peer_invoice_uri),
+ )
+ }
+ } else {
+ val qrCodeSize = getQrCodeSize()
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(32.dp)
+ .size(qrCodeSize)
+ .align(CenterHorizontally),
+ )
+ }
+ }
+
+}
+
+@Preview
+@Composable
+fun TransactionPeerPushDebitPreview(loading: Boolean = false) {
+ val t = TransactionPeerPushDebit(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending, if (loading) CreatePurse else Ready),
+ txActions = listOf(Retry, Suspend, Abort),
+ exchangeBaseUrl = "https://exchange.example.org/",
+ amountRaw = Amount.fromString("TESTKUDOS", "42.1337"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.23"),
+ info = PeerInfoShort(
+ expiration = Timestamp.fromMillis(System.currentTimeMillis() + 60 * 60 * 1000),
+ summary = "test invoice",
+ ),
+ talerUri = "https://exchange.example.org/peer/pull/credit",
+ error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED),
+ )
+
+ TalerSurface {
+ TransactionPeerComposable(t, true, null) {}
+ }
+}
+
+@Preview
+@Composable
+fun TransactionPeerPushDebitLoadingPreview() {
+ TransactionPeerPushDebitPreview(loading = true)
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
deleted file mode 100644
index e3b5dc2..0000000
--- a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.pending
-
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import android.view.View
-import android.view.View.GONE
-import android.view.View.VISIBLE
-import android.view.ViewGroup
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import net.taler.common.showError
-import net.taler.wallet.MainViewModel
-import net.taler.wallet.R
-import net.taler.wallet.TAG
-import net.taler.wallet.databinding.FragmentPendingOperationsBinding
-import org.json.JSONObject
-
-interface PendingOperationClickListener {
- fun onPendingOperationClick(type: String, detail: JSONObject)
- fun onPendingOperationActionClick(type: String, detail: JSONObject)
-}
-
-class PendingOperationsFragment : Fragment(), PendingOperationClickListener {
-
- private val model: MainViewModel by activityViewModels()
- private val pendingOperationsManager by lazy { model.pendingOperationsManager }
-
- private lateinit var ui: FragmentPendingOperationsBinding
- private val pendingAdapter = PendingOperationsAdapter(emptyList(), this)
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(true)
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- ui = FragmentPendingOperationsBinding.inflate(inflater, container, false)
- return ui.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- ui.listPending.apply {
- val myLayoutManager = LinearLayoutManager(requireContext())
- val myItemDecoration =
- DividerItemDecoration(requireContext(), myLayoutManager.orientation)
- layoutManager = myLayoutManager
- adapter = pendingAdapter
- addItemDecoration(myItemDecoration)
- }
-
- pendingOperationsManager.pendingOperations.observe(viewLifecycleOwner, {
- updatePending(it)
- })
- }
-
- override fun onStart() {
- super.onStart()
- pendingOperationsManager.getPending()
- }
-
- 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) {
- requireActivity().showError("No detail view for $type implemented yet.")
- }
-
- override fun onPendingOperationActionClick(type: String, detail: JSONObject) {
- when (type) {
- "proposal-choice" -> {
- Log.v(TAG, "got action click on proposal-choice")
- val proposalId = detail.optString("proposalId", "")
- if (proposalId == "") {
- return
- }
- model.paymentManager.abortProposal(proposalId)
- }
- }
- }
-
-}
-
-class PendingOperationsAdapter(
- private var items: List<PendingOperationInfo>,
- private val listener: PendingOperationClickListener
-) :
- RecyclerView.Adapter<PendingOperationsAdapter.MyViewHolder>() {
-
- init {
- setHasStableIds(false)
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
- val rowView =
- LayoutInflater.from(parent.context).inflate(R.layout.list_item_pending_operation, parent, false)
- return MyViewHolder(rowView)
- }
-
- override fun getItemCount(): Int {
- return items.size
- }
-
- override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
- val p = items[position]
- val pendingContainer = holder.rowView.findViewById<LinearLayout>(R.id.pending_container)
- pendingContainer.setOnClickListener {
- listener.onPendingOperationClick(p.type, p.detail)
- }
- when (p.type) {
- "proposal-choice" -> {
- val btn1 = holder.rowView.findViewById<TextView>(R.id.button_pending_action_1)
- btn1.text = btn1.context.getString(R.string.pending_operations_refuse)
- btn1.visibility = VISIBLE
- btn1.setOnClickListener {
- listener.onPendingOperationActionClick(p.type, p.detail)
- }
- }
- else -> {
- val btn1 = holder.rowView.findViewById<TextView>(R.id.button_pending_action_1)
- btn1.text = btn1.context.getString(R.string.pending_operations_no_action)
- btn1.visibility = GONE
- btn1.setOnClickListener {}
- }
- }
- val textView = holder.rowView.findViewById<TextView>(R.id.pending_text)
- val subTextView = holder.rowView.findViewById<TextView>(R.id.pending_subtext)
- subTextView.text = p.detail.toString(1)
- textView.text = p.type
- }
-
- fun update(items: List<PendingOperationInfo>) {
- this.items = items
- this.notifyDataSetChanged()
- }
-
- class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
deleted file mode 100644
index df778ed..0000000
--- a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.pending
-
-import android.util.Log
-import androidx.lifecycle.MutableLiveData
-import net.taler.wallet.TAG
-import net.taler.wallet.backend.WalletBackendApi
-import org.json.JSONObject
-
-open class PendingOperationInfo(
- val type: String,
- val detail: JSONObject
-)
-
-class PendingOperationsManager(private val walletBackendApi: WalletBackendApi) {
-
- val pendingOperations = MutableLiveData<List<PendingOperationInfo>>()
-
- internal fun getPending() {
- walletBackendApi.sendRequest("getPendingOperations") { isError, result ->
- if (isError) {
- Log.i(TAG, "got getPending error result: ${result.toString(2)}")
- return@sendRequest
- }
- Log.i(TAG, "got getPending result")
- val pendingList = mutableListOf<PendingOperationInfo>()
- val pendingJson = result.getJSONArray("pendingOperations")
- for (i in 0 until pendingJson.length()) {
- val p = pendingJson.getJSONObject(i)
- val type = p.getString("type")
- pendingList.add(PendingOperationInfo(type, p))
- }
- Log.i(TAG, "Got ${pendingList.size} pending operations")
- pendingOperations.postValue((pendingList))
- }
- }
-
- fun retryPendingNow() {
- walletBackendApi.sendRequest("retryPendingNow")
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt b/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt
index 9c292aa..96e939b 100644
--- a/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt
@@ -21,20 +21,17 @@ import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
-import net.taler.lib.common.Amount
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
sealed class RefundStatus {
- data class Error(val msg: String) : RefundStatus()
- data class Success(val response: RefundResponse) : RefundStatus()
+ data class Error(val error: TalerErrorInfo) : RefundStatus()
+ data class Success(val response: StartRefundQueryForUriResponse) : RefundStatus()
}
@Serializable
-data class RefundResponse(
- val amountEffectivePaid: Amount,
- val amountRefundGranted: Amount,
- val amountRefundGone: Amount,
- val pendingAtExchange: Boolean
+data class StartRefundQueryForUriResponse(
+ val transactionId: String,
)
class RefundManager(
@@ -45,10 +42,10 @@ class RefundManager(
fun refund(refundUri: String): LiveData<RefundStatus> {
val liveData = MutableLiveData<RefundStatus>()
scope.launch {
- api.request("applyRefund", RefundResponse.serializer()) {
+ api.request("startRefundQueryForUri", StartRefundQueryForUriResponse.serializer()) {
put("talerRefundUri", refundUri)
}.onError {
- liveData.postValue(RefundStatus.Error(it.userFacingMsg))
+ liveData.postValue(RefundStatus.Error(it))
}.onSuccess {
liveData.postValue(RefundStatus.Success(it))
}
diff --git a/wallet/src/main/java/net/taler/wallet/refund/RefundPaymentInfo.kt b/wallet/src/main/java/net/taler/wallet/refund/RefundPaymentInfo.kt
new file mode 100644
index 0000000..d5f59be
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/refund/RefundPaymentInfo.kt
@@ -0,0 +1,39 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.refund
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+class RefundPaymentInfo(
+ val summary: String,
+ @SerialName("summary_i18n")
+ val summaryI18n: Map<String, String>? = null,
+ /**
+ * More information about the merchant
+ */
+ val merchant: MerchantInfo,
+)
+
+@Serializable
+class MerchantInfo(
+ val name: String,
+ val logo: String? = null,
+ val website: String? = null,
+ val email: String? = null,
+)
diff --git a/wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt b/wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt
new file mode 100644
index 0000000..637b41a
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt
@@ -0,0 +1,123 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.refund
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.ErrorTransactionButton
+import net.taler.wallet.transactions.TransactionAction
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionRefund
+import net.taler.wallet.transactions.TransactionState
+import net.taler.wallet.transactions.TransitionsComposable
+
+@Composable
+fun TransactionRefundComposable(
+ t: TransactionRefund,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_refund),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Positive,
+ )
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_order_total),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+ val fee = t.amountRaw - t.amountEffective
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.transaction_order),
+ info = t.paymentInfo?.summary ?: "",
+ )
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
+ }
+}
+
+@Preview
+@Composable
+fun TransactionRefundComposablePreview() {
+ val t = TransactionRefund(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ paymentInfo = RefundPaymentInfo(
+ merchant = MerchantInfo(name = "Taler"),
+ summary = "Some Product that was bought and can have quite a long label",
+ ),
+ refundedTransactionId = "transactionId",
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ )
+ TalerSurface {
+ TransactionRefundComposable(t = t, devMode = true, spec = null) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/settings/BackupSettingsFragment.kt b/wallet/src/main/java/net/taler/wallet/settings/BackupSettingsFragment.kt
deleted file mode 100644
index 0dc7124..0000000
--- a/wallet/src/main/java/net/taler/wallet/settings/BackupSettingsFragment.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.settings
-
-import android.content.Intent
-import android.os.Bundle
-import android.view.View
-import androidx.preference.Preference
-import androidx.preference.PreferenceFragmentCompat
-import net.taler.wallet.R
-import org.gnu.anastasis.ui.MainActivity
-
-class BackupSettingsFragment : PreferenceFragmentCompat() {
-
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.settings_backup, rootKey)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- val recoveryPref: Preference = findPreference("pref_backup_recovery")!!
- recoveryPref.setOnPreferenceClickListener {
- val intent = Intent(requireContext(), MainActivity::class.java)
- startActivity(intent)
- true
- }
- }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt b/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt
index bd6ce1a..0435665 100644
--- a/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt
@@ -18,89 +18,101 @@ package net.taler.wallet.settings
import android.os.Bundle
import android.view.View
-import androidx.appcompat.app.AlertDialog
+import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
+import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.fragment.app.activityViewModels
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_SHORT
import com.google.android.material.snackbar.Snackbar
import net.taler.common.showError
-import net.taler.common.showLogViewer
-import net.taler.common.toRelativeTime
import net.taler.wallet.BuildConfig.FLAVOR
import net.taler.wallet.BuildConfig.VERSION_CODE
import net.taler.wallet.BuildConfig.VERSION_NAME
-import net.taler.wallet.BuildConfig.WALLET_CORE_VERSION
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.withdraw.WithdrawTestStatus
+import java.lang.System.currentTimeMillis
class SettingsFragment : PreferenceFragmentCompat() {
private val model: MainViewModel by activityViewModels()
+ private val settingsManager get() = model.settingsManager
private val withdrawManager by lazy { model.withdrawManager }
- private lateinit var prefBackup: Preference
private lateinit var prefDevMode: SwitchPreferenceCompat
private lateinit var prefWithdrawTest: Preference
private lateinit var prefLogcat: Preference
+ private lateinit var prefExportDb: Preference
+ private lateinit var prefImportDb: Preference
private lateinit var prefVersionApp: Preference
private lateinit var prefVersionCore: Preference
private lateinit var prefVersionExchange: Preference
private lateinit var prefVersionMerchant: Preference
+ private lateinit var prefTest: Preference
private lateinit var prefReset: Preference
private val devPrefs by lazy {
listOf(
- prefBackup,
+ prefVersionCore,
prefWithdrawTest,
prefLogcat,
- prefVersionApp,
- prefVersionCore,
+ prefExportDb,
+ prefImportDb,
prefVersionExchange,
prefVersionMerchant,
- prefReset
+ prefTest,
+ prefReset,
)
}
+ private val logLauncher = registerForActivityResult(CreateDocument("text/plain")) { uri ->
+ settingsManager.exportLogcat(uri)
+ }
+ private val dbExportLauncher =
+ registerForActivityResult(CreateDocument("application/json")) { uri ->
+ settingsManager.exportDb(uri)
+ }
+ private val dbImportLauncher =
+ registerForActivityResult(OpenDocument()) { uri ->
+ settingsManager.importDb(uri)
+ }
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_main, rootKey)
- prefBackup = findPreference("pref_backup")!!
prefDevMode = findPreference("pref_dev_mode")!!
prefWithdrawTest = findPreference("pref_testkudos")!!
prefLogcat = findPreference("pref_logcat")!!
+ prefExportDb = findPreference("pref_export_db")!!
+ prefImportDb = findPreference("pref_import_db")!!
prefVersionApp = findPreference("pref_version_app")!!
prefVersionCore = findPreference("pref_version_core")!!
prefVersionExchange = findPreference("pref_version_protocol_exchange")!!
prefVersionMerchant = findPreference("pref_version_protocol_merchant")!!
+ prefTest = findPreference("pref_test")!!
prefReset = findPreference("pref_reset")!!
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- model.lastBackup.observe(viewLifecycleOwner, {
- val time = it.toRelativeTime(requireContext())
- prefBackup.summary = getString(R.string.backup_last, time)
- })
+ prefVersionApp.summary = "$VERSION_NAME ($FLAVOR $VERSION_CODE)"
+ prefVersionCore.summary = "${model.walletVersion} (${model.walletVersionHash?.take(7)})"
+ model.exchangeVersion?.let { prefVersionExchange.summary = it }
+ model.merchantVersion?.let { prefVersionMerchant.summary = it }
- model.devMode.observe(viewLifecycleOwner, { enabled ->
+ model.devMode.observe(viewLifecycleOwner) { enabled ->
prefDevMode.isChecked = enabled
- if (enabled) {
- prefVersionApp.summary = "$VERSION_NAME ($FLAVOR $VERSION_CODE)"
- prefVersionCore.summary = WALLET_CORE_VERSION
- model.exchangeVersion?.let { prefVersionExchange.summary = it }
- model.merchantVersion?.let { prefVersionMerchant.summary = it }
- }
devPrefs.forEach { it.isVisible = enabled }
- })
+ }
prefDevMode.setOnPreferenceChangeListener { _, newValue ->
model.devMode.value = newValue as Boolean
true
}
- withdrawManager.testWithdrawalStatus.observe(viewLifecycleOwner, { status ->
+ withdrawManager.testWithdrawalStatus.observe(viewLifecycleOwner) { status ->
if (status == null) return@observe
val loading = status is WithdrawTestStatus.Withdrawing
prefWithdrawTest.isEnabled = !loading
@@ -109,34 +121,58 @@ class SettingsFragment : PreferenceFragmentCompat() {
requireActivity().showError(R.string.withdraw_error_test, status.message)
}
withdrawManager.testWithdrawalStatus.value = null
- })
+ }
prefWithdrawTest.setOnPreferenceClickListener {
withdrawManager.withdrawTestkudos()
true
}
prefLogcat.setOnPreferenceClickListener {
- requireContext().showLogViewer("taler-wallet")
+ logLauncher.launch("taler-wallet-log-${currentTimeMillis()}.txt")
+ true
+ }
+ prefExportDb.setOnPreferenceClickListener {
+ dbExportLauncher.launch("taler-wallet-db-${currentTimeMillis()}.json")
+ true
+ }
+ prefImportDb.setOnPreferenceClickListener {
+ showImportDialog()
+ true
+ }
+ prefTest.setOnPreferenceClickListener {
+ model.runIntegrationTest()
true
}
-
prefReset.setOnPreferenceClickListener {
showResetDialog()
true
}
}
- private fun showResetDialog() {
- AlertDialog.Builder(requireContext(), R.style.DialogTheme)
- .setMessage("Do you really want to reset the wallet and lose all coins and purchases?")
- .setPositiveButton("Reset") { _, _ ->
- model.dangerouslyReset()
- Snackbar.make(requireView(), "Wallet has been reset", LENGTH_SHORT).show()
+ private fun showImportDialog() {
+ MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setMessage(R.string.settings_dialog_import_message)
+ .setNegativeButton(R.string.import_db) { _, _ ->
+ dbImportLauncher.launch(arrayOf("application/json"))
}
- .setNegativeButton("Cancel") { _, _ ->
- Snackbar.make(requireView(), "Reset cancelled", LENGTH_SHORT).show()
+ .setPositiveButton(R.string.cancel) { _, _ ->
+ Snackbar.make(requireView(), getString(R.string.settings_alert_import_canceled), LENGTH_SHORT).show()
}
.show()
}
+ private fun showResetDialog() {
+ MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setMessage(R.string.settings_dialog_reset_message)
+ .setNegativeButton(R.string.reset) { _, _ ->
+ settingsManager.clearDb {
+ model.dangerouslyReset()
+ }
+ Snackbar.make(requireView(), getString(R.string.settings_alert_reset_done), LENGTH_SHORT).show()
+ }
+ .setPositiveButton(R.string.cancel) { _, _ ->
+ Snackbar.make(requireView(), getString(R.string.settings_alert_reset_canceled), LENGTH_SHORT).show()
+ }
+ .show()
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt b/wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt
new file mode 100644
index 0000000..8331d59
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt
@@ -0,0 +1,171 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.settings
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import android.widget.Toast
+import android.widget.Toast.LENGTH_LONG
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.wallet.R
+import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.backend.WalletResponse.Error
+import net.taler.wallet.backend.WalletResponse.Success
+import org.json.JSONObject
+
+class SettingsManager(
+ private val context: Context,
+ private val api: WalletBackendApi,
+ private val scope: CoroutineScope,
+) {
+ fun exportLogcat(uri: Uri?) {
+ if (uri == null) {
+ onLogExportError()
+ return
+ }
+ scope.launch(Dispatchers.IO) {
+ try {
+ context.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
+ val command = arrayOf("logcat", "-d", "*:V")
+ val proc = Runtime.getRuntime().exec(command)
+ proc.inputStream.copyTo(outputStream)
+ } ?: onLogExportError()
+ } catch (e: Exception) {
+ Log.e(SettingsManager::class.simpleName, "Error exporting log: ", e)
+ onLogExportError()
+ return@launch
+ }
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, R.string.settings_logcat_success, LENGTH_LONG).show()
+ }
+ }
+ }
+
+ private fun onLogExportError() {
+ Toast.makeText(context, R.string.settings_logcat_error, LENGTH_LONG).show()
+ }
+
+ fun exportDb(uri: Uri?) {
+ if (uri == null) {
+ onDbExportError()
+ return
+ }
+
+ scope.launch(Dispatchers.IO) {
+ when (val response = api.rawRequest("exportDb")) {
+ is Success -> {
+ try {
+ context.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
+ val data = Json.encodeToString(response.result)
+ val writer = outputStream.bufferedWriter()
+ writer.write(data)
+ writer.close()
+ }
+ } catch(e: Exception) {
+ Log.e(SettingsManager::class.simpleName, "Error exporting db: ", e)
+ withContext(Dispatchers.Main) {
+ onDbExportError()
+ }
+ return@launch
+ }
+
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, R.string.settings_db_export_success, LENGTH_LONG).show()
+ }
+ }
+ is Error -> {
+ Log.e(SettingsManager::class.simpleName, "Error exporting db: ${response.error}")
+ withContext(Dispatchers.Main) {
+ onDbExportError()
+ }
+ return@launch
+ }
+ }
+ }
+ }
+
+ fun importDb(uri: Uri?) {
+ if (uri == null) {
+ onDbImportError()
+ return
+ }
+
+ scope.launch(Dispatchers.IO) {
+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
+ try {
+ val reader = inputStream.bufferedReader()
+ val strData = reader.readText()
+ reader.close()
+ val jsonData = JSONObject(strData)
+ when (val response = api.rawRequest("importDb") {
+ put("dump", jsonData)
+ }) {
+ is Success -> {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, R.string.settings_db_import_success, LENGTH_LONG).show()
+ }
+ }
+ is Error -> {
+ Log.e(SettingsManager::class.simpleName, "Error importing db: ${response.error}")
+ withContext(Dispatchers.Main) {
+ onDbImportError()
+ }
+ return@launch
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(SettingsManager::class.simpleName, "Error importing db: ", e)
+ withContext(Dispatchers.Main) {
+ onDbImportError()
+ }
+ return@launch
+ }
+ }
+ }
+ }
+
+ fun clearDb(onSuccess: () -> Unit) {
+ scope.launch {
+ when (val response = api.rawRequest("clearDb")) {
+ is Success -> onSuccess()
+ is Error -> {
+ Log.e(SettingsManager::class.simpleName, "Error cleaning db: ${response.error}")
+ onDbClearError()
+ }
+ }
+ }
+ }
+
+ private fun onDbExportError() {
+ Toast.makeText(context, R.string.settings_db_export_error, LENGTH_LONG).show()
+ }
+
+ private fun onDbImportError() {
+ Toast.makeText(context, R.string.settings_db_import_error, LENGTH_LONG).show()
+ }
+
+ private fun onDbClearError() {
+ Toast.makeText(context, R.string.settings_db_clear_error, LENGTH_LONG).show()
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt
new file mode 100644
index 0000000..4e4bbe0
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt
@@ -0,0 +1,116 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountBalance
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import net.taler.wallet.R
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionMinorState.BankConfirmTransfer
+import net.taler.wallet.transactions.TransactionMinorState.ExchangeWaitReserve
+import net.taler.wallet.transactions.TransactionMinorState.KycRequired
+
+interface ActionListener {
+ enum class Type {
+ COMPLETE_KYC,
+ CONFIRM_WITH_BANK,
+ CONFIRM_MANUAL
+ }
+
+ fun onActionButtonClicked(tx: Transaction, type: Type)
+}
+
+@Composable
+fun ActionButton(
+ modifier: Modifier = Modifier,
+ tx: TransactionWithdrawal,
+ listener: ActionListener,
+) {
+ if (tx.txState.major == Pending) {
+ when (tx.txState.minor) {
+ KycRequired -> KycButton(modifier, tx, listener)
+ BankConfirmTransfer -> ConfirmBankButton(modifier, tx, listener)
+ ExchangeWaitReserve -> ConfirmManualButton(modifier, tx, listener)
+ else -> {}
+ }
+ }
+}
+
+@Composable
+private fun KycButton(
+ modifier: Modifier = Modifier,
+ tx: TransactionWithdrawal,
+ listener: ActionListener,
+) {
+ Button(
+ onClick = { listener.onActionButtonClicked(tx, ActionListener.Type.COMPLETE_KYC) },
+ modifier = modifier,
+ ) {
+ Text(stringResource(R.string.transaction_action_kyc))
+ }
+}
+
+@Composable
+private fun ConfirmBankButton(
+ modifier: Modifier = Modifier,
+ tx: TransactionWithdrawal,
+ listener: ActionListener,
+) {
+ Button(
+ onClick = { listener.onActionButtonClicked(tx, ActionListener.Type.CONFIRM_WITH_BANK) },
+ modifier = modifier,
+ ) {
+ val label = stringResource(R.string.withdraw_button_confirm_bank)
+ Icon(
+ Icons.Default.AccountBalance,
+ label,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(label)
+ }
+}
+
+@Composable
+private fun ConfirmManualButton(
+ modifier: Modifier = Modifier,
+ tx: TransactionWithdrawal,
+ listener: ActionListener,
+) {
+ Button(
+ onClick = { listener.onActionButtonClicked(tx, ActionListener.Type.CONFIRM_MANUAL) },
+ modifier = modifier,
+ ) {
+ val label = stringResource(R.string.withdraw_manual_ready_details_intro)
+ Icon(
+ Icons.Default.AccountBalance,
+ label,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(label)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/ErrorTransactionComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/ErrorTransactionComposable.kt
new file mode 100644
index 0000000..ea875d7
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/ErrorTransactionComposable.kt
@@ -0,0 +1,116 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.sp
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.copyToClipBoard
+
+@Composable
+fun ErrorTransactionButton(
+ modifier: Modifier = Modifier,
+ error: TalerErrorInfo,
+) {
+ val showDialog = remember { mutableStateOf(false) }
+
+ if (showDialog.value) {
+ @Suppress("OPT_IN_USAGE")
+ val json = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+ val message = json.encodeToString(error)
+ AlertDialog(
+ onDismissRequest = {
+ showDialog.value = false
+ },
+ title = {
+ Text(stringResource(R.string.nav_error))
+ },
+ text = {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier.verticalScroll(scrollState),
+ ) {
+ Text(
+ fontFamily = FontFamily.Monospace,
+ fontSize = 10.sp,
+ text = message,
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = {
+ showDialog.value = false
+ }) {
+ Text(stringResource(R.string.close))
+ }
+ },
+ confirmButton = {
+ val context = LocalContext.current
+ TextButton(onClick = {
+ copyToClipBoard(context, context.getString(R.string.nav_error), message)
+ }) {
+ Text(stringResource(R.string.copy))
+ }
+ })
+ }
+
+ Button(
+ modifier = modifier,
+ colors = ButtonDefaults.buttonColors(
+ contentColor = MaterialTheme.colorScheme.onError,
+ containerColor = MaterialTheme.colorScheme.error,
+ ),
+ onClick = {
+ showDialog.value = true
+ }
+ ) {
+ val label = stringResource(R.string.nav_error)
+ Icon(
+ imageVector = Icons.Default.Error,
+ contentDescription = label,
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(label)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
index 5e492f5..22dcc3f 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
@@ -32,16 +32,24 @@ import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.common.CurrencySpecification
import net.taler.common.exhaustive
import net.taler.common.toRelativeTime
import net.taler.wallet.R
import net.taler.wallet.transactions.TransactionAdapter.TransactionViewHolder
+import net.taler.wallet.transactions.TransactionMajorState.Aborted
+import net.taler.wallet.transactions.TransactionMajorState.Failed
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionMinorState.BankConfirmTransfer
+import net.taler.wallet.transactions.TransactionMinorState.KycRequired
internal class TransactionAdapter(
- private val listener: OnTransactionClickListener
+ private val listener: OnTransactionClickListener,
) : Adapter<TransactionViewHolder>() {
private var transactions: List<Transaction> = ArrayList()
+ private var currencySpec: CurrencySpecification? = null
+
lateinit var tracker: SelectionTracker<String>
val keyProvider = TransactionKeyProvider()
@@ -62,6 +70,11 @@ internal class TransactionAdapter(
holder.bind(transaction, tracker.isSelected(transaction.transactionId))
}
+ fun setCurrencySpec(spec: CurrencySpecification?) {
+ this.currencySpec = spec
+ this.notifyDataSetChanged()
+ }
+
fun update(updatedTransactions: List<Transaction>) {
this.transactions = updatedTransactions
this.notifyDataSetChanged()
@@ -74,6 +87,7 @@ internal class TransactionAdapter(
internal inner class TransactionViewHolder(private val v: View) : ViewHolder(v) {
private val context: Context = v.context
+ private val root: ViewGroup = v.findViewById(R.id.root)
private val icon: ImageView = v.findViewById(R.id.icon)
private val title: TextView = v.findViewById(R.id.title)
private val extraInfoView: TextView = v.findViewById(R.id.extraInfoView)
@@ -82,12 +96,12 @@ internal class TransactionAdapter(
private val pendingView: TextView = v.findViewById(R.id.pendingView)
private val amountColor = amount.currentTextColor
+ private val extraInfoColor = extraInfoView.currentTextColor
private val red = getColor(context, R.color.red)
private val green = getColor(context, R.color.green)
fun bind(transaction: Transaction, selected: Boolean) {
v.setOnClickListener { listener.onTransactionClicked(transaction) }
- v.isActivated = selected
if (transaction.error == null) {
icon.setImageResource(transaction.icon)
} else {
@@ -97,39 +111,98 @@ internal class TransactionAdapter(
bindExtraInfo(transaction)
time.text = transaction.timestamp.ms.toRelativeTime(context)
bindAmount(transaction)
- pendingView.visibility = if (transaction.pending) VISIBLE else GONE
+ pendingView.visibility = if (transaction.txState.major == Pending) VISIBLE else GONE
+ val bgColor = getColor(
+ context,
+ if (selected) R.color.selectedBackground
+ else android.R.color.transparent
+ )
+ root.setBackgroundColor(bgColor)
}
private fun bindExtraInfo(transaction: Transaction) {
- if (transaction.error != null) {
- extraInfoView.text =
- context.getString(R.string.payment_error, transaction.error!!.userFacingMsg)
- extraInfoView.setTextColor(red)
- extraInfoView.visibility = VISIBLE
- } else if (transaction is TransactionWithdrawal && !transaction.confirmed) {
- extraInfoView.setText(R.string.withdraw_waiting_confirm)
- extraInfoView.setTextColor(amountColor)
- extraInfoView.visibility = VISIBLE
- } else if (transaction is TransactionPayment && transaction.status != PaymentStatus.Paid && transaction.status != PaymentStatus.Accepted) {
- extraInfoView.setText(if (transaction.status == PaymentStatus.Aborted) R.string.payment_aborted else R.string.payment_failed)
- extraInfoView.setTextColor(amountColor)
- extraInfoView.visibility = VISIBLE
- } else {
- extraInfoView.visibility = GONE
+ when {
+ // Goes first so it always shows errors when present
+ transaction.error != null -> {
+ extraInfoView.text =
+ context.getString(R.string.payment_error, transaction.error!!.userFacingMsg)
+ extraInfoView.setTextColor(red)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction.txState.major == Aborted -> {
+ extraInfoView.setText(R.string.payment_aborted)
+ extraInfoView.setTextColor(red)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction.txState.major == Failed -> {
+ extraInfoView.setText(R.string.payment_failed)
+ extraInfoView.setTextColor(red)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction.txState.major == Pending -> when (transaction.txState.minor) {
+ BankConfirmTransfer -> {
+ extraInfoView.setText(R.string.withdraw_waiting_confirm)
+ extraInfoView.setTextColor(amountColor)
+ extraInfoView.visibility = VISIBLE
+ }
+ KycRequired -> {
+ extraInfoView.setText(R.string.transaction_action_kyc)
+ extraInfoView.setTextColor(amountColor)
+ extraInfoView.visibility = VISIBLE
+ }
+ else -> extraInfoView.visibility = GONE
+ }
+
+ transaction is TransactionWithdrawal && !transaction.confirmed -> {
+ extraInfoView.setText(R.string.withdraw_waiting_confirm)
+ extraInfoView.setTextColor(amountColor)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction is TransactionPeerPushCredit && transaction.info.summary != null -> {
+ extraInfoView.text = transaction.info.summary
+ extraInfoView.setTextColor(extraInfoColor)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction is TransactionPeerPushDebit && transaction.info.summary != null -> {
+ extraInfoView.text = transaction.info.summary
+ extraInfoView.setTextColor(extraInfoColor)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction is TransactionPeerPullCredit && transaction.info.summary != null -> {
+ extraInfoView.text = transaction.info.summary
+ extraInfoView.setTextColor(extraInfoColor)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ transaction is TransactionPeerPullDebit && transaction.info.summary != null -> {
+ extraInfoView.text = transaction.info.summary
+ extraInfoView.setTextColor(extraInfoColor)
+ extraInfoView.visibility = VISIBLE
+ }
+
+ else -> extraInfoView.visibility = GONE
}
}
private fun bindAmount(transaction: Transaction) {
- val amountStr = transaction.amountEffective.amountStr
+ val amountStr = transaction.amountEffective.withSpec(currencySpec).toString(showSymbol = false)
when (transaction.amountType) {
AmountType.Positive -> {
amount.text = context.getString(R.string.amount_positive, amountStr)
- amount.setTextColor(if (transaction.pending) amountColor else green)
+ amount.setTextColor(if (transaction.txState.major == Pending) amountColor else green)
}
+
AmountType.Negative -> {
amount.text = context.getString(R.string.amount_negative, amountStr)
- amount.setTextColor(if (transaction.pending) amountColor else red)
+ amount.setTextColor(if (transaction.txState.major == Pending) amountColor else red)
}
+
AmountType.Neutral -> {
amount.text = amountStr
amount.setTextColor(amountColor)
@@ -149,12 +222,13 @@ internal class TransactionAdapter(
internal class TransactionLookup(
private val list: RecyclerView,
- private val adapter: TransactionAdapter
+ private val adapter: TransactionAdapter,
) : ItemDetailsLookup<String>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
list.findChildViewUnder(e.x, e.y)?.let { view ->
val holder = list.getChildViewHolder(view)
- val position = holder.adapterPosition
+ val position = holder.bindingAdapterPosition
+ if (position < 0) return null
return object : ItemDetails<String>() {
override fun getPosition(): Int = position
override fun getSelectionKey(): String = adapter.keyProvider.getKey(position)
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt
new file mode 100644
index 0000000..d2be3cf
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt
@@ -0,0 +1,48 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.platform.ComposeView
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.deposit.TransactionDepositComposable
+
+class TransactionDepositFragment : TransactionDetailFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionDeposit) TransactionDepositComposable(
+ t = t,
+ devMode = devMode,
+ spec = balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ ) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt
index 866b363..09ca05b 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt
@@ -16,73 +16,151 @@
package net.taler.wallet.transactions
-import android.content.Intent
-import android.net.Uri
import android.os.Bundle
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import android.widget.TextView
+import android.util.Log
+import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
-import net.taler.common.startActivitySafe
-import net.taler.lib.common.Amount
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
+import net.taler.wallet.TAG
+import net.taler.wallet.showError
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Delete
+import net.taler.wallet.transactions.TransactionAction.Fail
+import net.taler.wallet.transactions.TransactionAction.Resume
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
abstract class TransactionDetailFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
- private val transactionManager by lazy { model.transactionManager }
- protected val transaction: Transaction? get() = transactionManager.selectedTransaction
+ protected val transactionManager by lazy { model.transactionManager }
+ protected val balanceManager by lazy { model.balanceManager }
+ protected val devMode get() = model.devMode.value == true
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(model.devMode.value == true)
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ transactionManager.selectedTransaction.observe(viewLifecycleOwner) {
+ requireActivity().apply {
+ it?.generalTitleRes?.let {
+ title = getString(it)
+ }
+ }
+ }
+ }
+
+ private fun dialogTitle(t: TransactionAction): Int = when (t) {
+ Delete -> R.string.transactions_delete_dialog_title
+ Abort -> R.string.transactions_abort_dialog_title
+ Fail -> R.string.transactions_fail_dialog_title
+ else -> error("unsupported action: $t")
+ }
+
+ private fun dialogMessage(t: TransactionAction): Int = when (t) {
+ Delete -> R.string.transactions_delete_dialog_message
+ Abort -> R.string.transactions_abort_dialog_message
+ Fail -> R.string.transactions_fail_dialog_message
+ else -> error("unsupported action: $t")
}
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- requireActivity().apply {
- transaction?.generalTitleRes?.let {
- title = getString(it)
+ private fun dialogButton(t: TransactionAction): Int = when (t) {
+ Delete -> R.string.transactions_delete
+ Abort -> R.string.transactions_abort
+ Fail -> R.string.transactions_fail
+ else -> error("unsupported")
+ }
+
+ protected fun onTransitionButtonClicked(t: Transaction, ta: TransactionAction) = when (ta) {
+ Delete -> showDialog(ta) { deleteTransaction(t) }
+ Abort -> showDialog(ta) { abortTransaction(t) }
+ Fail -> showDialog(ta) { failTransaction(t) }
+ Retry -> retryTransaction(t)
+ Suspend -> suspendTransaction(t)
+ Resume -> resumeTransaction(t)
+ }
+
+ private fun showDialog(tt: TransactionAction, onAction: () -> Unit) {
+ MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
+ .setTitle(dialogTitle(tt))
+ .setMessage(dialogMessage(tt))
+ .setNeutralButton(R.string.cancel) { dialog, _ ->
+ dialog.cancel()
+ }
+ .setNegativeButton(dialogButton(tt)) { dialog, _ ->
+ onAction()
+ dialog.dismiss()
+ }
+ .show()
+ }
+
+ private fun deleteTransaction(t: Transaction) {
+ transactionManager.deleteTransaction(t.transactionId) {
+ Log.e(TAG, "Error deleteTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
}
}
+ findNavController().popBackStack()
}
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.transactions_detail, menu)
+ private fun retryTransaction(t: Transaction) {
+ transactionManager.retryTransaction(t.transactionId) {
+ Log.e(TAG, "Error retryTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- else -> super.onOptionsItemSelected(item)
+ private fun abortTransaction(t: Transaction) {
+ transactionManager.abortTransaction(t.transactionId) {
+ Log.e(TAG, "Error abortTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
}
}
- protected fun bindOrderAndFee(
- orderSummaryView: TextView,
- orderAmountView: TextView,
- orderIdView: TextView,
- feeView: TextView,
- info: TransactionInfo,
- raw: Amount,
- fee: Amount
- ) {
- orderAmountView.text = raw.toString()
- feeView.text = getString(R.string.amount_negative, fee.toString())
- orderSummaryView.text = if (info.fulfillmentMessage == null) {
- info.summary
- } else {
- "${info.summary}\n\n${info.fulfillmentMessage}"
+ private fun failTransaction(t: Transaction) {
+ transactionManager.failTransaction(t.transactionId) {
+ Log.e(TAG, "Error failTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
}
- if (info.fulfillmentUrl?.startsWith("http") == true) {
- val i = Intent().apply {
- data = Uri.parse(info.fulfillmentUrl)
+ }
+
+ private fun suspendTransaction(t: Transaction) {
+ transactionManager.suspendTransaction(t.transactionId) {
+ Log.e(TAG, "Error suspendTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
}
- orderSummaryView.setOnClickListener { startActivitySafe(i) }
}
- orderIdView.text = getString(R.string.transaction_order_id, info.orderId)
}
+ private fun resumeTransaction(t: Transaction) {
+ transactionManager.resumeTransaction(t.transactionId) {
+ Log.e(TAG, "Error resumeTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDummyFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDummyFragment.kt
new file mode 100644
index 0000000..ad5e19c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDummyFragment.kt
@@ -0,0 +1,64 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.unit.dp
+import net.taler.wallet.compose.TalerSurface
+
+class TransactionDummyFragment : TransactionDetailFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState(null).value
+ if (t is DummyTransaction) TransactionDummyComposable(t)
+ }
+ }
+ }
+}
+
+@Composable
+fun TransactionDummyComposable(t: DummyTransaction) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ ErrorTransactionButton(error = t.error)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionLinkComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionLinkComposable.kt
new file mode 100644
index 0000000..e8fca0f
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionLinkComposable.kt
@@ -0,0 +1,86 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import android.R
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.getAttrColor
+
+@Composable
+// FIXME this assumes that it is used in a column and applies its own padding, not really re-usable
+fun TransactionLinkComposable(label: String, info: String, onClick: () -> Unit) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ val context = LocalContext.current
+ val linkColor = Color(context.getAttrColor(R.attr.textColorLink))
+ val annotatedString = buildAnnotatedString {
+ pushStringAnnotation(tag = "url", annotation = info)
+ withStyle(style = SpanStyle(color = linkColor)) {
+ append(info)
+ }
+ pop()
+ }
+ ClickableText(
+ modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
+ text = annotatedString,
+ style = TextStyle(fontSize = 24.sp, textAlign = TextAlign.Center),
+ ) { offset ->
+ annotatedString.getStringAnnotations(
+ tag = "url",
+ start = offset,
+ end = offset,
+ ).firstOrNull()?.let {
+ onClick()
+ }
+ }
+}
+
+@Preview
+@Composable
+fun TransactionLinkComposablePreview() {
+ TalerSurface {
+ Column(
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ TransactionLinkComposable(
+ label = "This is a label",
+ info = "This is some fulfillment message"
+ ) {}
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
index 6b5a79b..5399287 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
@@ -16,75 +16,203 @@
package net.taler.wallet.transactions
+import android.util.Log
import androidx.annotation.UiThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.switchMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
-import java.util.HashMap
+import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.transactions.TransactionAction.Delete
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import org.json.JSONObject
import java.util.LinkedList
sealed class TransactionsResult {
- class Error(val msg: String) : TransactionsResult()
+ class Error(val error: TalerErrorInfo) : TransactionsResult()
class Success(val transactions: List<Transaction>) : TransactionsResult()
}
class TransactionManager(
private val api: WalletBackendApi,
- private val scope: CoroutineScope
+ private val scope: CoroutineScope,
) {
private val mProgress = MutableLiveData<Boolean>()
val progress: LiveData<Boolean> = mProgress
- var selectedCurrency: String? = null
- var selectedTransaction: Transaction? = null
+ // FIXME if the app gets killed, this will not be restored and thus be unexpected null
+ // we should keep this in a savable, maybe using Hilt and SavedStateViewModel
+ var selectedScope: ScopeInfo? = null
val searchQuery = MutableLiveData<String>(null)
- private val allTransactions = HashMap<String, List<Transaction>>()
- private val mTransactions = HashMap<String, MutableLiveData<TransactionsResult>>()
+ private val mSelectedTransaction = MutableLiveData<Transaction?>(null)
+ val selectedTransaction: LiveData<Transaction?> = mSelectedTransaction
+ private val allTransactions = HashMap<ScopeInfo, List<Transaction>>()
+ private val mTransactions = HashMap<ScopeInfo, MutableLiveData<TransactionsResult>>()
val transactions: LiveData<TransactionsResult>
@UiThread
get() = searchQuery.switchMap { query ->
- val currency = selectedCurrency
- check(currency != null) { "Did not select currency before getting transactions" }
+ val scopeInfo = selectedScope
+ check(scopeInfo != null) { "Did not select scope before getting transactions" }
loadTransactions(query)
- mTransactions[currency]!! // non-null because filled in [loadTransactions]
+ mTransactions[scopeInfo]!! // non-null because filled in [loadTransactions]
}
@UiThread
fun loadTransactions(searchQuery: String? = null) = scope.launch {
- val currency = selectedCurrency ?: return@launch
- val liveData = mTransactions.getOrPut(currency) { MutableLiveData() }
- if (searchQuery == null && allTransactions.containsKey(currency)) {
- liveData.value = TransactionsResult.Success(allTransactions[currency]!!)
+ val scopeInfo = selectedScope ?: return@launch
+ val liveData = mTransactions.getOrPut(scopeInfo) { MutableLiveData() }
+ if (searchQuery == null && allTransactions.containsKey(scopeInfo)) {
+ liveData.value = TransactionsResult.Success(allTransactions[scopeInfo]!!)
}
if (liveData.value == null) mProgress.value = true
api.request("getTransactions", Transactions.serializer()) {
if (searchQuery != null) put("search", searchQuery)
- put("currency", currency)
+ put("scopeInfo", JSONObject(Json.encodeToString(scopeInfo)))
}.onError {
- liveData.postValue(TransactionsResult.Error(it.userFacingMsg))
+ liveData.postValue(TransactionsResult.Error(it))
mProgress.postValue(false)
}.onSuccess { result ->
val transactions = LinkedList(result.transactions)
- // TODO remove when fixed in wallet-core
- val comparator = compareBy<Transaction>(
- { it.pending },
- { it.timestamp.ms },
- { it.transactionId }
- )
- transactions.sortWith(comparator)
transactions.reverse() // show latest first
mProgress.value = false
liveData.value = TransactionsResult.Success(transactions)
- // update all transactions on UiThread if there was a currency
- if (searchQuery == null) allTransactions[currency] = transactions
+ // update selected transaction on UiThread (if it exists)
+ val selected = selectedTransaction.value
+ if (selected != null) transactions.find {
+ it.transactionId == selected.transactionId
+ }?.let {
+ mSelectedTransaction.value = it
+ }
+
+ // update all transactions on UiThread if there was a scope info
+ if (searchQuery == null) allTransactions[scopeInfo] = transactions
+ }
+ }
+
+ /**
+ * Returns true if given [transactionId] was found and selected, false otherwise.
+ */
+ @UiThread
+ suspend fun selectTransaction(transactionId: String): Boolean {
+ var transaction: Transaction? = null
+ api.request("getTransactionById", Transaction.serializer()) {
+ put("transactionId", transactionId)
+ }.onError {
+ Log.e(TAG, "Error getting transaction $it")
+ }.onSuccess { result ->
+ transaction = result
+ }
+ return if (transaction != null) {
+ mSelectedTransaction.value = transaction
+ true
+ } else {
+ false
+ }
+ }
+
+ suspend fun getTransactionById(transactionId: String): Transaction? {
+ var transaction: Transaction? = null
+ api.request("getTransactionById", Transaction.serializer()) {
+ put("transactionId", transactionId)
+ }.onError {
+ Log.e(TAG, "Error getting transaction $it")
+ }.onSuccess { result ->
+ transaction = result
+ }
+ return transaction
+ }
+
+ fun selectTransaction(transaction: Transaction) {
+ mSelectedTransaction.postValue(transaction)
+ }
+
+ fun deleteTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("deleteTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ // re-load transactions as our list is stale otherwise
+ loadTransactions()
+ }
+ }
+
+ fun retryTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("retryTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun abortTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("abortTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun failTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("failTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun suspendTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("suspendTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun resumeTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =
+ scope.launch {
+ api.request<Unit>("resumeTransaction") {
+ put("transactionId", transactionId)
+ }.onError {
+ onError(it)
+ }.onSuccess {
+ loadTransactions()
+ }
+ }
+
+ fun deleteTransactions(transactionIds: List<String>, onError: (it: TalerErrorInfo) -> Unit) {
+ allTransactions[selectedScope]?.filter { transaction ->
+ transaction.transactionId in transactionIds
+ }?.forEach { toBeDeletedTx ->
+ if (Delete in toBeDeletedTx.txActions) {
+ deleteTransaction(toBeDeletedTx.transactionId) {
+ onError(it)
+ }
+ }
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt
index 84c5c77..596a4a9 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt
@@ -20,37 +20,32 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import net.taler.common.toAbsoluteTime
-import net.taler.wallet.databinding.FragmentTransactionPaymentBinding
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.platform.ComposeView
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.launchInAppBrowser
+import net.taler.wallet.payment.TransactionPaymentComposable
class TransactionPaymentFragment : TransactionDetailFragment() {
- private lateinit var ui: FragmentTransactionPaymentBinding
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- ui = FragmentTransactionPaymentBinding.inflate(inflater, container, false)
- return ui.root
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionPayment) TransactionPaymentComposable(t, devMode,
+ balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ onFulfill = { url ->
+ launchInAppBrowser(requireContext(), url)
+ },
+ onTransition = {
+ onTransitionButtonClicked(t, it)
+ }
+ )
+ }
+ }
}
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val t = transaction as TransactionPayment
- ui.timeView.text = t.timestamp.ms.toAbsoluteTime(requireContext())
-
- ui.amountPaidWithFeesView.text = t.amountEffective.toString()
- val fee = t.amountEffective - t.amountRaw
- bindOrderAndFee(
- ui.orderSummaryView,
- ui.orderAmountView,
- ui.orderIdView,
- ui.feeView,
- t.info,
- t.amountRaw,
- fee
- )
- }
-
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt
new file mode 100644
index 0000000..27809a7
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt
@@ -0,0 +1,135 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.peer.TransactionPeerPullCreditComposable
+import net.taler.wallet.peer.TransactionPeerPullDebitComposable
+import net.taler.wallet.peer.TransactionPeerPushCreditComposable
+import net.taler.wallet.peer.TransactionPeerPushDebitComposable
+
+class TransactionPeerFragment : TransactionDetailFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState(null).value
+ if (t != null) TransactionPeerComposable(t, devMode,
+ balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ ) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TransactionPeerComposable(
+ t: Transaction,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ when (t) {
+ is TransactionPeerPullCredit -> TransactionPeerPullCreditComposable(t, spec)
+ is TransactionPeerPushCredit -> TransactionPeerPushCreditComposable(t, spec)
+ is TransactionPeerPullDebit -> TransactionPeerPullDebitComposable(t, spec)
+ is TransactionPeerPushDebit -> TransactionPeerPushDebitComposable(t, spec)
+ else -> error("unexpected transaction: ${t::class.simpleName}")
+ }
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error!!)
+ }
+ }
+}
+
+@Composable
+fun TransactionAmountComposable(label: String, amount: Amount, amountType: AmountType) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ Text(
+ modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
+ text = amount.toString(negative = amountType == AmountType.Negative),
+ fontSize = 24.sp,
+ color = when (amountType) {
+ AmountType.Positive -> colorResource(R.color.green)
+ AmountType.Negative -> MaterialTheme.colorScheme.error
+ AmountType.Neutral -> Color.Unspecified
+ },
+ )
+}
+
+@Composable
+fun TransactionInfoComposable(label: String, info: String) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ Text(
+ modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
+ text = info,
+ fontSize = 24.sp,
+ )
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt
index 717dd33..8f474f9 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt
@@ -19,38 +19,102 @@ package net.taler.wallet.transactions
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
-import android.view.View.GONE
import android.view.ViewGroup
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
import net.taler.common.toAbsoluteTime
import net.taler.wallet.R
-import net.taler.wallet.cleanExchange
-import net.taler.wallet.databinding.FragmentTransactionWithdrawalBinding
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionMajorState.Pending
class TransactionRefreshFragment : TransactionDetailFragment() {
- private lateinit var ui: FragmentTransactionWithdrawalBinding
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- ui = FragmentTransactionWithdrawalBinding.inflate(inflater, container, false)
- return ui.root
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionRefresh) TransactionRefreshComposable(t, devMode,
+ balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ ) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
+ }
}
+}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val t = transaction as TransactionRefresh
- ui.timeView.text = t.timestamp.ms.toAbsoluteTime(requireContext())
-
- ui.effectiveAmountLabel.visibility = GONE
- ui.effectiveAmountView.visibility = GONE
- ui.confirmWithdrawalButton.visibility = GONE
- ui.chosenAmountLabel.visibility = GONE
- ui.chosenAmountView.visibility = GONE
- val fee = t.amountEffective
- ui.feeView.text = getString(R.string.amount_negative, fee.toString())
- ui. exchangeView.text = cleanExchange(t.exchangeBaseUrl)
+@Composable
+private fun TransactionRefreshComposable(
+ t: TransactionRefresh,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ TransitionsComposable(t, devMode, onTransition)
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
}
+}
+@Preview
+@Composable
+private fun TransactionRefreshComposablePreview() {
+ val t = TransactionRefresh(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ )
+ Surface {
+ TransactionRefreshComposable(t, true, null) {}
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt
index 6628d6c..7992565 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt
@@ -20,42 +20,27 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.core.content.ContextCompat.getColor
-import net.taler.common.toAbsoluteTime
-import net.taler.wallet.R
-import net.taler.wallet.databinding.FragmentTransactionPaymentBinding
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.platform.ComposeView
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.refund.TransactionRefundComposable
class TransactionRefundFragment : TransactionDetailFragment() {
- private lateinit var ui: FragmentTransactionPaymentBinding
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- ui = FragmentTransactionPaymentBinding.inflate(inflater, container, false)
- return ui.root
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionRefund) TransactionRefundComposable(t, devMode,
+ balanceManager.getSpecForCurrency(t.amountRaw.currency)
+ ) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
+ }
}
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val t = transaction as TransactionRefund
- ui.timeView.text = t.timestamp.ms.toAbsoluteTime(requireContext())
-
- ui.amountPaidWithFeesLabel.text = getString(R.string.transaction_refund)
- ui.amountPaidWithFeesView.setTextColor(getColor(requireContext(), R.color.green))
- ui.amountPaidWithFeesView.text =
- getString(R.string.amount_positive, t.amountEffective.toString())
- val fee = t.amountRaw - t.amountEffective
- bindOrderAndFee(
- ui.orderSummaryView,
- ui.orderAmountView,
- ui.orderIdView,
- ui.feeView,
- t.info,
- t.amountRaw,
- fee
- )
- }
-
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt
new file mode 100644
index 0000000..7091c90
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt
@@ -0,0 +1,180 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TransactionState(
+ val major: TransactionMajorState,
+ val minor: TransactionMinorState? = null,
+) {
+ override fun equals(other: Any?): Boolean {
+ return if (other is TransactionState)
+ // if other.minor is null, then ignore minor in comparison
+ major == other.major && (other.minor == null || minor == other.minor)
+ else false
+ }
+
+ override fun hashCode(): Int {
+ var result = major.hashCode()
+ result = 31 * result + (minor?.hashCode() ?: 0)
+ return result
+ }
+}
+
+@Serializable
+enum class TransactionMajorState {
+ @SerialName("none")
+ None,
+
+ @SerialName("pending")
+ Pending,
+
+ @SerialName("done")
+ Done,
+
+ @SerialName("aborting")
+ Aborting,
+
+ @SerialName("aborted")
+ Aborted,
+
+ @SerialName("suspended")
+ Suspended,
+
+ @SerialName("dialog")
+ Dialog,
+
+ @SerialName("suspended-aborting")
+ SuspendedAborting,
+
+ @SerialName("failed")
+ Failed,
+
+ @SerialName("deleted")
+ Deleted,
+
+ @SerialName("expired")
+ Expired,
+
+ @SerialName("unknown")
+ Unknown;
+}
+
+@Serializable
+enum class TransactionMinorState {
+ @SerialName("unknown")
+ Unknown,
+
+ @SerialName("deposit")
+ Deposit,
+
+ @SerialName("kyc")
+ KycRequired,
+
+ @SerialName("aml")
+ AmlRequired,
+
+ @SerialName("merge-kyc")
+ MergeKycRequired,
+
+ @SerialName("track")
+ Track,
+
+ @SerialName("submit-payment")
+ SubmitPayment,
+
+ @SerialName("rebind-session")
+ RebindSession,
+
+ @SerialName("refresh")
+ Refresh,
+
+ @SerialName("pickup")
+ Pickup,
+
+ @SerialName("auto-refund")
+ AutoRefund,
+
+ @SerialName("user")
+ User,
+
+ @SerialName("bank")
+ Bank,
+
+ @SerialName("exchange")
+ Exchange,
+
+ @SerialName("claim-proposal")
+ ClaimProposal,
+
+ @SerialName("check-refund")
+ CheckRefund,
+
+ @SerialName("create-purse")
+ CreatePurse,
+
+ @SerialName("delete-purse")
+ DeletePurse,
+
+ @SerialName("refresh-expired")
+ RefreshExpired,
+
+ @SerialName("ready")
+ Ready,
+
+ @SerialName("merge")
+ Merge,
+
+ @SerialName("repurchase")
+ Repurchase,
+
+ @SerialName("bank-register-reserve")
+ BankRegisterReserve,
+
+ @SerialName("bank-confirm-transfer")
+ BankConfirmTransfer,
+
+ @SerialName("withdraw-coins")
+ WithdrawCoins,
+
+ @SerialName("exchange-wait-reserve")
+ ExchangeWaitReserve,
+
+ @SerialName("aborting-bank")
+ AbortingBank,
+
+ @SerialName("refused")
+ Refused,
+
+ @SerialName("withdraw")
+ Withdraw,
+
+ @SerialName("merchant-order-proposed")
+ MerchantOrderProposed,
+
+ @SerialName("proposed")
+ Proposed,
+
+ @SerialName("refund-available")
+ RefundAvailable,
+
+ @SerialName("accept-refund")
+ AcceptRefund
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
index 8a45bec..27e59bb 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
@@ -16,52 +16,81 @@
package net.taler.wallet.transactions
-import android.content.Intent
-import android.content.Intent.ACTION_VIEW
-import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import net.taler.common.startActivitySafe
-import net.taler.common.toAbsoluteTime
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import net.taler.wallet.MainViewModel
import net.taler.wallet.R
-import net.taler.wallet.cleanExchange
-import net.taler.wallet.databinding.FragmentTransactionWithdrawalBinding
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.launchInAppBrowser
+import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer
+import net.taler.wallet.transactions.WithdrawalDetails.TalerBankIntegrationApi
+import net.taler.wallet.withdraw.TransactionWithdrawalComposable
+import net.taler.wallet.withdraw.createManualTransferRequired
-class TransactionWithdrawalFragment : TransactionDetailFragment() {
+class TransactionWithdrawalFragment : TransactionDetailFragment(), ActionListener {
- private lateinit var ui: FragmentTransactionWithdrawalBinding
+ private val model: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { model.withdrawManager }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- ui = FragmentTransactionWithdrawalBinding.inflate(inflater, container, false)
- return ui.root
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ if (t is TransactionWithdrawal) TransactionWithdrawalComposable(
+ t = t,
+ devMode = devMode,
+ spec = balanceManager.getSpecForCurrency(t.amountRaw.currency),
+ actionListener = this@TransactionWithdrawalFragment,
+ ) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
+ }
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val t = transaction as TransactionWithdrawal
- ui.timeView.text = t.timestamp.ms.toAbsoluteTime(requireContext())
+ override fun onActionButtonClicked(tx: Transaction, type: ActionListener.Type) {
+ when (type) {
+ ActionListener.Type.COMPLETE_KYC -> {
+ if (tx !is TransactionWithdrawal) return
+ tx.kycUrl?.let {
+ launchInAppBrowser(requireContext(), it)
+ }
+ }
- ui.effectiveAmountLabel.text = getString(R.string.withdraw_total)
- ui.effectiveAmountView.text = t.amountEffective.toString()
- if (t.pending && t.withdrawalDetails is WithdrawalDetails.TalerBankIntegrationApi &&
- !t.confirmed && t.withdrawalDetails.bankConfirmationUrl != null
- ) {
- val i = Intent(ACTION_VIEW).apply {
- data = Uri.parse(t.withdrawalDetails.bankConfirmationUrl)
+ ActionListener.Type.CONFIRM_WITH_BANK -> {
+ if (tx !is TransactionWithdrawal) return
+ if (tx.withdrawalDetails !is TalerBankIntegrationApi) return
+ tx.withdrawalDetails.bankConfirmationUrl?.let { url ->
+ launchInAppBrowser(requireContext(), url)
+ }
}
- ui.confirmWithdrawalButton.setOnClickListener { startActivitySafe(i) }
- } else ui.confirmWithdrawalButton.visibility = View.GONE
- ui.chosenAmountLabel.text = getString(R.string.amount_chosen)
- ui.chosenAmountView.text =
- getString(R.string.amount_positive, t.amountRaw.toString())
- val fee = t.amountRaw - t.amountEffective
- ui.feeView.text = getString(R.string.amount_negative, fee.toString())
- ui.exchangeView.text = cleanExchange(t.exchangeBaseUrl)
- }
+ ActionListener.Type.CONFIRM_MANUAL -> {
+ if (tx !is TransactionWithdrawal) return
+ if (tx.withdrawalDetails !is ManualTransfer) return
+ if (tx.withdrawalDetails.exchangeCreditAccountDetails.isNullOrEmpty()) return
+ val status = createManualTransferRequired(
+ transactionId = tx.transactionId,
+ exchangeBaseUrl = tx.exchangeBaseUrl,
+ amountRaw = tx.amountRaw,
+ amountEffective = tx.amountEffective,
+ withdrawalAccountList = tx.withdrawalDetails.exchangeCreditAccountDetails,
+ )
+ withdrawManager.viewManualWithdrawal(status)
+ findNavController().navigate(
+ R.id.action_nav_transactions_detail_withdrawal_to_nav_exchange_manual_withdrawal_success,
+ )
+ }
+ }
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
index 50181c5..be36a13 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
@@ -17,30 +17,92 @@
package net.taler.wallet.transactions
import android.content.Context
+import android.util.Log
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.StringRes
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
import kotlinx.serialization.Transient
+import kotlinx.serialization.builtins.ListSerializer
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonElement
+import net.taler.common.Amount
import net.taler.common.ContractMerchant
import net.taler.common.ContractProduct
-import net.taler.lib.common.Amount
-import net.taler.lib.common.Timestamp
+import net.taler.common.Timestamp
import net.taler.wallet.R
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.common.CurrencySpecification
import net.taler.wallet.cleanExchange
+import net.taler.wallet.refund.RefundPaymentInfo
+import net.taler.wallet.transactions.TransactionMajorState.None
+import net.taler.wallet.transactions.TransactionMajorState.Pending
import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer
import net.taler.wallet.transactions.WithdrawalDetails.TalerBankIntegrationApi
+import java.util.UUID
@Serializable
-data class Transactions(val transactions: List<Transaction>)
+data class Transactions(
+ @Serializable(with = TransactionListSerializer::class)
+ val transactions: List<Transaction>,
+)
+
+class TransactionListSerializer : KSerializer<List<Transaction>> {
+ private val serializer = ListSerializer(TransactionSerializer())
+ override val descriptor: SerialDescriptor = serializer.descriptor
+
+ override fun deserialize(decoder: Decoder): List<Transaction> {
+ return decoder.decodeSerializableValue(serializer)
+ }
+
+ override fun serialize(encoder: Encoder, value: List<Transaction>) {
+ throw NotImplementedError()
+ }
+}
+
+class TransactionSerializer : KSerializer<Transaction> {
+
+ private val serializer = Transaction.serializer()
+ override val descriptor: SerialDescriptor = serializer.descriptor
+ private val jsonSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
+
+ override fun deserialize(decoder: Decoder): Transaction {
+ return try {
+ decoder.decodeSerializableValue(serializer)
+ } catch (e: SerializationException) {
+ Log.e(TAG, "Error deserializing transaction.", e)
+ DummyTransaction(
+ transactionId = UUID.randomUUID().toString(),
+ timestamp = Timestamp.now(),
+ error = TalerErrorInfo(
+ code = TalerErrorCode.UNKNOWN,
+ message = e.message,
+ extra = decoder.decodeSerializableValue(jsonSerializer)
+ ),
+ )
+ }
+ }
+
+ override fun serialize(encoder: Encoder, value: Transaction) {
+ throw NotImplementedError()
+ }
+}
@Serializable
sealed class Transaction {
abstract val transactionId: String
abstract val timestamp: Timestamp
- abstract val pending: Boolean
+ abstract val txState: TransactionState
+ abstract val txActions: List<TransactionAction>
abstract val error: TalerErrorInfo?
abstract val amountRaw: Amount
abstract val amountEffective: Amount
@@ -59,6 +121,28 @@ sealed class Transaction {
abstract val generalTitleRes: Int
}
+@Serializable
+enum class TransactionAction {
+ // Common States
+ @SerialName("delete")
+ Delete,
+
+ @SerialName("suspend")
+ Suspend,
+
+ @SerialName("resume")
+ Resume,
+
+ @SerialName("abort")
+ Abort,
+
+ @SerialName("fail")
+ Fail,
+
+ @SerialName("retry")
+ Retry,
+}
+
sealed class AmountType {
object Positive : AmountType()
object Negative : AmountType()
@@ -70,12 +154,14 @@ sealed class AmountType {
class TransactionWithdrawal(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
+ val kycUrl: String? = null,
val exchangeBaseUrl: String,
val withdrawalDetails: WithdrawalDetails,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
- override val amountEffective: Amount
+ override val amountEffective: Amount,
) : Transaction() {
override val icon = R.drawable.transaction_withdrawal
@@ -86,7 +172,7 @@ class TransactionWithdrawal(
override fun getTitle(context: Context) = cleanExchange(exchangeBaseUrl)
override val generalTitleRes = R.string.withdraw_title
val confirmed: Boolean
- get() = !pending && (
+ get() = txState.major != Pending && (
(withdrawalDetails is TalerBankIntegrationApi && withdrawalDetails.confirmed) ||
withdrawalDetails is ManualTransfer
)
@@ -97,12 +183,7 @@ sealed class WithdrawalDetails {
@Serializable
@SerialName("manual-transfer")
class ManualTransfer(
- /**
- * Payto URIs that the exchange supports.
- *
- * Already contains the amount and message.
- */
- val exchangePaytoUris: List<String>
+ val exchangeCreditAccountDetails: List<WithdrawalExchangeAccountDetails>? = null,
) : WithdrawalDetails()
@Serializable
@@ -124,16 +205,82 @@ sealed class WithdrawalDetails {
}
@Serializable
+data class WithdrawalExchangeAccountDetails (
+ /**
+ * Payto URI to credit the exchange.
+ *
+ * Depending on whether the (manual!) withdrawal is accepted or just
+ * being checked, this already includes the subject with the
+ * reserve public key.
+ */
+ val paytoUri: String,
+
+ /**
+ * Transfer amount. Might be in a different currency than the requested
+ * amount for withdrawal.
+ *
+ * Redundant with the amount in paytoUri, just included to avoid parsing.
+ */
+ val transferAmount: Amount? = null,
+
+ /**
+ * Currency specification for the external currency.
+ *
+ * Only included if this account requires a currency conversion.
+ */
+ val currencySpecification: CurrencySpecification? = null,
+
+ /**
+ * Further restrictions for sending money to the
+ * exchange.
+ */
+ val creditRestrictions: List<AccountRestriction>? = null,
+)
+
+@Serializable
+sealed class AccountRestriction {
+ @Serializable
+ @SerialName("deny")
+ data object DenyAllAccount: AccountRestriction()
+
+ @Serializable
+ @SerialName("regex")
+ data class RegexAccount(
+ // Regular expression that the payto://-URI of the
+ // partner account must follow. The regular expression
+ // should follow posix-egrep, but without support for character
+ // classes, GNU extensions, back-references or intervals. See
+ // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
+ // for a description of the posix-egrep syntax. Applications
+ // may support regexes with additional features, but exchanges
+ // must not use such regexes.
+ @SerialName("payto_regex")
+ val paytoRegex: String,
+
+ // Hint for a human to understand the restriction
+ // (that is hopefully easier to comprehend than the regex itself).
+ @SerialName("human_hint")
+ val humanHint: String,
+
+ // Map from IETF BCP 47 language tags to localized
+ // human hints.
+ @SerialName("human_hint_i18n")
+ val humanHintI18n: Map<String, String>? = null,
+ ): AccountRestriction()
+}
+
+@Serializable
@SerialName("payment")
class TransactionPayment(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val info: TransactionInfo,
- val status: PaymentStatus,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
- override val amountEffective: Amount
+ override val amountEffective: Amount,
+ val posConfirmation: String? = null,
) : Transaction() {
override val icon = R.drawable.ic_cash_usd_outline
override val detailPageNav = R.id.action_nav_transactions_detail_payment
@@ -151,7 +298,7 @@ class TransactionInfo(
val summary: String,
@SerialName("summary_i18n")
val summaryI18n: Map<String, String>? = null,
- val products: List<ContractProduct>,
+ val products: List<ContractProduct> = emptyList(),
val fulfillmentUrl: String? = null,
/**
* Message shown to the user after the payment is complete.
@@ -164,35 +311,17 @@ class TransactionInfo(
)
@Serializable
-enum class PaymentStatus {
- @SerialName("aborted")
- Aborted,
-
- @SerialName("failed")
- Failed,
-
- @SerialName("paid")
- Paid,
-
- @SerialName("accepted")
- Accepted
-}
-
-@Serializable
@SerialName("refund")
class TransactionRefund(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val refundedTransactionId: String,
- val info: TransactionInfo,
- /**
- * Part of the refund that couldn't be applied because the refund permissions were expired
- */
- val amountInvalid: Amount? = null,
+ val paymentInfo: RefundPaymentInfo? = null,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
- override val amountEffective: Amount
+ override val amountEffective: Amount,
) : Transaction() {
override val icon = R.drawable.transaction_refund
override val detailPageNav = R.id.action_nav_transactions_detail_refund
@@ -200,56 +329,199 @@ class TransactionRefund(
@Transient
override val amountType = AmountType.Positive
override fun getTitle(context: Context): String {
- return context.getString(R.string.transaction_refund_from, info.merchant.name)
+ val merchantName = paymentInfo?.merchant?.name ?: "null"
+ return context.getString(R.string.transaction_refund_from, merchantName)
}
override val generalTitleRes = R.string.refund_title
}
@Serializable
-@SerialName("tip")
-class TransactionTip(
+@SerialName("refresh")
+class TransactionRefresh(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+) : Transaction() {
+ override val icon = R.drawable.transaction_refresh
+ override val detailPageNav = R.id.action_nav_transactions_detail_refresh
+
+ @Transient
+ override val amountType = AmountType.Negative
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_refresh)
+ }
+
+ override val generalTitleRes = R.string.transaction_refresh
+}
+
+@Serializable
+@SerialName("deposit")
+class TransactionDeposit(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
- // TODO status: TipStatus,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ val targetPaytoUri: String,
+ val depositGroupId: String,
+) : Transaction() {
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val detailPageNav = R.id.action_nav_transactions_detail_deposit
+
+ @Transient
+ override val amountType = AmountType.Negative
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_deposit)
+ }
+
+ override val generalTitleRes = R.string.transaction_deposit
+}
+
+@Serializable
+data class PeerInfoShort(
+ val expiration: Timestamp? = null,
+ val summary: String? = null,
+)
+
+/**
+ * Debit because we paid someone's invoice.
+ */
+@Serializable
+@SerialName("peer-pull-debit")
+class TransactionPeerPullDebit(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val exchangeBaseUrl: String,
- val merchant: ContractMerchant,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
- override val amountEffective: Amount
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
) : Transaction() {
- override val icon = R.drawable.transaction_tip_accepted // TODO different when declined
- override val detailPageNav = 0
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val detailPageNav = R.id.nav_transactions_detail_peer
@Transient
- override val amountType = AmountType.Positive
+ override val amountType = AmountType.Negative
override fun getTitle(context: Context): String {
- return context.getString(R.string.transaction_tip_from, merchant.name)
+ return context.getString(R.string.transaction_peer_pull_debit)
}
- override val generalTitleRes = R.string.tip_title
+ override val generalTitleRes = R.string.transaction_peer_pull_debit
}
+/**
+ * Credit because someone paid for an invoice we created.
+ */
@Serializable
-@SerialName("refresh")
-class TransactionRefresh(
+@SerialName("peer-pull-credit")
+class TransactionPeerPullCredit(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val exchangeBaseUrl: String,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
- override val amountEffective: Amount
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
+ val talerUri: String,
+ // val completed: Boolean, maybe
) : Transaction() {
- override val icon = R.drawable.transaction_refresh
- override val detailPageNav = R.id.action_nav_transactions_detail_refresh
+ override val icon = R.drawable.transaction_withdrawal
+ override val detailPageNav = R.id.nav_transactions_detail_peer
+
+ override val amountType get() = AmountType.Positive
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_peer_pull_credit)
+ }
+
+ override val generalTitleRes = R.string.transaction_peer_pull_credit
+}
+
+/**
+ * Debit because we sent money to someone.
+ */
+@Serializable
+@SerialName("peer-push-debit")
+class TransactionPeerPushDebit(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
+ val exchangeBaseUrl: String,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
+ val talerUri: String? = null,
+ // val completed: Boolean, definitely
+) : Transaction() {
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val detailPageNav = R.id.nav_transactions_detail_peer
@Transient
override val amountType = AmountType.Negative
override fun getTitle(context: Context): String {
- return context.getString(R.string.transaction_refresh)
+ return context.getString(R.string.transaction_peer_push_debit)
}
- override val generalTitleRes = R.string.transaction_refresh
+ override val generalTitleRes = R.string.payment_title
+}
+
+/**
+ * We received money via a peer payment.
+ */
+@Serializable
+@SerialName("peer-push-credit")
+class TransactionPeerPushCredit(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
+ val exchangeBaseUrl: String,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
+) : Transaction() {
+ override val icon = R.drawable.transaction_withdrawal
+ override val detailPageNav = R.id.nav_transactions_detail_peer
+
+ @Transient
+ override val amountType = AmountType.Positive
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_peer_push_credit)
+ }
+
+ override val generalTitleRes = R.string.transaction_peer_push_credit
+}
+
+/**
+ * This represents a transaction that we can not parse for some reason.
+ */
+class DummyTransaction(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val error: TalerErrorInfo,
+) : Transaction() {
+ override val txState: TransactionState = TransactionState(None)
+ override val txActions: List<TransactionAction> = emptyList()
+ override val amountRaw: Amount = Amount.zero("TESTKUDOS")
+ override val amountEffective: Amount = Amount.zero("TESTKUDOS")
+ override val icon: Int = R.drawable.ic_bug_report
+ override val detailPageNav: Int = R.id.nav_transactions_detail_dummy
+ override val amountType: AmountType = AmountType.Neutral
+ override val generalTitleRes: Int = R.string.transaction_dummy_title
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_dummy_title)
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
index 90510e6..5243427 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
@@ -17,15 +17,15 @@
package net.taler.wallet.transactions
import android.os.Bundle
+import android.util.Log
import android.view.ActionMode
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
+import android.view.View.INVISIBLE
import android.view.ViewGroup
-import android.widget.Toast
-import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.fragment.app.Fragment
@@ -36,11 +36,16 @@ import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.selection.StorageStrategy
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import net.taler.common.fadeIn
import net.taler.common.fadeOut
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
+import net.taler.wallet.TAG
+import net.taler.wallet.balances.BalanceState.Success
import net.taler.wallet.databinding.FragmentTransactionsBinding
+import net.taler.wallet.showError
interface OnTransactionClickListener {
fun onTransactionClicked(transaction: Transaction)
@@ -50,10 +55,11 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
private val model: MainViewModel by activityViewModels()
private val transactionManager by lazy { model.transactionManager }
+ private val balanceManager by lazy { model.balanceManager }
private lateinit var ui: FragmentTransactionsBinding
private val transactionAdapter by lazy { TransactionAdapter(this) }
- private val currency by lazy { transactionManager.selectedCurrency!! }
+ private val scopeInfo by lazy { transactionManager.selectedScope!! }
private var tracker: SelectionTracker<String>? = null
private var actionMode: ActionMode? = null
@@ -64,8 +70,8 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
+ savedInstanceState: Bundle?,
+ ): View {
ui = FragmentTransactionsBinding.inflate(inflater, container, false)
return ui.root
}
@@ -102,22 +108,36 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
}
})
- transactionManager.progress.observe(viewLifecycleOwner, { show ->
- if (show) ui.progressBar.fadeIn() else ui.progressBar.fadeOut()
- })
- transactionManager.transactions.observe(viewLifecycleOwner, { result ->
- onTransactionsResult(result)
- })
- }
+ balanceManager.state.observe(viewLifecycleOwner) { state ->
+ if (state !is Success) return@observe
+ val balances = state.balances
+ // hide extra fab when in single currency mode (uses MainFragment's FAB)
+ if (balances.size == 1) ui.mainFab.visibility = INVISIBLE
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- model.balances.observe(viewLifecycleOwner, { balances ->
- balances.find { it.currency == currency }?.available?.let { amount ->
- requireActivity().title =
- getString(R.string.transactions_detail_title_balance, amount)
+ balances.find { it.scopeInfo == scopeInfo }?.let { balance ->
+ ui.amount.text = balance.available.toString(showSymbol = false)
+ transactionAdapter.setCurrencySpec(balance.available.spec)
}
- })
+ }
+ transactionManager.progress.observe(viewLifecycleOwner) { show ->
+ if (show) ui.progressBar.fadeIn() else ui.progressBar.fadeOut()
+ }
+ transactionManager.transactions.observe(viewLifecycleOwner) { result ->
+ onTransactionsResult(result)
+ }
+ ui.sendButton.setOnClickListener {
+ findNavController().navigate(R.id.sendFunds)
+ }
+ ui.receiveButton.setOnClickListener {
+ findNavController().navigate(R.id.action_global_receiveFunds)
+ }
+ ui.mainFab.setOnClickListener {
+ model.scanCode()
+ }
+ ui.mainFab.setOnLongClickListener {
+ findNavController().navigate(R.id.action_nav_transactions_to_nav_uri_input)
+ true
+ }
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -125,11 +145,17 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
tracker?.onSaveInstanceState(outState)
}
+ @Deprecated("Deprecated in Java")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.transactions, menu)
setupSearch(menu.findItem(R.id.action_search))
}
+ override fun onStart() {
+ super.onStart()
+ requireActivity().title = getString(R.string.transactions_detail_title_currency, scopeInfo.currency)
+ }
+
private fun setupSearch(item: MenuItem) {
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem) = true
@@ -145,7 +171,7 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
// workaround to avoid issues with some emulators and keyboard devices
// firing twice if a keyboard enter is used
// see https://code.google.com/p/android/issues/detail?id=24599
- item.actionView.clearFocus()
+ searchView.clearFocus()
onSearch(query)
return true
}
@@ -155,7 +181,7 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
override fun onTransactionClicked(transaction: Transaction) {
if (actionMode != null) return // don't react on clicks while in action mode
if (transaction.detailPageNav != 0) {
- transactionManager.selectedTransaction = transaction
+ transactionManager.selectTransaction(transaction)
findNavController().navigate(transaction.detailPageNav)
}
}
@@ -163,9 +189,10 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
private fun onTransactionsResult(result: TransactionsResult) = when (result) {
is TransactionsResult.Error -> {
ui.list.fadeOut()
- ui.emptyState.text = getString(R.string.transactions_error, result.msg)
+ ui.emptyState.text = getString(R.string.transactions_error, result.error.userFacingMsg)
ui.emptyState.fadeIn()
}
+
is TransactionsResult.Success -> {
if (result.transactions.isEmpty()) {
val isSearch = transactionManager.searchQuery.value != null
@@ -203,10 +230,32 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
when (item.itemId) {
R.id.transaction_delete -> {
- val s = "Not yet implemented. Pester Florian! ;)"
- Toast.makeText(requireContext(), s, LENGTH_LONG).show()
+ tracker?.selection?.toList()?.let { transactionIds ->
+ MaterialAlertDialogBuilder(
+ requireContext(),
+ R.style.MaterialAlertDialog_Material3,
+ )
+ .setTitle(R.string.transactions_delete)
+ .setMessage(R.string.transactions_delete_selected_dialog_message)
+ .setNeutralButton(R.string.cancel) { dialog, _ ->
+ dialog.cancel()
+ }
+ .setNegativeButton(R.string.transactions_delete) { dialog, _ ->
+ transactionManager.deleteTransactions(transactionIds) {
+ Log.e(TAG, "Error deleteTransaction $it")
+ if (model.devMode.value == true) {
+ showError(it)
+ } else {
+ showError(it.userFacingMsg)
+ }
+ }
+ dialog.dismiss()
+ }
+ .show()
+ }
mode.finish()
}
+
R.id.transaction_select_all -> transactionAdapter.selectAll()
}
return true
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt
new file mode 100644
index 0000000..424cc2a
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt
@@ -0,0 +1,113 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.transactions
+
+import androidx.compose.foundation.layout.Arrangement.Center
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import net.taler.wallet.R
+import net.taler.wallet.transactions.TransactionAction.*
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun TransitionsComposable(
+ t: Transaction,
+ devMode: Boolean,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ FlowRow(horizontalArrangement = Center) {
+ t.txActions.forEach {
+ if (it in arrayOf(Resume, Suspend)) {
+ if (devMode) TransitionComposable(it, onTransition)
+ } else {
+ TransitionComposable(it, onTransition)
+ }
+ }
+ }
+}
+
+@Composable
+fun TransitionComposable(t: TransactionAction, onClick: (t: TransactionAction) -> Unit) {
+ Button(
+ modifier = Modifier.padding(16.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = when (t) {
+ Delete -> MaterialTheme.colorScheme.error
+ Retry -> MaterialTheme.colorScheme.primary
+ Abort -> MaterialTheme.colorScheme.error
+ Fail -> MaterialTheme.colorScheme.error
+ Resume -> MaterialTheme.colorScheme.primary
+ Suspend -> MaterialTheme.colorScheme.primary
+ }
+ ),
+ onClick = { onClick(t) },
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ painter = when (t) {
+ Delete -> painterResource(id = R.drawable.ic_delete)
+ Retry -> painterResource(id = R.drawable.ic_retry)
+ Abort -> painterResource(id = R.drawable.ic_cancel)
+ Fail -> painterResource(id = R.drawable.ic_fail)
+ Resume -> painterResource(id = R.drawable.ic_resume)
+ Suspend -> painterResource(id = R.drawable.ic_suspend)
+ },
+ contentDescription = null,
+ tint = when (t) {
+ Delete -> MaterialTheme.colorScheme.onError
+ Retry -> MaterialTheme.colorScheme.onPrimary
+ Abort -> MaterialTheme.colorScheme.onError
+ Fail -> MaterialTheme.colorScheme.onError
+ Resume -> MaterialTheme.colorScheme.onPrimary
+ Suspend -> MaterialTheme.colorScheme.onPrimary
+ },
+ )
+ Text(
+ modifier = Modifier.padding(start = 8.dp),
+ text = when (t) {
+ Delete -> stringResource(R.string.transactions_delete)
+ Retry -> stringResource(R.string.transactions_retry)
+ Abort -> stringResource(R.string.transactions_abort)
+ Fail -> stringResource(R.string.transactions_fail)
+ Resume -> stringResource(R.string.transactions_resume)
+ Suspend -> stringResource(R.string.transactions_suspend)
+ },
+ color = when (t) {
+ Delete -> MaterialTheme.colorScheme.onError
+ Retry -> MaterialTheme.colorScheme.onPrimary
+ Abort -> MaterialTheme.colorScheme.onError
+ Fail -> MaterialTheme.colorScheme.onError
+ Resume -> MaterialTheme.colorScheme.onPrimary
+ Suspend -> MaterialTheme.colorScheme.onPrimary
+ },
+ )
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
index 38e09fa..56f56f7 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
@@ -20,28 +20,37 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
+import kotlinx.coroutines.launch
+import net.taler.common.Amount
import net.taler.common.EventObserver
import net.taler.common.fadeIn
import net.taler.common.fadeOut
-import net.taler.lib.common.Amount
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.cleanExchange
import net.taler.wallet.databinding.FragmentPromptWithdrawBinding
+import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.exchanges.SelectExchangeDialogFragment
import net.taler.wallet.withdraw.WithdrawStatus.Loading
import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails
import net.taler.wallet.withdraw.WithdrawStatus.TosReviewRequired
import net.taler.wallet.withdraw.WithdrawStatus.Withdrawing
+import net.taler.wallet.withdraw.WithdrawStatus.NeedsExchange
class PromptWithdrawFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
private val withdrawManager by lazy { model.withdrawManager }
+ private val transactionManager by lazy { model.transactionManager }
+
+ private val selectExchangeDialog = SelectExchangeDialogFragment()
private lateinit var ui: FragmentPromptWithdrawBinding
@@ -49,7 +58,7 @@ class PromptWithdrawFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
- ): View? {
+ ): View {
ui = FragmentPromptWithdrawBinding.inflate(inflater, container, false)
return ui.root
}
@@ -57,35 +66,43 @@ class PromptWithdrawFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- withdrawManager.withdrawStatus.observe(viewLifecycleOwner, {
+ withdrawManager.withdrawStatus.observe(viewLifecycleOwner) {
showWithdrawStatus(it)
- })
- withdrawManager.exchangeSelection.observe(viewLifecycleOwner, EventObserver {
- findNavController().navigate(R.id.action_promptWithdraw_to_selectExchangeFragment)
+ }
+
+ selectExchangeDialog.exchangeSelection.observe(viewLifecycleOwner, EventObserver {
+ onExchangeSelected(it)
})
}
private fun showWithdrawStatus(status: WithdrawStatus?): Any = when (status) {
null -> model.showProgressBar.value = false
is Loading -> model.showProgressBar.value = true
- is WithdrawStatus.NeedsExchange -> {
+ is NeedsExchange -> {
model.showProgressBar.value = false
- val exchangeSelection = status.exchangeSelection.getIfNotConsumed()
- if (exchangeSelection == null) { // already consumed
- findNavController().popBackStack()
- } else {
- withdrawManager.selectExchange(exchangeSelection)
- }
+ if (selectExchangeDialog.dialog?.isShowing != true) {
+ selectExchange()
+ } else {}
}
is TosReviewRequired -> onTosReviewRequired(status)
is ReceivedDetails -> onReceivedDetails(status)
is Withdrawing -> model.showProgressBar.value = true
+ is WithdrawStatus.ManualTransferRequired -> {
+ model.showProgressBar.value = false
+ findNavController().navigate(R.id.action_promptWithdraw_to_nav_exchange_manual_withdrawal_success)
+ }
is WithdrawStatus.Success -> {
model.showProgressBar.value = false
withdrawManager.withdrawStatus.value = null
- findNavController().navigate(R.id.action_promptWithdraw_to_nav_main)
- model.showTransactions(status.currency)
- Snackbar.make(requireView(), R.string.withdraw_initiated, LENGTH_LONG).show()
+ lifecycleScope.launch {
+ // now select new transaction based on currency and ID
+ if (transactionManager.selectTransaction(status.transactionId)) {
+ findNavController().navigate(R.id.action_promptWithdraw_to_nav_transactions_detail_withdrawal)
+ } else {
+ findNavController().navigate(R.id.action_promptWithdraw_to_nav_main)
+ }
+ Snackbar.make(requireView(), R.string.withdraw_initiated, LENGTH_LONG).show()
+ }
}
is WithdrawStatus.Error -> {
model.showProgressBar.value = false
@@ -98,7 +115,13 @@ class PromptWithdrawFragment : Fragment() {
if (s.showImmediately.getIfNotConsumed() == true) {
findNavController().navigate(R.id.action_promptWithdraw_to_reviewExchangeTOS)
} else {
- showContent(s.amountRaw, s.amountEffective, s.exchangeBaseUrl, s.talerWithdrawUri)
+ showContent(
+ amountRaw = s.amountRaw,
+ amountEffective = s.amountEffective,
+ exchange = s.exchangeBaseUrl,
+ uri = s.talerWithdrawUri,
+ exchanges = s.possibleExchanges,
+ )
ui.confirmWithdrawButton.apply {
text = getString(R.string.withdraw_button_tos)
setOnClickListener {
@@ -110,13 +133,24 @@ class PromptWithdrawFragment : Fragment() {
}
private fun onReceivedDetails(s: ReceivedDetails) {
- showContent(s.amountRaw, s.amountEffective, s.exchangeBaseUrl, s.talerWithdrawUri)
+ showContent(
+ amountRaw = s.amountRaw,
+ amountEffective = s.amountEffective,
+ exchange = s.exchangeBaseUrl,
+ uri = s.talerWithdrawUri,
+ ageRestrictionOptions = s.ageRestrictionOptions,
+ exchanges = s.possibleExchanges,
+ )
ui.confirmWithdrawButton.apply {
text = getString(R.string.withdraw_button_confirm)
setOnClickListener {
it.fadeOut()
ui.confirmProgressBar.fadeIn()
- withdrawManager.acceptWithdrawal()
+ val ageRestrict = (ui.ageSelector.selectedItem as String?)?.let { age ->
+ if (age == context.getString(R.string.withdraw_restrict_age_unrestricted)) null
+ else age.toIntOrNull()
+ }
+ withdrawManager.acceptWithdrawal(ageRestrict)
}
isEnabled = true
}
@@ -127,6 +161,8 @@ class PromptWithdrawFragment : Fragment() {
amountEffective: Amount,
exchange: String,
uri: String?,
+ exchanges: List<ExchangeItem> = emptyList(),
+ ageRestrictionOptions: List<Int>? = null,
) {
model.showProgressBar.value = false
ui.progressBar.fadeOut()
@@ -139,24 +175,75 @@ class PromptWithdrawFragment : Fragment() {
ui.chosenAmountView.text = amountRaw.toString()
ui.chosenAmountView.fadeIn()
- ui.feeLabel.fadeIn()
- ui.feeView.text =
- getString(R.string.amount_negative, (amountRaw - amountEffective).toString())
- ui.feeView.fadeIn()
+ val fee = amountRaw - amountEffective
+ if (!fee.isZero()) {
+ ui.feeLabel.fadeIn()
+ ui.feeView.text = getString(R.string.amount_negative, fee.toString())
+ ui.feeView.fadeIn()
+ }
ui.exchangeIntroView.fadeIn()
ui.withdrawExchangeUrl.text = cleanExchange(exchange)
ui.withdrawExchangeUrl.fadeIn()
- if (uri != null) { // no Uri for manual withdrawals
+ // no Uri for manual withdrawals, no selection for single exchange
+ if (uri != null && exchanges.size > 1) {
ui.selectExchangeButton.fadeIn()
ui.selectExchangeButton.setOnClickListener {
- val exchangeSelection = ExchangeSelection(amountRaw, uri)
- withdrawManager.selectExchange(exchangeSelection)
+ selectExchange()
}
}
+ if (ageRestrictionOptions != null) {
+ ui.ageLabel.fadeIn()
+ val context = requireContext()
+ val items = listOf(context.getString(R.string.withdraw_restrict_age_unrestricted)) +
+ ageRestrictionOptions.map { it.toString() }
+ ui.ageSelector.adapter = ArrayAdapter(context, R.layout.list_item_age, items)
+ ui.ageSelector.fadeIn()
+ }
+
ui.withdrawCard.fadeIn()
}
+ private fun selectExchange() {
+ val exchanges = when (val status = withdrawManager.withdrawStatus.value) {
+ is ReceivedDetails -> status.possibleExchanges
+ is NeedsExchange -> status.possibleExchanges
+ is TosReviewRequired -> status.possibleExchanges
+ else -> return
+ }
+ selectExchangeDialog.setExchanges(exchanges)
+ selectExchangeDialog.show(parentFragmentManager, "SELECT_EXCHANGE")
+ }
+
+ private fun onExchangeSelected(exchange: ExchangeItem) {
+ val status = withdrawManager.withdrawStatus.value
+ val amount = when (status) {
+ is ReceivedDetails -> status.amountRaw
+ is NeedsExchange -> status.amount
+ is TosReviewRequired -> status.amountRaw
+ else -> return
+ }
+ val uri = when (status) {
+ is ReceivedDetails -> status.talerWithdrawUri
+ is NeedsExchange -> status.talerWithdrawUri
+ is TosReviewRequired -> status.talerWithdrawUri
+ else -> return
+ }
+ val exchanges = when (status) {
+ is ReceivedDetails -> status.possibleExchanges
+ is NeedsExchange -> status.possibleExchanges
+ is TosReviewRequired -> status.possibleExchanges
+ else -> return
+ }
+
+ withdrawManager.getWithdrawalDetails(
+ exchangeBaseUrl = exchange.exchangeBaseUrl,
+ amount = amount,
+ showTosImmediately = false,
+ uri = uri,
+ possibleExchanges = exchanges,
+ )
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt
index 73fe760..7bf2e29 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt
@@ -22,7 +22,6 @@ 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 io.noties.markwon.Markwon
import net.taler.common.fadeIn
@@ -44,8 +43,8 @@ class ReviewExchangeTosFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
+ savedInstanceState: Bundle?,
+ ): View {
ui = FragmentReviewExchangeTosBinding.inflate(inflater, container, false)
return ui.root
}
@@ -56,7 +55,7 @@ class ReviewExchangeTosFragment : Fragment() {
ui.acceptTosCheckBox.setOnCheckedChangeListener { _, _ ->
withdrawManager.acceptCurrentTermsOfService()
}
- withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer {
+ withdrawManager.withdrawStatus.observe(viewLifecycleOwner) {
when (it) {
is WithdrawStatus.TosReviewRequired -> {
val sections = try {
@@ -65,7 +64,7 @@ class ReviewExchangeTosFragment : Fragment() {
parseTos(markwon, text)
} catch (e: ParseException) {
onTosError(e.message ?: "Unknown Error")
- return@Observer
+ return@observe
}
adapter.setSections(sections)
ui.tosList.adapter = adapter
@@ -80,14 +79,16 @@ class ReviewExchangeTosFragment : Fragment() {
is WithdrawStatus.ReceivedDetails -> {
findNavController().navigate(R.id.action_reviewExchangeTOS_to_promptWithdraw)
}
+ else -> {}
}
- })
+ }
}
private fun onTosError(msg: String) {
ui.tosList.fadeIn()
ui.progressBar.fadeOut()
- ui.buttonCard.fadeOut()
+ ui.acceptTosCheckBox.fadeIn()
+ // ui.buttonCard.fadeOut()
ui.errorView.text = getString(R.string.exchange_tos_error, "\n\n$msg")
ui.errorView.fadeIn()
}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt b/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt
index b198478..64dff8a 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt
@@ -16,6 +16,7 @@
package net.taler.wallet.withdraw
+import android.util.Log
import io.noties.markwon.Markwon
import kotlinx.serialization.Serializable
import org.commonmark.node.Code
@@ -50,7 +51,7 @@ internal fun parseTos(markwon: Markwon, text: String): List<TosSection> {
lastHeading = getNodeText(node)
if (lastHeading.isBlank()) throw ParseException("Empty heading", 0)
} else if (lastHeading == null) {
- throw ParseException("Found text before first primary heading", 0)
+ throw ParseException("The exchange ToS does not follow the correct format", 0)
} else {
section.appendChild(node)
}
@@ -77,6 +78,6 @@ private fun getNodeText(rootNode: Node): String {
@Serializable
data class TosResponse(
- val tos: String,
+ val content: String,
val currentEtag: String
)
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
new file mode 100644
index 0000000..5155b5b
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
@@ -0,0 +1,154 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.Timestamp
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.cleanExchange
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.transactions.ActionButton
+import net.taler.wallet.transactions.ActionListener
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.ErrorTransactionButton
+import net.taler.wallet.transactions.Transaction
+import net.taler.wallet.transactions.TransactionAction
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionState
+import net.taler.wallet.transactions.TransactionWithdrawal
+import net.taler.wallet.transactions.TransitionsComposable
+import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
+
+@Composable
+fun TransactionWithdrawalComposable(
+ t: TransactionWithdrawal,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ actionListener: ActionListener,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+
+ ActionButton(tx = t, listener = actionListener)
+
+ TransactionAmountComposable(
+ label = stringResource(R.string.amount_chosen),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+
+ val fee = t.amountRaw - t.amountEffective
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_total),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Positive,
+ )
+
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.withdraw_exchange),
+ info = cleanExchange(t.exchangeBaseUrl),
+ )
+
+ TransitionsComposable(t, devMode, onTransition)
+
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
+ }
+}
+
+@Preview
+@Composable
+fun TransactionWithdrawalComposablePreview() {
+ val t = TransactionWithdrawal(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ exchangeBaseUrl = "https://exchange.demo.taler.net/",
+ withdrawalDetails = ManualTransfer(
+ exchangeCreditAccountDetails = listOf(
+ WithdrawalExchangeAccountDetails(
+ paytoUri = "payto://IBAN/1231231231",
+ transferAmount = Amount.fromJSONString("NETZBON:42.23"),
+ currencySpecification = CurrencySpecification(
+ name = "NETZBON",
+ numFractionalInputDigits = 2,
+ numFractionalNormalDigits = 2,
+ numFractionalTrailingZeroDigits = 2,
+ altUnitNames = mapOf(0 to "NETZBON"),
+ ),
+ ),
+ ),
+ ),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ )
+
+ val listener = object : ActionListener {
+ override fun onActionButtonClicked(tx: Transaction, type: ActionListener.Type) {}
+ }
+
+ Surface {
+ TransactionWithdrawalComposable(t, true, null, listener) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
index 5afb125..e308b2a 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
@@ -16,35 +16,47 @@
package net.taler.wallet.withdraw
+import android.net.Uri
import android.util.Log
import androidx.annotation.UiThread
-import androidx.lifecycle.LiveData
+import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+import net.taler.common.Bech32
import net.taler.common.Event
import net.taler.common.toEvent
-import net.taler.lib.common.Amount
import net.taler.wallet.TAG
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.exchanges.ExchangeFees
import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails
sealed class WithdrawStatus {
data class Loading(val talerWithdrawUri: String? = null) : WithdrawStatus()
- data class NeedsExchange(val exchangeSelection: Event<ExchangeSelection>) : WithdrawStatus()
+
+ data class NeedsExchange(
+ val talerWithdrawUri: String,
+ val amount: Amount,
+ val possibleExchanges: List<ExchangeItem>,
+ ) : WithdrawStatus()
data class TosReviewRequired(
val talerWithdrawUri: String? = null,
val exchangeBaseUrl: String,
val amountRaw: Amount,
val amountEffective: Amount,
+ val withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
+ val ageRestrictionOptions: List<Int>? = null,
val tosText: String,
val tosEtag: String,
val showImmediately: Event<Boolean>,
+ val possibleExchanges: List<ExchangeItem> = emptyList(),
) : WithdrawStatus()
data class ReceivedDetails(
@@ -52,13 +64,62 @@ sealed class WithdrawStatus {
val exchangeBaseUrl: String,
val amountRaw: Amount,
val amountEffective: Amount,
+ val withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
+ val ageRestrictionOptions: List<Int>? = null,
+ val possibleExchanges: List<ExchangeItem> = emptyList(),
+ ) : WithdrawStatus()
+
+ data object Withdrawing : WithdrawStatus()
+
+ data class Success(val currency: String, val transactionId: String) : WithdrawStatus()
+
+ class ManualTransferRequired(
+ val transactionId: String?,
+ val transactionAmountRaw: Amount,
+ val transactionAmountEffective: Amount,
+ val exchangeBaseUrl: String,
+ val withdrawalTransfers: List<TransferData>,
) : WithdrawStatus()
- object Withdrawing : WithdrawStatus()
- data class Success(val currency: String) : WithdrawStatus()
data class Error(val message: String?) : WithdrawStatus()
}
+sealed class TransferData {
+ abstract val subject: String
+ abstract val amountRaw: Amount
+ abstract val amountEffective: Amount
+ abstract val withdrawalAccount: WithdrawalExchangeAccountDetails
+
+ val currency get() = withdrawalAccount.transferAmount?.currency
+
+ data class Taler(
+ override val subject: String,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ override val withdrawalAccount: WithdrawalExchangeAccountDetails,
+ val receiverName: String? = null,
+ val account: String,
+ ): TransferData()
+
+ data class IBAN(
+ override val subject: String,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ override val withdrawalAccount: WithdrawalExchangeAccountDetails,
+ val receiverName: String? = null,
+ val iban: String,
+ ): TransferData()
+
+ data class Bitcoin(
+ override val subject: String,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ override val withdrawalAccount: WithdrawalExchangeAccountDetails,
+ val account: String,
+ val segwitAddresses: List<String>,
+ ): TransferData()
+}
+
sealed class WithdrawTestStatus {
object Withdrawing : WithdrawTestStatus()
object Success : WithdrawTestStatus()
@@ -73,15 +134,31 @@ data class WithdrawalDetailsForUri(
)
@Serializable
-data class WithdrawalDetails(
+data class WithdrawExchangeResponse(
+ val exchangeBaseUrl: String,
+ val amount: Amount? = null,
+)
+
+@Serializable
+data class ManualWithdrawalDetails(
val tosAccepted: Boolean,
val amountRaw: Amount,
val amountEffective: Amount,
+ val withdrawalAccountsList: List<WithdrawalExchangeAccountDetails>,
+ val ageRestrictionOptions: List<Int>? = null,
+ val scopeInfo: ScopeInfo,
)
-data class ExchangeSelection(
- val amount: Amount,
- val talerWithdrawUri: String,
+@Serializable
+data class AcceptWithdrawalResponse(
+ val transactionId: String,
+)
+
+@Serializable
+data class AcceptManualWithdrawalResponse(
+ val reservePub: String,
+ val withdrawalAccountsList: List<WithdrawalExchangeAccountDetails>,
+ val transactionId: String,
)
class WithdrawManager(
@@ -92,8 +169,6 @@ class WithdrawManager(
val withdrawStatus = MutableLiveData<WithdrawStatus>()
val testWithdrawalStatus = MutableLiveData<WithdrawTestStatus>()
- private val _exchangeSelection = MutableLiveData<Event<ExchangeSelection>>()
- val exchangeSelection: LiveData<Event<ExchangeSelection>> = _exchangeSelection
var exchangeFees: ExchangeFees? = null
private set
@@ -106,11 +181,6 @@ class WithdrawManager(
}
}
- @UiThread
- fun selectExchange(selection: ExchangeSelection) {
- _exchangeSelection.value = selection.toEvent()
- }
-
fun getWithdrawalDetails(uri: String) = scope.launch {
withdrawStatus.value = WithdrawStatus.Loading(uri)
api.request("getWithdrawalDetailsForUri", WithdrawalDetailsForUri.serializer()) {
@@ -119,11 +189,18 @@ class WithdrawManager(
handleError("getWithdrawalDetailsForUri", error)
}.onSuccess { details ->
if (details.defaultExchangeBaseUrl == null) {
- val exchangeSelection = ExchangeSelection(details.amount, uri)
- withdrawStatus.value = WithdrawStatus.NeedsExchange(exchangeSelection.toEvent())
- } else {
- getWithdrawalDetails(details.defaultExchangeBaseUrl, details.amount, false, uri)
- }
+ withdrawStatus.value = WithdrawStatus.NeedsExchange(
+ talerWithdrawUri = uri,
+ amount = details.amount,
+ possibleExchanges = details.possibleExchanges,
+ )
+ } else getWithdrawalDetails(
+ exchangeBaseUrl = details.defaultExchangeBaseUrl,
+ amount = details.amount,
+ showTosImmediately = false,
+ uri = uri,
+ possibleExchanges = details.possibleExchanges,
+ )
}
}
@@ -132,9 +209,10 @@ class WithdrawManager(
amount: Amount,
showTosImmediately: Boolean = false,
uri: String? = null,
+ possibleExchanges: List<ExchangeItem> = emptyList(),
) = scope.launch {
withdrawStatus.value = WithdrawStatus.Loading(uri)
- api.request("getWithdrawalDetailsForAmount", WithdrawalDetails.serializer()) {
+ api.request("getWithdrawalDetailsForAmount", ManualWithdrawalDetails.serializer()) {
put("exchangeBaseUrl", exchangeBaseUrl)
put("amount", amount.toJSONString())
}.onError { error ->
@@ -146,16 +224,34 @@ class WithdrawManager(
exchangeBaseUrl = exchangeBaseUrl,
amountRaw = details.amountRaw,
amountEffective = details.amountEffective,
+ withdrawalAccountList = details.withdrawalAccountsList,
+ ageRestrictionOptions = details.ageRestrictionOptions,
+ possibleExchanges = possibleExchanges,
)
- } else getExchangeTos(exchangeBaseUrl, details, showTosImmediately, uri)
+ } else getExchangeTos(exchangeBaseUrl, details, showTosImmediately, uri, possibleExchanges)
}
}
+ @WorkerThread
+ suspend fun prepareManualWithdrawal(uri: String): WithdrawExchangeResponse? {
+ withdrawStatus.postValue(WithdrawStatus.Loading(uri))
+ var response: WithdrawExchangeResponse? = null
+ api.request("prepareWithdrawExchange", WithdrawExchangeResponse.serializer()) {
+ put("talerUri", uri)
+ }.onError {
+ handleError("prepareWithdrawExchange", it)
+ }.onSuccess {
+ response = it
+ }
+ return response
+ }
+
private fun getExchangeTos(
exchangeBaseUrl: String,
- details: WithdrawalDetails,
+ details: ManualWithdrawalDetails,
showImmediately: Boolean,
uri: String?,
+ possibleExchanges: List<ExchangeItem>,
) = scope.launch {
api.request("getExchangeTos", TosResponse.serializer()) {
put("exchangeBaseUrl", exchangeBaseUrl)
@@ -167,9 +263,12 @@ class WithdrawManager(
exchangeBaseUrl = exchangeBaseUrl,
amountRaw = details.amountRaw,
amountEffective = details.amountEffective,
- tosText = it.tos,
+ withdrawalAccountList = details.withdrawalAccountsList,
+ ageRestrictionOptions = details.ageRestrictionOptions,
+ tosText = it.content,
tosEtag = it.currentEtag,
showImmediately = showImmediately.toEvent(),
+ possibleExchanges = possibleExchanges,
)
}
}
@@ -190,38 +289,126 @@ class WithdrawManager(
exchangeBaseUrl = s.exchangeBaseUrl,
amountRaw = s.amountRaw,
amountEffective = s.amountEffective,
+ withdrawalAccountList = s.withdrawalAccountList,
+ ageRestrictionOptions = s.ageRestrictionOptions,
+ possibleExchanges = s.possibleExchanges,
)
}
}
@UiThread
- fun acceptWithdrawal() = scope.launch {
+ fun acceptWithdrawal(restrictAge: Int? = null) = scope.launch {
val status = withdrawStatus.value as ReceivedDetails
- val operation = if (status.talerWithdrawUri == null) {
- "acceptManualWithdrawal"
+ withdrawStatus.value = WithdrawStatus.Withdrawing
+ if (status.talerWithdrawUri == null) {
+ acceptManualWithdrawal(status, restrictAge)
} else {
- "acceptBankIntegratedWithdrawal"
+ acceptBankIntegratedWithdrawal(status, restrictAge)
}
- withdrawStatus.value = WithdrawStatus.Withdrawing
+ }
- api.request<Unit>(operation) {
+ private suspend fun acceptBankIntegratedWithdrawal(
+ status: ReceivedDetails,
+ restrictAge: Int? = null,
+ ) {
+ api.request("acceptBankIntegratedWithdrawal", AcceptWithdrawalResponse.serializer()) {
+ restrictAge?.let { put("restrictAge", restrictAge) }
put("exchangeBaseUrl", status.exchangeBaseUrl)
- if (status.talerWithdrawUri == null) {
- put("amount", status.amountRaw.toJSONString())
- } else {
- put("talerWithdrawUri", status.talerWithdrawUri)
- }
+ put("talerWithdrawUri", status.talerWithdrawUri)
}.onError {
- handleError(operation, it)
+ handleError("acceptBankIntegratedWithdrawal", it)
}.onSuccess {
- withdrawStatus.value = WithdrawStatus.Success(status.amountRaw.currency)
+ withdrawStatus.value =
+ WithdrawStatus.Success(status.amountRaw.currency, it.transactionId)
+ }
+ }
+
+ private suspend fun acceptManualWithdrawal(status: ReceivedDetails, restrictAge: Int? = null) {
+ api.request("acceptManualWithdrawal", AcceptManualWithdrawalResponse.serializer()) {
+ restrictAge?.let { put("restrictAge", restrictAge) }
+ put("exchangeBaseUrl", status.exchangeBaseUrl)
+ put("amount", status.amountRaw.toJSONString())
+ }.onError {
+ handleError("acceptManualWithdrawal", it)
+ }.onSuccess { response ->
+ withdrawStatus.value = createManualTransferRequired(
+ status = status,
+ response = response,
+ )
}
}
- @UiThread
private fun handleError(operation: String, error: TalerErrorInfo) {
Log.e(TAG, "Error $operation $error")
- withdrawStatus.value = WithdrawStatus.Error(error.userFacingMsg)
+ withdrawStatus.postValue(WithdrawStatus.Error(error.userFacingMsg))
+ }
+
+ /**
+ * A hack to be able to view bank details for manual withdrawal with the same logic.
+ * Don't call this from ongoing withdrawal processes as it destroys state.
+ */
+ fun viewManualWithdrawal(status: WithdrawStatus.ManualTransferRequired) {
+ require(status.transactionId != null) { "No transaction ID given" }
+ withdrawStatus.value = status
}
}
+
+fun createManualTransferRequired(
+ transactionId: String,
+ exchangeBaseUrl: String,
+ amountRaw: Amount,
+ amountEffective: Amount,
+ withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
+) = WithdrawStatus.ManualTransferRequired(
+ transactionId = transactionId,
+ transactionAmountRaw = amountRaw,
+ transactionAmountEffective = amountEffective,
+ exchangeBaseUrl = exchangeBaseUrl,
+ withdrawalTransfers = withdrawalAccountList.mapNotNull {
+ val uri = Uri.parse(it.paytoUri.replace("receiver-name=", "receiver_name="))
+ if ("bitcoin".equals(uri.authority, true)) {
+ val msg = uri.getQueryParameter("message").orEmpty()
+ val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg)
+ val reserve = reg?.value ?: uri.getQueryParameter("subject")!!
+ val segwitAddresses = Bech32.generateFakeSegwitAddress(reserve, uri.pathSegments.first())
+ TransferData.Bitcoin(
+ account = uri.lastPathSegment!!,
+ segwitAddresses = segwitAddresses,
+ subject = reserve,
+ amountRaw = amountRaw,
+ amountEffective = amountEffective,
+ withdrawalAccount = it.copy(paytoUri = uri.toString())
+ )
+ } else if (uri.authority.equals("x-taler-bank", true)) {
+ TransferData.Taler(
+ account = uri.lastPathSegment!!,
+ receiverName = uri.getQueryParameter("receiver_name"),
+ subject = uri.getQueryParameter("message") ?: "Error: No message in URI",
+ amountRaw = amountRaw,
+ amountEffective = amountEffective,
+ withdrawalAccount = it.copy(paytoUri = uri.toString()),
+ )
+ } else if (uri.authority.equals("iban", true)) {
+ TransferData.IBAN(
+ iban = uri.lastPathSegment!!,
+ receiverName = uri.getQueryParameter("receiver_name"),
+ subject = uri.getQueryParameter("message") ?: "Error: No message in URI",
+ amountRaw = amountRaw,
+ amountEffective = amountEffective,
+ withdrawalAccount = it.copy(paytoUri = uri.toString()),
+ )
+ } else null
+ },
+)
+
+fun createManualTransferRequired(
+ status: ReceivedDetails,
+ response: AcceptManualWithdrawalResponse,
+): WithdrawStatus.ManualTransferRequired = createManualTransferRequired(
+ transactionId = response.transactionId,
+ exchangeBaseUrl = status.exchangeBaseUrl,
+ amountRaw = status.amountRaw,
+ amountEffective = status.amountEffective,
+ withdrawalAccountList = response.withdrawalAccountsList,
+) \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt
index 47c2c6b..c499c3b 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt
@@ -1,6 +1,6 @@
/*
* This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
+ * (C) 2022 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-package net.taler.wallet.withdraw
+package net.taler.wallet.withdraw.manual
import android.net.Uri
import android.os.Bundle
@@ -24,12 +24,12 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
+import net.taler.common.Amount
+import net.taler.common.AmountParserException
import net.taler.common.hideKeyboard
-import net.taler.lib.common.Amount
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.databinding.FragmentManualWithdrawBinding
-import net.taler.wallet.scanQrCode
import java.util.Locale
class ManualWithdrawFragment : Fragment() {
@@ -44,16 +44,23 @@ class ManualWithdrawFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
- ): View? {
+ ): View {
ui = FragmentManualWithdrawBinding.inflate(inflater, container, false)
return ui.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- ui.qrCodeButton.setOnClickListener { scanQrCode(requireActivity()) }
+ arguments?.getString("amount")?.let {
+ val amount = Amount.fromJSONString(it)
+ ui.amountView.setText(amount.amountStr)
+ }
+
+ ui.qrCodeButton.setOnClickListener {
+ model.scanCode()
+ }
ui.currencyView.text = exchangeItem.currency
val paymentOptions = exchangeItem.paytoUris.mapNotNull { paytoUri ->
- Uri.parse(paytoUri).authority?.toUpperCase(Locale.getDefault())
+ Uri.parse(paytoUri).authority?.uppercase(Locale.getDefault())
}.joinToString(separator = "\n", prefix = "• ")
ui.paymentOptionsLabel.text =
getString(R.string.withdraw_manual_payment_options, exchangeItem.name, paymentOptions)
@@ -61,13 +68,19 @@ class ManualWithdrawFragment : Fragment() {
}
private fun onCheckFees() {
- if (ui.amountView.text?.isEmpty() != false) {
+ val currency = exchangeItem.currency
+ if (currency == null || ui.amountView.text?.isEmpty() != false) {
ui.amountLayout.error = getString(R.string.withdraw_amount_error)
return
}
ui.amountLayout.error = null
- val value = ui.amountView.text.toString().toLong()
- val amount = Amount(exchangeItem.currency, value, 0)
+ val amount: Amount
+ try {
+ amount = Amount.fromString(currency, ui.amountView.text.toString())
+ } catch (e: AmountParserException) {
+ ui.amountLayout.error = getString(R.string.withdraw_amount_error)
+ return
+ }
ui.amountView.hideKeyboard()
withdrawManager.getWithdrawalDetails(exchangeItem.exchangeBaseUrl, amount)
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
new file mode 100644
index 0000000..63413c2
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
@@ -0,0 +1,95 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import net.taler.common.openUri
+import net.taler.common.shareText
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.withdraw.TransferData
+import net.taler.wallet.withdraw.WithdrawStatus
+
+class ManualWithdrawSuccessFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { model.withdrawManager }
+ private val balanceManager by lazy { model.balanceManager }
+
+ private lateinit var status: WithdrawStatus.ManualTransferRequired
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ status = withdrawManager.withdrawStatus.value as WithdrawStatus.ManualTransferRequired
+
+ // Set action bar subtitle and unset on exit
+ if (status.withdrawalTransfers.size > 1) {
+ val activity = requireActivity() as AppCompatActivity
+
+ activity.apply {
+ supportActionBar?.subtitle = getString(R.string.withdraw_subtitle)
+ }
+
+ findNavController().addOnDestinationChangedListener { controller, destination, args ->
+ if (destination.id != R.id.nav_exchange_manual_withdrawal_success) {
+ activity.apply {
+ supportActionBar?.subtitle = null
+ }
+ }
+ }
+ }
+
+ setContent {
+ TalerSurface {
+ ScreenTransfer(
+ status = status,
+ spec = balanceManager.getSpecForCurrency(status.transactionAmountRaw.currency),
+ bankAppClick = { onBankAppClick(it) },
+ shareClick = { onShareClick(it) },
+ )
+ }
+ }
+ }
+
+ private fun onBankAppClick(transfer: TransferData) {
+ requireContext().openUri(
+ uri = transfer.withdrawalAccount.paytoUri,
+ title = requireContext().getString(R.string.share_payment)
+ )
+ }
+
+ private fun onShareClick(transfer: TransferData) {
+ requireContext().shareText(
+ text = transfer.withdrawalAccount.paytoUri,
+ )
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.withdraw_title)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt
new file mode 100644
index 0000000..35ff89c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt
@@ -0,0 +1,314 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.CURRENCY_BTC
+import net.taler.wallet.R
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.compose.ShareButton
+import net.taler.wallet.compose.copyToClipBoard
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
+import net.taler.wallet.withdraw.TransferData
+import net.taler.wallet.withdraw.WithdrawStatus
+
+@Composable
+fun ScreenTransfer(
+ status: WithdrawStatus.ManualTransferRequired,
+ spec: CurrencySpecification?,
+ bankAppClick: ((transfer: TransferData) -> Unit)?,
+ shareClick: ((transfer: TransferData) -> Unit)?,
+) {
+ // TODO: show some placeholder
+ if (status.withdrawalTransfers.isEmpty()) return
+
+ val defaultTransfer = status.withdrawalTransfers[0]
+ var selectedTransfer by remember { mutableStateOf(defaultTransfer) }
+
+ Column {
+ if (status.withdrawalTransfers.size > 1) {
+ TransferAccountChooser(
+ accounts = status.withdrawalTransfers.map { it.withdrawalAccount },
+ selectedAccount = selectedTransfer.withdrawalAccount,
+ onSelectAccount = { account ->
+ status.withdrawalTransfers.find {
+ it.withdrawalAccount.paytoUri == account.paytoUri
+ }?.let { selectedTransfer = it }
+ }
+ )
+ }
+
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ when (val transfer = selectedTransfer) {
+ is TransferData.Taler -> TransferTaler(
+ transfer = transfer,
+ exchangeBaseUrl = status.exchangeBaseUrl,
+ transactionAmountRaw = status.transactionAmountRaw,
+ transactionAmountEffective = status.transactionAmountEffective,
+ )
+
+ is TransferData.IBAN -> TransferIBAN(
+ transfer = transfer,
+ exchangeBaseUrl = status.exchangeBaseUrl,
+ transactionAmountRaw = status.transactionAmountRaw.withSpec(spec),
+ transactionAmountEffective = status.transactionAmountEffective.withSpec(spec),
+ )
+
+ is TransferData.Bitcoin -> TransferBitcoin(
+ transfer = transfer,
+ transactionAmountRaw = status.transactionAmountRaw.withSpec(spec),
+ transactionAmountEffective = status.transactionAmountEffective.withSpec(spec),
+ )
+ }
+
+ if (bankAppClick != null) {
+ Button(
+ onClick = { bankAppClick(selectedTransfer) },
+ modifier = Modifier
+ .padding(bottom = 16.dp),
+ ) {
+ Text(text = stringResource(R.string.withdraw_manual_ready_bank_button))
+ }
+ }
+
+ if (shareClick != null) {
+ ShareButton(
+ content = selectedTransfer.withdrawalAccount.paytoUri,
+ modifier = Modifier
+ .padding(bottom = 16.dp),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun DetailRow(
+ label: String,
+ content: String,
+ copy: Boolean = true,
+) {
+ val context = LocalContext.current
+
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 6.dp, end = 6.dp),
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+
+ Text(
+ modifier = Modifier.padding(
+ top = 8.dp,
+ start = 6.dp,
+ end = 6.dp,
+ ),
+ text = content,
+ style = MaterialTheme.typography.bodyLarge,
+ fontFamily = if (copy) FontFamily.Monospace else FontFamily.Default,
+ textAlign = TextAlign.Center,
+ )
+
+ if (copy) {
+ IconButton(
+ onClick = { copyToClipBoard(context, label, content) },
+ ) {
+ Icon(
+ imageVector = Icons.Default.ContentCopy,
+ contentDescription = stringResource(R.string.copy),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun WithdrawalAmountTransfer(
+ amountRaw: Amount,
+ amountEffective: Amount,
+ conversionAmountRaw: Amount,
+) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TransactionAmountComposable(
+ label = stringResource(R.string.withdraw_transfer),
+ amount = conversionAmountRaw,
+ amountType = AmountType.Neutral,
+ )
+
+ if (amountRaw.currency != conversionAmountRaw.currency) {
+ TransactionAmountComposable(
+ label = stringResource(R.string.withdraw_conversion),
+ amount = amountRaw,
+ amountType = AmountType.Neutral,
+ )
+ }
+
+ val fee = amountRaw - amountEffective
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee,
+ amountType = AmountType.Negative,
+ )
+ }
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_total),
+ amount = amountEffective,
+ amountType = AmountType.Positive,
+ )
+ }
+}
+
+@Composable
+fun TransferAccountChooser(
+ modifier: Modifier = Modifier,
+ accounts: List<WithdrawalExchangeAccountDetails>,
+ selectedAccount: WithdrawalExchangeAccountDetails,
+ onSelectAccount: (account: WithdrawalExchangeAccountDetails) -> Unit,
+) {
+ val selectedIndex = accounts.indexOfFirst {
+ it.paytoUri == selectedAccount.paytoUri
+ }
+
+ ScrollableTabRow(
+ selectedTabIndex = selectedIndex,
+ modifier = modifier,
+ edgePadding = 8.dp,
+ ) {
+ accounts.forEachIndexed { index, account ->
+ Tab(
+ selected = selectedAccount.paytoUri == account.paytoUri,
+ onClick = { onSelectAccount(account) },
+ text = {
+ if (account.currencySpecification?.name != null) {
+ Text(stringResource(
+ R.string.withdraw_account_currency,
+ index + 1,
+ account.currencySpecification.name,
+ ))
+ } else if (account.transferAmount?.currency != null) {
+ Text(stringResource(
+ R.string.withdraw_account_currency,
+ index + 1,
+ account.transferAmount.currency,
+ ))
+ } else Text(stringResource(R.string.withdraw_account, index + 1))
+ },
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun ScreenTransferPreview() {
+ Surface {
+ ScreenTransfer(
+ status = WithdrawStatus.ManualTransferRequired(
+ transactionId = "",
+ transactionAmountRaw = Amount.fromJSONString("KUDOS:10"),
+ transactionAmountEffective = Amount.fromJSONString("KUDOS:9.5"),
+ exchangeBaseUrl = "test.exchange.taler.net",
+ withdrawalTransfers = listOf(
+ TransferData.IBAN(
+ iban = "ASDQWEASDZXCASDQWE",
+ subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG",
+ amountRaw = Amount("KUDOS", 10, 0),
+ amountEffective = Amount("KUDOS", 9, 5),
+ withdrawalAccount = WithdrawalExchangeAccountDetails(
+ paytoUri = "https://taler.net/kudos",
+ transferAmount = Amount("KUDOS", 10, 0),
+ currencySpecification = CurrencySpecification(
+ "KUDOS",
+ numFractionalInputDigits = 2,
+ numFractionalNormalDigits = 2,
+ numFractionalTrailingZeroDigits = 2,
+ altUnitNames = emptyMap(),
+ ),
+ ),
+ ),
+ TransferData.Bitcoin(
+ account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
+ segwitAddresses = listOf(
+ "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq",
+ "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c"
+ ),
+ subject = "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
+ amountRaw = Amount(CURRENCY_BTC, 0, 14000000),
+ amountEffective = Amount(CURRENCY_BTC, 0, 14000000),
+ withdrawalAccount = WithdrawalExchangeAccountDetails(
+ paytoUri = "https://taler.net/btc",
+ transferAmount = Amount("BTC", 0, 14000000),
+ currencySpecification = CurrencySpecification(
+ "Bitcoin",
+ numFractionalInputDigits = 2,
+ numFractionalNormalDigits = 2,
+ numFractionalTrailingZeroDigits = 2,
+ altUnitNames = emptyMap(),
+ ),
+ ),
+ )
+ ),
+ ),
+ spec = null,
+ bankAppClick = {},
+ shareClick = {},
+ )
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt
new file mode 100644
index 0000000..c21ca7e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt
@@ -0,0 +1,112 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.compose.CopyToClipboardButton
+import net.taler.wallet.withdraw.TransferData
+
+@Composable
+fun TransferBitcoin(
+ transfer: TransferData.Bitcoin,
+ transactionAmountRaw: Amount,
+ transactionAmountEffective: Amount,
+) {
+ Column(
+ modifier = Modifier.padding(all = 16.dp),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(R.string.withdraw_manual_bitcoin_intro),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ )
+
+ BitcoinSegwitAddresses(
+ amount = transfer.amountRaw,
+ address = transfer.account,
+ segwitAddresses = transfer.segwitAddresses,
+ )
+
+ transfer.withdrawalAccount.transferAmount?.let { amount ->
+ WithdrawalAmountTransfer(
+ amountRaw = transactionAmountRaw,
+ amountEffective = transactionAmountEffective,
+ conversionAmountRaw = amount.withSpec(
+ transfer.withdrawalAccount.currencySpecification,
+ ),
+ )
+ }
+ }
+}
+
+@Composable
+fun BitcoinSegwitAddresses(amount: Amount, address: String, segwitAddresses: List<String>) {
+ Column {
+ val allSegwitAddresses = listOf(address) + segwitAddresses
+ for (segwitAddress in allSegwitAddresses) {
+ Row(modifier = Modifier.padding(vertical = 8.dp)) {
+ Column(modifier = Modifier.weight(0.3f)) {
+ Text(
+ text = segwitAddress,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.bodySmall,
+ )
+ Text(
+ text = if (segwitAddress == address)
+ amount.withCurrency("BTC").toString()
+ else SEGWIT_MIN.toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+ }
+ }
+
+ CopyToClipboardButton(
+ modifier = Modifier
+ .padding(top = 16.dp, start = 6.dp, end = 6.dp)
+ .align(CenterHorizontally),
+ label = "Bitcoin",
+ content = getCopyText(amount, address, segwitAddresses),
+ )
+ }
+}
+
+private val SEGWIT_MIN = Amount("BTC", 0, 294)
+
+private fun getCopyText(amount: Amount, addr: String, segwitAddresses: List<String>): String {
+ val sr = segwitAddresses.joinToString(separator = "\n") { s ->
+ "\n$s ${SEGWIT_MIN}\n"
+ }
+ return "$addr ${amount.withCurrency("BTC")}\n$sr"
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt
new file mode 100644
index 0000000..6c1b014
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt
@@ -0,0 +1,91 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.withdraw.TransferData
+
+@Composable
+fun TransferIBAN(
+ transfer: TransferData.IBAN,
+ exchangeBaseUrl: String,
+ transactionAmountRaw: Amount,
+ transactionAmountEffective: Amount,
+) {
+ Column(
+ modifier = Modifier.padding(all = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(
+ R.string.withdraw_manual_ready_intro,
+ transfer.amountRaw.toString()),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.withdraw_manual_ready_warning),
+ style = MaterialTheme.typography.bodyMedium,
+ color = colorResource(R.color.notice_text),
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(all = 8.dp)
+ .background(colorResource(R.color.notice_background))
+ .border(BorderStroke(2.dp, colorResource(R.color.notice_border)))
+ .padding(all = 16.dp)
+ )
+
+ transfer.receiverName?.let {
+ DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it)
+ }
+ DetailRow(stringResource(R.string.withdraw_manual_ready_iban), transfer.iban)
+ DetailRow(stringResource(R.string.withdraw_manual_ready_subject), transfer.subject)
+
+ TransactionInfoComposable(
+ label = stringResource(R.string.withdraw_exchange),
+ info = cleanExchange(exchangeBaseUrl),
+ )
+
+ transfer.withdrawalAccount.transferAmount?.let { amount ->
+ WithdrawalAmountTransfer(
+ amountRaw = transactionAmountRaw,
+ amountEffective = transactionAmountEffective,
+ conversionAmountRaw = amount.withSpec(
+ transfer.withdrawalAccount.currencySpecification,
+ ),
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt
new file mode 100644
index 0000000..cc6597e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt
@@ -0,0 +1,89 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.withdraw.TransferData
+
+@Composable
+fun TransferTaler(
+ transfer: TransferData.Taler,
+ exchangeBaseUrl: String,
+ transactionAmountRaw: Amount,
+ transactionAmountEffective: Amount,
+) {
+ Column(
+ modifier = Modifier.padding(all = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(
+ R.string.withdraw_manual_ready_intro,
+ transfer.amountRaw.toString()),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.withdraw_manual_ready_warning),
+ style = MaterialTheme.typography.bodyMedium,
+ color = colorResource(R.color.notice_text),
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(all = 8.dp)
+ .background(colorResource(R.color.notice_background))
+ .border(BorderStroke(2.dp, colorResource(R.color.notice_border)))
+ .padding(all = 16.dp)
+ )
+
+ transfer.receiverName?.let {
+ DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it)
+ }
+ DetailRow(stringResource(R.string.withdraw_manual_ready_account), transfer.account)
+ DetailRow(stringResource(R.string.withdraw_manual_ready_subject), transfer.subject)
+
+ TransactionInfoComposable(
+ label = stringResource(R.string.withdraw_exchange),
+ info = cleanExchange(exchangeBaseUrl),
+ )
+
+ transfer.withdrawalAccount.transferAmount?.let { amount ->
+ WithdrawalAmountTransfer(
+ amountRaw = transactionAmountRaw,
+ amountEffective = transactionAmountEffective,
+ conversionAmountRaw = amount,
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/res/drawable/ic_archive.xml b/wallet/src/main/res/drawable/ic_archive.xml
new file mode 100644
index 0000000..58a032c
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_archive.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2023 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<vector android:height="24dp" android:tint="?attr/colorControlNormal"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M20.54,5.23l-1.39,-1.68C18.88,3.21 18.47,3 18,3H6c-0.47,0 -0.88,0.21 -1.16,0.55L3.46,5.23C3.17,5.57 3,6.02 3,6.5V19c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V6.5c0,-0.48 -0.17,-0.93 -0.46,-1.27zM12,17.5L6.5,12H10v-2h4v2h3.5L12,17.5zM5.12,5l0.81,-1h12l0.94,1H5.12z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_baseline_account_tree.xml b/wallet/src/main/res/drawable/ic_baseline_account_tree.xml
deleted file mode 100644
index 7067bd3..0000000
--- a/wallet/src/main/res/drawable/ic_baseline_account_tree.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?attr/colorControlNormal"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:fillColor="@android:color/white"
- android:pathData="M22,11V3h-7v3H9V3H2v8h7V8h2v10h4v3h7v-8h-7v3h-2V8h2v3z" />
-</vector>
diff --git a/wallet/src/main/res/drawable/ic_baseline_backup.xml b/wallet/src/main/res/drawable/ic_baseline_backup.xml
deleted file mode 100644
index 006b014..0000000
--- a/wallet/src/main/res/drawable/ic_baseline_backup.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?attr/colorControlNormal"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:fillColor="@android:color/white"
- android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM14,13v4h-4v-4H7l5,-5 5,5h-3z" />
-</vector>
diff --git a/wallet/src/main/res/drawable/ic_baseline_vpn_key.xml b/wallet/src/main/res/drawable/ic_baseline_vpn_key.xml
deleted file mode 100644
index 7b554c9..0000000
--- a/wallet/src/main/res/drawable/ic_baseline_vpn_key.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?attr/colorControlNormal"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:fillColor="@android:color/white"
- android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
-</vector>
diff --git a/wallet/src/main/res/drawable/ic_fail.xml b/wallet/src/main/res/drawable/ic_fail.xml
new file mode 100644
index 0000000..a25c5ed
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_fail.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M14.69,2.21L4.33,11.49c-0.64,0.58 -0.28,1.65 0.58,1.73L13,14l-4.85,6.76c-0.22,0.31 -0.19,0.74 0.08,1.01h0c0.3,0.3 0.77,0.31 1.08,0.02l10.36,-9.28c0.64,-0.58 0.28,-1.65 -0.58,-1.73L11,10l4.85,-6.76c0.22,-0.31 0.19,-0.74 -0.08,-1.01l0,0C15.47,1.93 15,1.92 14.69,2.21z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_resume.xml b/wallet/src/main/res/drawable/ic_resume.xml
new file mode 100644
index 0000000..e3fd2e9
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_resume.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_retry.xml b/wallet/src/main/res/drawable/ic_retry.xml
new file mode 100644
index 0000000..98469ca
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_retry.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_suspend.xml b/wallet/src/main/res/drawable/ic_suspend.xml
new file mode 100644
index 0000000..938bd7f
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_suspend.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_unarchive.xml b/wallet/src/main/res/drawable/ic_unarchive.xml
new file mode 100644
index 0000000..8a48bba
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_unarchive.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2022 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M20.55,5.22l-1.39,-1.68C18.88,3.21 18.47,3 18,3H6C5.53,3 5.12,3.21 4.85,3.55L3.46,5.22C3.17,5.57 3,6.01 3,6.5V19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6.5C21,6.01 20.83,5.57 20.55,5.22zM12,9.5l5.5,5.5H14v2h-4v-2H6.5L12,9.5zM5.12,5l0.82,-1h12l0.93,1H5.12z" />
+</vector>
diff --git a/wallet/src/main/res/layout/app_content_main.xml b/wallet/src/main/res/layout/app_content_main.xml
index 6937e59..71fbcd4 100644
--- a/wallet/src/main/res/layout/app_content_main.xml
+++ b/wallet/src/main/res/layout/app_content_main.xml
@@ -36,20 +36,42 @@
style="@style/AppTheme.Toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+ android:theme="@style/Widget.Material3.ActionBar.Solid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+ <FrameLayout
+ android:id="@+id/offline_banner"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:background="?attr/colorPrimary"
+ app:layout_constraintTop_toBottomOf="@id/toolbar"
+ app:layout_constraintBottom_toTopOf="@id/progress_bar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:animateLayoutChanges="true"
+ android:visibility="gone"
+ tools:visibility="visible">
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:textAlignment="center"
+ android:textColor="?attr/colorOnPrimary"
+ android:text="@string/offline_banner" />
+ </FrameLayout>
+
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/progress_bar"
- style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal"
+ style="@style/Widget.MaterialProgressBar.ProgressBar"
android:layout_width="0dp"
android:layout_height="4dp"
android:elevation="4dp"
android:indeterminate="true"
android:visibility="invisible"
- app:layout_constraintBottom_toBottomOf="@+id/toolbar"
+ app:layout_constraintBottom_toBottomOf="@+id/offline_banner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:mpb_progressStyle="horizontal"
diff --git a/wallet/src/main/res/layout/dialog_exchange_add.xml b/wallet/src/main/res/layout/dialog_exchange_add.xml
index dfa0f70..bf90177 100644
--- a/wallet/src/main/res/layout/dialog_exchange_add.xml
+++ b/wallet/src/main/res/layout/dialog_exchange_add.xml
@@ -22,7 +22,7 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/urlLayout"
- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
diff --git a/wallet/src/main/res/layout/fragment_already_paid.xml b/wallet/src/main/res/layout/fragment_already_paid.xml
deleted file mode 100644
index 18ce0b5..0000000
--- a/wallet/src/main/res/layout/fragment_already_paid.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="15dp"
- android:orientation="vertical"
- tools:context=".payment.AlreadyPaidFragment">
-
- <Space
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="50dp"
- android:layout_gravity="center"
- android:text="@string/payment_already_paid"
- android:textAlignment="center"
- android:textColor="@android:color/holo_green_dark"
- app:autoSizeTextType="uniform" />
-
-
- <Space
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
-
- <Button
- android:id="@+id/backButton"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/button_back" />
-
-</LinearLayout>
diff --git a/wallet/src/main/res/layout/fragment_error.xml b/wallet/src/main/res/layout/fragment_error.xml
index b9f585f..2af166a 100644
--- a/wallet/src/main/res/layout/fragment_error.xml
+++ b/wallet/src/main/res/layout/fragment_error.xml
@@ -29,13 +29,13 @@
android:alpha="0.56"
android:src="@drawable/ic_error"
app:layout_constraintBottom_toTopOf="@+id/errorTitle"
- app:layout_constraintDimensionRatio="1:1"
+ app:layout_constraintDimensionRatio="1.5:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed"
- app:tint="@color/red"
+ app:tint="?colorError"
tools:ignore="ContentDescription" />
<TextView
@@ -45,7 +45,7 @@
android:layout_margin="16dp"
android:gravity="center_horizontal|top"
android:minHeight="64dp"
- android:textColor="@color/red"
+ android:textColor="?colorError"
app:autoSizeMaxTextSize="40sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toTopOf="@+id/errorMessage"
@@ -60,7 +60,7 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
- android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintBottom_toTopOf="@+id/errorDevMessage"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -74,7 +74,7 @@
android:layout_margin="16dp"
android:fontFamily="monospace"
android:gravity="center"
- android:textColor="@color/red"
+ android:textColor="?colorError"
android:textIsSelectable="true"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/backButton"
diff --git a/wallet/src/main/res/layout/fragment_exchange_fees.xml b/wallet/src/main/res/layout/fragment_exchange_fees.xml
index 6f8814f..03d69f5 100644
--- a/wallet/src/main/res/layout/fragment_exchange_fees.xml
+++ b/wallet/src/main/res/layout/fragment_exchange_fees.xml
@@ -44,7 +44,7 @@
app:layout_constraintStart_toEndOf="@+id/withdrawFeeLabel"
app:layout_constraintTop_toTopOf="@+id/withdrawFeeLabel"
tools:text="-0.23 TESTKUDOS"
- tools:textColor="@color/red" />
+ tools:textColor="?colorError" />
<TextView
android:id="@+id/overheadLabel"
@@ -64,7 +64,7 @@
app:layout_constraintStart_toEndOf="@+id/overheadLabel"
app:layout_constraintTop_toTopOf="@+id/overheadLabel"
tools:text="-0.42 TESTKUDOS"
- tools:textColor="@color/red" />
+ tools:textColor="?colorError" />
<TextView
android:id="@+id/expirationLabel"
diff --git a/wallet/src/main/res/layout/fragment_main.xml b/wallet/src/main/res/layout/fragment_main.xml
index 3f680ba..6f9e693 100644
--- a/wallet/src/main/res/layout/fragment_main.xml
+++ b/wallet/src/main/res/layout/fragment_main.xml
@@ -23,13 +23,14 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
- <com.google.android.material.floatingactionbutton.FloatingActionButton
+ <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/mainFab"
style="@style/FabStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/button_scan_qr_code"
- android:src="@drawable/ic_scan_qr"
+ android:text="@string/button_scan_qr_code_label"
+ app:icon="@drawable/ic_scan_qr"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/wallet/src/main/res/layout/fragment_manual_withdraw.xml b/wallet/src/main/res/layout/fragment_manual_withdraw.xml
index 5b37d2a..6d5f78b 100644
--- a/wallet/src/main/res/layout/fragment_manual_withdraw.xml
+++ b/wallet/src/main/res/layout/fragment_manual_withdraw.xml
@@ -14,105 +14,109 @@
~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-->
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/qrCodeButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ app:icon="@drawable/ic_scan_qr"
+ app:iconTint="?colorOnPrimary"
+ android:text="@string/button_scan_qr_code"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:ignore="RtlHardcoded" />
- <Button
- android:id="@+id/qrCodeButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="32dp"
- android:drawableLeft="@drawable/ic_scan_qr"
- android:text="@string/button_scan_qr_code"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_chainStyle="packed"
- tools:ignore="RtlHardcoded" />
-
- <TextView
- android:id="@+id/orView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="32dp"
- android:text="@string/or"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/qrCodeButton" />
-
- <TextView
- android:id="@+id/manualWithdrawIntro"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="32dp"
- android:text="@string/withdraw_manual_title"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orView" />
+ <TextView
+ android:id="@+id/orView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:text="@string/or"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/qrCodeButton" />
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/amountLayout"
- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginTop="16dp"
- android:layout_marginEnd="16dp"
- android:hint="@string/withdraw_amount"
- app:boxBackgroundMode="outline"
- app:endIconDrawable="@drawable/ic_cancel"
- app:endIconMode="clear_text"
- app:layout_constraintEnd_toStartOf="@+id/currencyView"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/manualWithdrawIntro">
+ <TextView
+ android:id="@+id/manualWithdrawIntro"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:text="@string/withdraw_manual_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/orView" />
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/amountView"
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/amountLayout"
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:ems="10"
- android:inputType="number" />
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:minWidth="128dp"
+ app:boxBackgroundMode="outline"
+ app:endIconDrawable="@drawable/ic_cancel"
+ app:endIconMode="clear_text"
+ app:layout_constraintEnd_toStartOf="@+id/currencyView"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/manualWithdrawIntro">
- </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/amountView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inputType="numberDecimal" />
- <TextView
- android:id="@+id/currencyView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:layout_constraintBottom_toBottomOf="@+id/amountLayout"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@+id/amountLayout"
- app:layout_constraintTop_toTopOf="@+id/amountLayout"
- tools:text="TESTKUDOS123" />
+ </com.google.android.material.textfield.TextInputLayout>
- <TextView
- android:id="@+id/paymentOptionsLabel"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_marginTop="32dp"
- android:layout_marginEnd="16dp"
- android:text="@string/withdraw_manual_payment_options"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/amountLayout" />
+ <TextView
+ android:id="@+id/currencyView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="@+id/amountLayout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/amountLayout"
+ app:layout_constraintTop_toTopOf="@+id/amountLayout"
+ tools:text="TESTKUDOS123" />
- <Button
- android:id="@+id/checkFeesButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="32dp"
- android:layout_marginEnd="16dp"
- android:text="@string/withdraw_manual_check_fees"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/paymentOptionsLabel"
- app:layout_constraintVertical_bias="0.0" />
+ <TextView
+ android:id="@+id/paymentOptionsLabel"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:text="@string/withdraw_manual_payment_options"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/amountLayout" />
+
+ <Button
+ android:id="@+id/checkFeesButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:text="@string/withdraw_manual_check_fees"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/paymentOptionsLabel"
+ app:layout_constraintVertical_bias="0.0" />
-</androidx.constraintlayout.widget.ConstraintLayout>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</ScrollView> \ No newline at end of file
diff --git a/wallet/src/main/res/layout/fragment_pending_operations.xml b/wallet/src/main/res/layout/fragment_pending_operations.xml
deleted file mode 100644
index 5030234..0000000
--- a/wallet/src/main/res/layout/fragment_pending_operations.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/list_pending"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:scrollbars="vertical"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- tools:listitem="@layout/list_item_pending_operation" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml
index 421911a..44d95de 100644
--- a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml
+++ b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml
@@ -22,106 +22,108 @@
tools:context=".withdraw.PromptWithdrawFragment">
<TextView
- android:id="@+id/introView"
+ android:id="@+id/chosenAmountLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
- android:layout_marginBottom="8dp"
android:gravity="center"
- android:text="@string/withdraw_total"
+ android:text="@string/amount_chosen"
android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/effectiveAmountView"
+ app:layout_constraintBottom_toTopOf="@+id/chosenAmountView"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:visibility="visible" />
<TextView
- android:id="@+id/effectiveAmountView"
+ android:id="@+id/chosenAmountView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:gravity="center"
- android:textColor="@color/green"
- android:textSize="24sp"
+ android:textSize="20sp"
android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/chosenAmountLabel"
+ app:layout_constraintBottom_toTopOf="@+id/feeLabel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/introView"
- tools:text="9.8 TESTKUDOS"
+ app:layout_constraintTop_toBottomOf="@id/chosenAmountLabel"
+ tools:text="10 TESTKUDOS"
tools:visibility="visible" />
<TextView
- android:id="@+id/chosenAmountLabel"
+ android:id="@+id/feeLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:gravity="center"
- android:text="@string/amount_chosen"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/chosenAmountView"
+ android:text="@string/withdraw_fees"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/feeView"
app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
+ app:layout_constraintTop_toBottomOf="@+id/chosenAmountView"
tools:visibility="visible" />
<TextView
- android:id="@+id/chosenAmountView"
+ android:id="@+id/feeView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:gravity="center"
+ android:textColor="?colorError"
android:textSize="20sp"
- android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/feeLabel"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/introView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountLabel"
- tools:text="10 TESTKUDOS"
+ app:layout_constraintTop_toBottomOf="@+id/feeLabel"
+ tools:text="-0.2 TESTKUDOS"
tools:visibility="visible" />
<TextView
- android:id="@+id/feeLabel"
+ android:id="@+id/introView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
android:gravity="center"
- android:text="@string/withdraw_fees"
+ android:text="@string/withdraw_total"
android:visibility="invisible"
- app:layout_constraintBottom_toTopOf="@+id/feeView"
+ app:layout_constraintBottom_toTopOf="@+id/effectiveAmountView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountView"
+ app:layout_constraintTop_toBottomOf="@id/feeView"
tools:visibility="visible" />
<TextView
- android:id="@+id/feeView"
+ android:id="@+id/effectiveAmountView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
- android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:gravity="center"
- android:textColor="@color/red"
- android:textSize="20sp"
+ android:textColor="@color/green"
+ android:textSize="24sp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/exchangeIntroView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeLabel"
- tools:text="-0.2 TESTKUDOS"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:text="9.8 TESTKUDOS"
tools:visibility="visible" />
<TextView
@@ -139,7 +141,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeView"
+ app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
tools:visibility="visible" />
<TextView
@@ -152,7 +154,7 @@
android:textSize="24sp"
android:visibility="invisible"
app:layout_constrainedWidth="true"
- app:layout_constraintBottom_toTopOf="@+id/withdrawCard"
+ app:layout_constraintBottom_toTopOf="@+id/ageLabel"
app:layout_constraintEnd_toStartOf="@+id/selectExchangeButton"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
@@ -168,12 +170,46 @@
android:backgroundTint="@color/colorPrimary"
android:contentDescription="@string/nav_exchange_fees"
android:src="@drawable/ic_edit"
- android:tint="?attr/colorOnPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/withdrawExchangeUrl"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/withdrawExchangeUrl"
app:layout_constraintTop_toTopOf="@+id/withdrawExchangeUrl"
+ app:tint="?attr/colorOnPrimary"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/ageLabel"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:text="@string/withdraw_restrict_age"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/ageSelector"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/withdrawExchangeUrl"
+ tools:visibility="visible" />
+
+ <Spinner
+ android:id="@+id/ageSelector"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:spinnerMode="dropdown"
+ android:textSize="20sp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/withdrawCard"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/ageLabel"
tools:visibility="visible" />
<ProgressBar
diff --git a/wallet/src/main/res/layout/fragment_review_exchange_tos.xml b/wallet/src/main/res/layout/fragment_review_exchange_tos.xml
index ec8d996..20c1013 100644
--- a/wallet/src/main/res/layout/fragment_review_exchange_tos.xml
+++ b/wallet/src/main/res/layout/fragment_review_exchange_tos.xml
@@ -48,7 +48,7 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
- android:textColor="@color/red"
+ android:textColor="?colorError"
android:textSize="16sp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
diff --git a/wallet/src/main/res/layout/fragment_transaction_payment.xml b/wallet/src/main/res/layout/fragment_transaction_payment.xml
deleted file mode 100644
index 20ba161..0000000
--- a/wallet/src/main/res/layout/fragment_transaction_payment.xml
+++ /dev/null
@@ -1,126 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fillViewport="true"
- tools:context=".transactions.TransactionDetailFragment">
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:id="@+id/timeView"
- style="@style/TransactionLabel.Time"
- app:layout_constraintBottom_toTopOf="@+id/amountPaidWithFeesLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_chainStyle="packed"
- tools:text="23 March 2020 23:42pm" />
-
- <TextView
- android:id="@+id/amountPaidWithFeesLabel"
- style="@style/TransactionLabel"
- android:text="@string/transaction_paid"
- app:layout_constraintBottom_toTopOf="@+id/amountPaidWithFeesView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/timeView" />
-
- <TextView
- android:id="@+id/amountPaidWithFeesView"
- style="@style/TransactionContent"
- android:textColor="@color/red"
- app:layout_constraintBottom_toTopOf="@+id/orderAmountLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/amountPaidWithFeesLabel"
- tools:text="-23.42 TESTKUDOS" />
-
- <TextView
- android:id="@+id/orderAmountLabel"
- style="@style/TransactionLabel"
- android:text="@string/transaction_order_total"
- app:layout_constraintBottom_toTopOf="@+id/orderAmountView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/amountPaidWithFeesView" />
-
- <TextView
- android:id="@+id/orderAmountView"
- style="@style/TransactionContent"
- app:layout_constraintBottom_toTopOf="@+id/feeLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orderAmountLabel"
- tools:text="23 TESTKUDOS" />
-
- <TextView
- android:id="@+id/feeLabel"
- style="@style/TransactionLabel"
- android:text="@string/withdraw_fees"
- app:layout_constraintBottom_toTopOf="@+id/feeView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orderAmountView" />
-
- <TextView
- android:id="@+id/feeView"
- style="@style/TransactionContent"
- android:textColor="@color/red"
- app:layout_constraintBottom_toTopOf="@+id/orderSummaryLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeLabel"
- tools:text="-0.42 TESTKUDOS" />
-
- <TextView
- android:id="@+id/orderSummaryLabel"
- style="@style/TransactionLabel"
- android:text="@string/transaction_order"
- app:layout_constraintBottom_toTopOf="@+id/orderSummaryView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeView" />
-
- <TextView
- android:id="@+id/orderSummaryView"
- style="@style/TransactionContent"
- android:textColor="?android:textColorPrimary"
- app:layout_constraintBottom_toTopOf="@+id/orderIdView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orderSummaryLabel"
- tools:text="Some Product that was bought and can have quite a long label" />
-
- <TextView
- android:id="@+id/orderIdView"
- style="@style/TransactionLabel"
- android:layout_marginBottom="16dp"
- android:text="@string/transaction_order_id"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/orderSummaryView" />
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
-</ScrollView>
diff --git a/wallet/src/main/res/layout/fragment_transaction_withdrawal.xml b/wallet/src/main/res/layout/fragment_transaction_withdrawal.xml
deleted file mode 100644
index 8fe3247..0000000
--- a/wallet/src/main/res/layout/fragment_transaction_withdrawal.xml
+++ /dev/null
@@ -1,128 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fillViewport="true"
- tools:context=".transactions.TransactionDetailFragment">
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:id="@+id/timeView"
- style="@style/TransactionLabel.Time"
- app:layout_constraintBottom_toTopOf="@+id/effectiveAmountLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_chainStyle="packed"
- tools:text="23 March 2020 23:42pm" />
-
- <TextView
- android:id="@+id/effectiveAmountLabel"
- style="@style/TransactionLabel"
- app:layout_constraintBottom_toTopOf="@+id/effectiveAmountView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/timeView"
- tools:text="@string/withdraw_total" />
-
- <TextView
- android:id="@+id/effectiveAmountView"
- style="@style/TransactionContent"
- android:textColor="@color/green"
- app:layout_constraintBottom_toTopOf="@+id/confirmWithdrawalButton"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/effectiveAmountLabel"
- tools:text="23.42 TESTKUDOS" />
-
- <Button
- android:id="@+id/confirmWithdrawalButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:drawableLeft="@drawable/ic_account_balance"
- android:text="@string/withdraw_button_confirm_bank"
- app:drawableTint="?attr/colorOnPrimarySurface"
- app:layout_constraintBottom_toTopOf="@+id/chosenAmountLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
- tools:ignore="RtlHardcoded" />
-
- <TextView
- android:id="@+id/chosenAmountLabel"
- style="@style/TransactionLabel"
- app:layout_constraintBottom_toTopOf="@+id/chosenAmountView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/confirmWithdrawalButton"
- tools:text="@string/amount_chosen" />
-
- <TextView
- android:id="@+id/chosenAmountView"
- style="@style/TransactionContent"
- app:layout_constraintBottom_toTopOf="@+id/feeLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountLabel"
- tools:text="24 TESTKUDOS" />
-
- <TextView
- android:id="@+id/feeLabel"
- style="@style/TransactionLabel"
- android:text="@string/withdraw_fees"
- app:layout_constraintBottom_toTopOf="@+id/feeView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/chosenAmountView" />
-
- <TextView
- android:id="@+id/feeView"
- style="@style/TransactionContent"
- android:textColor="@color/red"
- app:layout_constraintBottom_toTopOf="@+id/exchangeLabel"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeLabel"
- tools:text="-0.38 TESTKUDOS" />
-
- <TextView
- android:id="@+id/exchangeLabel"
- style="@style/TransactionLabel"
- android:text="@string/withdraw_exchange"
- app:layout_constraintBottom_toTopOf="@+id/exchangeView"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/feeView" />
-
- <TextView
- android:id="@+id/exchangeView"
- style="@style/TransactionContent"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/exchangeLabel"
- tools:text="exchange.demo.taler.net" />
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
-</ScrollView>
diff --git a/wallet/src/main/res/layout/fragment_transactions.xml b/wallet/src/main/res/layout/fragment_transactions.xml
index aaf638c..8fa46f5 100644
--- a/wallet/src/main/res/layout/fragment_transactions.xml
+++ b/wallet/src/main/res/layout/fragment_transactions.xml
@@ -14,29 +14,108 @@
~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/sendButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="10dp"
+ android:text="@string/transactions_send_funds"
+ app:layout_constraintBottom_toTopOf="@+id/divider"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/receiveButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="10dp"
+ android:text="@string/transactions_receive_funds"
+ app:layout_constraintBottom_toTopOf="@+id/divider"
+ app:layout_constraintEnd_toStartOf="@+id/amount"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toEndOf="@+id/sendButton"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/balanceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:text="@string/transactions_balance"
+ android:textSize="14sp"
+ app:layout_constraintBottom_toTopOf="@+id/amount"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toEndOf="@+id/receiveButton"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/amount"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
+ android:textSize="24sp"
+ android:textStyle="bold"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toTopOf="@+id/divider"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/receiveButton"
+ app:layout_constraintTop_toBottomOf="@+id/balanceLabel"
+ tools:text="23.42"
+ tools:visibility="visible" />
+
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/topBarrier"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="sendButton,receiveButton,amount" />
+
+ <com.google.android.material.divider.MaterialDivider
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/topBarrier" />
+
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_height="0dp"
android:scrollbars="vertical"
android:visibility="invisible"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/divider"
tools:listitem="@layout/list_item_transaction"
tools:visibility="visible" />
<TextView
android:id="@+id/emptyState"
- android:layout_width="wrap_content"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
+ android:layout_margin="16dp"
+ android:gravity="center"
android:text="@string/transactions_empty"
+ android:textSize="16sp"
android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/divider"
tools:visibility="visible" />
<ProgressBar
@@ -46,6 +125,22 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/divider"
tools:visibility="visible" />
-</FrameLayout>
+ <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+ android:id="@+id/mainFab"
+ style="@style/FabStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/button_scan_qr_code"
+ android:text="@string/button_scan_qr_code_label"
+ app:icon="@drawable/ic_scan_qr"
+ app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/layout/fragment_uri_input.xml b/wallet/src/main/res/layout/fragment_uri_input.xml
index 1e9934f..95c2297 100644
--- a/wallet/src/main/res/layout/fragment_uri_input.xml
+++ b/wallet/src/main/res/layout/fragment_uri_input.xml
@@ -22,7 +22,7 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/uriLayout"
- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
@@ -42,7 +42,7 @@
</com.google.android.material.textfield.TextInputLayout>
- <Button
+ <com.google.android.material.button.MaterialButton
android:id="@+id/pasteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -50,9 +50,10 @@
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
- android:drawableLeft="@drawable/ic_content_paste"
android:text="@string/paste"
- app:drawableTint="?attr/colorOnPrimarySurface"
+ android:textColor="?colorOnPrimary"
+ app:icon="@drawable/ic_content_paste"
+ app:iconTint="?colorOnPrimary"
app:layout_constraintEnd_toStartOf="@+id/okButton"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
@@ -68,6 +69,7 @@
android:layout_marginEnd="16dp"
android:backgroundTint="@color/green"
android:text="@string/ok"
+ android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/pasteButton"
app:layout_constraintTop_toBottomOf="@+id/uriLayout" />
diff --git a/wallet/src/main/res/layout/list_item_age.xml b/wallet/src/main/res/layout/list_item_age.xml
new file mode 100644
index 0000000..2d3a6e5
--- /dev/null
+++ b/wallet/src/main/res/layout/list_item_age.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2023 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ style="?android:attr/spinnerItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:padding="8dp"
+ android:singleLine="true"
+ android:textAlignment="inherit"
+ android:textSize="20sp" />
diff --git a/wallet/src/main/res/layout/list_item_balance.xml b/wallet/src/main/res/layout/list_item_balance.xml
index 475e7d6..53e3d89 100644
--- a/wallet/src/main/res/layout/list_item_balance.xml
+++ b/wallet/src/main/res/layout/list_item_balance.xml
@@ -27,8 +27,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
- android:textSize="40sp"
- app:layout_constraintEnd_toStartOf="@+id/balanceCurrencyView"
+ style="?textAppearanceDisplaySmall"
+ app:layout_constraintEnd_toStartOf="@+id/pendingView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
@@ -36,17 +36,18 @@
tools:text="100.50" />
<TextView
- android:id="@+id/balanceCurrencyView"
- android:layout_width="wrap_content"
+ android:id="@+id/scopeView"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginEnd="8dp"
- android:textSize="20sp"
- app:layout_constrainedWidth="true"
- app:layout_constraintBottom_toBottomOf="@+id/balanceAmountView"
- app:layout_constraintEnd_toStartOf="@+id/pendingView"
- app:layout_constraintStart_toEndOf="@+id/balanceAmountView"
- app:layout_constraintTop_toTopOf="@+id/balanceAmountView"
- tools:text="TESTKUDOS" />
+ android:layout_marginBottom="8dp"
+ style="?textAppearanceBodyMedium"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@id/balanceAmountView"
+ app:layout_constraintBottom_toTopOf="@id/balanceInboundAmount"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/pendingView"
+ tools:text="@string/balance_scope_exchange"
+ tools:visibility="visible"/>
<TextView
android:id="@+id/balanceInboundAmount"
@@ -54,12 +55,13 @@
android:layout_height="wrap_content"
android:textColor="@color/green"
android:textSize="20sp"
+ style="?textAppearanceBodyLarge"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/balanceInboundLabel"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/balanceAmountView"
+ app:layout_constraintTop_toBottomOf="@+id/scopeView"
tools:text="+10 TESTKUDOS"
tools:visibility="visible" />
@@ -70,6 +72,7 @@
android:layout_marginStart="8dp"
android:text="@string/balances_inbound_label"
android:textColor="@color/green"
+ style="?textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="@+id/balanceInboundAmount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/balanceInboundAmount"
diff --git a/wallet/src/main/res/layout/list_item_pending_operation.xml b/wallet/src/main/res/layout/list_item_pending_operation.xml
deleted file mode 100644
index bd606c3..0000000
--- a/wallet/src/main/res/layout/list_item_pending_operation.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/pending_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_margin="3dp"
- android:background="@drawable/pending_border"
- android:orientation="vertical"
- android:padding="3dp">
-
- <TextView
- android:id="@+id/pending_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="24sp"
- tools:text="My Pending Operation" />
-
- <Button
- android:id="@+id/button_pending_action_1"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- tools:text="Cancel Operation" />
-
- <TextView
- android:id="@+id/pending_subtext"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="14sp"
- tools:text="My further details" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/wallet/src/main/res/layout/list_item_transaction.xml b/wallet/src/main/res/layout/list_item_transaction.xml
index ed031c3..64d9045 100644
--- a/wallet/src/main/res/layout/list_item_transaction.xml
+++ b/wallet/src/main/res/layout/list_item_transaction.xml
@@ -17,6 +17,7 @@
<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/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
diff --git a/wallet/src/main/res/layout/nav_header_main.xml b/wallet/src/main/res/layout/nav_header_main.xml
index e70e80f..6837814 100644
--- a/wallet/src/main/res/layout/nav_header_main.xml
+++ b/wallet/src/main/res/layout/nav_header_main.xml
@@ -39,7 +39,7 @@
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/nav_header_title"
- android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/talerLogoView" />
diff --git a/wallet/src/main/res/menu/activity_main_drawer.xml b/wallet/src/main/res/menu/activity_main_drawer.xml
index d1cc462..42e4311 100644
--- a/wallet/src/main/res/menu/activity_main_drawer.xml
+++ b/wallet/src/main/res/menu/activity_main_drawer.xml
@@ -39,10 +39,7 @@
<group
android:id="@+id/nav_group_dev"
android:checkableBehavior="single">
- <item
- android:id="@+id/nav_pending_operations"
- android:icon="@drawable/ic_sync"
- android:title="@string/pending_operations_title" />
+ <!-- Future dev options go here -->
</group>
</menu>
</item>
diff --git a/wallet/src/main/res/menu/exchange.xml b/wallet/src/main/res/menu/exchange.xml
index 85ec08f..1d2c2e5 100644
--- a/wallet/src/main/res/menu/exchange.xml
+++ b/wallet/src/main/res/menu/exchange.xml
@@ -18,4 +18,10 @@
<item
android:id="@+id/action_manual_withdrawal"
android:title="@string/exchange_menu_manual_withdraw" />
+ <item
+ android:id="@+id/action_receive_peer"
+ android:title="@string/receive_peer" />
+ <item
+ android:id="@+id/action_delete"
+ android:title="@string/transactions_delete" />
</menu>
diff --git a/wallet/src/main/res/menu/pending_operations.xml b/wallet/src/main/res/menu/exchange_list.xml
index b632021..21c028d 100644
--- a/wallet/src/main/res/menu/pending_operations.xml
+++ b/wallet/src/main/res/menu/exchange_list.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
+ ~ (C) 2024 Taler Systems S.A.
~
~ GNU Taler is free software; you can redistribute it and/or modify it under the
~ terms of the GNU General Public License as published by the Free Software
@@ -16,9 +16,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
+
<item
- android:id="@+id/retry_pending"
- android:orderInCategory="100"
- android:title="@string/menu_retry_pending_operations"
+ android:id="@+id/action_add_dev_exchanges"
+ android:title="@string/exchange_list_add_dev"
app:showAsAction="never" />
+
</menu>
diff --git a/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index ac94b34..0648fb4 100644
--- a/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> \ No newline at end of file
diff --git a/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index ac94b34..0648fb4 100644
--- a/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/wallet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> \ No newline at end of file
diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml
index e8929c9..a968365 100644
--- a/wallet/src/main/res/navigation/nav_graph.xml
+++ b/wallet/src/main/res/navigation/nav_graph.xml
@@ -26,9 +26,6 @@
android:label="@string/balances_title"
tools:layout="@layout/fragment_balances">
<action
- android:id="@+id/action_nav_main_to_promptPayment"
- app:destination="@id/promptPayment" />
- <action
android:id="@+id/action_nav_main_to_nav_transactions"
app:destination="@id/nav_transactions" />
<action
@@ -37,17 +34,53 @@
</fragment>
<fragment
+ android:id="@+id/receiveFunds"
+ android:name="net.taler.wallet.ReceiveFundsFragment"
+ android:label="@string/transactions_receive_funds">
+ <action
+ android:id="@+id/action_receiveFunds_to_nav_prompt_withdraw"
+ app:destination="@id/promptWithdraw" />
+ <action
+ android:id="@+id/action_receiveFunds_to_nav_peer_pull"
+ app:destination="@id/nav_peer_pull" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/sendFunds"
+ android:name="net.taler.wallet.SendFundsFragment"
+ android:label="@string/transactions_send_funds">
+ <action
+ android:id="@+id/action_sendFunds_to_nav_deposit"
+ app:destination="@id/nav_deposit" />
+ <action
+ android:id="@+id/action_sendFunds_to_nav_peer_push"
+ app:destination="@id/nav_peer_push" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/nav_payto_uri"
+ android:name="net.taler.wallet.deposit.PayToUriFragment"
+ android:label="@string/transactions_send_funds">
+ <argument
+ android:name="uri"
+ app:argType="string" />
+ <action
+ android:id="@+id/action_nav_payto_uri_to_nav_deposit"
+ app:destination="@id/nav_deposit" />
+ </fragment>
+
+ <fragment
android:id="@+id/promptPayment"
android:name="net.taler.wallet.payment.PromptPaymentFragment"
- android:label="Review Payment"
+ android:label="@string/payment_prompt_title"
tools:layout="@layout/fragment_prompt_payment">
<action
android:id="@+id/action_promptPayment_to_nav_main"
app:destination="@id/nav_main"
app:popUpTo="@id/nav_main" />
<action
- android:id="@+id/action_promptPayment_to_alreadyPaid"
- app:destination="@id/alreadyPaid"
+ android:id="@+id/action_promptPayment_to_nav_transactions_detail_payment"
+ app:destination="@id/nav_transactions_detail_payment"
app:popUpTo="@id/nav_main" />
</fragment>
@@ -58,9 +91,6 @@
<action
android:id="@+id/action_nav_settings_to_nav_settings_exchanges"
app:destination="@id/nav_settings_exchanges" />
- <action
- android:id="@+id/action_nav_settings_to_nav_settings_backup"
- app:destination="@id/nav_settings_backup" />
</fragment>
<fragment
@@ -74,53 +104,170 @@
<fragment
android:id="@+id/nav_exchange_manual_withdrawal"
- android:name="net.taler.wallet.withdraw.ManualWithdrawFragment"
+ android:name="net.taler.wallet.withdraw.manual.ManualWithdrawFragment"
android:label="@string/withdraw_title">
<action
android:id="@+id/action_nav_exchange_manual_withdrawal_to_promptWithdraw"
app:destination="@id/promptWithdraw" />
+ <argument
+ android:name="amount"
+ app:argType="string"
+ app:nullable="false" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/nav_exchange_manual_withdrawal_success"
+ android:name="net.taler.wallet.withdraw.manual.ManualWithdrawSuccessFragment"
+ android:label="@string/withdraw_title">
+ <action
+ android:id="@+id/action_nav_exchange_manual_withdrawal_success_to_nav_main"
+ app:destination="@id/nav_main"
+ app:popUpTo="@id/nav_main" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/nav_deposit"
+ android:name="net.taler.wallet.deposit.DepositFragment"
+ android:label="@string/send_deposit_title">
+ <argument
+ android:name="amount"
+ app:argType="string"
+ app:nullable="false" />
+ <argument
+ android:name="IBAN"
+ android:defaultValue="@null"
+ app:argType="string"
+ app:nullable="true" />
+ <argument
+ android:name="receiverName"
+ android:defaultValue="@null"
+ app:argType="string"
+ app:nullable="true" />
+ <action
+ android:id="@+id/action_nav_deposit_to_nav_main"
+ app:destination="@id/nav_main"
+ app:popUpTo="@id/nav_main" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/nav_peer_pull"
+ android:name="net.taler.wallet.peer.OutgoingPullFragment"
+ android:label="@string/receive_peer_title">
+ <argument
+ android:name="amount"
+ android:defaultValue="@null"
+ app:argType="string"
+ app:nullable="true" />
+ <action
+ android:id="@+id/action_nav_peer_pull_to_nav_main"
+ app:destination="@id/nav_main"
+ app:popUpTo="@id/nav_main" />
+ <action
+ android:id="@+id/action_nav_peer_pull_to_nav_transactions_detail_peer"
+ app:destination="@id/nav_transactions_detail_peer"
+ app:popUpTo="@id/nav_main" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/nav_peer_push"
+ android:name="net.taler.wallet.peer.OutgoingPushFragment"
+ android:label="@string/send_peer_title">
+ <argument
+ android:name="amount"
+ android:defaultValue="@null"
+ app:argType="string"
+ app:nullable="true" />
+ <action
+ android:id="@+id/action_nav_peer_push_to_nav_main"
+ app:destination="@id/nav_main"
+ app:popUpTo="@id/nav_main" />
+ <action
+ android:id="@+id/action_nav_peer_push_to_nav_transactions_detail_peer"
+ app:destination="@id/nav_transactions_detail_peer"
+ app:popUpTo="@id/nav_main" />
</fragment>
<fragment
- android:id="@+id/nav_settings_backup"
- android:name="net.taler.wallet.settings.BackupSettingsFragment"
- android:label="@string/nav_settings_backup"/>
+ android:id="@+id/promptPullPayment"
+ android:name="net.taler.wallet.peer.IncomingPullPaymentFragment"
+ android:label="@string/pay_peer_title">
+ <action
+ android:id="@+id/action_promptPullPayment_to_nav_main"
+ app:destination="@id/nav_main"
+ app:popUpTo="@id/nav_main" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/promptPushPayment"
+ android:name="net.taler.wallet.peer.IncomingPushPaymentFragment"
+ android:label="@string/receive_peer_payment_title">
+ <action
+ android:id="@+id/action_promptPushPayment_to_nav_main"
+ app:destination="@id/nav_main"
+ app:popUpTo="@id/nav_main" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/promptPayTemplate"
+ android:name="net.taler.wallet.payment.PayTemplateFragment"
+ android:label="@string/payment_pay_template_title">
+ <action
+ android:id="@+id/action_promptPayTemplate_to_promptPayment"
+ app:destination="@+id/promptPayment"
+ app:popUpTo="@id/nav_main" />
+ <argument
+ android:name="uri"
+ app:argType="string" />
+ </fragment>
<fragment
android:id="@+id/nav_transactions"
android:name="net.taler.wallet.transactions.TransactionsFragment"
android:label="@string/transactions_title"
- tools:layout="@layout/fragment_transactions" />
+ tools:layout="@layout/fragment_transactions">
+ <action
+ android:id="@+id/action_nav_transactions_to_nav_uri_input"
+ app:destination="@id/nav_uri_input" />
+ </fragment>
<fragment
android:id="@+id/nav_transactions_detail_withdrawal"
android:name="net.taler.wallet.transactions.TransactionWithdrawalFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_withdrawal" />
+ android:label="@string/transactions_detail_title">
+ <action
+ android:id="@+id/action_nav_transactions_detail_withdrawal_to_nav_exchange_manual_withdrawal_success"
+ app:destination="@id/nav_exchange_manual_withdrawal_success" />
+ </fragment>
<fragment
android:id="@+id/nav_transactions_detail_payment"
android:name="net.taler.wallet.transactions.TransactionPaymentFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_payment" />
+ android:label="@string/transactions_detail_title" />
<fragment
android:id="@+id/nav_transactions_detail_refund"
android:name="net.taler.wallet.transactions.TransactionRefundFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_payment" />
+ android:label="@string/transactions_detail_title" />
<fragment
android:id="@+id/nav_transactions_detail_refresh"
android:name="net.taler.wallet.transactions.TransactionRefreshFragment"
- android:label="@string/transactions_detail_title"
- tools:layout="@layout/fragment_transaction_withdrawal" />
+ android:label="@string/transactions_detail_title" />
+
+ <fragment
+ android:id="@+id/nav_transactions_detail_deposit"
+ android:name="net.taler.wallet.transactions.TransactionDepositFragment"
+ android:label="@string/transactions_detail_title" />
+
+ <fragment
+ android:id="@+id/nav_transactions_detail_peer"
+ android:name="net.taler.wallet.transactions.TransactionPeerFragment"
+ android:label="@string/transactions_detail_title" />
<fragment
- android:id="@+id/alreadyPaid"
- android:name="net.taler.wallet.payment.AlreadyPaidFragment"
- android:label="@string/payment_already_paid_title"
- tools:layout="@layout/fragment_already_paid" />
+ android:id="@+id/nav_transactions_detail_dummy"
+ android:name="net.taler.wallet.transactions.TransactionDummyFragment"
+ android:label="@string/transactions_detail_title" />
<fragment
android:id="@+id/promptWithdraw"
@@ -128,9 +275,6 @@
android:label="@string/nav_prompt_withdraw"
tools:layout="@layout/fragment_prompt_withdraw">
<action
- android:id="@+id/action_promptWithdraw_to_selectExchangeFragment"
- app:destination="@id/selectExchangeFragment" />
- <action
android:id="@+id/action_promptWithdraw_to_reviewExchangeTOS"
app:destination="@id/reviewExchangeTOS" />
<action
@@ -138,6 +282,14 @@
app:destination="@id/nav_main"
app:popUpTo="@id/nav_main" />
<action
+ android:id="@+id/action_promptWithdraw_to_nav_exchange_manual_withdrawal_success"
+ app:destination="@id/nav_exchange_manual_withdrawal_success"
+ app:popUpTo="@id/nav_main" />
+ <action
+ android:id="@+id/action_promptWithdraw_to_nav_transactions_detail_withdrawal"
+ app:destination="@id/nav_transactions_detail_withdrawal"
+ app:popUpTo="@id/nav_main" />
+ <action
android:id="@+id/action_promptWithdraw_to_errorFragment"
app:destination="@id/errorFragment"
app:popUpTo="@id/nav_main" />
@@ -153,17 +305,6 @@
app:destination="@id/promptWithdraw"
app:popUpTo="@id/nav_main" />
</fragment>
- <fragment
- android:id="@+id/selectExchangeFragment"
- android:name="net.taler.wallet.exchanges.SelectExchangeFragment"
- android:label="@string/nav_exchange_select"
- tools:layout="@layout/fragment_exchange_list" />
-
- <fragment
- android:id="@+id/nav_pending_operations"
- android:name="net.taler.wallet.pending.PendingOperationsFragment"
- android:label="@string/pending_operations_title"
- tools:layout="@layout/fragment_pending_operations" />
<fragment
android:id="@+id/nav_uri_input"
@@ -178,16 +319,36 @@
tools:layout="@layout/fragment_error" />
<action
+ android:id="@+id/action_global_receiveFunds"
+ app:destination="@id/receiveFunds" />
+
+ <action
+ android:id="@+id/action_global_sendFunds"
+ app:destination="@id/sendFunds" />
+
+ <action
android:id="@+id/action_global_promptWithdraw"
app:destination="@id/promptWithdraw" />
<action
+ android:id="@+id/action_global_manual_withdrawal"
+ app:destination="@id/nav_exchange_manual_withdrawal" />
+
+ <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" />
+ android:id="@+id/action_global_prompt_pull_payment"
+ app:destination="@id/promptPullPayment" />
+
+ <action
+ android:id="@+id/action_global_prompt_push_payment"
+ app:destination="@id/promptPushPayment" />
+
+ <action
+ android:id="@+id/action_global_prompt_pay_template"
+ app:destination="@id/promptPayTemplate" />
<action
android:id="@+id/action_nav_transactions_detail_withdrawal"
@@ -205,4 +366,12 @@
android:id="@+id/action_nav_transactions_detail_refresh"
app:destination="@id/nav_transactions_detail_refresh" />
+ <action
+ android:id="@+id/action_nav_transactions_detail_deposit"
+ app:destination="@id/nav_transactions_detail_deposit" />
+
+ <action
+ android:id="@+id/action_nav_payto_uri"
+ app:destination="@id/nav_payto_uri" />
+
</navigation>
diff --git a/wallet/src/main/res/values-ar/strings.xml b/wallet/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000..ef8cdde
--- /dev/null
+++ b/wallet/src/main/res/values-ar/strings.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="app_name">محفظة نظام Taler</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">محفظة نظام Taler (تجريبي)</string>
+ <string name="ok">موافق</string>
+ <string name="menu_settings">الإعدادات</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">يجعل نظام GNU Taler من معاملات الخصوصية سهلة الاستخدام عبر الإنترنت سريعة وميسرة.</string>
+ <string name="nav_header_title">نظام GNU Taler</string>
+ <string name="nav_header_subtitle">محفظة</string>
+ <string name="nav_prompt_withdraw">سحب النقود الرقمية</string>
+ <string name="nav_exchange_tos">شروط الخدمة لشركة الصرافة</string>
+ <string name="nav_exchange_select">اختر شركة الصرافة</string>
+ <string name="nav_exchange_fees">رسوم شركة الصرافة</string>
+ <string name="nav_error">خطأ</string>
+ <string name="button_back">الرجوع إلى السابق</string>
+ <string name="button_scan_qr_code">مسح رمز الاستجابة السريع لنظام Taler</string>
+ <string name="enter_uri">أدخل مُعرّف الموارد الموحد لنظام Taler</string>
+ <string name="paste">لصق</string>
+ <string name="paste_invalid">تحتوي الحافظة على نوع بيانات غير صحيحة</string>
+ <string name="uri_invalid">مُعرّف الموارد الموحد لنظام Taler غير صحيح</string>
+ <string name="cancel">إلغاء</string>
+ <string name="menu">القائمة</string>
+ <string name="search">البحث</string>
+ <string name="or">أو</string>
+ <string name="offline">تتطلب العملية الاتصال بالإنترنت. يرجى التأكد من أنّ اتصالك بالإنترنت يعمل وحاول مرة أخرى.</string>
+ <string name="error_unsupported_uri">خطأ: مُعرّف الموارد الموحد لنظام URI غير معتمد.</string>
+ <string name="menu_retry_pending_operations">إعادة العمليات المعلقة</string>
+ <string name="host_apdu_service_desc">مدفوعات الاتصال قريب المدى لنظام Taler</string>
+ <string name="balances_title">الأرصدة</string>
+ <string name="amount_positive">%s +</string>
+ <string name="amount_negative">%s -</string>
+ <string name="amount_chosen">المبلغ المختار</string>
+ <string name="balances_inbound_label">الوارد</string>
+ <string name="balances_empty_state">لا يوجد عملة رقمية في محفظتك.
+\n
+\nيمكنك فحص المال من المصرف التجريبي:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="transactions_title">المعاملات</string>
+ <string name="transactions_empty">ليس لديك معاملات</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">هذا التطبيق عبارة عن محفظة لنظام GNU Taler. وهو نظام تجريبي للغاية، وغير جاهز للاستخدام في بيئة الإنتاج لغاية الآن. ونظام GNU Taler هو نظام دفع يحافظ على الخصوصية. يمكن للعملاء عدم الكشف عن هويتهم، لكن لا يمكن للتجار إخفاء دخلهم من خلال المدفوعات باستخدام نظام GNU Taler. هذا يساعد على تجنب التهرب الضريبي وغسيل الأموال. حالة الاستخدام الرئيسية لنظام GNU Taler هي المدفوعات؛ لا يُقصد به أن يكون قيمة مختزنة. تُدعم المدفوعات دائمًا بعملة موجودة. يتم الدفع بعد تحويل الأموال الموجودة إلى نقود إلكترونية بمساعدة شركة الصرافة ، وهي مزود خدمة الدفع لنظام Taler. عند الدفع، يحتاج العملاء فقط إلى محفظة مشحونة. يمكن للتاجر قبول المدفوعات دون إرغام عملائه على التسجيل في موقع التاجر. كما أنّ نظام GNU Taler محصن من العديد من أنواع الاحتيال، مثل التصيد الاحتيالي لمعلومات بطاقة الائتمان أو الاحتيال في استرداد الأموال. وفي حالة الضياع أو السرقة ، قد تختفي فقط كمية محدودة من المال المتبقي في المحفظة.</string>
+ <string name="transactions_empty_search">لم يُعثر على معاملات. جرب بحثًا مختلفًا.</string>
+ <string name="transactions_error">تعذر تحميل المعاملات
+\n
+\n%s</string>
+ <string name="transactions_detail_title">معاملة</string>
+ <string name="transactions_detail_title_balance">الرصيد: %s</string>
+ <string name="transactions_delete">حذف</string>
+ <string name="transactions_select_all">تحديد الكل</string>
+ <string name="transaction_paid">مدفوع</string>
+ <string name="transaction_order_total">المجموع</string>
+ <string name="transaction_order">شراء</string>
+ <string name="transaction_order_id">إيصال #%1$s</string>
+ <string name="transaction_tip_from">إكرامية من %s</string>
+ <string name="transaction_refund">استرداد المال</string>
+ <string name="transaction_refund_from">استرداد الأموال من %s</string>
+ <string name="transaction_pending">معلق</string>
+ <string name="transaction_refresh">رسوم تغيير انتهاء صلاحية العملة</string>
+ <string name="payment_title">دفع</string>
+ <string name="payment_fee">رسوم الدفع +%s</string>
+ <string name="payment_label_order_summary">شراء</string>
+ <string name="payment_error">خطأ: %s</string>
+ <string name="payment_balance_insufficient">رصيد غير كافٍ!</string>
+ <string name="payment_aborted">ملغي</string>
+ <string name="payment_failed">إخفاق</string>
+ <string name="payment_initiated">بدء الدفع</string>
+ <string name="payment_already_paid_title">مدفوع الآن</string>
+ <string name="withdraw_initiated">لقد بدأ السحب</string>
+ <string name="withdraw_title">السحب</string>
+ <string name="withdraw_total">سحب النقود</string>
+ <string name="withdraw_fees">رسوم</string>
+ <string name="withdraw_exchange">شركة الصرافة</string>
+ <string name="withdraw_button_confirm">تأكيد السحب</string>
+ <string name="withdraw_button_confirm_bank">التأكيد مع البنك</string>
+ <string name="withdraw_button_tos">مراجعة الشروط</string>
+ <string name="withdraw_manual_title">حوّل تحويلاً يدويًا لشركة الصرافة</string>
+ <string name="withdraw_amount">ما مقدار المال الذي تريد سحبه؟</string>
+ <string name="withdraw_manual_payment_options">خياراتُ الدفع معتمدة من %1$s:
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">تحقق من الرسوم</string>
+ <string name="withdraw_error_title">خطأ في السحب</string>
+ <string name="withdraw_error_test">خطأ في سحب TESTKUDOS</string>
+ <string name="exchange_settings_title">شركات الصرافة</string>
+ <string name="exchange_settings_summary">أدر قائمة شركات الصرافة المعروفة لهذه المحفظة</string>
+ <string name="exchange_list_title">شركات الصرافة</string>
+ <string name="exchange_list_currency">العملة: %s</string>
+ <string name="exchange_list_add">أضف شركة صرافة</string>
+ <string name="exchange_add_url">أدخل عنوان شركة الصرافة</string>
+ <string name="exchange_add_error">تعذر إضافة شركة الصرافة</string>
+ <string name="exchange_menu_manual_withdraw">سحب</string>
+ <string name="exchange_fee_withdrawal_fee_label">رسوم السحب:</string>
+ <string name="exchange_fee_overhead_label">خسارة تقريب الأرقام:</string>
+ <string name="exchange_fee_coin_fees_label">رسوم العملة</string>
+ <string name="exchange_fee_wire_fees_label">رسوم الحوالة</string>
+ <string name="exchange_fee_withdraw_fee">رسوم السحب: %s</string>
+ <string name="exchange_fee_deposit_fee">رسوم الإيداع: %s</string>
+ <string name="exchange_fee_refresh_fee">رسوم التحويل: %s</string>
+ <string name="exchange_fee_refund_fee">رسوم استرداد الأموال: %s</string>
+ <string name="exchange_fee_wire_fee_timespan">الفترة الزمنية: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">رسوم الحوالة: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">رسوم العملية المصرفية: %s</string>
+ <string name="exchange_tos_accept">الموافقة على شروط الخدمة</string>
+ <string name="pending_operations_title">عمليات معلقة</string>
+ <string name="pending_operations_refuse">رفض العرض</string>
+ <string name="pending_operations_no_action">(لا تفعل شيئًا)</string>
+ <string name="settings_dev_mode">وضع المُطوّر</string>
+ <string name="settings_withdraw_testkudos">سحب TESTKUDOS</string>
+ <string name="settings_withdraw_testkudos_summary">احصل على المال للاختبار</string>
+ <string name="settings_logcat">سجل تصحيح الأخطاء</string>
+ <string name="settings_version_app">نسخة التطبيق</string>
+ <string name="settings_version_core">النسخة الرئيسية للمحفظة</string>
+ <string name="settings_version_protocol_exchange">إصدارات شركات الصرافة المعتمدة</string>
+ <string name="settings_version_unknown">غير معروف</string>
+ <string name="settings_reset">إعادة تعيين المحفظة (خطر!)</string>
+ <string name="settings_reset_summary">يسحب أموالك</string>
+ <string name="nav_settings_backup">نسخ احتياطي</string>
+ <string name="backup_last">النسخة الاحتياطية الأخيرة: %s</string>
+ <string name="refund_title">استرداد الأموال</string>
+ <string name="refund_success">استُلم المبلغ المسترد: %s</string>
+ <string name="tip_title">نصيحة</string>
+ <string name="wifi_disabled_error">شغّل شبكة Wi-Fi للحصول على خدمة Wi-Fi مجانية</string>
+ <string name="payment_button_confirm">تأكيد الدفع</string>
+ <string name="payment_label_amount_total">المبلغ الكلي:</string>
+ <string name="payment_already_paid">لقد دفعت الآن لعملية الشراء.</string>
+ <string name="withdraw_waiting_confirm">بانتظار الموافقة</string>
+ <string name="withdraw_amount_error">أدخل مبلغًا صحيحًا</string>
+ <string name="withdraw_error_message">لا يمكن تنفيذ السحب الآن. يُرجى المحاولة فيما بعد!</string>
+ <string name="exchange_list_empty">لا توجد شركات صرافة معروفة
+\n
+\nأضف واحدة يدويًا أو اسحب العملة الرقمية!</string>
+ <string name="exchange_fee_coin_expiration_label">أقرب تاريخ انتهاء سريان العملة:</string>
+ <plurals name="exchange_fee_coin">
+ <item quantity="zero">العملة: %s (مستخدمة %d مرة)</item>
+ <item quantity="one">العملة: %s (مستخدمة %d مرة)</item>
+ <item quantity="two">العملتان: %s (المستخدمتين %d مرة)</item>
+ <item quantity="few">العملات: %s (مستخدمة %d مرات)</item>
+ <item quantity="many">العملات: %s (مستخدمة %d مرات)</item>
+ <item quantity="other">العملات: %s (مستخدمة %d مرات)</item>
+ </plurals>
+ <string name="exchange_tos_error">خطأ في عرض شروط الخدمة: %s</string>
+ <string name="settings_dev_mode_summary">يظهر المزيد من المعلومات المعدة لتصحيحها</string>
+ <string name="settings_logcat_summary">معاينة/إرسال السجل الداخلي</string>
+ <string name="settings_version_protocol_merchant">إصدارات التجار المعتمدين</string>
+ <string name="refund_error">خطأ في تنفيذ استرداد الأموال</string>
+ <string name="wifi_connect_error">تعذر الاتصال بشبكة Wi-Fi: %s</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-ca/strings.xml b/wallet/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000..591820b
--- /dev/null
+++ b/wallet/src/main/res/values-ca/strings.xml
@@ -0,0 +1,265 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2020 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ --><resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="app_name">Cartera Taler</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">Cartera Taler (experimental)</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler et permet fer transaccions privades, fàcil i ràpidament.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Aquesta aplicació és una cartera per al GNU Taler. És altament experimental i encara no està llesta per a l\'ús quotidià seriós. GNUTaler és un sistema de pagament respectuós amb la privadesa. Els clients es poden mantenir anònims, però els comerciants no poden amagar els seus ingressos fets amb GNU Taler. Això ajuda a evitar l\'evasió d\'impostos i el blanqueig de diners. L\'ús principal de GNU Taler és fer pagaments; no com a bé de refugi o d\'especulació. Els pagaments sempre estan recolzats per una divisa ja existent. Els pagaments es fan després de canviar diners existents per diners digitals amb l\'ajuda d\'un servei d\'intercanvi; és a dir, un proveïdor de serveis de pagament per al Taler. Per a fer un pagament, el client només necessita una cartera carregada de diners. El comerciant pot acceptar els pagaments sense fer que el client es registri al seu lloc web. El GNU Taler és immune a molts tipus de frau, com el «phishing» de dades bancàries o el frau de devolució. En cas de pèrdua o robament, només la quantitat limitada de diners que quedin a la cartera podrien ser perduts.</string>
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="nav_header_subtitle">Cartera</string>
+ <string name="nav_prompt_withdraw">Retira diners digitals</string>
+ <string name="nav_exchange_tos">Termes de servei de l\'intercanvi</string>
+ <string name="nav_exchange_select">Selecciona l\'intercanvi</string>
+ <string name="nav_exchange_fees">Comissions d\'intercanvi</string>
+ <string name="nav_error">Error</string>
+ <string name="button_back">Ves enrere</string>
+ <string name="button_scan_qr_code">Escaneja el codi QR de Taler</string>
+ <string name="enter_uri">Entra l\'URI de Taler</string>
+ <string name="copy" tools:override="true">Copia</string>
+ <string name="copy_uri">Copia l\'URI de Taler</string>
+ <string name="paste">Enganxa</string>
+ <string name="paste_invalid">El portaretalls conté un tipus invàlid de dada</string>
+ <string name="uri_invalid">No és una URI de Taler vàlida</string>
+ <string name="ok">D\'acord</string>
+ <string name="cancel">Cancel·la</string>
+ <string name="search">Cerca</string>
+ <string name="menu">Menú</string>
+ <string name="or">o bé</string>
+ <string name="currency">Divisa</string>
+ <!-- The count should be mirrored in RTL languages -->
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="offline">L\'operació requereix accés a internet. Si us plau, assegura\'t que la connexió a internet funciona i torna a intentar-ho.</string>
+ <string name="error_unsupported_uri">Error: Aquesta URI de Taler no està suportada.</string>
+ <string name="menu_settings">Paràmetres</string>
+ <string name="menu_retry_pending_operations">Reintenta les operacions pendents</string>
+ <string name="host_apdu_service_desc">Pagaments de Taler per NFC</string>
+ <string name="balances_title">Saldos</string>
+ <string name="amount_positive">+%s</string>
+ <string name="amount_negative">-%s</string>
+ <string name="amount_chosen">Quantitat escollida</string>
+ <string name="balances_inbound_label">entrant</string>
+ <string name="balances_empty_state">No hi ha diners digitals a la teva cartera.
+\n
+\nPots aconseguir diners de prova del banc de demostració:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="transactions_title">Transaccions</string>
+ <string name="transactions_balance">Saldo</string>
+ <string name="transactions_send_funds">Envia
+\nFons</string>
+ <string name="transactions_receive_funds">Rep
+\nFons</string>
+ <string name="transactions_empty">No tens cap transacció</string>
+ <string name="transactions_empty_search">No s\'han trobat transaccions. Prova una cerca diferent.</string>
+ <string name="transactions_error">No s\'han pogut carregar transaccions
+\n
+\n%s</string>
+ <string name="transactions_detail_title">Transacció</string>
+ <string name="transactions_detail_title_currency">%s Transaccions</string>
+ <string name="transactions_delete">Esborrar</string>
+ <string name="transactions_select_all">Seleccionar-ho tot</string>
+ <string name="transactions_delete_dialog_title">Esborrar la transacció</string>
+ <string name="transactions_delete_dialog_message">Estàs segur que vols eliminar aquesta transacció de la teva cartera\?</string>
+ <string name="transactions_delete_selected_dialog_message">Estàs segur que vols eliminar les transaccions seleccionades de la teva cartera\?</string>
+ <string name="transactions_cancel_dialog_message">Estàs segur que vols cancel·lar aquesta retirada\? Podries perdre els diners que encara estiguin en trànsit.</string>
+ <!-- Transactions -->
+ <string name="transaction_paid">Pagat</string>
+ <string name="transaction_order_total">Total</string>
+ <string name="transaction_order">Compra</string>
+ <string name="transaction_order_id">Rebut #%1$s</string>
+ <string name="transaction_tip_from">Recompensa de %s</string>
+ <string name="transaction_refund">Reemborsament</string>
+ <string name="transaction_refund_from">Reemborsament de %s</string>
+ <string name="transaction_pending">PENDENT</string>
+ <string name="transaction_refresh">Comissió de canvi de venciment de monedes</string>
+ <string name="transaction_deposit">Dipòsit</string>
+ <string name="transaction_peer_push_debit">Pagament anticipat</string>
+ <string name="transaction_peer_pull_credit">Factura</string>
+ <string name="transaction_peer_pull_debit">Factura pagada</string>
+ <string name="transaction_peer_push_credit">Pagament anticipat</string>
+ <string name="transaction_action_kyc">Completa KYC (política \'Know Your Customer\')</string>
+ <string name="payment_title">Pagament</string>
+ <string name="payment_fee">+%s comissió de pagament</string>
+ <string name="payment_button_confirm">Confirma el pagament</string>
+ <string name="payment_label_amount_total">Quantitat total:</string>
+ <string name="payment_label_order_summary">Compra</string>
+ <string name="payment_error">Error: %s</string>
+ <string name="payment_balance_insufficient">Saldo insuficient!</string>
+ <string name="payment_aborted">Avortat</string>
+ <string name="payment_failed">Ha fallat</string>
+ <string name="payment_initiated">Pagament començat</string>
+ <string name="payment_already_paid_title">Ja pagat</string>
+ <string name="payment_already_paid">Ja has pagat aquesta compra.</string>
+ <string name="receive_amount">Quantitat a rebre</string>
+ <string name="receive_amount_invalid">Quantitat invàlida</string>
+ <string name="receive_intro">Tria d\'on rebre els diners:</string>
+ <string name="receive_withdraw">Retirar diners d\'un compte bancari</string>
+ <string name="receive_peer">Factura una altra cartera</string>
+ <string name="receive_peer_title">Sol·licitud de pagament</string>
+ <string name="receive_peer_create_button">Crea factura</string>
+ <string name="receive_peer_invoice_instruction">Fes que el client escanegi aquest codi QR per pagar:</string>
+ <string name="receive_peer_invoice_uri">Alternativament, copia i envia aquesta URI:</string>
+ <string name="send_amount">Quantitat a enviar</string>
+ <string name="send_intro">Tria on vols enviar els diners:</string>
+ <string name="send_deposit">A un compte bancari</string>
+ <string name="send_deposit_bitcoin">A una cartera de Bitcoin</string>
+ <string name="send_deposit_title">Deposita a un compte bancari</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="send_deposit_name">Titular del compte</string>
+ <string name="send_deposit_bitcoin_address">Adreça de Bitcoin</string>
+ <string name="send_deposit_check_fees_button">Despeses de comprovació</string>
+ <string name="send_deposit_amount_effective">Quantitat efectiva</string>
+ <string name="send_deposit_create_button">Fes un dipòsit</string>
+ <string name="send_deposit_bitcoin_create_button">Transferir Bitcoin</string>
+ <string name="send_peer">A una altra cartera</string>
+ <string name="send_peer_bitcoin">A una altra cartera de Taler</string>
+ <string name="send_peer_title">Envia diners a una altra cartera</string>
+ <string name="send_peer_create_button">Envia diners ara</string>
+ <string name="send_peer_warning">Avís: Els diners sortiran de la cartera immediatament.</string>
+ <string name="send_peer_payment_instruction">Fes que el beneficiari escanegi aquest codi QR per rebre:</string>
+ <string name="send_peer_payment_amount_received">Quantitat rebuda</string>
+ <string name="send_peer_payment_amount_sent">Quantitat enviada</string>
+ <string name="send_peer_expiration_period">Expira en</string>
+ <string name="send_peer_expiration_1d">1 dia</string>
+ <string name="send_peer_expiration_7d">7 dies</string>
+ <string name="send_peer_expiration_30d">30 dies</string>
+ <string name="send_peer_expiration_custom">Personalitzat</string>
+ <string name="send_peer_expiration_days">Dies</string>
+ <string name="send_peer_expiration_hours">Hores</string>
+ <string name="send_peer_purpose">Propòsit</string>
+ <string name="pay_peer_title">Factura de pagament</string>
+ <string name="pay_peer_intro">Vols pagar aquesta factura\?</string>
+ <string name="receive_peer_payment_title">Rebre un pagament</string>
+ <string name="receive_peer_payment_intro">Vols rebre aquest pagament\?</string>
+ <string name="withdraw_initiated">Retirada iniciada</string>
+ <string name="withdraw_title">Retirada</string>
+ <string name="withdraw_total">Retirar</string>
+ <string name="withdraw_fees">Comissió</string>
+ <string name="withdraw_restrict_age">Restringeix l\'ús a l\'edat de</string>
+ <string name="withdraw_restrict_age_unrestricted">No restringit</string>
+ <string name="withdraw_exchange">Intercanvi</string>
+ <string name="withdraw_button_confirm">Confirmar retirada de diners</string>
+ <string name="withdraw_button_confirm_bank">Confirma amb el banc</string>
+ <string name="withdraw_button_tos">Comprova els termes</string>
+ <string name="withdraw_waiting_confirm">Esperant confirmació</string>
+ <string name="withdraw_manual_title">Fes una transferència manual a l\'intercanvi</string>
+ <string name="withdraw_amount">Quant vols retirar\?</string>
+ <string name="withdraw_amount_error">Insereix una quantitat vàlida</string>
+ <string name="withdraw_manual_payment_options">Les opcions de pagament estan suportades per %1$s:
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">Comprova la comissió</string>
+ <string name="withdraw_manual_ready_title">L\'intercanvi està llest perquè retiris els diners!</string>
+ <string name="withdraw_manual_ready_intro">Per completar el procés necessites transferir %s al teu compte bancari</string>
+ <string name="withdraw_manual_ready_details_intro">Detalls de transferència bancària</string>
+ <string name="withdraw_manual_bitcoin_title">L\'intercanvi de Bitcoin està llest perquè retiris els dieners</string>
+ <string name="withdraw_manual_bitcoin_intro">Ara fes una transacció dividida amb aquestes tres sortides.</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_subject">Concepte</string>
+ <string name="withdraw_manual_ready_bank_button">Obre en una app bancària</string>
+ <string name="withdraw_manual_ready_cancel">Cancel·la la retidada de diners</string>
+ <string name="withdraw_manual_ready_warning">Assegura\'t d\'emprar el concepte correcte, del contrari els diners no arribaran a la cartera.</string>
+ <string name="withdraw_error_title">Error retirant els diners</string>
+ <string name="withdraw_error_message">Retirar diners no és possible ara mateix. Si us plau, intenta-ho més tard!</string>
+ <string name="withdraw_error_test">Error retirant TESTKUDOS</string>
+ <string name="exchange_settings_title">Canvis</string>
+ <string name="exchange_settings_summary">Gestionar la llista de canvis coneguts per a aquesta cartera</string>
+ <string name="exchange_list_title">Canvis</string>
+ <string name="exchange_list_empty">No es coneixen canvis
+\n
+\nAfegeix-ne un manualment o retira diners digitals!</string>
+ <string name="exchange_list_currency">Divisa: %s</string>
+ <string name="exchange_list_add">Afegeix un canvi</string>
+ <string name="exchange_add_url">Afegeix adreça de canvi</string>
+ <string name="exchange_add_error">No s\'ha pogut afegir el canvi</string>
+ <string name="exchange_menu_manual_withdraw">Retirar</string>
+ <string name="exchange_fee_withdrawal_fee_label">Comissió de retirament:</string>
+ <string name="exchange_fee_overhead_label"/>
+ <string name="exchange_fee_coin_expiration_label"/>
+ <string name="exchange_fee_coin_fees_label"/>
+ <string name="exchange_fee_wire_fees_label"/>
+ <plurals name="exchange_fee_coin">
+ <item quantity="one"/>
+ <item quantity="other"/>
+ </plurals>
+ <string name="exchange_fee_withdraw_fee"/>
+ <string name="exchange_fee_deposit_fee"/>
+ <string name="exchange_fee_refresh_fee"/>
+ <string name="exchange_fee_refund_fee"/>
+ <string name="exchange_fee_wire_fee_timespan"/>
+ <string name="exchange_fee_wire_fee_wire_fee"/>
+ <string name="exchange_fee_wire_fee_closing_fee"/>
+ <string name="exchange_tos_accept"/>
+ <string name="exchange_tos_error"/>
+ <string name="pending_operations_title"/>
+ <string name="pending_operations_refuse"/>
+ <string name="pending_operations_no_action"/>
+ <string name="settings_dev_mode"/>
+ <string name="settings_dev_mode_summary"/>
+ <string name="settings_withdraw_testkudos"/>
+ <string name="settings_withdraw_testkudos_summary"/>
+ <string name="settings_logcat"/>
+ <string name="settings_logcat_summary"/>
+ <string name="settings_logcat_error"/>
+ <string name="settings_logcat_success"/>
+ <string name="settings_db_export"/>
+ <string name="settings_db_export_summary"/>
+ <string name="settings_db_export_error"/>
+ <string name="settings_db_export_success"/>
+ <string name="settings_version_app"/>
+ <string name="settings_version_core"/>
+ <string name="settings_version_protocol_exchange"/>
+ <string name="settings_version_protocol_merchant"/>
+ <string name="settings_version_unknown"/>
+ <string name="settings_reset"/>
+ <string name="settings_reset_summary"/>
+ <string name="nav_settings_backup"/>
+ <string name="backup_last"/>
+ <string name="refund_title"/>
+ <string name="refund_error"/>
+ <string name="refund_success"/>
+ <string name="tip_title"/>
+ <string name="tip_already_accepted"/>
+ <string name="tip_total"/>
+ <string name="tip_fees"/>
+ <string name="tip_exchange"/>
+ <string name="tip_merchant_url"/>
+ <string name="tip_button_confirm"/>
+ <string name="tip_received"/>
+ <string name="wifi_disabled_error"/>
+ <string name="wifi_connect_error"/>
+ <string name="transaction_dummy_title">Transacció desconeguda</string>
+ <string name="send_deposit_iban_error">L\'IBAN és invàlid</string>
+ <string name="send_deposit_bic_error">El BIC és invàlid</string>
+ <string name="button_scan_qr_code_label">Escaneja el codi QR</string>
+ <string name="payment_prompt_title">Revisar pagament</string>
+ <string name="payment_pay_template_title">Personalitza la teva comanda</string>
+ <string name="payment_create_order">Crea una comanda</string>
+ <string name="payment_confirmation_code">Codi de confirmació</string>
+ <string name="payment_template_error">Error creant la comanda</string>
+ <string name="transactions_retry">Reintentar</string>
+ <string name="transactions_abort">Avortar</string>
+ <string name="transactions_fail">Matar</string>
+ <string name="transactions_suspend">Suspendre</string>
+ <string name="transactions_fail_dialog_title">Matar transacció</string>
+ <string name="transactions_fail_dialog_message">Estàs segur que vols matar aquesta transacció\? Els diners en transit ES PERDRAN.</string>
+ <string name="transactions_resume">Continuar</string>
+ <string name="transactions_abort_dialog_title">Avortar transacció</string>
+ <string name="transactions_abort_dialog_message">Estàs segur que vols avortar aquesta transacció\? Els diners en trànsit es podrien perdre.</string>
+ <string name="offline_banner">Ni hi ha accés a internet</string>
+ <string name="exchange_list_error">No s\'han pogut llistar serveis de canvi</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-de/strings.xml b/wallet/src/main/res/values-de/strings.xml
index a6b3dae..5ea98e9 100644
--- a/wallet/src/main/res/values-de/strings.xml
+++ b/wallet/src/main/res/values-de/strings.xml
@@ -1,2 +1,266 @@
<?xml version="1.0" encoding="utf-8"?>
-<resources></resources> \ No newline at end of file
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="amount_chosen">Gewählter Betrag</string>
+ <string name="settings_version_app">App-Version</string>
+ <string name="host_apdu_service_desc">Taler-NFC-Zahlungen</string>
+ <string name="exchange_settings_title">Zahlungsdienstleister</string>
+ <string name="balances_inbound_label">Eingehend</string>
+ <string name="or">oder</string>
+ <string name="offline">Internetzugang erforderlich. Bitte stellen Sie sicher, dass Ihre Internetverbindung funktioniert, und versuchen Sie es noch einmal.</string>
+ <string name="error_unsupported_uri">Fehler: Dieser Taler-URI wird nicht unterstützt.</string>
+ <string name="menu_settings">Einstellungen</string>
+ <string name="menu_retry_pending_operations">Ausstehende Vorgänge wiederholen</string>
+ <string name="balances_title">Salden</string>
+ <string name="app_name">Taler Wallet</string>
+ <string name="transactions_title">Transaktionen</string>
+ <string name="transactions_empty">Sie haben keine Transaktionen</string>
+ <string name="transactions_empty_search">Keine Transaktion gefunden. Versuchen Sie eine andere Suche.</string>
+ <string name="transactions_detail_title">Transaktion</string>
+ <string name="transactions_detail_title_balance">Saldo: %s</string>
+ <string name="transactions_delete">Löschen</string>
+ <string name="transactions_select_all">Alle auswählen</string>
+ <string name="transaction_paid">Bezahlt</string>
+ <string name="transaction_order_total">Summe</string>
+ <string name="transaction_order">Einkauf</string>
+ <string name="transaction_order_id">Beleg #%1$s</string>
+ <string name="transaction_refund">Erstattung</string>
+ <string name="transaction_refund_from">Rückerstattung von %s</string>
+ <string name="transaction_pending">AUSSTEHEND</string>
+ <string name="transaction_refresh">Änderungsgebühr für das Ablaufdatum der Münze</string>
+ <string name="payment_title">Zahlung</string>
+ <string name="payment_button_confirm">Zahlung bestätigen</string>
+ <string name="payment_label_amount_total">Gesamtbetrag:</string>
+ <string name="payment_label_order_summary">Einkauf</string>
+ <string name="payment_error">Fehler: %s</string>
+ <string name="payment_balance_insufficient">Saldo unzureichend!</string>
+ <string name="payment_aborted">Abgebrochen</string>
+ <string name="payment_failed">Fehlgeschlagen</string>
+ <string name="payment_initiated">Zahlung eingeleitet</string>
+ <string name="payment_already_paid_title">Bereits bezahlt</string>
+ <string name="payment_already_paid">Sie haben diesen Kauf bereits bezahlt.</string>
+ <string name="withdraw_initiated">Auszahlung eingeleitet</string>
+ <string name="withdraw_title">Abhebung</string>
+ <string name="withdraw_total">Abheben</string>
+ <string name="withdraw_fees">Gebühr</string>
+ <string name="withdraw_exchange">Zahlungsdienstleister</string>
+ <string name="withdraw_button_confirm">Abhebung bestätigen</string>
+ <string name="withdraw_button_confirm_bank">Bestätigen mit</string>
+ <string name="withdraw_button_tos">Nutzungsbedingungen überprüfen</string>
+ <string name="withdraw_waiting_confirm">Warte auf Bestätigung</string>
+ <string name="withdraw_manual_title">Manuelle Überweisung an den Zahlungsdienstleister</string>
+ <string name="withdraw_amount">Wie viel soll abgehoben werden\?</string>
+ <string name="withdraw_amount_error">Gültigen Betrag eingeben</string>
+ <string name="withdraw_manual_payment_options">Zahlungsoptionen unterstützt von %1$s:
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">Überprüfen Sie die Gebühren</string>
+ <string name="withdraw_error_title">Auszahlungsfehler</string>
+ <string name="withdraw_error_message">Ein Abheben ist derzeit nicht möglich. Bitte zu einem späteren Zeitpunkt erneut versuchen!</string>
+ <string name="withdraw_error_test">Fehler beim Abheben von TESTKUDOS aufgetreten</string>
+ <string name="exchange_settings_summary">Verwaltung der Zahlungsdienstleister, die diesem Wallet bekannt sind</string>
+ <string name="payment_fee">+%s Zahlungsgebühr</string>
+ <string name="exchange_list_title">Liste von Zahlungsdienstleistern</string>
+ <string name="exchange_list_empty">Keine Zahlungsdienstleister bekannt
+\n
+\nFügen Sie einen Zahlungsdienstleister manuell hinzu oder heben Sie digitales Bargeld ab!</string>
+ <string name="exchange_list_currency">Währung: %s</string>
+ <string name="exchange_list_add">Zahlungsdienstleister hinzufügen</string>
+ <string name="exchange_add_error">Zahlungsdienstleister konnte nicht hinzugefügt werden</string>
+ <string name="exchange_menu_manual_withdraw">Abheben</string>
+ <string name="exchange_fee_withdrawal_fee_label">Abhebegebühr:</string>
+ <string name="exchange_fee_overhead_label">Rundungsverlust:</string>
+ <string name="exchange_fee_coin_expiration_label">Frühestes Gültigkeitsende:</string>
+ <string name="exchange_fee_coin_fees_label">Münzgebühr</string>
+ <string name="exchange_fee_wire_fees_label">Überweisungsgebühr</string>
+ <plurals name="exchange_fee_coin">
+ <item quantity="one">Münze: %s (verwendet %d Mal)</item>
+ <item quantity="other">Münze: %s (verwendet %d Male)</item>
+ </plurals>
+ <string name="exchange_fee_withdraw_fee">Abhebegebühr: %s</string>
+ <string name="exchange_fee_deposit_fee">Einlösegebühr: %s</string>
+ <string name="exchange_fee_refresh_fee">Wechselgebühr: %s</string>
+ <string name="exchange_fee_refund_fee">Rückerstattungsgebühr: %s</string>
+ <string name="exchange_fee_wire_fee_timespan">Zeitraum: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Überweisungsgebühr: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Rücküberweisungsgebühr: %s</string>
+ <string name="exchange_tos_accept">Allgemeine Geschäftsbedingungen akzeptieren</string>
+ <string name="pending_operations_title">Schwebende Vorgänge</string>
+ <string name="pending_operations_refuse">Vorschlag ablehnen</string>
+ <string name="pending_operations_no_action">(keine Aktion)</string>
+ <string name="settings_dev_mode">Entwicklermodus</string>
+ <string name="settings_dev_mode_summary">Zeigt weitere Informationen zum Debuggen an</string>
+ <string name="settings_withdraw_testkudos">TESTKUDOS abheben</string>
+ <string name="settings_withdraw_testkudos_summary">Geld für Testen bereitstellen</string>
+ <string name="settings_logcat">Fehlerprotokoll</string>
+ <string name="settings_logcat_summary">Internes Protokoll speichern</string>
+ <string name="settings_version_core">Wallet Core-Version</string>
+ <string name="settings_version_protocol_merchant">Unterstützte Händler-Versionen</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler ermöglicht schnelle und einfache Online-Transaktionen mit Datenschutz.</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">Taler-Wallet (experimental)</string>
+ <string name="settings_version_unknown">Unbekannt</string>
+ <string name="settings_reset">Brieftasche zurücksetzen (gefährlich!)</string>
+ <string name="settings_reset_summary">Wirft dein Geld weg</string>
+ <string name="nav_settings_backup">Sicherung (Backup)</string>
+ <string name="backup_last">Letzte Sicherung: %s</string>
+ <string name="refund_title">Erstattung</string>
+ <string name="refund_error">Fehler bei der Verarbeitung der Rückerstattung</string>
+ <string name="refund_success">Rückerstattung erhalten!</string>
+ <string name="tip_title">Trinkgeld</string>
+ <string name="wifi_disabled_error">Schalten Sie Wi-Fi ein, um kostenloses Wi-Fi zu erhalten</string>
+ <string name="wifi_connect_error">Es konnte keine Verbindung zu kostenlosem WLAN hergestellt werden: %s</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Diese App ist eine Geldbörse für GNU Taler (Wallet). Taler ist ein Bezahlsystem mit Datenschutz für die Käufer. Die Käufer bleiben anonym, das Einkommen der Händler hingegen ist offengelegt. Dies vermindert systematisch die Möglichkeit von Steuerhinterziehung und Geldwäsche. Die Hauptfunktion von GNU Taler ist die Abwicklung von Zahlungsvorgängen; es ist kein Mittel zur Geldaufbewahrung. Alle Zahlungen werden durch eine existierende Währung gedeckt. Zahlungen werden mit Hilfe eines Taler-Zahlungsdiensts (Exchange) durchgeführt, indem die Käufer Geld an den Zahlungsdienst überweisen und dieser elektronische Zahlungsmittel (Coins, Token) in die Wallets überträgt. Um eine Zahlung durchzuführen, benötigen Kunden ausreichende Coin-Werte im Wallet. Verkäufer können Zahlungen akzeptieren, ohne Kunden zur Registrierung auf Webseiten zwingen zu müssen. GNU Taler ist immun gegen viele Formen von Betrug wie z.B. Phishing von Kreditkartendaten oder Betrug durch Rückbuchungen. Im Fall von Verlust oder Diebstahl von Wallets verliert man höchstens die Werte, welche sich noch im Wallet befinden.</string>
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="nav_header_subtitle">Wallet</string>
+ <string name="nav_prompt_withdraw">Digitales Bargeld abheben</string>
+ <string name="nav_exchange_tos">Allgemeine Geschäftsbedingungen des Zahlungsdienstleisters</string>
+ <string name="nav_exchange_select">Zahlungsdienstleister auswählen</string>
+ <string name="nav_exchange_fees">Gebühren des Zahlungsdienstleisters</string>
+ <string name="enter_uri">Geben Sie die Taler-URI ein</string>
+ <string name="paste">Einfügen</string>
+ <string name="paste_invalid">Die Zwischenablage enthält einen ungültigen Datentyp</string>
+ <string name="uri_invalid">Keine gültige Taler-URI</string>
+ <string name="ok">Bestätigen</string>
+ <string name="cancel">Abbrechen</string>
+ <string name="search">Suche</string>
+ <string name="menu">Menü</string>
+ <string name="nav_error">Fehler</string>
+ <string name="balances_empty_state">In Ihrem Wallet befindet sich kein digitales Geld.
+\n
+\nSie können Testgeld von der Demo-Bank erhalten:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="transactions_error">Transaktionen konnten nicht geladen werden
+\n
+\n%s</string>
+ <string name="exchange_tos_error">Fehler beim Anzeigen der AGB: %s</string>
+ <string name="settings_version_protocol_exchange">Unterstützte Exchange-Versionen</string>
+ <string name="amount_negative">- %s</string>
+ <string name="amount_positive">+%s</string>
+ <string name="transaction_tip_from">Trinkgeld von %s</string>
+ <string name="exchange_add_url">Adresse des Zahlungsdienstleisters eingeben</string>
+ <string name="button_back">Zurück</string>
+ <string name="button_scan_qr_code">Taler-QR-Code einscannen</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_title">Der Zahlungsdienstleister ist für den Abhebevorgang bereit!</string>
+ <string name="withdraw_manual_ready_intro">Zum Abschluss des Abhebevorgangs müssen Sie %s auf das Bankkonto des Zahlungsdienstleisters überweisen</string>
+ <string name="withdraw_manual_ready_details_intro">Details der Überweisung</string>
+ <string name="withdraw_manual_ready_subject">Überweisungsvermerk</string>
+ <string name="withdraw_manual_ready_bank_button">In Banking-App öffnen</string>
+ <string name="withdraw_manual_ready_cancel">Abhebevorgang abbrechen</string>
+ <string name="withdraw_manual_ready_warning">Bitte vergewissern Sie sich, dass Sie den Überweisungsvermerk richtig angeben, da sonst der überwiesene Betrag nicht im Wallet ankommt.</string>
+ <string name="reset">Zurücksetzen</string>
+ <string name="payment_prompt_title">Zahlung überprüfen</string>
+ <string name="send_deposit_iban_error">IBAN ist ungültig</string>
+ <string name="send_peer_create_button">Jetzt Geld senden</string>
+ <string name="send_peer_title">Geld an ein anderes Wallet senden</string>
+ <string name="send_deposit_bitcoin_address">Bitcoin-Adresse</string>
+ <string name="transaction_action_kyc">KYC abschließen</string>
+ <string name="withdraw_restrict_age">Nutzung auf Alter beschränken</string>
+ <string name="send_deposit_bitcoin_create_button">Bitcoin übertragen</string>
+ <string name="settings_db_export">Datenbank exportieren</string>
+ <string name="transactions_delete_dialog_message">Sind Sie sicher, dass Sie diese Transaktion aus Ihrem Wallet entfernen möchten?</string>
+ <string name="transactions_delete_dialog_title">Transaktion löschen</string>
+ <string name="receive_peer_payment_intro">Möchten Sie diese Zahlung erhalten?</string>
+ <string name="transactions_abort">Abbrechen</string>
+ <string name="payment_pay_template_title">Passen Sie Ihre Bestellung an</string>
+ <string name="send_intro">Wählen Sie aus, wohin Sie Geld senden möchten:</string>
+ <string name="send_deposit_title">Einzahlung auf ein Bankkonto</string>
+ <string name="withdraw_bank">Bank</string>
+ <string name="withdraw_manual_ready_account">Konto</string>
+ <string name="withdraw_manual_ready_receiver">Name des Empfängers</string>
+ <string name="withdraw_transfer">Überweisung</string>
+ <string name="exchange_delete_force">Löschung erzwingen (bereinigen)</string>
+ <string name="settings_db_export_summary">Interne Datenbank speichern</string>
+ <string name="settings_db_clear_error">Fehler beim Bereinigen der Datenbank</string>
+ <string name="settings_alert_reset_canceled">Zurücksetzen abgebrochen</string>
+ <string name="exchange_not_contacted">Zahlungsdienstleister nicht kontaktiert</string>
+ <string name="send_peer_expiration_1d">1Tag</string>
+ <string name="settings_test">Integrationstest durchführen</string>
+ <string name="settings_test_summary">Führt Test-Transaktionen mit Demo-Setup durch</string>
+ <string name="copy" tools:override="true">Kopieren</string>
+ <string name="copy_uri">Taler-URI kopieren</string>
+ <string name="transaction_peer_pull_debit">Rechnung bezahlt</string>
+ <string name="transaction_peer_pull_credit">Rechnung</string>
+ <string name="transactions_receive_funds">Geld
+\nempfangen</string>
+ <string name="send_amount">Zu sendender Betrag</string>
+ <string name="pay_peer_title">Rechnung bezahlen</string>
+ <string name="pay_peer_intro">Möchten Sie diese Rechnung bezahlen?</string>
+ <string name="receive_peer_payment_title">Zahlung erhalten</string>
+ <string name="send_peer_expiration_hours">Stunden</string>
+ <string name="share_payment">Zahlungs-Link teilen</string>
+ <string name="offline_banner">Kein Internetzugang</string>
+ <string name="transactions_fail_dialog_title">Transaktion sofort abbrechen</string>
+ <string name="transaction_dummy_title">Unbekannte Transaktion</string>
+ <string name="receive_peer_title">Zahlung anfordern</string>
+ <string name="send_deposit">Auf ein Bankkonto</string>
+ <string name="send_peer_bitcoin">An ein anderes Taler-Wallet</string>
+ <string name="send_peer_expiration_custom">Benutzerdefiniert</string>
+ <string name="exchange_delete">Zahlungsdienstleister löschen</string>
+ <string name="settings_db_export_success">Datenbank in Datei exportiert</string>
+ <string name="receive_peer_create_button">Rechnung erstellen</string>
+ <string name="receive_peer_invoice_uri">Alternativ kopieren und senden Sie diesen URI:</string>
+ <string name="receive_peer_invoice_instruction">Lassen Sie die Käufer diesen QR-Code scannen, um den Betrag zu bezahlen:</string>
+ <string name="button_scan_qr_code_label">QR-Code scannen</string>
+ <string name="send_deposit_check_fees_button">Gebühren prüfen</string>
+ <string name="send_deposit_name">Kontoinhaber/in</string>
+ <string name="transactions_send_funds">Geld
+\nsenden</string>
+ <string name="currency">Währung</string>
+ <string name="transactions_balance">Guthaben</string>
+ <string name="withdraw_restrict_age_unrestricted">Uneingeschränkt</string>
+ <string name="settings_logcat_success">Protokoll wurde in Datei exportiert</string>
+ <string name="settings_logcat_error">Fehler beim Exportieren des Protokolls</string>
+ <string name="transactions_delete_selected_dialog_message">Sind Sie sicher, dass Sie die ausgewählten Transaktionen aus Ihrem Wallet entfernen möchten?</string>
+ <string name="transactions_resume">Fortsetzen</string>
+ <string name="transactions_abort_dialog_title">Transaktion abbrechen</string>
+ <string name="transactions_cancel_dialog_message">Sind Sie sicher, dass Sie diese Abhebung stornieren möchten? Gelder, die gerade noch übertragen werden, könnten verloren gehen.</string>
+ <string name="send_deposit_bic">BIC / SWIFT</string>
+ <string name="send_peer">An ein anderes Wallet</string>
+ <string name="payment_confirmation_code">Bestätigungscode</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_bic_error">BIC ist ungültig</string>
+ <string name="withdraw_subtitle">Ziel-Bankkonto auswählen</string>
+ <string name="settings_db_import">Datenbank importieren</string>
+ <string name="settings_db_import_summary">Datenbank aus Datei wiederherstellen</string>
+ <string name="settings_db_export_error">Fehler beim Exportieren der Datenbank</string>
+ <string name="settings_db_import_error">Fehler beim Importieren der Datenbank</string>
+ <string name="settings_dialog_import_message">Dieser Vorgang überschreibt Ihre vorhandene Datenbank. Möchten Sie fortfahren?</string>
+ <string name="settings_dialog_reset_message">Möchten Sie das Wallet wirklich zurücksetzen und alle Münzen und Einkäufe verlieren?</string>
+ <string name="settings_alert_reset_done">Wallet wurde zurückgesetzt</string>
+ <string name="send_deposit_bitcoin">An ein Bitcoin-Wallet</string>
+ <string name="send_peer_purpose">Zweck</string>
+ <string name="exchange_list_select">Zahlungsdienstleister auswählen</string>
+ <string name="exchange_dialog_delete_message">Sind Sie sicher, dass Sie diesen Zahlungsdienstleister löschen möchten? Wenn Sie diesen Vorgang erzwingen, führt dies zu einem Geldverlust.</string>
+ <string name="settings_db_import_success">Datenbank aus Datei importiert</string>
+ <string name="error_broken_uri">Fehler: Diese Taler-URI funktioniert (im Moment) nicht.</string>
+ <string name="settings_alert_import_canceled">Import abgebrochen</string>
+ <string name="import_db">Importieren</string>
+ <string name="transactions_detail_title_currency">%s Transaktionen</string>
+ <string name="receive_withdraw">Vom Bankkonto abheben</string>
+ <string name="send_deposit_amount_effective">Effektiver Betrag</string>
+ <string name="send_deposit_create_button">Einzahlung tätigen</string>
+ <string name="receive_amount">Zu erhaltender Betrag</string>
+ <string name="receive_amount_invalid">Betrag ungültig</string>
+ <string name="receive_intro">Wählen Sie aus, woher Sie Geld erhalten möchten:</string>
+ <string name="amount_sent">Gesendeter Betrag</string>
+ <string name="amount_received">Empfangener Betrag</string>
+ <string name="payment_pending">Zahlung nicht abgeschlossen - sie wird erneut versucht</string>
+ <string name="receive_peer_amount_invoiced">Rechnungsbetrag</string>
+ <string name="send_peer_payment_instruction">Lassen Sie den Geldempfänger diesen QR-Code scannen, um den Wert zu erhalten:</string>
+ <string name="transaction_deposit">Einlösen</string>
+ <string name="exchange_list_error">Es konnten keine Zahlungsdienstleister angezeigt werden</string>
+ <string name="withdraw_conversion_support">Dieser Zahlungsdienstleister unterstützt Währungsumtausch</string>
+ <string name="exchange_list_add_dev">Zahlungsdienstleister im Entwicklermodus hinzufügen</string>
+ <string name="send_peer_expiration_30d">30 Tage</string>
+ <string name="send_peer_expiration_7d">7 Tage</string>
+ <string name="balance_scope_exchange">Zahlungsdienstleister: %1$s</string>
+ <string name="balance_scope_auditor">Auditor: %1$s</string>
+ <string name="receive_peer">Rechnung an anderes Wallet stellen</string>
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="withdraw_manual_bitcoin_title">Bitcoin-Exchange zur Auszahlung bereit</string>
+ <string name="transactions_retry">Wiederholen</string>
+ <string name="send_peer_expiration_period">Läuft ab in</string>
+ <string name="transactions_suspend">Aussetzen</string>
+ <string name="transactions_abort_dialog_message">Sind Sie sicher, dass Sie diese Transaktion abbrechen möchten? Gelder, die gerade noch übertragen werden, könnten verloren gehen.</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-es/strings.xml b/wallet/src/main/res/values-es/strings.xml
index 078183c..bcc216d 100644
--- a/wallet/src/main/res/values-es/strings.xml
+++ b/wallet/src/main/res/values-es/strings.xml
@@ -1,142 +1,273 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="google_play_title" tools:keep="@string/google_play_title">Cartera Taler (experimental)</string>
- <string name="nav_prompt_withdraw">Retirar Efectivo Digital</string>
- <string name="nav_exchange_tos">Términos de servicio del Exchange</string>
- <string name="nav_exchange_select">Seleccionar Exchange</string>
- <string name="button_back">Volver atrás</string>
- <string name="enter_uri">Insertar URI Taler</string>
+ <string name="nav_prompt_withdraw">Retirar efectivo digital</string>
+ <string name="nav_exchange_tos">Términos de servicio del cambiario</string>
+ <string name="nav_exchange_select">Seleccionar cambiario</string>
+ <string name="button_back">Regresar</string>
+ <string name="enter_uri">Ingresa la URI de Taler</string>
<string name="paste">Pegar</string>
- <string name="paste_invalid">El portapapeles contiene un tipo de datos inválido</string>
+ <string name="paste_invalid">El portapapeles contiene un tipo de dato inválido</string>
<string name="ok">OK</string>
<string name="cancel">Cancelar</string>
<string name="search">Buscar</string>
<string name="menu">Menú</string>
<string name="or">o</string>
- <string name="error_unsupported_uri">Error: Esta URI de Taler no está soportada.</string>
+ <string name="error_unsupported_uri">Error: esta URI de Taler no está soportada.</string>
<string name="menu_settings">Ajustes</string>
- <string name="balances_title">Balances</string>
- <string name="amount_positive">+ %s</string>
- <string name="amount_chosen">Elegir Cantidad</string>
+ <string name="balances_title">Saldos</string>
+ <string name="amount_positive">+%s</string>
+ <string name="amount_chosen">Monto elegido</string>
<string name="balances_inbound_label">entrada</string>
<string name="transactions_empty">No tienes ninguna transacción</string>
<string name="transactions_detail_title">Transacción</string>
- <string name="transactions_delete">Borrar</string>
- <string name="transactions_select_all">Seleccionar Todo</string>
+ <string name="transactions_delete">Eliminar</string>
+ <string name="transactions_select_all">Seleccionar todo</string>
<string name="transaction_paid">Pagado</string>
<string name="transaction_order_total">Total</string>
<string name="transaction_order_id">Recibo #%1$s</string>
- <string name="transaction_tip_from">Propina de %s</string>
+ <string name="transaction_tip_from">Premio de %s</string>
<string name="transaction_refund">Devolución</string>
<string name="transaction_refund_from">Devolución de %s</string>
<string name="transaction_pending">PENDIENTE</string>
<string name="transaction_refresh">Comisión de cambio por expiración de moneda</string>
<string name="payment_title">Pago</string>
- <string name="payment_button_confirm">Confirmar Pago</string>
- <string name="payment_label_amount_total">Cantidad Total:</string>
+ <string name="payment_button_confirm">Confirmar pago</string>
+ <string name="payment_label_amount_total">Monto total:</string>
<string name="payment_label_order_summary">Compra</string>
<string name="payment_error">Error: %s</string>
- <string name="payment_balance_insufficient">Balance insuficiente!</string>
+ <string name="payment_balance_insufficient">¡Saldo insuficiente!</string>
<string name="payment_aborted">Abortado</string>
<string name="payment_failed">Fallido</string>
- <string name="payment_already_paid_title">Ya pagado</string>
+ <string name="payment_already_paid_title">Ya fue pagado</string>
<string name="payment_already_paid">Ya has pagado por esta compra.</string>
- <string name="withdraw_initiated">Retirada iniciada</string>
- <string name="withdraw_title">Retirada</string>
+ <string name="withdraw_initiated">Retiro iniciado</string>
+ <string name="withdraw_title">Retiro</string>
<string name="withdraw_total">Retirar</string>
<string name="withdraw_fees">Comisión</string>
- <string name="withdraw_exchange">Exchange</string>
- <string name="withdraw_button_confirm">Confirmar Retirada</string>
- <string name="withdraw_button_tos">Revisar Términos</string>
+ <string name="withdraw_exchange">Cambiario</string>
+ <string name="withdraw_button_confirm">Confirmar retiro</string>
+ <string name="withdraw_button_tos">Revisar términos</string>
<string name="withdraw_waiting_confirm">Esperando confirmación</string>
- <string name="withdraw_manual_title">Realizar transferencia manual al exchange</string>
- <string name="withdraw_amount">¿Cuanto quieres retirar\?</string>
+ <string name="withdraw_manual_title">Realizar transferencia manual al cambiario</string>
+ <string name="withdraw_amount">¿Cuánto deseas retirar\?</string>
<string name="withdraw_manual_check_fees">Comisiones por cheque</string>
- <string name="withdraw_error_title">Error en la retirada</string>
+ <string name="withdraw_error_title">Error en el retiro</string>
<string name="withdraw_error_test">Error retirando TESTKUDOS</string>
- <string name="exchange_settings_title">Exchanges</string>
- <string name="exchange_settings_summary">Gestionar la lista de exchanges conocidos en esta cartera</string>
- <string name="exchange_list_title">Exchanges</string>
- <string name="exchange_list_empty">Sin exchanges conocidos
+ <string name="exchange_settings_title">Cambiarios</string>
+ <string name="exchange_settings_summary">Gestionar la lista de cambiarios conocidos en esta cartera</string>
+ <string name="exchange_list_title">Cambiarios</string>
+ <string name="exchange_list_empty">Sin cambiarios conocidos
\n
-\nAñade uno manualmente para retirar efectivo digital!</string>
- <string name="exchange_list_currency">Divisa:%s</string>
- <string name="exchange_list_add">Añadir exchange</string>
- <string name="exchange_add_url">Inserta la dirección del exchange</string>
- <string name="exchange_add_error">No se pudo añadir el exchange</string>
- <string name="exchange_fee_withdrawal_fee_label">Comisión Retirada:</string>
- <string name="exchange_fee_overhead_label">Pérdida Redondeo:</string>
+\nAñade uno manualmente or retira efectivo digital.</string>
+ <string name="exchange_list_currency">Divisa: %s</string>
+ <string name="exchange_list_add">Añadir cambiario</string>
+ <string name="exchange_add_url">Ingresa dirección del cambiario</string>
+ <string name="exchange_add_error">No se pudo añadir el cambiario</string>
+ <string name="exchange_fee_withdrawal_fee_label">Comisión por retiro:</string>
+ <string name="exchange_fee_overhead_label">Pérdida por redondeo:</string>
<string name="exchange_fee_coin_expiration_label">Expiración de moneda más temprana:</string>
- <string name="exchange_fee_coin_fees_label">Comisiones Moneda</string>
- <string name="exchange_fee_wire_fees_label">Comisiones Transferencia</string>
- <string name="exchange_fee_withdraw_fee">Comisión Retirada:%s</string>
- <string name="exchange_fee_deposit_fee">Comisión Depósito:%s</string>
- <string name="exchange_fee_refresh_fee">Comisión Cambio:%s</string>
- <string name="exchange_fee_refund_fee">Comisión devolución: %s</string>
- <string name="exchange_fee_wire_fee_wire_fee">Comisión Transferencia: %s</string>
- <string name="exchange_fee_wire_fee_closing_fee">Comisión Cierre: %s</string>
- <string name="exchange_tos_accept">Aceptar Términos de Servicio</string>
- <string name="exchange_tos_error">Error mostrando los Términos de Servicio: %s</string>
- <string name="pending_operations_title">Operaciones Pendientes</string>
- <string name="pending_operations_refuse">Rechazar Propuesta</string>
+ <string name="exchange_fee_coin_fees_label">Comisiones de moneda</string>
+ <string name="exchange_fee_wire_fees_label">Comisiones de transferencia</string>
+ <string name="exchange_fee_withdraw_fee">Comisión por retiro: %s</string>
+ <string name="exchange_fee_deposit_fee">Comisión por depósito: %s</string>
+ <string name="exchange_fee_refresh_fee">Comisión por cambio: %s</string>
+ <string name="exchange_fee_refund_fee">Comisión por devolución: %s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Comisión por transferencia: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Comisión por cierre: %s</string>
+ <string name="exchange_tos_accept">Aceptar términos de servicio</string>
+ <string name="exchange_tos_error">Error mostrando los términos de servicio: %s</string>
+ <string name="pending_operations_title">Operaciones pendientes</string>
+ <string name="pending_operations_refuse">Rechazar propuesta</string>
<string name="pending_operations_no_action">(sin acción)</string>
- <string name="settings_dev_mode">Modo Desarrollador</string>
+ <string name="settings_dev_mode">Modo desarrollador</string>
<string name="settings_withdraw_testkudos">Retirar TESTKUDOS</string>
- <string name="settings_withdraw_testkudos_summary">Obtener dinero para realizar pruebas</string>
- <string name="settings_logcat">Log de Depuración</string>
- <string name="settings_dev_mode_summary">Muestra más información intencionada para depuración</string>
- <string name="settings_logcat_summary">Ver/enviar log interno</string>
- <string name="settings_version_app">Versión App</string>
- <string name="settings_version_core">Cartera Versión Principal</string>
- <string name="settings_version_protocol_exchange">Exchange Versiones Soportadas</string>
- <string name="settings_version_protocol_merchant">Versiones Soportadas Comerciantes</string>
+ <string name="settings_withdraw_testkudos_summary">Obtener dinero para pruebas</string>
+ <string name="settings_logcat">Bitácora de depuración</string>
+ <string name="settings_dev_mode_summary">Muestra más información de depuración</string>
+ <string name="settings_logcat_summary">Guardar bitácora interna</string>
+ <string name="settings_version_app">Versión de la aplicación</string>
+ <string name="settings_version_core">Versión de Wallet Core</string>
+ <string name="settings_version_protocol_exchange">Versiones de Exchange soportadas</string>
+ <string name="settings_version_protocol_merchant">Versiones de Merchant soportadas</string>
<string name="settings_version_unknown">Desconocido</string>
- <string name="settings_reset">Resetear Cartera (¡peligroso!)</string>
- <string name="settings_reset_summary">Tira tu dinero</string>
+ <string name="settings_reset">Reiniciar cartera (¡peligroso!)</string>
+ <string name="settings_reset_summary">Se deshace de tu dinero</string>
<string name="nav_settings_backup">Copia de seguridad</string>
- <string name="backup_last">Última copia de seguridad:%s</string>
+ <string name="backup_last">Última copia de seguridad: %s</string>
<string name="refund_title">Devolución</string>
<string name="refund_error">Error procesando la devolución</string>
- <string name="refund_success">Devolución recibida:%s</string>
- <string name="tip_title">Consejo</string>
- <string name="wifi_disabled_error">Conecta tu Wi-Fi para obtener Wi-Fi gratis</string>
- <string name="wifi_connect_error">No se pudo conectar a la Wi-Fi gratuita:%s</string>
+ <string name="refund_success">Devolución recibida: %s</string>
+ <string name="tip_title">Premio</string>
+ <string name="wifi_disabled_error">Enciende tu Wi-Fi para obtener Wi-Fi gratis</string>
+ <string name="wifi_connect_error">No se pudo conectar al Wi-Fi gratis: %s</string>
<string name="app_name">Cartera Taler</string>
- <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler hace transacciones online con privacidad-amigable, de forma rápida y sencilla.</string>
- <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Esta App es una cartera para GNU Taler. Es altamente experimental, y no está lista para producción todavía. GNU Taler es un sistema de pago con preservación de la privacidad. Los clientes pueden permanecer anónimos, pero los comerciantes no pueden esconder sus ingresos a través de GNU Taler. Eso ayuda a evitar la evasión de impuestos y el lavado de dinero. El uso principal de GNU Taler son los pagos;no es la intención la acumulación de valor. Los pagos son siempre respaldados por una divisa existente. El pago es realizado después de cambiar dinero existente en dinero electrónico con la ayuda de un servicio de Exchange que es, un proveedor de servicios de pago para Taler. Cuando se realiza un pago, los clientes solo necesitan una cartera cargada. Un comerciante puede aceptar pagos sin hacer registrarse a sus cliente en su página Web. GNU Taler es inmune contra varios tipos de fraude, como el phising de información de tarjetas de crédito o el fraude por devolución. En caso de pérdida o robo, solo una cantidad limitada de dinero dejado en la cartera podría perderse.</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler hace transacciones en línea amigables con la privacidad, de forma rápida y sencilla.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Esta aplicación es una cartera para GNU Taler. Es altamente experimental, y aún no está lista para producción. GNU Taler es un sistema de pago que preserva la privacidad. Los clientes pueden permanecer anónimos, pero los comerciantes no pueden esconder sus ingresos a través de GNU Taler. Esto ayuda a evitar la evasión de impuestos y el lavado de dinero. El uso principal de GNU Taler son los pagos; su intención no es servir como reserva de valor. Los pagos siempre están respaldados por una divisa existente. El pago se realiza después de cambiar dinero existente por dinero electrónico con la ayuda de un servicio de cambiario, que es un proveedor de servicios de pagos para Taler. Cuando se realiza un pago, los clientes solo necesitan una cartera con fondos. Un comerciante puede aceptar pagos sin hacer que sus clientes se registren en su página Web. GNU Taler es inmune contra varios tipos de fraude, como el phising de información de tarjetas de crédito o el fraude por devolución. En caso de pérdida o robo, solo una cantidad limitada de dinero guardado en la cartera podría perderse.</string>
<string name="nav_header_title">GNU Taler</string>
<string name="nav_header_subtitle">Cartera</string>
- <string name="nav_exchange_fees">Comisiones del Exchange</string>
+ <string name="nav_exchange_fees">Comisiones del cambiario</string>
<string name="nav_error">Error</string>
- <string name="button_scan_qr_code">Escanear código QR Taler</string>
- <string name="uri_invalid">Taler URI no válida</string>
- <string name="offline">La operación requiere acceso a Internet. Por favor asegúrate que tú conexión a Internet funciona y prueba de nuevo.</string>
- <string name="menu_retry_pending_operations">Reintentar Operaciones Pendientes</string>
- <string name="amount_negative">- %s</string>
+ <string name="button_scan_qr_code">Escanear código QR de Taler</string>
+ <string name="uri_invalid">URI de Taler inválida</string>
+ <string name="offline">La operación requiere acceso a Internet. Por favor asegúrate que tu conexión a Internet funcione y prueba de nuevo.</string>
+ <string name="menu_retry_pending_operations">Reintentar operaciones pendientes</string>
+ <string name="amount_negative">-%s</string>
<string name="balances_empty_state">No hay efectivo digital en tu cartera.
\n
-\nPuedes coger dinero de prueba desde el banco demo:
+\nPuedes obtener dinero de prueba desde el banco demostrativo:
\n
\nhttps://bank.demo.taler.net</string>
<string name="transactions_title">Transacciones</string>
<string name="transactions_empty_search">No se han encontrado transacciones. Prueba una búsqueda diferente.</string>
- <string name="transactions_error">No pudo cargar transacciones
+ <string name="transactions_error">No se pudo cargar las transacciones
\n
\n%s</string>
<string name="transactions_detail_title_balance">Balance: %s</string>
<string name="transaction_order">Compra</string>
- <string name="payment_fee">+%s comisión por pago</string>
+ <string name="payment_fee">+%s de comisión por pago</string>
<string name="payment_initiated">Pago iniciado</string>
<string name="withdraw_button_confirm_bank">Confirmar con el banco</string>
- <string name="withdraw_amount_error">Inserta una cantidad válida</string>
- <string name="withdraw_manual_payment_options">"Opciones de pago soportadas por %1$s:
+ <string name="withdraw_amount_error">Inserta un monto válido</string>
+ <string name="withdraw_manual_payment_options">Opciones de pago soportadas por %1$s:
\n
-\n%2$s"</string>
- <string name="withdraw_error_message">La retirada ahora mismo no es posible. Por favor prueba de nuevo más tarde!</string>
+\n%2$s</string>
+ <string name="withdraw_error_message">El retiro no es posible ahora mismo. Por favor prueba de nuevo más tarde.</string>
<string name="exchange_menu_manual_withdraw">Retirar</string>
<plurals name="exchange_fee_coin">
<item quantity="one">Moneda: %s (usada %d vez)</item>
- <item quantity="other">Moneda: %s (usadas %d veces)</item>
+ <item quantity="many">Moneda: %s (usada %d veces)</item>
+ <item quantity="other">Moneda: %s (usada %d veces)</item>
</plurals>
- <string name="exchange_fee_wire_fee_timespan">Intervalo de tiempo:%1$s - %2$s</string>
-</resources>
+ <string name="exchange_fee_wire_fee_timespan">Intervalo de tiempo: %1$s - %2$s</string>
+ <string name="host_apdu_service_desc">Pagos Taler con NFC</string>
+ <string name="transactions_delete_dialog_title">Eliminar transacción</string>
+ <string name="transactions_delete_dialog_message">¿Estás seguro de que deseas eliminar esta transacción de tu cartera\?</string>
+ <string name="withdraw_manual_ready_cancel">Cancelar retiro</string>
+ <string name="tip_already_accepted">Este premio ya fue aceptado.</string>
+ <string name="transaction_deposit">Depósito</string>
+ <string name="transaction_peer_pull_debit">Cobro pagado</string>
+ <string name="transaction_peer_push_credit">Pago push</string>
+ <string name="transaction_peer_pull_credit">Cobro</string>
+ <string name="receive_peer_invoice_instruction">Muestra este código QR a quien pagará:</string>
+ <string name="receive_peer_invoice_uri">O bien, copia y envía esta URI:</string>
+ <string name="receive_peer_create_button">Crear cobro</string>
+ <string name="pay_peer_title">Pagar factura</string>
+ <string name="send_peer_create_button">Enviar fondos ahora</string>
+ <string name="send_peer_warning">Los fondos saldrán de esta cartera inmediatamente.</string>
+ <string name="send_peer_payment_instruction">Muestra este código a quien deba recibir el dinero:</string>
+ <string name="tip_exchange">Cambiario</string>
+ <string name="tip_merchant_url">URL de comerciante</string>
+ <string name="withdraw_manual_ready_title">¡El cambiario está listo para el retiro!</string>
+ <string name="withdraw_manual_ready_details_intro">Detalles de la transferencia bancaria</string>
+ <string name="copy" tools:override="true">Copiar</string>
+ <string name="transactions_balance">Saldo</string>
+ <string name="withdraw_manual_ready_bank_button">Abrir en aplicación de banco</string>
+ <string name="copy_uri">Copiar URI de Taler</string>
+ <string name="transactions_send_funds">Enviar
+\nfondos</string>
+ <string name="transactions_receive_funds">Recibir
+\nfondos</string>
+ <string name="transactions_cancel_dialog_message">¿Estás seguro de que deseas cancelar este retiro\? Los fondos que están en tránsito podrían perderse.</string>
+ <string name="transactions_detail_title_currency">Transacciones de %s</string>
+ <string name="receive_peer_title">Solicitar pago</string>
+ <string name="receive_amount">Monto a recibir</string>
+ <string name="receive_intro">Elige desde dónde recibir el dinero:</string>
+ <string name="receive_peer">Cobrar a otra cartera</string>
+ <string name="receive_amount_invalid">Monto inválido</string>
+ <string name="receive_withdraw">Retiro desde cuenta de banco</string>
+ <string name="send_deposit_create_button">Hacer un depósito</string>
+ <string name="send_deposit_name">Titular de la cuenta</string>
+ <string name="send_deposit_amount_effective">Monto efectivo</string>
+ <string name="send_amount">Monto a enviar</string>
+ <string name="send_intro">Elige hacia dónde enviar el dinero:</string>
+ <string name="send_deposit">A una cuenta de banco</string>
+ <string name="send_deposit_title">Deposita a una cuenta de banco</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="receive_peer_payment_title">Recibir pago</string>
+ <string name="currency">Divisa</string>
+ <string name="send_peer">A otra cartera</string>
+ <string name="send_peer_title">Enviar dinero a otra cartera</string>
+ <string name="transaction_peer_push_debit">Pago push</string>
+ <string name="send_peer_payment_amount_received">Monto recibido</string>
+ <string name="send_peer_payment_amount_sent">Monto enviado</string>
+ <string name="pay_peer_intro">¿Quieres pagar este cobro\?</string>
+ <string name="receive_peer_payment_intro">¿Quieres recibir este pago\?</string>
+ <string name="withdraw_manual_bitcoin_intro">Ahora haz una transacción dividida con las siguientes tres salidas.</string>
+ <string name="withdraw_manual_ready_subject">Sujeto</string>
+ <string name="settings_logcat_error">Error exportando bitácora</string>
+ <string name="settings_logcat_success">Bitácora exportada al archivo</string>
+ <string name="tip_total">Total</string>
+ <string name="send_peer_purpose">Motivo</string>
+ <string name="send_peer_bitcoin">A otra cartera de Taler</string>
+ <string name="withdraw_manual_ready_intro">Para completar el proceso, necesitas transferir %s a la cuenta bancaria del cambiario</string>
+ <string name="withdraw_manual_bitcoin_title">Cambiario de Bitcoin listo para el retiro</string>
+ <string name="settings_db_export_summary">Guardar base de datos interna</string>
+ <string name="settings_db_export_success">Base de datos exportada al archivo</string>
+ <string name="tip_button_confirm">Aceptar premio</string>
+ <string name="tip_received">Premio recibido</string>
+ <string name="tip_fees">Comisión</string>
+ <string name="send_deposit_bitcoin">A una cartera de Bitcoin</string>
+ <string name="settings_db_export">Exportar base de datos</string>
+ <string name="settings_db_export_error">Error exportando base de datos</string>
+ <string name="withdraw_manual_ready_warning">Asegúrate de usar el sujeto correcto; de lo contrario, el dinero no llegará a esta cartera.</string>
+ <string name="transaction_action_kyc">Completar KYC</string>
+ <string name="withdraw_restrict_age">Restringir uso a edad</string>
+ <string name="withdraw_restrict_age_unrestricted">Sin restricciones</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="send_deposit_check_fees_button">Revisar comisiones</string>
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="transactions_delete_selected_dialog_message">¿Estás seguro de que deseas eliminar las transacciones seleccionadas de tu cartera\?</string>
+ <string name="send_deposit_bitcoin_address">Dirección Bitcoin</string>
+ <string name="send_deposit_bitcoin_create_button">Transferir Bitcoin</string>
+ <string name="transaction_dummy_title">Transacción desconocida</string>
+ <string name="send_deposit_bic_error">BIC es inválido</string>
+ <string name="send_peer_expiration_period">Expira en</string>
+ <string name="send_peer_expiration_1d">1 día</string>
+ <string name="send_peer_expiration_7d">7 días</string>
+ <string name="settings_test_summary">Realizar transacciones de prueba con el banco demostrativo</string>
+ <string name="settings_test">Ejecutar pruebas de integración</string>
+ <string name="button_scan_qr_code_label">Escanear código QR</string>
+ <string name="send_deposit_iban_error">IBAN es inválido</string>
+ <string name="send_peer_expiration_30d">30 días</string>
+ <string name="send_peer_expiration_custom">Personalizado</string>
+ <string name="send_peer_expiration_days">Días</string>
+ <string name="send_peer_expiration_hours">Horas</string>
+ <string name="exchange_list_error">No se pudo listar los cambiarios</string>
+ <string name="payment_prompt_title">Revisar pago</string>
+ <string name="payment_pay_template_title">Personaliza tu orden</string>
+ <string name="payment_create_order">Crear orden</string>
+ <string name="payment_confirmation_code">Código de confirmación</string>
+ <string name="payment_template_error">Error creando orden</string>
+ <string name="transactions_retry">Reintentar</string>
+ <string name="transactions_abort">Abortar</string>
+ <string name="transactions_fail">Matar</string>
+ <string name="transactions_suspend">Suspender</string>
+ <string name="transactions_resume">Resumir</string>
+ <string name="transactions_abort_dialog_title">Abortar transacción</string>
+ <string name="transactions_fail_dialog_message">¿Estás seguro de que deseas matar esta transacción\? Los fondos en tránsito SE PERDERÁN.</string>
+ <string name="exchange_not_contacted">Cambiario aún no contactado</string>
+ <string name="transactions_fail_dialog_title">Matar transacción</string>
+ <string name="transactions_abort_dialog_message">¿Estás seguro de que deseas abortar esta transacción\? Los fondos en tránsito podrían perderse.</string>
+ <string name="offline_banner">Sin acceso a internet</string>
+ <string name="error_broken_uri">Error: Este URI taler no funciona actualmente.</string>
+ <string name="withdraw_subtitle">Seleccione un cuenta bancaria destino</string>
+ <string name="withdraw_account">Cuenta #%1$d</string>
+ <string name="withdraw_account_currency">Cuenta #%1$d (%2$s)</string>
+ <string name="withdraw_transfer">Transferencia</string>
+ <string name="withdraw_conversion">Conversión</string>
+ <string name="withdraw_conversion_support">Este exchange soporta conversión de moneda</string>
+ <string name="exchange_list_add_dev">Agregar exchange de desarrollo</string>
+ <string name="settings_db_import">Importar base de datos</string>
+ <string name="settings_db_import_summary">Restaurar base de datos desde archivo</string>
+ <string name="settings_db_import_error">Error al importar la base de datos</string>
+ <string name="settings_db_import_success">Base de datos importada desde archivo</string>
+ <string name="settings_dialog_reset_message">Realmente quieres reiniciar la biiletera y perder todas las monedas y compras?</string>
+ <string name="settings_dialog_import_message">Esta operación sobreescribirá toda tu base de datos. Realmente quieres continuar?</string>
+ <string name="settings_alert_reset_done">La billetera ha sido reiniciada</string>
+ <string name="settings_alert_reset_canceled">Reinicio cancelado</string>
+ <string name="settings_alert_import_canceled">Importación cancelada</string>
+ <string name="import_db">Importar</string>
+ <string name="reset">Reiniciar</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-fi/strings.xml b/wallet/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000..7e88596
--- /dev/null
+++ b/wallet/src/main/res/values-fi/strings.xml
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2020 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ --><resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="app_name">Taler lompakko</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">Taler lompakko (kokeilu)</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler mahdollistaa yksityisyyttä kunnioittavat verkkomaksut nopeasti ja helposti.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Tämä sovellus on lompakko GNU Talerille. Se on erittäin kokeellinen eikä vielä valmis tuotantokäyttöön. GNU Taler on yksityisyyttä kunnioittava maksujärjestelmä. Asiakkaat voivat pysyä anonyymeinä, mutta kauppiaat eivät voi piilottaa tulojaan maksujen avulla GNU Talerilla. Tämä auttaa välttämään veronkiertoa ja rahanpesua. GNU Talerin ensisijainen käyttötarkoitus on maksut; sitä ei tarkoiteta arvon säilyttämiseen. Maksut ovat aina tuettuja olemassa olevalla valuutalla. Maksut suoritetaan vaihtamalla olemassa oleva raha sähköiseksi rahaksi Exchange-palvelun avulla, eli Talerin maksupalveluntarjoajan avulla. Maksua tehdessään asiakkaat tarvitsevat vain ladatun lompakon. Kauppias voi hyväksyä maksut ilman, että asiakkaiden tarvitsee rekisteröityä kauppiaan verkkosivustolle. GNU Taler on immuuni monille huijauksen muodoille, kuten luottokorttitietojen kalastelu tai palautuspetos. Jos lompakko katoaa tai varastetaan, vain lompakossa oleva rajoitettu rahasumma voi kadota.</string>
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="nav_header_subtitle">Lompakko</string>
+ <string name="nav_prompt_withdraw">Nosta sähköistä käteistä</string>
+ <string name="nav_exchange_tos">Vaidon käyttöehdot</string>
+ <string name="nav_exchange_select">Valitse vaihto</string>
+ <string name="nav_exchange_fees">Vaihdon maksut</string>
+ <string name="nav_error">Virhe</string>
+ <string name="button_back">Takaisin</string>
+ <string name="button_scan_qr_code">Skannaa Taler QR koodi</string>
+ <string name="button_scan_qr_code_label">Saknnaa QR koodi</string>
+ <string name="enter_uri">Syötä Taler URI</string>
+ <string name="copy" tools:override="true">Kopioi</string>
+ <string name="copy_uri">Kopioi Taler URI</string>
+ <string name="paste">Liitä</string>
+ <string name="paste_invalid">Leikepöytä sisältää virheellisen tietotyypin</string>
+ <string name="uri_invalid">Taler URI ei ole kelvollinen</string>
+ <string name="ok">OK</string>
+ <string name="cancel">Hylkää</string>
+ <string name="search">Haku</string>
+ <string name="menu">Valikko</string>
+ <string name="or">tai</string>
+ <string name="currency">Valuutta</string>
+ <!-- The count should be mirrored in RTL languages -->
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="import_db">Tuonti</string>
+ <string name="reset">Nollaa</string>
+ <string name="share_payment">Jaa maksu linkki</string>
+ <string name="offline">Toiminta vaatii Internetyhteyden. Varmista, että internetyhteytesi toimii ja yritä uudelleen.</string>
+ <string name="offline_banner">Ei yhteyttä</string>
+ <string name="error_unsupported_uri">Virhe: Tätä Taler URI:a ei tueta.</string>
+ <string name="error_broken_uri">Virhe: Tämä Taler URI ei (tällä hetkellä) toimi.</string>
+ <string name="menu_settings">Asetukset</string>
+ <string name="menu_retry_pending_operations">Yritä uudelleen odottavia toimintoja</string>
+ <string name="host_apdu_service_desc">Taler NFC maksut</string>
+ <string name="balances_title">Saldot</string>
+ <string name="amount_positive">+%s</string>
+ <string name="amount_negative">-%s</string>
+ <string name="amount_chosen">Valittu summa</string>
+ <string name="amount_sent">Summa lähetetty</string>
+ <string name="amount_received">Summa saatu</string>
+ <string name="balances_inbound_label">saapuva</string>
+ <string name="balances_empty_state">Lompakossasi ei ole sähköistä rahaa.
+\n
+\nVoit saada testirahaa demopankista:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="balance_scope_exchange">Vaihda:%1$s</string>
+ <string name="balance_scope_auditor">Tarkastaja: %1$s</string>
+ <string name="transactions_title">Tapahtumat</string>
+ <string name="transactions_balance">Saldo</string>
+ <string name="transactions_send_funds">Lähetä
+\nVarat</string>
+ <string name="transactions_receive_funds">Vastaanota
+\nVarat</string>
+ <string name="transactions_empty">Sinulla ei ole tapahtumia</string>
+ <string name="transactions_empty_search">Tapahtumia ei löytynyt. Yritä toista hakua.</string>
+ <string name="transactions_error">Tapahtumaa ei voinut ladata
+\n
+\n%s</string>
+ <string name="transactions_detail_title">Tapahtuma</string>
+ <string name="transactions_detail_title_currency">%s Tapahtumat</string>
+ <string name="transactions_delete">Poista</string>
+ <string name="transactions_retry">Yritä uudelleen</string>
+ <string name="transactions_abort">Peruuta</string>
+ <string name="transactions_fail">Lopeta</string>
+ <string name="transactions_suspend">Keskeytä</string>
+ <string name="transactions_resume">Jatka</string>
+ <string name="transactions_select_all">Valitse kaikki</string>
+ <string name="transactions_delete_dialog_title">Poista tapahtuma</string>
+ <string name="transactions_delete_dialog_message">Haluatko varmasti poistaa tämän tapahtuman lompakostasi?</string>
+ <string name="transactions_delete_selected_dialog_message">Haluatko varmasti poistaa valitut tapahtumat lompakostasi?</string>
+ <string name="transactions_abort_dialog_title">Peruuta tapahtuma</string>
+ <string name="transactions_abort_dialog_message">Haluatko varmasti keskeyttää tämän tapahtuman? Siirrettävät varat saattavat kadota.</string>
+ <string name="transactions_fail_dialog_title">Lopeta tapahtuma</string>
+ <string name="transactions_fail_dialog_message">Haluatko varmasti lopettaa tämän tapahtuman? Vielä siirrettävät varat MENETÄÄN.</string>
+ <string name="transactions_cancel_dialog_message">Haluatko varmasti peruuttaa tämän noston? Siirrettävät varat saattavat kadota.</string>
+ <!-- Transactions -->
+ <string name="transaction_paid">Maksettu</string>
+ <string name="transaction_order_total">Yhteensä</string>
+ <string name="transaction_order">Osto</string>
+ <string name="transaction_order_id">Kuitti #%1$s</string>
+ <string name="transaction_refund">Maksun palautus</string>
+ <string name="transaction_refund_from">Maksun palautus %s</string>
+ <string name="transaction_pending">ODOTTAA</string>
+ <string name="transaction_refresh">Kolikon voimassaolon vaihtomaksu</string>
+ <string name="transaction_deposit">Talletus</string>
+ <string name="transaction_peer_push_debit">Siirrä maksua</string>
+ <string name="transaction_peer_pull_credit">Lasku</string>
+ <string name="transaction_peer_pull_debit">Lasku maksettu</string>
+ <string name="transaction_peer_push_credit">Siirrä maksua</string>
+ <string name="transaction_action_kyc">Täydellinen KYC</string>
+ <string name="transaction_dummy_title">Tuntematon tapahtuma</string>
+ <string name="payment_title">Maksu</string>
+ <string name="payment_prompt_title">Näytä maksu</string>
+ <string name="payment_fee">+%s maksun käsittelymaksu</string>
+ <string name="payment_button_confirm">Vahvista maksu</string>
+ <string name="payment_label_amount_total">Kokonais summa:</string>
+ <string name="payment_label_order_summary">Osta</string>
+ <string name="payment_error">Virhe: %s</string>
+ <string name="payment_pending">Maksua ei suoritettu, sitä yritetään uudelleen</string>
+ <string name="payment_balance_insufficient">Saldo ei riitä!</string>
+ <string name="payment_aborted">Peruutettu</string>
+ <string name="payment_failed">Epäonnistui</string>
+ <string name="payment_initiated">Maksu aloitettu</string>
+ <string name="payment_already_paid_title">Jo maskettu</string>
+ <string name="payment_already_paid">Olet jo maksanut tämän ostoksen.</string>
+ <string name="payment_pay_template_title">Muokkaa tilausta</string>
+ <string name="payment_create_order">Luo tilaus</string>
+ <string name="payment_confirmation_code">Vahvistuskoodi</string>
+ <string name="payment_template_error">Virhe tilausken tekemisessä</string>
+ <string name="receive_amount">Vastaanotettava summa</string>
+ <string name="receive_amount_invalid">Virrheellinen summa</string>
+ <string name="receive_intro">Valitse mistä saat rahaa:</string>
+ <string name="receive_withdraw">Nosto pankkitililtä</string>
+ <string name="receive_peer">Laskuta toista lompakkoa</string>
+ <string name="receive_peer_title">Pyydä maksu</string>
+ <string name="receive_peer_create_button">Luo lasku</string>
+ <string name="receive_peer_invoice_instruction">Anna maksajan skannata tämä QR koodi maksaakseen:</string>
+ <string name="receive_peer_invoice_uri">Vaihtoehtoisesti kopioi ja lähetä tämä URI:</string>
+ <string name="receive_peer_amount_invoiced">Summa laskutettu</string>
+ <string name="send_amount">Lähetettävä summa</string>
+ <string name="send_intro">Valitse mihin lähetät rahaa:</string>
+ <string name="send_deposit">Pankkitilille</string>
+ <string name="send_deposit_bitcoin">Bitcoin lompakkoon</string>
+ <string name="send_deposit_title">Talletus pankkitilille</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_iban_error">IBAN on virheellinen</string>
+ <string name="send_deposit_bic_error">BIC on virheellinen</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="send_deposit_name">Tilin haltija</string>
+ <string name="send_deposit_bitcoin_address">Bitcoin osoite</string>
+ <string name="send_deposit_check_fees_button">Tarkista maksut</string>
+ <string name="send_deposit_amount_effective">Todellinen summa</string>
+ <string name="send_deposit_create_button">Tee talletus</string>
+ <string name="send_deposit_bitcoin_create_button">Siirrä Bitcoineja</string>
+ <string name="send_peer">Toiseen lompakkoon</string>
+ <string name="send_peer_bitcoin">Toiseen Taler lompakkoon</string>
+ <string name="send_peer_title">Lähetä rahaa toiseen lompakkoon</string>
+ <string name="send_peer_create_button">Lähetä varat nyt</string>
+ <string name="send_peer_payment_instruction">Anna maksunsaajan skannata tämä QR-koodi saadaksesi:</string>
+ <string name="send_peer_expiration_period">Vanhenee</string>
+ <string name="send_peer_expiration_1d">1 päivässä</string>
+ <string name="send_peer_expiration_7d">7 päivässä</string>
+ <string name="send_peer_expiration_30d">30 päivässä</string>
+ <string name="send_peer_expiration_custom">Mukautettu</string>
+ <string name="send_peer_expiration_days">Päivässä</string>
+ <string name="send_peer_expiration_hours">tunnissa</string>
+ <string name="send_peer_purpose">Tarkoitus</string>
+ <string name="pay_peer_title">Maksa lasku</string>
+ <string name="pay_peer_intro">Haluatko maksaa tämän laskun?</string>
+ <string name="receive_peer_payment_title">Vastaanota maksu</string>
+ <string name="receive_peer_payment_intro">Haluatko vastaanottaa tämän maksun?</string>
+ <string name="withdraw_initiated">Nosto aloitettu</string>
+ <string name="withdraw_title">Nosto</string>
+ <string name="withdraw_subtitle">Valiste kohdepankkitili</string>
+ <string name="withdraw_total">Nosta</string>
+ <string name="withdraw_fees">Maksu</string>
+ <string name="withdraw_restrict_age">Ikärajoitus</string>
+ <string name="withdraw_restrict_age_unrestricted">Rajoittamaton</string>
+ <string name="withdraw_exchange">Vaihto</string>
+ <string name="withdraw_bank">Pankki</string>
+ <string name="withdraw_button_confirm">Vahvista nosto</string>
+ <string name="withdraw_button_confirm_bank">Vahvista pankilla</string>
+ <string name="withdraw_button_tos">Tarkista ehdot</string>
+ <string name="withdraw_waiting_confirm">Odottaa vahvistusta</string>
+ <string name="withdraw_manual_title">Tee manuaalinen siirto vaihtoon</string>
+ <string name="withdraw_amount">Kuinka paljon nostetaan?</string>
+ <string name="withdraw_amount_error">Syötä kelvollinen summa</string>
+ <string name="withdraw_manual_payment_options">%1$s:n tukemat maksuvaihtoehdot:
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">Tarkista maksut</string>
+ <string name="withdraw_manual_ready_title">Vaihto on valmis nostettavaksi!</string>
+ <string name="withdraw_manual_ready_intro">Suorittaaksesi prosessin sinun on siirrettävä %s vaihtopankkitilille</string>
+ <string name="withdraw_manual_ready_details_intro">Pankkisiirtotiedot</string>
+ <string name="withdraw_manual_bitcoin_title">Bitcoin vaihto valmis nostettavaksi</string>
+ <string name="withdraw_manual_bitcoin_intro">Tee nyt jaettu tapahtuma seuraavalla kolmella ulostulolla.</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_account">Tili</string>
+ <string name="withdraw_manual_ready_receiver">Vastaanotajan nimi</string>
+ <string name="withdraw_manual_ready_subject">Aihe</string>
+ <string name="withdraw_manual_ready_bank_button">Avaa pankkisovelluksessa</string>
+ <string name="withdraw_manual_ready_cancel">Peruuta nosto</string>
+ <string name="withdraw_manual_ready_warning">Varmista, että käytät oikeaa aihetta, muuten rahat eivät tule tähän lompakkoon.</string>
+ <string name="withdraw_error_title">Nosto virhe</string>
+ <string name="withdraw_error_message">Nosto ei ole tällä hetkellä mahdollista. Yritä uudelleen myöhemmin!</string>
+ <string name="withdraw_error_test">Virhe nostaessa TESTKUDOS</string>
+ <string name="withdraw_account">Tili #%1$d</string>
+ <string name="withdraw_account_currency">Tili #%1$d (%2$s)</string>
+ <string name="withdraw_transfer">Vaihda</string>
+ <string name="withdraw_conversion">Muuntaminen</string>
+ <string name="withdraw_conversion_support">Tämä vaihto tukee valuutan muuntamista</string>
+ <string name="exchange_settings_title">Vaihdot</string>
+ <string name="exchange_settings_summary">Hallinnoi tämän lompakon tuntemien vaihtojen luetteloa</string>
+ <string name="exchange_list_title">Vaihdot</string>
+ <string name="exchange_list_empty">Vaihtoja ei tiedossa
+\n
+\nLisää yksi manuaalisesti tai nosta digitaalista käteistä!</string>
+ <string name="exchange_list_currency">Valuutta: %s</string>
+ <string name="exchange_list_add">Lisää vaihto</string>
+ <string name="exchange_list_select">Valitse vaihto</string>
+ <string name="exchange_delete">Poista vaihto</string>
+ <string name="exchange_delete_force">Pakota poisto (puhdistus)</string>
+ <string name="exchange_dialog_delete_message">Haluatko varmasti poistaa tämän vaihdon? Tämän toimenpiteen pakottaminen johtaa varojen menetykseen.</string>
+ <string name="exchange_not_contacted">Vaihtoon ei otettu yhteyttä</string>
+ <string name="exchange_add_url">Anna vaihton osoite</string>
+ <string name="exchange_add_error">Vaihtoa ei voitu lisätä</string>
+ <string name="exchange_list_error">Vaihtoja ei voitu luetteloida</string>
+ <string name="exchange_list_add_dev">Lisää kehitysvaihtoja</string>
+ <string name="exchange_menu_manual_withdraw">Nosta</string>
+ <string name="exchange_fee_withdrawal_fee_label">Noston maksu:</string>
+ <string name="exchange_fee_overhead_label">Pyöristystappio:</string>
+ <string name="exchange_fee_coin_expiration_label">Aikaisin kolikon vanheneminen:</string>
+ <string name="exchange_fee_coin_fees_label">Kolikon maksut</string>
+ <string name="exchange_fee_wire_fees_label">Sirtomaksut</string>
+ <plurals name="exchange_fee_coin">
+ <item quantity="one">Kolikko: %s (Käytetty %d kerran)</item>
+ <item quantity="other">Kolikko: %s (Käytetty %d kertaa)</item>
+ </plurals>
+ <string name="exchange_fee_withdraw_fee">Nostomaksu: %s</string>
+ <string name="exchange_fee_deposit_fee">Talletus maksu: %s</string>
+ <string name="exchange_fee_refresh_fee">Vahto maksu: %s</string>
+ <string name="exchange_fee_refund_fee">Hyvitysmaksu: %s</string>
+ <string name="exchange_fee_wire_fee_timespan">Ajanjakso: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Siirtomaksu: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Sulkemismaksu: %s</string>
+ <string name="exchange_tos_accept">Hyväksy käyttöehdot</string>
+ <string name="exchange_tos_error">Virhe näytettäessä käyttöehtoja: %s</string>
+ <string name="pending_operations_title">Odottavat toiminnot</string>
+ <string name="pending_operations_refuse">Hylkää ehdotus</string>
+ <string name="pending_operations_no_action">(ei toimintaa)</string>
+ <string name="settings_dev_mode">Kehittäjätila</string>
+ <string name="settings_dev_mode_summary">Näyttää enemmän virheenkorjaukseen tarkoitettuja tietoja</string>
+ <string name="settings_withdraw_testkudos">Nosta TESTKUDOS</string>
+ <string name="settings_withdraw_testkudos_summary">Hanki rahaa testauksesta</string>
+ <string name="settings_logcat">Virheenkorjausloki</string>
+ <string name="settings_logcat_summary">Tallenna sisäinen loki</string>
+ <string name="settings_logcat_error">Virhe lokin viemisessä</string>
+ <string name="settings_logcat_success">Loki viety tiedostoon</string>
+ <string name="settings_db_export">Vie tietokanta</string>
+ <string name="settings_db_export_summary">Tallenna sisäinen tietokanta</string>
+ <string name="settings_db_import">Tuo tietokanta</string>
+ <string name="settings_db_import_summary">Palauta tietokanta tiedostosta</string>
+ <string name="settings_db_export_error">Virhe vietäessä tietokantaa</string>
+ <string name="settings_db_import_error">Virhe tietokannan tuonnissa</string>
+ <string name="settings_db_clear_error">Virhe tietokannan puhdistuksessa</string>
+ <string name="settings_db_export_success">Tietokanta viety tiedostoon</string>
+ <string name="settings_db_import_success">Tietokanta tuotu tiedostosta</string>
+ <string name="settings_version_app">Sovelluksen versio</string>
+ <string name="settings_version_core">Lompakon ydinversio</string>
+ <string name="settings_version_protocol_exchange">Tuetut Exchange-versiot</string>
+ <string name="settings_version_protocol_merchant">Tuetut kauppiasversiot</string>
+ <string name="settings_version_unknown">Tuntematon</string>
+ <string name="settings_test">Suorita integrointitesti</string>
+ <string name="settings_test_summary">Suorittaa testitapahtumia demoasetuksella</string>
+ <string name="settings_reset">Nollaa lompakko (vaarallista!)</string>
+ <string name="settings_reset_summary">Heittää rahasi pois</string>
+ <string name="settings_dialog_reset_message">Haluatko todella nollata lompakon ja menettää kaikki kolikot ja ostokset?</string>
+ <string name="settings_dialog_import_message">Tämä toiminto korvaa olemassa olevan tietokantasi. Haluatko jatkaa?</string>
+ <string name="settings_alert_reset_done">Lompakko on nollattu</string>
+ <string name="settings_alert_reset_canceled">Nollaus peruutettu</string>
+ <string name="settings_alert_import_canceled">Tuonti peruutettu</string>
+ <string name="refund_title">Palautus</string>
+ <string name="refund_error">Palautuksen käsittelyssä tapahtui virhe</string>
+ <string name="refund_success">Palautus saatu!</string>
+ <string name="wifi_disabled_error">Ota Wi-Fi käyttöön saadaksesi ilmaisen Wi-Fi-yhteyden</string>
+ <string name="wifi_connect_error">Ei voitu muodostaa yhteyttä ilmaiseen Wi-Fi-verkkoon: %s</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-fr/strings.xml b/wallet/src/main/res/values-fr/strings.xml
index 00c97fc..e4ea6c8 100644
--- a/wallet/src/main/res/values-fr/strings.xml
+++ b/wallet/src/main/res/values-fr/strings.xml
@@ -1,22 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="menu_retry_pending_operations">Réessayer les opérations en attente</string>
- <string name="balances_empty_state">Il n\'y a pas d\'argent éléctronique dans votre portefeuille.
+ <string name="balances_empty_state">Il n\'y a pas d\'argent numérique dans votre portefeuille.
\n
\nVous pouvez obtenir de l\'argent de test auprès de la banque de démonstration :
\n
\nhttps://bank.demo.taler.net</string>
- <string name="withdraw_error_message">Pour l\'instant il n\'est pas possible de retirer. Veuillez réessayer plus tard !</string>
- <plurals name="exchange_fee_coin">
- <item quantity="one">Pièce : %s (utilisée %d fois)</item>
- <item quantity="other">Pièce : %s (utilisée %d fois)</item>
- </plurals>
+ <string name="withdraw_error_message">Le retrait n\'est actuellement pas possible. Veuillez réessayer plus tard !</string>
<string name="settings_dev_mode">Mode développeur</string>
- <string name="menu_settings">Réglages</string>
+ <string name="menu_settings">Paramètres</string>
<string name="settings_reset">Réinitialiser le portefeuille (dangereux!)</string>
<string name="app_name">Portefeuille Taler</string>
<string name="google_play_title" tools:keep="@string/google_play_title">Portefeuille Taler (expérimental)</string>
- <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler facilite et accélère les transactions en ligne respectueuses de la vie privée.</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler rend les transactions en ligne respectueuses de la vie privée, rapides et faciles.</string>
<string name="button_scan_qr_code">Scanner le QR code Taler</string>
<string name="balances_title">Soldes</string>
<string name="amount_positive">+ %s</string>
@@ -31,25 +27,25 @@
<string name="transactions_select_all">Tout sélectionner</string>
<string name="transaction_paid">Payé</string>
<string name="transaction_order_total">Total</string>
- <string name="transaction_order">Acheter</string>
- <string name="transaction_order_id">Reçu numéro %1$s</string>
+ <string name="transaction_order">Achat</string>
+ <string name="transaction_order_id">Reçu #%1$s</string>
<string name="transaction_refund">Remboursement</string>
- <string name="transaction_refresh">Monnaie obtenue</string>
+ <string name="transaction_refresh">Frais de change pour la monnaie expirée</string>
<string name="payment_fee">+%s frais de transaction</string>
<string name="payment_button_confirm">Confirmer le paiement</string>
<string name="nav_exchange_fees">Frais de change</string>
<string name="amount_negative">- %s</string>
<string name="nav_header_title">GNU Taler</string>
<string name="nav_header_subtitle">Portefeuille</string>
- <string name="nav_prompt_withdraw">Retirer de l\'argent électronique</string>
- <string name="nav_exchange_tos">Conditions générale d\'utilisation du change</string>
+ <string name="nav_prompt_withdraw">Retirer de l\'argent numérique</string>
+ <string name="nav_exchange_tos">Conditions générales d\'utilisation du service d\'échange</string>
<string name="nav_error">Erreur</string>
- <string name="button_back">Retour</string>
+ <string name="button_back">Retour en arrière</string>
<string name="payment_label_order_summary">Achat</string>
<string name="payment_balance_insufficient">Solde insuffisant !</string>
<string name="payment_already_paid_title">Déjà payé</string>
- <string name="payment_already_paid">Vous avez déjà payé cet achat.</string>
- <string name="withdraw_total">Retrait</string>
+ <string name="payment_already_paid">Vous avez déjà payé pour cet achat.</string>
+ <string name="withdraw_total">Retirer</string>
<string name="withdraw_fees">Frais</string>
<string name="withdraw_exchange">Change</string>
<string name="withdraw_button_confirm">Confirmer le retrait</string>
@@ -57,7 +53,7 @@
<string name="withdraw_error_title">Erreur de retrait</string>
<string name="exchange_fee_withdrawal_fee_label">Frais de retrait :</string>
<string name="exchange_fee_overhead_label">Erreur d\'arrondi :</string>
- <string name="exchange_fee_coin_fees_label">Frais de pièces</string>
+ <string name="exchange_fee_coin_fees_label">Frais de monnaie</string>
<string name="exchange_fee_wire_fees_label">Frais de virement</string>
<string name="exchange_fee_withdraw_fee">Frais de retrait : %s</string>
<string name="exchange_fee_deposit_fee">Frais de dépôt : %s</string>
@@ -66,7 +62,7 @@
<string name="exchange_fee_wire_fee_timespan">Durée : %1$s - %2$s</string>
<string name="exchange_fee_wire_fee_wire_fee">Frais de virement : %s</string>
<string name="exchange_fee_wire_fee_closing_fee">Frais de clôture : %s</string>
- <string name="exchange_tos_accept">Accepter les conditions générales d\'utilisations</string>
+ <string name="exchange_tos_accept">Accepter les conditions générales d\'utilisation</string>
<string name="exchange_tos_error">Erreur lors de l\'affichage des conditions générales d\'utilisation : %s</string>
<string name="pending_operations_title">Opérations en attente</string>
<string name="pending_operations_refuse">Refuser la proposition</string>
@@ -76,9 +72,133 @@
<string name="settings_withdraw_testkudos_summary">Obtenez de l\'argent pour les tests</string>
<string name="settings_reset_summary">Détruit votre argent</string>
<string name="refund_error">Erreur lors du traitement du remboursement</string>
- <string name="refund_success">Remboursement reçu: %s</string>
+ <string name="refund_success">Remboursement reçu : %s</string>
<string name="payment_error">Erreur : %s</string>
- <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Cette application est un portefeuille pour GNU Taler. Elle est très expérimentale et n\'est pas encore prête à être utilisée en production. GNU Taler est un système de paiement préservant la vie privée. Les clients peuvent rester anonymes, mais les commerçants ne peuvent pas cacher leurs revenus par des paiements avec GNU Taler. Cela permet d\'éviter l\'évasion fiscale et le blanchiment d\'argent. Le principal cas d\'utilisation de GNU Taler est celui des paiements ; il ne s\'agit pas d\'une réserve de valeur. Les paiements sont toujours garantis par une devise existante. Les paiements sont effectués après avoir échangé l\'argent existant en monnaie électronique avec l\'aide d\'un service d\'échange, c\'est-à-dire un fournisseur de services de paiement pour le Taler. Pour effectuer un paiement, les clients n\'ont besoin que d\'un portefeuille chargé. Un commerçant peut accepter des paiements sans que ses clients ne s\'inscrivent sur son site web. GNU Taler est immunisé contre de nombreux types de fraude, tels que l\'hameçonnage des informations relatives aux cartes de crédit ou la fraude par rétrofacturation. En cas de perte ou de vol, seule la quantité limitée d\'argent restant dans le portefeuille peut disparaître.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Cette application est un portefeuille pour GNU Taler. Cette dernière est expérimentale et n\'est pas encore prête à être utilisée en production. GNU Taler est un système de paiement préservant la vie privée. Les clients peuvent rester anonymes, mais les commerçants ne peuvent pas cacher leurs revenus avec GNU Taler. Cela permet d\'éviter l\'évasion fiscale et le blanchiment d\'argent. Le principal cas d\'utilisation de GNU Taler est celui des paiements ; il ne s\'agit pas d\'une réserve de valeur. Les paiements sont toujours garantis par une devise existante. Les paiements sont effectués après avoir échangé l\'argent existant en monnaie électronique avec l\'aide d\'un service d\'échange, qui est un prestataire de services de paiement pour Taler. Pour effectuer un paiement, les clients n\'ont besoin que d\'un portefeuille chargé. Un commerçant peut accepter des paiements sans demander à ses clients de s\'inscrire sur son site web. GNU Taler est immunisé contre de nombreux types de fraude, tels que l\'hameçonnage de cartes de crédit ou la fraude par rétrofacturation. En cas de perte ou de vol, seul le montant limité d\'argent restant dans le portefeuille peut être perdu.</string>
<string name="payment_label_amount_total">Montant total :</string>
- <string name="exchange_fee_coin_expiration_label">Expiration de la pièce au plus tôt :</string>
-</resources>
+ <string name="exchange_fee_coin_expiration_label">Expiration de monnaie prochaine :</string>
+ <string name="ok">OK</string>
+ <string name="cancel">Annuler</string>
+ <string name="offline">L\'opération nécessite un accès Internet. Veuillez vous assurer que votre connexion internet fonctionne et réessayez.</string>
+ <string name="error_unsupported_uri">Erreur : Cet URI Taler n\'est pas pris en charge.</string>
+ <string name="transactions_empty_search">Aucune transaction trouvée. Essayez une recherche différente.</string>
+ <string name="transaction_refund_from">Remboursement %s</string>
+ <string name="transaction_pending">EN ATTENTE</string>
+ <string name="payment_title">Paiement</string>
+ <string name="payment_aborted">Annulé</string>
+ <string name="payment_initiated">Paiement initié</string>
+ <string name="nav_exchange_select">Sélectionner le change</string>
+ <string name="enter_uri">Entrer l\'URI de Taler</string>
+ <string name="paste">Coller</string>
+ <string name="paste_invalid">Le presse-papiers contient un type de données non valide</string>
+ <string name="uri_invalid">URI Taler non valide</string>
+ <string name="search">Rechercher</string>
+ <string name="menu">Menu</string>
+ <string name="or">ou</string>
+ <string name="transaction_tip_from">Pourboire de %s</string>
+ <string name="payment_failed">Échec</string>
+ <string name="withdraw_initiated">Retrait initié</string>
+ <string name="withdraw_title">Retrait</string>
+ <string name="withdraw_button_confirm_bank">Confirmer avec la banque</string>
+ <string name="withdraw_waiting_confirm">En attente de confirmation</string>
+ <string name="withdraw_amount">Retirer quel montant \?</string>
+ <string name="withdraw_amount_error">Indiquer un montant valide</string>
+ <string name="withdraw_manual_payment_options">Modes de paiement fournis par %1$s :
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">Vérifier les frais</string>
+ <string name="host_apdu_service_desc">Paiements Taler NFC</string>
+ <string name="withdraw_manual_title">Effectuer un transfert manuel vers la bourse</string>
+ <string name="exchange_settings_title">Échanges</string>
+ <string name="exchange_settings_summary">Gérer la liste des échanges connus par ce portefeuille</string>
+ <string name="exchange_list_title">Échanges</string>
+ <string name="exchange_list_currency">Devise : %s</string>
+ <string name="exchange_menu_manual_withdraw">Retirer</string>
+ <string name="settings_logcat">Historique des bogues</string>
+ <string name="settings_logcat_summary">Voir/Envoyer un historique interne</string>
+ <string name="settings_version_app">Version mobile</string>
+ <string name="settings_version_unknown">Inconnu</string>
+ <string name="nav_settings_backup">Sauvegarde</string>
+ <string name="backup_last">Dernière sauvegarde : %s</string>
+ <string name="refund_title">Remboursement</string>
+ <string name="tip_title">Pourboire</string>
+ <string name="wifi_disabled_error">Activer le Wi-Fi pour profiter du Wi-Fi gratuit</string>
+ <string name="wifi_connect_error">Connection au Wi-Fi impossible : %s</string>
+ <string name="exchange_list_add">Ajoutez un échange</string>
+ <string name="exchange_add_error">L\'échange n\'a pas pu être ajouté</string>
+ <string name="withdraw_error_test">Erreur de retrait TESTKUDOS</string>
+ <string name="exchange_list_empty">Aucun échange connu
+\n
+\nAjoutez-en un manuellement ou retirer de l\'argent électronique !</string>
+ <string name="exchange_add_url">Entrez l\'adresse de l\'échange</string>
+ <string name="settings_version_protocol_merchant">Versions commerçantes compatibles</string>
+ <string name="settings_version_core">Version Wallet Core</string>
+ <string name="settings_version_protocol_exchange">Versions Echange compatibles</string>
+ <string name="transaction_action_kyc">Compléter le KYC</string>
+ <string name="transactions_cancel_dialog_message">Êtes-vous sûr de vouloir annuler ce retrait \? Les fonds encore en transit peuvent être perdus.</string>
+ <string name="transaction_deposit">Dépôt</string>
+ <string name="receive_withdraw">Retrait du compte bancaire</string>
+ <string name="receive_peer">Facture d\'un autre portefeuille</string>
+ <string name="receive_peer_title">Demande de paiement</string>
+ <string name="receive_peer_create_button">Créer une facture</string>
+ <string name="transactions_delete_dialog_title">Supprimer une transaction</string>
+ <string name="transactions_delete_dialog_message">Êtes-vous sûr de vouloir supprimer cette transaction de votre portefeuille \?</string>
+ <string name="receive_amount_invalid">Montant invalide</string>
+ <string name="receive_intro">Choisir où recevoir l\'argent :</string>
+ <string name="receive_peer_invoice_instruction">Laissez le payeur scanner ce QR code pour payer :</string>
+ <string name="copy" tools:override="true">Copier</string>
+ <string name="transactions_balance">Solde</string>
+ <string name="transaction_peer_pull_debit">Facture payée</string>
+ <string name="transaction_peer_push_credit">Paiement push</string>
+ <string name="copy_uri">Copier l\'URI de Taler</string>
+ <string name="transaction_peer_push_debit">Paiement push</string>
+ <string name="transaction_peer_pull_credit">Facture</string>
+ <string name="receive_amount">Montant à recevoir</string>
+ <string name="send_amount">Montant à envoyer</string>
+ <string name="send_intro">Choisir où envoyer l\'argent :</string>
+ <string name="transactions_detail_title_currency">%s Transactions</string>
+ <string name="currency">Devise</string>
+ <string name="transactions_receive_funds">Recevoir
+\ndes fonds</string>
+ <string name="transactions_send_funds">Envoyer
+\ndes fonds</string>
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="transactions_delete_selected_dialog_message">Êtes-vous sûr de vouloir supprimer les transactions sélectionnées de votre portefeuille \?</string>
+ <string name="receive_peer_invoice_uri">Sinon, copier et envoyer cet URI :</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="send_deposit_name">Titulaire du compte</string>
+ <string name="send_deposit_bitcoin_address">Adresse Bitcoin</string>
+ <string name="send_deposit_bitcoin_create_button">Transfert de Bitcoin</string>
+ <string name="send_peer_title">Envoyer de l\'argent vers un autre portefeuille</string>
+ <string name="send_peer_payment_amount_received">Montant reçu</string>
+ <string name="send_peer_payment_amount_sent">Montant envoyé</string>
+ <string name="withdraw_restrict_age">Limiter l\'utilisation selon l\'âge</string>
+ <string name="pay_peer_title">Payer la facture</string>
+ <string name="send_peer_warning">Avertissement : Les fonds quittent immédiatement le portefeuille.</string>
+ <string name="send_peer_payment_instruction">Laissez le bénéficiaire scanner ce code QR pour recevoir :</string>
+ <string name="send_deposit_check_fees_button">Vérifier les frais</string>
+ <string name="send_deposit_amount_effective">Montant effectif</string>
+ <string name="send_deposit_create_button">Effectuer un dépôt</string>
+ <string name="send_peer">Vers un autre portefeuille</string>
+ <string name="pay_peer_intro">Voulez-vous payer cette facture \?</string>
+ <string name="receive_peer_payment_title">Recevoir le paiement</string>
+ <string name="receive_peer_payment_intro">Voulez-vous recevoir ce paiement \?</string>
+ <string name="send_peer_purpose">Objectif</string>
+ <string name="send_deposit_bitcoin">Vers un portefeuille Bitcoin</string>
+ <string name="send_peer_bitcoin">Vers un autre portefeuille Taler</string>
+ <string name="send_peer_create_button">Envoyer des fonds maintenant</string>
+ <string name="send_deposit">Vers un compte bancaire</string>
+ <string name="send_deposit_title">Dépôt sur un compte bancaire</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="withdraw_restrict_age_unrestricted">Sans restriction</string>
+ <string name="withdraw_manual_ready_subject">Sujet</string>
+ <string name="withdraw_manual_ready_cancel">Annuler le retrait</string>
+ <string name="withdraw_manual_bitcoin_title">Le change de Bitcoin prêt pour les retraits</string>
+ <string name="withdraw_manual_bitcoin_intro">Effectuez maintenant une transaction fractionnée avec les trois sorties suivantes.</string>
+ <string name="withdraw_manual_ready_bank_button">Ouvrir dans l\'application bancaire</string>
+ <string name="withdraw_manual_ready_title">Le change est prêt pour le retrait !</string>
+ <string name="withdraw_manual_ready_intro">Pour compléter le processus, vous devez virer %s sur le compte bancaire de change</string>
+ <string name="withdraw_manual_ready_details_intro">Détails du virement bancaire</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_warning">Assurez-vous d\'utiliser le bon sujet, sinon l\'argent n\'arrivera pas dans ce portefeuille.</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-it/strings.xml b/wallet/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000..fdc4594
--- /dev/null
+++ b/wallet/src/main/res/values-it/strings.xml
@@ -0,0 +1,242 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="app_name">Wallet Taler</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">Wallet Taler (sperimentale)</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler rende veloci e agevoli le transazioni online rispettose della privacy.</string>
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="nav_header_subtitle">Wallet</string>
+ <string name="paste_invalid">Gli appunti contengono un tipo di dato non valido</string>
+ <string name="offline">L\'operazione richiede l\'accesso a Internet. Si prega di assicurarsi che la connessione Internet funzioni e riprovare.</string>
+ <string name="balances_empty_state">Non è presente denaro elettronico nel tuo wallet.
+\n
+\nPuoi ottenere denaro di prova dalla banca demo:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="nav_prompt_withdraw">Prelievo di Denaro Elettronico</string>
+ <string name="nav_exchange_tos">Termini di Servizio dello Scambio</string>
+ <string name="nav_exchange_select">Seleziona Scambio</string>
+ <string name="nav_exchange_fees">Tariffe di Scambio</string>
+ <string name="nav_error">Errore</string>
+ <string name="button_back">Indietro</string>
+ <string name="enter_uri">Inserisci URI Taler</string>
+ <string name="uri_invalid">URI Taler non valido</string>
+ <string name="ok">OK</string>
+ <string name="cancel">Cancella</string>
+ <string name="search">Cerca</string>
+ <string name="menu">Menu</string>
+ <string name="or">oppure</string>
+ <string name="error_unsupported_uri">Errore: Questo URI Taler non è supportato.</string>
+ <string name="menu_settings">Impostazioni</string>
+ <string name="menu_retry_pending_operations">Ritentare Operazioni in Corso</string>
+ <string name="host_apdu_service_desc">Pagamenti Taler NFC</string>
+ <string name="balances_title">Saldi</string>
+ <string name="amount_positive">+ %s</string>
+ <string name="amount_negative">- %s</string>
+ <string name="amount_chosen">Quantità Scelta</string>
+ <string name="balances_inbound_label">in arrivo</string>
+ <string name="transactions_title">Transazioni</string>
+ <string name="transactions_empty">Non hai nessuna transazione</string>
+ <string name="paste">Incolla</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Quest\'app è un wallet per GNU Taler. È altamente sperimentale, e non è ancora pronta per l\'uso nella produzione. GNU Taler è un sistema di pagamento che preserva la privacy. Gli utenti possono rimanere anonimi, ma i venditori non possono nascondere i loro guadagni tramite i pagamenti con GNU Taler. Questo aiuta ad evitare l\'evasione fiscale e il riciclaggio di denaro sporco. Il caso d\'uso principale di GNU Taler sono i pagamenti; esso non è inteso come un deposito di valuta. I pagamenti sono sempre finanziati da una valuta esistente. I pagamenti sono effettuati dopo aver scambiato del denaro esistente con del denaro elettronico con l\'aiuto di un servizio di Scambio, vale a dire, di un provider di servizi di pagamento per Taler. Quando effettuano un pagamento, gli utenti hanno bisogno soltanto di un wallet carico. Un venditore può accettare pagamenti senza che i suoi clienti si registrino sul suo sito web. GNU Taler è immune da molti tipi di frode, come ad esempio il phishing delle informazioni sulle carte di credito o la frode di storno di addebito. In caso di smarrimento o furto, soltanto la limitata quantità di denaro lasciata nel wallet potrebbe andare perduta.</string>
+ <string name="button_scan_qr_code">Scansiona il Codice QR Taler</string>
+ <string name="transactions_error">Impossibile caricare transazioni
+\n
+\n%s</string>
+ <string name="transactions_detail_title">Transazioni</string>
+ <string name="transactions_detail_title_balance">Saldo: %s</string>
+ <string name="transactions_delete">Elimina</string>
+ <string name="transactions_select_all">Seleziona tutto</string>
+ <string name="transaction_paid">Pagato</string>
+ <string name="transaction_order_total">Totale</string>
+ <string name="transaction_order">Acquisto</string>
+ <string name="transaction_tip_from">Ricompensa da %s</string>
+ <string name="transaction_refund">Rimborso</string>
+ <string name="transaction_refund_from">Rimborso di %s</string>
+ <string name="transaction_pending">IN ATTESA</string>
+ <string name="payment_title">Pagamento</string>
+ <string name="transactions_empty_search">Nessun transazione trovata. Prova una ricerca diversa.</string>
+ <string name="transaction_order_id">Ricevuta #%1$s</string>
+ <string name="transaction_refresh">Commissione per cambio della moneta a scadenza</string>
+ <string name="payment_fee">+%s tassa di commissione</string>
+ <string name="payment_button_confirm">Confermare pagamento</string>
+ <string name="payment_already_paid">Hai già pagato questo ordine.</string>
+ <string name="exchange_settings_summary">Gestire la lista di cambi conosciuti in questo portafoglio</string>
+ <string name="exchange_add_error">Non è stato possibile aggiungere il cambio</string>
+ <plurals name="exchange_fee_coin">
+ <item quantity="one">Moneta: %s (usata %d volte)</item>
+ <item quantity="other">Moneta: %s (usate %d volte)</item>
+ </plurals>
+ <string name="settings_withdraw_testkudos">Ritirare TESTKUDOS</string>
+ <string name="settings_logcat_summary">Salvare log interno</string>
+ <string name="settings_reset_summary">Scarta il tuo denaro</string>
+ <string name="wifi_disabled_error">Accendi il Wi-Fi per avere Wi-Fi gratis</string>
+ <string name="payment_label_amount_total">Quantità totale:</string>
+ <string name="payment_label_order_summary">Ordine</string>
+ <string name="payment_error">Errore: %s</string>
+ <string name="payment_balance_insufficient">Credito insufficiente!</string>
+ <string name="payment_aborted">Interrotto</string>
+ <string name="payment_failed">Fallito</string>
+ <string name="payment_initiated">Pagamento iniziato</string>
+ <string name="payment_already_paid_title">Pagato già</string>
+ <string name="withdraw_initiated">Ritiro iniziato</string>
+ <string name="withdraw_title">Ritiro</string>
+ <string name="withdraw_total">Ritirare</string>
+ <string name="withdraw_fees">Tassa</string>
+ <string name="withdraw_exchange">Cambio</string>
+ <string name="withdraw_button_confirm">Confermare ritiro</string>
+ <string name="withdraw_button_confirm_bank">Confermare con la banca</string>
+ <string name="withdraw_button_tos">Rivedere i termini</string>
+ <string name="withdraw_waiting_confirm">In attesa di conferma</string>
+ <string name="withdraw_amount">Quanto desideri ritirare\?</string>
+ <string name="withdraw_amount_error">Inserisci una quantità valida</string>
+ <string name="withdraw_manual_payment_options">Opzioni di pagamento sostenute da %1$s
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">Tassa di controllo</string>
+ <string name="withdraw_error_title">Errore nel ritiro</string>
+ <string name="withdraw_error_message">Il ritiro non è attualmente possibile. Per piacere riprova più tardi!</string>
+ <string name="withdraw_error_test">Errore nel ritiro TESTKUDOS</string>
+ <string name="exchange_settings_title">Cambi</string>
+ <string name="exchange_list_title">Cambi</string>
+ <string name="exchange_list_empty">Nessun cambio riconosciuto
+\n
+\nInseriscilo manualmente o ritira dei contanti digitali!</string>
+ <string name="exchange_list_currency">Valuta: %s</string>
+ <string name="exchange_list_add">Aggiungere cambio</string>
+ <string name="exchange_add_url">Inserire l\'indirizzo del cambio</string>
+ <string name="exchange_menu_manual_withdraw">Ritirare</string>
+ <string name="exchange_fee_withdrawal_fee_label">Tassa di commissione:</string>
+ <string name="exchange_fee_overhead_label">Perdita arrotondata:</string>
+ <string name="exchange_fee_coin_expiration_label">Prima scadenza monetaria:</string>
+ <string name="exchange_fee_coin_fees_label">Tassa monetaria</string>
+ <string name="exchange_fee_wire_fees_label">Commissione di bonifico</string>
+ <string name="exchange_fee_withdraw_fee">Tassa di commissione: %s</string>
+ <string name="exchange_fee_deposit_fee">Tassa di deposito: %s</string>
+ <string name="exchange_fee_refresh_fee">Tassa di cambio: %s</string>
+ <string name="exchange_fee_refund_fee">Tassa di rimborso: %s</string>
+ <string name="exchange_fee_wire_fee_timespan">Intervallo di tempo: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Tassa di bonifico: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Tassa di chiusura: %s</string>
+ <string name="exchange_tos_accept">Accettare i termini di servizio</string>
+ <string name="exchange_tos_error">Errore nel mostrare i Termini di servizio: %s</string>
+ <string name="pending_operations_title">Operazioni in attesa</string>
+ <string name="pending_operations_refuse">Rifiutare la richiesta</string>
+ <string name="pending_operations_no_action">(nessuna azione)</string>
+ <string name="settings_dev_mode">Modo Sviluppatore</string>
+ <string name="settings_dev_mode_summary">Mostra più informazioni riguardo la correzione</string>
+ <string name="settings_withdraw_testkudos_summary">Ottieni denaro per provare</string>
+ <string name="settings_logcat">Log di debug</string>
+ <string name="settings_version_app">Versione App</string>
+ <string name="settings_version_core">Portafoglio Versione Principale</string>
+ <string name="settings_version_protocol_exchange">Supportate Versioni Exchange</string>
+ <string name="settings_version_protocol_merchant">Supportate Versioni Commerciali</string>
+ <string name="settings_version_unknown">Sconosciuto</string>
+ <string name="settings_reset">Resettare il portafoglio (Pericoloso!)</string>
+ <string name="nav_settings_backup">Backup</string>
+ <string name="backup_last">Ultimo Backup: %s</string>
+ <string name="refund_title">Rimborso</string>
+ <string name="refund_error">Errore nel processare il rimborso</string>
+ <string name="refund_success">Rimborso ricevuto: %s</string>
+ <string name="tip_title">Ricompensa</string>
+ <string name="wifi_connect_error">Impossibile connettersi alla Wi-Fi gratuita: %s</string>
+ <string name="withdraw_manual_title">Effettuare un trasferimento manuale per lo scambio</string>
+ <string name="exchange_list_error">Impossibile elencare i cambi</string>
+ <string name="tip_total">Totale</string>
+ <string name="tip_already_accepted">Questa ricompensa è già stata accettata.</string>
+ <string name="receive_peer">Fattura un altro portafoglio</string>
+ <string name="transaction_dummy_title">Transazione Sconosciuta</string>
+ <string name="receive_peer_title">Richiedi pagamento</string>
+ <string name="send_deposit_iban_error">IBAN non valido</string>
+ <string name="send_deposit_bic_error">BIC non valido</string>
+ <string name="send_deposit_name">Titolare del conto</string>
+ <string name="pay_peer_title">Paga fattura</string>
+ <string name="send_peer_purpose">Causale</string>
+ <string name="withdraw_restrict_age_unrestricted">Senza restrizioni</string>
+ <string name="settings_db_export_success">Banca dati esportata nel file</string>
+ <string name="tip_received">Ricompensa ricevuta</string>
+ <string name="transactions_retry">Riprova</string>
+ <string name="transactions_suspend">Sospendi</string>
+ <string name="transactions_resume">Riprendi</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="send_peer_title">Invia denaro ad un altro portafoglio</string>
+ <string name="send_peer_create_button">Invia fondi adesso</string>
+ <string name="send_peer_warning">Attenzione: I fondi lasceranno il portafogli immediatamente.</string>
+ <string name="withdraw_manual_ready_title">Lo scambio è pronto per essere prelevato!</string>
+ <string name="withdraw_manual_ready_details_intro">Dettagli del trasferimento bancario</string>
+ <string name="withdraw_manual_ready_subject">Soggetto</string>
+ <string name="transaction_action_kyc">Completa KYC</string>
+ <string name="pay_peer_intro">Vuoi pagare questa fattura\?</string>
+ <string name="transactions_delete_dialog_title">Rimuovi Transazione</string>
+ <string name="transactions_delete_dialog_message">Sei sicuro di voler rimuovere questa transazione dal tuo portafogli?</string>
+ <string name="transaction_deposit">Deposita</string>
+ <string name="receive_peer_payment_title">Ricevi pagamento</string>
+ <string name="receive_peer_payment_intro">Vuoi ricevere questo pagamento\?</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="transactions_send_funds">Invia
+\nFondi</string>
+ <string name="withdraw_manual_ready_warning">Assicurati di usare il soggetto corretto, altrimenti il denaro non arriverà in questo portafogli.</string>
+ <string name="settings_db_export">Banca dati</string>
+ <string name="withdraw_manual_ready_intro">Per completare il processo devi trasferire %s al tuo conto bancario di cambio</string>
+ <string name="send_peer_expiration_1d">1 giorno</string>
+ <string name="send_peer_expiration_7d">7 giorni</string>
+ <string name="send_peer_expiration_period">Scade in</string>
+ <string name="send_peer_expiration_custom">Personalizzato</string>
+ <string name="send_peer_expiration_30d">30 giorni</string>
+ <string name="send_peer_expiration_days">Giorni</string>
+ <string name="send_peer_expiration_hours">Ore</string>
+ <string name="withdraw_manual_ready_bank_button">Apri nell\'applicazione bancaria</string>
+ <string name="settings_db_export_summary">Salva banca dati interna</string>
+ <string name="settings_test_summary">Realizza transazioni di prova con la configurazione demo</string>
+ <string name="settings_test">Avvia test d\'integrazione</string>
+ <string name="copy" tools:override="true">Copia</string>
+ <string name="transactions_receive_funds">Ricevi
+\nFondi</string>
+ <string name="transaction_peer_pull_credit">Fattura</string>
+ <string name="send_amount">Importo da inviare</string>
+ <string name="withdraw_manual_ready_cancel">Cancella prelievo</string>
+ <string name="currency">Valuta</string>
+ <string name="send_peer_payment_amount_sent">Importo inviato</string>
+ <string name="send_peer_payment_amount_received">Importo ricevuto</string>
+ <string name="settings_logcat_error">Errore di espostazione del log</string>
+ <string name="settings_logcat_success">Log esportato nel file</string>
+ <string name="tip_fees">Commissione</string>
+ <string name="tip_merchant_url">URL del commerciante</string>
+ <string name="button_scan_qr_code_label">Scannerizza codice QR</string>
+ <string name="transaction_peer_pull_debit">Fattura pagata</string>
+ <string name="receive_intro">Scegli da dove ricevere il denaro:</string>
+ <string name="receive_withdraw">Preleva dall\'account bancario</string>
+ <string name="receive_peer_create_button">Crea fattura</string>
+ <string name="receive_peer_invoice_instruction">Fai scansionare al pagante questo codice QR per pagare:</string>
+ <string name="receive_peer_invoice_uri">In alternativa, copia e invia questo URI:</string>
+ <string name="send_intro">Scegli dove inviare il denaro:</string>
+ <string name="send_deposit">Ad un conto bancario</string>
+ <string name="send_deposit_title">Deposita ad un conto bancario</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="tip_button_confirm">Accetta ricompensa</string>
+ <string name="send_deposit_bitcoin">Ad un portafoglio Bitcoin</string>
+ <string name="send_peer_bitcoin">Ad un altro portafogli Taler</string>
+ <string name="transactions_delete_selected_dialog_message">Sei sicuro di voler rimuovere le transazioni selezionate dal tuo portafogli?</string>
+ <string name="transactions_cancel_dialog_message">Sei sicuro di voler cancellare questo prelievo? I fondi in transito potrebbero andare persi.</string>
+ <string name="send_deposit_bitcoin_address">Indirizzo Bitcoin</string>
+ <string name="send_deposit_check_fees_button">Controlla commissioni</string>
+ <string name="send_deposit_amount_effective">Importo Effetivo</string>
+ <string name="send_deposit_create_button">Fai un deposito</string>
+ <string name="send_deposit_bitcoin_create_button">Trasferisci Bitcoin</string>
+ <string name="send_peer">Ad un altro portafoglio</string>
+ <string name="settings_db_export_error">Errore nell\'esportazione della banca dati</string>
+ <string name="transactions_abort">Annulla</string>
+ <string name="transactions_fail">Arresta</string>
+ <string name="transactions_abort_dialog_title">Annulla la Transazione</string>
+ <string name="transactions_fail_dialog_title">Annulla la Transazione</string>
+ <string name="transactions_fail_dialog_message">Sei sicuro di voler annullare questa transazione? I fondi ancora in transito ANDRANNO PERSI.</string>
+ <string name="transactions_abort_dialog_message">Sei sicuro di voler annullare questa transazione? I fondi ancora in transito potrebbero andare persi.</string>
+ <string name="copy_uri">Copia URI Taler</string>
+ <string name="transactions_balance">Saldo</string>
+ <string name="offline_banner">Nessun accesso ad internet</string>
+ <string name="transaction_peer_push_debit">Pagamento Push</string>
+ <string name="transactions_detail_title_currency">%sTransazioni</string>
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="error_broken_uri">Errore: Questo URI Taler è ( al momento ) non funzionante.</string>
+ <string name="import_db">Importa</string>
+ <string name="reset">Resetta</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-ko/strings.xml b/wallet/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000..e050aba
--- /dev/null
+++ b/wallet/src/main/res/values-ko/strings.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="app_name">탈러 지갑</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">탈러 지갑 (실험판)</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU 탈러는 사생활 보호가 되는 온라인 거래를 빠르고 쉽게 만듭니다.</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">이 어플리케이션은 GNU 탈러를 위한 지갑입니다. 실험적이며 아직 상용화로는 준비되어 있지 않습니다. GNU 탈러는 사생활 보호를 해주는 결제 시스템입니다. 고객은 익명성을 유지할 수 있지만, 상인은 GNU 탈러를 통한 결제로 얻은 수익을 숨길 수 없습니다. 이는 탈세와 돈세탁을 예방해줍니다. GNU 탈러의 주 용도는 결제입니다; 가치를 저장하는 용도가 아닙니다. 결제는 언제나 현존하는 통화로 뒷받침되어 있습니다. 결제는 거래소, 즉 탈러를 위한 결제 서비스 제공자의 도움으로 실존하는 화폐에서 전자화폐로 교환한 이후 결제가 진행됩니다. 결제 시, 고객은 충전된 지갑만 있으면 됩니다. 상인은 고객이 상인의 사이트에 등록하지 않고도 결제를 하도록 승인할 수 있습니다. GNU 탈러는 신용카드 정보 피싱 혹은 지불거절 사기 등 여러 종류의 사기에 면역이 되어 있습니다. 분실 혹은 도난의 경우, 지갑에 남아있던 제한적인 금액의 돈만 잃게 됩니다.</string>
+ <string name="payment_button_confirm">결제 확인</string>
+ <string name="payment_balance_insufficient">잔액 부족!</string>
+ <string name="withdraw_exchange">교환</string>
+ <string name="withdraw_manual_title">거래소에 수동으로 이체</string>
+ <string name="nav_header_title">GNU 탈러</string>
+ <string name="nav_header_subtitle">지갑</string>
+ <string name="nav_prompt_withdraw">디지털 현금 인출</string>
+ <string name="nav_exchange_tos">거래소의 서비스 약관</string>
+ <string name="nav_exchange_select">거래소 선택</string>
+ <string name="nav_exchange_fees">교환 수수료</string>
+ <string name="nav_error">오류</string>
+ <string name="button_back">뒤로 가기</string>
+ <string name="button_scan_qr_code">탈러 QR 코드를 스캔합니다</string>
+ <string name="enter_uri">탈러 URI 입력</string>
+ <string name="paste">붙여넣기</string>
+ <string name="paste_invalid">클립 보드에 잘못된 데이터 유형이 있습니다</string>
+ <string name="uri_invalid">유효한 탈러 URI가 아닙니다</string>
+ <string name="ok">네</string>
+ <string name="cancel">취소</string>
+ <string name="search">검색</string>
+ <string name="menu">메뉴</string>
+ <string name="or">혹은</string>
+ <string name="offline">작동하려면 인터넷 연결이 필요합니다. 인터넷이 연결되어 있는지 확인 후 다시 시도하세요.</string>
+ <string name="error_unsupported_uri">오류: 이 탈러 URI는 지원되지 않습니다.</string>
+ <string name="menu_settings">설정</string>
+ <string name="menu_retry_pending_operations">보류중인 작업 재시도</string>
+ <string name="host_apdu_service_desc">탈러 NFC 결제</string>
+ <string name="balances_title">잔고</string>
+ <string name="amount_positive">+ %s</string>
+ <string name="amount_negative">- %s</string>
+ <string name="amount_chosen">선택한 금액</string>
+ <string name="balances_inbound_label">인바운드(도착)</string>
+ <string name="balances_empty_state">지갑에 디지털 현금이 없습니다.
+\n
+\n데모 은행에서 테스트 금액을 받을 수 있습니다.
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="transactions_title">거래</string>
+ <string name="transactions_empty">거래가 없습니다</string>
+ <string name="transactions_empty_search">거래를 찾을 수 없습니다. 다르게 검색해보세요.</string>
+ <string name="transactions_error">거래를 불러올 수 없습니다
+\n
+\n%s</string>
+ <string name="transactions_detail_title">거래</string>
+ <string name="transactions_detail_title_balance">잔액 : %s</string>
+ <string name="transactions_delete">삭제</string>
+ <string name="transactions_select_all">모두 선택</string>
+ <string name="transaction_paid">결제완료</string>
+ <string name="transaction_order_total">합계</string>
+ <string name="transaction_order">구매</string>
+ <string name="transaction_order_id">영수증 #%1$s</string>
+ <string name="transaction_tip_from">%s의 팁</string>
+ <string name="transaction_refund">환불</string>
+ <string name="transaction_refund_from">%s로부터 환불</string>
+ <string name="transaction_pending">대기중</string>
+ <string name="transaction_refresh">코인 만료 변경 수수료</string>
+ <string name="payment_title">결제</string>
+ <string name="payment_fee">+%s 결제 수수료</string>
+ <string name="payment_label_amount_total">총 금액:</string>
+ <string name="payment_label_order_summary">구매</string>
+ <string name="payment_error">오류: %s</string>
+ <string name="payment_aborted">중단</string>
+ <string name="payment_failed">실패</string>
+ <string name="payment_initiated">결제 시작</string>
+ <string name="payment_already_paid_title">이미 지불</string>
+ <string name="payment_already_paid">이 구매에 대해 이미 결제하셨습니다.</string>
+ <string name="withdraw_initiated">출금 시작됨</string>
+ <string name="withdraw_title">출금</string>
+ <string name="withdraw_total">출금</string>
+ <string name="withdraw_fees">수수료</string>
+ <string name="withdraw_button_confirm">취소 확인</string>
+ <string name="withdraw_button_confirm_bank">은행과 확인</string>
+ <string name="withdraw_button_tos">조건 살펴보기</string>
+ <string name="withdraw_waiting_confirm">승인 대기 중</string>
+ <string name="withdraw_amount">얼마를 출금할까요\?</string>
+ <string name="withdraw_amount_error">유효한 금액을 입력하세요</string>
+ <string name="withdraw_manual_payment_options">%1$s이(가) 지원하는 결제 옵션:
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">수수료 확인</string>
+ <string name="withdraw_error_title">출금 오류</string>
+ <string name="withdraw_error_message">현재 인출이 불가능합니다. 다시 시도해주세요!</string>
+ <string name="withdraw_error_test">TESTKUDOS 인출 오류</string>
+ <string name="exchange_settings_title">거래소</string>
+ <string name="exchange_settings_summary">이 지갑이 알고 있는 거래소 목록 관리</string>
+ <string name="exchange_list_title">거래소</string>
+ <string name="exchange_list_empty">인식된 거래소 없음
+\n
+\n수동으로 추가하거나 디지털 현금을 인출하세요!</string>
+ <string name="exchange_list_currency">통화: %s</string>
+ <string name="exchange_list_add">거래소 추가</string>
+ <string name="exchange_add_url">거래소 주소 입력</string>
+ <string name="exchange_add_error">거래소를 추가할 수 없음</string>
+ <string name="exchange_menu_manual_withdraw">인출</string>
+ <string name="exchange_fee_withdrawal_fee_label">인출 수수료:</string>
+ <string name="exchange_fee_overhead_label">반올림으로 인한 손실:</string>
+ <string name="exchange_fee_coin_expiration_label">가장 이른 코인 만료일:</string>
+ <string name="exchange_fee_coin_fees_label">코인 수수료</string>
+ <string name="exchange_fee_wire_fees_label">송금 수수료</string>
+ <plurals name="exchange_fee_coin">
+ <item quantity="other">코인: %s (%d번 사용됨)</item>
+ </plurals>
+ <string name="exchange_fee_withdraw_fee">인출 수수료: %s</string>
+ <string name="exchange_fee_deposit_fee">입금 수수료: %s</string>
+ <string name="exchange_fee_refresh_fee">변경 수수료: %s</string>
+ <string name="exchange_fee_refund_fee">환불 수수료: %s</string>
+ <string name="exchange_fee_wire_fee_timespan">기간: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">송금 수수료: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">마감 수수료: %s</string>
+ <string name="exchange_tos_accept">서비스 약관 동의</string>
+ <string name="exchange_tos_error">서비스 약관 표시 오류: %s</string>
+ <string name="pending_operations_title">보류중인 작업</string>
+ <string name="pending_operations_refuse">제안 거부</string>
+ <string name="pending_operations_no_action">(작업 없음)</string>
+ <string name="settings_dev_mode">개발자 모드</string>
+ <string name="settings_withdraw_testkudos">TESTKUDOS 출금</string>
+ <string name="settings_withdraw_testkudos_summary">테스트를 위한 자금 얻기</string>
+ <string name="settings_logcat">디버그 로그</string>
+ <string name="settings_logcat_summary">내부 로그 보기/보내기</string>
+ <string name="settings_version_app">앱 버전</string>
+ <string name="settings_version_core">지갑 코어 버전</string>
+ <string name="settings_version_protocol_merchant">지원되는 상인 버전</string>
+ <string name="settings_version_unknown">알 수 없음</string>
+ <string name="settings_reset">지갑 재설정 (위험!)</string>
+ <string name="settings_reset_summary">당신의 돈을 버립니다</string>
+ <string name="nav_settings_backup">백업</string>
+ <string name="backup_last">마지막 백업: %s</string>
+ <string name="refund_title">환불</string>
+ <string name="refund_success">받은 환불: %s</string>
+ <string name="tip_title">팁</string>
+ <string name="wifi_connect_error">무료 Wi-Fi에 연결할 수 없습니다: %s</string>
+ <string name="settings_dev_mode_summary">디버깅을 위한 추가 정보가 표시됩니다</string>
+ <string name="settings_version_protocol_exchange">지원되는 거래소 버전</string>
+ <string name="refund_error">환불 처리 오류</string>
+ <string name="wifi_disabled_error">무료 Wi-Fi를 사용하기 위해 Wi-Fi를 켜세요</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-night/styles.xml b/wallet/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..d61986e
--- /dev/null
+++ b/wallet/src/main/res/values-night/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2023 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <style name="AppTheme" parent="AppTheme.Dark">
+ <item name="windowActionModeOverlay">true</item>
+ <item name="android:statusBarColor">?colorSurface</item>
+ <item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
+ <item name="android:navigationBarColor">?colorSurface</item>
+ <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
+ </style>
+
+</resources>
diff --git a/wallet/src/main/res/menu/transactions_detail.xml b/wallet/src/main/res/values-nl/strings.xml
index d4568d4..f23dced 100644
--- a/wallet/src/main/res/menu/transactions_detail.xml
+++ b/wallet/src/main/res/values-nl/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="utf-8"?>
+<!--
~ This file is part of GNU Taler
~ (C) 2020 Taler Systems S.A.
~
@@ -13,7 +14,5 @@
~ You should have received a copy of the GNU General Public License along with
~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-</menu>
+<resources>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-pt/strings.xml b/wallet/src/main/res/values-pt/strings.xml
new file mode 100644
index 0000000..8bb29bf
--- /dev/null
+++ b/wallet/src/main/res/values-pt/strings.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="tip_title">Gorjeta</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">A GNU Taler facilita e acelera transações online que respeitem a privacidade.</string>
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="nav_header_subtitle">Wallet</string>
+ <string name="nav_prompt_withdraw">Sacar Dinheiro Eletrónico</string>
+ <string name="nav_exchange_tos">Termos de Serviço do Câmbio</string>
+ <string name="nav_exchange_select">Selecionar Câmbio</string>
+ <string name="nav_exchange_fees">Taxas de Câmbio</string>
+ <string name="nav_error">Erro</string>
+ <string name="button_back">Voltar</string>
+ <string name="button_scan_qr_code">Digitalizar o Código QR da Taler</string>
+ <string name="enter_uri">Inserir URI Taler</string>
+ <string name="paste">Colar</string>
+ <string name="paste_invalid">A Área de Transferência contém um tipo de dados inválido</string>
+ <string name="uri_invalid">URI Taler inválido</string>
+ <string name="ok">OK</string>
+ <string name="cancel">Cancelar</string>
+ <string name="search">Pesquisar</string>
+ <string name="or">ou</string>
+ <string name="error_unsupported_uri">Erro: Este URI Taler não é suportado.</string>
+ <string name="menu_settings">Configurações</string>
+ <string name="menu_retry_pending_operations">Repetir Operações em Espera</string>
+ <string name="host_apdu_service_desc">Pagamentos Taler NFC</string>
+ <string name="balances_title">Saldos</string>
+ <string name="amount_positive">+ %s</string>
+ <string name="amount_negative">- %s</string>
+ <string name="amount_chosen">Quantidade Selecionada</string>
+ <string name="balances_inbound_label">recebido</string>
+ <string name="transactions_title">Transações</string>
+ <string name="transactions_empty">Não tem quaisquer transações</string>
+ <string name="transactions_empty_search">Não foram encontradas transações. Tente outra pesquisa.</string>
+ <string name="transactions_error">Não foi possível carregar as transações
+\n
+\n%s</string>
+ <string name="transactions_detail_title">Transação</string>
+ <string name="transactions_detail_title_balance">Saldo: %s</string>
+ <string name="transactions_delete">Eliminar</string>
+ <string name="transactions_select_all">Selecionar Tudo</string>
+ <string name="transaction_paid">Pagas</string>
+ <string name="transaction_order_total">Total</string>
+ <string name="transaction_order">Compra</string>
+ <string name="transaction_order_id">Recibo #%1$s</string>
+ <string name="transaction_refund">Reembolso</string>
+ <string name="transaction_refund_from">Reembolso de %s</string>
+ <string name="transaction_refresh">Taxa de mudança de validade da moeda</string>
+ <string name="payment_title">Pagamento</string>
+ <string name="payment_button_confirm">Confirmar Pagamento</string>
+ <string name="payment_label_amount_total">Valor Total:</string>
+ <string name="payment_label_order_summary">Compra</string>
+ <string name="payment_error">Erro: %s</string>
+ <string name="payment_balance_insufficient">Saldo insuficiente!</string>
+ <string name="payment_aborted">Anulado</string>
+ <string name="payment_failed">Falhou</string>
+ <string name="payment_initiated">Pagamento iniciado</string>
+ <string name="payment_already_paid">Já pagou esta compra.</string>
+ <string name="withdraw_initiated">Saque iniciado</string>
+ <string name="withdraw_title">Saque</string>
+ <string name="withdraw_total">Sacar</string>
+ <string name="withdraw_fees">Taxa</string>
+ <string name="withdraw_exchange">Câmbio</string>
+ <string name="withdraw_button_confirm">Confirmar Saque</string>
+ <string name="withdraw_button_confirm_bank">Confirmar com o banco</string>
+ <string name="withdraw_button_tos">Rever Termos</string>
+ <string name="withdraw_amount_error">Insira uma quantia válida</string>
+ <string name="withdraw_manual_check_fees">Verificar taxas</string>
+ <string name="withdraw_error_title">Erro ao Sacar</string>
+ <string name="withdraw_error_test">Erro ao sacar TESTKUDOS</string>
+ <string name="exchange_list_title">Câmbios</string>
+ <string name="exchange_list_empty">Não há câmbios associados
+\n
+\nAdicione um manualmente ou saque dinheiro virtual!</string>
+ <string name="exchange_list_currency">Moeda: %s</string>
+ <string name="exchange_list_add">Adicionar câmbio</string>
+ <string name="exchange_add_url">Inserir endereço do câmbio</string>
+ <string name="exchange_menu_manual_withdraw">Sacar</string>
+ <string name="exchange_fee_withdrawal_fee_label">Taxa de Saque:</string>
+ <string name="exchange_fee_overhead_label">Perda de Arredondamento:</string>
+ <string name="exchange_fee_coin_expiration_label">Primeira Moeda a Expirar:</string>
+ <string name="exchange_fee_coin_fees_label">Taxas de Moeda</string>
+ <string name="exchange_fee_wire_fees_label">Taxas de Transferência</string>
+ <string name="exchange_fee_withdraw_fee">Taxa de Saque: %s</string>
+ <string name="exchange_fee_deposit_fee">Taxa de Depósito: %s</string>
+ <string name="exchange_fee_refresh_fee">Taxa de Alteração: %s</string>
+ <string name="exchange_fee_refund_fee">Taxa de Reembolso: %s</string>
+ <string name="exchange_fee_wire_fee_timespan">Prazo de vigência: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Taxa de Transferência: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Taxa de Fechamento: %s</string>
+ <string name="exchange_tos_error">Erro ao mostrar os Termos de Serviço: %s</string>
+ <string name="pending_operations_refuse">Recusar Proposta</string>
+ <string name="settings_logcat_summary">Ver/enviar registro interno</string>
+ <string name="settings_version_app">Versão da Aplicação</string>
+ <string name="settings_version_protocol_exchange">Versões Suportadas de Câmbio</string>
+ <string name="settings_version_protocol_merchant">Versões Suportadas do Merchant</string>
+ <string name="settings_version_unknown">Desconhecido</string>
+ <string name="nav_settings_backup">Cópia de Segurança</string>
+ <string name="backup_last">Última Cópia de Segurança: %s</string>
+ <string name="refund_title">Reembolso</string>
+ <string name="refund_success">Reembolso recebido: %s</string>
+ <string name="menu">Menu</string>
+ <string name="pending_operations_no_action">(sem ação)</string>
+ <string name="settings_dev_mode">Modo de Programador</string>
+ <string name="settings_dev_mode_summary">Exibe mais informação destinada a depuração</string>
+ <string name="settings_withdraw_testkudos">Sacar TESTKUDOS</string>
+ <string name="settings_withdraw_testkudos_summary">Receber dinheiro para testes</string>
+ <string name="settings_logcat">Registro de Depuração</string>
+ <string name="settings_version_core">Versão Base do Wallet</string>
+ <string name="settings_reset">Repor Wallet (perigoso!)</string>
+ <string name="app_name">Taler Wallet</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">Taler Wallet (experimental)</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Esta aplicação é uma carteira para a GNU Taler e está numa fase altamente experimental, não estando pronta para o uso geral. A GNU Taler é um sistema de pagamentos que preserva a privacidade. Os Clientes podem manter o seu anonimato, mas os comerciantes não podem esconder o seu rendimento através de pagamentos com a GNU Taler. Isto ajuda a prevenir a evasão fiscal e a lavagem de dinheiro. A GNU Taler é para ser usada principalmente para pagamentos e não como uma reserva de valor. Pagamentos são sempre garantidos por uma moeda existente e feitos após a troca de dinheiro existente por dinheiro eletrónico através de um serviço de Câmbio, ou seja, um prestador de serviços de pagamento para a Taler. Quando fazem um pagamento, os clientes apenas precisam de uma carteira com dinheiro. Um comerciante pode aceitar pagamentos sem obrigar os seus clientes a se inscreverem no seu Website. A GNU Taler é imune contra diversos tipos de fraude, como phishing de dados de cartões de crédito ou fraude de estorno. Em casos de perda ou roubo, apenas há a possibilidade de perda da pequena quantidade de dinheiro que ainda estiver na carteira.</string>
+ <string name="offline">Operação requer acesso à internet. Por favor verifique a sua ligação à internet e tente novamente.</string>
+ <string name="balances_empty_state">Não há dinheiro virtual na sua carteira.
+\n
+\nPode adquirir dinheiro de teste do banco de demonstração:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="transaction_tip_from">Gorjeta de %s</string>
+ <string name="transaction_pending">PENDENTE</string>
+ <string name="payment_fee">+%s taxa de pagamento</string>
+ <string name="payment_already_paid_title">Pagamento já realizado</string>
+ <string name="withdraw_waiting_confirm">Aguardando confirmação</string>
+ <string name="withdraw_amount">Quanto quer sacar\?</string>
+ <string name="withdraw_manual_payment_options">Opções de pagamento suportados por %1$s:
+\n
+\n%2$s</string>
+ <string name="withdraw_error_message">De momento não é possível sacar a quantia. Por favor tente novamente mais tarde!</string>
+ <string name="exchange_settings_title">Câmbios</string>
+ <string name="exchange_settings_summary">Gerir lista de câmbios associados a esta carteira</string>
+ <string name="exchange_add_error">Não foi possível adicionar câmbio</string>
+ <plurals name="exchange_fee_coin">
+ <item quantity="one">Moeda: %s (usada %d vez)</item>
+ <item quantity="other">Moeda: %s (usada %d vezes)</item>
+ </plurals>
+ <string name="exchange_tos_accept">Aceitar os Termos de Serviço</string>
+ <string name="pending_operations_title">Operações em Espera</string>
+ <string name="refund_error">Erro ao processar reembolso</string>
+ <string name="settings_reset_summary">Perde o seu dinheiro</string>
+ <string name="wifi_disabled_error">Ligue a Wi-Fi para receber Wi-fi gratuita</string>
+ <string name="wifi_connect_error">Não foi possível ligar a Wi-Fi gratuita: %s</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-ru/strings.xml b/wallet/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000..c9a82ff
--- /dev/null
+++ b/wallet/src/main/res/values-ru/strings.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="ok">OK</string>
+ <string name="menu_settings">Настройки</string>
+ <string name="nav_header_subtitle">Кошелёк</string>
+ <string name="nav_prompt_withdraw">Вывести электронные наличные деньги</string>
+ <string name="nav_error">Ошибка</string>
+ <string name="button_back">Вернитесь назад</string>
+ <string name="button_scan_qr_code">Сканировать Taler QR-код</string>
+ <string name="enter_uri">Введите Taler URI</string>
+ <string name="paste">Вставить</string>
+ <string name="uri_invalid">Неверный Taler URI</string>
+ <string name="cancel">Отменить</string>
+ <string name="search">Поиск</string>
+ <string name="menu">Меню</string>
+ <string name="or">или</string>
+ <string name="offline">Для работы системы необходимо подключиться к Интернету. Пожалуйста, убедитесь, что у вас есть соединение с Интернетом, и попробуйте еще раз.</string>
+ <string name="error_unsupported_uri">Ошибка: данный Taler URI не поддерживается.</string>
+ <string name="menu_retry_pending_operations">Повторить незавершённые операции</string>
+ <string name="host_apdu_service_desc">Taler NFC-платежи</string>
+ <string name="amount_positive">+ %s</string>
+ <string name="balances_title">Балансы</string>
+ <string name="amount_chosen">Выбранная сумма</string>
+ <string name="balances_inbound_label">входящая</string>
+ <string name="app_name">Taler Кошелёк</string>
+ <string name="transactions_title">Транзакции</string>
+ <string name="transactions_empty">У вас нет транзакций</string>
+ <string name="transactions_empty_search">Не удалось найти транзакции. Попробуйте новый поиск.</string>
+ <string name="transactions_error">Не удалось загрузить транзакции
+\n
+\n%s</string>
+ <string name="transactions_detail_title">Транзакция</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">Taler Кошелёк (экспериментальное приложение)</string>
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="transactions_detail_title_balance">Баланс: %s</string>
+ <string name="transactions_select_all">Выбрать все</string>
+ <string name="transaction_paid">Оплачено</string>
+ <string name="balances_empty_state">В вашем кошельке нет электронных наличных денег.
+\n
+\nВы можете получить тестовые деньги в нашем демо-банке:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="transaction_order_id">Чек #%1$s</string>
+ <string name="transaction_order">Покупка</string>
+ <string name="transaction_order_total">Итого</string>
+ <string name="transactions_delete">Удалить</string>
+ <string name="transaction_refund">Возврат</string>
+ <string name="payment_error">Ошибка: %s</string>
+ <string name="payment_title">Платёж</string>
+ <string name="payment_button_confirm">Подтвердить платёж</string>
+ <string name="payment_label_amount_total">Общая сумма:</string>
+ <string name="payment_balance_insufficient">Недостаточно средств на балансе!</string>
+ <string name="nav_exchange_tos">Условия оказания услуг биржей</string>
+ <string name="nav_exchange_select">Выбрать биржу</string>
+ <string name="nav_exchange_fees">Комиссионные сборы биржи</string>
+ <string name="paste_invalid">Неверный тип данных в буфере обмена</string>
+ <string name="amount_negative">- %s</string>
+ <string name="transaction_tip_from">Вознаграждение от %s</string>
+ <string name="transaction_refund_from">Возврат от %s</string>
+ <string name="payment_fee">+%s комиссия за платёж</string>
+ <string name="transaction_refresh">Комиссия за обмен монет с истёкшим сроком годности</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler помогает осуществлять удобные и безопасные онлайн-транзакции быстро и просто.</string>
+ <string name="exchange_add_error">Не удалось добавить биржу</string>
+ <string name="exchange_fee_coin_fees_label">Комиссионные сборы за монеты</string>
+ <string name="payment_already_paid">Вы уже оплатили эту покупку.</string>
+ <string name="withdraw_exchange">Биржа</string>
+ <string name="withdraw_fees">Комиссия</string>
+ <string name="payment_aborted">Платёж отозван</string>
+ <string name="payment_failed">Платёж не прошёл</string>
+ <string name="payment_initiated">Платёж инициирован</string>
+ <string name="payment_already_paid_title">Уже оплачено</string>
+ <string name="withdraw_initiated">Вывод средств инициирован</string>
+ <string name="withdraw_title">Вывод средств</string>
+ <string name="withdraw_button_confirm">Подтвердить вывод средств</string>
+ <string name="withdraw_total">Вывести средства</string>
+ <string name="withdraw_manual_check_fees">Проверить комиссионные сборы</string>
+ <string name="exchange_fee_wire_fees_label">Комиссионные сборы за перевод</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Это приложение для кошелька GNU Taler. Его можно протестировать, но оно пока не готово к использованию. GNU Taler - платёжная система, обеспечивающая сохранность конфиденциальности персональных данных. Покупатели могут оставаться анонимными, а продавцы не могут скрывать информацию о своих доходах посредством платежей через GNU Taler. Это помогает избежать уклонения от уплаты налогов и отмывания денег. Основное назначение GNU Taler - это работа с платежами; система не предназначена для накопления средств. Система поддерживает платежи в любой существующей валюте. Платежи осуществляются после того, как текущие деньги обменены на электронные через биржу, - поставщика услуг по обмену электронной валюты для Taler. Покупателям всего лишь нужно пополнить свой кошелёк для осуществления платежа. Покупателям не нужно регистрироваться на сайте продавца для того, чтобы производить оплату. GNU Taler защищена от многих видов интернет-мошенничества, таких как фишинг, цель которого является получение доступа к данным банковских карт пользователей, или чарджбэк. В случае утери или кражи конфиденциальных данных лишь ограниченная денежная сумма может быть выведена с электронного кошелька.</string>
+ <string name="transaction_pending">ОЖИДАЕТ ПОДТВЕРЖДЕНИЯ</string>
+ <string name="withdraw_button_confirm_bank">Подтвердить с банком</string>
+ <string name="payment_label_order_summary">Покупка</string>
+ <string name="withdraw_button_tos">Проверить условия оказания услуг</string>
+ <string name="withdraw_waiting_confirm">Ожидает подтверждения</string>
+ <string name="withdraw_amount">Какую сумму вы хотели бы вывести\?</string>
+ <string name="withdraw_manual_title">Осуществить перевод на биржу вручную</string>
+ <string name="withdraw_manual_payment_options">Способы оплаты поддерживаются %1$s:
+\n
+\n%2$s</string>
+ <string name="withdraw_error_title">Ошибка при выводе средств</string>
+ <string name="withdraw_error_message">Вывод средств в данный момент не возможен. Пожалуйста, попробуйте ещё раз позже!</string>
+ <string name="withdraw_error_test">Ошибка при выводе TESTKUDOS</string>
+ <string name="exchange_settings_title">Биржи</string>
+ <string name="exchange_list_title">Биржи</string>
+ <string name="exchange_list_currency">Валюта: %s</string>
+ <string name="exchange_list_add">Добавить биржу</string>
+ <string name="exchange_add_url">Введите адрес биржи</string>
+ <string name="exchange_menu_manual_withdraw">Вывести средства</string>
+ <string name="exchange_fee_withdrawal_fee_label">Комиссия за вывод средств:</string>
+ <string name="exchange_fee_withdraw_fee">Комиссия за вывод средств: %s</string>
+ <string name="exchange_fee_wire_fee_timespan">Временной диапазон: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Комиссия за перевод: %s</string>
+ <string name="exchange_tos_accept">Принять условия оказания услуг</string>
+ <string name="pending_operations_title">Незавершенные операции</string>
+ <string name="pending_operations_refuse">Отклонить предложение</string>
+ <string name="pending_operations_no_action">(нет действий)</string>
+ <string name="settings_dev_mode">Режим разработчика</string>
+ <string name="settings_withdraw_testkudos">Вывести TESTKUDOS</string>
+ <string name="settings_withdraw_testkudos_summary">Получить деньги для тестирования</string>
+ <string name="settings_logcat">Журнал отладки</string>
+ <string name="settings_version_app">Версия приложения</string>
+ <string name="settings_reset">Переустановить кошелёк (Опасно!)</string>
+ <string name="nav_settings_backup">Резервная копия</string>
+ <string name="exchange_fee_refresh_fee">Комиссия за обмен: %s</string>
+ <string name="exchange_fee_overhead_label">Округленные убытки:</string>
+ <string name="exchange_fee_refund_fee">Комиссия за возврат: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Комиссия за закрытие: %s</string>
+ <string name="settings_logcat_summary">Просмотреть/отправить внутренний журнал</string>
+ <string name="settings_version_core">Базовая версия кошелька</string>
+ <string name="settings_version_protocol_exchange">Поддерживаемые версии биржи</string>
+ <string name="exchange_fee_coin_expiration_label">Самый ранний срок годности монеты:</string>
+ <string name="backup_last">Последняя резервная копия: %s</string>
+ <string name="refund_title">Возврат</string>
+ <string name="refund_error">Ошибка при обработке возврата</string>
+ <string name="refund_success">Возврат получен: %s</string>
+ <string name="tip_title">Вознаграждение</string>
+ <string name="wifi_connect_error">Не удалось подключиться к бесплатному Wi-Fi: %s</string>
+ <string name="wifi_disabled_error">Включите Wi-Fi, чтобы получить бесплатный Wi-Fi</string>
+ <string name="withdraw_amount_error">Введите допустимую сумму</string>
+ <string name="exchange_settings_summary">Управление каталогом бирж, известных этому кошельку</string>
+ <string name="exchange_list_empty">Неизвестные биржи
+\n
+\nДобавить биржу вручную или вывести электронные наличные деньги!</string>
+ <plurals name="exchange_fee_coin">
+ <item quantity="one">Монета: %s (использована %d раз)</item>
+ <item quantity="few">Монет: %s (использовано %d раз)</item>
+ <item quantity="many">Монеты: %s (использованы %d раз)</item>
+ <item quantity="other">Монеты: %s (использованы %d раз)</item>
+ </plurals>
+ <string name="exchange_tos_error">Ошибка при отображении условий оказания услуг: %s</string>
+ <string name="settings_dev_mode_summary">Показывает информацию, необходимую для отладки программы</string>
+ <string name="settings_version_protocol_merchant">Поддерживаемые версии мерчанта</string>
+ <string name="exchange_fee_deposit_fee">Комиссия за пополнение: %s</string>
+ <string name="settings_version_unknown">Неизвестна</string>
+ <string name="settings_reset_summary">Обнуляет ваши средства</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-tr/strings.xml b/wallet/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000..18c098b
--- /dev/null
+++ b/wallet/src/main/res/values-tr/strings.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="app_name">Taler Cüzdan</string>
+ <string name="search">Ara</string>
+ <string name="menu">Menü</string>
+ <string name="or">veya</string>
+ <string name="offline">İşlem internet erişimi gerektirir. Lütfen internet bağlantınızın çalıştığından emin olun ve tekrar deneyin.</string>
+ <string name="nav_exchange_select">Exchange\'i seç</string>
+ <string name="nav_exchange_fees">Exchange Ücretleri</string>
+ <string name="nav_error">Hata</string>
+ <string name="button_back">Geri git</string>
+ <string name="button_scan_qr_code">Taler QR Kodunu Tara</string>
+ <string name="enter_uri">Taler URI\'ni girin</string>
+ <string name="paste">Yapıştır</string>
+ <string name="ok">Tamam</string>
+ <string name="paste_invalid">Pano geçersiz bir veri türü içeriyor</string>
+ <string name="uri_invalid">Geçerli bir Taler URI\'i değil</string>
+ <string name="cancel">İptal</string>
+ <string name="google_play_title" tools:keep="@string/google_play_title">Taler Cüzdan (deneysel)</string>
+ <string name="google_play_short_desc" tools:keep="@string/google_play_short_desc">GNU Taler, gizlilik dostu çevrimiçi işlemleri hızlı ve kolay hale getirir.</string>
+ <string name="nav_header_title">GNU Taler</string>
+ <string name="nav_header_subtitle">Cüzdan</string>
+ <string name="nav_prompt_withdraw">Dijital Nakit Çek</string>
+ <string name="nav_exchange_tos">Exchange\'in Hizmet Şartları</string>
+ <string name="menu_settings">Ayarlar</string>
+ <string name="error_unsupported_uri">Hata: Bu Taler URI\'i desteklenmiyor.</string>
+ <string name="menu_retry_pending_operations">Bekleyen İşlemleri Yeniden Deneyin</string>
+ <string name="host_apdu_service_desc">Taler NFC Ödemeleri</string>
+ <string name="balances_title">Bakiyeler</string>
+ <string name="amount_positive">+. %s</string>
+ <string name="amount_negative">-. %s</string>
+ <string name="amount_chosen">Seçilen Miktar</string>
+ <string name="balances_inbound_label">gelen</string>
+ <string name="transactions_title">İşlemler</string>
+ <string name="transactions_empty">Herhangi bir işleminiz bulunmamaktadır</string>
+ <string name="transactions_empty_search">İşlem bulunamadı. Farklı bir arama deneyin.</string>
+ <string name="transactions_error">İşlemler yüklenemedi
+\n
+\n%s</string>
+ <string name="transactions_detail_title">İşlem</string>
+ <string name="transactions_detail_title_balance">Bakiye: %s</string>
+ <string name="transactions_delete">Sil</string>
+ <string name="transactions_select_all">Hepsini seç</string>
+ <string name="transaction_paid">Ödendi</string>
+ <string name="transaction_order_total">Toplam</string>
+ <string name="transaction_order">Satın al</string>
+ <string name="transaction_order_id">Fiş #%1$s</string>
+ <string name="transaction_tip_from">%s dan İkramiye</string>
+ <string name="transaction_refund">Para iadesi</string>
+ <string name="payment_title">Ödeme</string>
+ <string name="payment_fee">+%s ödeme ücreti</string>
+ <string name="payment_button_confirm">Ödemeyi Onayla</string>
+ <string name="payment_label_amount_total">Toplam Miktar:</string>
+ <string name="payment_label_order_summary">Satın alma</string>
+ <string name="payment_error">Hata: %s</string>
+ <string name="payment_balance_insufficient">Yetersiz bakiye!</string>
+ <string name="payment_aborted">Durduruldu</string>
+ <string name="payment_failed">Başarısız</string>
+ <string name="payment_already_paid">Bu satın alma işlemi için zaten ödeme yaptınız.</string>
+ <string name="payment_already_paid_title">Zaten ödendi</string>
+ <string name="withdraw_initiated">Para çekme işlemi başlatıldı</string>
+ <string name="withdraw_title">Para çekme</string>
+ <string name="withdraw_total">Para çek</string>
+ <string name="withdraw_fees">Ücret</string>
+ <string name="withdraw_exchange">Exchange</string>
+ <string name="withdraw_button_confirm_bank">Banka ile Onayla</string>
+ <string name="withdraw_button_tos">Şartları İncele</string>
+ <string name="withdraw_waiting_confirm">Onay bekliyor</string>
+ <string name="withdraw_manual_title">Exchange\'e manuel transfer yapın</string>
+ <string name="withdraw_amount">Ne kadar para çekilmeli\?</string>
+ <string name="withdraw_manual_payment_options">%1$s tarafından desteklenen ödeme seçenekleri:
+\n
+\n%2$s</string>
+ <string name="withdraw_manual_check_fees">Ücretleri kontrol edin</string>
+ <string name="transaction_refund_from">%s dan para iadesi</string>
+ <string name="transaction_pending">BEKLEYEN</string>
+ <string name="settings_dev_mode_summary">Hata gidermeye yönelik daha fazla bilgi gösterir</string>
+ <string name="settings_withdraw_testkudos">TESTKUDOS\'u çek</string>
+ <string name="settings_withdraw_testkudos_summary">Test için para al</string>
+ <string name="google_play_full_desc" tools:keep="@string/google_play_full_desc">Bu uygulama GNU Taler için bir cüzdandır. Oldukça deneyseldir ve henüz üretimde kullanıma hazır değildir. GNU Taler, gizliliği koruyan bir ödeme sistemidir. Müşteriler anonim kalabilir, ancak satıcılar GNU Taler ile yapılan ödemeler yoluyla gelirlerini gizleyemezler. Bu, vergi kaçakçılığının ve kara para aklamanın önlenmesine yardımcı olur. GNU Taler\'in birincil kullanım durumu ödemelerdir; bir değer saklama aracı olarak tasarlanmamıştır. Ödemeler her zaman mevcut bir para birimi tarafından desteklenir. Ödeme, bir Exchange hizmeti, yani Taler için bir ödeme hizmeti sağlayıcısı yardımıyla mevcut parayı elektronik paraya çevirdikten sonra yapılır. Müşteriler ödeme yaparken yalnızca ücretli bir cüzdana ihtiyaç duyar. Bir tüccar, müşterilerini tüccarın Web Sitesine kaydettirmeden ödemeleri kabul edebilir. GNU Taler, kredi kartı bilgilerinin kimlik avı veya ters ibraz sahtekarlığı gibi birçok dolandırıcılık türüne karşı bağışıktır. Kaybolma veya çalınma durumunda cüzdanda yalnızca sınırlı miktarda kalan para gitmiş olabilir.</string>
+ <string name="balances_empty_state">Cüzdanınızda dijital para bulunmamaktadır.
+\n
+\nDemo bankasından test parası alabilirsiniz:
+\n
+\nhttps://bank.demo.taler.net</string>
+ <string name="transaction_refresh">Demir para bitiş tarihi değişim ücreti</string>
+ <string name="payment_initiated">Ödeme başlatıldı</string>
+ <string name="withdraw_button_confirm">Para Çekmeyi Onayla</string>
+ <string name="withdraw_amount_error">Geçerli tutarı girin</string>
+ <string name="withdraw_error_message">Para çekme şu anda mümkün değil. Lütfen daha sonra tekrar deneyiniz!</string>
+ <string name="exchange_list_empty">Bilinen exchange yok
+\n
+\nManuel olarak bir tane ekleyin veya dijital parayı çekin!</string>
+ <string name="withdraw_manual_ready_title">Exchange para çekmek için hazır!</string>
+ <string name="withdraw_manual_ready_intro">İşlemi tamamlamak için exchange banka hesabına %s\'i havale etmeniz gerekmektedir</string>
+ <string name="withdraw_manual_ready_details_intro">Banka transfer detayları</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_subject">Konu</string>
+ <string name="withdraw_manual_ready_bank_button">Bankacılık uygulamasında aç</string>
+ <string name="withdraw_manual_ready_cancel">Para çekme işlemini iptal et</string>
+ <string name="withdraw_manual_ready_warning">Doğru konuyu kullandığınızdan emin olun, aksi takdirde para bu cüzdana ulaşmayacaktır.</string>
+ <string name="withdraw_error_title">Para Çekme Hatası</string>
+ <string name="withdraw_error_test">TESTKUDOS çekilirken hata oluştu</string>
+ <string name="exchange_settings_title">Exchangeler</string>
+ <string name="exchange_settings_summary">Bu cüzdan tarafından bilinen exchangelerin listesini yönetin</string>
+ <string name="exchange_list_title">Exchangeler</string>
+ <string name="exchange_list_currency">Para birimi: %s</string>
+ <string name="exchange_list_add">Exchange ekle</string>
+ <string name="exchange_add_url">Exchange adresini girin</string>
+ <string name="exchange_add_error">Exchange eklenemedi</string>
+ <string name="exchange_menu_manual_withdraw">Para çek</string>
+ <string name="exchange_fee_withdrawal_fee_label">Para çekme ücreti:</string>
+ <string name="exchange_fee_overhead_label">Yuvarlama Zararı:</string>
+ <string name="exchange_fee_coin_expiration_label">Madeni Paranın En Erken Sona Erme Tarihi:</string>
+ <string name="exchange_fee_coin_fees_label">Madeni Para Ücretleri</string>
+ <string name="exchange_fee_refresh_fee">Değişim Ücreti: %s</string>
+ <string name="pending_operations_no_action">(Hiçbir eylem)</string>
+ <string name="exchange_fee_wire_fees_label">Havale Ücretleri</string>
+ <string name="exchange_fee_withdraw_fee">Para çekme Ücreti: %s</string>
+ <string name="exchange_fee_deposit_fee">Depozito Ücreti: %s</string>
+ <string name="exchange_fee_refund_fee">Para iadesi Ücreti: %s</string>
+ <string name="exchange_fee_wire_fee_timespan">Zaman aralığı: %1$s - %2$s</string>
+ <string name="exchange_fee_wire_fee_wire_fee">Havale Ücreti: %s</string>
+ <string name="exchange_fee_wire_fee_closing_fee">Kapanış Ücreti: %s</string>
+ <string name="exchange_tos_accept">Hizmet Şartlarını Kabul Et</string>
+ <string name="exchange_tos_error">Hizmet Şartları gösterilirken hata: %s</string>
+ <string name="pending_operations_title">Bekleyen İşlemler</string>
+ <string name="pending_operations_refuse">Teklifi Reddet</string>
+ <string name="settings_dev_mode">Geliştirici modu</string>
+ <string name="settings_logcat">Hata giderme günlüğü</string>
+ <string name="settings_logcat_summary">Dahili günlüğü görüntüle/gönder</string>
+ <string name="settings_version_app">App Sürümü</string>
+ <string name="settings_version_core">Cüzdan Çekirdek Sürümü</string>
+ <string name="settings_version_protocol_exchange">Desteklenen Exchange Sürümleri</string>
+ <string name="settings_version_protocol_merchant">Desteklenen Satıcı Sürümleri</string>
+ <string name="settings_version_unknown">Bilinmeyen</string>
+ <string name="settings_reset">Cüzdanı Sıfırla (tehlikeli!)</string>
+ <string name="settings_reset_summary">Paranızı çöpe atar</string>
+ <string name="nav_settings_backup">Yedekle</string>
+ <string name="backup_last">Son Yedekleme: %s</string>
+ <string name="refund_title">Para iadesi</string>
+ <string name="refund_success">Para iadesi alındı: %s</string>
+ <string name="tip_title">İkramiye</string>
+ <string name="wifi_disabled_error">Ücretsiz Wi-Fi almak için Wi-Fi\'yi açın</string>
+ <string name="wifi_connect_error">Ücretsiz Wi-Fi\'ye bağlanılamadı: %s</string>
+ <string name="refund_error">Para iade işleminde hata oluştu</string>
+ <plurals name="exchange_fee_coin">
+ <item quantity="one">Madeni para: %s (%d kez kullanıldı)</item>
+ <item quantity="other">Madeni paralar: %s (%d kez kullanıldılar)</item>
+ </plurals>
+ <string name="transactions_send_funds">Gönder
+\nFonlar</string>
+ <string name="transactions_receive_funds">Al
+\nFonlar</string>
+ <string name="transactions_detail_title_currency">%s İşlemleri</string>
+ <string name="transactions_retry">Tekrar Dene</string>
+ <string name="transactions_abort">İptal Et</string>
+ <string name="transactions_cancel_dialog_message">Bu çekimi iptal etmek istediğinizden emin misiniz? Transit halindeki fonlar kaybolabilir.</string>
+ <string name="transactions_balance">Bakiye</string>
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values-v27/styles.xml b/wallet/src/main/res/values-v27/styles.xml
new file mode 100644
index 0000000..5a21b91
--- /dev/null
+++ b/wallet/src/main/res/values-v27/styles.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2023 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<resources>
+
+ <style name="AppTheme" parent="AppTheme.Light">
+ <item name="android:statusBarColor">?colorSurface</item>
+ <item name="android:windowLightStatusBar">true</item>
+ <item name="android:navigationBarColor">?colorSurface</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values/colors.xml b/wallet/src/main/res/values/colors.xml
index ba2804e..ce76c1d 100644
--- a/wallet/src/main/res/values/colors.xml
+++ b/wallet/src/main/res/values/colors.xml
@@ -18,4 +18,67 @@
<color name="colorPrimary">#3047a3</color>
<color name="colorPrimaryDark">#152238</color>
<color name="colorAccent">#BB1D24</color>
+
+ <!-- Material 3 theme builder -->
+ <color name="seed">#3047a3</color>
+ <color name="md_theme_light_primary">#4157B4</color>
+ <color name="md_theme_light_onPrimary">#FFFFFF</color>
+ <color name="md_theme_light_primaryContainer">#DDE1FF</color>
+ <color name="md_theme_light_onPrimaryContainer">#001355</color>
+ <color name="md_theme_light_secondary">#2B5EA7</color>
+ <color name="md_theme_light_onSecondary">#FFFFFF</color>
+ <color name="md_theme_light_secondaryContainer">#D7E3FF</color>
+ <color name="md_theme_light_onSecondaryContainer">#001B3E</color>
+ <color name="md_theme_light_tertiary">#B91B23</color>
+ <color name="md_theme_light_onTertiary">#FFFFFF</color>
+ <color name="md_theme_light_tertiaryContainer">#FFDAD7</color>
+ <color name="md_theme_light_onTertiaryContainer">#410004</color>
+ <color name="md_theme_light_error">#BA1A1A</color>
+ <color name="md_theme_light_errorContainer">#FFDAD6</color>
+ <color name="md_theme_light_onError">#FFFFFF</color>
+ <color name="md_theme_light_onErrorContainer">#410002</color>
+ <color name="md_theme_light_background">#F8FDFF</color>
+ <color name="md_theme_light_onBackground">#001F25</color>
+ <color name="md_theme_light_surface">#F8FDFF</color>
+ <color name="md_theme_light_onSurface">#001F25</color>
+ <color name="md_theme_light_surfaceVariant">#E2E1EC</color>
+ <color name="md_theme_light_onSurfaceVariant">#45464F</color>
+ <color name="md_theme_light_outline">#767680</color>
+ <color name="md_theme_light_inverseOnSurface">#D6F6FF</color>
+ <color name="md_theme_light_inverseSurface">#00363F</color>
+ <color name="md_theme_light_inversePrimary">#B8C3FF</color>
+ <color name="md_theme_light_shadow">#000000</color>
+ <color name="md_theme_light_surfaceTint">#4157B4</color>
+ <color name="md_theme_light_outlineVariant">#C6C5D0</color>
+ <color name="md_theme_light_scrim">#000000</color>
+ <color name="md_theme_dark_primary">#B8C3FF</color>
+ <color name="md_theme_dark_onPrimary">#042584</color>
+ <color name="md_theme_dark_primaryContainer">#263E9A</color>
+ <color name="md_theme_dark_onPrimaryContainer">#DDE1FF</color>
+ <color name="md_theme_dark_secondary">#AAC7FF</color>
+ <color name="md_theme_dark_onSecondary">#002F65</color>
+ <color name="md_theme_dark_secondaryContainer">#00458E</color>
+ <color name="md_theme_dark_onSecondaryContainer">#D7E3FF</color>
+ <color name="md_theme_dark_tertiary">#FFB3AD</color>
+ <color name="md_theme_dark_onTertiary">#680009</color>
+ <color name="md_theme_dark_tertiaryContainer">#930012</color>
+ <color name="md_theme_dark_onTertiaryContainer">#FFDAD7</color>
+ <color name="md_theme_dark_error">#FFB4AB</color>
+ <color name="md_theme_dark_errorContainer">#93000A</color>
+ <color name="md_theme_dark_onError">#690005</color>
+ <color name="md_theme_dark_onErrorContainer">#FFDAD6</color>
+ <color name="md_theme_dark_background">#001F25</color>
+ <color name="md_theme_dark_onBackground">#A6EEFF</color>
+ <color name="md_theme_dark_surface">#001F25</color>
+ <color name="md_theme_dark_onSurface">#A6EEFF</color>
+ <color name="md_theme_dark_surfaceVariant">#45464F</color>
+ <color name="md_theme_dark_onSurfaceVariant">#C6C5D0</color>
+ <color name="md_theme_dark_outline">#90909A</color>
+ <color name="md_theme_dark_inverseOnSurface">#001F25</color>
+ <color name="md_theme_dark_inverseSurface">#A6EEFF</color>
+ <color name="md_theme_dark_inversePrimary">#4157B4</color>
+ <color name="md_theme_dark_shadow">#000000</color>
+ <color name="md_theme_dark_surfaceTint">#B8C3FF</color>
+ <color name="md_theme_dark_outlineVariant">#45464F</color>
+ <color name="md_theme_dark_scrim">#000000</color>
</resources>
diff --git a/wallet/src/main/res/values/defaults.xml b/wallet/src/main/res/values/defaults.xml
index 0e2a6a1..dffc14e 100644
--- a/wallet/src/main/res/values/defaults.xml
+++ b/wallet/src/main/res/values/defaults.xml
@@ -16,6 +16,4 @@
<resources>
- <bool name="settings_backup_default">true</bool>
-
</resources> \ No newline at end of file
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 5c27eb4..2ec3d40 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -44,7 +44,10 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="button_back">Go Back</string>
<string name="button_scan_qr_code">Scan Taler QR Code</string>
+ <string name="button_scan_qr_code_label">Scan QR code</string>
<string name="enter_uri">Enter Taler URI</string>
+ <string name="copy" tools:override="true">Copy</string>
+ <string name="copy_uri">Copy Taler URI</string>
<string name="paste">Paste</string>
<string name="paste_invalid">Clipboard contains an invalid data type</string>
<string name="uri_invalid">Not a valid Taler URI</string>
@@ -53,9 +56,17 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="search">Search</string>
<string name="menu">Menu</string>
<string name="or">or</string>
+ <string name="currency">Currency</string>
+ <!-- The count should be mirrored in RTL languages -->
+ <string name="char_count">%1$d/%2$d</string>
+ <string name="import_db">Import</string>
+ <string name="reset">Reset</string>
+ <string name="share_payment">Share payment link</string>
<string name="offline">Operation requires internet access. Please ensure your internet connection works and try again.</string>
+ <string name="offline_banner">No internet access</string>
<string name="error_unsupported_uri">Error: This Taler URI is not supported.</string>
+ <string name="error_broken_uri">Error: This Taler URI is (currently) not working.</string>
<string name="menu_settings">Settings</string>
<string name="menu_retry_pending_operations">Retry Pending Operations</string>
@@ -63,50 +74,133 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="host_apdu_service_desc">Taler NFC Payments</string>
<string name="balances_title">Balances</string>
- <string name="amount_positive">+ %s</string>
- <string name="amount_negative">- %s</string>
+ <string name="amount_positive">+%s</string>
+ <string name="amount_negative">-%s</string>
<string name="amount_chosen">Chosen Amount</string>
+ <string name="amount_sent">Amount sent</string>
+ <string name="amount_received">Amount received</string>
<string name="balances_inbound_label">inbound</string>
<string name="balances_empty_state">There is no digital cash in your wallet.\n\nYou can get test money from the demo bank:\n\nhttps://bank.demo.taler.net</string>
+ <string name="balance_scope_exchange">Exchange: %1$s</string>
+ <string name="balance_scope_auditor">Auditor: %1$s</string>
<string name="transactions_title">Transactions</string>
+ <string name="transactions_balance">Balance</string>
+ <string name="transactions_send_funds">Send\nFunds</string>
+ <string name="transactions_send_funds_title">Send %1$s</string>
+ <string name="transactions_receive_funds">Receive\nFunds</string>
+ <string name="transactions_receive_funds_title">Receive %1$s</string>
<string name="transactions_empty">You don\'t have any transactions</string>
<string name="transactions_empty_search">No transactions found. Try a different search.</string>
<string name="transactions_error">Could not load transactions\n\n%s</string>
<string name="transactions_detail_title">Transaction</string>
- <string name="transactions_detail_title_balance">Balance: %s</string>
+ <string name="transactions_detail_title_currency">%s Transactions</string>
<string name="transactions_delete">Delete</string>
+ <string name="transactions_retry">Retry</string>
+ <string name="transactions_abort">Abort</string>
+ <string name="transactions_fail">Kill</string>
+ <string name="transactions_suspend">Suspend</string>
+ <string name="transactions_resume">Resume</string>
<string name="transactions_select_all">Select All</string>
+ <string name="transactions_delete_dialog_title">Delete Transaction</string>
+ <string name="transactions_delete_dialog_message">Are you sure you want to remove this transaction from your wallet?</string>
+ <string name="transactions_delete_selected_dialog_message">Are you sure you want to remove the selected transactions from your wallet?</string>
+ <string name="transactions_abort_dialog_title">Abort Transaction</string>
+ <string name="transactions_abort_dialog_message">Are you sure you want to abort this transaction? Funds still in transit might get lost.</string>
+ <string name="transactions_fail_dialog_title">Kill Transaction</string>
+ <string name="transactions_fail_dialog_message">Are you sure you want to kill this transaction? Funds still in transit WILL GET LOST.</string>
+ <string name="transactions_cancel_dialog_message">Are you sure you want to cancel this withdrawal? Funds still in transit might get lost.</string>
<!-- Transactions -->
<string name="transaction_paid">Paid</string>
<string name="transaction_order_total">Total</string>
<string name="transaction_order">Purchase</string>
<string name="transaction_order_id">Receipt #%1$s</string>
- <string name="transaction_tip_from">Tip from %s</string>
<string name="transaction_refund">Refund</string>
<string name="transaction_refund_from">Refund from %s</string>
<string name="transaction_pending">PENDING</string>
<string name="transaction_refresh">Coin expiry change fee</string>
+ <string name="transaction_deposit">Deposit</string>
+ <string name="transaction_peer_push_debit">Push payment</string>
+ <string name="transaction_peer_pull_credit">Invoice</string>
+ <string name="transaction_peer_pull_debit">Invoice paid</string>
+ <string name="transaction_peer_push_credit">Push payment</string>
+ <string name="transaction_action_kyc">Complete KYC</string>
+ <string name="transaction_dummy_title">Unknown Transaction</string>
<string name="payment_title">Payment</string>
+ <string name="payment_prompt_title">Review Payment</string>
<string name="payment_fee">+%s payment fee</string>
<string name="payment_button_confirm">Confirm Payment</string>
<string name="payment_label_amount_total">Total Amount:</string>
<string name="payment_label_order_summary">Purchase</string>
<string name="payment_error">Error: %s</string>
+ <string name="payment_pending">Payment not completed, it will be retried</string>
<string name="payment_balance_insufficient">Balance insufficient!</string>
<string name="payment_aborted">Aborted</string>
<string name="payment_failed">Failed</string>
<string name="payment_initiated">Payment initiated</string>
<string name="payment_already_paid_title">Already paid</string>
<string name="payment_already_paid">You\'ve already paid for this purchase.</string>
+ <string name="payment_pay_template_title">Customize your order</string>
+ <string name="payment_create_order">Create order</string>
+ <string name="payment_confirmation_code">Confirmation code</string>
+ <string name="payment_template_error">Error creating order</string>
+
+ <string name="receive_amount">Amount to receive</string>
+ <string name="receive_amount_invalid">Amount invalid</string>
+ <string name="receive_intro">Choose where to receive money from:</string>
+ <string name="receive_withdraw">Withdraw from bank account</string>
+ <string name="receive_peer">Invoice another wallet</string>
+ <string name="receive_peer_title">Request payment</string>
+ <string name="receive_peer_create_button">Create invoice</string>
+ <string name="receive_peer_invoice_instruction">Let the payer scan this QR code to pay:</string>
+ <string name="receive_peer_invoice_uri">Alternatively, copy and send this URI:</string>
+ <string name="receive_peer_amount_invoiced">Amount invoiced</string>
+
+ <string name="send_amount">Amount to send</string>
+ <string name="send_intro">Choose where to send money to:</string>
+ <string name="send_deposit">To a bank account</string>
+ <string name="send_deposit_bitcoin">To a Bitcoin wallet</string>
+ <string name="send_deposit_title">Deposit to a bank account</string>
+ <string name="send_deposit_iban">IBAN</string>
+ <string name="send_deposit_iban_error">IBAN is invalid</string>
+ <string name="send_deposit_bic_error">BIC is invalid</string>
+ <string name="send_deposit_bic">BIC/SWIFT</string>
+ <string name="send_deposit_name">Account holder</string>
+ <string name="send_deposit_bitcoin_address">Bitcoin address</string>
+ <string name="send_deposit_check_fees_button">Check fees</string>
+ <string name="send_deposit_amount_effective">Effective Amount</string>
+ <string name="send_deposit_create_button">Make deposit</string>
+ <string name="send_deposit_bitcoin_create_button">Transfer Bitcoin</string>
+ <string name="send_peer">To another wallet</string>
+ <string name="send_peer_bitcoin">To another Taler wallet</string>
+ <string name="send_peer_title">Send money to another wallet</string>
+ <string name="send_peer_create_button">Send funds now</string>
+ <string name="send_peer_payment_instruction">Let the payee scan this QR code to receive:</string>
+ <string name="send_peer_expiration_period">Expires in</string>
+ <string name="send_peer_expiration_1d">1 day</string>
+ <string name="send_peer_expiration_7d">7 days</string>
+ <string name="send_peer_expiration_30d">30 days</string>
+ <string name="send_peer_expiration_custom">Custom</string>
+ <string name="send_peer_expiration_days">Days</string>
+ <string name="send_peer_expiration_hours">Hours</string>
+ <string name="send_peer_purpose">Purpose</string>
+
+ <string name="pay_peer_title">Pay invoice</string>
+ <string name="pay_peer_intro">Do you want to pay this invoice?</string>
+ <string name="receive_peer_payment_title">Receive payment</string>
+ <string name="receive_peer_payment_intro">Do you want to receive this payment?</string>
<string name="withdraw_initiated">Withdrawal initiated</string>
<string name="withdraw_title">Withdrawal</string>
+ <string name="withdraw_subtitle">Select target bank account</string>
<string name="withdraw_total">Withdraw</string>
<string name="withdraw_fees">Fee</string>
+ <string name="withdraw_restrict_age">Restrict Usage to Age</string>
+ <string name="withdraw_restrict_age_unrestricted">Unrestricted</string>
<string name="withdraw_exchange">Exchange</string>
+ <string name="withdraw_bank">Bank</string>
<string name="withdraw_button_confirm">Confirm Withdraw</string>
<string name="withdraw_button_confirm_bank">Confirm with bank</string>
<string name="withdraw_button_tos">Review Terms</string>
@@ -116,9 +210,26 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="withdraw_amount_error">Enter valid amount</string>
<string name="withdraw_manual_payment_options">Payment options supported by %1$s:\n\n%2$s</string>
<string name="withdraw_manual_check_fees">Check fees</string>
+ <string name="withdraw_manual_ready_title">Exchange is ready for withdrawal!</string>
+ <string name="withdraw_manual_ready_intro">To complete the process you need to wire %s to the exchange bank account</string>
+ <string name="withdraw_manual_ready_details_intro">Bank transfer details</string>
+ <string name="withdraw_manual_bitcoin_title">Bitcoin exchange ready for withdrawal</string>
+ <string name="withdraw_manual_bitcoin_intro">Now make a split transaction with the following three outputs.</string>
+ <string name="withdraw_manual_ready_iban">IBAN</string>
+ <string name="withdraw_manual_ready_account">Account</string>
+ <string name="withdraw_manual_ready_receiver">Receiver name</string>
+ <string name="withdraw_manual_ready_subject">Subject</string>
+ <string name="withdraw_manual_ready_bank_button">Open in banking app</string>
+ <string name="withdraw_manual_ready_cancel">Cancel withdrawal</string>
+ <string name="withdraw_manual_ready_warning">Make sure to use the correct subject, otherwise the money will not arrive in this wallet.</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="withdraw_error_test">Error withdrawing TESTKUDOS</string>
+ <string name="withdraw_account">Account #%1$d</string>
+ <string name="withdraw_account_currency">Account #%1$d (%2$s)</string>
+ <string name="withdraw_transfer">Transfer</string>
+ <string name="withdraw_conversion">Conversion</string>
+ <string name="withdraw_conversion_support">This exchange supports currency conversion</string>
<string name="exchange_settings_title">Exchanges</string>
<string name="exchange_settings_summary">Manage list of exchanges known to this wallet</string>
@@ -126,8 +237,15 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="exchange_list_empty">No exchanges known\n\nAdd one manually or withdraw digital cash!</string>
<string name="exchange_list_currency">Currency: %s</string>
<string name="exchange_list_add">Add exchange</string>
+ <string name="exchange_list_select">Select exchange</string>
+ <string name="exchange_delete">Delete exchange</string>
+ <string name="exchange_delete_force">Force deletion (purge)</string>
+ <string name="exchange_dialog_delete_message">Are you sure you want to delete this exchange? Forcing this operation will result in a loss of funds.</string>
+ <string name="exchange_not_contacted">Exchange not contacted</string>
<string name="exchange_add_url">Enter address of exchange</string>
<string name="exchange_add_error">Could not add exchange</string>
+ <string name="exchange_list_error">Could not list exchanges</string>
+ <string name="exchange_list_add_dev">Add development exchanges</string>
<string name="exchange_menu_manual_withdraw">Withdraw</string>
<string name="exchange_fee_withdrawal_fee_label">Withdrawal Fee:</string>
@@ -158,23 +276,38 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="settings_withdraw_testkudos">Withdraw TESTKUDOS</string>
<string name="settings_withdraw_testkudos_summary">Get money for testing</string>
<string name="settings_logcat">Debug log</string>
- <string name="settings_logcat_summary">View/send internal log</string>
+ <string name="settings_logcat_summary">Save internal log</string>
+ <string name="settings_logcat_error">Error exporting log</string>
+ <string name="settings_logcat_success">Log exported to file</string>
+ <string name="settings_db_export">Export database</string>
+ <string name="settings_db_export_summary">Save internal database</string>
+ <string name="settings_db_import">Import database</string>
+ <string name="settings_db_import_summary">Restore database from file</string>
+ <string name="settings_db_export_error">Error exporting database</string>
+ <string name="settings_db_import_error">Error importing database</string>
+ <string name="settings_db_clear_error">Error cleaning database</string>
+ <string name="settings_db_export_success">Database exported to file</string>
+ <string name="settings_db_import_success">Database imported from file</string>
<string name="settings_version_app">App Version</string>
<string name="settings_version_core">Wallet Core Version</string>
<string name="settings_version_protocol_exchange">Supported Exchange Versions</string>
<string name="settings_version_protocol_merchant">Supported Merchant Versions</string>
<string name="settings_version_unknown">Unknown</string>
+ <string name="settings_test">Run integration test</string>
+ <string name="settings_test_summary">Performs test transactions with demo setup</string>
<string name="settings_reset">Reset Wallet (dangerous!)</string>
<string name="settings_reset_summary">Throws away your money</string>
- <string name="nav_settings_backup">Backup</string>
- <string name="backup_last">Last Backup: %s</string>
+ <string name="settings_dialog_reset_message">Do you really want to reset the wallet and lose all coins and purchases?</string>
+ <string name="settings_dialog_import_message">This operation will overwrite your existing database. Do you want to continue?</string>
+ <string name="settings_alert_reset_done">Wallet has been reset</string>
+ <string name="settings_alert_reset_canceled">Reset cancelled</string>
+ <string name="settings_alert_import_canceled">Import cancelled</string>
<string name="refund_title">Refund</string>
<string name="refund_error">Error processing refund</string>
- <string name="refund_success">Refund received: %s</string>
+ <string name="refund_success">Refund received!</string>
- <string name="tip_title">Tip</string>
<string name="wifi_disabled_error">Turn on Wi-Fi to get free Wi-Fi</string>
<string name="wifi_connect_error">Could not connect to free Wi-Fi: %s</string>
diff --git a/wallet/src/main/res/values/styles.xml b/wallet/src/main/res/values/styles.xml
index 33e31a3..d7d939f 100644
--- a/wallet/src/main/res/values/styles.xml
+++ b/wallet/src/main/res/values/styles.xml
@@ -14,32 +14,91 @@
~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-->
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools">
- <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
- <item name="colorPrimary">@color/colorPrimary</item>
- <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
- <item name="colorAccent">@color/colorAccent</item>
- <item name="colorOnPrimary">@android:color/white</item>
+ <style name="AppTheme.Light" parent="Theme.Material3.Light">
+ <!-- Material 3 theme builder -->
+ <item name="colorPrimary">@color/md_theme_light_primary</item>
+ <item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
+ <item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
+ <item name="colorOnPrimaryContainer">@color/md_theme_light_onPrimaryContainer</item>
+ <item name="colorSecondary">@color/md_theme_light_secondary</item>
+ <item name="colorOnSecondary">@color/md_theme_light_onSecondary</item>
+ <item name="colorSecondaryContainer">@color/md_theme_light_secondaryContainer</item>
+ <item name="colorOnSecondaryContainer">@color/md_theme_light_onSecondaryContainer</item>
+ <item name="colorTertiary">@color/md_theme_light_tertiary</item>
+ <item name="colorOnTertiary">@color/md_theme_light_onTertiary</item>
+ <item name="colorTertiaryContainer">@color/md_theme_light_tertiaryContainer</item>
+ <item name="colorOnTertiaryContainer">@color/md_theme_light_onTertiaryContainer</item>
+ <item name="colorError">@color/md_theme_light_error</item>
+ <item name="colorErrorContainer">@color/md_theme_light_errorContainer</item>
+ <item name="colorOnError">@color/md_theme_light_onError</item>
+ <item name="colorOnErrorContainer">@color/md_theme_light_onErrorContainer</item>
+ <item name="android:colorBackground">@color/md_theme_light_background</item>
+ <item name="colorOnBackground">@color/md_theme_light_onBackground</item>
+ <item name="colorSurface">@color/md_theme_light_surface</item>
+ <item name="colorOnSurface">@color/md_theme_light_onSurface</item>
+ <item name="colorSurfaceVariant">@color/md_theme_light_surfaceVariant</item>
+ <item name="colorOnSurfaceVariant">@color/md_theme_light_onSurfaceVariant</item>
+ <item name="colorOutline">@color/md_theme_light_outline</item>
+ <item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item>
+ <item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item>
+ <item name="colorPrimaryInverse">@color/md_theme_light_inversePrimary</item>
+ </style>
+
+ <style name="AppTheme.Dark" parent="Theme.Material3.Dark">
+ <item name="windowActionModeOverlay">true</item>
+ <item name="android:statusBarColor" tools:targetApi="m">?colorSurface</item>
+ <item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
+ <item name="android:navigationBarColor" tools:targetApi="o_mr1">?colorSurface</item>
+ <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
+
+ <!-- Material 3 theme builder -->
+ <item name="colorPrimary">@color/md_theme_dark_primary</item>
+ <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
+ <item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
+ <item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
+ <item name="colorSecondary">@color/md_theme_dark_secondary</item>
+ <item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
+ <item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
+ <item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
+ <item name="colorTertiary">@color/md_theme_dark_tertiary</item>
+ <item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
+ <item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
+ <item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
+ <item name="colorError">@color/md_theme_dark_error</item>
+ <item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
+ <item name="colorOnError">@color/md_theme_dark_onError</item>
+ <item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
+ <item name="android:colorBackground">@color/md_theme_dark_background</item>
+ <item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
+ <item name="colorSurface">@color/md_theme_dark_surface</item>
+ <item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
+ <item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
+ <item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
+ <item name="colorOutline">@color/md_theme_dark_outline</item>
+ <item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
+ <item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
+ <item name="colorPrimaryInverse">@color/md_theme_dark_inversePrimary</item>
+ </style>
+ <style name="AppTheme" parent="AppTheme.Light">
<item name="windowActionModeOverlay">true</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
- <item name="android:statusBarColor">@android:color/transparent</item>
</style>
- <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.ActionBar" />
+ <style name="AppTheme.AppBarOverlay" parent="Widget.Material3.ActionBar.Solid" />
- <style name="AppTheme.Toolbar" parent="Widget.MaterialComponents.Toolbar.Primary" />
+ <style name="AppTheme.Toolbar" parent="Widget.Material3.Toolbar" />
- <style name="DialogTheme" parent="Theme.MaterialComponents.DayNight.Dialog.Alert" />
+ <style name="DialogTheme" parent="Theme.Material3.DayNight.Dialog.Alert" />
<style name="TransactionTitle">
<item name="android:textSize">16sp</item>
- <item name="android:textColor">?android:textColorPrimary</item>
</style>
<style name="TransactionLabel">
@@ -72,13 +131,11 @@
<item name="cardElevation">8dp</item>
</style>
- <style name="FabStyle" parent="Widget.MaterialComponents.FloatingActionButton">
+ <style name="FabStyle" parent="Widget.Material3.FloatingActionButton.Primary">
<item name="android:layout_gravity">bottom|end</item>
<item name="android:layout_marginEnd">16dp</item>
<item name="android:layout_marginBottom">16dp</item>
- <item name="backgroundTint">@color/colorPrimary</item>
<item name="layout_dodgeInsetEdges">bottom</item>
- <item name="tint">?attr/colorOnPrimary</item>
</style>
</resources>
diff --git a/wallet/src/main/res/xml/network_security_config.xml b/wallet/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..de61259
--- /dev/null
+++ b/wallet/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+ <domain-config cleartextTrafficPermitted="true">
+ <domain includeSubdomains="true">localhost</domain>
+ </domain-config>
+</network-security-config>
diff --git a/wallet/src/main/res/xml/settings_backup.xml b/wallet/src/main/res/xml/settings_backup.xml
deleted file mode 100644
index f8c5839..0000000
--- a/wallet/src/main/res/xml/settings_backup.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ This file is part of GNU Taler
- ~ (C) 2020 Taler Systems S.A.
- ~
- ~ GNU Taler is free software; you can redistribute it and/or modify it under the
- ~ terms of the GNU General Public License as published by the Free Software
- ~ Foundation; either version 3, or (at your option) any later version.
- ~
- ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- ~
- ~ You should have received a copy of the GNU General Public License along with
- ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- -->
-
-<PreferenceScreen
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <SwitchPreferenceCompat
- app:defaultValue="@bool/settings_backup_default"
- app:fragment="net.taler.wallet.settings.BackupSettingsFragment"
- app:icon="@drawable/ic_baseline_backup"
- app:key="pref_backup_switch"
- app:summaryOff="Backup is disabled"
- app:summaryOn="Backup is enabled"
- app:title="Backup my wallet" />
-
- <Preference
- app:dependency="pref_backup_switch"
- app:icon="@drawable/ic_baseline_account_tree"
- app:key="pref_backup_services"
- app:selectable="false"
- app:summary="1 backup service set up"
- app:title="Backup services" />
-
- <PreferenceCategory
- app:iconSpaceReserved="false"
- app:summary="At least one of these is required to restore from backup"
- app:title="Recovery Options">
-
- <Preference
- app:dependency="pref_backup_switch"
- app:icon="@drawable/ic_baseline_vpn_key"
- app:key="pref_backup_secret"
- app:selectable="false"
- app:title="Show backup secret" />
-
- <Preference
- app:dependency="pref_backup_switch"
- app:icon="@drawable/ic_baseline_cloud_circle"
- app:key="pref_backup_recovery"
- app:summary="Not used"
- app:title="Secure cloud recovery"/>
-
- </PreferenceCategory>
-
-</PreferenceScreen>
diff --git a/wallet/src/main/res/xml/settings_main.xml b/wallet/src/main/res/xml/settings_main.xml
index a713838..3567e49 100644
--- a/wallet/src/main/res/xml/settings_main.xml
+++ b/wallet/src/main/res/xml/settings_main.xml
@@ -24,13 +24,6 @@
app:summary="@string/exchange_settings_summary"
app:title="@string/exchange_settings_title" />
- <Preference
- app:fragment="net.taler.wallet.settings.BackupSettingsFragment"
- app:icon="@drawable/ic_baseline_backup"
- app:isPreferenceVisible="false"
- app:key="pref_backup"
- app:title="Backup"
- tools:isPreferenceVisible="true" />
<SwitchPreferenceCompat
app:icon="@drawable/ic_developer_mode"
@@ -55,8 +48,24 @@
tools:isPreferenceVisible="true" />
<Preference
- app:icon="@drawable/ic_account_balance_wallet"
+ app:icon="@drawable/ic_unarchive"
+ app:isPreferenceVisible="false"
+ app:key="pref_export_db"
+ app:summary="@string/settings_db_export_summary"
+ app:title="@string/settings_db_export"
+ tools:isPreferenceVisible="true" />
+
+ <Preference
+ app:icon="@drawable/ic_archive"
app:isPreferenceVisible="false"
+ app:key="pref_import_db"
+ app:summary="@string/settings_db_import_summary"
+ app:title="@string/settings_db_import"
+ tools:isPreferenceVisible="true" />
+
+ <Preference
+ app:icon="@drawable/ic_account_balance_wallet"
+ app:isPreferenceVisible="true"
app:key="pref_version_app"
app:selectable="false"
app:summary="@string/settings_version_unknown"
@@ -91,6 +100,13 @@
tools:isPreferenceVisible="true" />
<Preference
+ app:isPreferenceVisible="false"
+ app:key="pref_test"
+ app:summary="@string/settings_test_summary"
+ app:title="@string/settings_test"
+ tools:isPreferenceVisible="true" />
+
+ <Preference
app:icon="@drawable/ic_nuke"
app:isPreferenceVisible="false"
app:key="pref_reset"
diff --git a/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt b/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
index e2389d8..5d3a80b 100644
--- a/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
+++ b/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
@@ -39,6 +39,11 @@ class WalletResponseTest {
"result": {
"balances": [
{
+ "scopeInfo": {
+ "currency": "TESTKUDOS",
+ "type": "exchange",
+ "url": "https://exchange.test.taler.net/"
+ },
"available": "TESTKUDOS:15.8",
"pendingIncoming": "TESTKUDOS:0",
"pendingOutgoing": "TESTKUDOS:0",
diff --git a/wallet/src/test/java/net/taler/wallet/payment/PaymentResponsesTest.kt b/wallet/src/test/java/net/taler/wallet/payment/PaymentResponsesTest.kt
index fbdc07d..f1eaa76 100644
--- a/wallet/src/test/java/net/taler/wallet/payment/PaymentResponsesTest.kt
+++ b/wallet/src/test/java/net/taler/wallet/payment/PaymentResponsesTest.kt
@@ -23,7 +23,6 @@ class PaymentResponsesTest {
private val json = Json {
ignoreUnknownKeys = true
- classDiscriminator = PreparePayResponse.discriminator
}
@Test
@@ -43,16 +42,16 @@ class PaymentResponsesTest {
"wire_method": "x-taler-bank",
"order_id": "2020.240-01MD5F476HMXW",
"timestamp": {
- "t_ms": 1598538535000
+ "t_s": 1598538535
},
"refund_deadline": {
- "t_ms": 1598538835000
+ "t_s": 1598538835
},
"pay_deadline": {
- "t_ms": 1598538835000
+ "t_s": 1598538835
},
"wire_transfer_deadline": {
- "t_ms": 1598542135000
+ "t_s": 1598542135
},
"max_wire_fee": "CHF:0.1",
"max_fee": "CHF:0.1",
diff --git a/wallet/src/test/java/net/taler/wallet/withdraw/WithdrawManagerKtTest.kt b/wallet/src/test/java/net/taler/wallet/withdraw/WithdrawManagerKtTest.kt
new file mode 100644
index 0000000..3072075
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/withdraw/WithdrawManagerKtTest.kt
@@ -0,0 +1,51 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw
+
+import net.taler.common.Bech32.Companion.generateFakeSegwitAddress
+import org.junit.Assert
+import org.junit.Test
+
+class WithdrawManagerKtTest {
+
+ @Test
+ fun generateMainnet() {
+ val (addr1, addr2) = generateFakeSegwitAddress("54ZN9AMVN1R0YZ68ZPVHHQA4KZE1V037M05FNMYH4JQ596YAKJEG",
+ "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq")
+
+ Assert.assertEquals(addr1, "bc1q9yl4f23f8a224xagwq8hej8akuvd63yl8nyedj")
+ Assert.assertEquals(addr2, "bc1q4yl4f2kurkqx0gq2ltfazf9w2jdu48yaqlghnp")
+ }
+
+ @Test
+ fun generateTestnet() {
+ val (addr1, addr2) = generateFakeSegwitAddress("54ZN9AMVN1R0YZ68ZPVHHQA4KZE1V037M05FNMYH4JQ596YAKJEG",
+ "tb1qhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
+
+ Assert.assertEquals(addr1, "tb1q9yl4f23f8a224xagwq8hej8akuvd63yld4l2kp")
+ Assert.assertEquals(addr2, "tb1q4yl4f2kurkqx0gq2ltfazf9w2jdu48ya2enygj")
+ }
+
+ @Test
+ fun generateRegnet() {
+ val (addr1, addr2) = generateFakeSegwitAddress("54ZN9AMVN1R0YZ68ZPVHHQA4KZE1V037M05FNMYH4JQ596YAKJEG",
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
+
+ Assert.assertEquals(addr1, "bcrt1q9yl4f23f8a224xagwq8hej8akuvd63yl0ux8pg")
+ Assert.assertEquals(addr2, "bcrt1q4yl4f2kurkqx0gq2ltfazf9w2jdu48yags2flm")
+ }
+}