From 219ce40a4241f04c2ff7369cb8c339359fe69fd8 Mon Sep 17 00:00:00 2001 From: Devin Morgan Date: Thu, 2 May 2024 10:20:26 -0400 Subject: [PATCH] Drop all pos related code from public ecom repo We want the private pos repo and the public ecom repo to have the ability to evolve separately accordingly their unique needs while still sharing common code. The first step in this journey is to make the public ecom repo completely unaware of the existence of the private pos repo. We do that by deleting all of the pos code Signed-off-by: Devin Morgan pos related files to delete Signed-off-by: Devin Morgan Remove real sandbox creds from sample app Signed-off-by: Devin Morgan --- .../joinforage/forage/android/ForageSDK.kt | 15 - .../com/joinforage/forage/android/Utils.kt | 11 - .../android/network/TokenizeCardService.kt | 14 - .../network/data/CheckBalanceRepository.kt | 25 - .../data/DeferPaymentRefundRepository.kt | 55 -- .../forage/android/pos/ForageTerminalSDK.kt | 821 ------------------ .../forage/android/pos/PosMethodParams.kt | 98 --- .../pos/PosPaymentMethodRequestBody.kt | 22 - .../android/pos/PosRefundPaymentRepository.kt | 94 -- .../forage/android/pos/PosRefundService.kt | 51 -- .../android/pos/PosRefundVaultResponse.kt | 30 - .../android/pos/PosVaultRequestParams.kt | 45 - .../android/ui/AbstractForageElement.kt | 20 +- .../forage/android/ui/ForageElement.kt | 23 - .../forage/android/ui/ForagePANEditText.kt | 2 +- .../forage/android/ui/ForagePINEditText.kt | 27 +- .../forage/android/ui/ForageVaultWrapper.kt | 142 --- .../android/vault/AbstractVaultSubmitter.kt | 45 +- .../android/vault/ForagePinSubmitter.kt | 125 --- .../forage/android/vault/VgsPinSubmitter.kt | 2 +- .../fixtures/CreatePaymentMethodFixtures.kt | 12 - .../forage/android/mock/MockServerUtils.kt | 57 -- .../forage/android/mock/MockServiceFactory.kt | 27 - .../android/pos/ForageTerminalSDKTest.kt | 532 ------------ .../pos/PosRefundPaymentRepositoryTest.kt | 215 ----- .../android/example/MainActivity.kt | 1 - .../android/example/pos/k9sdk/K9SDK.kt | 102 --- .../example/pos/receipts/CPayPrinter.kt | 125 --- .../example/pos/receipts/ReceiptFormatting.kt | 16 - .../example/pos/receipts/ReceiptPrinter.kt | 18 - .../pos/receipts/primitives/ReceiptLayout.kt | 128 --- .../receipts/primitives/ReceiptLayoutLine.kt | 83 -- .../receipts/primitives/ReceiptLinePart.kt | 51 -- .../templates/BalanceInquiryReceipt.kt | 39 - .../receipts/templates/BaseReceiptTemplate.kt | 31 - .../templates/txs/CashPurchaseTxReceipt.kt | 24 - .../txs/CashPurchaseWithCashbackTxReceipt.kt | 26 - .../templates/txs/CashWithdrawalTxReceipt.kt | 22 - .../templates/txs/SnapPurchaseTxReceipt.kt | 24 - .../receipts/templates/txs/TxDataLayout.kt | 59 -- .../templates/txs/TxReceiptTemplate.kt | 53 -- .../pos/receipts/templates/txs/TxType.kt | 93 -- .../android/example/ui/pos/POSComposeApp.kt | 764 ---------------- .../android/example/ui/pos/POSFragment.kt | 32 - .../android/example/ui/pos/POSViewModel.kt | 460 ---------- .../example/ui/pos/data/BalanceCheck.kt | 9 - .../android/example/ui/pos/data/Merchant.kt | 21 - .../android/example/ui/pos/data/POSUIState.kt | 63 -- .../example/ui/pos/data/PosPaymentRequest.kt | 70 -- .../example/ui/pos/data/PosPaymentResponse.kt | 43 - .../android/example/ui/pos/data/PosReceipt.kt | 29 - .../android/example/ui/pos/data/Refund.kt | 38 - .../ui/pos/data/tokenize/PosBalance.kt | 15 - .../example/ui/pos/data/tokenize/PosCard.kt | 13 - .../ui/pos/data/tokenize/PosPaymentMethod.kt | 15 - .../data/tokenize/PosTerminalResponseField.kt | 12 - .../example/ui/pos/network/PosApiService.kt | 86 -- .../ui/pos/screens/ActionSelectionScreen.kt | 86 -- .../ui/pos/screens/MerchantSetupScreen.kt | 108 --- .../ui/pos/screens/ReceiptPreviewScreen.kt | 20 - .../screens/balance/BalanceResultScreen.kt | 70 -- .../DeferredPaymentCaptureResultScreen.kt | 106 --- .../DeferredPaymentRefundResultScreen.kt | 106 --- .../screens/payment/EBTCashPurchaseScreen.kt | 70 -- .../EBTCashPurchaseWithCashBackScreen.kt | 84 -- .../payment/EBTCashWithdrawalScreen.kt | 70 -- .../screens/payment/EBTSnapPurchaseScreen.kt | 70 -- .../screens/payment/PaymentResultScreen.kt | 134 --- .../payment/PaymentTypeSelectionScreen.kt | 74 -- .../pos/screens/refund/RefundDetailsScreen.kt | 90 -- .../pos/screens/refund/RefundResultScreen.kt | 168 ---- .../screens/shared/MagSwipePANEntryScreen.kt | 41 - .../screens/shared/ManualPANEntryScreen.kt | 64 -- .../shared/PANMethodSelectionScreen.kt | 53 -- .../ui/pos/screens/shared/PinEntryScreen.kt | 104 --- .../screens/voids/VoidPaymentResultScreen.kt | 48 - .../ui/pos/screens/voids/VoidPaymentScreen.kt | 65 -- .../screens/voids/VoidRefundResultScreen.kt | 53 -- .../ui/pos/screens/voids/VoidRefundScreen.kt | 77 -- .../screens/voids/VoidTypeSelectionScreen.kt | 47 - .../ui/pos/ui/ComposableForagePANEditText.kt | 32 - .../ui/pos/ui/ComposableForagePINEditText.kt | 32 - .../android/example/ui/pos/ui/ErrorText.kt | 25 - .../example/ui/pos/ui/PaymentRefView.kt | 29 - .../android/example/ui/pos/ui/ReceiptView.kt | 165 ---- .../example/ui/pos/ui/ScreenWithBottomRow.kt | 35 - .../src/main/res/layout/fragment_pos.xml | 24 - .../src/main/res/menu/bottom_nav_menu.xml | 5 - .../main/res/navigation/mobile_navigation.xml | 4 - sample-app/src/main/res/values/strings.xml | 30 - 90 files changed, 17 insertions(+), 7247 deletions(-) delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/network/data/DeferPaymentRefundRepository.kt delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/pos/ForageTerminalSDK.kt delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/pos/PosMethodParams.kt delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/pos/PosPaymentMethodRequestBody.kt delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundPaymentRepository.kt delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundService.kt delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundVaultResponse.kt delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/pos/PosVaultRequestParams.kt delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/ui/ForageVaultWrapper.kt delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/vault/ForagePinSubmitter.kt delete mode 100644 forage-android/src/test/java/com/joinforage/forage/android/pos/ForageTerminalSDKTest.kt delete mode 100644 forage-android/src/test/java/com/joinforage/forage/android/pos/PosRefundPaymentRepositoryTest.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/k9sdk/K9SDK.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/CPayPrinter.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/ReceiptFormatting.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/ReceiptPrinter.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLayout.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLayoutLine.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLinePart.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/BalanceInquiryReceipt.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/BaseReceiptTemplate.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashPurchaseTxReceipt.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashPurchaseWithCashbackTxReceipt.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashWithdrawalTxReceipt.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/SnapPurchaseTxReceipt.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxDataLayout.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxReceiptTemplate.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxType.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSComposeApp.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSFragment.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSViewModel.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/BalanceCheck.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/Merchant.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/POSUIState.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosPaymentRequest.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosPaymentResponse.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosReceipt.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/Refund.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosBalance.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosCard.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosPaymentMethod.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosTerminalResponseField.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/network/PosApiService.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/ActionSelectionScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/MerchantSetupScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/ReceiptPreviewScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/balance/BalanceResultScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/deferred/DeferredPaymentCaptureResultScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/deferred/DeferredPaymentRefundResultScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashPurchaseScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashPurchaseWithCashBackScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashWithdrawalScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTSnapPurchaseScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/PaymentResultScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/PaymentTypeSelectionScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/refund/RefundDetailsScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/refund/RefundResultScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/MagSwipePANEntryScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/ManualPANEntryScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/PANMethodSelectionScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/PinEntryScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidPaymentResultScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidPaymentScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidRefundResultScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidRefundScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidTypeSelectionScreen.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ComposableForagePANEditText.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ComposableForagePINEditText.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ErrorText.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/PaymentRefView.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ReceiptView.kt delete mode 100644 sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ScreenWithBottomRow.kt delete mode 100644 sample-app/src/main/res/layout/fragment_pos.xml diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ForageSDK.kt b/forage-android/src/main/java/com/joinforage/forage/android/ForageSDK.kt index 8e55bcbd5..c7f9a94d7 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ForageSDK.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ForageSDK.kt @@ -18,8 +18,6 @@ import com.joinforage.forage.android.network.data.CheckBalanceRepository import com.joinforage.forage.android.network.data.DeferPaymentCaptureRepository import com.joinforage.forage.android.network.data.DeferPaymentRefundRepository import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.pos.PosRefundPaymentRepository -import com.joinforage.forage.android.pos.PosRefundService import com.joinforage.forage.android.ui.AbstractForageElement import com.joinforage.forage.android.ui.ForageConfig import com.joinforage.forage.android.ui.ForagePINEditText @@ -447,7 +445,6 @@ class ForageSDK : ForageSDKInterface { private val paymentService by lazy { createPaymentService() } private val messageStatusService by lazy { createMessageStatusService() } private val pollingService by lazy { createPollingService() } - private val posRefundService by lazy { PosRefundService(config.apiBaseUrl, logger, okHttpClient) } open fun createTokenizeCardService() = TokenizeCardService( config.apiBaseUrl, @@ -494,18 +491,6 @@ class ForageSDK : ForageSDKInterface { ) } - open fun createRefundPaymentRepository(foragePinEditText: ForagePINEditText): PosRefundPaymentRepository { - return PosRefundPaymentRepository( - vaultSubmitter = createVaultSubmitter(foragePinEditText), - encryptionKeyService = encryptionKeyService, - paymentMethodService = paymentMethodService, - paymentService = paymentService, - pollingService = pollingService, - logger = logger, - refundService = posRefundService - ) - } - private fun createVaultSubmitter(foragePinEditText: ForagePINEditText) = AbstractVaultSubmitter.create( foragePinEditText = foragePinEditText, logger = logger diff --git a/forage-android/src/main/java/com/joinforage/forage/android/Utils.kt b/forage-android/src/main/java/com/joinforage/forage/android/Utils.kt index 0df3964f5..d59857adf 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/Utils.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/Utils.kt @@ -23,14 +23,3 @@ internal fun TypedArray.getBoxCornerRadius(styleIndex: Int, defaultBoxCornerRadi val styledBoxCornerRadius = getDimension(styleIndex, 0f) return if (styledBoxCornerRadius == 0f) defaultBoxCornerRadius else styledBoxCornerRadius } - -// This extension splits the path by "/" and adds each segment individually to the path. -// This is to prevent the URL from getting corrupted through internal OKHttp URL encoding. -internal fun HttpUrl.Builder.addPathSegmentsSafe(path: String): HttpUrl.Builder { - path.split("/").forEach { segment -> - if (segment.isNotEmpty()) { - this.addPathSegment(segment) - } - } - return this -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/network/TokenizeCardService.kt b/forage-android/src/main/java/com/joinforage/forage/android/network/TokenizeCardService.kt index ba9d269dc..76135c627 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/network/TokenizeCardService.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/network/TokenizeCardService.kt @@ -6,7 +6,6 @@ import com.joinforage.forage.android.network.model.ForageApiResponse import com.joinforage.forage.android.network.model.ForageError import com.joinforage.forage.android.network.model.PaymentMethodRequestBody import com.joinforage.forage.android.network.model.RequestBody -import com.joinforage.forage.android.pos.PosPaymentMethodRequestBody import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -39,19 +38,6 @@ internal class TokenizeCardService( ForageApiResponse.Failure(listOf(ForageError(500, "unknown_server_error", ex.message.orEmpty()))) } - suspend fun tokenizePosCard(track2Data: String, reusable: Boolean = true): ForageApiResponse = try { - logger.i("[POS] POST request for Payment Method with Track 2 data") - tokenizeCardCoroutine( - PosPaymentMethodRequestBody( - track2Data = track2Data, - reusable = reusable - ) - ) - } catch (ex: IOException) { - logger.e("[POS] Failed while tokenizing PaymentMethod", ex) - ForageApiResponse.Failure(listOf(ForageError(500, "unknown_server_error", ex.message.orEmpty()))) - } - private suspend fun tokenizeCardCoroutine(requestBody: RequestBody): ForageApiResponse { val url = getTokenizeCardUrl() val okHttpRequestBody = requestBody diff --git a/forage-android/src/main/java/com/joinforage/forage/android/network/data/CheckBalanceRepository.kt b/forage-android/src/main/java/com/joinforage/forage/android/network/data/CheckBalanceRepository.kt index 60f824f49..4d8c357db 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/network/data/CheckBalanceRepository.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/network/data/CheckBalanceRepository.kt @@ -9,7 +9,6 @@ import com.joinforage.forage.android.network.PaymentMethodService import com.joinforage.forage.android.network.PollingService import com.joinforage.forage.android.network.model.ForageApiResponse import com.joinforage.forage.android.network.model.Message -import com.joinforage.forage.android.pos.PosBalanceVaultSubmitterParams import com.joinforage.forage.android.vault.AbstractVaultSubmitter import com.joinforage.forage.android.vault.VaultSubmitter import com.joinforage.forage.android.vault.VaultSubmitterParams @@ -71,30 +70,6 @@ internal class CheckBalanceRepository( } } - suspend fun posCheckBalance( - merchantId: String, - paymentMethodRef: String, - posTerminalId: String, - sessionToken: String - ): ForageApiResponse { - return checkBalance( - merchantId = merchantId, - paymentMethodRef = paymentMethodRef, - sessionToken = sessionToken, - getVaultRequestParams = { encryptionKeys, paymentMethod -> - PosBalanceVaultSubmitterParams( - baseVaultSubmitterParams = buildVaultRequestParams( - merchantId = merchantId, - encryptionKeys = encryptionKeys, - paymentMethod = paymentMethod, - sessionToken = sessionToken - ), - posTerminalId = posTerminalId - ) - } - ) - } - private fun buildVaultRequestParams( merchantId: String, encryptionKeys: EncryptionKeys, diff --git a/forage-android/src/main/java/com/joinforage/forage/android/network/data/DeferPaymentRefundRepository.kt b/forage-android/src/main/java/com/joinforage/forage/android/network/data/DeferPaymentRefundRepository.kt deleted file mode 100644 index 9b0c7b1b5..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/network/data/DeferPaymentRefundRepository.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.joinforage.forage.android.network.data - -import com.joinforage.forage.android.core.telemetry.UserAction -import com.joinforage.forage.android.model.EncryptionKeys -import com.joinforage.forage.android.model.Payment -import com.joinforage.forage.android.model.PaymentMethod -import com.joinforage.forage.android.network.EncryptionKeyService -import com.joinforage.forage.android.network.PaymentMethodService -import com.joinforage.forage.android.network.PaymentService -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.vault.AbstractVaultSubmitter -import com.joinforage.forage.android.vault.VaultSubmitter -import com.joinforage.forage.android.vault.VaultSubmitterParams -import java.util.UUID - -internal class DeferPaymentRefundRepository( - private val vaultSubmitter: VaultSubmitter, - private val encryptionKeyService: EncryptionKeyService, - private val paymentMethodService: PaymentMethodService, - private val paymentService: PaymentService -) { - /** - * @return if successful, the response.data field is an empty string - */ - suspend fun deferPaymentRefund( - merchantId: String, - paymentRef: String, - sessionToken: String - ): ForageApiResponse { - val encryptionKeys = when (val response = encryptionKeyService.getEncryptionKey()) { - is ForageApiResponse.Success -> EncryptionKeys.ModelMapper.from(response.data) - else -> return response - } - val payment = when (val response = paymentService.getPayment(paymentRef)) { - is ForageApiResponse.Success -> Payment.ModelMapper.from(response.data) - else -> return response - } - val paymentMethod = when (val response = paymentMethodService.getPaymentMethod(payment.paymentMethod)) { - is ForageApiResponse.Success -> PaymentMethod.ModelMapper.from(response.data) - else -> return response - } - - return vaultSubmitter.submit( - VaultSubmitterParams( - encryptionKeys = encryptionKeys, - idempotencyKey = UUID.randomUUID().toString(), - merchantId = merchantId, - path = AbstractVaultSubmitter.deferPaymentRefundPath(paymentRef), - paymentMethod = paymentMethod, - userAction = UserAction.DEFER_REFUND, - sessionToken = sessionToken - ) - ) - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/pos/ForageTerminalSDK.kt b/forage-android/src/main/java/com/joinforage/forage/android/pos/ForageTerminalSDK.kt deleted file mode 100644 index 550fbf44e..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/pos/ForageTerminalSDK.kt +++ /dev/null @@ -1,821 +0,0 @@ -package com.joinforage.forage.android.pos - -import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi -import com.joinforage.forage.android.CapturePaymentParams -import com.joinforage.forage.android.CheckBalanceParams -import com.joinforage.forage.android.DeferPaymentCaptureParams -import com.joinforage.forage.android.ForageConfigNotSetException -import com.joinforage.forage.android.ForageSDK -import com.joinforage.forage.android.ForageSDKInterface -import com.joinforage.forage.android.TokenizeEBTCardParams -import com.joinforage.forage.android.VaultType -import com.joinforage.forage.android.core.telemetry.CustomerPerceivedResponseMonitor -import com.joinforage.forage.android.core.telemetry.Log -import com.joinforage.forage.android.core.telemetry.UserAction -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.network.model.ForageError -import com.joinforage.forage.android.ui.ForagePANEditText -import com.joinforage.forage.android.ui.ForagePINEditText - -/** - * The entry point for **in-store POS Terminal** transactions. - * - * A [ForageTerminalSDK] instance interacts with the Forage API. - * - * **You need to call [`ForageTerminalSDK.init`][init] to initialize the SDK.** - * Then you can perform operations like: - *

- * * [Tokenizing card information][tokenizeCard] - * * [Checking the balance of a card][checkBalance] - * * [Collecting a card PIN for a payment and - * deferring the capture of the payment to the server][deferPaymentCapture] - * * [Capturing a payment immediately][capturePayment] - * * [Collecting a customer's card PIN for a refund and defer the completion of the refund to the - * server][deferPaymentRefund] - * * [Refunding a payment immediately][refundPayment] - *

- * - *```kotlin - * // Example: Initialize the Forage Terminal SDK - * val forageTerminalSdk = ForageTerminalSDK.init( - * context = androidContext, - * posTerminalId = "", - * posForageConfig = PosForageConfig( - * merchantId = "mid/123ab45c67", - * sessionToken = "sandbox_ey123..." - * ) - * ) - * ``` - * - * @see * [Forage guide to Terminal POS integrations](https://docs.joinforage.app/docs/forage-terminal-android) - * * [ForageSDK] to process online-only transactions - */ -class ForageTerminalSDK internal constructor(private val posTerminalId: String) : - ForageSDKInterface { - private var createServiceFactory = { sessionToken: String, merchantId: String, logger: Log -> - ForageSDK.ServiceFactory( - sessionToken = sessionToken, - merchantId = merchantId, - logger = logger - ) - } - - private var forageSdk: ForageSDK = ForageSDK() - - companion object { - private var calledInit = false - private var initSucceeded = false - - /** - * A method that initializes the [ForageTerminalSDK]. - * - * **You must call [init] ahead of calling - * any other methods on a ForageTerminalSDK instance.** - * - * Forage may perform some long running initialization operations in - * certain circumstances. The operations typically last less than 10 seconds and only occur - * infrequently. - * - * ⚠️The [ForageTerminalSDK.init] method is only available in the private - * distribution of the Forage Terminal SDK. - * - *```kotlin - * // Example: Initialize the Forage Terminal SDK - * try { - * val forageTerminalSdk = ForageTerminalSDK.init( - * context = androidContext, - * posTerminalId = "", - * posForageConfig = PosForageConfig( - * merchantId = "mid/123ab45c67", - * sessionToken = "sandbox_ey123..." - * ) - * ) - * - * // Use the forageTerminalSdk to call other methods - * // (e.g. tokenizeCard, checkBalance, etc.) - * } catch (e: Exception) { - * // handle initialization error - * } - * ``` - * - * @throws Exception If the initialization fails. - * - * @param context **Required**. The Android application context. - * @param posTerminalId **Required**. A string that uniquely identifies the POS Terminal - * used for a transaction. The max length of the string is 255 characters. - * @param posForageConfig **Required**. A [PosForageConfig] instance that specifies a - * `merchantId` and `sessionToken`. - */ - @RequiresApi(Build.VERSION_CODES.M) - @Throws(Exception::class) - suspend fun init( - context: Context, - posTerminalId: String, - posForageConfig: PosForageConfig - ): ForageTerminalSDK { - if (posTerminalId == "pos-sample-app-override") { - return ForageTerminalSDK(posTerminalId) - } - throw NotImplementedError( - """ - This method is not implemented in the public distribution of the Forage Terminal SDK. - Use the private distribution of the Forage Terminal SDK to access this method. - """.trimIndent() - ) - } - - private var createLogger: (posTerminalId: String) -> Log = { posTerminalId -> - Log.getInstance().addAttribute("pos_terminal_id", posTerminalId) - } - } - - // internal constructor facilitates testing - internal constructor( - posTerminalId: String, - forageSdk: ForageSDK, - createLogger: (String) -> Log, - createServiceFactory: ((String, String, Log) -> ForageSDK.ServiceFactory)? = null, - initSucceeded: Boolean = false - ) : this(posTerminalId) { - this.forageSdk = forageSdk - ForageTerminalSDK.createLogger = createLogger - if (createServiceFactory != null) { - this.createServiceFactory = createServiceFactory - } - - if (initSucceeded) { - // STOPGAP to allow testing without depending on the init method. - ForageTerminalSDK.initSucceeded = initSucceeded - calledInit = initSucceeded - } - } - - /** - * Tokenizes a card via a [ForagePANEdit - * Text][com.joinforage.forage.android.ui.ForagePANEditText] Element. - * * On success, the object includes a `ref` token that represents an instance of a Forage - * [`PaymentMethod`](https://docs.joinforage.app/reference/payment-methods). You can store - * the token in your database and reference it for future transactions, like to call - * [checkBalance] or to [create a Payment](https://docs.joinforage.app/reference/create-a-payment) - * in Forage's database. *(Example [PosPaymentMethod](https://github.com/teamforage/forage-android-sdk/blob/229a0c7d38dcae751070aed45ff2f7e7ea2a5abb/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosPaymentMethod.kt#L7) class)* - * * On failure, for example in the case of [`unsupported_bin`](https://docs.joinforage.app/reference/errors#unsupported_bin), - * the response includes a list of [ForageError][com.joinforage.forage.android.network.model.ForageError] - * objects that you can unpack to programmatically handle the error and display the appropriate - * customer-facing message based on the `ForageError.code`. - * ```kotlin - * // Example tokenizeCard call in a TokenizeViewModel.kt - * class TokenizeViewMode : ViewModel() { - * val merchantId = "mid/" - * val sessionToken = "" - * - * fun tokenizeCard(foragePanEditText: ForagePANEditText) = viewModelScope.launch { - * val response = forageTerminalSdk.tokenizeCard( - * foragePanEditText = foragePanEditText, - * reusable = true - * ) - * - * when (response) { - * is ForageApiResponse.Success -> { - * // parse response.data for the PaymentMethod object - * } - * is ForageApiResponse.Failure -> { - * // do something with error text (i.e. response.message) - * } - * } - * } - * } - * ``` - * @param foragePanEditText **Required**. A reference to a [ForagePANEditText] instance that - * collects the customer's card number. - * [setPosForageConfig][com.joinforage.forage.android.ui.ForageElement.setPosForageConfig] must - * have been called on the instance before it can be passed. - * @param reusable Optional. A boolean that indicates whether the same card can be used to create - * multiple payments. Defaults to true. - * @throws ForageConfigNotSetException If the [PosForageConfig] is not set for the provided - * [ForagePANEditText] instance. - * - * @return A [ForageApiResponse] object. - */ - suspend fun tokenizeCard( - foragePanEditText: ForagePANEditText, - reusable: Boolean = true - ): ForageApiResponse { - val logger = createLogger(posTerminalId) - logger.addAttribute("reusable", reusable) - logger.i("[POS] Tokenizing Payment Method via UI PAN entry on Terminal $posTerminalId") - - val initializationException = isInitializationExceptionOrNull(logger, "tokenizeCard") - if (initializationException != null) { - return initializationException - } - - val tokenizationResponse = - forageSdk.tokenizeEBTCard( - TokenizeEBTCardParams( - foragePanEditText = foragePanEditText, - reusable = reusable - ) - ) - if (tokenizationResponse is ForageApiResponse.Failure) { - logger.e( - "[POS] tokenizeCard failed on Terminal $posTerminalId: ${tokenizationResponse.errors[0]}" - ) - } - return tokenizationResponse - } - - /** - * Tokenizes a card via a magnetic swipe from a physical POS Terminal. - * * On success, the object includes a `ref` token that represents an instance of a Forage - * [`PaymentMethod`](https://docs.joinforage.app/reference/payment-methods). You can store - * the token for future transactions, like to call [checkBalance] or to - * [create a Payment](https://docs.joinforage.app/reference/create-a-payment) in Forage's database. - * * On failure, for example in the case of [`unsupported_bin`](https://docs.joinforage.app/reference/errors#unsupported_bin), - * the response includes a list of [ForageError][com.joinforage.forage.android.network.model.ForageError] - * objects that you can unpack to programmatically handle the error and display the appropriate - * customer-facing message based on the `ForageError.code`. - * ```kotlin - * // Example tokenizeCard(PosTokenizeCardParams) call in a TokenizePosViewModel.kt - * class TokenizePosViewModel : ViewModel() { - * val merchantId = "mid/" - * val sessionToken = "" - * - * fun tokenizePosCard(foragePinEditText: ForagePINEditText) = viewModelScope.launch { - * val response = forageTerminalSdk.tokenizeCard( - * PosTokenizeCardParams( - * forageConfig = ForageConfig( - * merchantId = merchantId, - * sessionToken = sessionToken - * ), - * track2Data = "" // "123456789123456789=123456789123", - * // reusable = true - * ) - * ) - * - * when (response) { - * is ForageApiResponse.Success -> { - * // parse response.data for the PaymentMethod object - * } - * is ForageApiResponse.Failure -> { - * // do something with error text (i.e. response.message) - * } - * } - * } - * } - * ``` - * @param params **Required**. A [PosTokenizeCardParams] model that passes the [PosForageConfig], the card's - * `track2Data`, and a `reusable` boolean that Forage uses to tokenize the card. - * - * @throws ForageConfigNotSetException If the [PosForageConfig] is not set for the provided - * [ForagePANEditText] instance. - * - * @return A [ForageAPIResponse][com.joinforage.forage.android.network.model.ForageApiResponse] - * object. - */ - suspend fun tokenizeCard(params: PosTokenizeCardParams): ForageApiResponse { - val (posForageConfig, track2Data, reusable) = params - val logger = createLogger(posTerminalId) - logger.addAttribute("reusable", reusable) - .addAttribute("merchant_ref", posForageConfig.merchantId) - - logger.i( - "[POS] Tokenizing Payment Method using magnetic card swipe with Track 2 data on Terminal $posTerminalId" - ) - - val initializationException = isInitializationExceptionOrNull(logger, "tokenizeCard") - if (initializationException != null) { - return initializationException - } - - val (merchantId, sessionToken) = posForageConfig - val serviceFactory = createServiceFactory(sessionToken, merchantId, logger) - val tokenizeCardService = serviceFactory.createTokenizeCardService() - - return tokenizeCardService.tokenizePosCard(track2Data = track2Data, reusable = reusable) - } - - /** - * Checks the balance of a previously created - * [`PaymentMethod`](https://docs.joinforage.app/reference/payment-methods) - * via a [ForagePINEditText][com.joinforage.forage.android.ui.ForagePINEditText] Element. - * - * ⚠️ _FNS prohibits balance inquiries on sites and apps that offer guest checkout. Skip this - * method if your customers can opt for guest checkout. If guest checkout is not an option, then - * it's up to you whether or not to add a balance inquiry feature. No FNS regulations apply._ - * * On success, the response object includes `snap` and `cash` fields that indicate - * the EBT Card's current SNAP and EBT Cash balances. *(Example [BalanceCheck](https://github.com/Forage-PCI-CDE/android-pos-terminal-sdk/blob/main/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/BalanceCheck.kt) class)* - * * On failure, for example in the case of - * [`ebt_error_14`](https://docs.joinforage.app/reference/errors#ebt_error_14), - * the response includes a list of - * [ForageError][com.joinforage.forage.android.network.model.ForageError] objects that you can - * unpack to programmatically handle the error and display the appropriate - * customer-facing message based on the `ForageError.code`. - * ```kotlin - * // Example checkBalance call in a BalanceCheckViewModel.kt - * class BalanceCheckViewModel : ViewModel() { - * val paymentMethodRef = "020xlaldfh" - * - * fun checkBalance(foragePinEditText: ForagePINEditText) = viewModelScope.launch { - * val response = forageTerminalSdk.checkBalance( - * CheckBalanceParams( - * foragePinEditText = foragePinEditText, - * paymentMethodRef = paymentMethodRef - * ) - * ) - * - * when (response) { - * is ForageApiResponse.Success -> { - * // response.data will have a .snap and a .cash value - * } - * is ForageApiResponse.Failure -> { - * // do something with error text (i.e. response.message) - * } - * } - * } - * } - * ``` - * @param params A [CheckBalanceParams] model that passes - * a [`foragePinEditText`][com.joinforage.forage.android.ui.ForagePINEditText] instance and a - * `paymentMethodRef`, found in the response from a call to [tokenizeEBTCard] or the - * [Create a `PaymentMethod`](https://docs.joinforage.app/reference/create-payment-method) - * endpoint, that Forage uses to check the payment method's balance. - * - * @throws [ForageConfigNotSetException] If the [PosForageConfig] is not set for the provided - * `foragePinEditText`. - * @see * [SDK errors](https://docs.joinforage.app/reference/errors#sdk-errors) for more - * information on error handling. - * * [Test EBT Cards](https://docs.joinforage.app/docs/test-ebt-cards#balance-inquiry-exceptions) - * to trigger balance inquiry exceptions during testing. - * @return A [ForageApiResponse] object. - */ - override suspend fun checkBalance(params: CheckBalanceParams): ForageApiResponse { - val logger = createLogger(posTerminalId) - val (foragePinEditText, paymentMethodRef) = params - - val illegalVaultException = isIllegalVaultExceptionOrNull(foragePinEditText, logger) - if (illegalVaultException != null) { - return illegalVaultException - } - - val (merchantId, sessionToken) = forageSdk._getForageConfigOrThrow(foragePinEditText) - - logger.addAttribute("merchant_ref", merchantId) - .addAttribute("payment_method_ref", paymentMethodRef) - - logger.i( - "[POS] Called checkBalance for PaymentMethod $paymentMethodRef on Terminal $posTerminalId" - ) - - val initializationException = isInitializationExceptionOrNull(logger, "checkBalance") - if (initializationException != null) { - return initializationException - } - - // This block is used for tracking Metrics! - // ------------------------------------------------------ - val measurement = - CustomerPerceivedResponseMonitor.newMeasurement( - vault = foragePinEditText.getVaultType(), - vaultAction = UserAction.BALANCE, - logger - ) - measurement.start() - // ------------------------------------------------------ - - val serviceFactory = createServiceFactory(sessionToken, merchantId, logger) - val balanceCheckService = serviceFactory.createCheckBalanceRepository(foragePinEditText) - val balanceResponse = - balanceCheckService.posCheckBalance( - merchantId = merchantId, - paymentMethodRef = paymentMethodRef, - posTerminalId = posTerminalId, - sessionToken = sessionToken - ) - forageSdk.processApiResponseForMetrics(balanceResponse, measurement) - - if (balanceResponse is ForageApiResponse.Failure) { - logger.e( - "[POS] checkBalance failed for PaymentMethod $paymentMethodRef on Terminal $posTerminalId: ${balanceResponse.errors[0]}", - attributes = - mapOf( - "payment_method_ref" to paymentMethodRef, - "pos_terminal_id" to posTerminalId - ) - ) - } - - return balanceResponse - } - - /** - * Immediately captures a payment via a - * [ForagePINEditText][com.joinforage.forage.android.ui.ForagePINEditText] Element. - * - * * On success, the object confirms the transaction. The response includes a Forage - * [`Payment`](https://docs.joinforage.app/reference/payments) object. *(Example [PosPaymentResponse](https://github.com/Forage-PCI-CDE/android-pos-terminal-sdk/blob/main/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosPaymentResponse.kt#L8) and [Receipt](https://github.com/Forage-PCI-CDE/android-pos-terminal-sdk/blob/main/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosReceipt.kt) class)* - * * On failure, for example in the case of - * [`card_not_reusable`](https://docs.joinforage.app/reference/errors#card_not_reusable) or - * [`ebt_error_51`](https://docs.joinforage.app/reference/errors#ebt_error_51) errors, the - * response includes a list of - * [ForageError][com.joinforage.forage.android.network.model.ForageError] objects that you can - * unpack to programmatically handle the error and display the appropriate - * customer-facing message based on the `ForageError.code`. - * ```kotlin - * // Example capturePayment call in a PaymentCaptureViewModel.kt - * class PaymentCaptureViewModel : ViewModel() { - * val snapPaymentRef = "s0alzle0fal" - * val merchantId = "mid/" - * val sessionToken = "" - * - * fun capturePayment(foragePinEditText: ForagePINEditText, paymentRef: String) = - * viewModelScope.launch { - * val response = forageTerminalSdk.capturePayment( - * CapturePaymentParams( - * foragePinEditText = foragePinEditText, - * paymentRef = snapPaymentRef - * ) - * ) - * - * when (response) { - * is ForageApiResponse.Success -> { - * // handle successful capture - * } - * is ForageApiResponse.Failure -> { - * val error = response.errors[0] - * - * // handle Insufficient Funds error - * if (error.code == "ebt_error_51") { - * val details = error.details as ForageErrorDetails.EbtError51Details - * val (snapBalance, cashBalance) = details - * - * // do something with balances ... - * } - * } - * } - * } - * } - *``` - * @param params A [CapturePaymentParams] model that passes a - * [`foragePinEditText`][com.joinforage.forage.android.ui.ForagePINEditText] - * instance and a `paymentRef`, returned by the - * [Create a Payment](https://docs.joinforage.app/reference/create-a-payment) endpoint, that - * Forage uses to capture a payment. - * - * @throws [ForageConfigNotSetException] If the [PosForageConfig] is not set for the provided - * `foragePinEditText`. - * @see - * * [SDK errors](https://docs.joinforage.app/reference/errors#sdk-errors) for more information - * on error handling. - * * [Test EBT Cards](https://docs.joinforage.app/docs/test-ebt-cards#payment-capture-exceptions) - * to trigger payment capture exceptions during testing. - * @return A [ForageApiResponse] object. - */ - override suspend fun capturePayment(params: CapturePaymentParams): ForageApiResponse { - val (foragePinEditText, paymentRef) = params - val logger = createLogger(posTerminalId).addAttribute("payment_ref", paymentRef) - logger.i("[POS] Called capturePayment for Payment $paymentRef") - - val illegalVaultException = isIllegalVaultExceptionOrNull(foragePinEditText, logger) - if (illegalVaultException != null) { - return illegalVaultException - } - val initializationException = isInitializationExceptionOrNull(logger, "capturePayment") - if (initializationException != null) { - return initializationException - } - - val captureResponse = forageSdk.capturePayment(params) - - if (captureResponse is ForageApiResponse.Failure) { - logger.e( - "[POS] capturePayment failed for payment $paymentRef on Terminal $posTerminalId: ${captureResponse.errors[0]}" - ) - } - return captureResponse - } - - /** - * Submits a card PIN via a - * [ForagePINEditText][com.joinforage.forage.android.ui.ForagePINEditText] Element and defers - * payment capture to the server. - * - * * On success, the `data` property of the [ForageApiResponse.Success] object resolves with an empty string. - * * On failure, for example in the case of [`expired_session_token`](https://docs.joinforage.app/reference/errors#expired_session_token) errors, the - * response includes a list of - * [ForageError][com.joinforage.forage.android.network.model.ForageError] objects that you can - * unpack to programmatically handle the error and display the appropriate - * customer-facing message based on the `ForageError.code`. - * ```kotlin - * // Example deferPaymentCapture call in a DeferPaymentCaptureViewModel.kt - * class DeferPaymentCaptureViewModel : ViewModel() { - * val snapPaymentRef = "s0alzle0fal" - * val merchantId = "mid/" - * val sessionToken = "" - * - * fun deferPaymentCapture(foragePinEditText: ForagePINEditText, paymentRef: String) = - * viewModelScope.launch { - * val response = forageTerminalSdk.deferPaymentCapture( - * DeferPaymentCaptureParams( - * foragePinEditText = foragePinEditText, - * paymentRef = snapPaymentRef - * ) - * ) - * - * when (response) { - * is ForageApiResponse.Success -> { - * // there will be no financial affects upon success - * // you need to capture from the server to formally - * // capture the payment - * } - * is ForageApiResponse.Failure -> { - * // handle an error response here - * } - * } - * } - * } - * ``` - * @param params A [DeferPaymentCaptureParams] model that passes a - * [`foragePinEditText`][com.joinforage.forage.android.ui.ForagePINEditText] instance and a - * `paymentRef`, returned by the - * [Create a Payment](https://docs.joinforage.app/reference/create-a-payment) endpoint, as the - * DeferPaymentCaptureParams. - * - * @throws [ForageConfigNotSetException] If the [PosForageConfig] is not set for the provided - * `foragePinEditText`. - * @see * [Defer EBT payment capture and refund completion to the server](https://docs.joinforage.app/docs/capture-ebt-payments-server-side) - * for the related step-by-step guide. - * * [Capture an EBT Payment](https://docs.joinforage.app/reference/capture-a-payment) - * for the API endpoint to call after [deferPaymentCapture]. - * * [SDK errors](https://docs.joinforage.app/reference/errors#sdk-errors) for more information - * on error handling. - * @return A [ForageApiResponse] object. - */ - override suspend fun deferPaymentCapture( - params: DeferPaymentCaptureParams - ): ForageApiResponse { - val (foragePinEditText, paymentRef) = params - val logger = createLogger(posTerminalId).addAttribute("payment_ref", paymentRef) - logger.i("[POS] Called deferPaymentCapture for Payment $paymentRef") - - val illegalVaultException = isIllegalVaultExceptionOrNull(foragePinEditText, logger) - if (illegalVaultException != null) { - return illegalVaultException - } - - val deferCaptureResponse = forageSdk.deferPaymentCapture(params) - - if (deferCaptureResponse is ForageApiResponse.Failure) { - logger.e( - "[POS] deferPaymentCapture failed for Payment $paymentRef on Terminal $posTerminalId: ${deferCaptureResponse.errors[0]}" - ) - } - return deferCaptureResponse - } - - /** - * Refunds a Payment via a [ForagePinEditText][com.joinforage.forage.android.ui.ForagePINEditText] - * Element. This method is only available for POS Terminal transactions. - * You must use [ForageTerminalSDK]. - * - * * On success, the response includes a Forage - * [`PaymentRefund`](https://docs.joinforage.app/reference/create-payment-refund) object. *(Example [Refund](https://github.com/Forage-PCI-CDE/android-pos-terminal-sdk/blob/0d845ea57d901bbca13775f4f2de4d4ed6f74791/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/Refund.kt#L7-L23) and [Receipt](https://github.com/Forage-PCI-CDE/android-pos-terminal-sdk/blob/main/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosReceipt.kt) class)* - * * On failure, for example in the case of - * [`ebt_error_61`](https://docs.joinforage.app/reference/errors#ebt_error_61), the response - * includes a list of [ForageError] objects. You can unpack the list to programmatically handle - * the error and display the appropriate customer-facing message based on the `ForageError.code`. - * ```kotlin - * // Example refundPayment call in a PosRefundViewModel.kt - * class PosRefundViewModel : ViewModel() { - * var paymentRef: String = "" - * var amount: Float = 0.0 - * var reason: String = "" - * var metadata: HashMap? = null - * - * fun refundPayment(foragePinEditText: ForagePINEditText) = viewModelScope.launch { - * val forageTerminalSdk = ForageTerminalSDK.init(...) // may throw! - * val refundParams = PosRefundPaymentParams( - * foragePinEditText, - * paymentRef, - * amount, - * reason, - * metadata, - * ) - * val response = forage.refundPayment(refundParams) - * - * when (response) { - * is ForageApiResponse.Success -> { - * // do something with response.data - * } - * is ForageApiResponse.Failure -> { - * // do something with response.errors - * } - * } - * } - * } - * ``` - * @param params A [PosRefundPaymentParams] model that passes a - * [`foragePinEditText`][com.joinforage.forage.android.ui.ForagePINEditText] instance, a - * `paymentRef`, returned by the [Create a Payment](https://docs.joinforage.app/reference/create-a-payment) - * endpoint, an `amount`, and a `reason` as the PosRefundPaymentParams. - * @throws ForageConfigNotSetException If the [PosForageConfig] is not set for the provided - * `foragePinEditText`. - * @see * [SDK errors](https://docs.joinforage.app/reference/errors#sdk-errors) for more information - * on error handling. - * @return A [ForageApiResponse] object. - */ - suspend fun refundPayment(params: PosRefundPaymentParams): ForageApiResponse { - val logger = createLogger(posTerminalId) - val (foragePinEditText, paymentRef, amount, reason) = params - val (merchantId, sessionToken) = forageSdk._getForageConfigOrThrow(foragePinEditText) - - val illegalVaultException = isIllegalVaultExceptionOrNull(foragePinEditText, logger) - if (illegalVaultException != null) { - return illegalVaultException - } - - logger.addAttribute("payment_ref", paymentRef).addAttribute("merchant_ref", merchantId) - logger.i( - """ - [POS] Called refundPayment for Payment $paymentRef - with amount: $amount - for reason: $reason - on Terminal: $posTerminalId - """.trimIndent() - ) - - val initializationException = isInitializationExceptionOrNull(logger, "refundPayment") - if (initializationException != null) { - return initializationException - } - - // This block is used for tracking Metrics! - // ------------------------------------------------------ - val measurement = - CustomerPerceivedResponseMonitor.newMeasurement( - vault = foragePinEditText.getVaultType(), - vaultAction = UserAction.REFUND, - logger - ) - measurement.start() - // ------------------------------------------------------ - - val serviceFactory = createServiceFactory(sessionToken, merchantId, logger) - val refundService = serviceFactory.createRefundPaymentRepository(foragePinEditText) - val refund = - refundService.refundPayment( - merchantId = merchantId, - posTerminalId = posTerminalId, - refundParams = params, - sessionToken = sessionToken - ) - forageSdk.processApiResponseForMetrics(refund, measurement) - - if (refund is ForageApiResponse.Failure) { - logger.e( - "[POS] refundPayment failed for Payment $paymentRef on Terminal $posTerminalId: ${refund.errors[0]}" - ) - } - - return refund - } - - /** - * Collects a card PIN for an EBT payment and defers - * the refund of the payment to the server. - * * On success, the `data` property of the [ForageApiResponse.Success] object resolves with an empty string. - * * On failure, the response includes a list of - * [ForageError][com.joinforage.forage.android.network.model.ForageError] objects that you can - * unpack to troubleshoot the issue. - * ```kotlin - * // Example deferPaymentRefund call in a PosDeferPaymentRefundViewModel.kt - * class PosDeferPaymentRefundViewModel : ViewModel() { - * var paymentRef: String = "" - * - * fun deferPaymentRefund(foragePinEditText: ForagePINEditText) = viewModelScope.launch { - * val forageTerminalSdk = ForageTerminalSDK.init(...) // may throw! - * val deferPaymentRefundParams = PosDeferPaymentRefundParams( - * foragePinEditText, - * paymentRef - * ) - * val response = forage.deferPaymentRefund(deferPaymentRefundParams) - * - * when (response) { - * is ForageApiResponse.Success -> { - * // do something with response.data - * } - * is ForageApiResponse.Failure -> { - * // do something with response.errors - * } - * } - * } - * } - * ``` - * @param params The [PosRefundPaymentParams] parameters required for refunding a Payment. - * @return A [ForageAPIResponse][com.joinforage.forage.android.network.model.ForageApiResponse] - * indicating the success or failure of the - * secure PIN submission. - * @see * [Defer EBT payment capture and refund completion to the server](https://docs.joinforage.app/docs/capture-ebt-payments-server-side) - * for the related step-by-step guide. - * @throws ForageConfigNotSetException If the passed ForagePINEditText instance - * hasn't had its ForageConfig set via .setForageConfig(). - */ - suspend fun deferPaymentRefund(params: PosDeferPaymentRefundParams): ForageApiResponse { - val logger = createLogger(posTerminalId) - val (foragePinEditText, paymentRef) = params - val (merchantId, sessionToken) = forageSdk._getForageConfigOrThrow(foragePinEditText) - - val illegalVaultException = isIllegalVaultExceptionOrNull(foragePinEditText, logger) - if (illegalVaultException != null) { - return illegalVaultException - } - - logger.addAttribute("payment_ref", paymentRef).addAttribute("merchant_ref", merchantId) - logger.i( - """ - [POS] Called deferPaymentRefund for Payment $paymentRef - on Terminal: $posTerminalId - """.trimIndent() - ) - - val initializationException = isInitializationExceptionOrNull(logger, "deferPaymentRefund") - if (initializationException != null) { - return initializationException - } - - // This block is used for tracking Metrics! - // ------------------------------------------------------ - val measurement = - CustomerPerceivedResponseMonitor.newMeasurement( - vault = foragePinEditText.getVaultType(), - vaultAction = UserAction.DEFER_REFUND, - logger - ) - measurement.start() - // ------------------------------------------------------ - - val serviceFactory = createServiceFactory(sessionToken, merchantId, logger) - val refundService = serviceFactory.createDeferPaymentRefundRepository(foragePinEditText) - val refund = - refundService.deferPaymentRefund( - merchantId = merchantId, - paymentRef = paymentRef, - sessionToken = sessionToken - ) - forageSdk.processApiResponseForMetrics(refund, measurement) - - if (refund is ForageApiResponse.Failure) { - logger.e( - "[POS] deferPaymentRefund failed for Payment $paymentRef on Terminal $posTerminalId: ${refund.errors[0]}" - ) - } - - return refund - } - - private fun isIllegalVaultExceptionOrNull( - foragePinEditText: ForagePINEditText, - logger: Log - ): ForageApiResponse? { - if (foragePinEditText.getVaultType() != VaultType.FORAGE_VAULT_TYPE) { - logger.e("[POS] checkBalance failed on Terminal $posTerminalId because the vault type is not forage") - return ForageApiResponse.Failure.fromError( - ForageError( - code = "invalid_input_data", - message = "IllegalStateException: Use ForageElement.setPosForageConfig, instead of ForageElement.setForageConfig.", - httpStatusCode = 400 - ) - ) - } - return null - } - - private fun isInitializationExceptionOrNull( - logger: Log, - methodName: String - ): ForageApiResponse? { - // The public distribution of the Forage Terminal SDK does not have an init method. - // So we always return null here! - return null - } - - /** - * Use one of the [tokenizeCard] options instead. - * - * @throws NotImplementedError - */ - @Deprecated( - message = - "This method is not applicable to the Forage Terminal SDK. Use the other tokenizeEBTCard methods.", - level = DeprecationLevel.ERROR - ) - override suspend fun tokenizeEBTCard(params: TokenizeEBTCardParams): ForageApiResponse { - throw NotImplementedError( - """ - This method is not applicable to the Forage Terminal SDK. - Use the other tokenizeEBTCard methods. - """.trimIndent() - ) - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosMethodParams.kt b/forage-android/src/main/java/com/joinforage/forage/android/pos/PosMethodParams.kt deleted file mode 100644 index ca71a4a9b..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosMethodParams.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.joinforage.forage.android.pos - -import com.joinforage.forage.android.ui.ForageElement -import com.joinforage.forage.android.ui.ForagePINEditText - -/** - * **[PosForageConfig] is only valid for in-store POS Terminal transactions via [ForageTerminalSDK].** - * - * The configuration details that Forage needs to create a functional [ForageElement]. - * - * Pass a [PosForageConfig] instance in a call to - * [setPosForageConfig][com.joinforage.forage.android.ui.ForageElement.setPosForageConfig] to - * configure an Element. - * [PosForageConfig] is also passed as a parameter to [PosTokenizeCardParams]. - * - * @property merchantId A unique Merchant ID that Forage provides during onboarding - * preceded by "mid/". - * For example, `mid/123ab45c67`. The Merchant ID can be found in the Forage - * [Sandbox](https://dashboard.sandbox.joinforage.app/login/) - * or [Production](https://dashboard.joinforage.app/login/) Dashboard. - * - * @property sessionToken A short-lived token that authenticates front-end requests to Forage. - * To create one, send a server-side `POST` request from your backend to the - * [`/session_token/`](https://docs.joinforage.app/reference/create-session-token) endpoint. - * - * @constructor Creates an instance of the [PosForageConfig] data class. - */ -data class PosForageConfig( - val merchantId: String, - val sessionToken: String -) - -/** - * A model that represents the parameters that [ForageTerminalSDK] requires to tokenize a card via - * a magnetic swipe from a physical POS Terminal. - * This data class is not supported for online-only transactions. - * [PosTokenizeCardParams] are passed to the - * [tokenizeCard][com.joinforage.forage.android.pos.ForageTerminalSDK.tokenizeCard] method. - * - * @property posForageConfig **Required**. The [PosForageConfig] configuration details required to - * authenticate with the Forage API. - * @property track2Data **Required**. The information encoded on Track 2 of the card’s magnetic - * stripe, excluding the start and stop sentinels and any LRC characters. _Example value_: - * `"123456789123456789=123456789123"` - * @property reusable Optional. A boolean that indicates whether the same card can be used to create - * multiple payments. Defaults to true. - */ -data class PosTokenizeCardParams( - val posForageConfig: PosForageConfig, - val track2Data: String, - val reusable: Boolean = true -) - -/** - * A model that represents the parameters that Forage requires to collect a card PIN and defer - * the refund of the payment to the server. - * [PosDeferPaymentRefundParams] are passed to the - * [deferPaymentRefund][com.joinforage.forage.android.pos.ForageTerminalSDK.deferPaymentRefund] method. - * - * @property foragePinEditText A reference to a [ForagePINEditText] instance. - * [setPosForageConfig][com.joinforage.forage.android.ui.ForageElement.setPosForageConfig] must - * be called on the instance before it can be passed. - * @property paymentRef A unique string identifier for a previously created - * [`Payment`](https://docs.joinforage.app/reference/payments) in Forage's - * database, returned by the - * [Create a `Payment`](https://docs.joinforage.app/reference/create-a-payment) endpoint. - */ -data class PosDeferPaymentRefundParams( - val foragePinEditText: ForagePINEditText, - val paymentRef: String -) - -/** - * A model that represents the parameters that [ForageTerminalSDK] requires to refund a Payment. - * [PosRefundPaymentParams] are passed to the - * [refundPayment][com.joinforage.forage.android.pos.ForageTerminalSDK.refundPayment] method. - * - * @property foragePinEditText **Required**. A reference to the [ForagePINEditText] instance that collected - * the card PIN for the refund. - * [setForageConfig][com.joinforage.forage.android.ui.ForageElement.setForageConfig] must be - * called on the instance before it can be passed. - * @property paymentRef **Required**. A unique string identifier for a previously created - * [`Payment`](https://docs.joinforage.app/reference/payments) in Forage's database, returned by the - * [Create a `Payment`](https://docs.joinforage.app/reference/create-a-payment) endpoint. - * @property amount **Required**. A positive decimal number that represents how much of the original - * payment to refund in USD. Precision to the penny is supported. - * The minimum amount that can be refunded is `0.01`. - * @property reason **Required**. A string that describes why the payment is to be refunded. - * @property metadata Optional. A map of merchant-defined key-value pairs. For example, some - * merchants attach their credit card processor’s ID for the customer making the refund. - */ -data class PosRefundPaymentParams( - val foragePinEditText: ForagePINEditText, - val paymentRef: String, - val amount: Float, - val reason: String, - val metadata: Map? = null -) diff --git a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosPaymentMethodRequestBody.kt b/forage-android/src/main/java/com/joinforage/forage/android/pos/PosPaymentMethodRequestBody.kt deleted file mode 100644 index cbc0800f1..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosPaymentMethodRequestBody.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.joinforage.forage.android.pos - -import com.joinforage.forage.android.network.model.RequestBody -import org.json.JSONObject - -internal data class PosPaymentMethodRequestBody( - val track2Data: String, - val type: String = "ebt", - val reusable: Boolean = true -) : RequestBody { - override fun toJSONObject(): JSONObject { - val cardObject = JSONObject() - cardObject.put("track_2_data", track2Data) - - val rootObject = JSONObject() - rootObject.put("card", cardObject) - rootObject.put("type", type) - rootObject.put("reusable", reusable) - - return rootObject - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundPaymentRepository.kt b/forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundPaymentRepository.kt deleted file mode 100644 index 32250714f..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundPaymentRepository.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.joinforage.forage.android.pos - -import com.joinforage.forage.android.core.telemetry.Log -import com.joinforage.forage.android.core.telemetry.UserAction -import com.joinforage.forage.android.model.EncryptionKeys -import com.joinforage.forage.android.model.Payment -import com.joinforage.forage.android.model.PaymentMethod -import com.joinforage.forage.android.network.EncryptionKeyService -import com.joinforage.forage.android.network.PaymentMethodService -import com.joinforage.forage.android.network.PaymentService -import com.joinforage.forage.android.network.PollingService -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.network.model.UnknownErrorApiResponse -import com.joinforage.forage.android.vault.AbstractVaultSubmitter -import com.joinforage.forage.android.vault.VaultSubmitter -import com.joinforage.forage.android.vault.VaultSubmitterParams - -internal class PosRefundPaymentRepository( - private val vaultSubmitter: VaultSubmitter, - private val encryptionKeyService: EncryptionKeyService, - private val paymentMethodService: PaymentMethodService, - private val paymentService: PaymentService, - private val pollingService: PollingService, - private val refundService: PosRefundService, - private val logger: Log -) { - /** - * @return the ForageAPIResponse containing the Refund object on success or an error on failure. - */ - suspend fun refundPayment( - merchantId: String, - posTerminalId: String, - refundParams: PosRefundPaymentParams, - sessionToken: String - ): ForageApiResponse { - try { - val paymentRef = refundParams.paymentRef - - val encryptionKeys = when (val response = encryptionKeyService.getEncryptionKey()) { - is ForageApiResponse.Success -> EncryptionKeys.ModelMapper.from(response.data) - else -> return response - } - val payment = when (val response = paymentService.getPayment(paymentRef)) { - is ForageApiResponse.Success -> Payment.ModelMapper.from(response.data) - else -> return response - } - val paymentMethod = when (val response = paymentMethodService.getPaymentMethod(payment.paymentMethod)) { - is ForageApiResponse.Success -> PaymentMethod.ModelMapper.from(response.data) - else -> return response - } - - val vaultResponse = when ( - val response = vaultSubmitter.submit( - params = PosRefundVaultSubmitterParams( - baseVaultSubmitterParams = VaultSubmitterParams( - encryptionKeys = encryptionKeys, - idempotencyKey = paymentRef, - merchantId = merchantId, - path = AbstractVaultSubmitter.refundPaymentPath(paymentRef), - paymentMethod = paymentMethod, - userAction = UserAction.REFUND, - sessionToken = sessionToken - ), - posTerminalId = posTerminalId, - refundParams = refundParams - ) - ) - ) { - is ForageApiResponse.Success -> PosRefundVaultResponse.ModelMapper.from(response.data) - else -> return response - } - - val pollingResponse = pollingService.execute( - contentId = vaultResponse.message.contentId, - operationDescription = "refund of Payment $payment" - ) - if (pollingResponse is ForageApiResponse.Failure) { - return pollingResponse - } - - val refundRef = vaultResponse.refundRef - return when (val refundResponse = refundService.getRefund(paymentRef, refundRef)) { - is ForageApiResponse.Success -> { - logger.i("[HTTP] Received updated Refund $refundRef for Payment $paymentRef") - return refundResponse - } - else -> refundResponse - } - } catch (err: Exception) { - logger.e("Failed to refund Payment ${refundParams.paymentRef}", err) - return UnknownErrorApiResponse - } - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundService.kt b/forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundService.kt deleted file mode 100644 index 0a3efdaf3..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundService.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.joinforage.forage.android.pos - -import com.joinforage.forage.android.addTrailingSlash -import com.joinforage.forage.android.core.telemetry.Log -import com.joinforage.forage.android.network.ForageConstants -import com.joinforage.forage.android.network.NetworkService -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.network.model.UnknownErrorApiResponse -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import java.io.IOException - -// For POS in-store transactions only. -internal class PosRefundService( - private val httpUrl: String, - private val logger: Log, - okHttpClient: OkHttpClient -) : NetworkService(okHttpClient, logger) { - suspend fun getRefund(paymentRef: String, refundRef: String): ForageApiResponse = try { - logger.addAttribute("refund_ref", refundRef) - logger.i("[HTTP] GET Refund $refundRef for Payment $paymentRef") - getRefundToCoroutine(paymentRef, refundRef) - } catch (ex: IOException) { - logger.e("[HTTP] Failed while trying to GET Refund $refundRef for Payment $paymentRef", ex) - UnknownErrorApiResponse - } - - private suspend fun getRefundToCoroutine(paymentRef: String, refundRef: String): ForageApiResponse { - val url = getRefundForPaymentUrl(paymentRef, refundRef) - - val request: Request = Request.Builder() - .url(url) - .header(ForageConstants.Headers.API_VERSION, "2023-05-15") - .get() - .build() - - return convertCallbackToCoroutine(request) - } - - private fun getRefundForPaymentUrl(paymentRef: String, refundRef: String): HttpUrl = httpUrl.toHttpUrlOrNull()!! - .newBuilder() - .addPathSegment(ForageConstants.PathSegment.API) - .addPathSegment(ForageConstants.PathSegment.PAYMENTS) - .addPathSegment(paymentRef) - .addPathSegment(ForageConstants.PathSegment.REFUNDS) - .addPathSegment(refundRef) - .addTrailingSlash() - .build() -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundVaultResponse.kt b/forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundVaultResponse.kt deleted file mode 100644 index a20767dbd..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosRefundVaultResponse.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.joinforage.forage.android.pos - -import com.joinforage.forage.android.network.model.Message -import org.json.JSONObject - -/** - * Shape of response from the vault proxy when refunding a payment. - * @property message the SQS Message. - * @property refundRef the reference string to the refund that was created. - */ -internal data class PosRefundVaultResponse( - val message: Message, - val refundRef: String -) { - object ModelMapper { - fun from(string: String): PosRefundVaultResponse { - val jsonObject = JSONObject(string) - - val messageJsonObject = jsonObject.getJSONObject("message") - - val message = Message.ModelMapper.from(messageJsonObject.toString()) - val refundRef = jsonObject.getString("ref") - - return PosRefundVaultResponse( - message = message, - refundRef = refundRef - ) - } - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosVaultRequestParams.kt b/forage-android/src/main/java/com/joinforage/forage/android/pos/PosVaultRequestParams.kt deleted file mode 100644 index 19c811922..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/pos/PosVaultRequestParams.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.joinforage.forage.android.pos - -import com.joinforage.forage.android.network.data.BaseVaultRequestParams -import com.joinforage.forage.android.vault.VaultSubmitterParams - -internal data class PosVaultRequestParams( - override val cardNumberToken: String, - override val encryptionKey: String, - val posTerminalId: String -) : BaseVaultRequestParams(cardNumberToken, encryptionKey) { - override fun equals(other: Any?): Boolean { - return super.equals(other) && other is PosVaultRequestParams && posTerminalId == other.posTerminalId - } - - override fun hashCode(): Int { - return super.hashCode() + posTerminalId.hashCode() - } -} - -internal data class PosBalanceVaultSubmitterParams( - val baseVaultSubmitterParams: VaultSubmitterParams, - val posTerminalId: String -) : VaultSubmitterParams( - encryptionKeys = baseVaultSubmitterParams.encryptionKeys, - idempotencyKey = baseVaultSubmitterParams.idempotencyKey, - merchantId = baseVaultSubmitterParams.merchantId, - path = baseVaultSubmitterParams.path, - paymentMethod = baseVaultSubmitterParams.paymentMethod, - userAction = baseVaultSubmitterParams.userAction, - sessionToken = baseVaultSubmitterParams.sessionToken -) - -internal data class PosRefundVaultSubmitterParams( - val baseVaultSubmitterParams: VaultSubmitterParams, - val posTerminalId: String, - val refundParams: PosRefundPaymentParams -) : VaultSubmitterParams( - encryptionKeys = baseVaultSubmitterParams.encryptionKeys, - idempotencyKey = baseVaultSubmitterParams.idempotencyKey, - merchantId = baseVaultSubmitterParams.merchantId, - path = baseVaultSubmitterParams.path, - paymentMethod = baseVaultSubmitterParams.paymentMethod, - userAction = baseVaultSubmitterParams.userAction, - sessionToken = baseVaultSubmitterParams.sessionToken -) diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ui/AbstractForageElement.kt b/forage-android/src/main/java/com/joinforage/forage/android/ui/AbstractForageElement.kt index 1a70e6f68..fd9b7ade0 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ui/AbstractForageElement.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ui/AbstractForageElement.kt @@ -5,7 +5,6 @@ import android.util.AttributeSet import android.widget.LinearLayout import com.joinforage.forage.android.core.StopgapGlobalState import com.joinforage.forage.android.core.element.state.ElementState -import com.joinforage.forage.android.pos.PosForageConfig /** * ⚠️ Forage developers use this class to manage common attributes across [ForageElement] types. @@ -24,14 +23,9 @@ abstract class AbstractForageElement( // setForageConfig is called. Side effect include // initializing logger module, feature flag module, // and view UI manipulation logic - protected abstract fun initWithForageConfig(forageConfig: ForageConfig, isPos: Boolean) + protected abstract fun initWithForageConfig(forageConfig: ForageConfig) override fun setForageConfig(forageConfig: ForageConfig) { - commonInitializer(forageConfig, false) - } - - // common initializer for both setForageConfig and setPosForageConfig - private fun commonInitializer(forageConfig: ForageConfig, isPos: Boolean) { // keep a record of whether this was the first time // setForageConfig is getting called. we'll use // this info later @@ -51,23 +45,13 @@ abstract class AbstractForageElement( // operations on any subsequent calls to setForageConfig // or else that could crash the app. if (isFirstCallToSet) { - initWithForageConfig(forageConfig, isPos) + initWithForageConfig(forageConfig) } else { // TODO: possible opportunity to log that // they tried to do sessionToken refreshing } } - override fun setPosForageConfig(posForageConfig: PosForageConfig) { - commonInitializer( - ForageConfig( - merchantId = posForageConfig.merchantId, - sessionToken = posForageConfig.sessionToken - ), - true - ) - } - // internal because submit methods need read-access // to the ForageConfig but not public because we // don't to lull developers into passing a diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ui/ForageElement.kt b/forage-android/src/main/java/com/joinforage/forage/android/ui/ForageElement.kt index af27088bf..f46e2dec9 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ui/ForageElement.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ui/ForageElement.kt @@ -4,7 +4,6 @@ import android.graphics.Typeface import com.joinforage.forage.android.core.element.SimpleElementListener import com.joinforage.forage.android.core.element.StatefulElementListener import com.joinforage.forage.android.core.element.state.ElementState -import com.joinforage.forage.android.pos.PosForageConfig /** * The configuration details that Forage needs to create a functional [ForageElement]. @@ -69,28 +68,6 @@ interface ForageElement { */ fun setForageConfig(forageConfig: ForageConfig) - /** - * ⚠️ **The [setPosForageConfig] method is only valid for in-store POS Terminal transactions.** - * - * Sets the necessary [PosForageConfig] configuration properties for a ForageElement. - * **[setPosForageConfig] must be called before any other methods can be executed on the Element.** - * ```kotlin - * // Example: Call setPosForageConfig on a ForagePINEditText Element - * val posForagePinEditText = root?.findViewById(R.id.foragePinEditText) - * posForagePinEditText.setPosForageConfig( - * PosForageConfig( - * sessionToken = "", - * merchantId = "mid/" - * ) - * ) - * ``` - * @see * [POS Terminal Android Quickstart](https://docs.joinforage.app/docs/forage-terminal-android) - * * [setForageConfig] for the equivalent online-only method. - * - * @param posForageConfig A [PosForageConfig] instance that specifies a `merchantId` and `sessionToken`. - */ - fun setPosForageConfig(posForageConfig: PosForageConfig) - /** * Explicitly request that the current input method's soft * input be shown to the user, if needed. This only has an diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ui/ForagePANEditText.kt b/forage-android/src/main/java/com/joinforage/forage/android/ui/ForagePANEditText.kt index 5c7c4bc42..9e6062aac 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ui/ForagePANEditText.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ui/ForagePANEditText.kt @@ -180,7 +180,7 @@ class ForagePANEditText @JvmOverloads constructor( imm!!.showSoftInput(textInputEditText, 0) } - override fun initWithForageConfig(forageConfig: ForageConfig, isPos: Boolean) { + override fun initWithForageConfig(forageConfig: ForageConfig) { // Must initialize DD at the beginning of each render function. DD requires the context, // so we need to wait until a context is present to run initialization code. However, // we have logging all over the SDK that relies on the render happening first. diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ui/ForagePINEditText.kt b/forage-android/src/main/java/com/joinforage/forage/android/ui/ForagePINEditText.kt index 6cd863788..21d42fc21 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ui/ForagePINEditText.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ui/ForagePINEditText.kt @@ -137,29 +137,24 @@ class ForagePINEditText @JvmOverloads constructor( } } - override fun initWithForageConfig(forageConfig: ForageConfig, isPos: Boolean) { + override fun initWithForageConfig(forageConfig: ForageConfig) { // Must initialize DD at the beginning of each render function. DD requires the context, // so we need to wait until a context is present to run initialization code. However, // we have logging all over the SDK that relies on the render happening first. val logger = Log.getInstance() logger.initializeDD(context, forageConfig) - if (isPos) { - // only use Forage Vault for POS traffic! - _SET_ONLY_vault = forageVaultWrapper + // initialize Launch Darkly singleton + val ldMobileKey = EnvConfig.fromForageConfig(forageConfig).ldMobileKey + val ldConfig = LDConfig.Builder().mobileKey(ldMobileKey).build() + LDManager.initialize(context.applicationContext as Application, ldConfig) + + // decide on a vault provider and the corresponding vault wrapper + val vaultType = LDManager.getVaultProvider(logger) + _SET_ONLY_vault = if (vaultType == VaultType.BT_VAULT_TYPE) { + btVaultWrapper } else { - // initialize Launch Darkly singleton - val ldMobileKey = EnvConfig.fromForageConfig(forageConfig).ldMobileKey - val ldConfig = LDConfig.Builder().mobileKey(ldMobileKey).build() - LDManager.initialize(context.applicationContext as Application, ldConfig) - - // decide on a vault provider and the corresponding vault wrapper - val vaultType = LDManager.getVaultProvider(logger) - _SET_ONLY_vault = if (vaultType == VaultType.BT_VAULT_TYPE) { - btVaultWrapper - } else { - vgsVaultWrapper - } + vgsVaultWrapper } _linearLayout.addView(vault.getUnderlying()) diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ui/ForageVaultWrapper.kt b/forage-android/src/main/java/com/joinforage/forage/android/ui/ForageVaultWrapper.kt deleted file mode 100644 index 3ae45f7d9..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ui/ForageVaultWrapper.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.joinforage.forage.android.ui - -import android.content.Context -import android.graphics.Color -import android.graphics.Typeface -import android.graphics.drawable.GradientDrawable -import android.text.InputFilter -import android.text.InputType -import android.util.AttributeSet -import android.util.TypedValue -import android.view.Gravity -import android.widget.EditText -import android.widget.LinearLayout -import com.basistheory.android.view.TextElement -import com.joinforage.forage.android.R -import com.joinforage.forage.android.VaultType -import com.joinforage.forage.android.core.element.state.PinElementStateManager -import com.verygoodsecurity.vgscollect.widget.VGSEditText - -internal class ForageVaultWrapper @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : VaultWrapper(context, attrs, defStyleAttr) { - private val _editText: EditText - override val manager: PinElementStateManager = PinElementStateManager.forEmptyInput() - - init { - context.obtainStyledAttributes(attrs, R.styleable.ForagePINEditText, defStyleAttr, 0) - .apply { - try { - val parsedStyles = parseStyles(context, attrs) - - _editText = EditText(context, null, parsedStyles.textInputLayoutStyleAttribute).apply { - layoutParams = - LinearLayout.LayoutParams( - parsedStyles.inputWidth, - parsedStyles.inputHeight - ) - - setTextIsSelectable(true) - isSingleLine = true - - val maxLength = 4 - filters = arrayOf(InputFilter.LengthFilter(maxLength)) - - if (parsedStyles.textColor != Color.BLACK) { - setTextColor(parsedStyles.textColor) - } - - if (parsedStyles.textSize != -1f) { - setTextSize(TypedValue.COMPLEX_UNIT_PX, parsedStyles.textSize) - } - - inputType = - InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD - - gravity = Gravity.CENTER - hint = parsedStyles.hint - setHintTextColor(parsedStyles.hintTextColor) - - val customBackground = GradientDrawable().apply { - setPaddingRelative(20, 20, 20, 20) - shape = GradientDrawable.RECTANGLE - cornerRadii = floatArrayOf( - parsedStyles.boxCornerRadiusTopStart, - parsedStyles.boxCornerRadiusTopStart, - parsedStyles.boxCornerRadiusTopEnd, - parsedStyles.boxCornerRadiusTopEnd, - parsedStyles.boxCornerRadiusBottomStart, - parsedStyles.boxCornerRadiusBottomStart, - parsedStyles.boxCornerRadiusBottomEnd, - parsedStyles.boxCornerRadiusBottomEnd - ) - setStroke(5, parsedStyles.boxStrokeColor) - setColor(parsedStyles.boxBackgroundColor) - } - background = customBackground - } - - _editText.setOnFocusChangeListener { _, hasFocus -> - manager.changeFocus(hasFocus) - } - val pinTextWatcher = PinTextWatcher(_editText) - pinTextWatcher.onInputChangeEvent { isComplete, isEmpty -> - manager.handleChangeEvent(isComplete, isEmpty) - } - _editText.addTextChangedListener(pinTextWatcher) - } finally { - recycle() - } - } - } - - override fun getVaultType(): VaultType { - return VaultType.FORAGE_VAULT_TYPE - } - - override fun clearText() { - _editText.setText("") - } - - override fun getForageTextElement(): EditText { - return _editText - } - - override fun getTextElement(): TextElement { - throw RuntimeException("Unimplemented for this vault!") - } - - override fun getVGSEditText(): VGSEditText { - throw RuntimeException("Unimplemented for this vault!") - } - - override fun getUnderlying(): EditText { - return _editText - } - - override var typeface: Typeface? - get() = _editText.typeface - set(value) { - if (value != null) { - _editText.typeface = value - } - } - - override fun setTextColor(textColor: Int) { - _editText.setTextColor(textColor) - } - - override fun setTextSize(textSize: Float) { - _editText.textSize = textSize - } - - override fun setHint(hint: String) { - _editText.hint = hint - } - - override fun setHintTextColor(hintTextColor: Int) { - _editText.setHintTextColor(hintTextColor) - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/vault/AbstractVaultSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/vault/AbstractVaultSubmitter.kt index 0dcdde3c6..98452f3c7 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/vault/AbstractVaultSubmitter.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/vault/AbstractVaultSubmitter.kt @@ -11,8 +11,6 @@ import com.joinforage.forage.android.network.ForageConstants import com.joinforage.forage.android.network.model.ForageApiResponse import com.joinforage.forage.android.network.model.ForageError import com.joinforage.forage.android.network.model.UnknownErrorApiResponse -import com.joinforage.forage.android.pos.PosBalanceVaultSubmitterParams -import com.joinforage.forage.android.pos.PosRefundVaultSubmitterParams import com.joinforage.forage.android.ui.ForagePINEditText internal val IncompletePinError = ForageApiResponse.Failure.fromError( @@ -175,39 +173,7 @@ internal abstract class AbstractVaultSubmitter( return UnknownErrorApiResponse } - protected fun buildRequestBody(vaultProxyRequest: VaultProxyRequest): HashMap { - val baseRequestBody = buildBaseRequestBody(vaultProxyRequest) - return when (vaultProxyRequest.params) { - is PosBalanceVaultSubmitterParams -> buildPosBalanceCheckRequestBody(baseRequestBody, vaultProxyRequest.params) - is PosRefundVaultSubmitterParams -> buildPosRefundRequestBody(baseRequestBody, vaultProxyRequest.params) - else -> baseRequestBody - } - } - - private fun buildPosRefundRequestBody( - body: HashMap, - posParams: PosRefundVaultSubmitterParams - ): HashMap { - body[ForageConstants.RequestBody.AMOUNT] = posParams.refundParams.amount - body[ForageConstants.RequestBody.REASON] = posParams.refundParams.reason - body[ForageConstants.RequestBody.METADATA] = posParams.refundParams.metadata ?: HashMap() - body[ForageConstants.RequestBody.POS_TERMINAL] = hashMapOf( - ForageConstants.RequestBody.PROVIDER_TERMINAL_ID to posParams.posTerminalId - ) - return body - } - - private fun buildPosBalanceCheckRequestBody( - body: HashMap, - posParams: PosBalanceVaultSubmitterParams - ): HashMap { - body[ForageConstants.RequestBody.POS_TERMINAL] = hashMapOf( - ForageConstants.RequestBody.PROVIDER_TERMINAL_ID to posParams.posTerminalId - ) - return body - } - - private fun buildBaseRequestBody(vaultProxyRequest: VaultProxyRequest): HashMap { + protected fun buildBaseRequestBody(vaultProxyRequest: VaultProxyRequest): HashMap { return hashMapOf( ForageConstants.RequestBody.CARD_NUMBER_TOKEN to vaultProxyRequest.vaultToken ) @@ -223,14 +189,7 @@ internal abstract class AbstractVaultSubmitter( logger = logger ) } - if (vaultType == VaultType.VGS_VAULT_TYPE) { - return VgsPinSubmitter( - context = foragePinEditText.context, - foragePinEditText = foragePinEditText, - logger = logger - ) - } - return ForagePinSubmitter( + return VgsPinSubmitter( context = foragePinEditText.context, foragePinEditText = foragePinEditText, logger = logger diff --git a/forage-android/src/main/java/com/joinforage/forage/android/vault/ForagePinSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/vault/ForagePinSubmitter.kt deleted file mode 100644 index 6f93c4de1..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/vault/ForagePinSubmitter.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.joinforage.forage.android.vault - -import android.content.Context -import com.joinforage.forage.android.VaultType -import com.joinforage.forage.android.addPathSegmentsSafe -import com.joinforage.forage.android.addTrailingSlash -import com.joinforage.forage.android.core.StopgapGlobalState -import com.joinforage.forage.android.core.telemetry.Log -import com.joinforage.forage.android.model.EncryptionKeys -import com.joinforage.forage.android.model.PaymentMethod -import com.joinforage.forage.android.network.ForageConstants -import com.joinforage.forage.android.network.NetworkService -import com.joinforage.forage.android.network.OkHttpClientBuilder -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.network.model.UnknownErrorApiResponse -import com.joinforage.forage.android.ui.ForagePINEditText -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import org.json.JSONObject - -internal class ForagePinSubmitter( - context: Context, - foragePinEditText: ForagePINEditText, - logger: Log -) : AbstractVaultSubmitter>( - context = context, - foragePinEditText = foragePinEditText, - logger = logger, - vaultType = VaultType.FORAGE_VAULT_TYPE -) { - // x-key header is not applicable to forage - override fun parseEncryptionKey(encryptionKeys: EncryptionKeys): String { - return "" - } - - override suspend fun submitProxyRequest(vaultProxyRequest: VaultProxyRequest): ForageApiResponse { - return try { - val apiUrl = buildVaultUrl(vaultProxyRequest.path) - val baseRequestBody = buildRequestBody(vaultProxyRequest) - val requestBody = buildForageVaultRequestBody(foragePinEditText, baseRequestBody) - - val request = Request.Builder() - .url(apiUrl) - .post(requestBody) - .build() - - val headerValues = vaultProxyRequest.params!! - - val okHttpClient = OkHttpClientBuilder.provideOkHttpClient( - sessionToken = headerValues.sessionToken, - merchantId = headerValues.merchantId, - traceId = logger.getTraceIdValue(), - idempotencyKey = headerValues.idempotencyKey - ) - - val vaultService: NetworkService = object : NetworkService(okHttpClient, logger) {} - - val rawForageVaultResponse = vaultService.convertCallbackToCoroutine(request) - - vaultToForageResponse(rawForageVaultResponse) - } catch (e: Exception) { - logger.e("Failed to send request to Forage Vault.", e) - UnknownErrorApiResponse - } - } - - override fun buildProxyRequest( - params: VaultSubmitterParams, - encryptionKey: String, - vaultToken: String - ) = super - .buildProxyRequest( - params = params, - encryptionKey = encryptionKey, - vaultToken = vaultToken - ) - .setHeader(ForageConstants.Headers.CONTENT_TYPE, "application/json") - - override fun getVaultToken(paymentMethod: PaymentMethod): String? = - pickVaultTokenByIndex(paymentMethod, 2) - - override fun parseVaultErrorMessage(vaultResponse: ForageApiResponse): String { - return vaultResponse.toString() - } - - override fun toForageSuccessOrNull(vaultResponse: ForageApiResponse): ForageApiResponse.Success? { - return if (vaultResponse is ForageApiResponse.Success) vaultResponse else null - } - - override fun toForageErrorOrNull(vaultResponse: ForageApiResponse): ForageApiResponse.Failure? { - return if (vaultResponse is ForageApiResponse.Failure) vaultResponse else null - } - - // Unlike VGS and Basis Theory, Vault-specific errors are handled in the try..catch block that makes - // the request to the vault, so we just return null here. - override fun toVaultErrorOrNull(vaultResponse: ForageApiResponse): ForageApiResponse.Failure? { - return null - } - - companion object { - // this code assumes that .setForageConfig() has been called - // on a Forage***EditText before VAULT_BASE_URL gets referenced - private val VAULT_BASE_URL = StopgapGlobalState.envConfig.vaultBaseUrl - - private fun buildVaultUrl(path: String): HttpUrl = - VAULT_BASE_URL.toHttpUrlOrNull()!! - .newBuilder() - .addPathSegment("proxy") - .addPathSegmentsSafe(path) - .addTrailingSlash() - .build() - - private fun buildForageVaultRequestBody(foragePinEditText: ForagePINEditText, baseRequestBody: Map): RequestBody { - val jsonBody = JSONObject(baseRequestBody) - jsonBody.put("pin", foragePinEditText.getForageTextElement().text.toString()) - - val mediaType = "application/json".toMediaTypeOrNull() - return jsonBody.toString().toRequestBody(mediaType) - } - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/vault/VgsPinSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/vault/VgsPinSubmitter.kt index d747ce244..d0b9117b7 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/vault/VgsPinSubmitter.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/vault/VgsPinSubmitter.kt @@ -52,7 +52,7 @@ internal class VgsPinSubmitter( .setMethod(HTTPMethod.POST) .setPath(vaultProxyRequest.path) .setCustomHeader(vaultProxyRequest.headers) - .setCustomData(buildRequestBody(vaultProxyRequest)) + .setCustomData(buildBaseRequestBody(vaultProxyRequest)) .build() vgsCollect.asyncSubmit(request) diff --git a/forage-android/src/test/java/com/joinforage/forage/android/fixtures/CreatePaymentMethodFixtures.kt b/forage-android/src/test/java/com/joinforage/forage/android/fixtures/CreatePaymentMethodFixtures.kt index 83c3f7709..9c510f517 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/fixtures/CreatePaymentMethodFixtures.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/fixtures/CreatePaymentMethodFixtures.kt @@ -1,6 +1,5 @@ package com.joinforage.forage.android.fixtures -import com.joinforage.forage.android.pos.PosPaymentMethodRequestBody import me.jorgecastillo.hiroaki.Method import me.jorgecastillo.hiroaki.models.PotentialRequestChain import me.jorgecastillo.hiroaki.models.error @@ -35,17 +34,6 @@ internal fun MockWebServer.givenPaymentMethod(cardNumber: String, reusable: Bool } ) -internal fun MockWebServer.givenPaymentMethod(posPaymentMethodRequestBody: PosPaymentMethodRequestBody) = whenever( - method = Method.POST, - sentToPath = "api/payment_methods/", - jsonBody = json { - "type" / "ebt" - "reusable" / posPaymentMethodRequestBody.reusable - "card" / json { - "track_2_data" / posPaymentMethodRequestBody.track2Data - } - } -) internal fun PotentialRequestChain.returnsPaymentMethodSuccessfully() = thenRespond( success( diff --git a/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServerUtils.kt b/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServerUtils.kt index ba0e3e165..2bba9111c 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServerUtils.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServerUtils.kt @@ -1,65 +1,8 @@ package com.joinforage.forage.android.mock -import com.joinforage.forage.android.fixtures.givenContentId -import com.joinforage.forage.android.fixtures.givenEncryptionKey -import com.joinforage.forage.android.fixtures.givenPaymentAndRefundRef -import com.joinforage.forage.android.fixtures.givenPaymentMethodRef -import com.joinforage.forage.android.fixtures.givenPaymentRef -import com.joinforage.forage.android.fixtures.returnsEncryptionKeySuccessfully -import com.joinforage.forage.android.fixtures.returnsMessageCompletedSuccessfully -import com.joinforage.forage.android.fixtures.returnsPayment -import com.joinforage.forage.android.fixtures.returnsPaymentMethod -import com.joinforage.forage.android.fixtures.returnsRefund -import com.joinforage.forage.android.network.model.ForageApiResponse -import okhttp3.mockwebserver.MockWebServer import org.json.JSONArray import org.json.JSONObject -// contains the minimal data needed to marshall a refund response into a PosRefundVaultResponse -internal val MOCK_VAULT_REFUND_RESPONSE = """ -{ - "ref": "${MockServiceFactory.ExpectedData.refundRef}", - "message": { - "content_id": "${MockServiceFactory.ExpectedData.contentId}", - "message_type": "0200", - "status": "sent_to_proxy", - "failed": false, - "errors": [] - } -} -""".trimIndent() - -internal fun mockSuccessfulPosRefund( - mockVaultSubmitter: MockVaultSubmitter, - server: MockWebServer -) { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentRef().returnsPayment() - server.givenPaymentMethodRef().returnsPaymentMethod() - server.givenContentId(MockServiceFactory.ExpectedData.contentId) - .returnsMessageCompletedSuccessfully() - server.givenPaymentAndRefundRef().returnsRefund() - - mockVaultSubmitter.setSubmitResponse( - path = "/api/payments/${MockServiceFactory.ExpectedData.paymentRef}/refunds/", - response = ForageApiResponse.Success(MOCK_VAULT_REFUND_RESPONSE) - ) -} - -internal fun mockSuccessfulPosDeferredRefund( - mockVaultSubmitter: MockVaultSubmitter, - server: MockWebServer -) { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentRef().returnsPayment() - server.givenPaymentMethodRef().returnsPaymentMethod() - - mockVaultSubmitter.setSubmitResponse( - path = "/api/payments/${MockServiceFactory.ExpectedData.paymentRef}/refunds/collect_pin/", - response = ForageApiResponse.Success("") - ) -} - internal fun getVaultMessageResponse(contentId: String): String { return JSONObject().apply { put("content_id", contentId) diff --git a/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServiceFactory.kt b/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServiceFactory.kt index 595fe0e1e..0bcb509ae 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServiceFactory.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServiceFactory.kt @@ -15,9 +15,6 @@ import com.joinforage.forage.android.network.data.CapturePaymentRepository import com.joinforage.forage.android.network.data.CheckBalanceRepository import com.joinforage.forage.android.network.data.DeferPaymentCaptureRepository import com.joinforage.forage.android.network.data.DeferPaymentRefundRepository -import com.joinforage.forage.android.pos.PosRefundPaymentRepository -import com.joinforage.forage.android.pos.PosRefundService -import com.joinforage.forage.android.pos.PosVaultRequestParams import com.joinforage.forage.android.ui.ForagePINEditText import okhttp3.mockwebserver.MockWebServer @@ -51,17 +48,6 @@ internal class MockServiceFactory( encryptionKey = "tok_sandbox_eZeWfkq1AkqYdiAJC8iweE" ) - // POS - const val posTerminalId: String = "pos-terminal-id-123" - const val refundRef: String = "refund123" - const val track2Data: String = "5077081212341234=491212012345" - const val refundAmount: Float = 1.23f - const val refundReason: String = "I feel like refunding this payment!" - val posVaultRequestParams: PosVaultRequestParams = PosVaultRequestParams( - cardNumberToken = "tok_sandbox_sYiPe9Q249qQ5wQyUPP5f7", - encryptionKey = "tok_sandbox_eZeWfkq1AkqYdiAJC8iweE", - posTerminalId = "pos-terminal-id-123" - ) } private val okHttpClient by lazy { @@ -76,7 +62,6 @@ internal class MockServiceFactory( private val paymentService by lazy { createPaymentService() } private val messageStatusService by lazy { createMessageStatusService() } private val pollingService by lazy { createPollingService() } - private val posRefundService by lazy { createPosRefundService() } private fun emptyUrl() = server.url("").toUrl().toString() @@ -125,17 +110,6 @@ internal class MockServiceFactory( ) } - override fun createRefundPaymentRepository(foragePinEditText: ForagePINEditText): PosRefundPaymentRepository { - return PosRefundPaymentRepository( - vaultSubmitter = mockVaultSubmitter, - encryptionKeyService = encryptionKeyService, - paymentMethodService = paymentMethodService, - paymentService = paymentService, - pollingService = pollingService, - logger = logger, - refundService = posRefundService - ) - } private fun createEncryptionKeyService() = EncryptionKeyService(emptyUrl(), okHttpClient, logger) private fun createPaymentMethodService() = PaymentMethodService(emptyUrl(), okHttpClient, logger) @@ -145,5 +119,4 @@ internal class MockServiceFactory( messageStatusService = messageStatusService, logger = logger ) - private fun createPosRefundService() = PosRefundService(emptyUrl(), logger, okHttpClient) } diff --git a/forage-android/src/test/java/com/joinforage/forage/android/pos/ForageTerminalSDKTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/pos/ForageTerminalSDKTest.kt deleted file mode 100644 index 572b0cb2b..000000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/pos/ForageTerminalSDKTest.kt +++ /dev/null @@ -1,532 +0,0 @@ -package com.joinforage.forage.android.pos - -import com.joinforage.forage.android.CapturePaymentParams -import com.joinforage.forage.android.CheckBalanceParams -import com.joinforage.forage.android.DeferPaymentCaptureParams -import com.joinforage.forage.android.ForageSDK -import com.joinforage.forage.android.TokenizeEBTCardParams -import com.joinforage.forage.android.VaultType -import com.joinforage.forage.android.core.telemetry.Log -import com.joinforage.forage.android.fixtures.givenContentId -import com.joinforage.forage.android.fixtures.givenEncryptionKey -import com.joinforage.forage.android.fixtures.givenPaymentMethod -import com.joinforage.forage.android.fixtures.givenPaymentMethodRef -import com.joinforage.forage.android.fixtures.givenPaymentRef -import com.joinforage.forage.android.fixtures.returnsEncryptionKeySuccessfully -import com.joinforage.forage.android.fixtures.returnsMessageCompletedSuccessfully -import com.joinforage.forage.android.fixtures.returnsMissingCustomerIdPaymentMethodSuccessfully -import com.joinforage.forage.android.fixtures.returnsPayment -import com.joinforage.forage.android.fixtures.returnsPaymentMethod -import com.joinforage.forage.android.fixtures.returnsPaymentMethodSuccessfully -import com.joinforage.forage.android.fixtures.returnsPaymentMethodWithBalance -import com.joinforage.forage.android.mock.MockLogger -import com.joinforage.forage.android.mock.MockServiceFactory -import com.joinforage.forage.android.mock.MockVaultSubmitter -import com.joinforage.forage.android.mock.getVaultMessageResponse -import com.joinforage.forage.android.mock.mockSuccessfulPosDeferredRefund -import com.joinforage.forage.android.mock.mockSuccessfulPosRefund -import com.joinforage.forage.android.model.Card -import com.joinforage.forage.android.model.PaymentMethod -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.network.model.ForageError -import com.joinforage.forage.android.ui.ForageConfig -import com.joinforage.forage.android.ui.ForagePANEditText -import com.joinforage.forage.android.ui.ForagePINEditText -import junit.framework.TestCase.assertTrue -import kotlinx.coroutines.test.runTest -import me.jorgecastillo.hiroaki.Method -import me.jorgecastillo.hiroaki.headers -import me.jorgecastillo.hiroaki.internal.MockServerSuite -import me.jorgecastillo.hiroaki.matchers.times -import me.jorgecastillo.hiroaki.models.json -import me.jorgecastillo.hiroaki.verify -import org.assertj.core.api.Assertions.assertThat -import org.json.JSONObject -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class ForageTerminalSDKTest : MockServerSuite() { - private lateinit var mockForagePanEditText: ForagePANEditText - private lateinit var mockForagePinEditText: ForagePINEditText - private lateinit var terminalSdk: ForageTerminalSDK - private lateinit var mockForageSdk: ForageSDK - private lateinit var mockLogger: MockLogger - private val expectedData = MockServiceFactory.ExpectedData - private lateinit var vaultSubmitter: MockVaultSubmitter - - @Before - fun setUp() { - super.setup() - - mockLogger = MockLogger() - vaultSubmitter = MockVaultSubmitter(VaultType.FORAGE_VAULT_TYPE) - // Use Mockito judiciously (mainly for mocking views)! - // Opt for dependency injection and inheritance over Mockito - mockForagePanEditText = mock(ForagePANEditText::class.java) - mockForagePinEditText = mock(ForagePINEditText::class.java) - mockForageSdk = mock(ForageSDK::class.java) - - val forageConfig = ForageConfig( - merchantId = expectedData.merchantId, - sessionToken = expectedData.sessionToken - ) - `when`(mockForagePinEditText.getForageConfig()).thenReturn(forageConfig) - `when`(mockForagePinEditText.getVaultType()).thenReturn(vaultSubmitter.getVaultType()) - - terminalSdk = createMockTerminalSdk() - } - - @Test - fun `POS should send the correct headers + body to tokenize the card`() = runTest { - server.givenPaymentMethod( - PosPaymentMethodRequestBody( - track2Data = expectedData.track2Data, - reusable = false - ) - ).returnsPaymentMethodSuccessfully() - - executeTokenizeCardWithTrack2Data( - track2Data = expectedData.track2Data, - reusable = false - ) - - server.verify("api/payment_methods/").called( - times = times(1), - method = Method.POST, - headers = headers( - "Authorization" to "Bearer ${expectedData.sessionToken}", - "Merchant-Account" to expectedData.merchantId - ), - jsonBody = json { - "type" / "ebt" - "reusable" / false - "card" / json { - "track_2_data" / expectedData.track2Data - } - } - ) - } - - @Test - fun `POS tokenize EBT card with Track 2 data successfully`() = runTest { - server.givenPaymentMethod( - PosPaymentMethodRequestBody( - track2Data = expectedData.track2Data, - reusable = true - ) - ).returnsMissingCustomerIdPaymentMethodSuccessfully() - - val paymentMethodResponse = executeTokenizeCardWithTrack2Data( - track2Data = expectedData.track2Data, - reusable = true - ) - assertThat(paymentMethodResponse).isExactlyInstanceOf(ForageApiResponse.Success::class.java) - val response = - PaymentMethod.ModelMapper.from((paymentMethodResponse as ForageApiResponse.Success).data) - assertThat(response).isEqualTo( - PaymentMethod( - ref = "2f148fe399", - type = "ebt", - balance = null, - card = Card( - last4 = "7845", - token = "tok_sandbox_sYiPe9Q249qQ5wQyUPP5f7" - ), - reusable = true, - customerId = null - ) - ) - assertFirstLoggedMessage("[POS] Tokenizing Payment Method using magnetic card swipe with Track 2 data on Terminal pos-terminal-id-123") - } - - @Test - fun `POS tokenize EBT card via UI-based PAN entry`() = runTest { - `when`( - mockForageSdk.tokenizeEBTCard( - TokenizeEBTCardParams( - foragePanEditText = mockForagePanEditText - ) - ) - ).thenReturn( - ForageApiResponse.Success("Success") - ) - - val terminalSdk = createMockTerminalSdk(false) - - val response = terminalSdk.tokenizeCard( - foragePanEditText = mockForagePanEditText - ) - assertFirstLoggedMessage("[POS] Tokenizing Payment Method via UI PAN entry on Terminal pos-terminal-id-123") - assertTrue(response is ForageApiResponse.Success) - assertTrue((response as ForageApiResponse.Success).data == "Success") - } - - @Test - fun `POS EBT checkBalance should succeed`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - // Get Payment Method is called twice! - server.givenPaymentMethodRef().returnsPaymentMethod() - server.givenPaymentMethodRef().returnsPaymentMethodWithBalance() - vaultSubmitter.setSubmitResponse( - path = "/api/payment_methods/${expectedData.paymentMethodRef}/balance/", - response = ForageApiResponse.Success(getVaultMessageResponse(expectedData.contentId)) - ) - server.givenContentId(expectedData.contentId) - .returnsMessageCompletedSuccessfully() - - val response = executeCheckBalance() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Success::class.java) - val successResponse = response as ForageApiResponse.Success - assertThat(successResponse.data).contains(expectedData.balance.cash) - assertThat(successResponse.data).contains(expectedData.balance.snap) - - assertMetricsLog() - val attributes = mockLogger.getMetricsLog().getAttributes() - - assertThat(attributes.getValue("response_time_ms").toString().toDouble()).isGreaterThan(0.0) - assertThat(attributes.getValue("vault_type").toString()).isEqualTo("forage") - assertThat(attributes.getValue("action").toString()).isEqualTo("balance") - assertThat(attributes.getValue("event_name").toString()).isEqualTo("customer_perceived_response") - assertThat(attributes.getValue("log_type").toString()).isEqualTo("metric") - } - - @Test - fun `POS checkBalance should return a failure when the VGS request fails`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentMethodRef().returnsPaymentMethod() - val failureResponse = ForageApiResponse.Failure(listOf(ForageError(500, "unknown_server_error", "Some error message from VGS"))) - vaultSubmitter.setSubmitResponse( - path = "/api/payment_methods/${expectedData.paymentMethodRef}/balance/", - response = failureResponse - ) - - val response = executeCheckBalance() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - assertThat(response).isEqualTo(failureResponse) - } - - fun `POS checkBalance should report metrics upon failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentMethodRef().returnsPaymentMethod() - val failureResponse = ForageApiResponse.Failure(listOf(ForageError(500, "unknown_server_error", "Some error message from VGS"))) - vaultSubmitter.setSubmitResponse( - path = "/api/payment_methods/${expectedData.paymentRef}/balance/", - response = failureResponse - ) - - executeCheckBalance() - - assertLoggedError( - expectedMessage = "[POS] checkBalance failed for PaymentMethod 1f148fe399 on Terminal pos-terminal-id-123", - failureResponse - ) - assertMetricsLog() - val attributes = mockLogger.getMetricsLog().getAttributes() - assertThat(attributes.getValue("response_time_ms").toString().toDouble()).isGreaterThan(0.0) - assertThat(attributes.getValue("vault_type").toString()).isEqualTo("forage") - assertThat(attributes.getValue("action").toString()).isEqualTo("balance") - assertThat(attributes.getValue("event_name").toString()).isEqualTo("customer_perceived_response") - assertThat(attributes.getValue("event_outcome").toString()).isEqualTo("failure") - assertThat(attributes.getValue("log_type").toString()).isEqualTo("metric") - assertThat(attributes.getValue("forage_error_code").toString()).isEqualTo("unknown_server_error") - } - - @Test - fun `POS refundPayment succeeds`() = runTest { - mockSuccessfulPosRefund( - mockVaultSubmitter = vaultSubmitter, - server = server - ) - val response = executeRefundPayment() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Success::class.java) - val refundResponse = JSONObject((response as ForageApiResponse.Success).data) - val refundRef = refundResponse.getString("ref") - val paymentRef = refundResponse.getString("payment_ref") - val refundAmount = refundResponse.getString("amount") - - assertThat(refundRef).isEqualTo(expectedData.refundRef) - assertThat(paymentRef).isEqualTo(expectedData.paymentRef) - assertThat(refundAmount).isEqualTo(expectedData.refundAmount.toString()) - - assertFirstLoggedMessage( - """ - [POS] Called refundPayment for Payment 6ae6a45ff1 - with amount: 1.23 - for reason: I feel like refunding this payment! - on Terminal: pos-terminal-id-123 - """.trimIndent() - ) - - assertMetricsLog() - val attributes = mockLogger.getMetricsLog().getAttributes() - assertThat(attributes.getValue("response_time_ms").toString().toDouble()).isGreaterThan(0.0) - assertThat(attributes.getValue("vault_type").toString()).isEqualTo("forage") - assertThat(attributes.getValue("action").toString()).isEqualTo("refund") - assertThat(attributes.getValue("event_name").toString()).isEqualTo("customer_perceived_response") - assertThat(attributes.getValue("log_type").toString()).isEqualTo("metric") - } - - @Test - fun `POS refundPayment should return a failure when the Vault request fails`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentRef().returnsPayment() - server.givenPaymentMethodRef().returnsPaymentMethod() - - val failureResponse = ForageApiResponse.Failure(listOf(ForageError(500, "unknown_server_error", "Some error message from VGS"))) - vaultSubmitter.setSubmitResponse( - path = "/api/payments/${expectedData.paymentRef}/refunds/", - response = failureResponse - ) - - executeRefundPayment() - - assertLoggedError( - expectedMessage = "[POS] refundPayment failed for Payment ${expectedData.paymentRef} on Terminal pos-terminal-id-123", - failureResponse - ) - assertMetricsLog() - val attributes = mockLogger.getMetricsLog().getAttributes() - assertThat(attributes.getValue("response_time_ms").toString().toDouble()).isGreaterThan(0.0) - assertThat(attributes.getValue("vault_type").toString()).isEqualTo("forage") - assertThat(attributes.getValue("action").toString()).isEqualTo("refund") - assertThat(attributes.getValue("event_name").toString()).isEqualTo("customer_perceived_response") - assertThat(attributes.getValue("event_outcome").toString()).isEqualTo("failure") - assertThat(attributes.getValue("log_type").toString()).isEqualTo("metric") - assertThat(attributes.getValue("forage_error_code").toString()).isEqualTo("unknown_server_error") - } - - @Test - fun `POS capturePayment`() = runTest { - `when`( - mockForageSdk.capturePayment( - CapturePaymentParams( - foragePinEditText = mockForagePinEditText, - paymentRef = "payment1234" - ) - ) - ).thenReturn( - ForageApiResponse.Success("Success") - ) - - val terminalSdk = createMockTerminalSdk(false) - val params = CapturePaymentParams( - foragePinEditText = mockForagePinEditText, - paymentRef = "payment1234" - ) - val response = terminalSdk.capturePayment(params) - assertTrue(response is ForageApiResponse.Success) - assertTrue((response as ForageApiResponse.Success).data == "Success") - } - - @Test - fun `POS illegal vault exception`() = runTest { - val terminalSdk = createMockTerminalSdk() - - val expectedLogSubstring = "because the vault type is not forage" - `when`(mockForagePinEditText.getVaultType()).thenReturn(VaultType.BT_VAULT_TYPE) - val balanceResponse = terminalSdk.checkBalance( - CheckBalanceParams( - foragePinEditText = mockForagePinEditText, - paymentMethodRef = "1f148fe399" - ) - ) - - val balanceError = (balanceResponse as ForageApiResponse.Failure).errors.first() - assertThat(balanceError.message).contains("IllegalStateException") - assertThat(mockLogger.errorLogs.last().getMessage()).contains(expectedLogSubstring) - assertThat(mockLogger.errorLogs.count()).isEqualTo(1) - - val refundResponse = terminalSdk.refundPayment( - PosRefundPaymentParams( - foragePinEditText = mockForagePinEditText, - paymentRef = expectedData.paymentRef, - amount = expectedData.refundAmount, - reason = expectedData.refundReason - ) - ) - - assertTrue(refundResponse is ForageApiResponse.Failure) - assertThat(mockLogger.errorLogs.count()).isEqualTo(2) - assertThat(mockLogger.errorLogs.last().getMessage()).contains(expectedLogSubstring) - } - - @Test - fun `POS deferPaymentCapture`() = runTest { - `when`( - mockForageSdk.deferPaymentCapture( - DeferPaymentCaptureParams( - foragePinEditText = mockForagePinEditText, - paymentRef = "payment1234" - ) - ) - ).thenReturn( - ForageApiResponse.Success("") - ) - val params = DeferPaymentCaptureParams( - foragePinEditText = mockForagePinEditText, - paymentRef = "payment1234" - ) - val terminalSdk = createMockTerminalSdk(false) - val response = terminalSdk.deferPaymentCapture(params) - assertTrue(response is ForageApiResponse.Success) - assertTrue((response as ForageApiResponse.Success).data == "") - } - - @Test - fun `POS deferPaymentRefund succeeds`() = runTest { - mockSuccessfulPosDeferredRefund( - mockVaultSubmitter = vaultSubmitter, - server = server - ) - val response = executeDeferPaymentRefund() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Success::class.java) - assertTrue((response as ForageApiResponse.Success).data == "") - - assertFirstLoggedMessage( - """ - [POS] Called deferPaymentRefund for Payment 6ae6a45ff1 - on Terminal: pos-terminal-id-123 - """.trimIndent() - ) - - assertMetricsLog() - val attributes = mockLogger.getMetricsLog().getAttributes() - assertThat(attributes.getValue("response_time_ms").toString().toDouble()).isGreaterThan(0.0) - assertThat(attributes.getValue("vault_type").toString()).isEqualTo("forage") - assertThat(attributes.getValue("action").toString()).isEqualTo("defer_refund") - assertThat( - attributes.getValue("event_name").toString() - ).isEqualTo("customer_perceived_response") - assertThat(attributes.getValue("log_type").toString()).isEqualTo("metric") - } - - @Test - fun `POS deferPaymentRefund fails`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentRef().returnsPayment() - server.givenPaymentMethodRef().returnsPaymentMethod() - val failureResponse = ForageApiResponse.Failure(listOf(ForageError(500, "unknown_server_error", "Some error message from VGS"))) - vaultSubmitter.setSubmitResponse( - path = "/api/payments/${expectedData.paymentRef}/refunds/collect_pin/", - response = failureResponse - ) - val response = executeDeferPaymentRefund() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - - assertLoggedError( - expectedMessage = "[POS] deferPaymentRefund failed for Payment ${expectedData.paymentRef} on Terminal pos-terminal-id-123", - failureResponse - ) - assertMetricsLog() - val attributes = mockLogger.getMetricsLog().getAttributes() - assertThat(attributes.getValue("response_time_ms").toString().toDouble()).isGreaterThan(0.0) - assertThat(attributes.getValue("vault_type").toString()).isEqualTo("forage") - assertThat(attributes.getValue("action").toString()).isEqualTo("defer_refund") - assertThat(attributes.getValue("event_name").toString()).isEqualTo("customer_perceived_response") - assertThat(attributes.getValue("event_outcome").toString()).isEqualTo("failure") - assertThat(attributes.getValue("log_type").toString()).isEqualTo("metric") - assertThat(attributes.getValue("forage_error_code").toString()).isEqualTo("unknown_server_error") - } - - private suspend fun executeTokenizeCardWithTrack2Data( - track2Data: String, - reusable: Boolean - ): ForageApiResponse { - val terminalSdk = createMockTerminalSdk() - return terminalSdk.tokenizeCard( - PosTokenizeCardParams( - posForageConfig = PosForageConfig( - merchantId = expectedData.merchantId, - sessionToken = expectedData.sessionToken - ), - track2Data = track2Data, - reusable = reusable - ) - ) - } - - private suspend fun executeCheckBalance(): ForageApiResponse { - val terminalSdk = createMockTerminalSdk() - return terminalSdk.checkBalance( - CheckBalanceParams( - foragePinEditText = mockForagePinEditText, - paymentMethodRef = "1f148fe399" - ) - ) - } - - private suspend fun executeRefundPayment(): ForageApiResponse { - val terminalSdk = createMockTerminalSdk() - return terminalSdk.refundPayment( - PosRefundPaymentParams( - foragePinEditText = mockForagePinEditText, - paymentRef = expectedData.paymentRef, - amount = expectedData.refundAmount, - reason = expectedData.refundReason - ) - ) - } - - private suspend fun executeDeferPaymentRefund(): ForageApiResponse { - val terminalSdk = createMockTerminalSdk() - return terminalSdk.deferPaymentRefund( - PosDeferPaymentRefundParams( - foragePinEditText = mockForagePinEditText, - paymentRef = expectedData.paymentRef - ) - ) - } - - private fun createMockTerminalSdk(withMockServiceFactory: Boolean = true): ForageTerminalSDK { - if (withMockServiceFactory) { - return ForageTerminalSDK( - posTerminalId = expectedData.posVaultRequestParams.posTerminalId, - forageSdk = ForageSDK(), - createLogger = { mockLogger }, - createServiceFactory = { _: String, _: String, logger: Log -> - MockServiceFactory( - mockVaultSubmitter = vaultSubmitter, - logger = logger, - server = server - ) - }, - initSucceeded = true - ) - } - return ForageTerminalSDK( - posTerminalId = expectedData.posTerminalId, - forageSdk = mockForageSdk, - createLogger = { mockLogger }, - initSucceeded = true - ) - } - - private fun assertLoggedError(expectedMessage: String, failureResponse: ForageApiResponse.Failure) { - val firstForageError = failureResponse.errors.first() - assertThat(mockLogger.errorLogs.first().getMessage()).contains( - """ - $expectedMessage: Code: ${firstForageError.code} - Message: ${firstForageError.message} - Status Code: ${firstForageError.httpStatusCode} - """.trimIndent() - ) - } - - private fun assertFirstLoggedMessage(expectedMessage: String) = - assertThat(mockLogger.infoLogs.first().getMessage()).contains(expectedMessage) - - private fun assertMetricsLog() = - assertThat(mockLogger.infoLogs.last().getMessage()).contains( - "[Metrics] Customer perceived response time" - ) -} diff --git a/forage-android/src/test/java/com/joinforage/forage/android/pos/PosRefundPaymentRepositoryTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/pos/PosRefundPaymentRepositoryTest.kt deleted file mode 100644 index 2dd9714d2..000000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/pos/PosRefundPaymentRepositoryTest.kt +++ /dev/null @@ -1,215 +0,0 @@ -package com.joinforage.forage.android.pos - -import com.joinforage.forage.android.VaultType -import com.joinforage.forage.android.core.telemetry.Log -import com.joinforage.forage.android.fixtures.givenContentId -import com.joinforage.forage.android.fixtures.givenEncryptionKey -import com.joinforage.forage.android.fixtures.givenPaymentAndRefundRef -import com.joinforage.forage.android.fixtures.givenPaymentMethodRef -import com.joinforage.forage.android.fixtures.givenPaymentRef -import com.joinforage.forage.android.fixtures.returnsEncryptionKeySuccessfully -import com.joinforage.forage.android.fixtures.returnsFailed -import com.joinforage.forage.android.fixtures.returnsFailedPayment -import com.joinforage.forage.android.fixtures.returnsFailedPaymentMethod -import com.joinforage.forage.android.fixtures.returnsFailedRefund -import com.joinforage.forage.android.fixtures.returnsMessageCompletedSuccessfully -import com.joinforage.forage.android.fixtures.returnsPayment -import com.joinforage.forage.android.fixtures.returnsPaymentMethod -import com.joinforage.forage.android.fixtures.returnsUnauthorizedEncryptionKey -import com.joinforage.forage.android.mock.MOCK_VAULT_REFUND_RESPONSE -import com.joinforage.forage.android.mock.MockServiceFactory -import com.joinforage.forage.android.mock.MockVaultSubmitter -import com.joinforage.forage.android.mock.mockSuccessfulPosRefund -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.network.model.ForageError -import com.joinforage.forage.android.ui.ForagePINEditText -import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.test.runTest -import me.jorgecastillo.hiroaki.internal.MockServerSuite -import org.assertj.core.api.Assertions.assertThat -import org.json.JSONObject -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.mock - -class PosRefundPaymentRepositoryTest : MockServerSuite() { - private lateinit var repository: PosRefundPaymentRepository - - private lateinit var mockServiceFactory: MockServiceFactory - private val mockVaultSubmitter = MockVaultSubmitter(VaultType.FORAGE_VAULT_TYPE) - private val expectedData = MockServiceFactory.ExpectedData - private lateinit var mockForagePinEditText: ForagePINEditText - - @Before - override fun setup() { - super.setup() - - mockForagePinEditText = mock(ForagePINEditText::class.java) - val logger = Log.getSilentInstance() - mockServiceFactory = MockServiceFactory( - mockVaultSubmitter = mockVaultSubmitter, - logger = logger, - server = server - ) - repository = mockServiceFactory.createRefundPaymentRepository(mockForagePinEditText) - } - - private suspend fun executeRefundPayment(): ForageApiResponse { - return repository.refundPayment( - merchantId = expectedData.merchantId, - posTerminalId = expectedData.posTerminalId, - refundParams = PosRefundPaymentParams( - foragePinEditText = mockForagePinEditText, - paymentRef = expectedData.paymentRef, - amount = 1.0f, - reason = "I feel like refunding!", - metadata = hashMapOf("meta" to "verse", "my_store_location_id" to "123456") - ), - sessionToken = expectedData.sessionToken - ) - } - - private fun setMockVaultResponse(response: ForageApiResponse) { - mockVaultSubmitter.setSubmitResponse( - path = "/api/payments/${expectedData.paymentRef}/refunds/", - response = response - ) - } - - @Test - fun `it should return a failure when the getting the encryption key fails`() = runTest { - server.givenEncryptionKey().returnsUnauthorizedEncryptionKey() - - val response = executeRefundPayment() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val clientError = response as ForageApiResponse.Failure - - assertThat(clientError.errors[0].message).contains("Authentication credentials were not provided.") - } - - @Test - fun `it should return a failure when the get payment returns a failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentRef().returnsFailedPayment() - - val expectedMessage = "Cannot find payment." - val expectedForageCode = "not_found" - val expectedStatusCode = 404 - - val response = executeRefundPayment() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val firstError = (response as ForageApiResponse.Failure).errors.first() - - assertThat(firstError.message).isEqualTo(expectedMessage) - assertThat(firstError.code).isEqualTo(expectedForageCode) - assertThat(firstError.httpStatusCode).isEqualTo(expectedStatusCode) - } - - @Test - fun `it should return a failure when the get payment method returns a failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentRef().returnsPayment() - server.givenPaymentMethodRef().returnsFailedPaymentMethod() - - val expectedMessage = "EBT Card could not be found" - val expectedForageCode = "not_found" - val expectedStatusCode = 404 - - val response = executeRefundPayment() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val firstError = (response as ForageApiResponse.Failure).errors.first() - - assertThat(firstError.message).isEqualTo(expectedMessage) - assertThat(firstError.code).isEqualTo(expectedForageCode) - assertThat(firstError.httpStatusCode).isEqualTo(expectedStatusCode) - } - - @Test - fun `it should fail if getting the refund fails`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentRef().returnsPayment() - server.givenPaymentMethodRef().returnsPaymentMethod() - server.givenContentId(expectedData.contentId) - .returnsMessageCompletedSuccessfully() - server.givenPaymentAndRefundRef().returnsFailedRefund() - - setMockVaultResponse(ForageApiResponse.Success(MOCK_VAULT_REFUND_RESPONSE)) - - val expectedMessage = "Refund with ref refund123 does not exist for current Merchant with FNS 1234567." - val expectedCode = "resource_not_found" - val expectedStatusCode = 404 - - val response = executeRefundPayment() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val firstError = (response as ForageApiResponse.Failure).errors.first() - assertThat(firstError.message).isEqualTo(expectedMessage) - assertThat(firstError.code).isEqualTo(expectedCode) - assertThat(firstError.httpStatusCode).isEqualTo(expectedStatusCode) - } - - @Test - fun `it should fail on vault proxy pin submission`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentRef().returnsPayment() - server.givenPaymentMethodRef().returnsPaymentMethod() - - val expectedMessage = "Only Payments in the succeeded state can be refunded, but Payment with ref abcdef123 is in the canceled state" - val expectedForageCode = "cannot_refund_payment" - val expectedStatusCode = 400 - setMockVaultResponse( - ForageApiResponse.Failure.fromError( - ForageError(400, code = expectedForageCode, message = expectedMessage) - ) - ) - - val response = executeRefundPayment() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val firstError = (response as ForageApiResponse.Failure).errors.first() - assertThat(firstError.message).isEqualTo(expectedMessage) - assertThat(firstError.code).isEqualTo(expectedForageCode) - assertThat(firstError.httpStatusCode).isEqualTo(expectedStatusCode) - } - - @Test - fun `it should fail when polling receives a failed SQS message`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - server.givenPaymentRef().returnsPayment() - server.givenPaymentMethodRef().returnsPaymentMethod() - - setMockVaultResponse(ForageApiResponse.Success(MOCK_VAULT_REFUND_RESPONSE)) - - server.givenContentId(expectedData.contentId) - .returnsFailed() - - val response = executeRefundPayment() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val firstError = (response as ForageApiResponse.Failure).errors.first() - - assertThat(firstError.httpStatusCode).isEqualTo(504) - assertThat(firstError.code).isEqualTo("ebt_error_91") - assertThat(firstError.message).contains("Authorizer not available (time-out) - Host Not Available") - } - - @Test - fun `it should succeed`() = runTest { - mockSuccessfulPosRefund(mockVaultSubmitter = mockVaultSubmitter, server = server) - - val response = executeRefundPayment() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Success::class.java) - when (response) { - is ForageApiResponse.Success -> { - assertEquals(expectedData.refundRef, JSONObject(response.data).getString("ref")) - } - else -> { - assertThat(false) - } - } - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/MainActivity.kt b/sample-app/src/main/java/com/joinforage/android/example/MainActivity.kt index 1418e222f..f5fad910c 100644 --- a/sample-app/src/main/java/com/joinforage/android/example/MainActivity.kt +++ b/sample-app/src/main/java/com/joinforage/android/example/MainActivity.kt @@ -31,7 +31,6 @@ class MainActivity : AppCompatActivity() { setOf( R.id.navigation_complete_flow, R.id.navigation_catalog, - R.id.navigation_pos ) ) setupActionBarWithNavController(navController, appBarConfiguration) diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/k9sdk/K9SDK.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/k9sdk/K9SDK.kt deleted file mode 100644 index 2769c3f3e..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/k9sdk/K9SDK.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.joinforage.android.example.pos.k9sdk - -import android.content.Context -import android.os.RemoteException -import android.util.Log -import com.pos.sdk.DeviceManager -import com.pos.sdk.DevicesFactory -import com.pos.sdk.callback.ResultCallback -import com.pos.sdk.magcard.IMagCardListener -import com.pos.sdk.magcard.MagCardDevice -import com.pos.sdk.magcard.TrackData -import com.pos.sdk.printer.PrinterDevice -import com.pos.sdk.sys.SystemDevice -import okio.ByteString.Companion.decodeHex - -enum class DeviceType { - K9_TERMINAL, - EMULATOR, - UNKNOWN -} - -fun hexToAscii(hex: String) = String(hex.decodeHex().toByteArray()) - -class K9SDK() { - private var _deviceManager: DeviceManager? = null - private val _printDevice: PrinterDevice? - get() = _deviceManager?.printDevice - private val _magCardDevice: MagCardDevice? - get() = _deviceManager?.magneticDevice - private val _systemDevice: SystemDevice? - get() = _deviceManager?.systemDevice - - val terminalId: String - get() = _systemDevice?.getSystemInfo(SystemDevice.SystemInfoType.SN) ?: "Android Emulator" - - var currentDeviceType: DeviceType = DeviceType.UNKNOWN - val isUsable - get() = currentDeviceType == DeviceType.K9_TERMINAL - - fun init(context: Context): K9SDK { - DevicesFactory.create( - context, - object : ResultCallback { - override fun onFinish(deviceManager: DeviceManager) { - _deviceManager = deviceManager - currentDeviceType = DeviceType.K9_TERMINAL - Log.i("CPay SDK", "initialized successfully") - } - - override fun onError(i: Int, s: String) { - Log.i("CPay SDK", "failed to initialize successfully") - currentDeviceType = DeviceType.EMULATOR - } - } - ) - return this - } - - fun listenForMagneticCardSwipe( - onSwipeCardSuccess: (track2Data: String) -> Unit - ): Boolean { - // CPay SDK methods should only run if they are supported - // by the current runtime env (i.e. the app is being run - // on Centerm K9 POS terminal). Otherwise don't run this - if (!isUsable) return false - - _magCardDevice?.swipeCard( - 20000, - true, - object : IMagCardListener.Stub() { - @Throws(RemoteException::class) - override fun onSwipeCardTimeout() { - Log.i("CPay SDK", "Swipe card time out") - } - - @Throws(RemoteException::class) - override fun onSwipeCardException(i: Int) { - Log.i("CPay SDK", "Swipe card error $i") - } - - @Throws(RemoteException::class) - override fun onSwipeCardSuccess(trackData: TrackData) { - val track2Data = hexToAscii(trackData.secondTrackData) - Log.i("CPay SDK", "Parsed track2Data: $track2Data") - onSwipeCardSuccess(track2Data) - } - - @Throws(RemoteException::class) - override fun onSwipeCardFail() { - Log.i("CPay SDK", "Swipe card failed ") - } - - @Throws(RemoteException::class) - override fun onCancelSwipeCard() { - Log.i("CPay SDK", "Swipe card canceled") - } - } - ) - - return true - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/CPayPrinter.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/CPayPrinter.kt deleted file mode 100644 index a276440de..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/CPayPrinter.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.joinforage.android.example.pos.receipts - -import android.os.Bundle -import android.util.Log -import com.joinforage.android.example.pos.receipts.primitives.LinePartAlignment -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayout -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayoutLine -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLinePart -import com.pos.sdk.printer.PrinterDevice -import com.pos.sdk.printer.param.MultipleTextPrintItemParam -import com.pos.sdk.printer.param.PrintItemAlign -import com.pos.sdk.printer.param.TextPrintItemParam - -// TODO: write tests for the classes and methods in this file -// as they are business logic and are straightforward to test - -/** - * This class and the methods it uses effectively map - * ReceiptLayouts, ReceiptLayoutLines, and ReceiptLineParts - * to the data structures that the CPay SDK uses for printing - */ -internal class CPayPrinter(private val cpayPrinter: PrinterDevice) { - fun setLayout(layout: ReceiptLayout) { - // clear the buffer before setting anything new - cpayPrinter.clearBufferArea() - - // transform and write lines to the CPay printer's buffer - layout.lines.forEach { - CPayLine.of(it).addLineToPrinter(cpayPrinter) - } - } - fun print() { - val state = cpayPrinter.printSync(Bundle()) - Log.i("CPay SDK", "result is ${state.stateCode},msg is ${state.stateMsg}") - } -} - -/** - * An abstract class that represents an abstraction over - * lines on a receipt in the CPay SDK. This makes it easier - * to isolate and test business logic for handling the - * difference between single and multi part receipt lines - */ -internal abstract class CPayLine(protected val line: ReceiptLayoutLine) { - abstract fun addLineToPrinter(cpayPrinter: PrinterDevice) - companion object { - fun of(line: ReceiptLayoutLine): CPayLine { - return if (line.parts.size == 1) { - SingleCPayLine(line) - } else { - MultiCPayLine(line) - } - } - } -} - -internal class SingleCPayLine(line: ReceiptLayoutLine) : CPayLine(line) { - override fun addLineToPrinter(cpayPrinter: PrinterDevice) { - val single = processSinglePartLine(line) - cpayPrinter.addTextPrintItem(single) - } -} - -internal class MultiCPayLine(line: ReceiptLayoutLine) : CPayLine(line) { - override fun addLineToPrinter(cpayPrinter: PrinterDevice) { - val multi = processMultiPartLine(line) - cpayPrinter.addMultipleTextPrintItem(multi) - } -} - -/** - * A function that maps ReceiptLayoutLines with more than 1 part into - * CPay SDK representations of lines with more than one column. - * - * While TextPrintItemParam are the smallest representation in the - * CPay SDK, MultipleTextPrintItemParam represent lines with multiple - * parts are are composed of TextPrintItemParams - */ -internal fun processMultiPartLine(line: ReceiptLayoutLine): MultipleTextPrintItemParam { - val scales = line.parts.map { part -> part.colWeight }.toFloatArray() - val textPrintItems = line.parts.map { processLinePart(it) }.toTypedArray() - return MultipleTextPrintItemParam(scales, textPrintItems) -} - -/** - * a function that maps ReceiptLayoutLInes with exactly 1 part - * to CPay SDK representations of lines with exactly one column, - * called TextPrintItemParam. - */ -internal fun processSinglePartLine(line: ReceiptLayoutLine): TextPrintItemParam { - val part = line.parts[0] - return processLinePart(part) -} - -/** - * Here the smallest units of our ReceiptLayout data structure - * get transformed into the atomic units of receipts for the - * CPay SDK. In our case, the atomic units of a receipt are - * ReceiptLineParts. For CPay SDK, the atomic units are - * TextPrintItemParam - */ -internal fun processLinePart(part: ReceiptLinePart): TextPrintItemParam { - val item = TextPrintItemParam() - - // set the string content to be printed - item.content = part.content - - // set any formatting of the string content - item.textSize = part.format.textSize - item.lineSpace = part.format.lineSpace - item.isUnderLine = part.format.isUnderLine - item.isBold = part.format.isBold - item.isItalic = part.format.isItalic - item.isStrikeThruText = part.format.isStrikeThruText - - // set the alignment - when (part.alignment) { - LinePartAlignment.LEFT -> item.itemAlign = PrintItemAlign.LEFT - LinePartAlignment.CENTER -> item.itemAlign = PrintItemAlign.CENTER - LinePartAlignment.RIGHT -> item.itemAlign = PrintItemAlign.RIGHT - } - - // we're done - return item -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/ReceiptFormatting.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/ReceiptFormatting.kt deleted file mode 100644 index 386ae21ea..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/ReceiptFormatting.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.joinforage.android.example.pos.receipts - -/** - * A class to capture the different ways that text on - * a physical or digital receipt can be formatted. - * - * NOTE: that this excludes alignment as alignment by design - */ -data class ReceiptFormatting( - val textSize: Int = 24, - val lineSpace: Int = 1, - val isUnderLine: Boolean = false, - val isBold: Boolean = false, - val isItalic: Boolean = false, - val isStrikeThruText: Boolean = false -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/ReceiptPrinter.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/ReceiptPrinter.kt deleted file mode 100644 index 30fbb3009..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/ReceiptPrinter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.joinforage.android.example.pos.receipts - -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayout -import com.pos.sdk.printer.PrinterDevice - -/** - * A class that effectively transforms our internal representation - * of a receipt (i.e. a ReceiptLayout), into the datastructures - * that POS printers can understand. Right now it only supports - * printing with the CPay SDK, which is used for our POS terminal - */ -internal class ReceiptPrinter(private val layout: ReceiptLayout) { - internal fun printWithCPayTerminal(printer: PrinterDevice) { - val cpayPrinter = CPayPrinter(printer) - cpayPrinter.setLayout(layout) - cpayPrinter.print() - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLayout.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLayout.kt deleted file mode 100644 index 72e87acd1..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLayout.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.joinforage.android.example.pos.receipts.primitives - -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod -import java.text.SimpleDateFormat -import java.util.Locale - -fun formatReceiptTimestamp(timestamp: String): String? { - val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()) - val date = inputFormat.parse(timestamp) - val outputFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) - return date?.let { outputFormat.format(it) } -} - -/** - * The high level data structure that describes the text - * laid out on a receipt. This data structure is meant to - * be consumed any code that cares about. In practice, - * the ReceiptPrinter service consumes ReceiptLayout - * instance and translates it into the data structures that - * the CPay SDK cares about. And, the ReceiptViewer consumes - * a ReceiptLayout and displays a LinearLayout representation - * of the receipt. - * - * Ultimately a ReceiptLayout is a list of lines - * (called ReceiptLayoutLines) and each line has a list of - * parts (called ReceiptLineParts). parts are what is - * ultimately formatted, aligned, and contains text. - */ -internal open class ReceiptLayout( - internal vararg val lines: ReceiptLayoutLine -) { - companion object { - fun forError(msg: String) = ReceiptLayout( - ReceiptLayoutLine.singleColCenter(msg) - ) - - fun bottomPadding() = ReceiptLayout( - ReceiptLayoutLine.lineBreak(), - ReceiptLayoutLine.lineBreak(), - ReceiptLayoutLine.lineBreak() - ) - - private fun forShortAddressMerchant( - name: String, - line1: String, - cityStateZip: String, - merchTermId: String, - clerkId: String - ) = ReceiptLayout( - ReceiptLayoutLine.singleColCenter(name), - ReceiptLayoutLine.singleColCenter(line1), - ReceiptLayoutLine.singleColCenter(cityStateZip), - ReceiptLayoutLine.singleColLeft(merchTermId), - ReceiptLayoutLine.singleColLeft(clerkId) - ) - - private fun forLongAddressMerchant( - name: String, - line1: String, - line2: String, - cityStateZip: String, - merchTermId: String, - clerkId: String - ) = ReceiptLayout( - ReceiptLayoutLine.singleColCenter(name), - ReceiptLayoutLine.singleColCenter(line1), - ReceiptLayoutLine.singleColCenter(line2), - ReceiptLayoutLine.singleColCenter(cityStateZip), - ReceiptLayoutLine.singleColLeft(merchTermId), - ReceiptLayoutLine.singleColLeft(clerkId) - ) - - fun forMerchant(name: String, line1: String, line2: String?, city: String, state: String, zipCode: String, merchantTerminalId: String): ReceiptLayout { - val cityStateZip = "$city, $state, $zipCode" - val merchTermId = "MERCH TERM ID $merchantTerminalId" - val clerkId = "CLERK # 001" - if (line2 != null) { - return forLongAddressMerchant(name, line1, line2, cityStateZip, merchTermId, clerkId) - } - return forShortAddressMerchant(name, line1, cityStateZip, merchTermId, clerkId) - } - - fun forMerchant(merchant: Merchant?): ReceiptLayout { - if (merchant == null) { - return forError("Merchant details not found.") - } - - if (merchant.address == null) { - return forError("Merchant is missing address.") - } - - return forMerchant( - merchant.name, - merchant.address.line1, - merchant.address.line2, - merchant.address.city, - merchant.address.state, - merchant.address.zipcode, - merchant.ref - ) - } - - fun forTx( - terminalId: String, - txTimestamp: String, - paymentMethod: PosPaymentMethod?, - seqId: String?, - txType: String - ): ReceiptLayout { - if (paymentMethod == null) { - return forError("Missing tokenized PaymentMethod") - } - - val card = paymentMethod.card - val formattedTime = formatReceiptTimestamp(txTimestamp) - return ReceiptLayout( - ReceiptLayoutLine.singleColLeft("TERM ID $terminalId"), - ReceiptLayoutLine.singleColLeft("SEQ # $seqId"), - ReceiptLayoutLine.singleColLeft("$formattedTime"), // wrapped in string since its String? - ReceiptLayoutLine.singleColLeft("CARD# XXXXXXXXX${card?.last4}"), - ReceiptLayoutLine.singleColLeft("STATE: ${card?.state}"), - ReceiptLayoutLine.lineBreak(), - ReceiptLayoutLine.singleColCenter(txType) - ) - } - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLayoutLine.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLayoutLine.kt deleted file mode 100644 index 4ccd98539..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLayoutLine.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.joinforage.android.example.pos.receipts.primitives - -import com.joinforage.android.example.pos.receipts.ReceiptFormatting - -/** - * @param parts - the list of ReceiptLineParts that should * occupy this line. You can have as many as you want but - * a physical receipt is only so wide so in practice there - * probably shouldn't be more than 4. You can think of it - * like have 1 column per ReceiptLinePart in the list - */ -internal class ReceiptLayoutLine(internal vararg val parts: ReceiptLinePart) { - - // a bunch of factory methods that should make assembling - // the layout of an actual receipt easier. These are supposed - // to serve as lego pieces that you can stack together to - // create actually useful receipts - companion object { - internal fun lineBreak( - format: ReceiptFormatting = ReceiptFormatting() - ) = ReceiptLayoutLine( - ReceiptLinePart.left("", format) - ) - - internal fun singleColLeft( - col1: String, - format: ReceiptFormatting = ReceiptFormatting() - ) = ReceiptLayoutLine( - ReceiptLinePart.left(col1, format) - ) - - internal fun singleColCenter( - col1: String, - format: ReceiptFormatting = ReceiptFormatting() - ) = ReceiptLayoutLine( - ReceiptLinePart.center(col1, format) - ) - - internal fun singleColRight( - col1: String, - format: ReceiptFormatting = ReceiptFormatting() - ) = ReceiptLayoutLine( - ReceiptLinePart.right(col1, format) - ) - - internal fun doubleColLeft( - col1: String, - col2: String, - format: ReceiptFormatting = ReceiptFormatting() - ) = ReceiptLayoutLine( - ReceiptLinePart.left(col1, format, 1f), - ReceiptLinePart.left(col2, format, 1f) - ) - - internal fun doubleColRight( - col1: String, - col2: String, - format: ReceiptFormatting = ReceiptFormatting() - ) = ReceiptLayoutLine( - ReceiptLinePart.right(col1, format, 1f), - ReceiptLinePart.right(col2, format, 1f) - ) - - internal fun doubleColCenter( - col1: String, - col2: String, - format: ReceiptFormatting = ReceiptFormatting() - ) = ReceiptLayoutLine( - ReceiptLinePart.center(col1, format), - ReceiptLinePart.center(col2, format) - ) - - internal fun tripleCol( - col1: String, - col2: String, - col3: String, - format: ReceiptFormatting = ReceiptFormatting() - ) = ReceiptLayoutLine( - ReceiptLinePart.left(col1, format), - ReceiptLinePart.center(col2, format), - ReceiptLinePart.center(col3, format) - ) - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLinePart.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLinePart.kt deleted file mode 100644 index 25507f9f0..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/primitives/ReceiptLinePart.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.joinforage.android.example.pos.receipts.primitives - -import com.joinforage.android.example.pos.receipts.ReceiptFormatting - -/** - * an internal representation of the different - * text alignments we support - */ -internal enum class LinePartAlignment { - LEFT, CENTER, RIGHT -} - -/** - * @param content - the text value of this ReceiptLinePart - * @param alignment - the horizontal alignment of the text - * within the part - * @param format - the text formatting to be applied to this - * specific part (e.g. bold, italic, underline, etc) - * @param colWeight - the relative weight of this ReceiptLinePart - * compared to other ReceiptLinePart within the same - * ReceiptLayoutLine. For example if there were two parts in - * a line and the left part had weight of 1.5 and the right part - * had a weight of 0.5. Then the left part would be 3x the width - * and the right part would be 1x the width. This (colWeight) - * along with alignment are the two mechanism available for - * hacking together the preferred horizontal layout of text - */ -internal class ReceiptLinePart( - internal val content: String, - internal val alignment: LinePartAlignment, - internal val format: ReceiptFormatting, - internal val colWeight: Float = 1f -) { - // these are a bunch of helper factory methods that should make - // using ReceiptLineParts in practice easier since you only need - // to really specify content and choose an alignment factory - companion object { - fun left(content: String, format: ReceiptFormatting, colWeight: Float) = - ReceiptLinePart(content, LinePartAlignment.LEFT, format, colWeight) - fun left(content: String, format: ReceiptFormatting) = - ReceiptLinePart(content, LinePartAlignment.LEFT, format) - fun center(content: String, format: ReceiptFormatting, colWeight: Float) = - ReceiptLinePart(content, LinePartAlignment.CENTER, format, colWeight) - fun center(content: String, format: ReceiptFormatting) = - ReceiptLinePart(content, LinePartAlignment.CENTER, format) - fun right(content: String, format: ReceiptFormatting, colWeight: Float) = - ReceiptLinePart(content, LinePartAlignment.RIGHT, format, colWeight) - fun right(content: String, format: ReceiptFormatting) = - ReceiptLinePart(content, LinePartAlignment.RIGHT, format) - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/BalanceInquiryReceipt.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/BalanceInquiryReceipt.kt deleted file mode 100644 index c453d738c..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/BalanceInquiryReceipt.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.joinforage.android.example.pos.receipts.templates - -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayout -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayoutLine -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod - -internal fun getApprovedHeader() = ReceiptLayout( - ReceiptLayoutLine.singleColCenter("*****APPROVED*****") -) -internal fun getDeclinedHeader(balanceCheckError: String) = ReceiptLayout( - ReceiptLayoutLine.singleColCenter("*****DECLINED*****"), - ReceiptLayoutLine.singleColCenter(balanceCheckError) -) -internal fun getHeader(balanceCheckError: String?) = - if (balanceCheckError.isNullOrEmpty()) { - getApprovedHeader() - } else { - getDeclinedHeader(balanceCheckError) - } - -internal class BalanceInquiryReceipt( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - balanceCheckError: String? -) : BaseReceiptTemplate(merchant, terminalId, paymentMethod) { - private val _balance = paymentMethod?.balance - override val timestamp = _balance?.updated.toString() - override val seqNumber: String = _balance?.sequenceNumber.toString() - override val cashBal: String = _balance?.non_snap!! - override val snapBal: String = _balance?.snap!! - override val title = "BALANCE INQUIRY" - override val mainContent = ReceiptLayout( - *getHeader(balanceCheckError).lines, - ReceiptLayoutLine.doubleColCenter("SNAP BAL", snapBal), - ReceiptLayoutLine.doubleColCenter("EBT CASH BAL", cashBal) - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/BaseReceiptTemplate.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/BaseReceiptTemplate.kt deleted file mode 100644 index 2d2ef08df..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/BaseReceiptTemplate.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.joinforage.android.example.pos.receipts.templates - -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayout -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod - -internal abstract class BaseReceiptTemplate( - private val _merchant: Merchant?, - private val _terminalId: String, - private val _paymentMethod: PosPaymentMethod? -) { - abstract val timestamp: String - abstract val seqNumber: String - abstract val title: String - abstract val mainContent: ReceiptLayout - abstract val snapBal: String - abstract val cashBal: String - - open fun getReceiptLayout() = ReceiptLayout( - *ReceiptLayout.forMerchant(_merchant).lines, - *ReceiptLayout.forTx( - _terminalId, - timestamp, - _paymentMethod, - seqNumber, - title - ).lines, - *mainContent.lines, - *ReceiptLayout.bottomPadding().lines - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashPurchaseTxReceipt.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashPurchaseTxReceipt.kt deleted file mode 100644 index cc803ed47..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashPurchaseTxReceipt.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.joinforage.android.example.pos.receipts.templates.txs - -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod - -internal class CashPurchaseTxReceipt : TxReceiptTemplate { - private var cashAmt: String = receipt.ebtCashAmount - constructor( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - receipt: Receipt, - title: String - ) : super(merchant, terminalId, paymentMethod, receipt, title) { - cashAmt = if (isRefund(receipt)) negateAmt(cashAmt) else cashAmt - } - - override val txContent = CashPaymentLayout( - snapBal, - cashBal, - cashAmt - ).getLayout() -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashPurchaseWithCashbackTxReceipt.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashPurchaseWithCashbackTxReceipt.kt deleted file mode 100644 index 96e9d6a2b..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashPurchaseWithCashbackTxReceipt.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.joinforage.android.example.pos.receipts.templates.txs - -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod - -internal class CashPurchaseWithCashbackTxReceipt : TxReceiptTemplate { - private var cashAmt: String = receipt.ebtCashAmount - private val cashBackAmt: String = receipt.cashBackAmount - constructor( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - receipt: Receipt, - title: String - ) : super(merchant, terminalId, paymentMethod, receipt, title) { - cashAmt = if (isRefund(receipt)) negateAmt(cashAmt) else cashAmt - } - - override val txContent = CashPurchaseWithCashbackLayout( - snapBal, - cashBal, - cashAmt, - cashBackAmt - ).getLayout() -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashWithdrawalTxReceipt.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashWithdrawalTxReceipt.kt deleted file mode 100644 index d512df922..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/CashWithdrawalTxReceipt.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.joinforage.android.example.pos.receipts.templates.txs - -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod - -internal class CashWithdrawalTxReceipt : TxReceiptTemplate { - // TODO: find out whether it's even possible to negate a cashback tx - constructor( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - receipt: Receipt, - title: String - ) : super(merchant, terminalId, paymentMethod, receipt, title) - - override val txContent = CashWithdrawalLayout( - snapBal, - cashBal, - receipt.ebtCashAmount - ).getLayout() -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/SnapPurchaseTxReceipt.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/SnapPurchaseTxReceipt.kt deleted file mode 100644 index 70b62f773..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/SnapPurchaseTxReceipt.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.joinforage.android.example.pos.receipts.templates.txs - -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod - -internal class SnapPurchaseTxReceipt : TxReceiptTemplate { - private var snapAmt = receipt.snapAmount - constructor( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - receipt: Receipt, - title: String - ) : super(merchant, terminalId, paymentMethod, receipt, title) { - snapAmt = if (isRefund(receipt)) negateAmt(snapAmt) else snapAmt - } - - override val txContent = SnapPaymentLayout( - snapBal, - cashBal, - snapAmt - ).getLayout() -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxDataLayout.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxDataLayout.kt deleted file mode 100644 index 6cdc93fdf..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxDataLayout.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.joinforage.android.example.pos.receipts.templates.txs - -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayout -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayoutLine - -internal abstract class TxDataLayout { - abstract val snapAmt: String - abstract val cashAmt: String - abstract val snapBal: String - abstract val cashBal: String - open val withdrawalAmt: String = ZERO_TX - val header = ReceiptLayoutLine.tripleCol("", "TX AMT", "END BAL") - val snap - get() = ReceiptLayoutLine.tripleCol("SNAP", snapAmt, snapBal) - val cash - get() = ReceiptLayoutLine.tripleCol("CASH", cashAmt, cashBal) - val withdrawal - get() = ReceiptLayoutLine.tripleCol("CS W/D", withdrawalAmt, cashBal) - - fun getLayout() = if (withdrawalAmt == ZERO_TX) { - ReceiptLayout(header, snap, cash) - } else { - ReceiptLayout(header, snap, cash, withdrawal) - } -} - -internal class SnapPaymentLayout( - override val snapBal: String, - override val cashBal: String, - override val snapAmt: String -) : TxDataLayout() { - override val cashAmt: String = ZERO_TX -} - -internal class CashPaymentLayout( - override val snapBal: String, - override val cashBal: String, - override val cashAmt: String -) : TxDataLayout() { - override val snapAmt: String = ZERO_TX -} - -internal class CashPurchaseWithCashbackLayout( - override val snapBal: String, - override val cashBal: String, - override val cashAmt: String, - override val withdrawalAmt: String -) : TxDataLayout() { - override val snapAmt: String = ZERO_TX -} - -internal class CashWithdrawalLayout( - override val snapBal: String, - override val cashBal: String, - override val withdrawalAmt: String -) : TxDataLayout() { - override val snapAmt: String = ZERO_TX - override val cashAmt: String = withdrawalAmt -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxReceiptTemplate.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxReceiptTemplate.kt deleted file mode 100644 index 076d41d44..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxReceiptTemplate.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.joinforage.android.example.pos.receipts.templates.txs - -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayout -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayoutLine -import com.joinforage.android.example.pos.receipts.templates.BaseReceiptTemplate -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod - -fun negateAmt(amt: String) = "-$amt" - -internal fun isApproved(receipt: Receipt) = receipt.message == "Approved" -internal fun getTxApprovedHeader() = ReceiptLayout( - ReceiptLayoutLine.singleColCenter("*****APPROVED*****") -) -internal fun getTxDeclinedHeader(receipt: Receipt) = ReceiptLayout( - ReceiptLayoutLine.singleColCenter("*****DECLINED*****"), - ReceiptLayoutLine.singleColCenter(receipt.message) -) -internal fun getTxVoidedHeader() = ReceiptLayout( - ReceiptLayoutLine.singleColCenter("*****VOIDED*****") -) -internal fun getTxOutcome(receipt: Receipt): ReceiptLayout { - if (receipt.isVoided) { - return getTxVoidedHeader() - } else if (isApproved(receipt)) return getTxApprovedHeader() - return getTxDeclinedHeader(receipt) -} - -internal abstract class TxReceiptTemplate( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - protected val receipt: Receipt, - title: String -) : BaseReceiptTemplate( - merchant, - terminalId, - paymentMethod -) { - abstract val txContent: ReceiptLayout - - override val seqNumber: String = receipt.sequenceNumber - override val timestamp: String = receipt.created - override val snapBal: String = receipt.balance?.snap ?: "--" - override val cashBal: String = receipt.balance?.nonSnap ?: "--" - override val title = title - override val mainContent: ReceiptLayout - get() = ReceiptLayout( - *getTxOutcome(receipt).lines, - *txContent.lines - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxType.kt b/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxType.kt deleted file mode 100644 index fd495b8b1..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/pos/receipts/templates/txs/TxType.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.joinforage.android.example.pos.receipts.templates.txs - -import com.joinforage.android.example.ui.pos.data.FundingType -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.TransactionType - -fun isPayment(receipt: Receipt) = receipt.transactionType == "Payment" -fun isRefund(receipt: Receipt) = receipt.transactionType == "Refund" - -val ZERO_TX = "0.00" -fun spentSnap(receipt: Receipt) = receipt.snapAmount != ZERO_TX -fun spentEbtCash(receipt: Receipt) = receipt.ebtCashAmount != ZERO_TX -fun withdrewEbtCash(receipt: Receipt) = receipt.cashBackAmount != ZERO_TX - -enum class TxType(val title: String) { - SNAP_PAYMENT("SNAP PAYMENT"), - CASH_PAYMENT("CASH PAYMENT"), - CASH_WITHDRAWAL("CASH WITHDRAWAL"), - CASH_PURCHASE_WITH_CASHBACK("CASH PURCHASE WITH CASHBACK"), - REFUND_SNAP_PAYMENT("REFUND SNAP PAYMENT"), - REFUND_CASH_PAYMENT("REFUND CASH PAYMENT"), - REFUND_CASH_WITHDRAWAL("REFUND CASH WITHDRAWAL"), - REFUND_CASH_PURCHASE_WITH_CASHBACK("REFUND CASH PURCHASE WITH CASHBACK"), - UNKNOWN("UNKNOWN"); - - companion object { - fun forReceipt(receipt: Receipt): TxType { - if (isPayment(receipt)) { - if (spentSnap(receipt)) { - return SNAP_PAYMENT - } - if (spentEbtCash(receipt) && withdrewEbtCash(receipt)) { - return CASH_PURCHASE_WITH_CASHBACK - } - if (!spentEbtCash(receipt) && withdrewEbtCash(receipt)) { - return CASH_WITHDRAWAL - } - if (spentEbtCash(receipt) && !withdrewEbtCash(receipt)) { - return CASH_PAYMENT - } - } - - if (isRefund(receipt)) { - if (spentSnap(receipt)) { - return REFUND_SNAP_PAYMENT - } - if (spentEbtCash(receipt) && withdrewEbtCash(receipt)) { - return REFUND_CASH_PURCHASE_WITH_CASHBACK - } - if (!spentEbtCash(receipt) && withdrewEbtCash(receipt)) { - return REFUND_CASH_WITHDRAWAL - } - if (spentEbtCash(receipt) && !withdrewEbtCash(receipt)) { - return REFUND_CASH_PAYMENT - } - } - - return UNKNOWN - } - - fun forPayment(transactionType: String?, fundingType: String): TxType { - if (transactionType == TransactionType.Withdrawal.value) { - return CASH_WITHDRAWAL - } - if (transactionType == TransactionType.PurchaseWithCashBack.value) { - return CASH_PURCHASE_WITH_CASHBACK - } - if (fundingType == FundingType.EBTSnap.value) { - return SNAP_PAYMENT - } - if (fundingType == FundingType.EBTCash.value) { - return CASH_PAYMENT - } - return UNKNOWN - } - - fun forRefund(transactionType: String?, fundingType: String): TxType { - if (transactionType == TransactionType.Withdrawal.value) { - return REFUND_CASH_WITHDRAWAL - } - if (transactionType == TransactionType.PurchaseWithCashBack.value) { - return REFUND_CASH_PURCHASE_WITH_CASHBACK - } - if (fundingType == FundingType.EBTSnap.value) { - return REFUND_SNAP_PAYMENT - } - if (fundingType == FundingType.EBTCash.value) { - return REFUND_CASH_PAYMENT - } - return UNKNOWN - } - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSComposeApp.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSComposeApp.kt deleted file mode 100644 index f8291d8db..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSComposeApp.kt +++ /dev/null @@ -1,764 +0,0 @@ -package com.joinforage.android.example.ui.pos - -import android.util.Log -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import com.joinforage.android.example.R -import com.joinforage.android.example.pos.k9sdk.K9SDK -import com.joinforage.android.example.pos.receipts.templates.txs.TxType -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.data.PosPaymentRequest -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.ReceiptBalance -import com.joinforage.android.example.ui.pos.data.RefundUIState -import com.joinforage.android.example.ui.pos.screens.ActionSelectionScreen -import com.joinforage.android.example.ui.pos.screens.MerchantSetupScreen -import com.joinforage.android.example.ui.pos.screens.balance.BalanceResultScreen -import com.joinforage.android.example.ui.pos.screens.deferred.DeferredPaymentCaptureResultScreen -import com.joinforage.android.example.ui.pos.screens.deferred.DeferredPaymentRefundResultScreen -import com.joinforage.android.example.ui.pos.screens.payment.EBTCashPurchaseScreen -import com.joinforage.android.example.ui.pos.screens.payment.EBTCashPurchaseWithCashBackScreen -import com.joinforage.android.example.ui.pos.screens.payment.EBTCashWithdrawalScreen -import com.joinforage.android.example.ui.pos.screens.payment.EBTSnapPurchaseScreen -import com.joinforage.android.example.ui.pos.screens.payment.PaymentResultScreen -import com.joinforage.android.example.ui.pos.screens.payment.PaymentTypeSelectionScreen -import com.joinforage.android.example.ui.pos.screens.refund.RefundDetailsScreen -import com.joinforage.android.example.ui.pos.screens.refund.RefundResultScreen -import com.joinforage.android.example.ui.pos.screens.shared.MagSwipePANEntryScreen -import com.joinforage.android.example.ui.pos.screens.shared.ManualPANEntryScreen -import com.joinforage.android.example.ui.pos.screens.shared.PANMethodSelectionScreen -import com.joinforage.android.example.ui.pos.screens.shared.PINEntryScreen -import com.joinforage.android.example.ui.pos.screens.voids.VoidPaymentResultScreen -import com.joinforage.android.example.ui.pos.screens.voids.VoidPaymentScreen -import com.joinforage.android.example.ui.pos.screens.voids.VoidRefundResultScreen -import com.joinforage.android.example.ui.pos.screens.voids.VoidRefundScreen -import com.joinforage.android.example.ui.pos.screens.voids.VoidTypeSelectionScreen -import com.joinforage.forage.android.ui.ForagePANEditText -import com.joinforage.forage.android.ui.ForagePINEditText -import java.sql.Timestamp -import java.text.SimpleDateFormat -import java.util.Locale - -enum class POSScreen(@StringRes val title: Int) { - MerchantSetupScreen(title = R.string.title_pos_merchant_setup), - ActionSelectionScreen(title = R.string.title_pos_action_selection), - BIChoosePANMethodScreen(title = R.string.title_pos_balance_inquiry), - BIManualPANEntryScreen(title = R.string.title_pos_bi_manual_pan_entry), - BIMagSwipePANEntryScreen(title = R.string.title_pos_bi_mag_swipe_pan_entry), - BIPINEntryScreen(title = R.string.title_pos_bi_pin_entry), - BIResultScreen(title = R.string.title_pos_balance_inquiry_result), - PAYTransactionTypeSelectionScreen(title = R.string.title_pos_payment_type_selection_screen), - PAYChoosePANMethodScreen(title = R.string.title_pos_payment_choose_pan_method), - PAYSnapPurchaseScreen(title = R.string.title_pos_payment_snap_purchase_screen), - PAYEBTCashPurchaseScreen(title = R.string.title_pos_payment_ebt_cash), - PAYEBTCashWithdrawalScreen(title = R.string.title_pos_payment_cash_withdrawal), - PAYEBTCashPurchaseWithCashBackScreen(title = R.string.title_pos_payment_with_cashback), - PAYManualPANEntryScreen(title = R.string.title_pos_payment_manual_pan_entry), - PAYMagSwipePANEntryScreen(title = R.string.title_pos_payment_swipe_card_entry), - PAYPINEntryScreen(title = R.string.title_pos_payment_pin_entry), - PAYResultScreen(title = R.string.title_pos_payment_receipt), - PAYErrorResultScreen(title = R.string.title_pos_payment_receipt), - REFUNDDetailsScreen(title = R.string.title_pos_refund_details), - REFUNDPINEntryScreen(title = R.string.title_pos_refund_pin_entry), - REFUNDResultScreen(title = R.string.title_pos_refund_result), - REFUNDErrorResultScreen(title = R.string.title_pos_refund_result), - VOIDTransactionTypeSelectionScreen(title = R.string.title_pos_void_action_selection), - VOIDPaymentScreen(title = R.string.title_pos_void_payment), - VOIDRefundScreen(title = R.string.title_pos_void_refund), - VOIDPaymentResultScreen(title = R.string.title_pos_void_payment_result), - VOIDRefundResultScreen(title = R.string.title_pos_void_refund_result), - DEFERPaymentCaptureResultScreen(title = R.string.title_pos_defer_payment_result), - DEFERPaymentRefundResultScreen(title = R.string.title_pos_defer_refund_result) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun POSComposeApp( - viewModel: POSViewModel = viewModel(), - navController: NavHostController = rememberNavController() -) { - val backStackEntry by navController.currentBackStackEntryAsState() - val currentScreen = POSScreen.valueOf( - backStackEntry?.destination?.route ?: POSScreen.MerchantSetupScreen.name - ) - - var panElement: ForagePANEditText? by rememberSaveable { - mutableStateOf(null) - } - - var pinElement: ForagePINEditText? by rememberSaveable { - mutableStateOf(null) - } - - val context = LocalContext.current - val k9SDK by remember { - mutableStateOf(K9SDK().init(context)) - } - - var pageTitle: String? by rememberSaveable { - mutableStateOf(null) - } - - Scaffold( - topBar = { - TopAppBar( - title = { Text(pageTitle ?: stringResource(currentScreen.title)) }, - navigationIcon = { - if (navController.previousBackStackEntry != null) { - IconButton(onClick = { - val isPayTypeScreen = navController.previousBackStackEntry?.destination?.route == POSScreen.PAYTransactionTypeSelectionScreen.name - if (isPayTypeScreen) { - pageTitle = null - } - navController.navigateUp() - }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = stringResource(R.string.pos_back_button) - ) - } - } - }, - actions = { - val isMerchantScreen = navController.currentBackStackEntry?.destination?.route == POSScreen.MerchantSetupScreen.name - val isActionScreen = navController.currentBackStackEntry?.destination?.route == POSScreen.ActionSelectionScreen.name - if (!isMerchantScreen && !isActionScreen) { - IconButton( - onClick = { - navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) - pageTitle = null - viewModel.resetUiState() - }, - modifier = Modifier.withTestId("pos_back_to_action_selection_button") - ) { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = stringResource(R.string.pos_restart) - ) - } - } - } - ) - } - ) { innerPadding -> - val uiState by viewModel.uiState.collectAsState() - - NavHost( - navController = navController, - startDestination = POSScreen.MerchantSetupScreen.name, - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(bottom = 72.dp, start = 16.dp, end = 16.dp) - ) { - composable(route = POSScreen.MerchantSetupScreen.name) { - MerchantSetupScreen( - terminalId = k9SDK.terminalId, - merchantId = uiState.merchantId, - sessionToken = uiState.sessionToken, - onSaveButtonClicked = { merchantId, sessionToken -> - viewModel.setSessionToken(sessionToken) - viewModel.setMerchantId(merchantId, onSuccess = { - navController.navigate(POSScreen.ActionSelectionScreen.name) - }) - } - ) - } - composable(route = POSScreen.ActionSelectionScreen.name) { - ActionSelectionScreen( - merchantDetails = uiState.merchant, - onBackButtonClicked = { navController.popBackStack(POSScreen.MerchantSetupScreen.name, inclusive = false) }, - onBalanceButtonClicked = { navController.navigate(POSScreen.BIChoosePANMethodScreen.name) }, - onPaymentButtonClicked = { navController.navigate(POSScreen.PAYTransactionTypeSelectionScreen.name) }, - onRefundButtonClicked = { navController.navigate(POSScreen.REFUNDDetailsScreen.name) }, - onVoidButtonClicked = { navController.navigate(POSScreen.VOIDTransactionTypeSelectionScreen.name) } - ) - } - composable(route = POSScreen.BIChoosePANMethodScreen.name) { - PANMethodSelectionScreen( - onManualEntryButtonClicked = { - viewModel.resetTokenizationError() - navController.navigate(POSScreen.BIManualPANEntryScreen.name) - }, - onSwipeButtonClicked = { - viewModel.resetTokenizationError() - navController.navigate(POSScreen.BIMagSwipePANEntryScreen.name) - }, - onBackButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) } - ) - } - composable(route = POSScreen.BIManualPANEntryScreen.name) { - ManualPANEntryScreen( - posForageConfig = uiState.posForageConfig, - onSubmitButtonClicked = { - if (panElement != null) { - panElement!!.clearFocus() - viewModel.tokenizeEBTCard( - context = context, - panElement as ForagePANEditText, - k9SDK.terminalId, - onSuccess = { - if (it?.ref != null) { - Log.i("POSComposeApp", "Successfully tokenized EBT card with ref: $it.ref") - viewModel.resetPinActionErrors() - navController.navigate(POSScreen.BIPINEntryScreen.name) - } - } - ) - } - }, - onBackButtonClicked = { navController.popBackStack(POSScreen.BIChoosePANMethodScreen.name, inclusive = false) }, - withPanElementReference = { panElement = it }, - errorText = uiState.tokenizationError - ) - } - composable(route = POSScreen.BIMagSwipePANEntryScreen.name) { - MagSwipePANEntryScreen( - onLaunch = { - k9SDK.listenForMagneticCardSwipe { track2Data -> - viewModel.tokenizeEBTCard(context, track2Data, k9SDK.terminalId) { - if (it?.ref != null) { - Log.i("POSComposeApp", "Successfully tokenized EBT card with ref: $it.ref") - viewModel.resetPinActionErrors() - navController.navigate(POSScreen.BIPINEntryScreen.name) - } - } - } - }, - onBackButtonClicked = { navController.popBackStack(POSScreen.BIChoosePANMethodScreen.name, inclusive = false) }, - errorText = uiState.tokenizationError - ) - } - composable(route = POSScreen.BIPINEntryScreen.name) { - PINEntryScreen( - posForageConfig = uiState.posForageConfig, - paymentMethodRef = uiState.tokenizedPaymentMethod?.ref, - onSubmitButtonClicked = { - if (pinElement != null && uiState.tokenizedPaymentMethod?.ref != null) { - pinElement!!.clearFocus() - viewModel.checkEBTCardBalance( - context = context, - pinElement as ForagePINEditText, - paymentMethodRef = uiState.tokenizedPaymentMethod!!.ref, - k9SDK.terminalId, - onSuccess = { - if (it != null) { - Log.i("POSComposeApp", "Successfully checked balance of EBT card: $it") - navController.navigate(POSScreen.BIResultScreen.name) - } - } - ) - } - }, - onBackButtonClicked = { navController.popBackStack(POSScreen.BIManualPANEntryScreen.name, inclusive = false) }, - withPinElementReference = { pinElement = it }, - errorText = uiState.balanceCheckError - ) - } - composable(route = POSScreen.BIResultScreen.name) { - BalanceResultScreen( - merchant = uiState.merchant, - terminalId = uiState.tokenizedPaymentMethod?.balance?.posTerminal?.terminalId ?: "Unknown", - paymentMethod = uiState.tokenizedPaymentMethod, - balanceCheckError = uiState.balanceCheckError, - onBackButtonClicked = { navController.popBackStack(POSScreen.BIPINEntryScreen.name, inclusive = false) }, - onDoneButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) } - ) - } - composable(route = POSScreen.PAYTransactionTypeSelectionScreen.name) { - PaymentTypeSelectionScreen( - onSnapPurchaseClicked = { - navController.navigate(POSScreen.PAYSnapPurchaseScreen.name) - pageTitle = "EBT SNAP Purchase" - }, - onCashPurchaseClicked = { - navController.navigate(POSScreen.PAYEBTCashPurchaseScreen.name) - pageTitle = "EBT Cash Purchase" - }, - onCashWithdrawalClicked = { - navController.navigate(POSScreen.PAYEBTCashWithdrawalScreen.name) - pageTitle = "EBT Cash Withdrawal" - }, - onCashPurchaseCashbackClicked = { - navController.navigate(POSScreen.PAYEBTCashPurchaseWithCashBackScreen.name) - pageTitle = "EBT Cash Purchase + Cashback" - }, - onCancelButtonClicked = { - navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) - } - ) - } - composable(route = POSScreen.PAYSnapPurchaseScreen.name) { - EBTSnapPurchaseScreen( - onConfirmButtonClicked = { snapAmount -> - val payment = PosPaymentRequest.forSnapPayment(snapAmount, k9SDK.terminalId) - viewModel.setLocalPayment(payment = payment) - navController.navigate(POSScreen.PAYChoosePANMethodScreen.name) - }, - onCancelButtonClicked = { - navController.popBackStack(POSScreen.PAYTransactionTypeSelectionScreen.name, inclusive = false) - pageTitle = null - } - ) - } - composable(route = POSScreen.PAYEBTCashPurchaseScreen.name) { - EBTCashPurchaseScreen( - onConfirmButtonClicked = { ebtCashAmount -> - val payment = - PosPaymentRequest.forEbtCashPayment(ebtCashAmount, k9SDK.terminalId) - viewModel.setLocalPayment(payment) - navController.navigate(POSScreen.PAYChoosePANMethodScreen.name) - }, - onCancelButtonClicked = { - navController.popBackStack( - POSScreen.PAYTransactionTypeSelectionScreen.name, - inclusive = false - ) - pageTitle = null - } - ) - } - composable(route = POSScreen.PAYEBTCashWithdrawalScreen.name) { - EBTCashWithdrawalScreen( - onConfirmButtonClicked = { ebtCashWithdrawalAmount -> - val payment = PosPaymentRequest.forEbtCashWithdrawal( - ebtCashWithdrawalAmount, - k9SDK.terminalId - ) - viewModel.setLocalPayment(payment) - navController.navigate(POSScreen.PAYChoosePANMethodScreen.name) - }, - onCancelButtonClicked = { - navController.popBackStack( - POSScreen.PAYTransactionTypeSelectionScreen.name, - inclusive = false - ) - pageTitle = null - } - ) - } - composable(route = POSScreen.PAYEBTCashPurchaseWithCashBackScreen.name) { - EBTCashPurchaseWithCashBackScreen( - onConfirmButtonClicked = { ebtCashAmount, cashBackAmount -> - val payment = PosPaymentRequest.forEbtCashPaymentWithCashBack( - ebtCashAmount, - cashBackAmount, - k9SDK.terminalId - ) - viewModel.setLocalPayment(payment) - navController.navigate(POSScreen.PAYChoosePANMethodScreen.name) - }, - onCancelButtonClicked = { - navController.popBackStack( - POSScreen.PAYTransactionTypeSelectionScreen.name, - inclusive = false - ) - pageTitle = null - } - ) - } - composable(route = POSScreen.PAYChoosePANMethodScreen.name) { - PANMethodSelectionScreen( - onManualEntryButtonClicked = { - viewModel.resetTokenizationError() - navController.navigate(POSScreen.PAYManualPANEntryScreen.name) - }, - onSwipeButtonClicked = { - viewModel.resetTokenizationError() - navController.navigate(POSScreen.PAYMagSwipePANEntryScreen.name) - }, - onBackButtonClicked = { navController.popBackStack(POSScreen.PAYTransactionTypeSelectionScreen.name, inclusive = false) } - ) - } - composable(route = POSScreen.PAYManualPANEntryScreen.name) { - ManualPANEntryScreen( - posForageConfig = uiState.posForageConfig, - onSubmitButtonClicked = { - Log.i("POSComposeApp", "Calling onSubmitButtonClicked in ManualPANEntryScreen in PAYChoosePANMethodScreen") - if (panElement != null) { - panElement!!.clearFocus() - viewModel.tokenizeEBTCard( - context, - panElement as ForagePANEditText, - k9SDK.terminalId, - onSuccess = { tokenizedCard -> - Log.i("POSComposeApp", "payment method? — $tokenizedCard") - if (tokenizedCard?.ref != null && uiState.localPayment != null) { - Log.i("POSComposeApp", "Successfully tokenized EBT card with ref: $tokenizedCard.ref") - val payment = uiState.localPayment!!.copy(paymentMethodRef = tokenizedCard.ref) - viewModel.createPayment(payment = payment, onSuccess = { serverPayment -> - if (serverPayment.ref !== null) { - viewModel.resetPinActionErrors() - navController.navigate(POSScreen.PAYPINEntryScreen.name) - } - }) - } - } - ) - } - }, - onBackButtonClicked = { navController.popBackStack(POSScreen.PAYChoosePANMethodScreen.name, inclusive = false) }, - withPanElementReference = { panElement = it }, - errorText = uiState.tokenizationError ?: uiState.createPaymentError - ) - } - composable(route = POSScreen.PAYMagSwipePANEntryScreen.name) { - MagSwipePANEntryScreen( - onLaunch = { - k9SDK.listenForMagneticCardSwipe { track2Data -> - viewModel.tokenizeEBTCard(context, track2Data, k9SDK.terminalId) { tokenizedCard -> - if (tokenizedCard?.ref != null) { - Log.i("POSComposeApp", "Successfully tokenized EBT card with ref: $tokenizedCard.ref") - val payment = uiState.localPayment!!.copy(paymentMethodRef = tokenizedCard.ref) - viewModel.createPayment(payment = payment, onSuccess = { serverPayment -> - if (serverPayment.ref !== null) { - viewModel.resetPinActionErrors() - navController.navigate(POSScreen.PAYPINEntryScreen.name) - } - }) - } - } - } - }, - onBackButtonClicked = { navController.popBackStack(POSScreen.PAYChoosePANMethodScreen.name, inclusive = false) }, - errorText = uiState.tokenizationError ?: uiState.createPaymentError - ) - } - composable(route = POSScreen.PAYPINEntryScreen.name) { - PINEntryScreen( - posForageConfig = uiState.posForageConfig, - paymentMethodRef = uiState.createPaymentResponse?.paymentMethod, - onSubmitButtonClicked = { - if (pinElement != null && uiState.createPaymentResponse?.ref != null) { - pinElement!!.clearFocus() - viewModel.capturePayment( - context = context, - foragePinEditText = pinElement as ForagePINEditText, - terminalId = k9SDK.terminalId, - paymentRef = uiState.createPaymentResponse!!.ref!!, - onSuccess = { - navController.navigate(POSScreen.PAYResultScreen.name) - }, - onFailure = { sequenceNumber -> - if (sequenceNumber != null) { - navController.navigate(POSScreen.PAYErrorResultScreen.name) - } - } - ) - } - }, - onDeferButtonClicked = { - if (pinElement != null && uiState.createPaymentResponse?.ref != null) { - pinElement!!.clearFocus() - viewModel.deferPaymentCapture( - context = context, - foragePinEditText = pinElement as ForagePINEditText, - terminalId = k9SDK.terminalId, - paymentRef = uiState.createPaymentResponse!!.ref!!, - onSuccess = { - navController.navigate(POSScreen.DEFERPaymentCaptureResultScreen.name) - }, - onFailure = { - navController.navigate(POSScreen.PAYErrorResultScreen.name) - } - ) - } - }, - onBackButtonClicked = { navController.popBackStack(POSScreen.PAYTransactionTypeSelectionScreen.name, inclusive = false) }, - withPinElementReference = { pinElement = it }, - errorText = uiState.capturePaymentError - ) - } - composable(route = POSScreen.DEFERPaymentCaptureResultScreen.name) { - DeferredPaymentCaptureResultScreen( - terminalId = k9SDK.terminalId, - paymentRef = uiState.createPaymentResponse!!.ref!!, - onBackButtonClicked = { navController.popBackStack(POSScreen.PAYPINEntryScreen.name, inclusive = false) }, - onDoneButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) } - ) - } - composable(route = POSScreen.DEFERPaymentRefundResultScreen.name) { - DeferredPaymentRefundResultScreen( - terminalId = k9SDK.terminalId, - paymentRef = uiState.localRefundState!!.paymentRef, - onBackButtonClicked = { navController.popBackStack(POSScreen.REFUNDPINEntryScreen.name, inclusive = false) }, - onDoneButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) } - ) - } - composable(route = POSScreen.PAYErrorResultScreen.name) { - val errorReceipt = uiState.capturePaymentResponse?.receipt ?: Receipt( - refNumber = uiState.createPaymentResponse!!.ref!!, - isVoided = true, // We should only use this manual receipt when in the late-notification scenario - snapAmount = (if (uiState.createPaymentResponse!!.fundingType == "ebt_snap") uiState.createPaymentResponse!!.amount else "0.00").toString(), - ebtCashAmount = (if (uiState.createPaymentResponse!!.fundingType == "ebt_cash") uiState.createPaymentResponse!!.amount else "0.00").toString(), - cashBackAmount = uiState.createPaymentResponse!!.cashBackAmount.toString(), - otherAmount = "0.00", - salesTaxApplied = "0.00", - last4 = uiState.tokenizedPaymentMethod!!.card!!.last4, - transactionType = "Payment", - sequenceNumber = uiState.capturePaymentResponse?.sequenceNumber ?: uiState.createPaymentResponse?.sequenceNumber ?: "ERROR", - balance = ReceiptBalance( - id = 0.toDouble(), - snap = uiState.tokenizedPaymentMethod!!.balance?.snap ?: "ERROR", - nonSnap = uiState.tokenizedPaymentMethod!!.balance?.non_snap ?: "ERROR", - updated = uiState.tokenizedPaymentMethod!!.balance?.updated ?: "ERROR" - ), - created = uiState.createPaymentResponse!!.created, - message = uiState.capturePaymentError ?: "Unknown Error" - ) - - PaymentResultScreen( - merchant = uiState.merchant, - terminalId = uiState.capturePaymentResponse?.posTerminal?.terminalId ?: "Unknown", - paymentMethod = uiState.tokenizedPaymentMethod, - paymentRef = errorReceipt.refNumber, - txType = uiState.localPayment?.let { TxType.forPayment(it.transactionType, it.fundingType) } ?: TxType.forReceipt(errorReceipt), - receipt = errorReceipt, - onBackButtonClicked = { navController.popBackStack(POSScreen.PAYPINEntryScreen.name, inclusive = false) }, - onDoneButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) }, - onReloadButtonClicked = { - if (uiState.createPaymentResponse?.ref != null) { - viewModel.fetchPayment(uiState.createPaymentResponse!!.ref!!) - } - } - ) - } - composable(route = POSScreen.PAYResultScreen.name) { - PaymentResultScreen( - merchant = uiState.merchant, - terminalId = uiState.capturePaymentResponse?.posTerminal?.terminalId ?: "Unknown", - paymentMethod = uiState.tokenizedPaymentMethod, - paymentRef = uiState.capturePaymentResponse!!.ref!!, - txType = uiState.capturePaymentResponse?.let { TxType.forPayment(it.transactionType, it.fundingType!!) } ?: uiState.localPayment?.let { TxType.forPayment(it.transactionType, it.fundingType) }, - receipt = uiState.capturePaymentResponse?.receipt, - onBackButtonClicked = { navController.popBackStack(POSScreen.PAYPINEntryScreen.name, inclusive = false) }, - onDoneButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) }, - onReloadButtonClicked = { - if (uiState.createPaymentResponse?.ref != null) { - viewModel.fetchPayment(uiState.createPaymentResponse!!.ref!!) - } - } - ) - } - composable(route = POSScreen.REFUNDDetailsScreen.name) { - RefundDetailsScreen( - onConfirmButtonClicked = { paymentRef, amount, reason -> - val refundState = RefundUIState( - paymentRef = paymentRef, - amount = amount, - reason = reason - ) - viewModel.setLocalRefundState(refundState) { - viewModel.resetPinActionErrors() - navController.navigate(POSScreen.REFUNDPINEntryScreen.name) - } - }, - onCancelButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) } - ) - } - composable(route = POSScreen.REFUNDPINEntryScreen.name) { - PINEntryScreen( - posForageConfig = uiState.posForageConfig, - paymentMethodRef = uiState.tokenizedPaymentMethod?.ref, - onSubmitButtonClicked = { - if (pinElement != null && uiState.localRefundState != null) { - pinElement!!.clearFocus() - viewModel.refundPayment( - context = context, - foragePinEditText = pinElement as ForagePINEditText, - terminalId = k9SDK.terminalId, - paymentRef = uiState.localRefundState!!.paymentRef, - amount = uiState.localRefundState!!.amount, - reason = uiState.localRefundState!!.reason, - onSuccess = { - navController.navigate(POSScreen.REFUNDResultScreen.name) - }, - onFailure = { - navController.navigate(POSScreen.REFUNDErrorResultScreen.name) - } - ) - } - }, - onDeferButtonClicked = { - if (pinElement != null && uiState.localRefundState != null) { - pinElement!!.clearFocus() - viewModel.deferPaymentRefund( - context = context, - foragePinEditText = pinElement as ForagePINEditText, - terminalId = k9SDK.terminalId, - paymentRef = uiState.localRefundState!!.paymentRef, - onSuccess = { - navController.navigate(POSScreen.DEFERPaymentRefundResultScreen.name) - }, - onFailure = { - navController.navigate(POSScreen.REFUNDErrorResultScreen.name) - } - ) - } - }, - onBackButtonClicked = { navController.popBackStack(POSScreen.REFUNDDetailsScreen.name, inclusive = false) }, - withPinElementReference = { pinElement = it }, - errorText = uiState.refundPaymentError - ) - } - composable(route = POSScreen.REFUNDErrorResultScreen.name) { - val errorReceipt = uiState.refundPaymentResponse?.receipt ?: uiState.capturePaymentResponse!!.receipt!!.copy( - transactionType = "Refund", - created = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()).format(Timestamp(System.currentTimeMillis())), - message = uiState.refundPaymentError ?: uiState.capturePaymentResponse?.receipt?.message ?: "" - ) - - RefundResultScreen( - merchant = uiState.merchant, - terminalId = uiState.refundPaymentResponse?.posTerminal?.terminalId ?: uiState.capturePaymentResponse?.posTerminal?.terminalId ?: "Unknown", - paymentMethod = uiState.tokenizedPaymentMethod, - paymentRef = uiState.localRefundState!!.paymentRef, - refundRef = uiState.refundPaymentResponse?.ref, - txType = uiState.capturePaymentResponse?.let { TxType.forRefund(it.transactionType, it.fundingType!!) } ?: TxType.forReceipt(errorReceipt), - receipt = errorReceipt, - fetchedPayment = uiState.capturePaymentResponse, - onRefundRefClicked = { paymentRef, refundRef -> viewModel.fetchRefund(paymentRef, refundRef) }, - onBackButtonClicked = { navController.popBackStack(POSScreen.REFUNDPINEntryScreen.name, inclusive = false) }, - onDoneButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) }, - onReloadButtonClicked = { - if (uiState.localRefundState?.paymentRef != null) { - if (uiState.capturePaymentResponse?.ref == null) { - viewModel.fetchPayment(uiState.localRefundState!!.paymentRef) - } - } - } - ) - } - composable(route = POSScreen.REFUNDResultScreen.name) { - RefundResultScreen( - merchant = uiState.merchant, - terminalId = uiState.refundPaymentResponse?.posTerminal?.terminalId ?: uiState.capturePaymentResponse?.posTerminal?.terminalId ?: "Unknown", - paymentMethod = uiState.tokenizedPaymentMethod, - paymentRef = uiState.localRefundState!!.paymentRef, - refundRef = uiState.refundPaymentResponse?.ref, - txType = uiState.capturePaymentResponse?.let { TxType.forRefund(it.transactionType, it.fundingType!!) }, - receipt = uiState.refundPaymentResponse?.receipt, - fetchedPayment = uiState.capturePaymentResponse, - onRefundRefClicked = { paymentRef, refundRef -> viewModel.fetchRefund(paymentRef, refundRef) }, - onBackButtonClicked = { navController.popBackStack(POSScreen.REFUNDPINEntryScreen.name, inclusive = false) }, - onDoneButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) }, - onReloadButtonClicked = { - if (uiState.localRefundState?.paymentRef != null) { - if (uiState.capturePaymentResponse?.ref == null) { - viewModel.fetchPayment(uiState.localRefundState!!.paymentRef) - } - } - } - ) - } - composable(route = POSScreen.VOIDTransactionTypeSelectionScreen.name) { - VoidTypeSelectionScreen( - onPaymentButtonClicked = { navController.navigate(POSScreen.VOIDPaymentScreen.name) }, - onRefundButtonClicked = { navController.navigate(POSScreen.VOIDRefundScreen.name) }, - onCancelButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) } - ) - } - composable(route = POSScreen.VOIDPaymentScreen.name) { - VoidPaymentScreen( - onConfirmButtonClicked = { paymentRef -> - Log.i("POSComposeApp", "Voiding payment: $paymentRef") - viewModel.voidPayment(paymentRef) { - Log.i("POSComposeApp", "Voided payment: $it") - navController.navigate(POSScreen.VOIDPaymentResultScreen.name) - } - }, - onCancelButtonClicked = { - navController.popBackStack(POSScreen.VOIDTransactionTypeSelectionScreen.name, inclusive = false) - }, - errorText = uiState.voidPaymentError - ) - } - composable(route = POSScreen.VOIDRefundScreen.name) { - VoidRefundScreen( - onConfirmButtonClicked = { paymentRef, refundRef -> - Log.i("POSComposeApp", "Voiding refund: $refundRef") - viewModel.voidRefund(paymentRef, refundRef) { - Log.i("POSComposeApp", "Voided refund: $it") - navController.navigate(POSScreen.VOIDRefundResultScreen.name) - } - }, - onCancelButtonClicked = { - navController.popBackStack(POSScreen.VOIDTransactionTypeSelectionScreen.name, inclusive = false) - }, - errorText = uiState.voidRefundError - ) - } - composable(route = POSScreen.VOIDPaymentResultScreen.name) { - VoidPaymentResultScreen( - merchant = uiState.merchant, - terminalId = uiState.voidPaymentResponse?.posTerminal?.terminalId ?: "Unknown", - paymentMethod = uiState.tokenizedPaymentMethod, - paymentRef = uiState.voidPaymentResponse!!.ref!!, - txType = uiState.voidPaymentResponse?.let { it1 -> - it1.receipt?.let { it2 -> - TxType.forReceipt( - it2 - ) - } - }, - receipt = uiState.voidPaymentResponse!!.receipt, - onBackButtonClicked = { navController.popBackStack(POSScreen.VOIDPaymentScreen.name, inclusive = false) }, - onDoneButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) } - ) - } - composable(route = POSScreen.VOIDRefundResultScreen.name) { - VoidRefundResultScreen( - merchant = uiState.merchant, - terminalId = uiState.voidRefundResponse?.posTerminal?.terminalId ?: "Unknown", - paymentMethod = uiState.tokenizedPaymentMethod, - paymentRef = uiState.voidRefundResponse!!.paymentRef, - refundRef = uiState.voidRefundResponse?.ref, - txType = uiState.voidRefundResponse?.let { it1 -> - it1.receipt?.let { it2 -> - TxType.forReceipt( - it2 - ) - } - }, - receipt = uiState.voidRefundResponse!!.receipt, - onBackButtonClicked = { navController.popBackStack(POSScreen.VOIDRefundScreen.name, inclusive = false) }, - onDoneButtonClicked = { navController.popBackStack(POSScreen.ActionSelectionScreen.name, inclusive = false) } - ) - } - } - } -} - -@Preview -@Composable -fun PosAppPreview() { - POSComposeApp() -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSFragment.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSFragment.kt deleted file mode 100644 index ac29a7ccd..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSFragment.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.joinforage.android.example.ui.pos - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.Fragment -import com.joinforage.android.example.databinding.FragmentPosBinding - -class POSFragment : Fragment() { - private var _binding: FragmentPosBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentPosBinding.inflate(inflater, container, false) - val root: View = binding.root - - binding.composeView.apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - POSComposeApp() - } - } - - return root - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSViewModel.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSViewModel.kt deleted file mode 100644 index 3814b0cbe..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/POSViewModel.kt +++ /dev/null @@ -1,460 +0,0 @@ -package com.joinforage.android.example.ui.pos - -import android.annotation.SuppressLint -import android.content.Context -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.joinforage.android.example.ui.pos.data.BalanceCheck -import com.joinforage.android.example.ui.pos.data.BalanceCheckJsonAdapter -import com.joinforage.android.example.ui.pos.data.POSUIState -import com.joinforage.android.example.ui.pos.data.PosPaymentRequest -import com.joinforage.android.example.ui.pos.data.PosPaymentResponse -import com.joinforage.android.example.ui.pos.data.PosPaymentResponseJsonAdapter -import com.joinforage.android.example.ui.pos.data.Refund -import com.joinforage.android.example.ui.pos.data.RefundJsonAdapter -import com.joinforage.android.example.ui.pos.data.RefundUIState -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethodJsonAdapter -import com.joinforage.android.example.ui.pos.network.PosApiService -import com.joinforage.forage.android.CapturePaymentParams -import com.joinforage.forage.android.CheckBalanceParams -import com.joinforage.forage.android.DeferPaymentCaptureParams -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.pos.ForageTerminalSDK -import com.joinforage.forage.android.pos.PosDeferPaymentRefundParams -import com.joinforage.forage.android.pos.PosForageConfig -import com.joinforage.forage.android.pos.PosRefundPaymentParams -import com.joinforage.forage.android.pos.PosTokenizeCardParams -import com.joinforage.forage.android.ui.ForagePANEditText -import com.joinforage.forage.android.ui.ForagePINEditText -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import retrofit2.HttpException -import java.util.UUID - -@SuppressLint("NewApi") -class POSViewModel : ViewModel() { - private val _uiState = MutableStateFlow(POSUIState()) - val uiState: StateFlow = _uiState.asStateFlow() - private val api - get() = PosApiService.from(uiState.value.posForageConfig) - - fun setSessionToken(sessionToken: String) { - _uiState.update { it.copy(sessionToken = sessionToken) } - } - - fun setMerchantId(merchantId: String, onSuccess: () -> Unit) { - _uiState.update { it.copy(merchantId = merchantId) } - onSuccess() - } - - fun setLocalPayment(payment: PosPaymentRequest) { - _uiState.update { it.copy(localPayment = payment) } - } - - fun setLocalRefundState(refundState: RefundUIState, onComplete: () -> Unit) { - viewModelScope.launch { - try { - val payment = api.getPayment(refundState.paymentRef) - val paymentMethod = api.getPaymentMethod(payment.paymentMethod) - _uiState.update { it.copy(localRefundState = refundState, tokenizedPaymentMethod = paymentMethod) } - onComplete() - } catch (e: HttpException) { - _uiState.update { it.copy(localRefundState = refundState, tokenizedPaymentMethod = null) } - onComplete() - } - } - } - - fun fetchPayment(paymentRef: String) { - viewModelScope.launch { - try { - val payment = api.getPayment(paymentRef) - _uiState.update { it.copy(capturePaymentResponse = payment, capturePaymentError = null) } - } catch (e: HttpException) { - _uiState.update { it.copy(capturePaymentError = e.toString()) } - } - } - } - - fun fetchRefund(paymentRef: String, refundRef: String) { - viewModelScope.launch { - try { - val refund = api.getRefund(paymentRef, refundRef) - _uiState.update { it.copy(refundPaymentResponse = refund, refundPaymentError = null) } - } catch (e: HttpException) { - _uiState.update { it.copy(refundPaymentError = e.toString()) } - } - } - } - - fun resetUiState() { - // this needs to be in a coroutine and delayed to allow for the back-stack - // to be fully popped before some of the data it depends on disappears. - // There's probably a better way to do this but this works for now. - viewModelScope.launch { - delay(1000) - _uiState.update { - POSUIState( - merchantId = it.merchantId, - sessionToken = it.sessionToken - ) - } - } - } - - fun resetTokenizationError() { - _uiState.update { it.copy(tokenizationError = null) } - } - - fun resetPinActionErrors() { - _uiState.update { - it.copy( - balanceCheckError = null, - capturePaymentError = null, - refundPaymentError = null - ) - } - } - - fun createPayment(payment: PosPaymentRequest, onSuccess: (response: PosPaymentResponse) -> Unit) { - val idempotencyKey = UUID.randomUUID().toString() - - viewModelScope.launch { - try { - val response = api.createPayment( - idempotencyKey = idempotencyKey, - payment = payment - ) - _uiState.update { it.copy(createPaymentResponse = response, createPaymentError = null) } - onSuccess(response) - } catch (e: HttpException) { - Log.e("POSViewModel", "Create payment call failed: $e") - _uiState.update { it.copy(createPaymentError = e.toString(), createPaymentResponse = null) } - } - } - } - - fun tokenizeEBTCard(context: Context, foragePanEditText: ForagePANEditText, terminalId: String, onSuccess: (data: PosPaymentMethod?) -> Unit) { - viewModelScope.launch { - val forageTerminalSdk = initForageTerminalSDK(context, terminalId) - val response = forageTerminalSdk.tokenizeCard( - foragePanEditText = foragePanEditText, - reusable = true - ) - - when (response) { - is ForageApiResponse.Success -> { - val moshi = Moshi.Builder().build() - val jsonAdapter: JsonAdapter = PosPaymentMethodJsonAdapter(moshi) - val tokenizedPaymentMethod = jsonAdapter.fromJson(response.data) - _uiState.update { it.copy(tokenizedPaymentMethod = tokenizedPaymentMethod, tokenizationError = null) } - onSuccess(tokenizedPaymentMethod) - } - is ForageApiResponse.Failure -> { - Log.e("POSViewModel", response.toString()) - _uiState.update { it.copy(tokenizationError = response.toString(), tokenizedPaymentMethod = null) } - } - } - } - } - - fun tokenizeEBTCard(context: Context, track2Data: String, terminalId: String, onSuccess: (data: PosPaymentMethod?) -> Unit) { - viewModelScope.launch { - val forageTerminalSdk = initForageTerminalSDK(context, terminalId) - val response = forageTerminalSdk.tokenizeCard( - PosTokenizeCardParams( - uiState.value.posForageConfig, - track2Data - ) - ) - - when (response) { - is ForageApiResponse.Success -> { - val moshi = Moshi.Builder().build() - val jsonAdapter: JsonAdapter = PosPaymentMethodJsonAdapter(moshi) - val tokenizedPaymentMethod = jsonAdapter.fromJson(response.data) - _uiState.update { it.copy(tokenizedPaymentMethod = tokenizedPaymentMethod, tokenizationError = null) } - onSuccess(tokenizedPaymentMethod) - } - is ForageApiResponse.Failure -> { - Log.e("POSViewModel", response.toString()) - _uiState.update { it.copy(tokenizationError = response.toString(), tokenizedPaymentMethod = null) } - } - } - } - } - - fun checkEBTCardBalance(context: Context, foragePinEditText: ForagePINEditText, paymentMethodRef: String, terminalId: String, onSuccess: (response: BalanceCheck?) -> Unit) { - viewModelScope.launch { - val forageTerminalSdk = initForageTerminalSDK(context, terminalId) - val response = forageTerminalSdk.checkBalance( - CheckBalanceParams( - foragePinEditText = foragePinEditText, - paymentMethodRef = paymentMethodRef - ) - ) - - when (response) { - is ForageApiResponse.Success -> { - val moshi = Moshi.Builder().build() - val jsonAdapter: JsonAdapter = BalanceCheckJsonAdapter(moshi) - val balance = jsonAdapter.fromJson(response.data) - _uiState.update { it.copy(balance = balance, balanceCheckError = null) } - - // we need to refetch the EBT Card here because - // `ForageTerminalSDK(terminalId).checkBalance` - // does not return the timestamp of the balance - // check and we need to display the timestamp - // on the receipt - val updatedCard = api.getPaymentMethod(paymentMethodRef) - _uiState.update { - it.copy(tokenizedPaymentMethod = updatedCard) - } - onSuccess(balance) - } - is ForageApiResponse.Failure -> { - Log.e("POSViewModel", response.toString()) - _uiState.update { it.copy(balanceCheckError = response.toString(), balance = null) } - } - } - } - } - - fun deferPaymentCapture( - context: Context, - foragePinEditText: ForagePINEditText, - terminalId: String, - paymentRef: String, - onSuccess: () -> Unit, - onFailure: () -> Unit - ) { - viewModelScope.launch { - val forageTerminalSdk = initForageTerminalSDK(context, terminalId) - val response = forageTerminalSdk.deferPaymentCapture( - DeferPaymentCaptureParams( - foragePinEditText = foragePinEditText, - paymentRef = paymentRef - ) - ) - - when (response) { - is ForageApiResponse.Success -> { - onSuccess() - } - is ForageApiResponse.Failure -> { - Log.e("POSViewModel", response.errors[0].message) - var payment: PosPaymentResponse? = null - try { - payment = api.getPayment(paymentRef) - } catch (e: HttpException) { - Log.e("POSViewModel", "Failed to re-fetch payment $paymentRef after failed deferred capture") - } - _uiState.update { it.copy(capturePaymentError = response.errors[0].message, capturePaymentResponse = payment) } - onFailure() - } - } - } - } - - fun deferPaymentRefund( - context: Context, - foragePinEditText: ForagePINEditText, - terminalId: String, - paymentRef: String, - onSuccess: () -> Unit, - onFailure: () -> Unit - ) { - viewModelScope.launch { - val forageTerminalSdk = initForageTerminalSDK(context, terminalId) - val response = forageTerminalSdk.deferPaymentRefund( - PosDeferPaymentRefundParams( - foragePinEditText = foragePinEditText, - paymentRef = paymentRef - ) - ) - - when (response) { - is ForageApiResponse.Success -> { - onSuccess() - } - is ForageApiResponse.Failure -> { - Log.e("POSViewModel", response.errors[0].message) - var payment: PosPaymentResponse? = null - try { - payment = api.getPayment(paymentRef) - } catch (e: HttpException) { - Log.e("POSViewModel", "Failed to re-fetch payment after failed deferred refund attempt. PaymentRef: $paymentRef") - } - _uiState.update { it.copy(refundPaymentError = response.errors[0].message, capturePaymentResponse = payment) } - onFailure() - } - } - } - } - - fun capturePayment(context: Context, foragePinEditText: ForagePINEditText, terminalId: String, paymentRef: String, onSuccess: () -> Unit, onFailure: (sequenceNumber: String?) -> Unit) { - viewModelScope.launch { - val forageTerminalSdk = initForageTerminalSDK(context, terminalId) - val response = forageTerminalSdk.capturePayment( - CapturePaymentParams( - foragePinEditText = foragePinEditText, - paymentRef = paymentRef - ) - ) - - when (response) { - is ForageApiResponse.Success -> { - val moshi = Moshi.Builder().build() - val jsonAdapter: JsonAdapter = PosPaymentResponseJsonAdapter(moshi) - val paymentResponse = jsonAdapter.fromJson(response.data) - _uiState.update { it.copy(capturePaymentResponse = paymentResponse, capturePaymentError = null) } - - // we need to refetch the EBT Card here because - // capturing a payment does not include the updated - // balance we need to display the balance on the receipt - val paymentMethodRef = paymentResponse!!.paymentMethod - val updatedCard = api.getPaymentMethod(paymentMethodRef) - _uiState.update { - it.copy(tokenizedPaymentMethod = updatedCard) - } - onSuccess() - } - is ForageApiResponse.Failure -> { - Log.e("POSViewModel", response.errors[0].message) - var payment: PosPaymentResponse? = null - try { - payment = api.getPayment(paymentRef) - } catch (e: HttpException) { - Log.e("POSViewModel", "Failed to re-fetch payment $paymentRef after failed capture") - } - _uiState.update { it.copy(capturePaymentError = response.errors[0].message, capturePaymentResponse = payment) } - onFailure(payment?.sequenceNumber) - } - } - } - } - - fun refundPayment(context: Context, foragePinEditText: ForagePINEditText, terminalId: String, amount: Float, paymentRef: String, reason: String, onSuccess: () -> Unit, onFailure: () -> Unit) { - viewModelScope.launch { - val forageTerminalSdk = initForageTerminalSDK(context, terminalId) - val response = forageTerminalSdk.refundPayment( - PosRefundPaymentParams( - foragePinEditText = foragePinEditText, - amount = amount, - paymentRef = paymentRef, - reason = reason - ) - ) - - try { - val payment = api.getPayment(paymentRef) - val paymentMethod = api.getPaymentMethod(payment.paymentMethod) - _uiState.update { it.copy(tokenizedPaymentMethod = paymentMethod, tokenizationError = null, capturePaymentResponse = payment, capturePaymentError = null) } - } catch (e: HttpException) { - Log.e("POSViewModel", "Looking up payment method for refund failed. PaymentRef: $paymentRef") - } - - when (response) { - is ForageApiResponse.Success -> { - val moshi = Moshi.Builder().build() - val jsonAdapter: JsonAdapter = RefundJsonAdapter(moshi) - val refundResponse = jsonAdapter.fromJson(response.data) - _uiState.update { it.copy(refundPaymentResponse = refundResponse, refundPaymentError = null) } - onSuccess() - } - is ForageApiResponse.Failure -> { - Log.e("POSViewModel", response.toString()) - var payment: PosPaymentResponse? = null - var refund: Refund? = null - try { - payment = api.getPayment(paymentRef) - val mostRecentRefundRef = payment.refunds.lastOrNull() - if (mostRecentRefundRef != null) { - refund = api.getRefund(paymentRef, mostRecentRefundRef) - } - } catch (e: HttpException) { - Log.e("POSViewModel", "Failed to re-fetch payment or refund after failed refund attempt. PaymentRef: $paymentRef") - } - _uiState.update { it.copy(refundPaymentError = response.errors[0].message, refundPaymentResponse = refund, capturePaymentResponse = payment) } - onFailure() - } - } - } - } - - fun voidPayment(paymentRef: String, onSuccess: (response: PosPaymentResponse) -> Unit) { - val idempotencyKey = UUID.randomUUID().toString() - - viewModelScope.launch { - try { - val response = api.voidPayment( - idempotencyKey = idempotencyKey, - paymentRef = paymentRef - ) - val payment = api.getPayment(paymentRef) - val paymentMethod = api.getPaymentMethod(response.paymentMethod) - if (response.receipt != null && payment.receipt != null) { - response.receipt!!.isVoided = true - response.receipt!!.balance?.snap = ((response.receipt!!.balance?.snap?.toDouble() ?: 0.0) + payment.receipt!!.snapAmount.toDouble()).toString() - response.receipt!!.balance?.nonSnap = ((response.receipt!!.balance?.nonSnap?.toDouble() ?: 0.0) + payment.receipt!!.ebtCashAmount.toDouble()).toString() - } - _uiState.update { it.copy(voidPaymentResponse = response, voidPaymentError = null, tokenizedPaymentMethod = paymentMethod) } - onSuccess(response) - Log.i("POSViewModel", "Void payment call succeeded: $response") - } catch (e: HttpException) { - Log.e("POSViewModel", "Void payment call failed: $e") - _uiState.update { it.copy(voidPaymentError = e.toString(), voidPaymentResponse = null) } - } - } - } - - fun voidRefund(paymentRef: String, refundRef: String, onSuccess: (response: Refund) -> Unit) { - val idempotencyKey = UUID.randomUUID().toString() - - viewModelScope.launch { - try { - val payment = api.getPayment(paymentRef) - val refund = api.getRefund(paymentRef, refundRef) - val response = api.voidRefund( - idempotencyKey = idempotencyKey, - paymentRef = paymentRef, - refundRef = refundRef - ) - val paymentMethod = api.getPaymentMethod(payment.paymentMethod) - if (payment.receipt != null) { - response.receipt!!.isVoided = true - response.receipt.balance?.snap = ((response.receipt.balance?.snap?.toDouble() ?: 0.0) - refund.receipt!!.snapAmount!!.toDouble()).toString() - response.receipt.balance?.nonSnap = ((response.receipt.balance?.nonSnap?.toDouble() ?: 0.0) - refund.receipt!!.ebtCashAmount!!.toDouble()).toString() - } - _uiState.update { it.copy(voidRefundResponse = response, voidRefundError = null, tokenizedPaymentMethod = paymentMethod) } - onSuccess(response) - Log.i("POSViewModel", "Void refund call succeeded: $response") - } catch (e: HttpException) { - Log.e("POSViewModel", "Void refund call failed: $e") - _uiState.update { it.copy(voidRefundError = e.toString(), voidRefundResponse = null) } - } - } - } - - private suspend fun initForageTerminalSDK(context: Context, terminalId: String): ForageTerminalSDK { - // Setting `posTerminalId = "pos-sample-app-override"` allows - // us to run the POS sample app from the public repository - // without raising a "NotImplementedError". - return ForageTerminalSDK.init( - context = context, - posTerminalId = "pos-sample-app-override", - posForageConfig = PosForageConfig( - merchantId = _uiState.value.merchantId, - sessionToken = _uiState.value.sessionToken - ) - ) - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/BalanceCheck.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/BalanceCheck.kt deleted file mode 100644 index 57a9e940b..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/BalanceCheck.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.joinforage.android.example.ui.pos.data - -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class BalanceCheck( - val snap: String, - val cash: String -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/Merchant.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/Merchant.kt deleted file mode 100644 index 8302b0573..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/Merchant.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.joinforage.android.example.ui.pos.data - -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class Merchant( - val ref: String, - val name: String, - val fns: String, - val address: Address? -) - -@JsonClass(generateAdapter = true) -data class Address( - val city: String, - val country: String, - val line1: String, - val line2: String?, - val state: String, - val zipcode: String -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/POSUIState.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/POSUIState.kt deleted file mode 100644 index 76ad82a57..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/POSUIState.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.joinforage.android.example.ui.pos.data - -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod -import com.joinforage.forage.android.pos.PosForageConfig - -data class POSUIState( - val merchantId: String = "1234567", // - val sessionToken: String = "sandbox_eyabcdef....", // - - // Tokenizing EBT Cards - val tokenizedPaymentMethod: PosPaymentMethod? = null, - val tokenizationError: String? = null, - - // Checking balances of those EBT Cards - val balance: BalanceCheck? = null, - val balanceCheckError: String? = null, - - // Creating a payment - val localPayment: PosPaymentRequest? = null, // Used to build up the payment object before we send it - val createPaymentResponse: PosPaymentResponse? = null, - val createPaymentError: String? = null, - - // Capturing that payment - val capturePaymentResponse: PosPaymentResponse? = null, - val capturePaymentError: String? = null, - - // Refunding a payment - val localRefundState: RefundUIState? = null, // Used to build up the refund object before we send it - val refundPaymentResponse: Refund? = null, - val refundPaymentError: String? = null, - - // Voiding a payment - val voidPaymentResponse: PosPaymentResponse? = null, - val voidPaymentError: String? = null, - - // Voiding a refund - val voidRefundResponse: Refund? = null, - val voidRefundError: String? = null -) { - val posForageConfig: PosForageConfig - get() = PosForageConfig(merchantId, sessionToken) - - val merchant - get() = Merchant( - name = "POS Test Merchant", - ref = "testMerchantRef", - fns = merchantId, - address = Address( - line1 = "171 E 2nd St", - line2 = null, - city = "New York", - state = "NY", - zipcode = "10009", - country = "USA" - ) - ) -} - -data class RefundUIState( - val paymentRef: String, - val amount: Float, - val reason: String -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosPaymentRequest.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosPaymentRequest.kt deleted file mode 100644 index b48c8e977..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosPaymentRequest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.joinforage.android.example.ui.pos.data - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class PosPaymentRequest( - val amount: String, - @Json(name = "funding_type") val fundingType: String, - @Json(name = "payment_method") val paymentMethodRef: String, - val description: String, - @Json(name = "pos_terminal") val posTerminal: PosTerminalRequestField, - val metadata: Map, - @Json(name = "transaction_type") val transactionType: String? = null, - @Json(name = "cash_back_amount") val cashBackAmount: String? = null -) { - companion object { - fun forSnapPayment(snapAmount: String, terminalId: String) = PosPaymentRequest( - amount = snapAmount, - description = "Testing POS certification app payments (SNAP Purchase)", - fundingType = FundingType.EBTSnap.value, - paymentMethodRef = "", - posTerminal = PosTerminalRequestField(providerTerminalId = terminalId), - metadata = mapOf() - ) - fun forEbtCashPayment(ebtCashAmount: String, terminalId: String) = PosPaymentRequest( - amount = ebtCashAmount, - description = "Testing POS certification app payments (EBT Cash Purchase)", - fundingType = FundingType.EBTCash.value, - paymentMethodRef = "", - posTerminal = PosTerminalRequestField(providerTerminalId = terminalId), - metadata = mapOf() - ) - fun forEbtCashWithdrawal(ebtCashWithdrawalAmount: String, terminalId: String) = PosPaymentRequest( - amount = ebtCashWithdrawalAmount, - description = "Testing POS certification app payments (EBT Cash Withdrawal)", - fundingType = FundingType.EBTCash.value, - paymentMethodRef = "", - posTerminal = PosTerminalRequestField(providerTerminalId = terminalId), - metadata = mapOf(), - transactionType = TransactionType.Withdrawal.value - ) - fun forEbtCashPaymentWithCashBack(ebtCashAmount: String, cashBackAmount: String, terminalId: String) = PosPaymentRequest( - amount = (ebtCashAmount.toDouble() + cashBackAmount.toDouble()).toString(), - cashBackAmount = cashBackAmount, - transactionType = TransactionType.PurchaseWithCashBack.value, - description = "Testing POS certification app payments (EBT Cash Purchase with Cash Back)", - fundingType = FundingType.EBTCash.value, - paymentMethodRef = "", - posTerminal = PosTerminalRequestField(providerTerminalId = terminalId), - metadata = mapOf() - ) - } -} - -enum class FundingType(val value: String) { - EBTSnap(value = "ebt_snap"), - EBTCash(value = "ebt_cash"), - CreditTPP(value = "credit_tpp") -} - -enum class TransactionType(val value: String) { - Withdrawal(value = "withdrawal"), - PurchaseWithCashBack(value = "purchase_with_cash_back") -} - -@JsonClass(generateAdapter = true) -data class PosTerminalRequestField( - @Json(name = "provider_terminal_id") val providerTerminalId: String -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosPaymentResponse.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosPaymentResponse.kt deleted file mode 100644 index 085b0ba9c..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosPaymentResponse.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.joinforage.android.example.ui.pos.data - -import com.joinforage.android.example.ui.pos.data.tokenize.PosTerminalResponseField -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class PosPaymentResponse( - @Json(name = "ref") - var ref: String?, - @Json(name = "merchant") - var merchant: String?, - @Json(name = "funding_type") - var fundingType: String?, - var amount: Float?, - var description: String?, - var metadata: Map?, - @Json(name = "payment_method") - var paymentMethod: String, - @Json(name = "delivery_address") - var deliveryAddress: Address?, - @Json(name = "is_delivery") - var isDelivery: Boolean?, - var created: String, - var updated: String?, - var status: String?, - @Json(name = "last_processing_error") - var lastProcessingError: String?, - @Json(name = "success_date") - var successDate: String?, - var receipt: Receipt?, - var refunds: List, - @Json(name = "pos_terminal") - val posTerminal: PosTerminalResponseField?, - @Json(name = "customer_id") - var customerId: String?, - @Json(name = "cash_back_amount") - var cashBackAmount: Float?, - @Json(name = "sequence_number") - var sequenceNumber: String?, - @Json(name = "transaction_type") - var transactionType: String? -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosReceipt.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosReceipt.kt deleted file mode 100644 index 71f320e8e..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/PosReceipt.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.joinforage.android.example.ui.pos.data - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class Receipt( - @Json(name = "ref_number") val refNumber: String, - @Json(name = "is_voided") var isVoided: Boolean, - @Json(name = "snap_amount") val snapAmount: String, - @Json(name = "ebt_cash_amount") val ebtCashAmount: String, - @Json(name = "cash_back_amount") val cashBackAmount: String, - @Json(name = "other_amount") val otherAmount: String, - @Json(name = "sales_tax_applied") val salesTaxApplied: String, - val balance: ReceiptBalance?, - @Json(name = "last_4") val last4: String, - val message: String, - @Json(name = "transaction_type") val transactionType: String, - val created: String, - @Json(name = "sequence_number") val sequenceNumber: String -) - -@JsonClass(generateAdapter = true) -data class ReceiptBalance( - val id: Double, - var snap: String? = "--", - @Json(name = "non_snap") var nonSnap: String? = "--", - val updated: String -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/Refund.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/Refund.kt deleted file mode 100644 index 8fa93ca06..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/Refund.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.joinforage.android.example.ui.pos.data - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class Refund( - val ref: String, - @Json(name = "payment_ref") val paymentRef: String, - @Json(name = "funding_type") val fundingType: String, - val amount: String, - val reason: String, - val metadata: Map, - val created: String, - val updated: String, - val status: String, - @Json(name = "last_processing_error") val lastProcessingError: String?, - val receipt: Receipt?, - @Json(name = "pos_terminal") val posTerminal: RefundPosTerminal, - @Json(name = "external_order_id") val externalOrderId: String?, - val message: RefundVoidMessage?, - @Json(name = "sequence_number") val sequenceNumber: String? -) - -@JsonClass(generateAdapter = true) -data class RefundPosTerminal( - @Json(name = "terminal_id") val terminalId: String, - @Json(name = "provider_terminal_id") val providerTerminalId: String -) - -@JsonClass(generateAdapter = true) -data class RefundVoidMessage( - @Json(name = "content_id") val contentId: String, - @Json(name = "message_type") val messageType: String, - val status: String, - val failed: Boolean, - val errors: Array -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosBalance.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosBalance.kt deleted file mode 100644 index e06c0715c..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosBalance.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.joinforage.android.example.ui.pos.data.tokenize - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class PosBalance( - val snap: String, - val non_snap: String, - val updated: String, - @Json(name = "pos_terminal") - val posTerminal: PosTerminalResponseField?, - @Json(name = "sequence_number") - val sequenceNumber: String? = null -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosCard.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosCard.kt deleted file mode 100644 index 26dc03b6c..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosCard.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.joinforage.android.example.ui.pos.data.tokenize - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class PosCard( - @Json(name = "last_4") - val last4: String, - val created: String, - val token: String, - val state: String? -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosPaymentMethod.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosPaymentMethod.kt deleted file mode 100644 index f060898f4..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosPaymentMethod.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.joinforage.android.example.ui.pos.data.tokenize - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class PosPaymentMethod( - val ref: String, - val type: String, - val reusable: Boolean, - val card: PosCard?, - val balance: PosBalance?, - @Json(name = "customer_id") - val customerId: String? = null -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosTerminalResponseField.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosTerminalResponseField.kt deleted file mode 100644 index 87523f964..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/data/tokenize/PosTerminalResponseField.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.joinforage.android.example.ui.pos.data.tokenize - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class PosTerminalResponseField( - @Json(name = "terminal_id") - val terminalId: String, - @Json(name = "provider_terminal_id") - val providerTerminalId: String -) diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/network/PosApiService.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/network/PosApiService.kt deleted file mode 100644 index e9a00a99c..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/network/PosApiService.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.joinforage.android.example.ui.pos.network - -import com.joinforage.android.example.network.model.EnvConfig -import com.joinforage.android.example.ui.pos.data.PosPaymentRequest -import com.joinforage.android.example.ui.pos.data.PosPaymentResponse -import com.joinforage.android.example.ui.pos.data.Refund -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod -import com.joinforage.forage.android.pos.PosForageConfig -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.POST -import retrofit2.http.Path - -private val moshi = Moshi.Builder() - .addLast(KotlinJsonAdapterFactory()) - .build() - -interface PosApiService { - @POST("api/payments/") - suspend fun createPayment( - @Header("Idempotency-Key") idempotencyKey: String, - @Body payment: PosPaymentRequest - ): PosPaymentResponse - - @GET("api/payments/{paymentRef}/") - suspend fun getPayment( - @Path("paymentRef") paymentRef: String - ): PosPaymentResponse - - @POST("api/payments/{paymentRef}/void/") - suspend fun voidPayment( - @Header("Idempotency-Key") idempotencyKey: String, - @Path("paymentRef") paymentRef: String - ): PosPaymentResponse - - @GET("api/payments/{paymentRef}/refunds/{refundRef}/") - suspend fun getRefund( - @Path("paymentRef") paymentRef: String, - @Path("refundRef") refundRef: String - ): Refund - - @POST("api/payments/{paymentRef}/refunds/{refundRef}/void/") - suspend fun voidRefund( - @Header("Idempotency-Key") idempotencyKey: String, - @Path("paymentRef") paymentRef: String, - @Path("refundRef") refundRef: String - ): Refund - - @POST("api/payment_methods/{paymentMethodRef}/") - suspend fun getPaymentMethod( - @Path("paymentMethodRef") paymentMethodRef: String - ): PosPaymentMethod - - companion object { - internal fun from(posForageConfig: PosForageConfig): PosApiService { - val commonHeadersInterceptor = Interceptor { chain -> - val newRequest = chain.request().newBuilder() - .addHeader("Authorization", "Bearer ${posForageConfig.sessionToken}") - .addHeader("Merchant-Account", posForageConfig.merchantId) - .build() - chain.proceed(newRequest) - } - - val okHttpClient = OkHttpClient.Builder() - .addInterceptor(commonHeadersInterceptor) - .build() - - val env = EnvConfig.fromSessionToken(posForageConfig.sessionToken) - - val retrofit = Retrofit.Builder() - .baseUrl(env.baseUrl) - .client(okHttpClient) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() - - return retrofit.create(PosApiService::class.java) - } - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/ActionSelectionScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/ActionSelectionScreen.kt deleted file mode 100644 index be190a206..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/ActionSelectionScreen.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun ActionSelectionScreen( - merchantDetails: Merchant?, - onBackButtonClicked: () -> Unit, - onBalanceButtonClicked: () -> Unit, - onPaymentButtonClicked: () -> Unit, - onRefundButtonClicked: () -> Unit, - onVoidButtonClicked: () -> Unit -) { - ScreenWithBottomRow( - mainContent = { - Box { - Column { - Text("Merchant FNS: ${merchantDetails?.fns ?: "Unknown"}") - } - } - Column( - modifier = Modifier.padding(48.dp) - ) { - Button( - onClick = onBalanceButtonClicked, - modifier = Modifier.fillMaxWidth().withTestId("pos_select_balance_inquiry_button") - ) { - Text("Balance Inquiry") - } - Spacer(modifier = Modifier.height(8.dp)) - Button( - onClick = onPaymentButtonClicked, - modifier = Modifier.fillMaxWidth().withTestId("pos_select_purchase_button") - ) { - Text("Create a Payment / Purchase") - } - Spacer(modifier = Modifier.height(8.dp)) - Button( - onClick = onRefundButtonClicked, - modifier = Modifier.fillMaxWidth().withTestId("pos_select_refund_button") - ) { - Text("Make a Refund / Return") - } - Spacer(modifier = Modifier.height(8.dp)) - Button( - onClick = onVoidButtonClicked, - modifier = Modifier.fillMaxWidth().withTestId("pos_select_void_button") - ) { - Text("Void / Reverse a Transaction") - } - } - }, - bottomRowContent = { - Button(onClick = onBackButtonClicked) { - Text("Back") - } - } - ) -} - -@Preview -@Composable -fun ActionSelectionScreenPreview() { - ActionSelectionScreen( - merchantDetails = null, - onBackButtonClicked = {}, - onBalanceButtonClicked = {}, - onPaymentButtonClicked = {}, - onRefundButtonClicked = {} - ) { - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/MerchantSetupScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/MerchantSetupScreen.kt deleted file mode 100644 index 3315f4fbe..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/MerchantSetupScreen.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun MerchantSetupScreen( - terminalId: String, - merchantId: String, - sessionToken: String, - onSaveButtonClicked: (String, String) -> Unit -) { - var merchantIdInput by rememberSaveable { - mutableStateOf(merchantId) - } - - var sessionTokenInput by rememberSaveable { - mutableStateOf(sessionToken) - } - - ScreenWithBottomRow( - mainContent = { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Text("Terminal ID", fontWeight = FontWeight.Bold) - Text(terminalId) - } - Spacer(modifier = Modifier.height(16.dp)) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Text("Merchant ID", fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.width(48.dp)) - TextField( - value = merchantIdInput, - onValueChange = { merchantIdInput = it }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - modifier = Modifier.withTestId("pos_merchant_id_text_field") - ) - } - Spacer(modifier = Modifier.height(16.dp)) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Text("Session Token", fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.width(16.dp)) - TextField( - value = sessionTokenInput, - onValueChange = { sessionTokenInput = it }, - modifier = Modifier.withTestId("pos_session_token_text_field") - ) - } - }, - bottomRowContent = { - Button( - onClick = { onSaveButtonClicked(merchantIdInput, sessionTokenInput) } - ) { - Text( - "Bind POS to Merchant", - modifier = Modifier.withTestId("pos_bind_to_merchant_button") - ) - } - } - ) -} - -@Preview -@Composable -fun MerchantSetupScreenPreview() { - MerchantSetupScreen( - terminalId = "preview terminal id", - merchantId = "preview merchant id", - sessionToken = "preview session token", - onSaveButtonClicked = { _, _ -> } - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/ReceiptPreviewScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/ReceiptPreviewScreen.kt deleted file mode 100644 index 59f647da7..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/ReceiptPreviewScreen.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayout -import com.joinforage.android.example.ui.pos.ui.ReceiptView - -@Composable -internal fun ReceiptPreviewScreen(receiptLayout: ReceiptLayout) { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { context -> - ReceiptView(context).apply { - setReceiptLayout(receiptLayout) - } - } - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/balance/BalanceResultScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/balance/BalanceResultScreen.kt deleted file mode 100644 index 8eb1abb9a..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/balance/BalanceResultScreen.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.balance - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.joinforage.android.example.pos.receipts.templates.BalanceInquiryReceipt -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod -import com.joinforage.android.example.ui.pos.screens.ReceiptPreviewScreen - -@Composable -fun BalanceResultScreen( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - balanceCheckError: String?, - onBackButtonClicked: () -> Unit, - onDoneButtonClicked: () -> Unit -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxSize() - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (paymentMethod?.balance == null) { - Text("There was a problem checking your balance.") - } else { - val receipt = BalanceInquiryReceipt( - merchant, - terminalId, - paymentMethod, - balanceCheckError - ) - ReceiptPreviewScreen(receipt.getReceiptLayout()) - } - } - if (paymentMethod?.balance == null) { - Button(onClick = onBackButtonClicked, modifier = Modifier.withTestId("pos_try_again_button")) { - Text("Try Again") - } - } else { - Button(onClick = onDoneButtonClicked, modifier = Modifier.withTestId("pos_done_button")) { - Text("Done") - } - } - } -} - -@Preview -@Composable -fun BalanceResultScreenPreview() { - BalanceResultScreen( - merchant = null, - terminalId = "", - paymentMethod = null, - balanceCheckError = "", - onBackButtonClicked = {}, - onDoneButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/deferred/DeferredPaymentCaptureResultScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/deferred/DeferredPaymentCaptureResultScreen.kt deleted file mode 100644 index 31f2065ae..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/deferred/DeferredPaymentCaptureResultScreen.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.deferred - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ElevatedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.joinforage.android.example.ui.pos.ui.PaymentRefView - -@Composable -internal fun DeferredPaymentCaptureResultScreen( - terminalId: String, - paymentRef: String, - onBackButtonClicked: () -> Unit, - onDoneButtonClicked: () -> Unit -) { - val clipboardManager = LocalClipboardManager.current - - val postRequestPrompt = """ - Send a POST request to - /api/payments/$paymentRef/capture_payment/ - to complete the payment - """.trimIndent() - - val docsLink = "https://docs.joinforage.app/docs/capture-ebt-payments-server-side#step-4-capture-the-payment-server-side" - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxSize().padding(16.dp) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text("Terminal ID: $terminalId") - Button(onClick = { - clipboardManager.setText(AnnotatedString(terminalId)) - }, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Copy") - } - } - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - PaymentRefView(paymentRef = paymentRef) - } - Spacer(modifier = Modifier.height(48.dp)) - - Text(postRequestPrompt, fontFamily = FontFamily.Monospace) - - Spacer(modifier = Modifier.height(48.dp)) - - Button(onClick = { - clipboardManager.setText(AnnotatedString(docsLink)) - }, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Copy Documentation Link") - } - } - Row { - Column { - Button(onClick = onBackButtonClicked) { - Text("Try Again") - } - } - Spacer(modifier = Modifier.width(8.dp)) - Column { - ElevatedButton(onClick = onDoneButtonClicked) { - Text("Done") - } - } - } - } -} - -@Preview -@Composable -fun DeferredPaymentCaptureResultScreenPreview() { - DeferredPaymentCaptureResultScreen( - terminalId = "", - paymentRef = "", - onBackButtonClicked = {}, - onDoneButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/deferred/DeferredPaymentRefundResultScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/deferred/DeferredPaymentRefundResultScreen.kt deleted file mode 100644 index 3c2d80d5c..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/deferred/DeferredPaymentRefundResultScreen.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.deferred - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ElevatedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.joinforage.android.example.ui.pos.ui.PaymentRefView - -@Composable -internal fun DeferredPaymentRefundResultScreen( - terminalId: String, - paymentRef: String, - onBackButtonClicked: () -> Unit, - onDoneButtonClicked: () -> Unit -) { - val clipboardManager = LocalClipboardManager.current - - val postRequestPrompt = """ - Send a POST request to - /api/payments/$paymentRef/refunds/ - to complete the refund - """.trimIndent() - - val docsLink = "https://docs.joinforage.app/docs/capture-ebt-payments-server-side#step-2-complete-the-refund-server-side" - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxSize().padding(16.dp) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text("Terminal ID: $terminalId") - Button(onClick = { - clipboardManager.setText(AnnotatedString(terminalId)) - }, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Copy") - } - } - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - PaymentRefView(paymentRef = paymentRef) - } - Spacer(modifier = Modifier.height(48.dp)) - - Text(postRequestPrompt, fontFamily = FontFamily.Monospace) - - Spacer(modifier = Modifier.height(48.dp)) - - Button(onClick = { - clipboardManager.setText(AnnotatedString(docsLink)) - }, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Copy Documentation Link") - } - } - Row { - Column { - Button(onClick = onBackButtonClicked) { - Text("Try Again") - } - } - Spacer(modifier = Modifier.width(8.dp)) - Column { - ElevatedButton(onClick = onDoneButtonClicked) { - Text("Done") - } - } - } - } -} - -@Preview -@Composable -fun DeferredPaymentRefundResultScreenPreview() { - DeferredPaymentRefundResultScreen( - terminalId = "", - paymentRef = "", - onBackButtonClicked = {}, - onDoneButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashPurchaseScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashPurchaseScreen.kt deleted file mode 100644 index 065443fa3..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashPurchaseScreen.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.payment - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun EBTCashPurchaseScreen( - onConfirmButtonClicked: (amount: String) -> Unit, - onCancelButtonClicked: () -> Unit -) { - var ebtCashAmount by rememberSaveable { - mutableStateOf("") - } - - ScreenWithBottomRow( - mainContent = { - Text("EBT Cash Purchase (no cashback)", fontSize = 18.sp) - OutlinedTextField( - value = ebtCashAmount, - onValueChange = { ebtCashAmount = it }, - label = { Text("EBT Cash Dollar Amount") }, - prefix = { Text("$") }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - modifier = Modifier.withTestId("pos_amount_text_field") - ) - }, - bottomRowContent = { - Button(onClick = onCancelButtonClicked, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Cancel") - } - Spacer(modifier = Modifier.width(12.dp)) - Button( - onClick = { onConfirmButtonClicked(ebtCashAmount) }, - modifier = Modifier.withTestId("pos_submit_button") - ) { - Text("Confirm") - } - } - ) -} - -@Preview -@Composable -fun EBTCashPurchaseScreenPreview() { - EBTCashPurchaseScreen( - onConfirmButtonClicked = {}, - onCancelButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashPurchaseWithCashBackScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashPurchaseWithCashBackScreen.kt deleted file mode 100644 index e2a3b7a97..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashPurchaseWithCashBackScreen.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.payment - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun EBTCashPurchaseWithCashBackScreen( - onConfirmButtonClicked: (ebtCashAmount: String, cashBackAmount: String) -> Unit, - onCancelButtonClicked: () -> Unit -) { - var ebtCashAmount by rememberSaveable { - mutableStateOf("") - } - - var cashBackAmount by rememberSaveable { - mutableStateOf("") - } - - ScreenWithBottomRow( - mainContent = { - Text("EBT Cash Purchase (with cashback)", fontSize = 18.sp) - OutlinedTextField( - value = ebtCashAmount, - onValueChange = { ebtCashAmount = it }, - label = { Text("EBT Cash Amount") }, - prefix = { Text("$") }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Next - ), - modifier = Modifier.withTestId("pos_amount_text_field") - ) - OutlinedTextField( - value = cashBackAmount, - onValueChange = { cashBackAmount = it }, - label = { Text("Cash Back Amount") }, - prefix = { Text("$") }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ) - ) - }, - bottomRowContent = { - Button(onClick = onCancelButtonClicked, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Cancel") - } - Spacer(modifier = Modifier.width(12.dp)) - Button( - onClick = { onConfirmButtonClicked(ebtCashAmount, cashBackAmount) }, - modifier = Modifier.withTestId("pos_submit_button") - ) { - Text("Confirm") - } - } - ) -} - -@Preview -@Composable -fun EBTCashPurchaseWithCashBackScreenPreview() { - EBTCashPurchaseWithCashBackScreen( - onConfirmButtonClicked = { _, _ -> }, - onCancelButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashWithdrawalScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashWithdrawalScreen.kt deleted file mode 100644 index aba5a7660..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTCashWithdrawalScreen.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.payment - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun EBTCashWithdrawalScreen( - onConfirmButtonClicked: (amount: String) -> Unit, - onCancelButtonClicked: () -> Unit -) { - var ebtCashWithdrawalAmount by rememberSaveable { - mutableStateOf("") - } - - ScreenWithBottomRow( - mainContent = { - Text("EBT Cash Withdrawal", fontSize = 18.sp) - OutlinedTextField( - value = ebtCashWithdrawalAmount, - onValueChange = { ebtCashWithdrawalAmount = it }, - label = { Text("Withdrawal Amount") }, - prefix = { Text("$") }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - modifier = Modifier.withTestId("pos_amount_text_field") - ) - }, - bottomRowContent = { - Button(onClick = onCancelButtonClicked, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Cancel") - } - Spacer(modifier = Modifier.width(12.dp)) - Button( - onClick = { onConfirmButtonClicked(ebtCashWithdrawalAmount) }, - modifier = Modifier.withTestId("pos_submit_button") - ) { - Text("Confirm") - } - } - ) -} - -@Preview -@Composable -fun EBTCashWithdrawalScreenPreview() { - EBTCashWithdrawalScreen( - onConfirmButtonClicked = {}, - onCancelButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTSnapPurchaseScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTSnapPurchaseScreen.kt deleted file mode 100644 index 9349aab3c..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/EBTSnapPurchaseScreen.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.payment - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun EBTSnapPurchaseScreen( - onConfirmButtonClicked: (snapAmount: String) -> Unit, - onCancelButtonClicked: () -> Unit -) { - var snapAmount by rememberSaveable { - mutableStateOf("") - } - - ScreenWithBottomRow( - mainContent = { - Text("SNAP Purchase", fontSize = 18.sp) - OutlinedTextField( - value = snapAmount, - onValueChange = { snapAmount = it }, - label = { Text("SNAP Dollar Amount") }, - prefix = { Text("$") }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - modifier = Modifier.withTestId("pos_amount_text_field") - ) - }, - bottomRowContent = { - Button(onClick = onCancelButtonClicked, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Cancel") - } - Spacer(modifier = Modifier.width(12.dp)) - Button( - onClick = { onConfirmButtonClicked(snapAmount) }, - modifier = Modifier.withTestId("pos_submit_button") - ) { - Text("Confirm") - } - } - ) -} - -@Preview -@Composable -fun EBTSnapPurchaseScreenPreview() { - EBTSnapPurchaseScreen( - onConfirmButtonClicked = {}, - onCancelButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/PaymentResultScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/PaymentResultScreen.kt deleted file mode 100644 index 67fcc8fea..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/PaymentResultScreen.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.payment - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.tooling.preview.Preview -import com.joinforage.android.example.pos.receipts.templates.BaseReceiptTemplate -import com.joinforage.android.example.pos.receipts.templates.txs.CashPurchaseTxReceipt -import com.joinforage.android.example.pos.receipts.templates.txs.CashPurchaseWithCashbackTxReceipt -import com.joinforage.android.example.pos.receipts.templates.txs.CashWithdrawalTxReceipt -import com.joinforage.android.example.pos.receipts.templates.txs.SnapPurchaseTxReceipt -import com.joinforage.android.example.pos.receipts.templates.txs.TxType -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod -import com.joinforage.android.example.ui.pos.screens.ReceiptPreviewScreen - -@Composable -fun PaymentResultScreen( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - paymentRef: String, - txType: TxType?, - receipt: Receipt?, - onBackButtonClicked: () -> Unit, - onDoneButtonClicked: () -> Unit, - onReloadButtonClicked: () -> Unit -) { - val clipboardManager = LocalClipboardManager.current - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxSize() - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (txType == null || receipt == null) { - Text("Transaction Type or Receipt unavailable. Terminal might be offline.") - Button(onClick = onReloadButtonClicked) { - Text("Re-fetch Payment") - } - } else { - var receiptTemplate: BaseReceiptTemplate? = null - if (txType == TxType.SNAP_PAYMENT) { - receiptTemplate = SnapPurchaseTxReceipt( - merchant, - terminalId, - paymentMethod, - receipt, - txType.title - ) - } - if (txType == TxType.CASH_PAYMENT) { - receiptTemplate = CashPurchaseTxReceipt( - merchant, - terminalId, - paymentMethod, - receipt, - txType.title - ) - } - if (txType == TxType.CASH_PURCHASE_WITH_CASHBACK) { - receiptTemplate = CashPurchaseWithCashbackTxReceipt( - merchant, - terminalId, - paymentMethod, - receipt, - txType.title - ) - } - if (txType == TxType.CASH_WITHDRAWAL) { - receiptTemplate = CashWithdrawalTxReceipt( - merchant, - terminalId, - paymentMethod, - receipt, - txType.title - ) - } - Column { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text("Payment Ref: $paymentRef") - Button(onClick = { - clipboardManager.setText(AnnotatedString(paymentRef)) - }, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Copy") - } - } - ReceiptPreviewScreen(receiptTemplate!!.getReceiptLayout()) - } - } - } - if (paymentMethod?.balance == null) { - Button(onClick = onBackButtonClicked) { - Text("Try Again") - } - } else { - Button(onClick = onDoneButtonClicked) { - Text("Done") - } - } - } -} - -@Preview -@Composable -fun PaymentResultScreenPreview() { - PaymentResultScreen( - merchant = null, - terminalId = "", - paymentMethod = null, - paymentRef = "", - txType = null, - receipt = null, - onBackButtonClicked = {}, - onDoneButtonClicked = {}, - onReloadButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/PaymentTypeSelectionScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/PaymentTypeSelectionScreen.kt deleted file mode 100644 index f3c032627..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/payment/PaymentTypeSelectionScreen.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.payment - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun PaymentTypeSelectionScreen( - onSnapPurchaseClicked: () -> Unit, - onCashPurchaseClicked: () -> Unit, - onCashWithdrawalClicked: () -> Unit, - onCashPurchaseCashbackClicked: () -> Unit, - onCancelButtonClicked: () -> Unit -) { - ScreenWithBottomRow( - mainContent = { - Text("Select a transaction type", fontSize = 18.sp) - Spacer(modifier = Modifier.height(12.dp)) - Button( - onClick = onSnapPurchaseClicked, - modifier = Modifier.fillMaxWidth().withTestId("pos_select_snap_purchase_button") - ) { - Text("EBT SNAP Purchase") - } - Spacer(modifier = Modifier.height(4.dp)) - Button( - onClick = onCashPurchaseClicked, - modifier = Modifier.fillMaxWidth().withTestId("pos_select_cash_purchase_button") - ) { - Text("EBT Cash Purchase") - } - Spacer(modifier = Modifier.height(4.dp)) - Button( - onClick = onCashWithdrawalClicked, - modifier = Modifier.fillMaxWidth().withTestId("pos_select_cash_withdrawal_button") - ) { - Text("EBT Cash Withdrawal (no purchase)") - } - Spacer(modifier = Modifier.height(4.dp)) - Button( - onClick = onCashPurchaseCashbackClicked, - modifier = Modifier.fillMaxWidth().withTestId("pos_select_cash_purchase_cashback_button") - ) { - Text("EBT Cash Purchase + Cashback") - } - }, - bottomRowContent = { - Button(onClick = onCancelButtonClicked) { - Text("Cancel") - } - } - ) -} - -@Preview -@Composable -fun PaymentTypeSelectionScreenPreview() { - PaymentTypeSelectionScreen( - onSnapPurchaseClicked = {}, - onCashPurchaseClicked = {}, - onCashWithdrawalClicked = {}, - onCashPurchaseCashbackClicked = {}, - onCancelButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/refund/RefundDetailsScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/refund/RefundDetailsScreen.kt deleted file mode 100644 index 5a524d0b0..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/refund/RefundDetailsScreen.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.refund - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun RefundDetailsScreen( - onConfirmButtonClicked: (paymentRef: String, amount: Float, reason: String) -> Unit, - onCancelButtonClicked: () -> Unit -) { - var paymentRefInput by rememberSaveable { - mutableStateOf("") - } - - var refundAmountInput by rememberSaveable { - mutableStateOf("") - } - - var reasonInput by rememberSaveable { - mutableStateOf("") - } - - ScreenWithBottomRow( - mainContent = { - Text("Configure the details of the refund", fontSize = 18.sp) - Spacer(modifier = Modifier.height(16.dp)) - OutlinedTextField( - value = paymentRefInput, - onValueChange = { paymentRefInput = it }, - label = { Text("Payment ref") }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next - ), - modifier = Modifier.withTestId("pos_refund_payment_ref_text_field") - ) - OutlinedTextField( - value = refundAmountInput, - onValueChange = { refundAmountInput = it }, - label = { Text("Refund amount") }, - prefix = { Text("$") }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Next - ), - modifier = Modifier.withTestId("pos_refund_amount_text_field") - ) - OutlinedTextField( - value = reasonInput, - onValueChange = { reasonInput = it }, - label = { Text("Refund reason") }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done - ), - modifier = Modifier.withTestId("pos_refund_reason_text_field") - ) - }, - bottomRowContent = { - Button(onClick = onCancelButtonClicked, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Cancel") - } - Spacer(modifier = Modifier.width(12.dp)) - Button( - onClick = { onConfirmButtonClicked(paymentRefInput, refundAmountInput.toFloat(), reasonInput) }, - modifier = Modifier.withTestId("pos_submit_button") - ) { - Text("Confirm") - } - } - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/refund/RefundResultScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/refund/RefundResultScreen.kt deleted file mode 100644 index c70aadb5f..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/refund/RefundResultScreen.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.refund - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.tooling.preview.Preview -import com.joinforage.android.example.pos.receipts.templates.BaseReceiptTemplate -import com.joinforage.android.example.pos.receipts.templates.txs.CashPurchaseTxReceipt -import com.joinforage.android.example.pos.receipts.templates.txs.CashPurchaseWithCashbackTxReceipt -import com.joinforage.android.example.pos.receipts.templates.txs.CashWithdrawalTxReceipt -import com.joinforage.android.example.pos.receipts.templates.txs.SnapPurchaseTxReceipt -import com.joinforage.android.example.pos.receipts.templates.txs.TxType -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.PosPaymentResponse -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod -import com.joinforage.android.example.ui.pos.screens.ReceiptPreviewScreen - -@Composable -fun RefundResultScreen( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - paymentRef: String, - refundRef: String?, - txType: TxType?, - receipt: Receipt?, - fetchedPayment: PosPaymentResponse?, - onRefundRefClicked: (paymentRef: String, refundRef: String) -> Unit, - onBackButtonClicked: () -> Unit, - onDoneButtonClicked: () -> Unit, - onReloadButtonClicked: () -> Unit -) { - val clipboardManager = LocalClipboardManager.current - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxSize() - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (txType == null || receipt == null) { - Text("Transaction Type or Receipt unavailable. Terminal might be offline.") - if (fetchedPayment?.ref == null) { - Text("Re-fetch payment to see a list of refunds on the payment.") - Button(onClick = onReloadButtonClicked) { - Text("Re-fetch Payment") - } - } else { - Text("Select a Refund ref from payment (${fetchedPayment.ref}) to view the receipt for:") - fetchedPayment.refunds.forEach { refundRef -> - Button(onClick = { onRefundRefClicked(fetchedPayment.ref!!, refundRef) }) { - Text(refundRef) - } - } - } - } else { - var receiptTemplate: BaseReceiptTemplate? = null - if (txType == TxType.REFUND_SNAP_PAYMENT) { - receiptTemplate = SnapPurchaseTxReceipt( - merchant, - terminalId, - paymentMethod, - receipt, - txType.title - ) - } - if (txType == TxType.REFUND_CASH_PAYMENT) { - receiptTemplate = CashPurchaseTxReceipt( - merchant, - terminalId, - paymentMethod, - receipt, - txType.title - ) - } - if (txType == TxType.REFUND_CASH_PURCHASE_WITH_CASHBACK) { - receiptTemplate = CashPurchaseWithCashbackTxReceipt( - merchant, - terminalId, - paymentMethod, - receipt, - txType.title - ) - } - if (txType == TxType.REFUND_CASH_WITHDRAWAL) { - receiptTemplate = CashWithdrawalTxReceipt( - merchant, - terminalId, - paymentMethod, - receipt, - txType.title - ) - } - Column { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text("Payment Ref: $paymentRef") - Button(onClick = { - clipboardManager.setText(AnnotatedString(paymentRef)) - }, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Copy") - } - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text("Refund Ref: $refundRef") - if (refundRef != null) { - Button(onClick = { - clipboardManager.setText(AnnotatedString(refundRef)) - }, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Copy") - } - } - } - if (receiptTemplate != null) { - ReceiptPreviewScreen(receiptTemplate.getReceiptLayout()) - } else { - Text("Couldn't find receipt template matching transaction type: ${txType.title}") - } - } - } - } - if (paymentMethod?.balance == null) { - Button(onClick = onBackButtonClicked) { - Text("Try Again") - } - } else { - Button(onClick = onDoneButtonClicked) { - Text("Done") - } - } - } -} - -@Preview -@Composable -fun RefundResultScreenPreview() { - RefundResultScreen( - merchant = null, - terminalId = "", - paymentMethod = null, - paymentRef = "", - refundRef = "", - txType = null, - receipt = null, - fetchedPayment = null, - onRefundRefClicked = { _, _ -> }, - onBackButtonClicked = {}, - onDoneButtonClicked = {}, - onReloadButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/MagSwipePANEntryScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/MagSwipePANEntryScreen.kt deleted file mode 100644 index 9330d504d..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/MagSwipePANEntryScreen.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.shared - -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.tooling.preview.Preview -import com.joinforage.android.example.ui.pos.ui.ErrorText -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun MagSwipePANEntryScreen( - onLaunch: () -> Unit, - onBackButtonClicked: () -> Unit, - errorText: String? = null -) { - LaunchedEffect(Unit) { - onLaunch() - } - - ScreenWithBottomRow( - mainContent = { - Text("Swipe your EBT card now...") - ErrorText(errorText) - }, - bottomRowContent = { - Button(onClick = onBackButtonClicked) { - Text("Back") - } - } - ) -} - -@Preview -@Composable -fun MagSwipePANEntryScreenPreview() { - MagSwipePANEntryScreen( - onLaunch = {}, - onBackButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/ManualPANEntryScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/ManualPANEntryScreen.kt deleted file mode 100644 index a1348a781..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/ManualPANEntryScreen.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.shared - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ComposableForagePANEditText -import com.joinforage.android.example.ui.pos.ui.ErrorText -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow -import com.joinforage.forage.android.pos.PosForageConfig -import com.joinforage.forage.android.ui.ForagePANEditText - -@Composable -fun ManualPANEntryScreen( - posForageConfig: PosForageConfig, - onSubmitButtonClicked: () -> Unit, - onBackButtonClicked: () -> Unit, - withPanElementReference: (element: ForagePANEditText) -> Unit, - errorText: String? = null -) { - ScreenWithBottomRow( - mainContent = { - Text("Manually enter your card number") - Spacer(modifier = Modifier.height(8.dp)) - ComposableForagePANEditText( - posForageConfig, - withPanElementReference = withPanElementReference - ) - Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = onSubmitButtonClicked, - modifier = Modifier.withTestId("pos_submit_button") - ) { - Text("Submit") - } - Spacer(modifier = Modifier.height(16.dp)) - ErrorText(errorText) - }, - bottomRowContent = { - Button( - onClick = onBackButtonClicked, - modifier = Modifier.withTestId("pos_back_button") - ) { - Text("Back") - } - } - ) -} - -@Preview -@Composable -fun ManualPANEntryScreenPreview() { - ManualPANEntryScreen( - posForageConfig = PosForageConfig("", ""), - onSubmitButtonClicked = {}, - onBackButtonClicked = {}, - withPanElementReference = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/PANMethodSelectionScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/PANMethodSelectionScreen.kt deleted file mode 100644 index 1530e8f00..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/PANMethodSelectionScreen.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.shared - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun PANMethodSelectionScreen( - onManualEntryButtonClicked: () -> Unit, - onSwipeButtonClicked: () -> Unit, - onBackButtonClicked: () -> Unit -) { - ScreenWithBottomRow( - mainContent = { - Text("How do you want to read your EBT card?") - Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = onManualEntryButtonClicked, - modifier = Modifier.withTestId("pos_select_pan_ui_entry_button") - ) { - Text("Manually Enter Card Number") - } - Button(onClick = onSwipeButtonClicked) { - Text("Swipe Card") - } - }, - bottomRowContent = { - Button( - onClick = onBackButtonClicked, - modifier = Modifier.withTestId("pos_back_button") - ) { - Text("Back") - } - } - ) -} - -@Preview -@Composable -fun PANMethodSelectionScreenPreview() { - PANMethodSelectionScreen( - onManualEntryButtonClicked = {}, - onSwipeButtonClicked = {}, - onBackButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/PinEntryScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/PinEntryScreen.kt deleted file mode 100644 index 907f3f120..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/shared/PinEntryScreen.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.shared - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.joinforage.android.example.ui.extensions.withTestId -import com.joinforage.android.example.ui.pos.ui.ComposableForagePINEditText -import com.joinforage.android.example.ui.pos.ui.ErrorText -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow -import com.joinforage.forage.android.pos.PosForageConfig -import com.joinforage.forage.android.ui.ForagePINEditText - -@Composable -fun PINEntryScreen( - posForageConfig: PosForageConfig, - paymentMethodRef: String?, - onSubmitButtonClicked: () -> Unit, - onBackButtonClicked: () -> Unit, - withPinElementReference: (element: ForagePINEditText) -> Unit, - onDeferButtonClicked: (() -> Unit)? = null, - errorText: String? = null -) { - ScreenWithBottomRow( - mainContent = { - if (paymentMethodRef != null) { - Card { - Column(modifier = Modifier.padding(16.dp)) { - Text("Payment Method", fontWeight = FontWeight.SemiBold) - Text( - "Ref: $paymentMethodRef", - modifier = Modifier.withTestId("pos_payment_method_ref_text") - ) - } - } - Spacer(modifier = Modifier.height(18.dp)) - Text("Enter your card PIN") - ComposableForagePINEditText( - posForageConfig = posForageConfig, - withPinElementReference = withPinElementReference - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - OutlinedButton( - onClick = onSubmitButtonClicked, - modifier = Modifier.withTestId("pos_submit_button") - ) { - Text("Complete Now") - } - - if (onDeferButtonClicked != null) { - Spacer(modifier = Modifier.width(8.dp)) - Button( - onClick = onDeferButtonClicked, - modifier = Modifier.withTestId("pos_collect_pin_defer_button") - ) { - Text("Defer to Server") - } - } - } - } else { - Text("There was an issue adding your card") - } - ErrorText(errorText) - }, - bottomRowContent = { - Button(onClick = onBackButtonClicked) { - if (paymentMethodRef != null) { - Text("Back") - } else { - Text("Try Again") - } - } - } - ) -} - -@Preview -@Composable -fun PINEntryScreenPreview() { - PINEntryScreen( - posForageConfig = PosForageConfig("", ""), - paymentMethodRef = "", - onSubmitButtonClicked = {}, - onBackButtonClicked = {}, - withPinElementReference = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidPaymentResultScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidPaymentResultScreen.kt deleted file mode 100644 index 5e0fa2da7..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidPaymentResultScreen.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.voids - -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview -import com.joinforage.android.example.pos.receipts.templates.txs.TxType -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod -import com.joinforage.android.example.ui.pos.screens.payment.PaymentResultScreen - -@Composable -fun VoidPaymentResultScreen( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - paymentRef: String, - txType: TxType?, - receipt: Receipt?, - onBackButtonClicked: () -> Unit, - onDoneButtonClicked: () -> Unit -) { - PaymentResultScreen( - merchant, - terminalId, - paymentMethod, - paymentRef, - txType, - receipt, - onBackButtonClicked, - onDoneButtonClicked, - onReloadButtonClicked = {} - ) -} - -@Preview -@Composable -fun VoidPaymentResultScreenPreview() { - VoidPaymentResultScreen( - merchant = null, - terminalId = "", - paymentMethod = null, - paymentRef = "", - txType = null, - receipt = null, - onBackButtonClicked = {}, - onDoneButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidPaymentScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidPaymentScreen.kt deleted file mode 100644 index 056ef44ff..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidPaymentScreen.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.voids - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.joinforage.android.example.ui.pos.ui.ErrorText -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun VoidPaymentScreen( - onConfirmButtonClicked: (paymentRef: String) -> Unit, - onCancelButtonClicked: () -> Unit, - errorText: String? = null -) { - var paymentRefInput by rememberSaveable { - mutableStateOf("") - } - - ScreenWithBottomRow( - mainContent = { - Text("Enter the ref of the payment to void", fontSize = 18.sp) - OutlinedTextField( - value = paymentRefInput, - onValueChange = { paymentRefInput = it }, - label = { Text("Payment ref") }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Done - ) - ) - ErrorText(errorText) - }, - bottomRowContent = { - Button(onClick = onCancelButtonClicked, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Cancel") - } - Spacer(modifier = Modifier.width(12.dp)) - Button(onClick = { onConfirmButtonClicked(paymentRefInput) }) { - Text("Confirm") - } - } - ) -} - -@Preview -@Composable -fun VoidPaymentScreenPreview() { - VoidPaymentScreen( - onConfirmButtonClicked = {}, - onCancelButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidRefundResultScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidRefundResultScreen.kt deleted file mode 100644 index 48ec45ada..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidRefundResultScreen.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.voids - -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview -import com.joinforage.android.example.pos.receipts.templates.txs.TxType -import com.joinforage.android.example.ui.pos.data.Merchant -import com.joinforage.android.example.ui.pos.data.Receipt -import com.joinforage.android.example.ui.pos.data.tokenize.PosPaymentMethod -import com.joinforage.android.example.ui.pos.screens.refund.RefundResultScreen - -@Composable -fun VoidRefundResultScreen( - merchant: Merchant?, - terminalId: String, - paymentMethod: PosPaymentMethod?, - paymentRef: String, - refundRef: String?, - txType: TxType?, - receipt: Receipt?, - onBackButtonClicked: () -> Unit, - onDoneButtonClicked: () -> Unit -) { - RefundResultScreen( - merchant, - terminalId, - paymentMethod, - paymentRef, - refundRef, - txType, - receipt, - fetchedPayment = null, - onRefundRefClicked = { _, _ -> }, - onBackButtonClicked, - onDoneButtonClicked, - onReloadButtonClicked = {} - ) -} - -@Preview -@Composable -fun VoidRefundResultScreenPreview() { - VoidRefundResultScreen( - merchant = null, - terminalId = "", - paymentMethod = null, - paymentRef = "", - refundRef = "", - txType = null, - receipt = null, - onBackButtonClicked = {}, - onDoneButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidRefundScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidRefundScreen.kt deleted file mode 100644 index efb468faa..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidRefundScreen.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.voids - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.joinforage.android.example.ui.pos.ui.ErrorText -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun VoidRefundScreen( - onConfirmButtonClicked: (paymentRef: String, refundRef: String) -> Unit, - onCancelButtonClicked: () -> Unit, - errorText: String? = null -) { - var refundRefInput by rememberSaveable { - mutableStateOf("") - } - - var paymentRefInput by rememberSaveable { - mutableStateOf("") - } - - ScreenWithBottomRow( - mainContent = { - Text("Enter the ref of the refund to void and the ref of the payment that was refunded", fontSize = 18.sp) - OutlinedTextField( - value = refundRefInput, - onValueChange = { refundRefInput = it }, - label = { Text("Refund ref") }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next - ) - ) - OutlinedTextField( - value = paymentRefInput, - onValueChange = { paymentRefInput = it }, - label = { Text("Payment ref") }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Done - ) - ) - ErrorText(errorText) - }, - bottomRowContent = { - Button(onClick = onCancelButtonClicked, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Cancel") - } - Spacer(modifier = Modifier.width(12.dp)) - Button(onClick = { onConfirmButtonClicked(paymentRefInput, refundRefInput) }) { - Text("Confirm") - } - } - ) -} - -@Preview -@Composable -fun VoidRefundScreenPreview() { - VoidRefundScreen( - onConfirmButtonClicked = { _, _ -> }, - onCancelButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidTypeSelectionScreen.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidTypeSelectionScreen.kt deleted file mode 100644 index 92f243427..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/screens/voids/VoidTypeSelectionScreen.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.joinforage.android.example.ui.pos.screens.voids - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.joinforage.android.example.ui.pos.ui.ScreenWithBottomRow - -@Composable -fun VoidTypeSelectionScreen( - onPaymentButtonClicked: () -> Unit, - onRefundButtonClicked: () -> Unit, - onCancelButtonClicked: () -> Unit -) { - ScreenWithBottomRow( - mainContent = { - Text("Select a transaction type to void", fontSize = 18.sp) - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = onPaymentButtonClicked) { - Text("Payment / Purchase") - } - Button(onClick = onRefundButtonClicked) { - Text("Refund / Return") - } - }, - bottomRowContent = { - Button(onClick = onCancelButtonClicked) { - Text("Cancel") - } - } - ) -} - -@Preview -@Composable -fun VoidTypeSelectionScreenPreview() { - VoidTypeSelectionScreen( - onPaymentButtonClicked = {}, - onRefundButtonClicked = {}, - onCancelButtonClicked = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ComposableForagePANEditText.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ComposableForagePANEditText.kt deleted file mode 100644 index ff1d0bc1c..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ComposableForagePANEditText.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.joinforage.android.example.ui.pos.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.viewinterop.AndroidView -import com.joinforage.forage.android.pos.PosForageConfig -import com.joinforage.forage.android.ui.ForagePANEditText - -@Composable -fun ComposableForagePANEditText( - posForageConfig: PosForageConfig, - withPanElementReference: (element: ForagePANEditText) -> Unit -) { - AndroidView( - factory = { context -> - ForagePANEditText(context).apply { - this.setPosForageConfig(posForageConfig = posForageConfig) - this.requestFocus() - withPanElementReference(this) - } - } - ) -} - -@Preview -@Composable -fun ComposableForagePANEditTextPreview() { - ComposableForagePANEditText( - posForageConfig = PosForageConfig("", ""), - withPanElementReference = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ComposableForagePINEditText.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ComposableForagePINEditText.kt deleted file mode 100644 index 361a80216..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ComposableForagePINEditText.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.joinforage.android.example.ui.pos.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.viewinterop.AndroidView -import com.joinforage.forage.android.pos.PosForageConfig -import com.joinforage.forage.android.ui.ForagePINEditText - -@Composable -fun ComposableForagePINEditText( - posForageConfig: PosForageConfig, - withPinElementReference: (element: ForagePINEditText) -> Unit -) { - AndroidView( - factory = { context -> - ForagePINEditText(context).apply { - this.setPosForageConfig(posForageConfig) - this.requestFocus() - withPinElementReference(this) - } - } - ) -} - -@Preview -@Composable -fun ComposableForagePINEditTextPreview() { - ComposableForagePINEditText( - posForageConfig = PosForageConfig("", ""), - withPinElementReference = {} - ) -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ErrorText.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ErrorText.kt deleted file mode 100644 index f6a521b14..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ErrorText.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.joinforage.android.example.ui.pos.ui - -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.joinforage.android.example.ui.extensions.withTestId - -@Composable -fun ErrorText(text: String?) { - if (text != null) { - Text( - text, - color = MaterialTheme.colorScheme.error, - modifier = Modifier.withTestId("pos_error_text") - ) - } -} - -@Preview -@Composable -fun ErrorTextPreview() { - ErrorText("Whoops!") -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/PaymentRefView.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/PaymentRefView.kt deleted file mode 100644 index a77354a3e..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/PaymentRefView.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.joinforage.android.example.ui.pos.ui - -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.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.tooling.preview.Preview -import com.joinforage.android.example.ui.extensions.withTestId - -@Composable -fun PaymentRefView(paymentRef: String) { - val clipboardManager = LocalClipboardManager.current - - Text("Payment Ref: $paymentRef", modifier = Modifier.withTestId("pos_payment_ref_text")) - Button(onClick = { - clipboardManager.setText(AnnotatedString(paymentRef)) - }, colors = ButtonDefaults.elevatedButtonColors()) { - Text("Copy") - } -} - -@Preview -@Composable -fun PaymentRefViewPreview() { - PaymentRefView("ref-1234") -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ReceiptView.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ReceiptView.kt deleted file mode 100644 index a42c48433..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ReceiptView.kt +++ /dev/null @@ -1,165 +0,0 @@ -package com.joinforage.android.example.ui.pos.ui - -import android.content.Context -import android.graphics.Paint -import android.graphics.Typeface -import android.text.SpannableString -import android.text.style.UnderlineSpan -import android.view.Gravity -import android.widget.Button -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import com.joinforage.android.example.R -import com.joinforage.android.example.pos.receipts.ReceiptPrinter -import com.joinforage.android.example.pos.receipts.primitives.LinePartAlignment -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayout -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLayoutLine -import com.joinforage.android.example.pos.receipts.primitives.ReceiptLinePart -import com.pos.sdk.DevicesFactory - -internal fun createReceiptPartTextView(context: Context, part: ReceiptLinePart) = TextView(context).apply { - layoutParams = LinearLayout.LayoutParams( - 0, - LinearLayout.LayoutParams.WRAP_CONTENT, - part.colWeight - ) - gravity = when (part.alignment) { - LinePartAlignment.LEFT -> Gravity.START - LinePartAlignment.CENTER -> Gravity.CENTER_HORIZONTAL - LinePartAlignment.RIGHT -> Gravity.END - } - - // extract commonly used variables - val format = part.format - val content = part.content - - // take measures to support underline text - val spannableString = SpannableString(content) - if (format.isUnderLine) { - spannableString.setSpan(UnderlineSpan(), 0, content.length, 0) - } - - // set the text - text = spannableString - - // handle remaining formatting - setLineSpacing(format.lineSpace.toFloat(), 1f) - setTypeface( - null, - when { - format.isBold && format.isItalic -> Typeface.BOLD_ITALIC - format.isBold -> Typeface.BOLD - format.isItalic -> Typeface.ITALIC - else -> Typeface.NORMAL - } - ) - if (format.isStrikeThruText) { - paintFlags = paintFlags or Paint.STRIKE_THRU_TEXT_FLAG - } - - // fonts appear much larger on the screen than on the receipt - // so we shrink the font size on the display to keep the sizes - // similar - val adjustedFontSize = format.textSize.toFloat() / 2 - textSize = adjustedFontSize -} - -internal fun createReceiptLineLinearLayout(context: Context, line: ReceiptLayoutLine) = LinearLayout(context).apply { - line.parts.forEach { part -> - val textView = createReceiptPartTextView(context, part) - addView(textView) - } -} - -internal fun createReceiptDisplay(context: Context, receiptLayout: ReceiptLayout) = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) - - // Set background color - val backgroundColor = ContextCompat.getColor(context, R.color.light_grey) - setBackgroundColor(backgroundColor) - - // Convert 8dp padding to pixels - val paddingInPixels = (8 * context.resources.displayMetrics.density).toInt() - setPadding(paddingInPixels, paddingInPixels, paddingInPixels, paddingInPixels) - - // add the TextViews that make up the ReceiptDisplay's content - receiptLayout.lines.forEach { line -> - val lineLayout = createReceiptLineLinearLayout(context, line) - addView(lineLayout) - } -} - -class ReceiptView( - context: Context -) : ScrollView(context) { - private val scrollableContent: LinearLayout - private val printReceiptBtn: Button - private var receiptDisplay: LinearLayout - private var receiptLayout = ReceiptLayout( - ReceiptLayoutLine.singleColCenter("This is an empty receipt.") - ) - - init { - // organize the scrollview itself will house (the root) - // which will house the container that will overflow as scroll - layoutParams = LinearLayout.LayoutParams( - ConstraintLayout.LayoutParams.MATCH_PARENT, - ConstraintLayout.LayoutParams.WRAP_CONTENT - ) - tag = "pos_receipt_view" - val rootPaddingInPixels = (16 * context.resources.displayMetrics.density).toInt() - setPadding(rootPaddingInPixels, rootPaddingInPixels, rootPaddingInPixels, rootPaddingInPixels) - - // organize and add the subviews - scrollableContent = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) - - printReceiptBtn = Button(context).apply { - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) - text = "Print Receipt" - - // when the button is clicked, have it pass the current - // receiptLayout (datastructure) to our ReceiptPrinter - // service so it so we can print it on the POS terminal - setOnClickListener { - val cpayPrinter = DevicesFactory.getDeviceManager().printDevice - ReceiptPrinter(receiptLayout).printWithCPayTerminal(cpayPrinter) - } - } - addView(printReceiptBtn) - - // organize the linear layout that will hold the receipt details - receiptDisplay = createReceiptDisplay(context, receiptLayout) - addView(receiptDisplay) - } - addView(scrollableContent) - } - - internal fun setReceiptLayout(newReceiptLayout: ReceiptLayout) { - // remove the old view if it exists - scrollableContent.removeView(receiptDisplay) - // set the new receiptLayout value - receiptLayout = newReceiptLayout - // create the new receiptView and save it - val newReceiptDisplay = createReceiptDisplay(context, newReceiptLayout) - // set the new receiptView field - receiptDisplay = newReceiptDisplay - // add the new receiptView to the parent view - scrollableContent.addView(receiptDisplay) - } -} diff --git a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ScreenWithBottomRow.kt b/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ScreenWithBottomRow.kt deleted file mode 100644 index 224eded43..000000000 --- a/sample-app/src/main/java/com/joinforage/android/example/ui/pos/ui/ScreenWithBottomRow.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.joinforage.android.example.ui.pos.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -inline fun ScreenWithBottomRow( - mainContent: @Composable ColumnScope.() -> Unit, - bottomRowContent: @Composable RowScope.() -> Unit -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxSize() - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - mainContent() - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - bottomRowContent() - } - } -} diff --git a/sample-app/src/main/res/layout/fragment_pos.xml b/sample-app/src/main/res/layout/fragment_pos.xml deleted file mode 100644 index 06af55b24..000000000 --- a/sample-app/src/main/res/layout/fragment_pos.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/sample-app/src/main/res/menu/bottom_nav_menu.xml b/sample-app/src/main/res/menu/bottom_nav_menu.xml index e080d3ebf..7e252a90d 100644 --- a/sample-app/src/main/res/menu/bottom_nav_menu.xml +++ b/sample-app/src/main/res/menu/bottom_nav_menu.xml @@ -11,9 +11,4 @@ android:icon="@drawable/ic_dashboard_black_24dp" android:title="@string/title_dashboard" /> - - \ No newline at end of file diff --git a/sample-app/src/main/res/navigation/mobile_navigation.xml b/sample-app/src/main/res/navigation/mobile_navigation.xml index 225d63d57..366de3575 100644 --- a/sample-app/src/main/res/navigation/mobile_navigation.xml +++ b/sample-app/src/main/res/navigation/mobile_navigation.xml @@ -5,10 +5,6 @@ android:id="@+id/mobile_navigation" app:startDestination="@+id/navigation_complete_flow"> - forage-android Tokenize Catalog - POS Balance Setup Authentication Tokenize EBT Card @@ -26,33 +25,4 @@ Defer SNAP Capture EBT Cash Defer EBT Cash - Merchant Setup - Select an Action - Back - Balance Inquiry - Balance Inquiry - Balance Inquiry - Balance Inquiry - Balance Inquiry - Create a Payment / Purchase - Create SNAP Purchase - Create a Payment / Purchase - Create a Payment / Purchase - Create a Payment / Purchase - Create a Payment / Purchase - Payment Receipt - Create EBT Cash Purchase - Create Cash Withdrawal - Create EBT Cash with Cashback - Make a Refund / Return - Void a Transaction - Make a Refund / Return - Refund Result - Void a Payment - Void a Refund - Void Payment Result - Void Refund Result - Deferred Payment Capture Result - Deferred Payment Refund Result - Restart