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 775fd9803..7c363ef18 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 @@ -460,7 +458,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, @@ -507,18 +504,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 0e8eb89eb..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 @@ -2,7 +2,6 @@ package com.joinforage.forage.android import android.content.res.TypedArray import okhttp3.HttpUrl -import org.json.JSONObject import kotlin.random.Random /** @@ -24,28 +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 -} - -/** - * [JSONObject.optString] has trouble falling back to `null` and seems to fallback to `"null"` (string) instead - */ -internal fun JSONObject.getStringOrNull(fieldName: String): String? { - if (!has(fieldName) || isNull(fieldName)) { - return null - } - return optString(fieldName) -} - -internal fun JSONObject.hasNonNull(fieldName: String): Boolean { - return has(fieldName) && !isNull(fieldName) -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/AbstractVaultSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/AbstractVaultSubmitter.kt new file mode 100644 index 000000000..0575dc34e --- /dev/null +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/AbstractVaultSubmitter.kt @@ -0,0 +1,211 @@ +package com.joinforage.forage.android.vault + +import android.content.Context +import com.joinforage.forage.android.VaultType +import com.joinforage.forage.android.core.telemetry.Log +import com.joinforage.forage.android.core.telemetry.UserAction +import com.joinforage.forage.android.core.telemetry.VaultProxyResponseMonitor +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.model.ForageApiResponse +import com.joinforage.forage.android.network.model.ForageError +import com.joinforage.forage.android.network.model.UnknownErrorApiResponse +import com.joinforage.forage.android.ui.ForagePINEditText + +internal val IncompletePinError = ForageApiResponse.Failure.fromError( + ForageError(400, "user_error", "Invalid EBT Card PIN entered. Please enter your 4-digit PIN.") +) + +internal open class VaultSubmitterParams( + open val encryptionKeys: EncryptionKeys, + open val idempotencyKey: String, + open val merchantId: String, + open val path: String, + open val paymentMethod: PaymentMethod, + open val sessionToken: String, + open val userAction: UserAction +) + +internal interface VaultSubmitter { + suspend fun submit(params: VaultSubmitterParams): ForageApiResponse + + fun getVaultType(): VaultType +} + +internal abstract class AbstractVaultSubmitter( + protected val context: Context, + protected val foragePinEditText: ForagePINEditText, + protected val logger: Log, + private val vaultType: VaultType +) : VaultSubmitter { + // interface methods + override suspend fun submit(params: VaultSubmitterParams): ForageApiResponse { + logger.addAttribute("payment_method_ref", params.paymentMethod.ref) + .addAttribute("merchant_ref", params.merchantId) + logger.i("[$vaultType] Sending ${params.userAction} request to $vaultType") + + // If the PIN isn't valid (less than 4 numbers) then return a response here. + if (!foragePinEditText.getElementState().isComplete) { + logger.w("[$vaultType] User attempted to submit an incomplete PIN") + return IncompletePinError + } + + val vaultToken = getVaultToken(params.paymentMethod) + val encryptionKey = parseEncryptionKey(params.encryptionKeys) + + // if a vault provider is missing a token, we will + // gracefully fail here + if (vaultToken.isNullOrEmpty()) { + logger.e("Vault token is missing from Payments API response") + return UnknownErrorApiResponse + } + + // ========= USED FOR REPORTING IMPORTANT METRICS ========= + val proxyResponseMonitor = VaultProxyResponseMonitor( + vault = vaultType, + userAction = params.userAction, + metricsLogger = logger + ) + proxyResponseMonitor + .setPath(params.path) + .setMethod("POST") + .start() + // ========================================================== + + val vaultProxyRequest = buildProxyRequest( + params = params, + encryptionKey = encryptionKey, + vaultToken = vaultToken + ).setPath(params.path).setParams(params) + + val forageResponse = submitProxyRequest(vaultProxyRequest) + proxyResponseMonitor.end() + + // FNS requirement to clear the PIN after each submission + foragePinEditText.clearText() + + if (forageResponse is ForageApiResponse.Failure && forageResponse.errors.isNotEmpty()) { + val forageError = forageResponse.errors.first() + proxyResponseMonitor.setForageErrorCode(forageError.code) + proxyResponseMonitor.setHttpStatusCode(forageError.httpStatusCode) + } else { + proxyResponseMonitor.setHttpStatusCode(200) + } + proxyResponseMonitor.logResult() + + return forageResponse + } + + override fun getVaultType(): VaultType { + return vaultType + } + + // abstract methods + internal abstract fun parseEncryptionKey(encryptionKeys: EncryptionKeys): String + internal abstract suspend fun submitProxyRequest(vaultProxyRequest: VaultProxyRequest): ForageApiResponse + internal abstract fun getVaultToken(paymentMethod: PaymentMethod): String? + + /** + * @return [UnknownErrorApiResponse] if the response is a vault error, or null if it is not + */ + internal abstract fun toVaultErrorOrNull(vaultResponse: VaultResponse): ForageApiResponse.Failure? + internal abstract fun toForageErrorOrNull(vaultResponse: VaultResponse): ForageApiResponse.Failure? + internal abstract fun toForageSuccessOrNull(vaultResponse: VaultResponse): ForageApiResponse.Success? + + /** + * @return A string containing the raw error details of the vault error. + * To be used for internal error reporting. + */ + internal abstract fun parseVaultErrorMessage(vaultResponse: VaultResponse): String + + // concrete methods + protected open fun buildProxyRequest( + params: VaultSubmitterParams, + encryptionKey: String, + vaultToken: String + ) = VaultProxyRequest.emptyRequest() + .setHeader(ForageConstants.Headers.X_KEY, encryptionKey) + .setHeader(ForageConstants.Headers.MERCHANT_ACCOUNT, params.merchantId) + .setHeader(ForageConstants.Headers.IDEMPOTENCY_KEY, params.idempotencyKey) + .setHeader(ForageConstants.Headers.TRACE_ID, logger.getTraceIdValue()) + .setHeader(ForageConstants.Headers.API_VERSION, "default") + .setToken(vaultToken) + + // PaymentMethod.card.token is in the comma-separated format ,, + protected fun pickVaultTokenByIndex(paymentMethod: PaymentMethod, index: Int): String? { + val tokensString = paymentMethod.card.token + val tokensList = tokensString.split(TOKEN_DELIMITER) + + val noTokenStoredInVault = tokensList.size <= index + if (noTokenStoredInVault) return null + + return tokensList[index] + } + + protected fun vaultToForageResponse(vaultResponse: VaultResponse): ForageApiResponse { + if (vaultResponse == null) { + logger.e("[$vaultType] Received null response from $vaultType") + return UnknownErrorApiResponse + } + + val vaultError = toVaultErrorOrNull(vaultResponse) + if (vaultError != null) { + val rawVaultError = parseVaultErrorMessage(vaultResponse) + logger.e("[$vaultType] Received error from $vaultType: $rawVaultError") + return vaultError + } + + val forageApiErrorResponse = toForageErrorOrNull(vaultResponse) + if (forageApiErrorResponse != null) { + val firstError = forageApiErrorResponse.errors[0] + logger.e("[$vaultType] Received ForageError from $vaultType: $firstError") + return forageApiErrorResponse + } + + val forageApiSuccess = toForageSuccessOrNull(vaultResponse) + if (forageApiSuccess != null) { + logger.i("[$vaultType] Received successful response from $vaultType") + return forageApiSuccess + } + logger.e("[$vaultType] Received malformed response from $vaultType: $vaultResponse") + + return UnknownErrorApiResponse + } + + protected fun buildBaseRequestBody(vaultProxyRequest: VaultProxyRequest): HashMap { + return hashMapOf( + ForageConstants.RequestBody.CARD_NUMBER_TOKEN to vaultProxyRequest.vaultToken + ) + } + + internal companion object { + internal fun create(foragePinEditText: ForagePINEditText, logger: Log): VaultSubmitter { + val vaultType = foragePinEditText.getVaultType() + if (vaultType == VaultType.BT_VAULT_TYPE) { + return BasisTheoryPinSubmitter( + context = foragePinEditText.context, + foragePinEditText = foragePinEditText, + logger = logger + ) + } + return VgsPinSubmitter( + context = foragePinEditText.context, + foragePinEditText = foragePinEditText, + logger = logger + ) + } + + const val TOKEN_DELIMITER = "," + + internal fun balancePath(paymentMethodRef: String) = + "/api/payment_methods/$paymentMethodRef/balance/" + + internal fun capturePaymentPath(paymentRef: String) = + "/api/payments/$paymentRef/capture/" + + internal fun deferPaymentCapturePath(paymentRef: String) = + "/api/payments/$paymentRef/collect_pin/" + + } +} 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 9226b9501..b16426934 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.PollingService import com.joinforage.forage.android.network.model.ForageApiResponse import com.joinforage.forage.android.network.model.Message import com.joinforage.forage.android.network.model.PaymentMethod -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 619a0d1c1..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.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.network.model.Payment -import com.joinforage.forage.android.network.model.PaymentMethod -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(response.data) - else -> return response - } - val paymentMethod = when (val response = paymentMethodService.getPaymentMethod(payment.paymentMethodRef)) { - is ForageApiResponse.Success -> PaymentMethod(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 c6abaffe5..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.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.Payment -import com.joinforage.forage.android.network.model.PaymentMethod -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(response.data) - else -> return response - } - val paymentMethod = when (val response = paymentMethodService.getPaymentMethod(payment.paymentMethodRef)) { - is ForageApiResponse.Success -> PaymentMethod(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 500549e79..e69de29bb 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 @@ -1,256 +0,0 @@ -package com.joinforage.forage.android.vault - -import android.content.Context -import com.joinforage.forage.android.VaultType -import com.joinforage.forage.android.core.telemetry.Log -import com.joinforage.forage.android.core.telemetry.UserAction -import com.joinforage.forage.android.core.telemetry.VaultProxyResponseMonitor -import com.joinforage.forage.android.model.EncryptionKeys -import com.joinforage.forage.android.network.ForageConstants -import com.joinforage.forage.android.network.model.EbtCard -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.network.model.ForageError -import com.joinforage.forage.android.network.model.PaymentMethod -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( - ForageError(400, "user_error", "Invalid EBT Card PIN entered. Please enter your 4-digit PIN.") -) - -internal open class VaultSubmitterParams( - open val encryptionKeys: EncryptionKeys, - open val idempotencyKey: String, - open val merchantId: String, - open val path: String, - open val paymentMethod: PaymentMethod, - open val sessionToken: String, - open val userAction: UserAction -) - -internal interface VaultSubmitter { - suspend fun submit(params: VaultSubmitterParams): ForageApiResponse - - fun getVaultType(): VaultType -} - -internal abstract class AbstractVaultSubmitter( - protected val context: Context, - protected val foragePinEditText: ForagePINEditText, - protected val logger: Log, - private val vaultType: VaultType -) : VaultSubmitter { - // interface methods - override suspend fun submit(params: VaultSubmitterParams): ForageApiResponse { - logger.addAttribute("payment_method_ref", params.paymentMethod.ref) - .addAttribute("merchant_ref", params.merchantId) - logger.i("[$vaultType] Sending ${params.userAction} request to $vaultType") - - // If the PIN isn't valid (less than 4 numbers) then return a response here. - if (!foragePinEditText.getElementState().isComplete) { - logger.w("[$vaultType] User attempted to submit an incomplete PIN") - return IncompletePinError - } - - val vaultToken = getVaultToken(params.paymentMethod) - val encryptionKey = parseEncryptionKey(params.encryptionKeys) - - // if a vault provider is missing a token, we will - // gracefully fail here - if (vaultToken.isNullOrEmpty()) { - logger.e("Vault token is missing from Payments API response") - return UnknownErrorApiResponse - } - - // ========= USED FOR REPORTING IMPORTANT METRICS ========= - val proxyResponseMonitor = VaultProxyResponseMonitor( - vault = vaultType, - userAction = params.userAction, - metricsLogger = logger - ) - proxyResponseMonitor - .setPath(params.path) - .setMethod("POST") - .start() - // ========================================================== - - val vaultProxyRequest = buildProxyRequest( - params = params, - encryptionKey = encryptionKey, - vaultToken = vaultToken - ).setPath(params.path).setParams(params) - - val forageResponse = submitProxyRequest(vaultProxyRequest) - proxyResponseMonitor.end() - - // FNS requirement to clear the PIN after each submission - foragePinEditText.clearText() - - if (forageResponse is ForageApiResponse.Failure && forageResponse.errors.isNotEmpty()) { - val forageError = forageResponse.errors.first() - proxyResponseMonitor.setForageErrorCode(forageError.code) - proxyResponseMonitor.setHttpStatusCode(forageError.httpStatusCode) - } else { - proxyResponseMonitor.setHttpStatusCode(200) - } - proxyResponseMonitor.logResult() - - return forageResponse - } - - override fun getVaultType(): VaultType { - return vaultType - } - - // abstract methods - internal abstract fun parseEncryptionKey(encryptionKeys: EncryptionKeys): String - internal abstract suspend fun submitProxyRequest(vaultProxyRequest: VaultProxyRequest): ForageApiResponse - internal abstract fun getVaultToken(paymentMethod: PaymentMethod): String? - - /** - * @return [UnknownErrorApiResponse] if the response is a vault error, or null if it is not - */ - internal abstract fun toVaultErrorOrNull(vaultResponse: VaultResponse): ForageApiResponse.Failure? - internal abstract fun toForageErrorOrNull(vaultResponse: VaultResponse): ForageApiResponse.Failure? - internal abstract fun toForageSuccessOrNull(vaultResponse: VaultResponse): ForageApiResponse.Success? - - /** - * @return A string containing the raw error details of the vault error. - * To be used for internal error reporting. - */ - internal abstract fun parseVaultErrorMessage(vaultResponse: VaultResponse): String - - // concrete methods - protected open fun buildProxyRequest( - params: VaultSubmitterParams, - encryptionKey: String, - vaultToken: String - ) = VaultProxyRequest.emptyRequest() - .setHeader(ForageConstants.Headers.X_KEY, encryptionKey) - .setHeader(ForageConstants.Headers.MERCHANT_ACCOUNT, params.merchantId) - .setHeader(ForageConstants.Headers.IDEMPOTENCY_KEY, params.idempotencyKey) - .setHeader(ForageConstants.Headers.TRACE_ID, logger.getTraceIdValue()) - .setHeader(ForageConstants.Headers.API_VERSION, "default") - .setToken(vaultToken) - - // PaymentMethod.card.token is in the comma-separated format ,, - protected fun pickVaultTokenByIndex(paymentMethod: PaymentMethod, index: Int): String? { - val tokensString = (paymentMethod.card as EbtCard).token - val tokensList = tokensString.split(TOKEN_DELIMITER) - - val noTokenStoredInVault = tokensList.size <= index - if (noTokenStoredInVault) return null - - return tokensList[index] - } - - protected fun vaultToForageResponse(vaultResponse: VaultResponse): ForageApiResponse { - if (vaultResponse == null) { - logger.e("[$vaultType] Received null response from $vaultType") - return UnknownErrorApiResponse - } - - val vaultError = toVaultErrorOrNull(vaultResponse) - if (vaultError != null) { - val rawVaultError = parseVaultErrorMessage(vaultResponse) - logger.e("[$vaultType] Received error from $vaultType: $rawVaultError") - return vaultError - } - - val forageApiErrorResponse = toForageErrorOrNull(vaultResponse) - if (forageApiErrorResponse != null) { - val firstError = forageApiErrorResponse.errors[0] - logger.e("[$vaultType] Received ForageError from $vaultType: $firstError") - return forageApiErrorResponse - } - - val forageApiSuccess = toForageSuccessOrNull(vaultResponse) - if (forageApiSuccess != null) { - logger.i("[$vaultType] Received successful response from $vaultType") - return forageApiSuccess - } - logger.e("[$vaultType] Received malformed response from $vaultType: $vaultResponse") - - 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 { - return hashMapOf( - ForageConstants.RequestBody.CARD_NUMBER_TOKEN to vaultProxyRequest.vaultToken - ) - } - - internal companion object { - internal fun create(foragePinEditText: ForagePINEditText, logger: Log): VaultSubmitter { - val vaultType = foragePinEditText.getVaultType() - if (vaultType == VaultType.BT_VAULT_TYPE) { - return BasisTheoryPinSubmitter( - context = foragePinEditText.context, - foragePinEditText = foragePinEditText, - logger = logger - ) - } - if (vaultType == VaultType.VGS_VAULT_TYPE) { - return VgsPinSubmitter( - context = foragePinEditText.context, - foragePinEditText = foragePinEditText, - logger = logger - ) - } - return ForagePinSubmitter( - context = foragePinEditText.context, - foragePinEditText = foragePinEditText, - logger = logger - ) - } - - const val TOKEN_DELIMITER = "," - - internal fun balancePath(paymentMethodRef: String) = - "/api/payment_methods/$paymentMethodRef/balance/" - - internal fun capturePaymentPath(paymentRef: String) = - "/api/payments/$paymentRef/capture/" - - internal fun deferPaymentCapturePath(paymentRef: String) = - "/api/payments/$paymentRef/collect_pin/" - - internal fun refundPaymentPath(paymentRef: String) = "/api/payments/$paymentRef/refunds/" - - internal fun deferPaymentRefundPath(paymentRef: String) = "/api/payments/$paymentRef/refunds/collect_pin/" - } -} 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 0ff45c8ae..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.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.PaymentMethod -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 4b2519b24..8db2ad59f 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 470e4870d..075f81561 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 @@ -2,8 +2,8 @@ package com.joinforage.forage.android.mock import com.joinforage.forage.android.ForageSDK import com.joinforage.forage.android.core.telemetry.Log -import com.joinforage.forage.android.model.USState import com.joinforage.forage.android.network.EncryptionKeyService +import com.joinforage.forage.android.model.USState import com.joinforage.forage.android.network.MessageStatusService import com.joinforage.forage.android.network.OkHttpClientBuilder import com.joinforage.forage.android.network.PaymentMethodService @@ -15,11 +15,7 @@ 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.network.model.Balance import com.joinforage.forage.android.network.model.EbtBalance -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 @@ -55,17 +51,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 { @@ -80,7 +65,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() @@ -129,17 +113,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) @@ -149,5 +122,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 97819c118..000000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/pos/ForageTerminalSDKTest.kt +++ /dev/null @@ -1,536 +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.network.model.EbtBalance -import com.joinforage.forage.android.network.model.EbtCard -import com.joinforage.forage.android.network.model.ForageApiResponse -import com.joinforage.forage.android.network.model.ForageError -import com.joinforage.forage.android.network.model.PaymentMethod -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((paymentMethodResponse as ForageApiResponse.Success).data) - assertThat(response).isEqualTo( - PaymentMethod( - ref = "2f148fe399", - type = "ebt", - balance = null, - card = EbtCard( - last4 = "7845", - token = "tok_sandbox_sYiPe9Q249qQ5wQyUPP5f7", - usState = (response.card as EbtCard).usState, - fingerprint = (response.card as EbtCard).fingerprint - ), - 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 - val balance = expectedData.balance as EbtBalance - assertThat(successResponse.data).contains(balance.cash) - assertThat(successResponse.data).contains(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