From 87d3be4ac2af8478ba4f2bf9001baa778224e5fc Mon Sep 17 00:00:00 2001 From: Thomas Vidas Date: Mon, 21 Aug 2023 16:35:27 -0400 Subject: [PATCH 1/9] feat: Update sample app, update to latest ChipDna SDK, remove misc files like DS_Store and legacy documentation files --- .gitignore | 12 +- .idea/compiler.xml | 6 - .idea/jarRepositories.xml | 35 -- .idea/misc.xml | 10 - .idea/vcs.xml | 6 - _layouts/default.html | 50 -- app/.gitignore | 1 - app/build.gradle | 59 -- app/build.gradle.kts | 88 +++ app/proguard-rules.pro | 2 +- app/src/main/AndroidManifest.xml | 39 +- .../fmsampleclient/ChipDnaHelper.kt | 316 ----------- .../fmsampleclient/MainActivity.kt | 523 ------------------ .../fattmerchant/fmsampleclient/OmniSample.kt | 11 - .../fmsampleclient/PermissionsManager.kt | 125 ----- .../com/staxpayments/sample/MainActivity.kt | 29 + .../staxpayments/sample/MainApplication.kt | 26 + .../sample}/SignatureProvider.kt | 4 +- .../staxpayments/sample/state/StaxUiState.kt | 9 + .../sample/ui/components/WideButton.kt | 56 ++ .../sample/ui/screens/MainScreen.kt | 156 ++++++ .../com/staxpayments/sample/ui/theme/Color.kt | 178 ++++++ .../com/staxpayments/sample/ui/theme/Theme.kt | 72 +++ .../com/staxpayments/sample/ui/theme/Type.kt | 126 +++++ .../sample/viewmodel/StaxViewModel.kt | 362 ++++++++++++ app/src/main/res/layout/activity_main.xml | 182 ------ app/src/main/res/values/styles.xml | 11 - app/src/main/res/values/themes.xml | 6 + assets/css/style.scss | 41 -- build.gradle | 37 -- build.gradle.kts | 5 + cardpresent/.DS_Store | Bin 8196 -> 8196 bytes cardpresent/.gitignore | 1 - cardpresent/.idea/gradle.xml | 16 - cardpresent/.idea/misc.xml | 6 - cardpresent/.idea/workspace.xml | 52 -- cardpresent/build.gradle | 21 +- cardpresent/libs/ChipDnaMobile.jar | Bin 851055 -> 864112 bytes cardpresent/src/main/AndroidManifest.xml | 7 +- .../android/chipdna/ChipDnaDriver.kt | 196 ++++--- .../android/chipdna/ChipDnaUtils.kt | 48 +- .../android/chipdna/TransactionGateway.kt | 2 +- .../fattmerchant/omni/data/MobileReader.kt | 5 + .../omni/data/TransactionResult.kt | 1 - .../omni/usecase/VoidTransaction.kt | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- settings.gradle | 2 - settings.gradle.kts | 22 + tokenization/.gitignore | 1 - tokenization/build.gradle | 4 +- 50 files changed, 1325 insertions(+), 1648 deletions(-) delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/jarRepositories.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/vcs.xml delete mode 100644 _layouts/default.html delete mode 100644 app/.gitignore delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts delete mode 100644 app/src/main/java/com/fattmerchant/fmsampleclient/ChipDnaHelper.kt delete mode 100644 app/src/main/java/com/fattmerchant/fmsampleclient/MainActivity.kt delete mode 100644 app/src/main/java/com/fattmerchant/fmsampleclient/OmniSample.kt delete mode 100644 app/src/main/java/com/fattmerchant/fmsampleclient/PermissionsManager.kt create mode 100644 app/src/main/java/com/staxpayments/sample/MainActivity.kt create mode 100644 app/src/main/java/com/staxpayments/sample/MainApplication.kt rename app/src/main/java/com/{fattmerchant/fmsampleclient => staxpayments/sample}/SignatureProvider.kt (82%) create mode 100644 app/src/main/java/com/staxpayments/sample/state/StaxUiState.kt create mode 100644 app/src/main/java/com/staxpayments/sample/ui/components/WideButton.kt create mode 100644 app/src/main/java/com/staxpayments/sample/ui/screens/MainScreen.kt create mode 100644 app/src/main/java/com/staxpayments/sample/ui/theme/Color.kt create mode 100644 app/src/main/java/com/staxpayments/sample/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/staxpayments/sample/ui/theme/Type.kt create mode 100644 app/src/main/java/com/staxpayments/sample/viewmodel/StaxViewModel.kt delete mode 100644 app/src/main/res/layout/activity_main.xml delete mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/themes.xml delete mode 100644 assets/css/style.scss delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 cardpresent/.gitignore delete mode 100644 cardpresent/.idea/gradle.xml delete mode 100644 cardpresent/.idea/misc.xml delete mode 100644 cardpresent/.idea/workspace.xml delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts delete mode 100644 tokenization/.gitignore 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..8c5c2528 --- /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") + +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/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..2690d9bd --- /dev/null +++ b/app/src/main/java/com/staxpayments/sample/viewmodel/StaxViewModel.kt @@ -0,0 +1,362 @@ +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 { + getConnectedReader( + onReaderFound = { reader -> + if (reader != null) { + disconnectReader( + mobileReader = reader, + onDisconnected = { log("Reader disconnected") }, + onFail = { log(it.toString()) } + ) + } else { + log("There is no connected reader") + } + }, + // Failed to get connected reader... + onFail = { + log(it.toString()) + } + ) + } + } + + fun onCancelTransaction() { + Omni.shared()?.cancelMobileReaderTransaction( + completion = { + log("Successfully canceled the transaction") + }, + 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - -