From 163f1311116d4019a144d1cd98270a4e2fe1fd77 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 3 Feb 2020 12:27:34 -0300 Subject: Require valid configuration before showing UI --- .../java/net/taler/merchantpos/MainActivity.kt | 22 ++- .../java/net/taler/merchantpos/MainViewModel.kt | 2 +- .../java/net/taler/merchantpos/MerchantHistory.kt | 210 ++++++++++----------- .../merchantpos/config/ConfigFetcherFragment.kt | 46 +++++ .../net/taler/merchantpos/config/MerchantConfig.kt | 4 +- .../merchantpos/config/MerchantConfigFragment.kt | 6 +- .../net/taler/merchantpos/order/OrderFragment.kt | 10 +- .../taler/merchantpos/payment/PaymentManager.kt | 9 +- .../taler/merchantpos/payment/PaymentSuccess.kt | 30 --- .../merchantpos/payment/PaymentSuccessFragment.kt | 27 +++ .../merchantpos/payment/ProcessPaymentFragment.kt | 1 + app/src/main/res/layout/activity_main.xml | 1 + app/src/main/res/layout/content_main.xml | 4 +- .../main/res/layout/fragment_config_fetcher.xml | 29 +++ app/src/main/res/layout/fragment_order.xml | 1 + app/src/main/res/navigation/nav_graph.xml | 66 +++++-- app/src/main/res/values/strings.xml | 1 + 17 files changed, 297 insertions(+), 172 deletions(-) create mode 100644 app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt delete mode 100644 app/src/main/java/net/taler/merchantpos/payment/PaymentSuccess.kt create mode 100644 app/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt create mode 100644 app/src/main/res/layout/fragment_config_fetcher.xml diff --git a/app/src/main/java/net/taler/merchantpos/MainActivity.kt b/app/src/main/java/net/taler/merchantpos/MainActivity.kt index 9841339..e26146d 100644 --- a/app/src/main/java/net/taler/merchantpos/MainActivity.kt +++ b/app/src/main/java/net/taler/merchantpos/MainActivity.kt @@ -10,7 +10,7 @@ import androidx.core.view.GravityCompat.START import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.Observer import androidx.navigation.NavController -import androidx.navigation.findNavController +import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController import com.google.android.material.navigation.NavigationView @@ -22,6 +22,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { private val nfcManager = NfcManager() private var nfcAdapter: NfcAdapter? = null + private lateinit var nav: NavController private lateinit var drawerLayout: DrawerLayout override fun onCreate(savedInstanceState: Bundle?) { @@ -40,10 +41,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { drawerLayout = findViewById(R.id.drawer_layout) val navView: NavigationView = findViewById(R.id.nav_view) - navView.setNavigationItemSelectedListener(this) - - val navController = findNavController(R.id.nav_host_fragment) val appBarConfiguration = AppBarConfiguration( setOf( R.id.order, @@ -51,7 +49,20 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { R.id.merchantHistory ), drawerLayout ) - toolbar.setupWithNavController(navController, appBarConfiguration) + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + nav = navHostFragment.navController + toolbar.setupWithNavController(nav, appBarConfiguration) + } + + override fun onStart() { + super.onStart() + if (!model.configManager.config.isValid()) { + nav.navigate(R.id.action_global_merchantSettings) + } else if (model.configManager.merchantConfig == null) { + nav.navigate(R.id.action_global_configFetcher) + } } public override fun onResume() { @@ -67,7 +78,6 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { override fun onNavigationItemSelected(item: MenuItem): Boolean { // Handle navigation view item clicks here. - val nav: NavController = findNavController(R.id.nav_host_fragment) when (item.itemId) { R.id.nav_order -> nav.navigate(R.id.action_global_order) R.id.nav_history -> nav.navigate(R.id.action_global_merchantHistory) diff --git a/app/src/main/java/net/taler/merchantpos/MainViewModel.kt b/app/src/main/java/net/taler/merchantpos/MainViewModel.kt index c07c996..c14ab66 100644 --- a/app/src/main/java/net/taler/merchantpos/MainViewModel.kt +++ b/app/src/main/java/net/taler/merchantpos/MainViewModel.kt @@ -29,7 +29,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { get() = configManager.merchantConfig init { - if (configManager.merchantConfig == null) { + if (configManager.merchantConfig == null && configManager.config.isValid()) { configManager.fetchConfig(configManager.config, false) } } diff --git a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt index 167bb9d..eb8288c 100644 --- a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt +++ b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt @@ -1,6 +1,5 @@ package net.taler.merchantpos - import android.annotation.SuppressLint import android.os.Bundle import android.util.Log @@ -11,85 +10,42 @@ import android.widget.TextView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.MutableLiveData +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import com.android.volley.Request +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.android.volley.Request.Method.GET import com.android.volley.RequestQueue -import com.android.volley.Response -import com.android.volley.VolleyError +import com.android.volley.Response.ErrorListener +import com.android.volley.Response.Listener import com.android.volley.toolbox.Volley import com.google.android.material.snackbar.Snackbar +import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT +import kotlinx.android.synthetic.main.fragment_merchant_history.* +import net.taler.merchantpos.HistoryItemAdapter.HistoryItemViewHolder import net.taler.merchantpos.config.MerchantRequest import org.json.JSONObject import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle +import java.time.format.FormatStyle.SHORT import java.util.* - - -data class HistoryItem( - val orderId: String, - val amount: Amount, - val summary: String, - val timestamp: Instant -) - -class MyAdapter(private var myDataset: List) : - RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { - val view = - LayoutInflater.from(parent.context).inflate(R.layout.history_row, parent, false) - return MyViewHolder(view) - } - - override fun getItemCount(): Int { - return myDataset.size - } - - override fun onBindViewHolder(holder: MyViewHolder, position: Int) { - val item = myDataset[position] - val summaryTextView = holder.rowView.findViewById(R.id.text_history_summary) - summaryTextView.text = myDataset[position].summary - - val amount = myDataset[position].amount - val amountTextView = holder.rowView.findViewById(R.id.text_history_amount) - @SuppressLint("SetTextI18n") - amountTextView.text = "${amount.amount} ${amount.currency}" - - val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) - .withLocale(Locale.UK) - .withZone(ZoneId.systemDefault()) - val timestampTextView = holder.rowView.findViewById(R.id.text_history_time) - timestampTextView.text = formatter.format(item.timestamp) - - val orderIdTextView = holder.rowView.findViewById(R.id.text_history_order_id) - orderIdTextView.text = item.orderId - } - - fun setData(dataset: List) { - this.myDataset = dataset - this.notifyDataSetChanged() - } - - class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView) -} - -fun parseTalerTimestamp(s: String): Instant { - return Instant.ofEpochSecond(s.substringAfterLast('(').substringBeforeLast(')').toLong()) -} - /** * Fragment to display the merchant's payment history, * received from the backend. */ class MerchantHistory : Fragment() { + + companion object { + const val TAG = "taler-merchant" + } + private lateinit var queue: RequestQueue private val model: MainViewModel by activityViewModels() - private val historyListAdapter = MyAdapter(listOf()) + private val historyListAdapter = HistoryItemAdapter(listOf()) private val isLoading = MutableLiveData().apply { value = false } @@ -99,10 +55,49 @@ class MerchantHistory : Fragment() { queue = Volley.newRequestQueue(context) } - private fun onNetworkError(volleyError: VolleyError?) { - this.isLoading.value = false - val mySnackbar = Snackbar.make(view!!, "Network Error", Snackbar.LENGTH_SHORT) - mySnackbar.show() + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_merchant_history, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + list_history.apply { + layoutManager = LinearLayoutManager(requireContext()) + addItemDecoration(DividerItemDecoration(context, VERTICAL)) + adapter = historyListAdapter + } + + swiperefresh.isRefreshing = false + swiperefresh.setOnRefreshListener { + Log.v(TAG, "refreshing!") + fetchHistory() + } + + this.isLoading.observe(viewLifecycleOwner, androidx.lifecycle.Observer { loading -> + Log.v(TAG, "setting refreshing to $loading") + swiperefresh.isRefreshing = loading + }) + } + + override fun onStart() { + super.onStart() + if (model.configManager.merchantConfig?.instance == null) { + findNavController().navigate(R.id.action_global_merchantSettings) + } else { + fetchHistory() + } + } + + private fun fetchHistory() { + isLoading.value = true + val merchantConfig = model.configManager.merchantConfig!! + val params = mapOf("instance" to merchantConfig.instance) + val req = MerchantRequest(GET, merchantConfig, "history", params, null, + Listener { onHistoryResponse(it) }, + ErrorListener { onNetworkError() }) + queue.add(req) } private fun onHistoryResponse(body: JSONObject) { @@ -123,55 +118,56 @@ class MerchantHistory : Fragment() { historyListAdapter.setData(data) } - private fun fetchHistory() { - isLoading.value = true - val instance = model.merchantConfig!!.instance - val req = MerchantRequest( - Request.Method.GET, - model.merchantConfig!!, - "history", - mapOf("instance" to instance), - null, - Response.Listener { onHistoryResponse(it) }, - Response.ErrorListener { onNetworkError(it) }) - queue.add(req) + private fun onNetworkError() { + this.isLoading.value = false + Snackbar.make(view!!, "Network Error", LENGTH_SHORT).show() } - override fun onResume() { - super.onResume() - fetchHistory() - } +} - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val myLayoutManager = LinearLayoutManager(this@MerchantHistory.context) - val myItemDecoration = DividerItemDecoration(context, myLayoutManager.orientation) - // Inflate the layout for this fragment - val view = inflater.inflate(R.layout.fragment_merchant_history, container, false) - view.findViewById(R.id.list_history).apply { - layoutManager = myLayoutManager - adapter = historyListAdapter - addItemDecoration(myItemDecoration) - } +data class HistoryItem( + val orderId: String, + val amount: Amount, + val summary: String, + val timestamp: Instant +) - val refreshLayout = view.findViewById(R.id.swiperefresh) - refreshLayout.isRefreshing = false - refreshLayout.setOnRefreshListener { - Log.v(TAG, "refreshing!") - fetchHistory() - } +class HistoryItemAdapter(private var items: List) : Adapter() { - this.isLoading.observe(viewLifecycleOwner, androidx.lifecycle.Observer { loading -> - Log.v(TAG, "setting refreshing to $loading") - refreshLayout.isRefreshing = loading - }) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryItemViewHolder { + val v = LayoutInflater.from(parent.context).inflate(R.layout.history_row, parent, false) + return HistoryItemViewHolder(v) + } - return view + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: HistoryItemViewHolder, position: Int) { + holder.bind(items[position]) } - companion object { - const val TAG = "taler-merchant" + fun setData(items: List) { + this.items = items + this.notifyDataSetChanged() } + + class HistoryItemViewHolder(v: View) : ViewHolder(v) { + + private val summaryTextView: TextView = v.findViewById(R.id.text_history_summary) + private val amountTextView: TextView = v.findViewById(R.id.text_history_amount) + private val timestampTextView: TextView = v.findViewById(R.id.text_history_time) + private val orderIdTextView: TextView = v.findViewById(R.id.text_history_order_id) + private val formatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(SHORT) + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()) + + fun bind(item: HistoryItem) { + summaryTextView.text = item.summary + val amount = item.amount + @SuppressLint("SetTextI18n") + amountTextView.text = "${amount.amount} ${amount.currency}" + timestampTextView.text = formatter.format(item.timestamp) + orderIdTextView.text = item.orderId + } + } + } diff --git a/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt b/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt new file mode 100644 index 0000000..4d387da --- /dev/null +++ b/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt @@ -0,0 +1,46 @@ +package net.taler.merchantpos.config + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT +import net.taler.merchantpos.MainViewModel +import net.taler.merchantpos.R + +class ConfigFetcherFragment : Fragment() { + + private val model: MainViewModel by activityViewModels() + private val configManager by lazy { model.configManager } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_config_fetcher, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + configManager.configUpdateResult.observe(viewLifecycleOwner, Observer { result -> + when { + result == null -> return@Observer + result.error -> onNetworkError(result.authError) + else -> findNavController().navigate(R.id.order) + } + }) + } + + private fun onNetworkError(authError: Boolean) { + val res = if (authError) R.string.config_auth_error else R.string.config_error + Snackbar.make(view!!, res, LENGTH_SHORT).show() + findNavController().navigate(R.id.action_configFetcher_to_merchantSettings) + } + +} diff --git a/app/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt b/app/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt index 63dd487..c237a24 100644 --- a/app/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt +++ b/app/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt @@ -7,7 +7,9 @@ data class Config( val configUrl: String, val username: String, val password: String -) +) { + fun isValid() = !configUrl.isBlank() +} data class MerchantConfig( @JsonProperty("base_url") diff --git a/app/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt b/app/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt index b824d38..27e22ad 100644 --- a/app/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt +++ b/app/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt @@ -8,6 +8,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT import kotlinx.android.synthetic.main.fragment_merchant_settings.* @@ -51,10 +52,6 @@ class MerchantConfigFragment : Fragment() { configManager.configUpdateResult.removeObservers(viewLifecycleOwner) }) } - } - - override fun onStart() { - super.onStart() updateView() } @@ -85,6 +82,7 @@ class MerchantConfigFragment : Fragment() { onResultReceived() updateView() Snackbar.make(view!!, "Changed to new $currency merchant", LENGTH_SHORT).show() + findNavController().navigate(R.id.order) } private fun onNetworkError(authError: Boolean) { diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt index f4c5a15..99f6c57 100644 --- a/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt +++ b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt @@ -35,9 +35,11 @@ class OrderFragment : Fragment() { if (state == UNDO) { restartButton.setText(R.string.order_undo) restartButton.isEnabled = true + completeButton.isEnabled = false } else { restartButton.setText(R.string.order_restart) restartButton.isEnabled = state == ENABLED + completeButton.isEnabled = state == ENABLED } }) } @@ -45,13 +47,13 @@ class OrderFragment : Fragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) val nav: NavController = findNavController(requireActivity(), R.id.nav_host_fragment) - reconfigureButton.setOnClickListener { nav.navigate(R.id.action_global_merchantSettings) } - historyButton.setOnClickListener { nav.navigate(R.id.action_global_merchantHistory) } - logoutButton.setOnClickListener { nav.navigate(R.id.action_global_merchantSettings) } + reconfigureButton.setOnClickListener { nav.navigate(R.id.action_order_to_merchantSettings) } + historyButton.setOnClickListener { nav.navigate(R.id.action_order_to_merchantHistory) } + logoutButton.setOnClickListener { nav.navigate(R.id.action_order_to_merchantSettings) } completeButton.setOnClickListener { val order = orderManager.order.value ?: return@setOnClickListener paymentManager.createPayment(order) - nav.navigate(R.id.action_createPayment_to_processPayment) + nav.navigate(R.id.action_order_to_processPayment) } } diff --git a/app/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt b/app/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt index bb030e2..3e6ee2c 100644 --- a/app/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt +++ b/app/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt @@ -32,7 +32,7 @@ class PaymentManager( ) { private val mPayment = MutableLiveData() - var payment: LiveData = mPayment + val payment: LiveData = mPayment private val checkTimer = object : CountDownTimer(TIMEOUT, CHECK_INTERVAL) { override fun onTick(millisUntilFinished: Long) { @@ -111,12 +111,13 @@ class PaymentManager( * Called when the /check-payment response gave a result. */ private fun onPaymentChecked(checkPaymentResponse: JSONObject) { + val currentValue = requireNotNull(mPayment.value) if (checkPaymentResponse.getBoolean("paid")) { - mPayment.value = mPayment.value!!.copy(paid = true) + mPayment.value = currentValue.copy(paid = true) checkTimer.cancel() - } else { + } else if (currentValue.talerPayUri == null) { val talerPayUri = checkPaymentResponse.getString("taler_pay_uri") - mPayment.value = mPayment.value!!.copy(talerPayUri = talerPayUri) + mPayment.value = currentValue.copy(talerPayUri = talerPayUri) } } diff --git a/app/src/main/java/net/taler/merchantpos/payment/PaymentSuccess.kt b/app/src/main/java/net/taler/merchantpos/payment/PaymentSuccess.kt deleted file mode 100644 index c62abbc..0000000 --- a/app/src/main/java/net/taler/merchantpos/payment/PaymentSuccess.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.taler.merchantpos.payment - - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import androidx.fragment.app.Fragment -import androidx.navigation.findNavController -import net.taler.merchantpos.R - -/** - * A simple [Fragment] subclass. - */ -class PaymentSuccess : Fragment() { - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val view = inflater.inflate(R.layout.fragment_payment_success, container, false) - view.findViewById