[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-cashier-terminal-android] 04/06: First complete prototype
From: |
gnunet |
Subject: |
[taler-cashier-terminal-android] 04/06: First complete prototype |
Date: |
Wed, 19 Feb 2020 21:31:58 +0100 |
This is an automated email from the git hooks/post-receive script.
torsten-grote pushed a commit to branch master
in repository cashier-terminal-android.
commit 146c81c00f465dfc3872c7cb4ba4adae9bc0519f
Author: Torsten Grote <address@hidden>
AuthorDate: Wed Feb 19 10:36:53 2020 -0300
First complete prototype
---
.idea/dictionaries/user.xml | 7 +
app/build.gradle | 6 +-
app/src/main/java/net/taler/cashier/Amount.kt | 12 ++
.../main/java/net/taler/cashier/BalanceFragment.kt | 1 +
.../main/java/net/taler/cashier/ConfigFragment.kt | 23 ++-
app/src/main/java/net/taler/cashier/HttpHelper.kt | 49 +++++-
.../main/java/net/taler/cashier/MainViewModel.kt | 123 ++++----------
.../java/net/taler/cashier/TransactionFragment.kt | 86 ----------
app/src/main/java/net/taler/cashier/Utils.kt | 21 +++
.../net/taler/cashier/withdraw/ErrorFragment.kt | 33 ++++
.../net/taler/cashier/{ => withdraw}/NfcManager.kt | 10 +-
.../taler/cashier/{ => withdraw}/QrCodeManager.kt | 2 +-
.../net/taler/cashier/withdraw/SuccessFragment.kt | 33 ++++
.../taler/cashier/withdraw/TransactionFragment.kt | 146 ++++++++++++++++
.../cashier/{ => withdraw}/WithdrawFragment.kt | 27 ++-
.../net/taler/cashier/withdraw/WithdrawManager.kt | 184 +++++++++++++++++++++
app/src/main/res/drawable/ic_check_circle.xml | 11 ++
app/src/main/res/drawable/ic_error.xml | 11 ++
.../fragment_transaction.xml | 26 ++-
app/src/main/res/layout/activity_main.xml | 5 +-
app/src/main/res/layout/fragment_balance.xml | 5 +-
app/src/main/res/layout/fragment_config.xml | 5 +-
app/src/main/res/layout/fragment_error.xml | 49 ++++++
app/src/main/res/layout/fragment_success.xml | 49 ++++++
app/src/main/res/layout/fragment_transaction.xml | 8 +-
app/src/main/res/layout/fragment_withdraw.xml | 19 +--
app/src/main/res/navigation/nav_graph.xml | 27 ++-
app/src/main/res/values/colors.xml | 3 +
app/src/main/res/values/strings.xml | 8 +-
app/src/main/res/values/styles.xml | 16 +-
build.gradle | 2 +-
31 files changed, 763 insertions(+), 244 deletions(-)
diff --git a/.idea/dictionaries/user.xml b/.idea/dictionaries/user.xml
new file mode 100644
index 0000000..6a82105
--- /dev/null
+++ b/.idea/dictionaries/user.xml
@@ -0,0 +1,7 @@
+<component name="ProjectDictionaryState">
+ <dictionary name="user">
+ <words>
+ <w>taler</w>
+ </words>
+ </dictionary>
+</component>
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 055f4ba..217ad35 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -32,10 +32,10 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'androidx.core:core-ktx:1.1.0'
+ implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.security:security-crypto:1.0.0-alpha02'
- implementation 'com.google.android.material:material:1.1.0'
+ implementation 'com.google.android.material:material:1.2.0-alpha04'
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
@@ -50,7 +50,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:3.12.6"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- testImplementation 'junit:junit:4.12'
+ testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
diff --git a/app/src/main/java/net/taler/cashier/Amount.kt
b/app/src/main/java/net/taler/cashier/Amount.kt
index c1307e9..5aaae65 100644
--- a/app/src/main/java/net/taler/cashier/Amount.kt
+++ b/app/src/main/java/net/taler/cashier/Amount.kt
@@ -4,10 +4,22 @@ data class Amount(val currency: String, val amount: String) {
companion object {
+ private val SIGNED_REGEX = Regex("""([+\-])(\w+):([0-9.]+)""")
+
fun fromString(strAmount: String): Amount {
val components = strAmount.split(":")
return Amount(components[0], components[1])
}
+
+ fun fromStringSigned(strAmount: String): Amount? {
+ val groups = SIGNED_REGEX.matchEntire(strAmount)?.groupValues ?:
emptyList()
+ if (groups.size < 4) return null
+ var amount = groups[3].toDoubleOrNull() ?: return null
+ if (groups[1] == "-") amount *= -1
+ val currency = groups[2]
+ return Amount(currency, String.format("%.2f", amount))
+ }
+
}
}
diff --git a/app/src/main/java/net/taler/cashier/BalanceFragment.kt
b/app/src/main/java/net/taler/cashier/BalanceFragment.kt
index 90e2e8b..50e8488 100644
--- a/app/src/main/java/net/taler/cashier/BalanceFragment.kt
+++ b/app/src/main/java/net/taler/cashier/BalanceFragment.kt
@@ -30,6 +30,7 @@ class BalanceFragment : Fragment() {
viewModel.balance.observe(viewLifecycleOwner, Observer { balance ->
balanceView.text = balance
progressBar.visibility = INVISIBLE
+ fab.show()
})
fab.setOnClickListener {
BalanceFragmentDirections.actionBalanceFragmentToWithdrawFragment().let {
diff --git a/app/src/main/java/net/taler/cashier/ConfigFragment.kt
b/app/src/main/java/net/taler/cashier/ConfigFragment.kt
index fc53124..e48a32d 100644
--- a/app/src/main/java/net/taler/cashier/ConfigFragment.kt
+++ b/app/src/main/java/net/taler/cashier/ConfigFragment.kt
@@ -16,6 +16,8 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import kotlinx.android.synthetic.main.fragment_config.*
+private const val URL_BANK_TEST = "https://bank.test.taler.net"
+
class ConfigFragment : Fragment() {
private val viewModel: MainViewModel by activityViewModels()
@@ -30,9 +32,17 @@ class ConfigFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (savedInstanceState == null) {
- urlView.editText!!.setText(viewModel.config.bankUrl)
+ if (viewModel.config.bankUrl.isBlank() && BuildConfig.DEBUG) {
+ urlView.editText!!.setText(URL_BANK_TEST)
+ } else {
+ urlView.editText!!.setText(viewModel.config.bankUrl)
+ }
usernameView.editText!!.setText(viewModel.config.username)
passwordView.editText!!.setText(viewModel.config.password)
+ } else {
+
urlView.editText!!.setText(savedInstanceState.getCharSequence("urlView"))
+
usernameView.editText!!.setText(savedInstanceState.getCharSequence("usernameView"))
+
passwordView.editText!!.setText(savedInstanceState.getCharSequence("passwordView"))
}
saveButton.setOnClickListener {
val config = Config(
@@ -65,6 +75,14 @@ class ConfigFragment : Fragment() {
}
}
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ // for some reason automatic restore isn't working at the moment!?
+ outState.putCharSequence("urlView", urlView.editText?.text)
+ outState.putCharSequence("usernameView", usernameView.editText?.text)
+ outState.putCharSequence("passwordView", passwordView.editText?.text)
+ }
+
private fun checkConfig(config: Config): Boolean {
if (!config.bankUrl.startsWith("https://")) {
urlView.error = getString(R.string.config_bank_url_error)
@@ -86,7 +104,8 @@ class ConfigFragment : Fragment() {
val action =
ConfigFragmentDirections.actionConfigFragmentToBalanceFragment()
findNavController().navigate(action)
} else {
- Snackbar.make(view!!, R.string.config_error, LENGTH_LONG).show()
+ val res = if (result.authError) R.string.config_error_auth else
R.string.config_error
+ Snackbar.make(view!!, res, LENGTH_LONG).show()
}
saveButton.visibility = VISIBLE
progressBar.visibility = INVISIBLE
diff --git a/app/src/main/java/net/taler/cashier/HttpHelper.kt
b/app/src/main/java/net/taler/cashier/HttpHelper.kt
index fa23bb3..6b9b1fa 100644
--- a/app/src/main/java/net/taler/cashier/HttpHelper.kt
+++ b/app/src/main/java/net/taler/cashier/HttpHelper.kt
@@ -1,46 +1,76 @@
package net.taler.cashier
+import android.util.Log
import androidx.annotation.WorkerThread
import okhttp3.Credentials
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
-import okhttp3.Response
+import org.json.JSONObject
object HttpHelper {
+ private val TAG = HttpHelper::class.java.simpleName
private const val MIME_TYPE_JSON = "application/json"
@WorkerThread
- fun makeJsonGetRequest(url: String, config: Config): Response {
+ fun makeJsonGetRequest(url: String, config: Config): HttpJsonResult {
val request = Request.Builder()
.addHeader("Accept", MIME_TYPE_JSON)
.url(url)
.get()
.build()
- return getHttpClient(config.username, config.password)
+ val response = try {
+ getHttpClient(config.username, config.password)
.newCall(request)
.execute()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error retrieving $url", e)
+ return HttpJsonResult.Error(500)
+ }
+ return if (response.code() == 200 && response.body() != null) {
+ val jsonObject = JSONObject(response.body()!!.string())
+ HttpJsonResult.Success(jsonObject)
+ } else {
+ Log.e(TAG, "Received status ${response.code()} from $url expected
200")
+ HttpJsonResult.Error(response.code())
+ }
}
private val MEDIA_TYPE_JSON = MediaType.parse("$MIME_TYPE_JSON;
charset=utf-8")
@WorkerThread
- fun makeJsonPostRequest(url: String, body: String, config: Config):
Response {
+ fun makeJsonPostRequest(url: String, body: String, config: Config):
HttpJsonResult {
val request = Request.Builder()
.addHeader("Accept", MIME_TYPE_JSON)
.url(url)
.post(RequestBody.create(MEDIA_TYPE_JSON, body))
.build()
- return getHttpClient(config.username, config.password)
- .newCall(request)
- .execute()
+ val response = try {
+ getHttpClient(config.username, config.password)
+ .newCall(request)
+ .execute()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error retrieving $url", e)
+ return HttpJsonResult.Error(500)
+ }
+ return if (response.code() == 200 && response.body() != null) {
+ val jsonObject = JSONObject(response.body()!!.string())
+ HttpJsonResult.Success(jsonObject)
+ } else {
+ Log.e(TAG, "Received status ${response.code()} from $url expected
200")
+ HttpJsonResult.Error(response.code())
+ }
}
private fun getHttpClient(username: String, password: String) =
OkHttpClient.Builder().authenticator { _, response ->
val credential = Credentials.basic(username, password)
+ if (credential == response.request().header("Authorization")) {
+ // If we already failed with these credentials, don't retry
+ return@authenticator null
+ }
response
.request()
.newBuilder()
@@ -49,3 +79,8 @@ object HttpHelper {
}.build()
}
+
+sealed class HttpJsonResult {
+ class Error(val statusCode: Int) : HttpJsonResult()
+ class Success(val json: JSONObject) : HttpJsonResult()
+}
diff --git a/app/src/main/java/net/taler/cashier/MainViewModel.kt
b/app/src/main/java/net/taler/cashier/MainViewModel.kt
index 9218122..b3b1184 100644
--- a/app/src/main/java/net/taler/cashier/MainViewModel.kt
+++ b/app/src/main/java/net/taler/cashier/MainViewModel.kt
@@ -2,7 +2,6 @@ package net.taler.cashier
import android.annotation.SuppressLint
import android.app.Application
-import android.graphics.Bitmap
import android.util.Log
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
@@ -17,9 +16,9 @@ import androidx.security.crypto.MasterKeys
import androidx.security.crypto.MasterKeys.AES256_GCM_SPEC
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import net.taler.cashier.Amount.Companion.fromStringSigned
import net.taler.cashier.HttpHelper.makeJsonGetRequest
-import net.taler.cashier.HttpHelper.makeJsonPostRequest
-import org.json.JSONObject
+import net.taler.cashier.withdraw.WithdrawManager
private val TAG = MainViewModel::class.java.simpleName
@@ -44,7 +43,8 @@ class MainViewModel(private val app: Application) :
AndroidViewModel(app) {
password = prefs.getString(PREF_KEY_PASSWORD, "")!!
)
- private var currency: String = prefs.getString(PREF_KEY_PASSWORD,
"TESTKUDOS")!!
+ internal var currency: String = prefs.getString(PREF_KEY_CURRENCY,
"TESTKUDOS")!!
+ private set
private val mConfigResult = MutableLiveData<ConfigResult>()
val configResult: LiveData<ConfigResult> = mConfigResult
@@ -52,11 +52,8 @@ class MainViewModel(private val app: Application) :
AndroidViewModel(app) {
private val mBalance = MutableLiveData<String>()
val balance: LiveData<String> = mBalance
- private val mWithdrawAmount = MutableLiveData<String>()
- val withdrawAmount: LiveData<String> = mWithdrawAmount
-
- private val mWithdrawResult = MutableLiveData<WithdrawResult>()
- val withdrawResult: LiveData<WithdrawResult> = mWithdrawResult
+ internal val withdrawManager =
+ WithdrawManager(app, this)
fun hasConfig() = config.bankUrl.isNotEmpty()
&& config.username.isNotEmpty()
@@ -70,32 +67,24 @@ class MainViewModel(private val app: Application) :
AndroidViewModel(app) {
fun checkAndSaveConfig(config: Config) {
mConfigResult.value = null
viewModelScope.launch(Dispatchers.IO) {
- // TODO use to-be-done final URL
- val url = "${config.bankUrl}/history?delta=1&direction=both"
+ val url = "${config.bankUrl}/accounts/${config.username}/balance"
Log.d(TAG, "Checking config: $url")
- val response = try {
- makeJsonGetRequest(url, config)
- } catch (e: Exception) {
- Log.e(TAG, "Error fetching $url", e)
- mConfigResult.postValue(ConfigResult(false))
- return@launch
- }
- if (response.code() == 200 && response.body() != null) {
- // get and save currency
- val json = JSONObject(response.body()!!.string())
- val history = json.getJSONArray("data")
- val amount = history.getJSONObject(0).getString("amount")
- currency = Amount.fromString(amount).currency
- prefs.edit().putString(PREF_KEY_CURRENCY, currency).apply()
-
- // save config
- saveConfig(config)
- mConfigResult.postValue(ConfigResult(true))
- } else {
- Log.e(TAG, "Error fetching $url - Response:
${response.code()}")
- val authError = false
- mConfigResult.postValue(ConfigResult(false, authError))
+ val result = when (val response = makeJsonGetRequest(url, config))
{
+ is HttpJsonResult.Success -> {
+ val balance = response.json.getString("balance")
+ val amount = fromStringSigned(balance)!!
+ currency = amount.currency
+ prefs.edit().putString(PREF_KEY_CURRENCY, currency).apply()
+ // save config
+ saveConfig(config)
+ ConfigResult(true)
+ }
+ is HttpJsonResult.Error -> {
+ val authError = response.statusCode == 401
+ ConfigResult(false, authError)
+ }
}
+ mConfigResult.postValue(result)
}
}
@@ -112,23 +101,17 @@ class MainViewModel(private val app: Application) :
AndroidViewModel(app) {
fun getBalance() = viewModelScope.launch(Dispatchers.IO) {
check(hasConfig()) { "No config to get balance" }
- val url = "${config.bankUrl}/history?delta=50&direction=both"
- val response = try {
- makeJsonGetRequest(url, config)
- } catch (e: Exception) {
- Log.e(TAG, "Error fetching $url", e)
- mBalance.postValue(app.getString(R.string.balance_error))
- return@launch
- }
- val result = if (response.code() == 200 && response.body() != null) {
- // TODO get real amount here once API is available
- val json = JSONObject(response.body()!!.string())
- val amountStr =
json.getJSONArray("data").getJSONObject(0).getString("amount")
- val amount = Amount.fromString(amountStr)
- "${amount.amount} ${amount.currency}"
- } else {
- Log.e(TAG, "Error fetching $url - Response: ${response.code()}")
- app.getString(R.string.balance_error)
+ val url = "${config.bankUrl}/accounts/${config.username}/balance"
+ Log.d(TAG, "Checking balance at $url")
+ val result = when (val response = makeJsonGetRequest(url, config)) {
+ is HttpJsonResult.Success -> {
+ val balance = response.json.getString("balance")
+ val amount = fromStringSigned(balance)!!
+ "${amount.amount} ${amount.currency}"
+ }
+ is HttpJsonResult.Error -> {
+ app.getString(R.string.balance_error)
+ }
}
mBalance.postValue(result)
}
@@ -137,40 +120,6 @@ class MainViewModel(private val app: Application) :
AndroidViewModel(app) {
saveConfig(config.copy(password = ""))
}
- @UiThread
- fun hasSufficientBalance(amount: Int): Boolean {
- val balanceStr = balance.value?.split(' ')?.get(0)
- if (balanceStr == app.getString(R.string.balance_error)) return false
- val balanceDouble = balanceStr?.toDouble() ?: 0.0
- return amount <= balanceDouble
- }
-
- @UiThread
- fun withdraw(amount: Int) {
- check(amount > 0) { "Withdraw amount was <= 0" }
- mWithdrawResult.value = null
- mWithdrawAmount.value = "$amount $currency"
- viewModelScope.launch(Dispatchers.IO) {
- val url = "${config.bankUrl}/taler-bank-api/testing/withdraw-uri"
- val body = JSONObject(mapOf("amount" to
"${currency}:${amount}")).toString()
- val response = try {
- makeJsonPostRequest(url, body, config)
- } catch (e: Exception) {
- Log.e(TAG, "Error fetching $url", e)
- val errorStr = app.getString(R.string.withdraw_error_fetch)
- return@launch
mWithdrawResult.postValue(WithdrawResult.Error(errorStr))
- }
- if (response.code() == 200 && response.body() != null) {
- val json = JSONObject(response.body()!!.string())
- Log.e("TEST", json.toString(4))
- } else {
- Log.e(TAG, "Error fetching $url - Response:
${response.code()}")
- val errorStr = app.getString(R.string.withdraw_error_fetch)
- mWithdrawResult.postValue(WithdrawResult.Error(errorStr))
- }
- }
- }
-
}
data class Config(
@@ -180,9 +129,3 @@ data class Config(
)
class ConfigResult(val success: Boolean, val authError: Boolean = false)
-
-sealed class WithdrawResult {
- object InsufficientBalance : WithdrawResult()
- class Error(val msg: String) : WithdrawResult()
- class Success(val talerUri: String, val qrCode: Bitmap) : WithdrawResult()
-}
diff --git a/app/src/main/java/net/taler/cashier/TransactionFragment.kt
b/app/src/main/java/net/taler/cashier/TransactionFragment.kt
deleted file mode 100644
index 13e6005..0000000
--- a/app/src/main/java/net/taler/cashier/TransactionFragment.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package net.taler.cashier
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.View.INVISIBLE
-import android.view.View.VISIBLE
-import android.view.ViewGroup
-import androidx.core.content.ContextCompat.getColor
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.Observer
-import androidx.navigation.fragment.findNavController
-import kotlinx.android.synthetic.main.fragment_transaction.*
-import net.taler.cashier.WithdrawResult.Error
-import net.taler.cashier.WithdrawResult.InsufficientBalance
-import net.taler.cashier.WithdrawResult.Success
-
-class TransactionFragment : Fragment() {
-
- private val viewModel: MainViewModel by activityViewModels()
- private val nfcManager = NfcManager()
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- return inflater.inflate(R.layout.fragment_transaction, container,
false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- viewModel.withdrawAmount.observe(viewLifecycleOwner, Observer { amount
->
- amountView.text = amount
- })
- viewModel.withdrawResult.observe(viewLifecycleOwner, Observer { result
->
- if (result != null) {
- progressBar.animate()
- .alpha(0f)
- .withEndAction { progressBar.visibility = INVISIBLE }
- .setDuration(750)
- .start()
- }
- when (result) {
- is InsufficientBalance -> {
- val c = getColor(requireContext(),
R.color.design_default_color_error)
- introView.setTextColor(c)
- introView.text =
getString(R.string.withdraw_error_insufficient_balance)
- }
- is Error -> {
- val c = getColor(requireContext(),
R.color.design_default_color_error)
- introView.setTextColor(c)
- introView.text = result.msg
- }
- is Success -> {
- // start NFC
- nfcManager.setTagString(result.talerUri)
- NfcManager.start(requireActivity(), nfcManager)
- // show QR code
- qrCodeView.alpha = 0f
- qrCodeView.animate()
- .alpha(1f)
- .withStartAction {
- qrCodeView.visibility = VISIBLE
- qrCodeView.setImageBitmap(result.qrCode)
- }
- .setDuration(750)
- .start()
- }
- }
- })
- cancelButton.setOnClickListener { findNavController().popBackStack() }
- }
-
- override fun onStart() {
- super.onStart()
- if (viewModel.withdrawResult.value is Success) {
- NfcManager.start(requireActivity(), nfcManager)
- }
- }
-
- override fun onStop() {
- super.onStop()
- NfcManager.stop(requireActivity())
- }
-
-}
diff --git a/app/src/main/java/net/taler/cashier/Utils.kt
b/app/src/main/java/net/taler/cashier/Utils.kt
index edcdad2..5c9cf5c 100644
--- a/app/src/main/java/net/taler/cashier/Utils.kt
+++ b/app/src/main/java/net/taler/cashier/Utils.kt
@@ -1,5 +1,9 @@
package net.taler.cashier
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+
object Utils {
private const val HEX_CHARS = "0123456789ABCDEF"
@@ -34,3 +38,20 @@ object Utils {
}
}
+
+fun View.fadeIn(endAction: () -> Unit = {}) {
+ alpha = 0f
+ visibility = VISIBLE
+ animate().alpha(1f).withEndAction {
+ endAction.invoke()
+ }.start()
+}
+
+fun View.fadeOut(endAction: () -> Unit = {}) {
+ if (visibility == INVISIBLE) return
+ animate().alpha(0f).withEndAction {
+ visibility = INVISIBLE
+ alpha = 1f
+ endAction.invoke()
+ }.start()
+}
diff --git a/app/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt
b/app/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt
new file mode 100644
index 0000000..acb0ea3
--- /dev/null
+++ b/app/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt
@@ -0,0 +1,33 @@
+package net.taler.cashier.withdraw
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_success.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+
+class ErrorFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { viewModel.withdrawManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_error, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ withdrawManager.resetState()
+ backButton.setOnClickListener {
+ findNavController().popBackStack()
+ }
+ }
+
+}
diff --git a/app/src/main/java/net/taler/cashier/NfcManager.kt
b/app/src/main/java/net/taler/cashier/withdraw/NfcManager.kt
similarity index 96%
rename from app/src/main/java/net/taler/cashier/NfcManager.kt
rename to app/src/main/java/net/taler/cashier/withdraw/NfcManager.kt
index 5473eb0..3aa1caa 100644
--- a/app/src/main/java/net/taler/cashier/NfcManager.kt
+++ b/app/src/main/java/net/taler/cashier/withdraw/NfcManager.kt
@@ -1,4 +1,4 @@
-package net.taler.cashier
+package net.taler.cashier.withdraw
import android.app.Activity
import android.content.Context
@@ -23,14 +23,18 @@ class NfcManager : NfcAdapter.ReaderCallback {
* Enables NFC reader mode. Don't forget to call [stop] afterwards.
*/
fun start(activity: Activity, nfcManager: NfcManager) {
- getNfcAdapter(activity)?.enableReaderMode(activity, nfcManager,
nfcManager.flags, null)
+ getNfcAdapter(
+ activity
+ )?.enableReaderMode(activity, nfcManager, nfcManager.flags, null)
}
/**
* Disables NFC reader mode. Call after [start].
*/
fun stop(activity: Activity) {
- getNfcAdapter(activity)?.disableReaderMode(activity)
+ getNfcAdapter(
+ activity
+ )?.disableReaderMode(activity)
}
private fun getNfcAdapter(context: Context): NfcAdapter? {
diff --git a/app/src/main/java/net/taler/cashier/QrCodeManager.kt
b/app/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt
similarity index 95%
rename from app/src/main/java/net/taler/cashier/QrCodeManager.kt
rename to app/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt
index 61060a2..a5a5496 100644
--- a/app/src/main/java/net/taler/cashier/QrCodeManager.kt
+++ b/app/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt
@@ -1,4 +1,4 @@
-package net.taler.cashier
+package net.taler.cashier.withdraw
import android.graphics.Bitmap
import android.graphics.Bitmap.Config.RGB_565
diff --git a/app/src/main/java/net/taler/cashier/withdraw/SuccessFragment.kt
b/app/src/main/java/net/taler/cashier/withdraw/SuccessFragment.kt
new file mode 100644
index 0000000..53459f9
--- /dev/null
+++ b/app/src/main/java/net/taler/cashier/withdraw/SuccessFragment.kt
@@ -0,0 +1,33 @@
+package net.taler.cashier.withdraw
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_success.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+
+class SuccessFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { viewModel.withdrawManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_success, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ withdrawManager.resetState()
+ backButton.setOnClickListener {
+ findNavController().popBackStack()
+ }
+ }
+
+}
diff --git
a/app/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt
b/app/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt
new file mode 100644
index 0000000..f359cbc
--- /dev/null
+++ b/app/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt
@@ -0,0 +1,146 @@
+package net.taler.cashier.withdraw
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat.getColor
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_transaction.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+import net.taler.cashier.fadeIn
+import net.taler.cashier.fadeOut
+import
net.taler.cashier.withdraw.TransactionFragmentDirections.Companion.actionTransactionFragmentToErrorFragment
+import
net.taler.cashier.withdraw.TransactionFragmentDirections.Companion.actionTransactionFragmentToSuccessFragment
+import net.taler.cashier.withdraw.WithdrawResult.Error
+import net.taler.cashier.withdraw.WithdrawResult.InsufficientBalance
+import net.taler.cashier.withdraw.WithdrawResult.Success
+
+class TransactionFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { viewModel.withdrawManager }
+ private val nfcManager = NfcManager()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_transaction, container,
false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ withdrawManager.withdrawAmount.observe(viewLifecycleOwner, Observer {
amount ->
+ amountView.text = amount
+ })
+ withdrawManager.withdrawResult.observe(viewLifecycleOwner, Observer {
result ->
+ onWithdrawResultReceived(result)
+ })
+ withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer {
status ->
+ onWithdrawStatusChanged(status)
+ })
+ cancelButton.setOnClickListener {
+ findNavController().popBackStack()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (withdrawManager.withdrawResult.value is Success) {
+ NfcManager.start(
+ requireActivity(),
+ nfcManager
+ )
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ NfcManager.stop(requireActivity())
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!requireActivity().isChangingConfigurations) {
+ withdrawManager.abort()
+ }
+ }
+
+ private fun onWithdrawResultReceived(result: WithdrawResult?) {
+ if (result != null) {
+ progressBar.animate()
+ .alpha(0f)
+ .withEndAction { progressBar?.visibility = INVISIBLE }
+ .setDuration(750)
+ .start()
+ }
+ when (result) {
+ is InsufficientBalance -> {
+ val c = getColor(
+ requireContext(),
+ R.color.design_default_color_error
+ )
+ introView.setTextColor(c)
+ introView.text =
getString(R.string.withdraw_error_insufficient_balance)
+ }
+ is Error -> {
+ val c = getColor(
+ requireContext(),
+ R.color.design_default_color_error
+ )
+ introView.setTextColor(c)
+ introView.text = result.msg
+ }
+ is Success -> {
+ // start NFC
+ nfcManager.setTagString(result.talerUri)
+ NfcManager.start(
+ requireActivity(),
+ nfcManager
+ )
+ // show QR code
+ qrCodeView.alpha = 0f
+ qrCodeView.animate()
+ .alpha(1f)
+ .withStartAction {
+ qrCodeView.visibility = VISIBLE
+ qrCodeView.setImageBitmap(result.qrCode)
+ }
+ .setDuration(750)
+ .start()
+ }
+ }
+ }
+
+ private fun onWithdrawStatusChanged(status: WithdrawStatus?): Any = when
(status) {
+ is WithdrawStatus.SelectionDone -> {
+ qrCodeView.fadeOut()
+ progressBar.fadeIn()
+ introView.fadeOut {
+ introView.text = getString(R.string.transaction_intro_scanned)
+ introView.fadeIn()
+ }
+ }
+ is WithdrawStatus.Confirmed ->
actionTransactionFragmentToSuccessFragment().let {
+ findNavController().navigate(it)
+ }
+ is WithdrawStatus.Aborted -> onError()
+ is WithdrawStatus.Error -> onError()
+ null -> {
+ // no-op
+ }
+ }
+
+ private fun onError() {
+ actionTransactionFragmentToErrorFragment().let {
+ findNavController().navigate(it)
+ }
+ }
+
+}
diff --git a/app/src/main/java/net/taler/cashier/WithdrawFragment.kt
b/app/src/main/java/net/taler/cashier/withdraw/WithdrawFragment.kt
similarity index 74%
rename from app/src/main/java/net/taler/cashier/WithdrawFragment.kt
rename to app/src/main/java/net/taler/cashier/withdraw/WithdrawFragment.kt
index 75fd99f..d4b4c8d 100644
--- a/app/src/main/java/net/taler/cashier/WithdrawFragment.kt
+++ b/app/src/main/java/net/taler/cashier/withdraw/WithdrawFragment.kt
@@ -1,4 +1,4 @@
-package net.taler.cashier
+package net.taler.cashier.withdraw
import android.os.Bundle
import android.view.LayoutInflater
@@ -9,10 +9,13 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_withdraw.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
class WithdrawFragment : Fragment() {
private val viewModel: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { viewModel.withdrawManager }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@@ -27,19 +30,26 @@ class WithdrawFragment : Fragment() {
button20.setOnClickListener { onAmountButtonPressed(20) }
button50.setOnClickListener { onAmountButtonPressed(50) }
+ if (savedInstanceState != null) {
+
amountView.editText!!.setText(savedInstanceState.getCharSequence("amount"))
+ }
amountView.editText!!.setOnEditorActionListener { _, actionId, _ ->
if (actionId == IME_ACTION_GO) {
onAmountConfirmed(getAmountFromView())
true
} else false
}
- clearButton.setOnClickListener {
- amountView.editText!!.text = null
- amountView.error = null
- }
fab.setOnClickListener { onAmountConfirmed(getAmountFromView()) }
}
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ // for some reason automatic restore isn't working at the moment!?
+ amountView?.editText?.text.let {
+ outState.putCharSequence("amount", it)
+ }
+ }
+
private fun onAmountButtonPressed(amount: Int) {
amountView.editText!!.setText(amount.toString())
amountView.error = null
@@ -54,12 +64,13 @@ class WithdrawFragment : Fragment() {
private fun onAmountConfirmed(amount: Int) {
if (amount <= 0) {
amountView.error = getString(R.string.withdraw_error_zero)
- } else if (!viewModel.hasSufficientBalance(amount)) {
+ } else if (!withdrawManager.hasSufficientBalance(amount)) {
amountView.error =
getString(R.string.withdraw_error_insufficient_balance)
} else {
amountView.error = null
- viewModel.withdraw(amount)
-
WithdrawFragmentDirections.actionWithdrawFragmentToTransactionFragment().let {
+ withdrawManager.withdraw(amount)
+
WithdrawFragmentDirections.actionWithdrawFragmentToTransactionFragment()
+ .let {
findNavController().navigate(it)
}
}
diff --git a/app/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
b/app/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
new file mode 100644
index 0000000..6ccf344
--- /dev/null
+++ b/app/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
@@ -0,0 +1,184 @@
+package net.taler.cashier.withdraw
+
+import android.app.Application
+import android.graphics.Bitmap
+import android.os.CountDownTimer
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.distinctUntilChanged
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import net.taler.cashier.HttpHelper.makeJsonGetRequest
+import net.taler.cashier.HttpHelper.makeJsonPostRequest
+import net.taler.cashier.HttpJsonResult.Error
+import net.taler.cashier.HttpJsonResult.Success
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+import org.json.JSONObject
+import java.util.concurrent.TimeUnit.MINUTES
+import java.util.concurrent.TimeUnit.SECONDS
+
+private val TAG = WithdrawManager::class.java.simpleName
+
+private val INTERVAL = SECONDS.toMillis(1)
+private val TIMEOUT = MINUTES.toMillis(2)
+
+class WithdrawManager(
+ private val app: Application,
+ private val viewModel: MainViewModel
+) {
+ private val scope
+ get() = viewModel.viewModelScope
+
+ private val config
+ get() = viewModel.config
+
+ private val currency
+ get() = viewModel.currency
+
+ private val mWithdrawAmount = MutableLiveData<String>()
+ val withdrawAmount: LiveData<String> = mWithdrawAmount
+
+ private val mWithdrawResult = MutableLiveData<WithdrawResult>()
+ val withdrawResult: LiveData<WithdrawResult> = mWithdrawResult
+
+ private val mWithdrawStatus = MutableLiveData<WithdrawStatus>()
+ val withdrawStatus: LiveData<WithdrawStatus> =
mWithdrawStatus.distinctUntilChanged()
+
+ @UiThread
+ fun hasSufficientBalance(amount: Int): Boolean {
+ val balanceStr = viewModel.balance.value?.split(' ')?.get(0)
+ if (balanceStr == app.getString(R.string.balance_error)) return false
+ val balanceDouble = balanceStr?.toDouble() ?: 0.0
+ return amount <= balanceDouble
+ }
+
+ @UiThread
+ fun withdraw(amount: Int) {
+ check(amount > 0) { "Withdraw amount was <= 0" }
+ mWithdrawResult.value = null
+ mWithdrawAmount.value = "$amount $currency"
+ scope.launch(Dispatchers.IO) {
+ val url =
"${config.bankUrl}/accounts/${config.username}/withdrawals"
+ Log.d(TAG, "Starting withdrawal at $url")
+ val body = JSONObject(mapOf("amount" to
"${currency}:${amount}")).toString()
+ when (val result = makeJsonPostRequest(url, body, config)) {
+ is Success -> {
+ val talerUri = result.json.getString("taler_withdraw_uri")
+ val withdrawResult = WithdrawResult.Success(
+ id = result.json.getString("withdrawal_id"),
+ talerUri = talerUri,
+ qrCode = QrCodeManager.makeQrCode(talerUri)
+ )
+ mWithdrawResult.postValue(withdrawResult)
+ timer.start()
+ }
+ is Error -> {
+ val errorStr = app.getString(R.string.withdraw_error_fetch)
+ mWithdrawResult.postValue(WithdrawResult.Error(errorStr))
+ }
+ }
+ }
+ }
+
+ private val timer: CountDownTimer = object : CountDownTimer(TIMEOUT,
INTERVAL) {
+ override fun onTick(millisUntilFinished: Long) {
+ val result = withdrawResult.value
+ if (result is WithdrawResult.Success) {
+ checkWithdrawStatus(result.id)
+ } else {
+ cancel()
+ }
+ }
+
+ override fun onFinish() {
+ abort()
+ mWithdrawStatus.postValue(WithdrawStatus.Error)
+ cancel()
+ }
+ }
+
+ private fun checkWithdrawStatus(withdrawalId: String) =
scope.launch(Dispatchers.IO) {
+ val url =
"${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}"
+ Log.d(TAG, "Checking withdraw status at $url")
+ val response = makeJsonGetRequest(url, config)
+ if (response !is Success) return@launch // ignore errors and continue
trying
+ when {
+ response.json.getBoolean("aborted") -> {
+ mWithdrawStatus.postValue(WithdrawStatus.Aborted)
+ timer.cancel()
+ }
+ response.json.getBoolean("confirmation_done") -> {
+ mWithdrawStatus.postValue(WithdrawStatus.Confirmed)
+ viewModel.getBalance()
+ timer.cancel()
+ }
+ response.json.getBoolean("selection_done") -> {
+ if (mWithdrawStatus.value != WithdrawStatus.SelectionDone) {
+ mWithdrawStatus.postValue(WithdrawStatus.SelectionDone)
+ confirm(withdrawalId)
+ }
+ }
+ }
+ }
+
+ /**
+ * Aborts the last [withdrawResult], if it exists und there is no
[withdrawStatus].
+ * Otherwise this is a no-op.
+ */
+ fun abort() {
+ val result = withdrawResult.value
+ val status = withdrawStatus.value
+ if (result is WithdrawResult.Success && status == null) {
+ timer.cancel()
+ abort(result.id)
+ }
+ }
+
+ private fun abort(withdrawalId: String) = scope.launch(Dispatchers.IO) {
+ val url =
"${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/abort"
+ Log.d(TAG, "Aborting withdrawal at $url")
+ makeJsonPostRequest(url, "", config)
+ }
+
+ @WorkerThread
+ private fun confirm(withdrawalId: String) {
+ val url =
+
"${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/confirm"
+ Log.d(TAG, "Confirming withdrawal at $url")
+ when (val result = makeJsonPostRequest(url, "", config)) {
+ is Success -> {
+ mWithdrawStatus.postValue(WithdrawStatus.Confirmed)
+ }
+ is Error -> {
+ Log.e(TAG, "Error confirming withdrawal. Status code:
${result.statusCode}")
+ mWithdrawStatus.postValue(WithdrawStatus.Error)
+ }
+ }
+ }
+
+ @UiThread
+ fun resetState() {
+ mWithdrawAmount.value = null
+ mWithdrawResult.value = null
+ mWithdrawStatus.value = null
+ }
+
+}
+
+sealed class WithdrawResult {
+ object InsufficientBalance : WithdrawResult()
+ class Error(val msg: String) : WithdrawResult()
+ class Success(val id: String, val talerUri: String, val qrCode: Bitmap) :
WithdrawResult()
+}
+
+sealed class WithdrawStatus {
+ object Error : WithdrawStatus()
+ object Aborted : WithdrawStatus()
+ object SelectionDone : WithdrawStatus()
+ object Confirmed : WithdrawStatus()
+}
diff --git a/app/src/main/res/drawable/ic_check_circle.xml
b/app/src/main/res/drawable/ic_check_circle.xml
new file mode 100644
index 0000000..7fbbed1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_check_circle.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:alpha="0.56"
+ android:tint="@color/green"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48
10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_error.xml
b/app/src/main/res/drawable/ic_error.xml
new file mode 100644
index 0000000..b7e22a0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_error.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:alpha="0.56"
+ android:tint="@color/red"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48
10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
+</vector>
diff --git a/app/src/main/res/layout/fragment_transaction.xml
b/app/src/main/res/layout-w550dp/fragment_transaction.xml
similarity index 75%
copy from app/src/main/res/layout/fragment_transaction.xml
copy to app/src/main/res/layout-w550dp/fragment_transaction.xml
index 3804595..0076f25 100644
--- a/app/src/main/res/layout/fragment_transaction.xml
+++ b/app/src/main/res/layout-w550dp/fragment_transaction.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".TransactionFragment">
+ tools:context=".withdraw.TransactionFragment">
<TextView
android:id="@+id/introView"
@@ -14,7 +14,7 @@
android:gravity="center_horizontal"
android:text="@string/transaction_intro"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
- app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -25,11 +25,18 @@
android:layout_margin="32dp"
android:gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
- app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/introView"
tools:text="50 KUDOS" />
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.5" />
+
<ImageView
android:id="@+id/qrCodeView"
android:layout_width="256dp"
@@ -37,9 +44,10 @@
android:layout_margin="32dp"
android:keepScreenOn="true"
android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/amountView"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@tools:sample/avatars"
tools:visibility="visible" />
@@ -56,13 +64,13 @@
<Button
android:id="@+id/cancelButton"
- style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="32dp"
- android:text="@string/transaction_cancel"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/red"
+ android:text="@string/transaction_abort"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/qrCodeView"
app:layout_constraintVertical_bias="1.0" />
diff --git a/app/src/main/res/layout/activity_main.xml
b/app/src/main/res/layout/activity_main.xml
index 413d578..bf7f543 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -11,12 +11,11 @@
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
- android:background="?attr/colorPrimary"
- app:popupTheme="@style/AppTheme.PopupOverlay" />
+ android:background="?attr/colorPrimary" />
</com.google.android.material.appbar.AppBarLayout>
diff --git a/app/src/main/res/layout/fragment_balance.xml
b/app/src/main/res/layout/fragment_balance.xml
index 348deb6..dd376d0 100644
--- a/app/src/main/res/layout/fragment_balance.xml
+++ b/app/src/main/res/layout/fragment_balance.xml
@@ -50,8 +50,9 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/default_margin"
- android:tint="@color/design_default_color_surface"
+ android:visibility="invisible"
app:srcCompat="@drawable/ic_withdraw"
- app:useCompatPadding="true" />
+ app:useCompatPadding="true"
+ tools:visibility="visible" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/fragment_config.xml
b/app/src/main/res/layout/fragment_config.xml
index 49855d2..0da0c14 100644
--- a/app/src/main/res/layout/fragment_config.xml
+++ b/app/src/main/res/layout/fragment_config.xml
@@ -7,11 +7,11 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/urlView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
android:hint="@string/config_bank_url"
- app:boxBackgroundMode="outline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@@ -25,6 +25,7 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/usernameView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
@@ -43,6 +44,7 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
@@ -62,7 +64,6 @@
<Button
android:id="@+id/saveButton"
- style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
diff --git a/app/src/main/res/layout/fragment_error.xml
b/app/src/main/res/layout/fragment_error.xml
new file mode 100644
index 0000000..143a92d
--- /dev/null
+++ b/app/src/main/res/layout/fragment_error.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/frameLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".withdraw.SuccessFragment">
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="32dp"
+ android:src="@drawable/ic_error"
+ app:layout_constraintBottom_toTopOf="@+id/textView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="ContentDescription" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/textView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="32dp"
+ android:text="@string/transaction_error"
+ android:textAlignment="center"
+ android:textColor="@color/red"
+ app:autoSizeMaxTextSize="42sp"
+ app:autoSizeTextType="uniform"
+ app:layout_constraintBottom_toTopOf="@+id/backButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/imageView" />
+
+ <Button
+ android:id="@+id/backButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/transaction_button_back"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_success.xml
b/app/src/main/res/layout/fragment_success.xml
new file mode 100644
index 0000000..e59072e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_success.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/frameLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".withdraw.SuccessFragment">
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="32dp"
+ android:src="@drawable/ic_check_circle"
+ app:layout_constraintBottom_toTopOf="@+id/textView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="ContentDescription" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/textView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_margin="32dp"
+ android:text="@string/transaction_success"
+ android:textAlignment="center"
+ android:textColor="@color/green"
+ app:autoSizeMaxTextSize="42sp"
+ app:autoSizeTextType="uniform"
+ app:layout_constraintBottom_toTopOf="@+id/backButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/imageView" />
+
+ <Button
+ android:id="@+id/backButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/transaction_button_back"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_transaction.xml
b/app/src/main/res/layout/fragment_transaction.xml
index 3804595..8083fcf 100644
--- a/app/src/main/res/layout/fragment_transaction.xml
+++ b/app/src/main/res/layout/fragment_transaction.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".TransactionFragment">
+ tools:context=".withdraw.TransactionFragment">
<TextView
android:id="@+id/introView"
@@ -56,11 +56,11 @@
<Button
android:id="@+id/cancelButton"
- style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="32dp"
- android:text="@string/transaction_cancel"
+ android:layout_margin="16dp"
+ android:backgroundTint="@color/red"
+ android:text="@string/transaction_abort"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
diff --git a/app/src/main/res/layout/fragment_withdraw.xml
b/app/src/main/res/layout/fragment_withdraw.xml
index 4a98660..e038978 100644
--- a/app/src/main/res/layout/fragment_withdraw.xml
+++ b/app/src/main/res/layout/fragment_withdraw.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".WithdrawFragment">
+ tools:context=".withdraw.WithdrawFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -23,10 +23,14 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/amountView"
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="32dp"
- app:boxBackgroundMode="outline"
+ android:hint="@string/withdraw_input_amount"
+ app:endIconDrawable="@drawable/ic_clear"
+ app:endIconMode="clear_text"
+ app:endIconTint="?attr/colorControlNormal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button5">
@@ -41,17 +45,6 @@
</com.google.android.material.textfield.TextInputLayout>
- <androidx.appcompat.widget.AppCompatImageButton
- android:id="@+id/clearButton"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:contentDescription="@string/withdraw_clear"
- android:src="@drawable/ic_clear"
- app:layout_constraintEnd_toEndOf="@+id/amountView"
- app:layout_constraintTop_toTopOf="@+id/amountView"
- app:tint="?attr/colorControlNormal" />
-
<Button
android:id="@+id/button5"
style="@style/AmountButton"
diff --git a/app/src/main/res/navigation/nav_graph.xml
b/app/src/main/res/navigation/nav_graph.xml
index 930d13f..5d7dd78 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -32,7 +32,7 @@
<fragment
android:id="@+id/withdrawFragment"
- android:name="net.taler.cashier.WithdrawFragment"
+ android:name="net.taler.cashier.withdraw.WithdrawFragment"
android:label="fragment_withdraw"
tools:layout="@layout/fragment_withdraw">
<action
@@ -45,9 +45,30 @@
<fragment
android:id="@+id/transactionFragment"
- android:name="net.taler.cashier.TransactionFragment"
+ android:name="net.taler.cashier.withdraw.TransactionFragment"
android:label="fragment_transaction"
- tools:layout="@layout/fragment_transaction" />
+ tools:layout="@layout/fragment_transaction">
+ <action
+ android:id="@+id/action_transactionFragment_to_successFragment"
+ app:destination="@id/successFragment"
+ app:launchSingleTop="true"
+ app:popUpTo="@+id/balanceFragment" />
+ <action
+ android:id="@+id/action_transactionFragment_to_errorFragment"
+ app:destination="@id/errorFragment"
+ app:launchSingleTop="true"
+ app:popUpTo="@+id/withdrawFragment" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/successFragment"
+ android:name="net.taler.cashier.withdraw.SuccessFragment"
+ tools:layout="@layout/fragment_success" />
+
+ <fragment
+ android:id="@+id/errorFragment"
+ android:name="net.taler.cashier.withdraw.ErrorFragment"
+ tools:layout="@layout/fragment_error" />
<action
android:id="@+id/action_global_configFragment"
diff --git a/app/src/main/res/values/colors.xml
b/app/src/main/res/values/colors.xml
index 9f6551e..d9acb88 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,4 +3,7 @@
<color name="colorPrimary">#1565C0</color>
<color name="colorPrimaryDark">#6A1B9A</color>
<color name="colorAccent">#D81B60</color>
+
+ <color name="green">#4CAF50</color>
+ <color name="red">#D32F2F</color>
</resources>
diff --git a/app/src/main/res/values/strings.xml
b/app/src/main/res/values/strings.xml
index db80499..ab15c09 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -8,12 +8,14 @@
<string name="config_bank_url_error">The address in invalid.</string>
<string name="config_username_error">Please enter your username</string>
<string name="config_error">Error retrieving configuration</string>
+ <string name="config_error_auth">Invalid username or password</string>
<string name="balance_current_label">Current Balance:</string>
<string name="balance_error">ERROR</string>
<string name="action_reconfigure">Reconfigure</string>
<string name="action_lock">Lock</string>
+ <string name="withdraw_input_amount">Amount</string>
<string name="withdraw_clear">clear amount</string>
<string name="withdraw_into">How much to cash in?</string>
<string name="withdraw_error_zero">Enter positive amount</string>
@@ -21,6 +23,10 @@
<string name="withdraw_error_fetch">Error communicating with bank</string>
<string name="transaction_intro">Scan code or use NFC\nwith the Taler
wallet app\nto get</string>
- <string name="transaction_cancel">Cancel</string>
+ <string name="transaction_intro_scanned">Waiting for confirmation…</string>
+ <string name="transaction_abort">Abort</string>
+ <string name="transaction_success">Transaction successful</string>
+ <string name="transaction_error">Transaction error</string>
+ <string name="transaction_button_back">Go back</string>
</resources>
diff --git a/app/src/main/res/values/styles.xml
b/app/src/main/res/values/styles.xml
index e3ef098..667fe05 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,10 +1,16 @@
<resources>
- <style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
+ <style name="AppTheme"
parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
+ <item
name="colorOnPrimary">@color/design_default_color_background</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorSecondary">@color/colorAccent</item>
+ <item
name="colorOnSecondary">@color/design_default_color_background</item>
<item name="colorAccent">@color/colorAccent</item>
- <!-- Customize your theme here. -->
+
+ <!-- https://stackoverflow.com/q/59081672/4856311 -->
+ <item
name="actionOverflowMenuStyle">@style/Widget.MaterialComponents.PopupMenu.Overflow
+ </item>
</style>
<style name="AppTheme.NoActionBar">
@@ -12,11 +18,9 @@
<item name="windowNoTitle">true</item>
</style>
- <style name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
-
- <style name="AppTheme.PopupOverlay"
parent="ThemeOverlay.AppCompat.DayNight" />
+ <style name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
- <style name="AmountButton" parent="Widget.AppCompat.Button.Colored">
+ <style name="AmountButton" parent="Widget.MaterialComponents.Button">
<item name="android:minWidth">48dp</item>
<item name="android:layout_marginStart">@dimen/default_margin</item>
<item name="android:layout_marginEnd">@dimen/default_margin</item>
diff --git a/build.gradle b/build.gradle
index a212d44..0e297f6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,6 @@
buildscript {
ext.kotlin_version = '1.3.61'
- ext.nav_version = '2.2.0'
+ ext.nav_version = '2.2.1'
repositories {
google()
--
To stop receiving notification emails like this one, please contact
address@hidden.
- [taler-cashier-terminal-android] branch master updated (41dc9a4 -> a8307d0), gnunet, 2020/02/19
- [taler-cashier-terminal-android] 03/06: Get and save currency when doing configuration, gnunet, 2020/02/19
- [taler-cashier-terminal-android] 02/06: Use QR code and NFC to make taler:// Uri available, gnunet, 2020/02/19
- [taler-cashier-terminal-android] 01/06: Add screen for entering amount to withdraw, gnunet, 2020/02/19
- [taler-cashier-terminal-android] 04/06: First complete prototype,
gnunet <=
- [taler-cashier-terminal-android] 05/06: Add app logo and optimize UX workflow, gnunet, 2020/02/19
- [taler-cashier-terminal-android] 06/06: Add README, license and copyright headers, gnunet, 2020/02/19