diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-08-22 23:37:54 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-08-22 23:38:08 +0200 |
commit | 2a457a1c46a5da4985035d8bff1db914de161049 (patch) | |
tree | 28a95fa5de18eeb4a8a46d8ee293dac0d50ed505 | |
parent | eb9594079589a0126c3fb5f9a2f6ecd56950eebc (diff) | |
download | merchant-terminal-android-2a457a1c46a5da4985035d8bff1db914de161049.tar.gz merchant-terminal-android-2a457a1c46a5da4985035d8bff1db914de161049.tar.bz2 merchant-terminal-android-2a457a1c46a5da4985035d8bff1db914de161049.zip |
UX improvements / prototype support for NFC tunneling
-rw-r--r-- | .idea/codeStyles/Project.xml | 109 | ||||
-rw-r--r-- | app/build.gradle | 2 | ||||
-rw-r--r-- | app/src/main/AndroidManifest.xml | 7 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/CreatePayment.kt | 65 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/MainActivity.kt | 251 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/PaymentSuccess.kt | 29 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/PosTerminalViewModel.kt | 9 | ||||
-rw-r--r-- | app/src/main/java/net/taler/merchantpos/ProcessPayment.kt | 126 | ||||
-rw-r--r-- | app/src/main/res/layout/fragment_create_payment.xml | 7 | ||||
-rw-r--r-- | app/src/main/res/layout/fragment_payment_success.xml | 38 | ||||
-rw-r--r-- | app/src/main/res/layout/fragment_process_payment.xml | 3 | ||||
-rw-r--r-- | app/src/main/res/navigation/nav_graph.xml | 12 | ||||
-rw-r--r-- | build.gradle | 2 | ||||
-rw-r--r-- | gradle/wrapper/gradle-wrapper.properties | 4 |
14 files changed, 518 insertions, 146 deletions
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 1bec35e..ce889bd 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -3,6 +3,115 @@ <JetCodeStyleSettings> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> </JetCodeStyleSettings> + <codeStyleSettings language="XML"> + <arrangement> + <rules> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:android</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:id</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:name</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>name</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>style</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>ANDROID_ATTRIBUTE_ORDER</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>.*</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + </rules> + </arrangement> + </codeStyleSettings> <codeStyleSettings language="kotlin"> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> </codeStyleSettings> diff --git a/app/build.gradle b/app/build.gradle index 22225ac..0f36545 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { buildToolsVersion "29.0.1" defaultConfig { applicationId "net.taler.merchantpos" - minSdkVersion 28 + minSdkVersion 27 targetSdkVersion 29 versionCode 1 versionName "1.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 52f77ef..0920771 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,11 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="net.taler.merchantpos"> + <uses-permission android:name="android.permission.NFC" /> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-feature android:name="android.hardware.nfc" + android:required="true" /> + <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" @@ -20,7 +25,5 @@ </intent-filter> </activity> </application> - - <uses-permission android:name="android.permission.INTERNET"/> </manifest>
\ No newline at end of file diff --git a/app/src/main/java/net/taler/merchantpos/CreatePayment.kt b/app/src/main/java/net/taler/merchantpos/CreatePayment.kt index 330b9e8..e07802f 100644 --- a/app/src/main/java/net/taler/merchantpos/CreatePayment.kt +++ b/app/src/main/java/net/taler/merchantpos/CreatePayment.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.EditText import androidx.lifecycle.ViewModelProviders import androidx.navigation.fragment.findNavController import com.android.volley.Request @@ -34,19 +35,11 @@ private const val ARG_PARAM2 = "param2" * */ class CreatePayment : Fragment() { - // TODO: Rename and change types of parameters - private var param1: String? = null - private var param2: String? = null - private var listener: OnFragmentInteractionListener? = null private lateinit var queue: RequestQueue private lateinit var model: PosTerminalViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - arguments?.let { - param1 = it.getString(ARG_PARAM1) - param2 = it.getString(ARG_PARAM2) - } model = activity?.run { ViewModelProviders.of(this)[PosTerminalViewModel::class.java] @@ -56,11 +49,14 @@ class CreatePayment : Fragment() { } private fun onRequestPayment() { - val amount = "TESTKUDOS:10.00" + val amountValStr = activity!!.findViewById<EditText>(R.id.edit_payment_amount).text + val amount = "TESTKUDOS:${amountValStr}" model.activeAmount = amount + model.activeSubject = activity!!.findViewById<EditText>(R.id.edit_payment_subject).text + var order = JSONObject().also { it.put("amount", amount) - it.put("summary", "hello world") + it.put("summary", model.activeSubject!!) it.put("fulfillment_url", "https://example.com") it.put("instance", "default") } @@ -119,53 +115,4 @@ class CreatePayment : Fragment() { return view } - override fun onAttach(context: Context) { - super.onAttach(context) - if (context is OnFragmentInteractionListener) { - listener = context - } else { - throw RuntimeException(context.toString() + " must implement OnFragmentInteractionListener") - } - } - - override fun onDetach() { - super.onDetach() - listener = null - } - - /** - * This interface must be implemented by activities that contain this - * fragment to allow an interaction in this fragment to be communicated - * to the activity and potentially other fragments contained in that - * activity. - * - * - * See the Android Training lesson [Communicating with Other Fragments] - * (http://developer.android.com/training/basics/fragments/communicating.html) - * for more information. - */ - interface OnFragmentInteractionListener { - // TODO: Update argument type and name - fun onFragmentInteraction(uri: Uri) - } - - companion object { - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment CreatePayment. - */ - // TODO: Rename and change types and number of parameters - @JvmStatic - fun newInstance(param1: String, param2: String) = - CreatePayment().apply { - arguments = Bundle().apply { - putString(ARG_PARAM1, param1) - putString(ARG_PARAM2, param2) - } - } - } } diff --git a/app/src/main/java/net/taler/merchantpos/MainActivity.kt b/app/src/main/java/net/taler/merchantpos/MainActivity.kt index 4f67f9c..23d0417 100644 --- a/app/src/main/java/net/taler/merchantpos/MainActivity.kt +++ b/app/src/main/java/net/taler/merchantpos/MainActivity.kt @@ -1,7 +1,10 @@ package net.taler.merchantpos -import android.net.Uri +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.nfc.tech.IsoDep import android.os.Bundle +import android.util.Log import androidx.core.view.GravityCompat import android.view.MenuItem import androidx.drawerlayout.widget.DrawerLayout @@ -14,16 +17,243 @@ import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController +import net.taler.merchantpos.Utils.Companion.hexStringToByteArray +import org.json.JSONObject +import java.io.ByteArrayOutputStream +import java.net.URL +import javax.net.ssl.HttpsURLConnection + + +class Utils { + companion object { + private val HEX_CHARS = "0123456789ABCDEF" + fun hexStringToByteArray(data: String): ByteArray { + + val result = ByteArray(data.length / 2) + + for (i in 0 until data.length step 2) { + val firstIndex = HEX_CHARS.indexOf(data[i]); + val secondIndex = HEX_CHARS.indexOf(data[i + 1]); + + val octet = firstIndex.shl(4).or(secondIndex) + result.set(i.shr(1), octet.toByte()) + } + + return result + } + + private val HEX_CHARS_ARRAY = "0123456789ABCDEF".toCharArray() + fun toHex(byteArray: ByteArray): String { + val result = StringBuffer() + + byteArray.forEach { + val octet = it.toInt() + val firstIndex = (octet and 0xF0).ushr(4) + val secondIndex = octet and 0x0F + result.append(HEX_CHARS_ARRAY[firstIndex]) + result.append(HEX_CHARS_ARRAY[secondIndex]) + } + + return result.toString() + } + } +} + +val TALER_AID = "A0000002471001" + + +fun writeApduLength(stream: ByteArrayOutputStream, size: Int) { + when { + size == 0 -> { + // No size field needed! + } + size <= 255 -> // One byte size field + stream.write(size) + size <= 65535 -> { + stream.write(0) + // FIXME: is this supposed to be little or big endian? + stream.write(size and 0xFF) + stream.write((size ushr 8) and 0xFF) + } + else -> throw Error("payload too big") + } +} + +fun apduSelectFile(): ByteArray { + return hexStringToByteArray("00A4040007A0000002471001") +} + + +fun apduPutData(payload: ByteArray): ByteArray { + val stream = ByteArrayOutputStream() + + // Class + stream.write(0x00) + + // Instruction 0xDA = put data + stream.write(0xDA) + + // Instruction parameters + // (proprietary encoding) + stream.write(0x01) + stream.write(0x00) + + writeApduLength(stream, payload.size) + + stream.write(payload) + + return stream.toByteArray() +} + +fun apduPutTalerData(talerInst: Int, payload: ByteArray): ByteArray { + val realPayload = ByteArrayOutputStream() + realPayload.write(talerInst) + realPayload.write(payload) + return apduPutData(realPayload.toByteArray()) +} + +fun apduGetData(): ByteArray { + val stream = ByteArrayOutputStream() + + // Class + stream.write(0x00) + + // Instruction 0xCA = get data + stream.write(0xCA) + + // Instruction parameters + // (proprietary encoding) + stream.write(0x01) + stream.write(0x00) + + // Max expected response size, two + // zero bytes denotes 65536 + stream.write(0x0) + stream.write(0x0) + + return stream.toByteArray() +} + class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, - CreatePayment.OnFragmentInteractionListener, ProcessPayment.OnFragmentInteractionListener { - override fun onFragmentInteraction(uri: Uri) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + NfcAdapter.ReaderCallback { + + companion object { + const val TAG = "taler-merchant" + } + + private lateinit var model: PosTerminalViewModel + private var nfcAdapter: NfcAdapter? = null + + private var currentTag: IsoDep? = null + + override fun onTagDiscovered(tag: Tag?) { + + Log.v(TAG, "tag discovered") + + val isoDep = IsoDep.get(tag) + isoDep.connect() + + currentTag = isoDep + + isoDep.transceive(apduSelectFile()) + + val contractUri: String? = model.activeContractUri + + if (contractUri != null) { + isoDep.transceive(apduPutTalerData(1, contractUri.toByteArray())) + } + + // FIXME: use better pattern for sleeps in between requests + // -> start with fast polling, poll more slowly if no requests are coming + + while (true) { + try { + val reqFrame = isoDep.transceive(apduGetData()) + if (reqFrame.size < 2) { + Log.v(TAG, "request frame too small") + break + } + val req = ByteArray(reqFrame.size - 2) + if (req.isEmpty()) { + continue + } + reqFrame.copyInto(req, 0, 0, reqFrame.size - 2) + val jsonReq = JSONObject(req.toString(Charsets.UTF_8)) + val reqId = jsonReq.getInt("id") + Log.v(TAG, "got request $jsonReq") + val jsonInnerReq = jsonReq.getJSONObject("request") + val method = jsonInnerReq.getString("method") + val urlStr = jsonInnerReq.getString("url") + Log.v(TAG, "url '$urlStr'") + Log.v(TAG, "method '$method'") + val url = URL(urlStr) + val conn: HttpsURLConnection = url.openConnection() as HttpsURLConnection + conn.setRequestProperty("Accept", "application/json") + conn.connectTimeout = 5000 + conn.doInput = true + when (method) { + "get" -> { + conn.requestMethod = "GET" + } + "postJson" -> { + conn.requestMethod = "POST" + conn.doOutput = true + conn.setRequestProperty("Content-Type", "application/json; utf-8") + val body = jsonInnerReq.getString("body") + conn.outputStream.write(body.toByteArray(Charsets.UTF_8)) + } + else -> { + throw Exception("method not supported") + } + } + Log.v(TAG, "connecting") + conn.connect() + Log.v(TAG, "connected") + + val statusCode = conn.responseCode + val tunnelResp = JSONObject() + tunnelResp.put("id", reqId) + tunnelResp.put("status", conn.responseCode) + + if (statusCode == 200) { + val stream = conn.inputStream + val httpResp = stream.buffered().readBytes() + tunnelResp.put("responseJson", JSONObject(httpResp.toString(Charsets.UTF_8))) + } + + Log.v(TAG, "sending: $tunnelResp") + + isoDep.transceive(apduPutTalerData(2, tunnelResp.toString().toByteArray())) + } catch (e: Exception) { + Log.v(TAG, "exception during NFC loop: ${e}") + break + } + } + + isoDep.close() + } + + public override fun onResume() { + super.onResume() + nfcAdapter?.enableReaderMode( + this, this, + NfcAdapter.FLAG_READER_NFC_A or + NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, + null + ) } + public override fun onPause() { + super.onPause() + nfcAdapter?.disableReaderMode(this) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + nfcAdapter = NfcAdapter.getDefaultAdapter(this) + setContentView(R.layout.activity_main) val toolbar: Toolbar = findViewById(R.id.toolbar) setSupportActionBar(toolbar) @@ -35,13 +265,20 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte val navController = findNavController(R.id.nav_host_fragment) val appBarConfiguration = - AppBarConfiguration(setOf(R.id.createPayment, R.id.merchantSettings, R.id.merchantHistory), drawerLayout) + AppBarConfiguration( + setOf( + R.id.createPayment, + R.id.merchantSettings, + R.id.merchantHistory + ), drawerLayout + ) findViewById<Toolbar>(R.id.toolbar) .setupWithNavController(navController, appBarConfiguration) - val model = ViewModelProviders.of(this)[PosTerminalViewModel::class.java] - model.merchantConfig = MerchantConfig("https://backend.test.taler.net", "default", "sandbox") + model = ViewModelProviders.of(this)[PosTerminalViewModel::class.java] + model.merchantConfig = + MerchantConfig("https://backend.test.taler.net", "default", "sandbox") } diff --git a/app/src/main/java/net/taler/merchantpos/PaymentSuccess.kt b/app/src/main/java/net/taler/merchantpos/PaymentSuccess.kt new file mode 100644 index 0000000..20b6ed1 --- /dev/null +++ b/app/src/main/java/net/taler/merchantpos/PaymentSuccess.kt @@ -0,0 +1,29 @@ +package net.taler.merchantpos + + +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 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<Button>(R.id.button_success_back).setOnClickListener { + activity!!.findNavController(R.id.nav_host_fragment).navigateUp() + } + return view + } + + +} diff --git a/app/src/main/java/net/taler/merchantpos/PosTerminalViewModel.kt b/app/src/main/java/net/taler/merchantpos/PosTerminalViewModel.kt index 1e174ef..bcbc0c8 100644 --- a/app/src/main/java/net/taler/merchantpos/PosTerminalViewModel.kt +++ b/app/src/main/java/net/taler/merchantpos/PosTerminalViewModel.kt @@ -1,15 +1,18 @@ package net.taler.merchantpos +import android.text.Editable import androidx.lifecycle.ViewModel class PosTerminalViewModel : ViewModel() { + var activeSubject: Editable? = null var merchantConfig: MerchantConfig? = null var activeOrderId: String? = null var activeAmount: String? = null var activeContractUri: String? = null fun activeAmountPretty(): String? { - val a = activeAmount ?: return null - return a.replace(":", " ") + val amount = activeAmount ?: return null + val components = amount.split(":") + return "${components[1]} ${components[0]}" } -}
\ No newline at end of file +} diff --git a/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt b/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt index c98a3e4..26ef1c7 100644 --- a/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt +++ b/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt @@ -14,6 +14,8 @@ import com.google.zxing.common.BitMatrix import com.google.zxing.qrcode.QRCodeWriter import android.opengl.ETC1.getWidth import android.opengl.ETC1.getHeight +import android.os.Handler +import android.util.Log import android.widget.Button import android.widget.ImageView import android.widget.TextView @@ -22,7 +24,14 @@ import androidx.activity.addCallback import androidx.lifecycle.ViewModelProviders import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController +import com.android.volley.Request +import com.android.volley.RequestQueue +import com.android.volley.Response +import com.android.volley.VolleyError +import com.android.volley.toolbox.Volley import com.google.android.material.snackbar.Snackbar +import org.json.JSONObject +import java.net.URLEncoder // TODO: Rename parameter arguments, choose names that match @@ -32,32 +41,68 @@ private const val ARG_PARAM2 = "param2" /** * A simple [Fragment] subclass. - * Activities that contain this fragment must implement the - * [ProcessPayment.OnFragmentInteractionListener] interface - * to handle interaction events. - * Use the [ProcessPayment.newInstance] factory method to - * create an instance of this fragment. * */ class ProcessPayment : Fragment() { - // TODO: Rename and change types of parameters - private var param1: String? = null - private var param2: String? = null - private var listener: OnFragmentInteractionListener? = null + private var paused: Boolean = true + private lateinit var queue: RequestQueue private lateinit var model: PosTerminalViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - arguments?.let { - param1 = it.getString(ARG_PARAM1) - param2 = it.getString(ARG_PARAM2) - } model = activity?.run { ViewModelProviders.of(this)[PosTerminalViewModel::class.java] } ?: throw Exception("Invalid Activity") + queue = Volley.newRequestQueue(context) + + } + + private fun onCheckPayment(checkPaymentResponse: JSONObject) { + if (paused) { + return + } + //Log.v("taler-merchant", "got check payment result ${checkPaymentResponse}") + if (checkPaymentResponse.getBoolean("paid")) { + queue.cancelAll { true } + findNavController().navigate(R.id.action_processPayment_to_paymentSuccess) + return + } + } + + private fun onNetworkError(volleyError: VolleyError?) { + val mySnackbar = Snackbar.make(view!!, "Network Error", Snackbar.LENGTH_SHORT) + mySnackbar.show() + } + + private fun checkPaid() { + if (paused) { + return + } + //Log.v("taler-merchant", "checkig if payment happened") + val params = mapOf("order_id" to model.activeOrderId!!, "instance" to model.merchantConfig!!.instance) + var req = MerchantInternalRequest(Request.Method.GET, model.merchantConfig!!, "check-payment", params, null, + Response.Listener { onCheckPayment(it) }, Response.ErrorListener { onNetworkError(it) }) + queue.add(req) + val handler = Handler() + handler.postDelayed({ + checkPaid() + }, 500) + + } + + override fun onResume() { + this.paused = false + checkPaid() + super.onResume() + } + + override fun onPause() { + this.paused = true + super.onPause() + queue.cancelAll { true } } override fun onCreateView( @@ -67,7 +112,8 @@ class ProcessPayment : Fragment() { // Inflate the layout for this fragment val view = inflater.inflate(R.layout.fragment_process_payment, container, false) val img = view.findViewById<ImageView>(R.id.qrcode) - val myBitmap = makeQrCode(model.activeContractUri!!) + val talerPayUrl = "talerpay:" + URLEncoder.encode(model.activeContractUri!!, "utf-8") + val myBitmap = makeQrCode(talerPayUrl) img.setImageBitmap(myBitmap) val cancelPaymentButton = view.findViewById<Button>(R.id.button_cancel_payment) cancelPaymentButton.setOnClickListener { @@ -99,56 +145,6 @@ class ProcessPayment : Fragment() { bmp.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) } } - return bmp; - } - - override fun onAttach(context: Context) { - super.onAttach(context) - if (context is OnFragmentInteractionListener) { - listener = context - } else { - throw RuntimeException(context.toString() + " must implement OnFragmentInteractionListener") - } - } - - override fun onDetach() { - super.onDetach() - listener = null - } - - /** - * This interface must be implemented by activities that contain this - * fragment to allow an interaction in this fragment to be communicated - * to the activity and potentially other fragments contained in that - * activity. - * - * - * See the Android Training lesson [Communicating with Other Fragments] - * (http://developer.android.com/training/basics/fragments/communicating.html) - * for more information. - */ - interface OnFragmentInteractionListener { - // TODO: Update argument type and name - fun onFragmentInteraction(uri: Uri) - } - - companion object { - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment ProcessPayment. - */ - // TODO: Rename and change types and number of parameters - @JvmStatic - fun newInstance(param1: String, param2: String) = - ProcessPayment().apply { - arguments = Bundle().apply { - putString(ARG_PARAM1, param1) - putString(ARG_PARAM2, param2) - } - } + return bmp } } diff --git a/app/src/main/res/layout/fragment_create_payment.xml b/app/src/main/res/layout/fragment_create_payment.xml index 2549e91..f72f263 100644 --- a/app/src/main/res/layout/fragment_create_payment.xml +++ b/app/src/main/res/layout/fragment_create_payment.xml @@ -28,10 +28,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textPersonName" - android:text="Payment Subject" + android:text="Demo Payment" android:ems="10" android:layout_gravity="top" - android:id="@+id/editText5"/> + android:id="@+id/edit_payment_subject"/> <TextView android:text="Amount (TESTKUDOS)" android:layout_width="match_parent" @@ -42,7 +42,8 @@ android:layout_height="wrap_content" android:inputType="numberDecimal" android:ems="10" - android:id="@+id/editText6" android:layout_weight="0" android:text="10"/> + android:id="@+id/edit_payment_amount" android:layout_weight="0" + android:text="1"/> <Space android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/app/src/main/res/layout/fragment_payment_success.xml b/app/src/main/res/layout/fragment_payment_success.xml new file mode 100644 index 0000000..8db64df --- /dev/null +++ b/app/src/main/res/layout/fragment_payment_success.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="15dp" + tools:context=".PaymentSuccessful"> + + + <LinearLayout + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <Space android:layout_width="match_parent" android:layout_height="0dp" + android:layout_weight="1"/> + + <TextView + android:layout_gravity="center" + android:layout_width="match_parent" + android:textAlignment="center" + android:layout_height="50dp" + android:text="Payment Received" + android:autoSizeTextType="uniform" + android:textColor="@android:color/holo_green_dark"/> + + + <Space android:layout_width="match_parent" android:layout_height="0dp" + android:layout_weight="1"/> + <Button + android:text="Go Back" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/button_success_back"/> + + </LinearLayout> + +</FrameLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_process_payment.xml b/app/src/main/res/layout/fragment_process_payment.xml index 0ff8257..3f1764e 100644 --- a/app/src/main/res/layout/fragment_process_payment.xml +++ b/app/src/main/res/layout/fragment_process_payment.xml @@ -23,8 +23,7 @@ android:layout_height="275dp" tools:src="@tools:sample/backgrounds/scenic[16]" android:id="@+id/qrcode" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="32dp" - android:background="#CEC02424"/> + app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="32dp" /> <TextView android:text="Please scan QR Code or use NFC to pay" android:layout_width="wrap_content" diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index b6345a1..3a5b470 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -9,7 +9,12 @@ <action android:id="@+id/action_createPayment_to_processPayment" app:destination="@id/processPayment"/> </fragment> <fragment android:id="@+id/processPayment" android:name="net.taler.merchantpos.ProcessPayment" - android:label="Payment Prompt" tools:layout="@layout/fragment_process_payment"/> + android:label="Payment Prompt" tools:layout="@layout/fragment_process_payment"> + <action + android:id="@+id/action_processPayment_to_paymentSuccess" + app:destination="@id/paymentSuccess" + app:popUpTo="@id/createPayment"/> + </fragment> <fragment android:id="@+id/merchantHistory" android:name="net.taler.merchantpos.MerchantHistory" android:label="Payment History" tools:layout="@layout/fragment_merchant_history"/> <action android:id="@+id/action_global_merchantHistory" app:destination="@id/merchantHistory"/> @@ -17,4 +22,9 @@ <fragment android:id="@+id/merchantSettings" android:name="net.taler.merchantpos.MerchantSettings" android:label="Merchant Settings" tools:layout="@layout/fragment_merchant_settings"/> <action android:id="@+id/action_global_merchantSettings" app:destination="@id/merchantSettings"/> + <fragment + android:id="@+id/paymentSuccess" + android:name="net.taler.merchantpos.PaymentSuccess" + android:label="Payment Received" + tools:layout="@layout/fragment_payment_success" /> </navigation>
\ No newline at end of file diff --git a/build.gradle b/build.gradle index 438d280..4420c59 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:3.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a20e04a..344e98a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Aug 10 13:25:15 CEST 2019 +#Tue Aug 20 23:27:29 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip |