diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-09-01 18:33:03 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-09-01 18:33:03 +0200 |
commit | 6f963cdfa0e918dbe35c99dafdb648dfa0586d7b (patch) | |
tree | 83ba25c8fa6d7f5eca46f38ba80e5040090436e4 /app/src/main/java/net/taler/wallet | |
parent | b9fd051a1bf453e923ddbbf86cf8602d154278e1 (diff) | |
download | wallet-android-6f963cdfa0e918dbe35c99dafdb648dfa0586d7b.tar.gz wallet-android-6f963cdfa0e918dbe35c99dafdb648dfa0586d7b.tar.bz2 wallet-android-6f963cdfa0e918dbe35c99dafdb648dfa0586d7b.zip |
withdraw via QR code, various bug fixes
Diffstat (limited to 'app/src/main/java/net/taler/wallet')
-rw-r--r-- | app/src/main/java/net/taler/wallet/MainActivity.kt | 41 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/PromptPayment.kt | 2 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/PromptWithdraw.kt | 101 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/ShowBalance.kt | 38 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/WalletViewModel.kt | 162 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/WithdrawSuccessful.kt | 26 |
6 files changed, 305 insertions, 65 deletions
diff --git a/app/src/main/java/net/taler/wallet/MainActivity.kt b/app/src/main/java/net/taler/wallet/MainActivity.kt index 44cd8a6..3d07821 100644 --- a/app/src/main/java/net/taler/wallet/MainActivity.kt +++ b/app/src/main/java/net/taler/wallet/MainActivity.kt @@ -18,6 +18,7 @@ import androidx.lifecycle.ViewModelProviders import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController +import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar import com.google.zxing.integration.android.IntentIntegrator @@ -41,6 +42,14 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte navView.menu.getItem(0).isChecked = true + val fab: FloatingActionButton = findViewById(R.id.fab) + fab.setOnClickListener { + val integrator = IntentIntegrator(this) + integrator.setPrompt("Place merchant's QR Code inside the viewfinder rectangle to initiate payment.") + integrator.initiateScan(listOf("QR_CODE")) + } + fab.hide() + navView.setNavigationItemSelectedListener(this) @@ -166,20 +175,26 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } val url = scanResult.contents!! - if (!url.startsWith("talerpay:")) { - val bar: Snackbar = Snackbar.make( - findViewById(R.id.nav_host_fragment), - "Scanned QR code doesn't contain Taler payment.", - Snackbar.LENGTH_SHORT - ) - bar.show() - return + when { + url.startsWith("taler://pay") -> { + Log.v(TAG, "navigating!") + findNavController(R.id.nav_host_fragment).navigate(R.id.action_showBalance_to_promptPayment) + model.preparePay(url) + } + url.startsWith("taler://withdraw") -> { + Log.v(TAG, "navigating!") + findNavController(R.id.nav_host_fragment).navigate(R.id.action_showBalance_to_promptWithdraw) + model.getWithdrawalInfo(url) + } + else -> { + val bar: Snackbar = Snackbar.make( + findViewById(R.id.nav_host_fragment), + "Scanned QR code doesn't contain Taler payment.", + Snackbar.LENGTH_SHORT + ) + bar.show() + } } - - Log.v(TAG, "navigating!") - - findNavController(R.id.nav_host_fragment).navigate(R.id.action_showBalance_to_promptPayment) - model.preparePay(url) } diff --git a/app/src/main/java/net/taler/wallet/PromptPayment.kt b/app/src/main/java/net/taler/wallet/PromptPayment.kt index 07d3dd2..1d828e3 100644 --- a/app/src/main/java/net/taler/wallet/PromptPayment.kt +++ b/app/src/main/java/net/taler/wallet/PromptPayment.kt @@ -32,7 +32,7 @@ class PromptPayment : Fragment() { var fragmentView: View? = null - fun triggerLoading() { + private fun triggerLoading() { val loading = model.payStatus.value == null || (model.payStatus.value is PayStatus.Loading) val myActivity = activity!! val progressBar = myActivity.findViewById<MaterialProgressBar>(R.id.progress_bar) diff --git a/app/src/main/java/net/taler/wallet/PromptWithdraw.kt b/app/src/main/java/net/taler/wallet/PromptWithdraw.kt new file mode 100644 index 0000000..785da42 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/PromptWithdraw.kt @@ -0,0 +1,101 @@ +package net.taler.wallet + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.navigation.findNavController +import com.google.android.material.snackbar.Snackbar +import me.zhanghai.android.materialprogressbar.MaterialProgressBar + + +class PromptWithdraw : Fragment() { + + private lateinit var model: WalletViewModel + + private fun triggerLoading() { + val loading = + model.withdrawStatus.value is WithdrawStatus.Loading || model.withdrawStatus.value is WithdrawStatus.Withdrawing + val myActivity = activity!! + val progressBar = myActivity.findViewById<MaterialProgressBar>(R.id.progress_bar) + if (loading) { + progressBar.visibility = View.VISIBLE + } else { + progressBar.visibility = View.INVISIBLE + } + } + + private fun showWithdrawStatus(view: View, status: WithdrawStatus) { + val confirmButton = view.findViewById<Button>(R.id.button_confirm_withdraw) + val promptWithdraw = view.findViewById<View>(R.id.prompt_withdraw) + when (status) { + is WithdrawStatus.ReceivedDetails -> { + promptWithdraw.visibility = View.VISIBLE + confirmButton.isEnabled = true + val promptWithdraw = view.findViewById<View>(R.id.prompt_withdraw) + promptWithdraw.visibility = View.VISIBLE + val amountView = view.findViewById<TextView>(R.id.withdraw_amount) + val exchangeView = view.findViewById<TextView>(R.id.withdraw_exchange) + exchangeView.text = status.suggestedExchange + @SuppressLint("SetTextI18n") + amountView.text = "${status.amount.amount} ${status.amount.currency}" + } + is WithdrawStatus.Success -> { + this.model.withdrawStatus.value = WithdrawStatus.None() + activity!!.findNavController(R.id.nav_host_fragment) + .navigate(R.id.action_promptWithdraw_to_withdrawSuccessful) + } + is WithdrawStatus.Loading -> { + promptWithdraw.visibility = View.INVISIBLE + // Wait + } + is WithdrawStatus.Withdrawing -> { + confirmButton.isEnabled = false + + } + is WithdrawStatus.None -> { + + } + else -> { + val bar = Snackbar.make(view, "Bug: Unexpected result", Snackbar.LENGTH_SHORT) + bar.show() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + model = activity?.run { + ViewModelProviders.of(this)[WalletViewModel::class.java] + } ?: throw Exception("Invalid Activity") + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_prompt_withdraw, container, false) + + this.model.withdrawStatus.observe(this, Observer { + triggerLoading() + showWithdrawStatus(view, it) + }) + + view.findViewById<Button>(R.id.button_confirm_withdraw).setOnClickListener { + val status = this.model.withdrawStatus.value + if (status !is WithdrawStatus.ReceivedDetails) { + return@setOnClickListener + } + model.acceptWithdrawal(status.talerWithdrawUri, status.suggestedExchange) + } + + return view + } +} diff --git a/app/src/main/java/net/taler/wallet/ShowBalance.kt b/app/src/main/java/net/taler/wallet/ShowBalance.kt index ba1bf14..a45d4fa 100644 --- a/app/src/main/java/net/taler/wallet/ShowBalance.kt +++ b/app/src/main/java/net/taler/wallet/ShowBalance.kt @@ -1,7 +1,6 @@ package net.taler.wallet -import android.app.Activity import android.os.Bundle import android.util.Log import androidx.fragment.app.Fragment @@ -15,7 +14,6 @@ import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.snackbar.Snackbar import com.google.zxing.integration.android.IntentIntegrator import me.zhanghai.android.materialprogressbar.MaterialProgressBar @@ -35,10 +33,22 @@ class MyAdapter(private var myDataset: WalletBalances) : RecyclerView.Adapter<My } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { + val amount = myDataset.byCurrency[position].available + val amountIncoming = myDataset.byCurrency[position].pendingIncoming val currencyView = holder.rowView.findViewById<TextView>(R.id.balance_currency) - currencyView.text = myDataset.byCurrency[position].currency + currencyView.text = amount.currency val amountView = holder.rowView.findViewById<TextView>(R.id.balance_amount) - amountView.text = myDataset.byCurrency[position].amount + amountView.text = amount.amount + + val amountIncomingRow = holder.rowView.findViewById<View>(R.id.balance_row_pending) + + val amountIncomingView = holder.rowView.findViewById<TextView>(R.id.balance_pending) + if (amountIncoming.isZero()) { + amountIncomingRow.visibility = View.GONE + } else { + amountIncomingRow.visibility = View.VISIBLE + amountIncomingView.text = "${amountIncoming.amount} ${amountIncoming.currency}" + } } fun update(updatedBalances: WalletBalances) { @@ -63,7 +73,7 @@ class ShowBalance : Fragment() { fun triggerLoading() { val loading: Boolean = - (model.isBalanceLoading.value == true) || (model.balances.value == null) || !model.balances.value!!.initialized + (model.testWithdrawalInProgress.value == true) || (model.balances.value == null) || !model.balances.value!!.initialized val myActivity = activity!! val progressBar = myActivity.findViewById<MaterialProgressBar>(R.id.progress_bar) @@ -87,11 +97,6 @@ class ShowBalance : Fragment() { ViewModelProviders.of(this)[WalletViewModel::class.java] } ?: throw Exception("Invalid Activity") - - model.isBalanceLoading.observe(this, Observer { loading -> - Log.v("taler-wallet", "observing balance loading ${loading} in show balance") - triggerLoading() - }) } @@ -129,13 +134,6 @@ class ShowBalance : Fragment() { model.withdrawTestkudos() } - val payNfcButton = view.findViewById<Button>(R.id.button_pay_nfc) - payNfcButton.setOnClickListener { - val bar: Snackbar = Snackbar.make(view, "Sorry, NFC is not implemented yet!", Snackbar.LENGTH_SHORT) - bar.show() - } - - this.balancesView = view.findViewById(R.id.list_balances) this.balancesPlaceholderView = view.findViewById(R.id.list_balances_placeholder) @@ -159,6 +157,12 @@ class ShowBalance : Fragment() { updateBalances(it) }) + model.testWithdrawalInProgress.observe(this, Observer { loading -> + Log.v("taler-wallet", "observing balance loading ${loading} in show balance") + withdrawTestkudosButton.isEnabled = !loading + triggerLoading() + }) + return view } } diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt b/app/src/main/java/net/taler/wallet/WalletViewModel.kt index f644c8d..356eb8a 100644 --- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt +++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt @@ -9,13 +9,16 @@ import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import org.json.JSONObject +import java.io.File import java.io.InputStream import java.lang.Exception -import java.nio.file.Paths val TAG = "taler-wallet" -class AssetModuleLoader(private val assetManager: AssetManager, private val rootPath: String = "node_modules") : +class AssetModuleLoader( + private val assetManager: AssetManager, + private val rootPath: String = "node_modules" +) : AkonoJni.LoadModuleHandler { private fun makeResult(localPath: String, stream: InputStream): ModuleResult { @@ -26,7 +29,8 @@ class AssetModuleLoader(private val assetManager: AssetManager, private val root } private fun tryPath(rawAssetPath: String): ModuleResult? { - val assetPath = Paths.get(rawAssetPath).normalize().toString() + //val assetPath = Paths.get(rawAssetPath).normalize().toString() + val assetPath = File(rawAssetPath).normalize().path try { val moduleStream = assetManager.open(assetPath) return makeResult(assetPath, moduleStream) @@ -54,13 +58,15 @@ class AssetModuleLoader(private val assetManager: AssetManager, private val root } Log.i(TAG, "main field is $mainFile") try { - val modPath = Paths.get("$assetPath/$mainFile").normalize().toString() + //val modPath = Paths.get("$assetPath/$mainFile").normalize().toString() + val modPath = File("$assetPath/$mainFile").normalize().path return makeResult(modPath, assetManager.open(modPath)) } catch (e: Exception) { // ignore } try { - val modPath = Paths.get("$assetPath/$mainFile.js").normalize().toString() + //val modPath = Paths.get("$assetPath/$mainFile.js").normalize().toString() + val modPath = File("$assetPath/$mainFile.js").normalize().path return makeResult(modPath, assetManager.open(modPath)) } catch (e: Exception) { } @@ -105,7 +111,8 @@ class AssetDataHandler(private val assetManager: AssetManager) : AkonoJni.GetDat override fun handleGetData(what: String): ByteArray? { if (what == "taler-emscripten-lib.wasm") { Log.i(TAG, "loading emscripten binary from taler-wallet") - val stream = assetManager.open("node_modules/taler-wallet/emscripten/taler-emscripten-lib.wasm") + val stream = + assetManager.open("node_modules/taler-wallet/emscripten/taler-emscripten-lib.wasm") val bytes: ByteArray = stream.readBytes() Log.i(TAG, "size of emscripten binary: ${bytes.size}") return bytes @@ -117,16 +124,24 @@ class AssetDataHandler(private val assetManager: AssetManager) : AkonoJni.GetDat } data class Amount(val currency: String, val amount: String) { + fun isZero(): Boolean { + return amount.toDouble() == 0.0 + } + companion object { - val FRACTIONAL_BASE = 1e8; + const val FRACTIONAL_BASE = 1e8; fun fromJson(jsonAmount: JSONObject): Amount { val amountCurrency = jsonAmount.getString("currency") val amountValue = jsonAmount.getString("value") val amountFraction = jsonAmount.getString("fraction") val amountIntValue = Integer.parseInt(amountValue) val amountIntFraction = Integer.parseInt(amountFraction) - return Amount(amountCurrency, (amountIntValue + amountIntFraction / FRACTIONAL_BASE).toString()) + return Amount( + amountCurrency, + (amountIntValue + amountIntFraction / FRACTIONAL_BASE).toString() + ) } + fun fromString(strAmount: String): Amount { val components = strAmount.split(":") return Amount(components[0], components[1]) @@ -134,40 +149,60 @@ data class Amount(val currency: String, val amount: String) { } } +data class BalanceEntry(val available: Amount, val pendingIncoming: Amount) -data class WalletBalances(val initialized: Boolean, val byCurrency: List<Amount>) + +data class WalletBalances(val initialized: Boolean, val byCurrency: List<BalanceEntry>) data class ContractTerms(val summary: String, val amount: Amount) open class PayStatus { class None : PayStatus() class Loading : PayStatus() - data class Prepared(val contractTerms: ContractTerms, val proposalId: Int, val totalFees: Amount) : PayStatus() + data class Prepared( + val contractTerms: ContractTerms, + val proposalId: Int, + val totalFees: Amount + ) : PayStatus() + data class InsufficientBalance(val contractTerms: ContractTerms) : PayStatus() data class AlreadyPaid(val contractTerms: ContractTerms) : PayStatus() data class Error(val error: String) : PayStatus() class Success : PayStatus() } +open class WithdrawStatus { + class None : WithdrawStatus() + data class Loading(val talerWithdrawUri: String) : WithdrawStatus() + class Success : WithdrawStatus() + data class ReceivedDetails( + val talerWithdrawUri: String, + val amount: Amount, + val suggestedExchange: String + ) : WithdrawStatus() + + data class Withdrawing(val talerWithdrawUri: String) : WithdrawStatus() +} + class WalletViewModel(val app: Application) : AndroidViewModel(app) { private lateinit var myAkono: AkonoJni private var initialized = false - private var withdrawInProgress: Int = 0 - - val balances: MutableLiveData<WalletBalances> = MutableLiveData() - - val isBalanceLoading: MutableLiveData<Boolean> = MutableLiveData() + val testWithdrawalInProgress: MutableLiveData<Boolean> = MutableLiveData<Boolean>().apply { + value = false + } - //val isProposalLoading: MutableLiveData<Boolean> = MutableLiveData() + val balances: MutableLiveData<WalletBalances> = MutableLiveData<WalletBalances>().apply { + value = WalletBalances(false, listOf()) + } - val payStatus: MutableLiveData<PayStatus> = MutableLiveData() + val payStatus: MutableLiveData<PayStatus> = MutableLiveData<PayStatus>().apply { + value = PayStatus.None() + } - init { - isBalanceLoading.value = false - balances.value = WalletBalances(false, listOf()) - payStatus.value = PayStatus.None() + val withdrawStatus: MutableLiveData<WithdrawStatus> = MutableLiveData<WithdrawStatus>().apply { + value = WithdrawStatus.None() } fun init() { @@ -201,23 +236,50 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { Log.v(TAG, "got response for operation $operation") when (operation) { "withdrawTestkudos" -> { - withdrawInProgress-- - if (withdrawInProgress == 0) { - isBalanceLoading.postValue(false) - } + testWithdrawalInProgress.postValue(false) } "getBalances" -> { - val balanceList = mutableListOf<Amount>(); + val balanceList = mutableListOf<BalanceEntry>(); val result = message.getJSONObject("result") val byCurrency = result.getJSONObject("byCurrency") val currencyList = byCurrency.keys().asSequence().toList().sorted() for (currency in currencyList) { - val jsonAmount = byCurrency.getJSONObject(currency).getJSONObject("available") + val jsonAmount = byCurrency.getJSONObject(currency) + .getJSONObject("available") val amount = Amount.fromJson(jsonAmount) - balanceList.add(amount) + val jsonAmountIncoming = byCurrency.getJSONObject(currency) + .getJSONObject("pendingIncoming") + val amountIncoming = Amount.fromJson(jsonAmountIncoming) + balanceList.add(BalanceEntry(amount, amountIncoming)) } balances.postValue(WalletBalances(true, balanceList)) } + "getWithdrawalInfo" -> { + Log.v(TAG, "got getWithdrawalInfo result") + val status = withdrawStatus.value + if (status !is WithdrawStatus.Loading) { + Log.v(TAG, "ignoring withdrawal info result, not loading.") + return + } + val result = message.getJSONObject("result") + val suggestedExchange = result.getString("suggestedExchange") + val amount = Amount.fromJson(result.getJSONObject("amount")) + withdrawStatus.postValue( + WithdrawStatus.ReceivedDetails( + status.talerWithdrawUri, + amount, + suggestedExchange + ) + ) + } + "acceptWithdrawal" -> { + Log.v(TAG, "got acceptWithdrawal result") + val status = withdrawStatus.value + if (status !is WithdrawStatus.Withdrawing) { + Log.v(TAG, "ignoring acceptWithdrawal result, invalid state") + } + withdrawStatus.postValue(WithdrawStatus.Success()) + } "preparePay" -> { Log.v(TAG, "got preparePay result") val result = message.getJSONObject("result") @@ -238,9 +300,15 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { totalFees = Amount.fromJson(result.getJSONObject("totalFees")) } val res = when (status) { - "payment-possible" -> PayStatus.Prepared(contractTerms!!, proposalId!!, totalFees!!) + "payment-possible" -> PayStatus.Prepared( + contractTerms!!, + proposalId!!, + totalFees!! + ) "paid" -> PayStatus.AlreadyPaid(contractTerms!!) - "insufficient-balance" -> PayStatus.InsufficientBalance(contractTerms!!) + "insufficient-balance" -> PayStatus.InsufficientBalance( + contractTerms!! + ) "error" -> PayStatus.Error("got some error") else -> PayStatus.Error("unkown status") } @@ -257,6 +325,7 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { }) myAkono.evalNodeCode("console.log('hello world from taler wallet-android')") + myAkono.evalNodeCode("require('source-map-support').install();") myAkono.evalNodeCode("tw = require('taler-wallet');") myAkono.evalNodeCode("tw.installAndroidWalletListener();") @@ -296,8 +365,7 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { return } - withdrawInProgress++ - this.isBalanceLoading.value = true + testWithdrawalInProgress.value = true val msg = JSONObject() msg.put("operation", "withdrawTestkudos") @@ -337,7 +405,7 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { sendInitMessage() - isBalanceLoading.value = false + testWithdrawalInProgress.value = false balances.value = WalletBalances(false, listOf()) getBalances() @@ -365,6 +433,32 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { msg.put("args", respJson) myAkono.sendMessage(msg.toString()) + } + + fun getWithdrawalInfo(talerWithdrawUri: String) { + val msg = JSONObject() + msg.put("operation", "getWithdrawalInfo") + + val args = JSONObject() + msg.put("args", args) + args.put("talerWithdrawUri", talerWithdrawUri) + + withdrawStatus.value = WithdrawStatus.Loading(talerWithdrawUri) + + myAkono.sendMessage(msg.toString()) + } + + fun acceptWithdrawal(talerWithdrawUri: String, selectedExchange: String) { + val msg = JSONObject() + msg.put("operation", "acceptWithdrawal") + val args = JSONObject() + msg.put("args", args) + args.put("talerWithdrawUri", talerWithdrawUri) + args.put("selectedExchange", selectedExchange) + + withdrawStatus.value = WithdrawStatus.Withdrawing(talerWithdrawUri) + + myAkono.sendMessage(msg.toString()) } -}
\ No newline at end of file +} diff --git a/app/src/main/java/net/taler/wallet/WithdrawSuccessful.kt b/app/src/main/java/net/taler/wallet/WithdrawSuccessful.kt new file mode 100644 index 0000000..c16dced --- /dev/null +++ b/app/src/main/java/net/taler/wallet/WithdrawSuccessful.kt @@ -0,0 +1,26 @@ +package net.taler.wallet + + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.navigation.findNavController + +/** + * A simple [Fragment] subclass. + */ +class WithdrawSuccessful : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_withdraw_successful, container, false) + view.findViewById<Button>(R.id.button_success_back).setOnClickListener { + activity!!.findNavController(R.id.nav_host_fragment).navigateUp() + } + return view + } +} |