diff --git a/.gitignore b/.gitignore
index 520a8635..4b97ba5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,11 +34,10 @@ captures/
# Intellij
*.iml
-.idea/workspace.xml
-.idea/tasks.xml
-.idea/gradle.xml
-.idea/dictionaries
-.idea/libraries
+.idea/
+misc.xml
+deploymentTargetDropDown.xml
+render.experimental.xml
# Keystore files
*.jks
@@ -53,3 +52,6 @@ google-services.json
freeline.py
freeline/
freeline_project_description.json
+
+# Mac files
+.DS_Store
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index fb7f4a8a..00000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
deleted file mode 100644
index 247f71d9..00000000
--- a/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index ba9cfe8c..00000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f..00000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/_layouts/default.html b/_layouts/default.html
deleted file mode 100644
index 6168d589..00000000
--- a/_layouts/default.html
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-{% seo %}
-
-
-
-
-
-
-
-
-
- {{ content }}
-
-
-
-
- {% if site.google_analytics %}
-
- {% endif %}
-
-
diff --git a/app/.gitignore b/app/.gitignore
deleted file mode 100644
index 796b96d1..00000000
--- a/app/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 37ec47d5..00000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,59 +0,0 @@
-apply plugin: 'com.android.application'
-
-apply plugin: 'kotlin-android'
-
-apply plugin: 'kotlin-android-extensions'
-
-apply plugin: "org.jlleitschuh.gradle.ktlint"
-
-android {
- compileSdkVersion 30
- defaultConfig {
- applicationId "com.fattmerchant.fmsampleclient"
- minSdkVersion 23
- targetSdkVersion 30
- versionCode 2
- versionName "1.0.1"
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- }
-
- packagingOptions {
- exclude 'META-INF/*.kotlin_module'
- }
-
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
-
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
-}
-
-dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-// implementation 'androidx.core:core-ktx:1.0.2'
- implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
- implementation "androidx.activity:activity-ktx:1.3.0-alpha07"
- implementation 'androidx.fragment:fragment-ktx:1.3.3'
- implementation 'com.google.android.material:material:1.3.0'
- testImplementation 'junit:junit:4.13'
-// androidTestImplementation 'androidx.test:runner:1.2.0'
-// androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
-
-// implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.2'
-// implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2'
-
- implementation project(":cardpresent")
-}
-
-
-ktlint {
- android.set(true)
- outputColorName.set("RED")
-}
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 00000000..2885395e
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,88 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+val staxApiKey: String = gradleLocalProperties(rootDir).getProperty("staxApiKey") ?: "NoApiKey"
+
+android {
+ namespace = "com.staxpayments"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.staxpayments.sample"
+ minSdk = 23
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0.0"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ packaging {
+ resources {
+ excludes += "META-INF/*.kotlin_module"
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ buildTypes {
+ release {
+ buildConfigField("String", "STAX_API_KEY", "\"$staxApiKey\"")
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ debug {
+ isDefault = true
+ isDebuggable = true
+ buildConfigField("String", "STAX_API_KEY", "\"$staxApiKey\"")
+ proguardFiles(
+ getDefaultProguardFile("proguard-android.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+}
+
+dependencies {
+ // Stax SDK
+ implementation(project(":cardpresent"))
+
+ // Dependencies
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
+ implementation("androidx.activity:activity-compose:1.7.2")
+
+ // Jetpack Compose
+ implementation(platform("androidx.compose:compose-bom:2023.08.00"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+
+ // Google Accompanist
+ implementation("com.google.accompanist:accompanist-permissions:0.33.0-alpha")
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index f1b42451..2f9dc5a4 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/app/src/androidTest/java/com/fattmerchant/fmsampleclient/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/fattmerchant/fmsampleclient/ExampleInstrumentedTest.kt
deleted file mode 100644
index bfe59a29..00000000
--- a/app/src/androidTest/java/com/fattmerchant/fmsampleclient/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.fattmerchant.fmsampleclient
-
-import android.support.test.InstrumentationRegistry
-import android.support.test.runner.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getTargetContext()
- assertEquals("com.fattmerchant.fmsampleclient", appContext.packageName)
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 12a025e3..23e5a9e0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,40 +1,38 @@
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
+
+
-
+ android:theme="@style/Theme.AppCompat.NoActionBar">
+
@@ -42,8 +40,9 @@
-
-
+
\ No newline at end of file
diff --git a/app/src/main/java/com/fattmerchant/fmsampleclient/ChipDnaHelper.kt b/app/src/main/java/com/fattmerchant/fmsampleclient/ChipDnaHelper.kt
deleted file mode 100644
index 1e5329e9..00000000
--- a/app/src/main/java/com/fattmerchant/fmsampleclient/ChipDnaHelper.kt
+++ /dev/null
@@ -1,316 +0,0 @@
-package com.fattmerchant.fmsampleclient
-
-import android.content.Context
-import android.os.AsyncTask
-import android.util.Log
-import com.creditcall.chipdnamobile.ChipDnaMobile
-import com.creditcall.chipdnamobile.ChipDnaMobileSerializer
-import com.creditcall.chipdnamobile.IAvailablePinPadsListener
-import com.creditcall.chipdnamobile.IConnectAndConfigureFinishedListener
-import com.creditcall.chipdnamobile.IDeferredAuthorizationListener
-import com.creditcall.chipdnamobile.IForceAcceptanceListener
-import com.creditcall.chipdnamobile.IPartialApprovalListener
-import com.creditcall.chipdnamobile.IProcessReceiptFinishedListener
-import com.creditcall.chipdnamobile.ISignatureVerificationListener
-import com.creditcall.chipdnamobile.ITransactionFinishedListener
-import com.creditcall.chipdnamobile.ITransactionUpdateListener
-import com.creditcall.chipdnamobile.IVerifyIdListener
-import com.creditcall.chipdnamobile.IVoiceReferralListener
-import com.creditcall.chipdnamobile.ParameterKeys
-import com.creditcall.chipdnamobile.ParameterValues
-import com.creditcall.chipdnamobile.Parameters
-import org.xmlpull.v1.XmlPullParserException
-import java.io.IOException
-import java.lang.ref.WeakReference
-import java.text.SimpleDateFormat
-import java.util.Date
-
-class ChipDnaHelper(
- var apiKey: String? = "v8EknC7d3rhgyvDWrSrU6QM2PBT573K2",
- var environment: String? = ParameterValues.LiveEnvironment,
- var appId: String? = "FMDEMO"
-) : IConnectAndConfigureFinishedListener, IAvailablePinPadsListener {
-
- data class SelectablePinPad(var name: String, var connectionType: String)
-
- companion object {
-
- /** Authenticates with ChipDNA */
- class ChipDnaAuthTask(var context: WeakReference? = null) : AsyncTask() {
-
- interface ChipDnaAuthTaskResponse {
- fun processFinish(parameters: Parameters?)
- }
-
- var delegate: WeakReference? = null
-
- override fun doInBackground(vararg params: String): Parameters? {
- return context?.get()?.let { context ->
- val parameters = Parameters()
- parameters.add(ParameterKeys.Password, params.first())
- return ChipDnaMobile.initialize(context, parameters)
- }
- }
-
- override fun onPostExecute(response: Parameters?) {
- delegate?.get()?.processFinish(response)
- }
- }
-
- /** Deserializes available pin pads from a [Parameters] object */
- class DeserializePinPadTask(var context: WeakReference? = null) :
- AsyncTask>() {
- interface DeserializePinPadTaskResponse {
- fun processFinish(availableReaders: List?)
- }
-
- var delegate: WeakReference? = null
-
- override fun doInBackground(vararg params: String): List {
- val availablePinPadsList = ArrayList()
- try {
- val availablePinPadsHashMap = ChipDnaMobileSerializer.deserializeAvailablePinPads(params[0])
-
- for (connectionType in availablePinPadsHashMap.keys) {
- for (pinpad in availablePinPadsHashMap[connectionType]!!) {
- availablePinPadsList.add(SelectablePinPad(pinpad, connectionType))
- }
- }
- } catch (e: XmlPullParserException) {
- e.printStackTrace()
- } catch (e: IOException) {
- e.printStackTrace()
- }
-
- return availablePinPadsList
- }
-
- override fun onPostExecute(availablePinPadsList: List?) {
- delegate?.get()?.processFinish(availablePinPadsList)
- }
- }
-
- class ConnectPinPadTask(var context: WeakReference? = null) :
- AsyncTask() {
- interface ConnectPinPadTaskResponse {
- fun processFinish(parameters: Parameters)
- }
-
- var delegate: WeakReference? = null
-
- override fun doInBackground(vararg params: SelectablePinPad?): Parameters {
- val pinpad = params.first()!!
- var requestParams = Parameters()
- requestParams.add(ParameterKeys.PinPadName, pinpad.name)
- requestParams.add(ParameterKeys.PinPadConnectionType, pinpad.connectionType)
- return ChipDnaMobile.getInstance().setProperties(requestParams)
- }
-
- override fun onPostExecute(result: Parameters) {
- Log.d("connectpintask", result.toString())
- delegate?.get()?.processFinish(result)
- }
- }
- }
-
- fun submitTransaction() {
- val params = Parameters()
- params.add(ParameterKeys.Amount, "01")
- params.add(ParameterKeys.AmountType, ParameterValues.AmountTypeActual)
- params.add(ParameterKeys.Currency, "USD")
-
- val userRef = String.format("CDM-%s", SimpleDateFormat("yy-MM-dd-HH.mm.ss").format(Date()))
- params.add(ParameterKeys.UserReference, userRef)
- params.add(ParameterKeys.TransactionType, ParameterValues.Sale)
- params.add(ParameterKeys.PaymentMethod, ParameterValues.Card)
-
- doAuthorizeTransaction(params)
- }
-
- fun doAuthorizeTransaction(params: Parameters) {
- Log.d("doAuthorizeTran", params.toString())
-
- var p = Parameters()
-
- val response = ChipDnaMobile.getInstance().startTransaction(params)
- if (response.containsKey(ParameterKeys.Result) && response.getValue(ParameterKeys.Result).equals(ParameterValues.FALSE)) {
- }
- }
-
- fun searchForReaders() {
- val parameters = Parameters().apply {
- add(ParameterKeys.SearchConnectionTypeBluetooth, ParameterValues.TRUE)
- }
- ChipDnaMobile.getInstance().clearAllAvailablePinPadsListeners()
- ChipDnaMobile.getInstance().addAvailablePinPadsListener(this)
- ChipDnaMobile.getInstance().getAvailablePinPads(parameters)
- }
-
- fun registerListeners() {
- ChipDnaMobile.getInstance().addConnectAndConfigureFinishedListener(this)
-
- val transactionListener = TransactionListener()
- ChipDnaMobile.getInstance().addTransactionUpdateListener(transactionListener)
- ChipDnaMobile.getInstance().addTransactionFinishedListener(transactionListener)
- ChipDnaMobile.getInstance().addDeferredAuthorizationListener(transactionListener)
- ChipDnaMobile.getInstance().addSignatureVerificationListener(transactionListener)
- ChipDnaMobile.getInstance().addVoiceReferralListener(transactionListener)
- ChipDnaMobile.getInstance().addPartialApprovalListener(transactionListener)
- ChipDnaMobile.getInstance().addForceAcceptanceListener(transactionListener)
- ChipDnaMobile.getInstance().addVerifyIdListener(transactionListener)
-
- ChipDnaMobile.getInstance().addProcessReceiptFinishedListener(ProcessReceiptListener())
- }
-
- fun initialize(appContext: Context, completion: (() -> Unit)? = null) {
- val authTask = ChipDnaAuthTask(WeakReference(appContext))
- authTask.delegate = WeakReference(object : ChipDnaAuthTask.ChipDnaAuthTaskResponse {
- override fun processFinish(parameters: Parameters?) {
- setCredentials()
-// registerListeners()
- completion?.invoke()
-// val response =
-// ChipDnaMobile.getInstance().connectAndConfigure(ChipDnaMobile.getInstance().getStatus(null))
-// if (response.containsKey(ParameterKeys.Result) && response.getValue(ParameterKeys.Result).equals(
-// ParameterValues.FALSE
-// )
-// ) {
-// completion?.invoke()
-// }
- }
- })
-
- authTask.execute("password")
- }
-
- private fun setCredentials() {
- if (appId == null || apiKey == null || environment == null) {
- return
- }
-
- // Credentials are set in ChipDnaMobile Status object. It's recommended that you fetch fresh ChipDnaMobile Status object each time you wish to make changes.
- // This ensures the set of properties used is always up to date with the version of properties in ChipDnaMobile
- val statusParameters = ChipDnaMobile.getInstance().getStatus(null)
-
- // Credentials are returned to ChipDnaMobile as a set of Parameters
- val requestParameters = Parameters()
-
- requestParameters.add(ParameterKeys.ApiKey, apiKey)
- requestParameters.add(ParameterKeys.Environment, environment)
- requestParameters.add(ParameterKeys.ApplicationIdentifier, appId?.toUpperCase())
-
- // Once all changes have been made a call to .setProperties() is required in order for the changes to take effect.
- // Parameters are passed within this method and added to the ChipDna Mobile status object.
- ChipDnaMobile.getInstance().setProperties(requestParameters)
- }
-
- /*
- Listeners
- */
-
- override fun onConnectAndConfigureFinished(params: Parameters?) {
- System.out.println("Connected and configured")
- }
-
- fun connectForReal() {
- var response = ChipDnaMobile.getInstance().connectAndConfigure(ChipDnaMobile.getInstance().getStatus(null))
- registerListeners()
- Log.d("connectforreal", response.toString())
- }
-
- override fun onAvailablePinPads(parameters: Parameters?) {
- val availablePinPadsXml = parameters?.getValue(ParameterKeys.AvailablePinPads)
- val task = DeserializePinPadTask()
- task.delegate = WeakReference(object : DeserializePinPadTask.DeserializePinPadTaskResponse {
- override fun processFinish(availableReaders: List?) {
- availableReaders?.first()?.let {
- val task = ConnectPinPadTask()
- task.delegate = WeakReference(object : ConnectPinPadTask.ConnectPinPadTaskResponse {
- override fun processFinish(parameters: Parameters) {
- connectForReal()
- }
- })
- task.execute(it)
- }
- }
- })
- task.execute(availablePinPadsXml)
- }
-
- private inner class ProcessReceiptListener : IProcessReceiptFinishedListener {
- override fun onProcessReceiptFinishedListener(parameters: Parameters) {
- Log.d("receipt", parameters.toString())
- }
- }
-
- private inner class TransactionListener :
- ITransactionUpdateListener,
- ITransactionFinishedListener,
- IDeferredAuthorizationListener,
- ISignatureVerificationListener,
- IVoiceReferralListener,
- IPartialApprovalListener,
- IForceAcceptanceListener,
- IVerifyIdListener {
- override fun onTransactionUpdateListener(parameters: Parameters) {
- Log.d("transaction", (parameters.getValue(ParameterKeys.TransactionUpdate)))
- }
-
- override fun onTransactionFinishedListener(parameters: Parameters) {
- Log.d("transactionFinished", parameters.toString())
- }
-
- override fun onSignatureVerification(parameters: Parameters) {
- Log.d("transaction", "Signature Check Required")
-
- if (parameters.getValue(ParameterKeys.ResponseRequired) != ParameterValues.TRUE) {
- // Signature handled on PINpad. No call to ChipDna Mobile required.
- return
- }
-
- val operatorPinRequired = parameters.getValue(ParameterKeys.OperatorPinRequired) == ParameterValues.TRUE
- val receiptDataXml = parameters.getValue(ParameterKeys.ReceiptData)
-
- val approveSignatureParams = Parameters()
- approveSignatureParams.add(ParameterKeys.Result, ParameterValues.TRUE)
-
- ChipDnaMobile.getInstance().continueSignatureVerification(approveSignatureParams)
-// Thread(Runnable { requestSignatureCheck(operatorPinRequired, false, receiptDataXml) }).start()
- }
-
- override fun onVoiceReferral(parameters: Parameters) {
- Log.d("transaction", "Voice Referral Check Required")
-
- if (parameters.getValue(ParameterKeys.ResponseRequired) != ParameterValues.TRUE) {
- // Voice referral handled on PINpad. No call to ChipDna Mobile required.
- return
- }
-
- val phoneNumber = parameters.getValue(ParameterKeys.ReferralNumber)
- val operatorPinRequired = parameters.getValue(ParameterKeys.OperatorPinRequired) == ParameterValues.TRUE
-
-// Thread(Runnable { requestVoiceReferral(phoneNumber, operatorPinRequired) }).start()
- }
-
- /*
- Other ChipDna Mobile Callbacks, not required in this demo.
- You may need to implement some of these depending on what your terminal supports.
- */
-
- override fun onVerifyId(parameters: Parameters) {
- Log.d("transaction", parameters.toString())
- }
-
- override fun onDeferredAuthorizationListener(parameters: Parameters) {
- Log.d("transaction", parameters.toString())
- }
-
- override fun onForceAcceptance(parameters: Parameters) {
- Log.d("transaction", parameters.toString())
- }
-
- override fun onPartialApproval(parameters: Parameters) {
- Log.d("transaction", parameters.toString())
- }
- }
-}
diff --git a/app/src/main/java/com/fattmerchant/fmsampleclient/MainActivity.kt b/app/src/main/java/com/fattmerchant/fmsampleclient/MainActivity.kt
deleted file mode 100644
index 73387f3d..00000000
--- a/app/src/main/java/com/fattmerchant/fmsampleclient/MainActivity.kt
+++ /dev/null
@@ -1,523 +0,0 @@
-package com.fattmerchant.fmsampleclient
-
-import android.Manifest
-import android.app.AlertDialog
-import android.content.Context
-import android.os.Bundle
-import android.text.method.ScrollingMovementMethod
-import android.view.View
-import android.widget.EditText
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import com.fattmerchant.android.InitParams
-import com.fattmerchant.android.Omni
-import com.fattmerchant.omni.Environment
-import com.fattmerchant.omni.TransactionUpdateListener
-import com.fattmerchant.omni.UserNotificationListener
-import com.fattmerchant.omni.data.Amount
-import com.fattmerchant.omni.data.MobileReader
-import com.fattmerchant.omni.data.TransactionRequest
-import com.fattmerchant.omni.data.TransactionUpdate
-import com.fattmerchant.omni.data.UserNotification
-import com.fattmerchant.omni.data.models.BankAccount
-import com.fattmerchant.omni.data.models.CreditCard
-import com.fattmerchant.omni.data.models.OmniException
-import com.fattmerchant.omni.data.models.PaymentMethod
-import com.fattmerchant.omni.data.models.Transaction
-import kotlinx.android.synthetic.main.activity_main.*
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-import java.util.logging.Logger
-
-class MainActivity : AppCompatActivity(), PermissionsManager {
-
- val staxKey = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjaGFudCI6ImViNDhlZjk5LWFhNzgtNDk2ZS05YjAxLTQyMWY4ZGFmNzMyMyIsImdvZFVzZXIiOnRydWUsImJyYW5kIjoiZmF0dG1lcmNoYW50Iiwic3ViIjoiMzBjNmVlYjYtNjRiNi00N2Y2LWJjZjYtNzg3YTljNTg3OThiIiwiaXNzIjoiaHR0cDovL2FwaWRldjAxLmZhdHRsYWJzLmNvbS9hdXRoZW50aWNhdGUiLCJpYXQiOjE2NDA1NzA4MDAsImV4cCI6MTY0MDY1NzIwMCwibmJmIjoxNjQwNTcwODAwLCJqdGkiOiJ3SjlDa0tqRGNlRHRzMzBhIn0.WcFvqSf0wDungNBPOX4nWfiGAv4uX8sXRVfMMCNx6LU"
-
- val log = Logger.getLogger("MainActivity")
-
- var connectedReader: MobileReader? = null
-
- fun log(msg: String?) {
- log.info("[${Thread.currentThread().name}] $msg")
- }
-
- var transaction: Transaction? = null
-
- fun getAmount(): Int {
- return textInput_amount.text.toString().toFloat().times(100).toInt()
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- setupButtons()
- initializeOmni(staxKey)
- textView.movementMethod = ScrollingMovementMethod()
-// showApiKeyDialog()
- }
-
- override var permissionRequestLauncherCallback: ((Boolean) -> Unit)? = null
- override fun getActivity(): AppCompatActivity {
- return this
- }
-
- override fun getContext(): Context {
- return this
- }
-
- override var permissionRequestLauncher =
- registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
- permissionRequestLauncherCallback?.invoke(isGranted)
- }
-
- private fun setupPerformSaleWithReaderButton() {
- buttonPerformSaleWithReader.setOnClickListener {
- val amount = Amount(getAmount())
- updateStatus("Attempting to charge ${amount.dollarsString()}")
- val request = TransactionRequest(amount)
-// request.customerId = "bbe13c96-8bf6-4cb5-8d5c-24896cf0e0db"
-
- // Listen to transaction updates delivered by the Omni SDK
- Omni.shared()?.transactionUpdateListener = object : TransactionUpdateListener {
- override fun onTransactionUpdate(transactionUpdate: TransactionUpdate) {
- updateStatus("${transactionUpdate.value} | ${transactionUpdate.userFriendlyMessage}")
- }
- }
-
- Omni.shared()?.userNotificationListener = object : UserNotificationListener {
- override fun onUserNotification(userNotification: UserNotification) {
- updateStatus("${userNotification.value} | ${userNotification.userFriendlyMessage}")
- }
-
- override fun onRawUserNotification(userNotification: String) {
- updateStatus(userNotification)
- }
- }
-
- Omni.shared()?.takeMobileReaderTransaction(request, {
-
- val msg = if (it.success == true) {
- "Successfully executed transaction"
- } else {
- "Transaction declined"
- }
-
- runOnUiThread {
- updateStatus(msg)
- }
-
- transaction = it
- }, {
- updateStatus("Couldn't perform sale: ${it.message}. ${it.detail}")
- })
- }
-
- buttonPerformSaleWithReader.isEnabled = true
- }
-
- private fun setupPerformSaleButton() {
- buttonPerformSale.setOnClickListener {
- val amount = Amount(getAmount())
- updateStatus("Attempting to charge ${amount.dollarsString()}")
- val request = TransactionRequest(amount, CreditCard("Test Payment", "4111111111111111", "0224", "32812"))
-
- Omni.shared()?.pay(request, {
- val msg = if (it.success == true) {
- "Successfully executed transaction"
- } else {
- "Transaction declined"
- }
-
- runOnUiThread {
- updateStatus(msg)
- }
-
- transaction = it
- }, {
- updateStatus("Couldn't perform sale: ${it.message}. ${it.detail}")
- })
- }
-
- buttonPerformSale.isEnabled = true
- }
-
- private fun setupPerformAuthButton() {
- buttonPerformAuth.setOnClickListener {
- val amount = Amount(getAmount())
- updateStatus("Attempting to auth ${amount.dollarsString()}")
- val request = TransactionRequest(amount)
- request.preauth = true
-
- Omni.shared()?.takeMobileReaderTransaction(request, {
-
- val msg = if (it.success == true) {
- "Successfully authed transaction"
- } else {
- "Transaction declined"
- }
-
- runOnUiThread {
- updateStatus(msg)
- }
-
- transaction = it
- }, {
- updateStatus("Couldn't perform auth: ${it.message}. ${it.detail}")
- })
- }
- }
-
- private fun setupCaptureLastAuthButton() {
- buttonCaptureLastAuth.setOnClickListener {
- if (transaction?.id == null) { return@setOnClickListener }
- val transactionId = transaction?.id!!
-
- val amount = Amount(getAmount())
- updateStatus("Attempting to capture last auth")
-
- Omni.shared()?.capturePreauthTransaction(transactionId, amount, {
- val msg = if (it.success == true) {
- "Successfully captured transaction"
- } else {
- "Transaction declined"
- }
-
- runOnUiThread {
- updateStatus(msg)
- }
- }, {
- updateStatus("Couldn't perform capture: ${it.message}. ${it.detail}")
- })
- }
- }
-
- private fun setupVoidLastAuthButton() {
- buttonVoidLastAuth.setOnClickListener {
- if (transaction?.id == null) { return@setOnClickListener }
- val transactionId = transaction?.id!!
-
- Omni.shared()?.voidTransaction(transactionId, {
- val msg = if (it.success == true) {
- "Successfully voided transaction"
- } else {
- "Transaction declined"
- }
-
- runOnUiThread {
- updateStatus(msg)
- }
- }, {
- updateStatus("Couldn't perform void: ${it.message}. ${it.detail}")
- })
- }
- }
-
- private fun setupTokenizeCardButton() {
- buttonTokenizeCard.setOnClickListener {
- Omni.shared()?.tokenize(CreditCard.testCreditCard(), {
- val msg = "Successfully tokenized credit card"
- runOnUiThread {
- updateStatus(msg)
- updateStatus(it)
- }
- }, {
- runOnUiThread {
- updateStatus("Couldn't tokenize card: ${it.message}. ${it.detail}")
- }
- })
- }
- }
-
- private fun setupTokenizeBankButton() {
- buttonTokenizeBank.setOnClickListener {
- var andre3000 = BankAccount.testBankAccount().apply {
- personName = "Andree Threestacks"
- }
- Omni.shared()?.tokenize(andre3000, {
- val msg = "Successfully tokenized bank account"
- runOnUiThread {
- updateStatus(msg)
- updateStatus(it)
- }
- }, {
- runOnUiThread {
- updateStatus("Couldn't tokenize card: ${it.message}. ${it.detail}")
- }
- })
- }
- }
-
- private fun setupRefundButton() {
- buttonRefundPreviousTransaction.setOnClickListener {
- updateStatus("Fetching list of transactions")
- Omni.shared()?.getTransactions({ transactions ->
-
- // Figure out which transactions are refundable
- val refundableTransactions = transactions.filter {
- it.source?.contains("CPSDK") == true
- || it.source?.contains("terminalservice.dejavoo") == true
- }
-
- chooseTransaction(refundableTransactions) { transactionToRefund ->
- updateStatus("Trying to refund ${transactionToRefund.pretty()}")
- Omni.shared()?.refundMobileReaderTransaction(transactionToRefund, {
- updateStatus("Refunded ${transactionToRefund.pretty()}")
- }, {
- updateStatus("Error refunding: ${it.message} ${it.detail}")
- })
- }
- }, {
- updateStatus(it.message ?: "Could not get transactions")
- })
- }
- }
-
- private fun setupVoidButton() {
- buttonVoidTransaction.setOnClickListener {
- updateStatus("Fetching list of transactions")
- Omni.shared()?.getTransactions({ transactions ->
-
- // Figure out which transactions are refundable
- val voidableTransactions = transactions.filter {
- it.source?.contains("CPSDK") == true
- || it.source?.contains("terminalservice.dejavoo") == true
- }
-
- chooseTransaction(voidableTransactions) { transactionToRefund ->
- updateStatus("Trying to void ${transactionToRefund.pretty()}")
- Omni.shared()?.voidMobileReaderTransaction(transactionToRefund, {
- updateStatus("Voided ${transactionToRefund.pretty()}")
- }, {
- updateStatus("Error voiding: ${it.message} ${it.detail}")
- })
- }
- }, {
- updateStatus(it.message ?: "Could not get transactions")
- })
- }
- }
-
- private fun setupInitializeButton() {
- buttonInitialize.setOnClickListener {
- showApiKeyDialog()
- }
- }
-
- private fun setupConnectReaderButton() {
- buttonConnectReader.setOnClickListener {
- searchAndConnectReader()
- }
- }
-
- private fun setupReaderDetailsButton() {
- buttonConnectedReaderDetails.setOnClickListener {
- Omni.shared()?.getConnectedReader({ connectedReader ->
- connectedReader?.let { reader ->
- updateStatus("Connected Reader:")
- updateStatus(reader)
- } ?: updateStatus("There is no connected reader")
- }, { exception ->
- updateStatus(exception)
- }) ?: updateStatus("Could not get connected reader")
- }
- }
-
- private fun setupDisconnectReaderButton() {
- buttonDisconnectReader.setOnClickListener {
- Omni.shared()?.getConnectedReader({ connectedReader ->
- connectedReader?.let { reader ->
- Omni.shared()?.disconnectReader(reader, {
- updateStatus("Reader disconnected")
- }, {
- updateStatus(it)
- })
- } ?: updateStatus("There is no connected reader")
- }, { exception ->
- updateStatus(exception)
- }) ?: updateStatus("Could not get connected reader")
- }
- }
-
- private fun showApiKeyDialog() {
- val editText = EditText(this).apply { maxLines = 1 }
- updateStatus("Attempting to initialize CPSDK")
- AlertDialog.Builder(this)
- .setTitle("Please provide a Stax api token")
- .setView(editText)
- .setCancelable(false)
- .setPositiveButton("Done") { dialog, _ ->
- dialog.dismiss()
- // If you want to not use the apikey dialog, modify the initializeOmni call like below
- // initializeOmni("insert api key here")
- initializeOmni("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjaGFudCI6ImViNDhlZjk5LWFhNzgtNDk2ZS05YjAxLTQyMWY4ZGFmNzMyMyIsImdvZFVzZXIiOnRydWUsImJyYW5kIjoiZmF0dG1lcmNoYW50Iiwic3ViIjoiMzBjNmVlYjYtNjRiNi00N2Y2LWJjZjYtNzg3YTljNTg3OThiIiwiaXNzIjoiaHR0cDovL2FwaWRldjAxLmZhdHRsYWJzLmNvbS9hdXRoZW50aWNhdGUiLCJpYXQiOjE2MjIxMjgyMzQsImV4cCI6MTYyMjIxNDYzNCwibmJmIjoxNjIyMTI4MjM0LCJqdGkiOiJUYU9RSnV0cElEeWx6MzNoIn0.FosF0OCb4wm3O3Uj98V23xiJ8PN9HDNAqx-k8nhlptA")
- }.show()
- }
-
- private fun showQABuildHashDialog(apiKey: String) {
- val editText = EditText(this).apply { maxLines = 1 }
- updateStatus("Attempting to take QA build hash")
- AlertDialog.Builder(this)
- .setTitle("Please provide a Stax QA Build Hash")
- .setView(editText)
- .setCancelable(false)
- .setPositiveButton("Done") { dialog, _ ->
- val qaBuildHash: String = editText.text.toString()
- if (qaBuildHash.isEmpty()) {
- editText.error = "QA Build Hash is not valid"
- } else {
- dialog.dismiss()
- initializeOmniWithEnvironment(apiKey = apiKey, environment = Environment.QA(qaBuildHash = qaBuildHash))
- }
- }.show()
- }
-
- private fun setupButtons() {
- setupInitializeButton()
- setupPerformSaleWithReaderButton()
- setupPerformSaleButton()
- setupRefundButton()
- setupConnectReaderButton()
- setupDisconnectReaderButton()
- setupVoidButton()
- setupReaderDetailsButton()
- setupDisconnectReaderButton()
- setupTokenizeBankButton()
- setupTokenizeCardButton()
- setupPerformAuthButton()
- setupCaptureLastAuthButton()
- setupVoidLastAuthButton()
- }
-
- private fun Transaction.pretty(): String {
- return "total: $${this.total}\nid: ${id!!.substring(0, 7)}..."
- }
-
- private fun chooseTransaction(transactions: List, completion: (Transaction) -> Unit) {
- updateStatus("Displaying list of transactions")
- runOnUiThread {
- AlertDialog.Builder(this)
- .setTitle("Select a transaction")
- .setItems(
- transactions.map { it.pretty() }.toTypedArray()
- ) { _, which ->
- updateStatus("Transaction chosen: ${transactions[which].pretty()}")
- completion(transactions[which])
- }
- .setNeutralButton("Nevermind") { dialog, _ ->
- updateStatus("Transaction not chosen")
- dialog.dismiss()
- }
- .setCancelable(true)
- .show()
- }
- }
-
- private fun searchAndConnectReader() {
- runIfPermissionGranted(
- Manifest.permission.ACCESS_FINE_LOCATION,
- R.string.permission_rationale_title,
- R.string.permission_rationale_message_fine_location,
- R.string.permission_denied_title,
- R.string.permission_rationale_message_fine_location
- ) {
- updateStatus("Searching for readers...")
- Omni.shared()?.getAvailableReaders {
- val readers = it.map { it.getName() }.toTypedArray()
- updateStatus("Found readers: ${it.map { it.getName() }}")
-
- runOnUiThread {
- AlertDialog.Builder(this@MainActivity)
- .setItems(readers) { dialog, which ->
- val selected = it[which]
-
- updateStatus("Trying to connect to [${selected.getName()}]")
-
- Omni.shared()?.connectReader(selected, { reader ->
- this.connectedReader = reader
- buttonDisconnectReader.isEnabled = true
- updateStatus("Connected to [${reader.getName()}]")
-
- runOnUiThread {
- buttonPerformSaleWithReader.isEnabled = true
- }
- }, { error ->
- updateStatus("Error connecting: $error")
- })
- }.create().show()
- }
- }
- }
- }
-
- private fun updateStatus(reader: MobileReader) = runOnUiThread {
- val readerString = """Mobile Reader:
- Name: ${reader.getName()}
- Serial: ${reader.serialNumber()}
- Make: ${reader.getMake()}
- Model: ${reader.getModel()}
- Firmware: ${reader.getFirmwareVersion()}
- """.trimIndent()
-
- updateStatus(readerString)
- }
-
- private fun updateStatus(msg: String) = runOnUiThread {
- val newText = formatMessage(msg) + "\n" + textView.text
- textView.text = newText
- }
-
- private fun updateStatus(paymentMethod: PaymentMethod) = runOnUiThread {
- var message = "PaymentMethod: "
- message += "\n\t id: ${paymentMethod.id ?: ""}"
- message += "\n\t customerId: ${paymentMethod.customerId}"
- message += "\n\t method: ${paymentMethod.method ?: ""}"
- updateStatus(message)
- }
-
- private fun updateStatus(exception: OmniException) = updateStatus("[${exception.message}] ${exception.detail}")
-
- private fun formatMessage(msg: String): String {
- val dateFormat = SimpleDateFormat("h:m:ss", Locale.US)
- return "${dateFormat.format(Date())} | $msg"
- }
-
- private fun initializeOmni(apiKey: String, environment: Environment = Environment.DEV) {
-
- if (environment == Environment.QA()) {
- showQABuildHashDialog(apiKey = apiKey)
- return
- } else {
- initializeOmniWithEnvironment(apiKey = apiKey, environment = environment)
- }
- }
-
- private fun initializeOmniWithEnvironment(apiKey: String, environment: Environment) {
- updateStatus("Trying to initialize")
- Omni.initialize(
- InitParams(applicationContext, application, apiKey, environment), {
- runOnUiThread {
- updateStatus("Initialized")
- buttonRefundPreviousTransaction.isEnabled = true
- buttonInitialize.visibility = View.GONE
- }
- Omni.shared()?.signatureProvider = SignatureProvider()
- }
- ) {
- updateStatus("${it.message}. ${it.detail}")
- }
-
-// Omni.initialize(
-// InitParams(applicationContext, application, apiKey, OmniApi.Environment.DEV), {
-// runOnUiThread {
-// updateStatus("Initialized")
-// buttonRefundPreviousTransaction.isEnabled = true
-// buttonInitialize.visibility = View.GONE
-// }
-// Omni.shared()?.signatureProvider = SignatureProvider()
-// }
-// ) {
-// updateStatus("${it.message}. ${it.detail}")
-// }
- }
-}
diff --git a/app/src/main/java/com/fattmerchant/fmsampleclient/OmniSample.kt b/app/src/main/java/com/fattmerchant/fmsampleclient/OmniSample.kt
deleted file mode 100644
index f0b0d4fd..00000000
--- a/app/src/main/java/com/fattmerchant/fmsampleclient/OmniSample.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.fattmerchant.fmsampleclient
-
-import android.app.Application
-
-class OmniSample : Application() {
-
- override fun onCreate() {
- super.onCreate()
-// System.setProperty("kotlinx.coroutines.debug", "on")
- }
-}
diff --git a/app/src/main/java/com/fattmerchant/fmsampleclient/PermissionsManager.kt b/app/src/main/java/com/fattmerchant/fmsampleclient/PermissionsManager.kt
deleted file mode 100644
index 78b1fa08..00000000
--- a/app/src/main/java/com/fattmerchant/fmsampleclient/PermissionsManager.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-package com.fattmerchant.fmsampleclient
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.provider.Settings
-import androidx.activity.result.ActivityResultLauncher
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.ContextCompat
-
-/**
- * An object that can request permissions. This should be an Activity or a Fragment
- *
- * ## Usage
- * After declaring conformance to PermissionsManager, add the `permissionRequestLauncher` and
- * the `permissionsRequestLauncherCallback`. It's very important that both of those are implemented
- * like this:
- *
- * ```
- * override var permissionRequestLauncherCallback: ((Boolean) -> Unit)? = null
- *
- * override var permissionRequestLauncher
- * = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
- * permissionRequestLauncherCallback?.invoke(isGranted)
- * }
- * ```
- *
- * **In order for this to work properly, the `permissionRequestLauncher` MUST invoke the
- * `permissionRequestLauncherCallback`!!**
- */
-interface PermissionsManager {
- var permissionRequestLauncher: ActivityResultLauncher
- var permissionRequestLauncherCallback: ((Boolean) -> Unit)?
- fun getActivity(): AppCompatActivity?
- fun getContext(): Context?
-}
-
-/**
- * Ensures that the given `permission` is granted, and only then invokes `performWhenAllowed`.
- *
- * This handles everything from asking for the permissions to showing the alert dialogs informing
- * the user about why they should grant the permission
- */
-fun PermissionsManager.runIfPermissionGranted(
- permission: String,
- permissionRationaleTitle: Int,
- permissionRationaleMessage: Int,
- permissionDeniedTitle: Int,
- permissionDeniedMessage: Int,
- performWhenAllowed: () -> (Unit)
-) {
-
- val activity: Activity = getActivity() ?: return
- val ctx: Context = getContext() ?: return
- when {
- // Permission is already granted
- ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED -> {
- performWhenAllowed()
- }
-
- // Android is requesting that we tell the user why we want permissions
- activity.shouldShowRequestPermissionRationale(permission) -> {
- AlertDialog.Builder(ctx)
- .setTitle(permissionRationaleTitle)
- .setMessage(permissionRationaleMessage)
- .setNegativeButton("No thanks") { dialog, _ ->
- dialog.dismiss()
- }
- .setPositiveButton("Okay") { dialog, _ ->
- permissionRequestLauncherCallback = { isGranted: Boolean ->
- if (isGranted) {
- performWhenAllowed()
- }
- }
- permissionRequestLauncher.launch(permission)
- dialog.dismiss()
- }
- .create()
- .show()
- }
-
- else -> {
- permissionRequestLauncherCallback = { isGranted: Boolean ->
- if (isGranted) {
- performWhenAllowed()
- } else if (activity.shouldShowRequestPermissionRationale(permission)) {
- // The user denied the permission, but we can still ask again because maybe
- // they don't understand _why_ it's so important that they give us
- // permission
- runIfPermissionGranted(
- permission,
- permissionRationaleTitle,
- permissionRationaleMessage,
- permissionDeniedTitle,
- permissionDeniedMessage,
- performWhenAllowed
- )
- } else {
- // If we've asked for the permission, AND it's not granted, AND
- // we should not show the permission rationale, then we can safely assume
- // that the user has chosen to never see this permission again. Any further
- // attempts to request the permission are automatically denied by
- // Android so our only remedy is to politely ask the user to grant the
- // permission in the settings app
- AlertDialog.Builder(ctx)
- .setTitle(permissionDeniedTitle)
- .setMessage(permissionDeniedMessage)
- .setNegativeButton("No thanks") { dialog, _ ->
- dialog.dismiss()
- }
- .setPositiveButton("Settings") { dialog, _ ->
- dialog.dismiss()
- activity.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + BuildConfig.APPLICATION_ID)))
- }
- .create()
- .show()
- }
- }
- permissionRequestLauncher.launch(permission)
- }
- }
-}
diff --git a/app/src/main/java/com/staxpayments/sample/MainActivity.kt b/app/src/main/java/com/staxpayments/sample/MainActivity.kt
new file mode 100644
index 00000000..ac884bc3
--- /dev/null
+++ b/app/src/main/java/com/staxpayments/sample/MainActivity.kt
@@ -0,0 +1,29 @@
+package com.staxpayments.sample
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import com.staxpayments.sample.ui.screens.MainScreen
+import com.staxpayments.sample.ui.theme.StaxAndroidSDKTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ StaxAndroidSDKTheme {
+ MainScreen()
+ }
+ }
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+private fun MainActivityPreview() {
+ StaxAndroidSDKTheme {
+ MainScreen()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/staxpayments/sample/MainApplication.kt b/app/src/main/java/com/staxpayments/sample/MainApplication.kt
new file mode 100644
index 00000000..5a9a610f
--- /dev/null
+++ b/app/src/main/java/com/staxpayments/sample/MainApplication.kt
@@ -0,0 +1,26 @@
+package com.staxpayments.sample
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.content.Context
+
+class MainApplication : Application() {
+
+ /**
+ * Generally, we shouldn't store context like this. However, because it's a small example,
+ * and because we're using the application context and not the activity or fragment context,
+ * it's fine for this exmaple
+ */
+ companion object {
+ @SuppressLint("StaticFieldLeak")
+ lateinit var context: Context
+ lateinit var application: Application
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ context = applicationContext
+ application = this
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/fattmerchant/fmsampleclient/SignatureProvider.kt b/app/src/main/java/com/staxpayments/sample/SignatureProvider.kt
similarity index 82%
rename from app/src/main/java/com/fattmerchant/fmsampleclient/SignatureProvider.kt
rename to app/src/main/java/com/staxpayments/sample/SignatureProvider.kt
index 77a014ef..a703acac 100644
--- a/app/src/main/java/com/fattmerchant/fmsampleclient/SignatureProvider.kt
+++ b/app/src/main/java/com/staxpayments/sample/SignatureProvider.kt
@@ -1,9 +1,7 @@
-package com.fattmerchant.fmsampleclient
-
+package com.staxpayments.sample
import com.fattmerchant.omni.SignatureProviding
class SignatureProvider : SignatureProviding {
-
override fun signatureRequired(completion: (String) -> Unit) {
completion("signature")
}
diff --git a/app/src/main/java/com/staxpayments/sample/state/StaxUiState.kt b/app/src/main/java/com/staxpayments/sample/state/StaxUiState.kt
new file mode 100644
index 00000000..622eafe1
--- /dev/null
+++ b/app/src/main/java/com/staxpayments/sample/state/StaxUiState.kt
@@ -0,0 +1,9 @@
+package com.staxpayments.sample.state
+
+/**
+ * The UI State class with the StaxViewModel
+ * This is only used for logging into the big log view
+ */
+data class StaxUiState(
+ val logString: String = ""
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/staxpayments/sample/ui/components/WideButton.kt b/app/src/main/java/com/staxpayments/sample/ui/components/WideButton.kt
new file mode 100644
index 00000000..566220ae
--- /dev/null
+++ b/app/src/main/java/com/staxpayments/sample/ui/components/WideButton.kt
@@ -0,0 +1,56 @@
+package com.staxpayments.sample.ui.components
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.staxpayments.sample.ui.theme.Gray50
+import com.staxpayments.sample.ui.theme.Purple500
+import com.staxpayments.sample.ui.theme.Purple800
+import com.staxpayments.sample.ui.theme.StaxAndroidSDKTheme
+
+@Composable
+fun WideButton(
+ modifier: Modifier = Modifier,
+ text: String = "",
+ onClick: () -> Unit = {}
+) {
+
+ Row(
+ modifier = modifier.fillMaxWidth()
+ ) {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { onClick() },
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if(isSystemInDarkTheme()) Purple800 else Purple500
+ )
+ ) {
+ Text(
+ text = text.uppercase(),
+ color = Gray50,
+ )
+ }
+ }
+}
+
+@Preview(device = Devices.NEXUS_5)
+@Composable
+private fun WideButtonPreview() {
+ StaxAndroidSDKTheme {
+ WideButton(
+ modifier = Modifier.padding(horizontal = 40.dp),
+ text = "Hello",
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/staxpayments/sample/ui/screens/MainScreen.kt b/app/src/main/java/com/staxpayments/sample/ui/screens/MainScreen.kt
new file mode 100644
index 00000000..63a94db5
--- /dev/null
+++ b/app/src/main/java/com/staxpayments/sample/ui/screens/MainScreen.kt
@@ -0,0 +1,156 @@
+package com.staxpayments.sample.ui.screens
+
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.rememberMultiplePermissionsState
+import com.google.accompanist.permissions.rememberPermissionState
+import com.staxpayments.sample.ui.components.WideButton
+import com.staxpayments.sample.ui.theme.Gray50
+import com.staxpayments.sample.ui.theme.Purple500
+import com.staxpayments.sample.ui.theme.Purple800
+import com.staxpayments.sample.ui.theme.StaxAndroidSDKTheme
+import com.staxpayments.sample.viewmodel.StaxViewModel
+
+// Only use bluetooth permissions if on Android S (12) or higher.
+val bluetoothPermissionsList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ listOf(
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN,
+ )
+} else {
+ listOf()
+}
+
+@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
+@Composable
+fun MainScreen(
+ staxViewModel: StaxViewModel = viewModel()
+) {
+ val topAppBarColor = if (isSystemInDarkTheme()) Purple800 else Purple500
+ val padding = 16.dp
+
+ val staxUiState by staxViewModel.uiState.collectAsState()
+
+
+ val locationPermissionLauncher = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
+ val bluetoothPermissionLauncher = rememberMultiplePermissionsState(bluetoothPermissionsList) {
+ // TODO: Check Permissions Results
+ locationPermissionLauncher.launchPermissionRequest()
+ }
+
+ LaunchedEffect(Unit) {
+ bluetoothPermissionLauncher.launchMultiplePermissionRequest()
+ }
+
+ Scaffold(
+ topBar = { TopAppBar(
+ title = { Text(
+ text = "Stax SDK Sample",
+ color = Gray50,
+ fontWeight = FontWeight.Bold,
+ ) },
+ colors = TopAppBarDefaults.largeTopAppBarColors(
+ containerColor = topAppBarColor,
+ )
+ ) }
+ ) {
+ // Main Container
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = padding)
+ ) {
+ Spacer(modifier = Modifier.size(it.calculateTopPadding() + 16.dp))
+
+ // Content
+ Column {
+ // Log View
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(3f)
+ .verticalScroll(rememberScrollState()),
+ text = staxUiState.logString
+ )
+
+ // Amount Text Input
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = padding),
+ value = "0.01",
+ onValueChange = {},
+ enabled = false,
+ label = { Text("Amount") }
+ )
+
+ // Buttons
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .weight(2f)
+ ) {
+ val context = LocalContext.current
+
+ WideButton(text = "Initialize") { staxViewModel.onInitialize() }
+ WideButton(text = "Search & Connect to Reader") { staxViewModel.onSearchAndConnectToReaders(context) }
+ WideButton(text = "Perform Sale With Reader") { staxViewModel.onPerformSaleWithReader() }
+ WideButton(text = "Perform Auth With Reader") { staxViewModel.onPerformAuthWithReader() }
+ WideButton(text = "Capture Last Auth") { staxViewModel.onCaptureLastAuth() }
+ WideButton(text = "Void Last Transaction") { staxViewModel.onVoidLastTransaction() }
+ WideButton(text = "Tokenize Card") { staxViewModel.onTokenizeCard() }
+ WideButton(text = "Get Connected Reader Details") { staxViewModel.onGetConnectedReaderDetails() }
+ WideButton(text = "Disconnect Reader") { staxViewModel.onDisconnectReader() }
+ WideButton(text = "Cancel Transaction") { staxViewModel.onCancelTransaction() }
+ }
+ Spacer(modifier = Modifier.size(padding))
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true, device = Devices.NEXUS_5)
+@Composable
+private fun MainScreenPreviewLight() {
+ StaxAndroidSDKTheme {
+ MainScreen()
+ }
+}
+
+@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES, device = Devices.NEXUS_5)
+@Composable
+private fun MainScreenPreviewDark() {
+ StaxAndroidSDKTheme {
+ MainScreen()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/staxpayments/sample/ui/theme/Color.kt b/app/src/main/java/com/staxpayments/sample/ui/theme/Color.kt
new file mode 100644
index 00000000..0d111e13
--- /dev/null
+++ b/app/src/main/java/com/staxpayments/sample/ui/theme/Color.kt
@@ -0,0 +1,178 @@
+package com.staxpayments.sample.ui.theme
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+// Truffle Design Colors
+// https://www.figma.com/file/3I0yooruw9JpjXSh9j3CRZ/Truffle-Design-System?node-id=0%3A1&t=rkjtLCUzwY00vxAn-0
+
+val Purple50 = Color(0xFFF7E9FC)
+val Purple100 = Color(0xFFEFD2F9)
+val Purple400 = Color(0xFFC967EA)
+val Purple500 = Color(0xFFB93BE4)
+val Purple600 = Color(0xFF8C35B8)
+val Purple700 = Color(0xFF602F8B)
+val Purple800 = Color(0xFF33295F)
+
+val Yellow50 = Color(0xFFFEFBE6)
+val Yellow100 = Color(0xFFFDF6CE)
+val Yellow200 = Color(0xFFFCEEA1)
+val Yellow400 = Color(0xFFF8DC3D)
+val Yellow500 = Color(0xFFD7B342)
+val Yellow700 = Color(0xFF7F7F38)
+val Yellow800 = Color(0xFF425135)
+
+val Green50 = Color(0xFFEBF9F0)
+val Green200 = Color(0xFFC4EDD3)
+val Green300 = Color(0xFF9DE2B5)
+val Green400 = Color(0xFF75D697)
+val Green500 = Color(0xFF59A97E)
+val Green600 = Color(0xFF3E7D65)
+val Green800 = Color(0xFF22504C)
+
+val Teal50 = Color(0xFFECF9F5)
+val Teal200 = Color(0xFFBDEADC)
+val Teal300 = Color(0xFF96DEC7)
+val Teal400 = Color(0xFF6ED1B2)
+val Teal600 = Color(0xFF49C59E)
+val Teal700 = Color(0xFF25745B)
+val Teal800 = Color(0xFF1C5946)
+
+val Blue50 = Color(0xFFECF9F9)
+val Blue100 = Color(0xFFD9F2F2)
+val Blue300 = Color(0xFF8CD9D9)
+val Blue400 = Color(0xFF66CCCC)
+val Blue500 = Color(0xFF63A8AE)
+val Blue700 = Color(0xFF367880)
+val Blue800 = Color(0xFF1E4D59)
+
+val Gray50 = Color(0xFFF2F2F2)
+val Gray100 = Color(0xFFDDDFE4)
+val Gray200 = Color(0xFFBDC9CC)
+val Gray400 = Color(0xFF8D9799)
+val Gray500 = Color(0xFF627684)
+val Gray600 = Color(0xFF435E70)
+val Gray700 = Color(0xFF294455)
+
+val StaxBlack = Color(0xFF062333)
+
+val NeutralBlue100 = Color(0xFFCEECFD)
+val NeutralBlue500 = Color(0xFF009BF2)
+val NeutralBlue800 = Color(0xFF004166)
+
+val PositiveGreen200 = Color(0xFFDFFFE8)
+val PositiveGreen500 = Color(0xFF28CB35)
+val PositiveGreen800 = Color(0xFF21A446)
+
+val WarningYellow200 = Color(0xFFFDF6CE)
+val WarningYellow500 = Color(0xFFF8DC3D)
+val WarningYellow700 = Color(0xFFD67300)
+
+val AlertRed100 = Color(0xFFFF9999)
+val AlertRed500 = Color(0xFFFF4646)
+val AlertRed600 = Color(0xFFCC0000)
+
+@Composable
+@Preview(showBackground = true)
+fun ColorPalettePreview() {
+ Column {
+ Row {
+ Column {
+ Box(modifier = Modifier.background(Purple50).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Purple100).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Purple400).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Purple500).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Purple600).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Purple700).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Purple800).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(StaxBlack).width(60.dp).height(40.dp).padding(4.dp))
+ }
+ Column {
+ Box(modifier = Modifier.background(Yellow50).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Yellow100).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Yellow200).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Yellow400).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Yellow500).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Yellow700).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Yellow800).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(StaxBlack).width(60.dp).height(40.dp).padding(4.dp))
+ }
+
+ Column {
+ Box(modifier = Modifier.background(Green50).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Green200).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Green300).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Green400).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Green500).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Green600).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Green800).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(StaxBlack).width(60.dp).height(40.dp).padding(4.dp))
+ }
+
+ Column {
+ Box(modifier = Modifier.background(Teal50).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Teal200).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Teal300).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Teal400).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Teal600).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Teal700).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Teal800).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(StaxBlack).width(60.dp).height(40.dp).padding(4.dp))
+ }
+
+ Column {
+ Box(modifier = Modifier.background(Blue50).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Blue100).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Blue300).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Blue400).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Blue500).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Blue700).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Blue800).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(StaxBlack).width(60.dp).height(40.dp).padding(4.dp))
+ }
+
+ Column {
+ Box(modifier = Modifier.background(Gray50).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Gray100).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Gray200).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Gray400).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Gray500).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Gray600).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(Gray700).width(60.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(StaxBlack).width(60.dp).height(40.dp).padding(4.dp))
+ }
+ }
+ Row {
+ Column {
+ Box(modifier = Modifier.background(NeutralBlue100).width(90.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(NeutralBlue500).width(90.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(NeutralBlue800).width(90.dp).height(40.dp).padding(4.dp))
+ }
+ Column {
+ Box(modifier = Modifier.background(PositiveGreen200).width(90.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(PositiveGreen500).width(90.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(PositiveGreen800).width(90.dp).height(40.dp).padding(4.dp))
+ }
+ Column {
+ Box(modifier = Modifier.background(WarningYellow200).width(90.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(WarningYellow500).width(90.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(WarningYellow700).width(90.dp).height(40.dp).padding(4.dp))
+ }
+ Column {
+ Box(modifier = Modifier.background(AlertRed100).width(89.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(AlertRed500).width(89.dp).height(40.dp).padding(4.dp))
+ Box(modifier = Modifier.background(AlertRed600).width(89.dp).height(40.dp).padding(4.dp))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/staxpayments/sample/ui/theme/Theme.kt b/app/src/main/java/com/staxpayments/sample/ui/theme/Theme.kt
new file mode 100644
index 00000000..3eaed072
--- /dev/null
+++ b/app/src/main/java/com/staxpayments/sample/ui/theme/Theme.kt
@@ -0,0 +1,72 @@
+package com.staxpayments.sample.ui.theme
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val darkScheme = darkColorScheme(
+ primary = Purple800,
+ secondary = Teal400,
+)
+
+private val lightScheme = lightColorScheme(
+ primary = Purple500,
+ secondary = Teal400,
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun StaxAndroidSDKTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ dynamicColor: Boolean = false,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> darkScheme
+ else -> lightScheme
+ }
+
+ val typography = when {
+ darkTheme -> darkTypography
+ else -> lightTypography
+ }
+
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/staxpayments/sample/ui/theme/Type.kt b/app/src/main/java/com/staxpayments/sample/ui/theme/Type.kt
new file mode 100644
index 00000000..9db0cb54
--- /dev/null
+++ b/app/src/main/java/com/staxpayments/sample/ui/theme/Type.kt
@@ -0,0 +1,126 @@
+package com.staxpayments.sample.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+val baseTypography = Typography(
+ displayLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = 48.sp,
+ lineHeight = 72.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ displayMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = 36.sp,
+ lineHeight = 54.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ displaySmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = 28.sp,
+ lineHeight = 42.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ headlineLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = 24.sp,
+ lineHeight = 36.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ headlineMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ lineHeight = 27.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ bodySmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ lineHeight = 18.sp,
+ letterSpacing = 0.5.sp,
+ )
+)
+
+val lightTypography = baseTypography.copy(
+ displayLarge = baseTypography.displayLarge.copy(
+ color = StaxBlack
+ ),
+ displayMedium = baseTypography.displayMedium.copy(
+ color = StaxBlack
+ ),
+ displaySmall = baseTypography.displaySmall.copy(
+ color = StaxBlack
+ ),
+ headlineLarge = baseTypography.headlineLarge.copy(
+ color = StaxBlack
+ ),
+ headlineMedium = baseTypography.headlineMedium.copy(
+ color = StaxBlack
+ ),
+ headlineSmall = baseTypography.headlineSmall.copy(
+ color = StaxBlack
+ ),
+ bodyLarge = baseTypography.bodyLarge.copy(
+ color = StaxBlack
+ ),
+ bodyMedium = baseTypography.bodyMedium.copy(
+ color = StaxBlack
+ ),
+ bodySmall = baseTypography.bodySmall.copy(
+ color = StaxBlack
+ ),
+)
+
+val darkTypography = baseTypography.copy(
+ displayLarge = baseTypography.displayLarge.copy(
+ color = Gray50
+ ),
+ displayMedium = baseTypography.displayMedium.copy(
+ color = Gray50
+ ),
+ displaySmall = baseTypography.displaySmall.copy(
+ color = Gray50
+ ),
+ headlineLarge = baseTypography.headlineLarge.copy(
+ color = Gray50
+ ),
+ headlineMedium = baseTypography.headlineMedium.copy(
+ color = Gray50
+ ),
+ headlineSmall = baseTypography.headlineSmall.copy(
+ color = Gray50
+ ),
+ bodyLarge = baseTypography.bodyLarge.copy(
+ color = Gray50
+ ),
+ bodyMedium = baseTypography.bodyMedium.copy(
+ color = Gray50
+ ),
+ bodySmall = baseTypography.bodySmall.copy(
+ color = Gray50
+ ),
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/staxpayments/sample/viewmodel/StaxViewModel.kt b/app/src/main/java/com/staxpayments/sample/viewmodel/StaxViewModel.kt
new file mode 100644
index 00000000..2b75ba9a
--- /dev/null
+++ b/app/src/main/java/com/staxpayments/sample/viewmodel/StaxViewModel.kt
@@ -0,0 +1,363 @@
+package com.staxpayments.sample.viewmodel
+
+import android.app.AlertDialog
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import com.fattmerchant.android.InitParams
+import com.fattmerchant.android.Omni
+import com.fattmerchant.omni.TransactionUpdateListener
+import com.fattmerchant.omni.UserNotificationListener
+import com.fattmerchant.omni.data.Amount
+import com.fattmerchant.omni.data.MobileReader
+import com.fattmerchant.omni.data.TransactionRequest
+import com.fattmerchant.omni.data.TransactionUpdate
+import com.fattmerchant.omni.data.UserNotification
+import com.fattmerchant.omni.data.models.CreditCard
+import com.fattmerchant.omni.data.models.Transaction
+import com.staxpayments.BuildConfig
+import com.staxpayments.sample.MainApplication
+import com.staxpayments.sample.SignatureProvider
+import com.staxpayments.sample.state.StaxUiState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class StaxViewModel : ViewModel() {
+ // Set the api key value by setting `staxApiKey` in your `local.properties` file
+ private val apiKey = BuildConfig.STAX_API_KEY
+ private var reader: MobileReader? = null
+ private var lastTransaction: Transaction? = null
+
+ private val _uiState = MutableStateFlow(StaxUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ /**
+ * Creates a new message in the UI Logger
+ */
+ private fun log(str: String) {
+ val date = SimpleDateFormat("hh:mm:ss", Locale.US).format(Date())
+ val msg = "$date | $str"
+ _uiState.update { state ->
+ state.copy(logString = state.logString + "$msg\n")
+ }
+ }
+
+ /**
+ * Runs the main Stax.initialize() code
+ */
+ fun onInitialize() {
+ log("Initializing...")
+
+ /**
+ * Pass in your required parameters. `context` and `application` are not stored, but
+ * required for the initialization with our hardware. Rather than use static variables
+ * as used in this example, Stax recommends running your initialization code in a
+ * custom Application class. However, for this example, we delay initialization to show
+ * how it all works.
+ */
+ val params = InitParams(
+ MainApplication.context,
+ MainApplication.application,
+ apiKey
+ )
+ Omni.initialize(
+ params = params,
+ completion = {
+ log("Initialized!")
+ Omni.shared()?.signatureProvider = SignatureProvider()
+ },
+ error = { exception ->
+ log("There was an error initializing...")
+ log("${exception.message}. ${exception.detail}")
+ }
+ )
+ }
+
+ /**
+ * Searches for readers over BLE, shows an alert dialog, and connects to it
+ */
+ fun onSearchAndConnectToReaders(context: Context) {
+ log("Searching for readers...")
+
+
+ /**
+ * `Omni.shared().getAvailableReaders` returns a list of readers that you can potentially
+ * connect to. These readers are searched over Bluetooth and not connected to when running
+ * `getAvailableReaders()`. To connect them, we'll need to run `Stax.instance().connectReader`
+ */
+
+ Omni.shared()?.getAvailableReaders { found ->
+ val readers = found.map { "${it.getName()} - ${it.getConnectionType()}" }.toTypedArray()
+ log("Found readers: ${found.map { it.getName() }}")
+
+ val dialog = AlertDialog.Builder(context).setItems(readers) { _, which ->
+ val selected = found[which]
+ log("Trying to connect to [${selected.getName()}]...")
+
+ /**
+ * Passing in one of the readers that was found, we call `Stax.instance().connectReader`
+ * to initiate the Bluetooth connection to the hardware reader. Depending on if the
+ * connection is a success or fail determines which of the two callbacks are called.
+ */
+ Omni.shared()?.connectReader(
+ mobileReader = selected,
+ onConnected = { connected ->
+ reader = connected
+ log("Connected to [${reader?.getName()}]!")
+ },
+ onFail = { errorMsg ->
+ log("Error connecting: $errorMsg")
+ }
+ )
+ }.create()
+ dialog.show()
+ }
+ }
+
+ /**
+ * Performs a charge of $0.01 on the reader
+ * TODO: Read value from text input
+ */
+ fun onPerformSaleWithReader() {
+ // The Amount class is used for handling off-by-one errors, rounding, and more
+ val amount = Amount(1)
+ val request = TransactionRequest(amount)
+
+ log("Attempting to charge ${amount.dollarsString()}")
+ Omni.shared()?.apply {
+ // Listen to transaction updates delivered by the Stax SDK
+ transactionUpdateListener = object : TransactionUpdateListener {
+ override fun onTransactionUpdate(transactionUpdate: TransactionUpdate) {
+ log("${transactionUpdate.value} | ${transactionUpdate.userFriendlyMessage}")
+ }
+ }
+
+ // Listen to user-level notifications
+ userNotificationListener = object : UserNotificationListener {
+ override fun onUserNotification(userNotification: UserNotification) {
+ log("${userNotification.value} | ${userNotification.userFriendlyMessage}")
+ }
+
+ override fun onRawUserNotification(userNotification: String) {
+ log(userNotification)
+ }
+ }
+
+ /**
+ * To run a charge, you call the `Stax.instance().takeMobileReaderTransaction()` function.
+ * The function takes in a [TransactionRequest], a completion handler, and an error handler.
+ * The completion handler is called if the transaction gets a response from the mobile
+ * reader. If there is a problem with either the hardware or the api during the function,
+ * the error handler is called.
+ */
+ takeMobileReaderTransaction(
+ request = request,
+ completion = { transaction ->
+ if (transaction.success == true) {
+ log("Successfully executed transaction")
+ } else {
+ log("Transaction declined")
+ }
+ lastTransaction = transaction
+ },
+ error = {
+ log("Couldn't perform sale: ${it.message}. ${it.detail}")
+ }
+ )
+ }
+ }
+
+ /**
+ * Performs a pre auth of $0.01 on the reader
+ * TODO: Read value from text input
+ */
+ fun onPerformAuthWithReader() {
+ // The Amount class also supports floats for more human-readable values
+ val amount = Amount(0.01)
+ val request = TransactionRequest(amount)
+ request.preauth = true
+
+ log("Attempting to auth ${amount.dollarsString()}")
+
+ /**
+ * To run a Pre-Authorization, you call the `Stax.instance().takeMobileReaderTransaction()`
+ * function, but set the request.preauth value to `true.
+ */
+ Omni.shared()?.takeMobileReaderTransaction(
+ request = request,
+ completion = { transaction ->
+ if (transaction.success == true) {
+ log("Successfully authorized transaction")
+ } else {
+ log("Transaction declined")
+ }
+ lastTransaction = transaction
+ },
+ error = {
+ log("Couldn't perform auth: ${it.message}. ${it.detail}")
+ }
+ )
+ }
+
+ /**
+ * Takes the last transaction as a pre-auth and attempts to capture it
+ * TODO: Read value from text input
+ */
+ fun onCaptureLastAuth() {
+ if (lastTransaction?.id == null) { return }
+
+ val id = lastTransaction?.id!!
+ val amount = Amount(0.01)
+
+ log("Attempting to capture last auth...")
+
+ /**
+ * To capture a pre-authorized transaction, you call the `Stax.instance().capturePreAuthTransaction()`
+ * function. The function takes in an ID, as well as an optional amount. If no amount is
+ * passed in, the full pre-authorized value will be captured.
+ */
+ Omni.shared()?.capturePreauthTransaction(
+ transactionId = id,
+ amount = amount,
+ completion = { transaction ->
+ if (transaction.success == true) {
+ log("Successfully authorized transaction")
+ } else {
+ log("Transaction declined")
+ }
+ },
+ error = {
+ log("Couldn't perform capture: ${it.message}. ${it.detail}")
+ }
+ )
+ }
+
+ /**
+ * Voids the previous transaction
+ */
+ fun onVoidLastTransaction() {
+ if (lastTransaction?.id == null) { return }
+ val id = lastTransaction?.id!!
+
+ log("Attempting to void last transaction...")
+
+ /**
+ * Voiding the last transaction only requires the transaction id of the transaction you
+ * would like to void.
+ */
+ Omni.shared()?.voidTransaction(
+ transactionId = id,
+ completion = { transaction ->
+ if (transaction.success == true) {
+ log("Successfully voided transaction")
+ } else {
+ log("Transaction declined")
+ }
+ },
+ error = {
+ log("Couldn't perform void: ${it.message}. ${it.detail}")
+ }
+ )
+
+ }
+
+ /**
+ * Tokenize the test card
+ */
+ fun onTokenizeCard() {
+ /**
+ * Tokenizing a credit card does not use the hardware, but it is a helpful tool for
+ * tokenizing cards for use with the API. To tokenize a credit card, you create a
+ * [CreditCard] object, and pass it into the `Stax.instance().tokenize()` function.
+ */
+ val card = CreditCard(
+ personName = "John Doe", // "First Last" format
+ cardNumber = "4111111111111111", // A Test Credit Card number
+ cardExp = "0530", // "MMYY" format
+ addressZip = "55555", // 5 digit zip code
+ address1 = "123 Orange Avenue", // Street address
+ addressCity = "Orlando", // City
+ addressState = "FL", // State code. NOT the fully qualified state name
+ )
+
+ Omni.shared()?.tokenize(
+ creditCard = card,
+ completion = { paymentMethod ->
+ log("Successfully tokenized credit card")
+ log(paymentMethod.toString())
+ },
+ error = {
+ log("Couldn't tokenize card: ${it.message}. ${it.detail}")
+ }
+ )
+ }
+
+ /**
+ * Show reader details
+ */
+ fun onGetConnectedReaderDetails() {
+ /**
+ * You can get some of the connection details for the hardware reader by running the
+ * `Stax.instance().getConnectedReader()` function. This allows you to read various
+ * hardware details that may be helpful for debugging issues with the Stax support team
+ */
+ Omni.shared()?.getConnectedReader(
+ onReaderFound = { reader ->
+ if (reader != null) {
+ log("Connected Reader:")
+ log(reader.toString())
+ } else {
+ log("There is no connected reader")
+ }
+ },
+ onFail = {
+ log(it.toString())
+ }
+ )
+ }
+
+ /**
+ * Disconnect the current reader
+ */
+ fun onDisconnectReader() {
+ /**
+ * You can disconnect the current reader by running the `Stax.instance().disconnectReader()`
+ * function. In this example, we check if the reader is connected before trying to disconnect.
+ */
+ Omni.shared()?.apply {
+ disconnectReader(
+ mobileReader = null,
+ onDisconnected = {
+ log("Reader disconnected")
+ reader = null
+ },
+ onFail = { log(it.toString()) }
+ )
+ }
+ }
+
+ fun onCancelTransaction() {
+ Omni.shared()?.cancelMobileReaderTransaction(
+ completion = {
+ log("Successfully canceled the transaction")
+ Omni.shared()?.disconnectReader(
+ mobileReader = null,
+ onDisconnected = {
+ log("Reader disconnected (from cancel)")
+ reader = null
+ },
+ onFail = {
+ log(it.toString())
+ }
+ )
+ },
+ error = {
+ log(it.toString())
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index ea08c534..00000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,182 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
deleted file mode 100644
index e18f895d..00000000
--- a/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..14bb3526
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/css/style.scss b/assets/css/style.scss
deleted file mode 100644
index 4408440e..00000000
--- a/assets/css/style.scss
+++ /dev/null
@@ -1,41 +0,0 @@
----
----
-
-@import "{{ site.theme }}";
-@import url(https://fonts.googleapis.com/css?family=Nunito:300,500);
-
-$navy: #00416B;
-$navy-dark: #386486;
-$accent-blue: #45B5D0;
-$navy-60: #6C87A3;
-
-body {
- font-family: Nunito;
-}
-
-.page-header {
- background-color: $navy;
- background-image: none;
-
- a {
- color: #ffffff;
- }
-}
-
-a {
- color: $accent-blue;
-}
-
-.main-content {
- color: $navy-60;
-
- h1, h2, h3, h4, h5, h6 {
- font-size: 1.2em;
- font-weight: bold;
- color: $navy-dark;
-
- a {
- color: $navy-dark;
- }
- }
-}
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index ae42ec41..00000000
--- a/build.gradle
+++ /dev/null
@@ -1,37 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
- ext.kotlin_version = '1.5.30'
- ext.ktor_version = '1.1.4'
- repositories {
- google()
- mavenCentral()
- jcenter()
- maven { url "https://jitpack.io" }
- maven {
- url "https://plugins.gradle.org/m2/"
- }
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:4.1.3'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- classpath "org.jlleitschuh.gradle:ktlint-gradle:10.3.0"
-
- }
-}
-
-allprojects {
- repositories {
- google()
- mavenCentral()
- jcenter()
- maven { url "https://jitpack.io" }
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 00000000..95d3d779
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ id("com.android.application") version "8.1.0" apply false
+ id("com.android.library") version "8.1.0" apply false
+ id("org.jetbrains.kotlin.android") version "1.9.0" apply false
+}
\ No newline at end of file
diff --git a/cardpresent/.DS_Store b/cardpresent/.DS_Store
index 25f72b89..dc1f03c2 100644
Binary files a/cardpresent/.DS_Store and b/cardpresent/.DS_Store differ
diff --git a/cardpresent/.gitignore b/cardpresent/.gitignore
deleted file mode 100644
index 796b96d1..00000000
--- a/cardpresent/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/cardpresent/.idea/gradle.xml b/cardpresent/.idea/gradle.xml
deleted file mode 100644
index d55d9abb..00000000
--- a/cardpresent/.idea/gradle.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cardpresent/.idea/misc.xml b/cardpresent/.idea/misc.xml
deleted file mode 100644
index fb0d77e8..00000000
--- a/cardpresent/.idea/misc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cardpresent/.idea/workspace.xml b/cardpresent/.idea/workspace.xml
deleted file mode 100644
index 31999388..00000000
--- a/cardpresent/.idea/workspace.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1604430535676
-
-
- 1604430535676
-
-
-
-
\ No newline at end of file
diff --git a/cardpresent/build.gradle b/cardpresent/build.gradle
index 811a67a0..126c2136 100644
--- a/cardpresent/build.gradle
+++ b/cardpresent/build.gradle
@@ -6,11 +6,12 @@ plugins {
group = 'com.github.fattmerchantorg'
android {
- compileSdkVersion 33
+ namespace "com.fattmerchant"
+ compileSdk 34
defaultConfig {
- minSdkVersion 23
- targetSdkVersion 33
+ minSdk 23
+ targetSdk 34
versionCode 15
versionName "2.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -35,8 +36,8 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
buildTypes {
@@ -94,9 +95,9 @@ dependencies {
}
// Ktor
- implementation "io.ktor:ktor-client-core:$ktor_version"
- implementation "io.ktor:ktor-client-okhttp:$ktor_version"
- implementation "io.ktor:ktor-client-ios:$ktor_version"
- implementation "io.ktor:ktor-client-json:$ktor_version"
- implementation "io.ktor:ktor-client-gson:$ktor_version"
+ implementation "io.ktor:ktor-client-core:1.1.4"
+ implementation "io.ktor:ktor-client-okhttp:1.1.4"
+ implementation "io.ktor:ktor-client-ios:1.1.4"
+ implementation "io.ktor:ktor-client-json:1.1.4"
+ implementation "io.ktor:ktor-client-gson:1.1.4"
}
diff --git a/cardpresent/libs/ChipDnaMobile.jar b/cardpresent/libs/ChipDnaMobile.jar
index e05f7e53..ce46cbb4 100644
Binary files a/cardpresent/libs/ChipDnaMobile.jar and b/cardpresent/libs/ChipDnaMobile.jar differ
diff --git a/cardpresent/src/main/AndroidManifest.xml b/cardpresent/src/main/AndroidManifest.xml
index 629e5c86..bdae66c8 100644
--- a/cardpresent/src/main/AndroidManifest.xml
+++ b/cardpresent/src/main/AndroidManifest.xml
@@ -1,7 +1,2 @@
-
-
-
-
-
+
diff --git a/cardpresent/src/main/java/com/fattmerchant/android/MobileReaderDriverRepository.kt b/cardpresent/src/main/java/com/fattmerchant/android/MobileReaderDriverRepository.kt
index 936e1c67..69f0f9b6 100644
--- a/cardpresent/src/main/java/com/fattmerchant/android/MobileReaderDriverRepository.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/android/MobileReaderDriverRepository.kt
@@ -33,11 +33,7 @@ internal class MobileReaderDriverRepository : MobileReaderDriverRepository {
return null
}
- override suspend fun getDriverFor(mobileReader: MobileReader): MobileReaderDriver? {
- mobileReader.serialNumber()?.let { serial ->
- return getInitializedDrivers().firstOrNull {
- it.familiarSerialNumbers.contains(serial)
- }
- } ?: return null
+ override suspend fun getDriverFor(mobileReader: MobileReader?): MobileReaderDriver? {
+ return getInitializedDrivers().firstOrNull()
}
}
diff --git a/cardpresent/src/main/java/com/fattmerchant/android/chipdna/ChipDnaDriver.kt b/cardpresent/src/main/java/com/fattmerchant/android/chipdna/ChipDnaDriver.kt
index 87c34f53..aaef0896 100644
--- a/cardpresent/src/main/java/com/fattmerchant/android/chipdna/ChipDnaDriver.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/android/chipdna/ChipDnaDriver.kt
@@ -4,9 +4,7 @@ import android.content.Context
import com.creditcall.chipdnamobile.ChipDnaMobile
import com.creditcall.chipdnamobile.ChipDnaMobileSerializer
import com.creditcall.chipdnamobile.DeviceStatus
-import com.creditcall.chipdnamobile.IAvailablePinPadsListener
import com.creditcall.chipdnamobile.IConfigurationUpdateListener
-import com.creditcall.chipdnamobile.IConnectAndConfigureFinishedListener
import com.creditcall.chipdnamobile.IDeviceUpdateListener
import com.creditcall.chipdnamobile.ParameterKeys
import com.creditcall.chipdnamobile.ParameterValues
@@ -32,6 +30,8 @@ import com.fattmerchant.omni.data.models.Transaction
import com.fattmerchant.omni.usecase.CancelCurrentTransactionException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
@@ -47,8 +47,7 @@ internal class ChipDnaDriver :
IConfigurationUpdateListener,
IDeviceUpdateListener {
- class ConnectReaderException(message: String? = null) :
- MobileReaderDriver.ConnectReaderException(mapDetailMessage(message)) {
+ class ConnectReaderException(message: String? = null) : MobileReaderDriver.ConnectReaderException(mapDetailMessage(message)) {
companion object {
fun mapDetailMessage(chipDnaMessage: String?): String? {
return when (chipDnaMessage) {
@@ -61,14 +60,6 @@ internal class ChipDnaDriver :
}
companion object {
- /** The make of the pin pad */
- internal final class PinPadManufacturer(val name: String) {
- companion object {
- val Miura = PinPadManufacturer("Miura")
- val BBPOS = PinPadManufacturer("BBPOS")
- }
- }
-
/**
* Attempts to get the connected mobile reader.
*
@@ -96,9 +87,9 @@ internal class ChipDnaDriver :
override val source: String = "NMI"
override var mobileReaderConnectionStatusListener: MobileReaderConnectionStatusListener? = null
- val log = Logger.getLogger("ChipDNA")
+ private val logger = Logger.getLogger("ChipDNA")
fun log(msg: String?) {
- log.info("[${Thread.currentThread().name}] $msg")
+ logger.info("[${Thread.currentThread().name}] $msg")
}
/**
@@ -106,7 +97,7 @@ internal class ChipDnaDriver :
* happens at runtime.
*
* For example, if the user wants to disconnect a reader, we have to use
- * the ChipDnaMobile.dispose() method. This method uninitializes the SDK and we have to
+ * the ChipDnaMobile.dispose() method. This method un-initializes the SDK and we have to
* initialize again if we want to reconnect a reader. When we want to reconnect, we use these
* args
* */
@@ -161,79 +152,116 @@ internal class ChipDnaDriver :
}
override suspend fun isInitialized(): Boolean {
- return ChipDnaMobile.isInitialized()
+ val isInitialized = ChipDnaMobile.isInitialized()
+ if (!isInitialized && initArgs.isNotEmpty()) {
+ async { initialize(initArgs) }.await()
+ return ChipDnaMobile.isInitialized()
+ }
+ return isInitialized
}
override suspend fun searchForReaders(args: Map): List {
- val parameters = Parameters().apply {
- add(ParameterKeys.SearchConnectionTypeBluetooth, ParameterValues.TRUE)
- add(ParameterKeys.SearchConnectionTypeUsb, ParameterValues.TRUE)
- }
- ChipDnaMobile.getInstance().clearAllAvailablePinPadsListeners()
-
- val pinPads = suspendCancellableCoroutine> { cont ->
- val availablePinPadsListener: IAvailablePinPadsListener? = null
- ChipDnaMobile.getInstance().addAvailablePinPadsListener { params ->
- val availablePinPadsXml = params?.getValue(ParameterKeys.AvailablePinPads)
- val pinPads = deserializePinPads(availablePinPadsXml!!)
- availablePinPadsListener?.let { ChipDnaMobile.getInstance().removeAvailablePinPadsListener(it) }
-
- cont.resume(pinPads)
+ if (!isInitialized()) {
+ if (initArgs.isNotEmpty()) {
+ async { initialize(initArgs) }.await()
+ } else {
+ throw OmniGeneralException.uninitialized
}
+ }
- ChipDnaMobile.getInstance().getAvailablePinPads(parameters)
+ val readers = suspendCancellableCoroutine { continuation ->
+ ChipDnaMobile.getInstance().apply {
+ clearAllAvailablePinPadsListeners()
+ addAvailablePinPadsListener { params ->
+ val xml = params.getValue(ParameterKeys.AvailablePinPads)
+ val pads = deserializePinPads(xml)
+ continuation.resume(pads)
+ }
+ }.getAvailablePinPads(
+ Parameters().apply {
+ add(ParameterKeys.SearchConnectionTypeBluetoothLe, ParameterValues.TRUE)
+ add(ParameterKeys.SearchConnectionTypeBluetooth, ParameterValues.TRUE)
+ add(ParameterKeys.SearchConnectionTypeUsb, ParameterValues.TRUE)
+ }
+ )
}
- return pinPads.map {
- mapPinPadToMobileReader(it)
+ return readers.map { reader ->
+ val type = ConnectionType.parse(reader.connectionType)
+ mapPinPadToMobileReader(reader, type)
}
}
override suspend fun connectReader(reader: MobileReader): MobileReader? {
+ if (!isInitialized()) {
+ if (initArgs.isNotEmpty()) {
+ async { initialize(initArgs) }.await()
+ } else {
+ throw OmniGeneralException.uninitialized
+ }
+ }
- val requestParams = Parameters()
- requestParams.add(ParameterKeys.PinPadName, reader.getName())
- requestParams.add(ParameterKeys.PinPadConnectionType, ParameterValues.BluetoothConnectionType)
-
- ChipDnaMobile.getInstance().setProperties(requestParams)
-
- return suspendCancellableCoroutine { cont ->
- var connectAndConfigureListener: IConnectAndConfigureFinishedListener? = null
- connectAndConfigureListener = IConnectAndConfigureFinishedListener { params ->
- ChipDnaMobile.getInstance().removeConnectAndConfigureFinishedListener(connectAndConfigureListener)
-
- if (params[ParameterKeys.Result] == ParameterValues.TRUE) {
- // Reader is connected. Add the serial number to the list of familiar ones
- // And return the hydrated mobile reader
- Companion.getConnectedReader()?.let { connectedReader ->
- connectedReader.serialNumber()?.let { familiarSerialNumbers.add(it) }
- cont.resume(connectedReader)
+ // Set properties of reader to connect to here. Adding them as
+ // connectAndConfigure params causes it to never connect correctly
+ ChipDnaMobile.getInstance().setProperties(
+ Parameters().apply {
+ add(ParameterKeys.PinPadName, reader.getName())
+ add(ParameterKeys.PinPadConnectionType, reader.getConnectionType().toParameterValue())
+ }
+ )
+
+ return suspendCancellableCoroutine { continuation ->
+ val connectAndConfigureParams = ChipDnaMobile.getInstance().getStatus(null)
+ ChipDnaMobile.getInstance().apply {
+ clearAllConnectAndConfigureFinishedListeners()
+ addConnectAndConfigureFinishedListener { params ->
+ if (params[ParameterKeys.Result] == ParameterValues.TRUE) {
+ getConnectedChipDnaReader()?.let { connectedReader ->
+ connectedReader.serialNumber()?.let { familiarSerialNumbers.add(it) }
+ continuation.resume(connectedReader)
+ }
+ return@addConnectAndConfigureFinishedListener
}
- return@IConnectAndConfigureFinishedListener
+
+ val error = params[ParameterKeys.ErrorDescription]
+ continuation.resumeWithException(ConnectReaderException(error))
}
+ }.connectAndConfigure(connectAndConfigureParams)
+ }
+ }
- val error = params[ParameterKeys.ErrorDescription]
- cont.resumeWithException(ConnectReaderException(error))
+ override suspend fun disconnect(reader: MobileReader?, error: (OmniException) -> Unit): Boolean {
+ val retryLimit = 3
+ val retryTimes = arrayOf(500L, 1000L, 2000L)
+ var retryCount = 0
+
+ do {
+ // Check the result. If the disconnect was successful, break out of the retry loop.
+ val result = ChipDnaMobile.dispose(null)
+ if (result[ParameterKeys.Result] == ParameterValues.TRUE) {
+ mobileReaderConnectionStatusListener?.mobileReaderConnectionStatusUpdate(MobileReaderConnectionStatus.DISCONNECTED)
+ return true
}
- ChipDnaMobile.getInstance().addConnectAndConfigureFinishedListener(connectAndConfigureListener)
- ChipDnaMobile.getInstance().connectAndConfigure(requestParams)
- }
- }
+ // Wait a beat and retry. The failure is due to another transaction in progress like canceling a transaction.
+ async { delay(retryTimes[retryCount]) }.await()
+ retryCount++
+ } while (retryCount < retryLimit)
- override suspend fun disconnect(reader: MobileReader, error: (OmniException) -> Unit): Boolean {
- ChipDnaMobile.dispose(null)
- mobileReaderConnectionStatusListener?.mobileReaderConnectionStatusUpdate(MobileReaderConnectionStatus.DISCONNECTED)
- initialize(initArgs)
- return true
+ return false
}
override suspend fun getConnectedReader(): MobileReader? {
// ChipDna must be initialized
- if (!ChipDnaMobile.isInitialized()) {
- throw OmniGeneralException.uninitialized
+ return if (!ChipDnaMobile.isInitialized()) {
+ if (initArgs.isNotEmpty()) {
+ async { initialize(initArgs) }.await()
+ Companion.getConnectedReader()
+ } else {
+ throw OmniGeneralException.uninitialized
+ }
} else {
- return Companion.getConnectedReader()
+ Companion.getConnectedReader()
}
}
@@ -269,37 +297,43 @@ internal class ChipDnaDriver :
override suspend fun performTransaction(request: TransactionRequest, signatureProvider: SignatureProviding?, transactionUpdateListener: TransactionUpdateListener?, userNotificationListener: UserNotificationListener?): TransactionResult {
val paymentRequestParams = withTransactionRequest(request)
- val result = suspendCancellableCoroutine { cont ->
+ val result = suspendCancellableCoroutine { continuation ->
val transactionListener = ChipDnaTransactionListener()
transactionListener.onFinish = {
- ChipDnaMobile.getInstance().removeTransactionUpdateListener(transactionListener)
- ChipDnaMobile.getInstance().removeTransactionFinishedListener(transactionListener)
- ChipDnaMobile.getInstance().removeDeferredAuthorizationListener(transactionListener)
- ChipDnaMobile.getInstance().removeSignatureVerificationListener(transactionListener)
- ChipDnaMobile.getInstance().removeVoiceReferralListener(transactionListener)
- ChipDnaMobile.getInstance().removePartialApprovalListener(transactionListener)
- ChipDnaMobile.getInstance().removeForceAcceptanceListener(transactionListener)
- ChipDnaMobile.getInstance().removeVerifyIdListener(transactionListener)
- cont.resume(it)
+ ChipDnaMobile.getInstance().apply {
+ removeTransactionUpdateListener(transactionListener)
+ removeTransactionFinishedListener(transactionListener)
+ removeDeferredAuthorizationListener(transactionListener)
+ removeSignatureVerificationListener(transactionListener)
+ removeVoiceReferralListener(transactionListener)
+ removePartialApprovalListener(transactionListener)
+ removeForceAcceptanceListener(transactionListener)
+ removeVerifyIdListener(transactionListener)
+ }
+ continuation.resume(it)
}
transactionListener.signatureProvider = signatureProvider
transactionListener.transactionUpdateListener = transactionUpdateListener
transactionListener.userNotificationListener = userNotificationListener
- ChipDnaMobile.getInstance().addUserNotificationListener(transactionListener)
- ChipDnaMobile.getInstance().addApplicationSelectionListener(transactionListener)
- ChipDnaMobile.getInstance().addTransactionUpdateListener(transactionListener)
- ChipDnaMobile.getInstance().addTransactionFinishedListener(transactionListener)
- ChipDnaMobile.getInstance().addDeferredAuthorizationListener(transactionListener)
- ChipDnaMobile.getInstance().addSignatureVerificationListener(transactionListener)
- ChipDnaMobile.getInstance().addVoiceReferralListener(transactionListener)
- ChipDnaMobile.getInstance().addPartialApprovalListener(transactionListener)
- ChipDnaMobile.getInstance().addForceAcceptanceListener(transactionListener)
- ChipDnaMobile.getInstance().addVerifyIdListener(transactionListener)
-
- val response = ChipDnaMobile.getInstance().startTransaction(paymentRequestParams)
+
+ ChipDnaMobile.getInstance().apply {
+ addUserNotificationListener(transactionListener)
+ addApplicationSelectionListener(transactionListener)
+ addTransactionUpdateListener(transactionListener)
+ addTransactionFinishedListener(transactionListener)
+ addDeferredAuthorizationListener(transactionListener)
+ addSignatureVerificationListener(transactionListener)
+ addVoiceReferralListener(transactionListener)
+ addPartialApprovalListener(transactionListener)
+ addForceAcceptanceListener(transactionListener)
+ addVerifyIdListener(transactionListener)
+ }
+
// TODO: Handle the case where ChipDnaMobile didn't actually start the transaction
+ // val response = ChipDnaMobile.getInstance().startTransaction(paymentRequestParams)
+ ChipDnaMobile.getInstance().startTransaction(paymentRequestParams)
}
// Check for errors
@@ -334,10 +368,10 @@ internal class ChipDnaDriver :
// Build the TransactionResult and return
val firstName = result[ParameterKeys.CardHolderFirstName]
val lastName = result[ParameterKeys.CardHolderLastName]
- val addressZip = result[ParameterKeys.BillingZipCode]
- val address1 = result[ParameterKeys.BillingAddress1]
- val address2 = result[ParameterKeys.BillingAddress2]
- val addressState = result[ParameterKeys.BillingState]
+ // val addressZip = result[ParameterKeys.BillingZipCode]
+ // val address1 = result[ParameterKeys.BillingAddress1]
+ // val address2 = result[ParameterKeys.BillingAddress2]
+ // val addressState = result[ParameterKeys.BillingState]
var ccExpiration: String? = null
val receiptData = ChipDnaMobileSerializer.deserializeReceiptData(result[ParameterKeys.ReceiptData])
@@ -356,14 +390,12 @@ internal class ChipDnaDriver :
externalId = result[ParameterKeys.TransactionId]
cardHolderFirstName = firstName
cardHolderLastName = lastName
- cardType = result[ParameterKeys.CardSchemeId]?.toLowerCase(Locale.ROOT)
+ cardType = result[ParameterKeys.CardSchemeId]?.lowercase(Locale.ROOT)
cardExpiration = ccExpiration
amount = Amount(cents = result[ParameterKeys.Amount]?.toInt() ?: request.amount.cents)
-
this.source = this@ChipDnaDriver.source
success = result[ParameterKeys.TransactionResult] == ParameterValues.Approved
transactionSource = receiptData[ReceiptFieldKey.TRANSACTION_SOURCE]?.value
-
result[ParameterKeys.CustomerVaultId]?.let { token ->
paymentToken = "nmi_$token"
}
@@ -388,16 +420,15 @@ internal class ChipDnaDriver :
add(ParameterKeys.UserReference, ref)
}
- var response = ChipDnaMobile.getInstance().voidTransaction(voidRequestParams)
-
// TODO: Handle errors
+ // val response = ChipDnaMobile.getInstance().voidTransaction(voidRequestParams)
+ ChipDnaMobile.getInstance().voidTransaction(voidRequestParams)
return TransactionResult()
}
override fun voidTransaction(transactionResult: TransactionResult, completion: (Boolean) -> Unit) {
val userRef = transactionResult.userReference ?: return completion(false)
-
val params = Parameters().apply {
add(ParameterKeys.UserReference, userRef)
}
@@ -422,7 +453,7 @@ internal class ChipDnaDriver :
return if (result[ParameterKeys.TransactionResult] == ParameterValues.Approved) {
TransactionResult().apply {
- this.request = request
+ // this.request = request
success = true
transactionType = "refund"
amount = Amount(cents = amountCents)
@@ -435,13 +466,11 @@ internal class ChipDnaDriver :
override suspend fun cancelCurrentTransaction(error: ((OmniException) -> Unit)?): Boolean {
val result = ChipDnaMobile.getInstance().terminateTransaction(null)
result?.let {
- val success = result[ParameterKeys.Result] == ParameterValues.TRUE
- if (success) {
- return success
+ if (result[ParameterKeys.Result] == ParameterValues.TRUE) {
+ return true
} else {
val status = ChipDnaMobile.getInstance().getStatus(null)
- val idle = status[ParameterKeys.ChipDnaStatus] == "IDLE"
- if (idle) {
+ if (status[ParameterKeys.ChipDnaStatus] == "IDLE") {
error?.invoke(CancelCurrentTransactionException.NoTransactionToCancel)
} else {
error?.invoke(CancelCurrentTransactionException.Unknown)
@@ -465,13 +494,14 @@ internal class ChipDnaDriver :
return listOf()
}
- val availablePinPadsList = ArrayList()
-
+ val pinPadsList = mutableListOf()
try {
- val availablePinPadsHashMap = ChipDnaMobileSerializer.deserializeAvailablePinPads(pinPadsXml)
- for (connectionType in availablePinPadsHashMap.keys) {
- for (pinPad in availablePinPadsHashMap[connectionType]!!) {
- availablePinPadsList.add(SelectablePinPad(pinPad, connectionType))
+ val pinPadsMap = ChipDnaMobileSerializer.deserializeAvailablePinPads(pinPadsXml)
+ for (connectionType in pinPadsMap.keys) {
+ pinPadsMap[connectionType]?.let { pinPads ->
+ for (pinPad in pinPads) {
+ pinPadsList.add(SelectablePinPad(pinPad, connectionType))
+ }
}
}
} catch (e: XmlPullParserException) {
@@ -480,7 +510,7 @@ internal class ChipDnaDriver :
e.printStackTrace()
}
- return availablePinPadsList
+ return pinPadsList
}
override fun onConfigurationUpdateListener(parameters: Parameters?) {
diff --git a/cardpresent/src/main/java/com/fattmerchant/android/chipdna/ChipDnaUtils.kt b/cardpresent/src/main/java/com/fattmerchant/android/chipdna/ChipDnaUtils.kt
index 6d510174..99636d35 100644
--- a/cardpresent/src/main/java/com/fattmerchant/android/chipdna/ChipDnaUtils.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/android/chipdna/ChipDnaUtils.kt
@@ -1,5 +1,7 @@
package com.fattmerchant.android.chipdna
+import com.creditcall.chipdnamobile.ChipDnaMobile
+import com.creditcall.chipdnamobile.ChipDnaMobileSerializer
import com.creditcall.chipdnamobile.DeviceStatus
import com.creditcall.chipdnamobile.ParameterKeys
import com.creditcall.chipdnamobile.ParameterValues
@@ -16,19 +18,62 @@ import java.util.Locale
import com.creditcall.chipdnamobile.TransactionUpdate as ChipDnaTransactionUpdate
import com.creditcall.chipdnamobile.UserNotification as ChipDnaUserNotification
+enum class ConnectionType {
+ BT,
+ BLE,
+ USB,
+ UNKNOWN;
+
+ companion object {
+ fun parse(str: String): ConnectionType {
+ val bt = ParameterValues.BluetoothConnectionType
+ val ble = ParameterValues.BluetoothLeConnectionType
+ val usb = ParameterValues.UsbConnectionType
+
+ return if (str.equals(bt, ignoreCase = true)) { BT }
+ else if (str.equals(ble, ignoreCase = true)) { BLE }
+ else if (str.equals(usb, ignoreCase = true)) { USB }
+ else { UNKNOWN }
+ }
+ }
+
+ fun toParameterValue(): String {
+ return if (this == BT) { ParameterValues.BluetoothConnectionType }
+ else if (this == BLE) { ParameterValues.BluetoothLeConnectionType }
+ else if (this == USB) { ParameterValues.UsbConnectionType }
+ else { ParameterValues.BluetoothLeConnectionType } // Default to BLE
+ }
+}
+
+/**
+ * Attempts to get the connected mobile reader.
+ * @return the connected [MobileReader], if found. Null otherwise
+ */
+internal fun getConnectedChipDnaReader(): MobileReader? {
+ val chipDnaMobileStatus = ChipDnaMobile.getInstance().getStatus(null)
+ val deviceStatusXml = chipDnaMobileStatus[ParameterKeys.DeviceStatus] ?: return null
+ val deviceStatus = ChipDnaMobileSerializer.deserializeDeviceStatus(deviceStatusXml)
+
+ return when (deviceStatus.status) {
+ DeviceStatus.DeviceStatusEnum.DeviceStatusConnected -> mapDeviceStatusToMobileReader(deviceStatus)
+ else -> null
+ }
+}
+
/**
* Makes an instance of [MobileReader] for the given [pinPad]
*
* @param pinPad a pin pad that can be connected
* @return a [MobileReader]
*/
-internal fun mapPinPadToMobileReader(pinPad: ChipDnaDriver.SelectablePinPad): MobileReader {
+internal fun mapPinPadToMobileReader(pinPad: ChipDnaDriver.SelectablePinPad, connectionType: ConnectionType): MobileReader {
return object : MobileReader {
override fun getName() = pinPad.name
override fun getFirmwareVersion(): String? = null
override fun getMake(): String? = null
override fun getModel(): String? = null
override fun serialNumber(): String? = null
+ override fun getConnectionType(): ConnectionType = connectionType
}
}
@@ -45,6 +90,7 @@ internal fun mapDeviceStatusToMobileReader(deviceStatus: DeviceStatus): MobileRe
override fun getMake(): String? = deviceStatus.make
override fun getModel(): String? = deviceStatus.model
override fun serialNumber(): String? = deviceStatus.serialNumber
+ override fun getConnectionType(): ConnectionType = ConnectionType.UNKNOWN
}
}
diff --git a/cardpresent/src/main/java/com/fattmerchant/android/chipdna/TransactionGateway.kt b/cardpresent/src/main/java/com/fattmerchant/android/chipdna/TransactionGateway.kt
index 568b13ee..c28694e2 100644
--- a/cardpresent/src/main/java/com/fattmerchant/android/chipdna/TransactionGateway.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/android/chipdna/TransactionGateway.kt
@@ -11,7 +11,7 @@ import io.ktor.client.request.get
internal class TransactionGateway {
companion object {
/** Transaction Gateway's baseUrl */
- val baseUrl = "https://secure.nmi.com/api/query.php"
+ private const val baseUrl = "https://secure.nmi.com/api/query.php"
/**
* Fetches the expiration date of the card used to run the transaction with the given id
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/Environment.kt b/cardpresent/src/main/java/com/fattmerchant/omni/Environment.kt
index de431d36..530365d7 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/Environment.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/Environment.kt
@@ -1,6 +1,6 @@
package com.fattmerchant.omni
-sealed class Environment {
+open class Environment {
object LIVE : Environment()
object DEV : Environment()
data class QA(val qaBuildHash: String = "") : Environment()
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/Omni.kt b/cardpresent/src/main/java/com/fattmerchant/omni/Omni.kt
index 70a35df7..1a24a72b 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/Omni.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/Omni.kt
@@ -222,7 +222,7 @@ open class Omni internal constructor(internal var omniApi: OmniApi) {
* @param onDisconnected a block to run once finished. It will receive true if the reader was disconencted
* @param onFail a block to run if this operation fails. Receives an [OmniException]
*/
- fun disconnectReader(mobileReader: MobileReader, onDisconnected: (Boolean) -> Unit, onFail: (OmniException) -> Unit) {
+ fun disconnectReader(mobileReader: MobileReader?, onDisconnected: (Boolean) -> Unit, onFail: (OmniException) -> Unit) {
if (!initialized) {
onFail(OmniGeneralException.uninitialized)
return
@@ -234,7 +234,6 @@ open class Omni internal constructor(internal var omniApi: OmniApi) {
mobileReaderDriverRepository,
mobileReader
)
-
onDisconnected(job.start(onFail))
}
}
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/data/MobileReader.kt b/cardpresent/src/main/java/com/fattmerchant/omni/data/MobileReader.kt
index fdeb4e64..77dbdfc2 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/data/MobileReader.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/data/MobileReader.kt
@@ -1,5 +1,7 @@
package com.fattmerchant.omni.data
+import com.fattmerchant.android.chipdna.ConnectionType
+
/**
* A mobile reader that can take a payment
*/
@@ -18,4 +20,7 @@ interface MobileReader {
/** The serial number of the mobile reader */
fun serialNumber(): String?
+
+ /** The connection type of the mobile reader */
+ fun getConnectionType(): ConnectionType
}
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/data/MobileReaderDriver.kt b/cardpresent/src/main/java/com/fattmerchant/omni/data/MobileReaderDriver.kt
index ba415188..916ee3bf 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/data/MobileReaderDriver.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/data/MobileReaderDriver.kt
@@ -86,7 +86,7 @@ internal interface MobileReaderDriver {
* @param error a block to run if something goes wrong
* @return true if the reader was disconnected
*/
- suspend fun disconnect(reader: MobileReader, error: (OmniException) -> Unit): Boolean
+ suspend fun disconnect(reader: MobileReader?, error: (OmniException) -> Unit): Boolean
/**
* Gets the [MobileReader] that is currently connected and accessible via the receiver
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/data/TransactionResult.kt b/cardpresent/src/main/java/com/fattmerchant/omni/data/TransactionResult.kt
index d9561028..6b6d7f36 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/data/TransactionResult.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/data/TransactionResult.kt
@@ -1,7 +1,6 @@
package com.fattmerchant.omni.data
import com.fattmerchant.android.chipdna.ChipDnaDriver
-import com.fattmerchant.cpresent.BuildConfig
import com.fattmerchant.omni.data.models.Customer
import com.fattmerchant.omni.data.models.Invoice
import com.fattmerchant.omni.data.models.PaymentMethod
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/data/repository/MobileReaderDriverRepository.kt b/cardpresent/src/main/java/com/fattmerchant/omni/data/repository/MobileReaderDriverRepository.kt
index 7e002696..e699bda7 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/data/repository/MobileReaderDriverRepository.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/data/repository/MobileReaderDriverRepository.kt
@@ -32,5 +32,5 @@ internal interface MobileReaderDriverRepository {
* @param mobileReader
* @return
*/
- suspend fun getDriverFor(mobileReader: MobileReader): MobileReaderDriver?
+ suspend fun getDriverFor(mobileReader: MobileReader?): MobileReaderDriver?
}
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/networking/OmniApi.kt b/cardpresent/src/main/java/com/fattmerchant/omni/networking/OmniApi.kt
index 1533b2aa..de58de5f 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/networking/OmniApi.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/networking/OmniApi.kt
@@ -34,6 +34,7 @@ class OmniApi {
LIVE -> "https://apiprod.fattlabs.com/"
DEV -> "https://apidev.fattlabs.com/"
is QA -> "https://api-qa-${(environment as QA).qaBuildHash}.qabuilds.fattpay.com/"
+ else -> ""
}
private val httpClient = HttpClient {
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/usecase/DisconnectMobileReader.kt b/cardpresent/src/main/java/com/fattmerchant/omni/usecase/DisconnectMobileReader.kt
index ca072a0d..da59a497 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/usecase/DisconnectMobileReader.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/usecase/DisconnectMobileReader.kt
@@ -23,7 +23,7 @@ class DisconnectMobileReaderException(detail: String) : OmniException("Could not
internal class DisconnectMobileReader(
override val coroutineContext: CoroutineContext,
private var mobileReaderDriverRepository: MobileReaderDriverRepository,
- private var mobileReader: MobileReader
+ private var mobileReader: MobileReader?,
) : CoroutineScope {
/**
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/usecase/TakeMobileReaderPayment.kt b/cardpresent/src/main/java/com/fattmerchant/omni/usecase/TakeMobileReaderPayment.kt
index ec6a1529..dff06f1d 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/usecase/TakeMobileReaderPayment.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/usecase/TakeMobileReaderPayment.kt
@@ -163,7 +163,6 @@ internal class TakeMobileReaderPayment(
voidAndFail(it)
} ?: return@coroutineScope null
}
-
}
// Create a PaymentMethod
@@ -204,31 +203,7 @@ internal class TakeMobileReaderPayment(
voidAndFail(it)
} ?: return@coroutineScope null
- // If we the transaction is preauth, we don't need to capture it
- if (request.preauth) {
- return@coroutineScope createdTransaction
- }
-
- // If the transaction is not an NMI one, then we don't need to do the auth capture step
- if (!result.source.contains("NMI")) {
- return@coroutineScope createdTransaction
- }
-
- val successfullyCaptured = reader.capture(createdTransaction)
-
- if (successfullyCaptured) {
- return@coroutineScope createdTransaction
- } else {
- // Mark Stax transaction as failed
- createdTransaction.success = false
- createdTransaction.message = "Error capturing the transaction"
-
- // Fail the transaction in Stax
- transactionRepository.update(createdTransaction) { }
-
- voidAndFail(TakeMobileReaderPaymentException("Could not capture transaction"))
- return@coroutineScope null
- }
+ return@coroutineScope createdTransaction
}
/**
diff --git a/cardpresent/src/main/java/com/fattmerchant/omni/usecase/VoidTransaction.kt b/cardpresent/src/main/java/com/fattmerchant/omni/usecase/VoidTransaction.kt
index e06eef2f..485c5e52 100644
--- a/cardpresent/src/main/java/com/fattmerchant/omni/usecase/VoidTransaction.kt
+++ b/cardpresent/src/main/java/com/fattmerchant/omni/usecase/VoidTransaction.kt
@@ -15,7 +15,7 @@ class VoidTransaction(
suspend fun start(failure: (OmniException) -> Unit): Transaction? = coroutineScope {
/*
- As of 5/21/21, only NMI supports preauth and only NMI is offered to partners so we really
+ As of 5/21/21, only NMI supports pre-auth and only NMI is offered to partners so we really
shouldn't be hitting this code for anything except NMI. With that assumption, we can get
away with asking the Stax API to perform the capture for us
*/
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 5d0c8caa..6f74f062 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Apr 30 14:36:11 EDT 2021
+#Mon Aug 21 14:17:30 EDT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index 4c1c4eb2..00000000
--- a/settings.gradle
+++ /dev/null
@@ -1,2 +0,0 @@
-include ':tokenization', ':cardpresent', ':app'
-
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 00000000..fb98793c
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,22 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ maven(url = "https://jitpack.io")
+ maven(url = "https://plugins.gradle.org/m2/")
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven(url = "https://jitpack.io")
+ maven(url = "https://plugins.gradle.org/m2/")
+ }
+}
+rootProject.name = "Stax Android SDK"
+include(":tokenization")
+include(":cardpresent")
+include(":app")
\ No newline at end of file
diff --git a/tokenization/.gitignore b/tokenization/.gitignore
deleted file mode 100644
index 796b96d1..00000000
--- a/tokenization/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/tokenization/build.gradle b/tokenization/build.gradle
index c8056278..1a4bafec 100644
--- a/tokenization/build.gradle
+++ b/tokenization/build.gradle
@@ -6,6 +6,7 @@ plugins {
group = 'com.github.fattmerchantorg'
android {
+ namespace = "com.fattmerchant"
compileSdkVersion 33
defaultConfig {
minSdkVersion 23
@@ -18,8 +19,8 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
buildTypes {
@@ -37,6 +38,3 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation 'androidx.test:rules:1.5.0'
}
-repositories {
- mavenCentral()
-}
diff --git a/tokenization/src/main/AndroidManifest.xml b/tokenization/src/main/AndroidManifest.xml
index f8a8e424..1327573d 100644
--- a/tokenization/src/main/AndroidManifest.xml
+++ b/tokenization/src/main/AndroidManifest.xml
@@ -1,6 +1,4 @@
-
-
+