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 index bb4a2a10..d78227d7 100644 --- 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 @@ -1,5 +1,8 @@ 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 @@ -20,8 +23,10 @@ import com.joinforage.forage.android.ui.ForagePINEditText * The entry point for **in-store POS Terminal** transactions. * * A [ForageTerminalSDK] instance interacts with the Forage API. - * Provide a unique POS Terminal ID, the `posTerminalId` parameter, to perform operations like: * + * **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 @@ -30,20 +35,26 @@ import com.joinforage.forage.android.ui.ForagePINEditText * * [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: Create a ForageTerminalSDK instance - * val forage = ForageTerminalSDK(posTerminalId) + * // Example: Initialize the Forage Terminal SDK + * val forageTerminalSdk = ForageTerminalSDK.init( + * context = androidContext, + * posTerminalId = "", + * posForageConfig = PosForageConfig( + * merchantId = "mid/123ab45c67", + * sessionToken = "sandbox_ey123..." + * ) + * ) * ``` - * @param posTerminalId **Required**. A string that uniquely identifies the POS Terminal - * used for a transaction. The max length of the string is 255 characters. + * * @see * [Forage guide to Terminal POS integrations](https://docs.joinforage.app/docs/forage-terminal-android) * * [ForageSDK] to process online-only transactions */ -class ForageTerminalSDK( - private val posTerminalId: String -) : ForageSDKInterface { - private var createServiceFactory = { - sessionToken: String, merchantId: String, logger: Log -> +class ForageTerminalSDK internal constructor(private val posTerminalId: String) : + ForageSDKInterface { + private var createServiceFactory = { sessionToken: String, merchantId: String, logger: Log -> ForageSDK.ServiceFactory( sessionToken = sessionToken, merchantId = merchantId, @@ -52,20 +63,93 @@ class ForageTerminalSDK( } private var forageSdk: ForageSDK = ForageSDK() - private var createLogger: () -> Log = { Log.getInstance().addAttribute("pos_terminal_id", posTerminalId) } + + 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: () -> Log, - createServiceFactory: ((String, String, Log) -> ForageSDK.ServiceFactory)? = null + createLogger: (String) -> Log, + createServiceFactory: ((String, String, Log) -> ForageSDK.ServiceFactory)? = null, + initSucceeded: Boolean = false ) : this(posTerminalId) { this.forageSdk = forageSdk - this.createLogger = createLogger + 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 + } } /** @@ -87,8 +171,7 @@ class ForageTerminalSDK( * val sessionToken = "" * * fun tokenizeCard(foragePanEditText: ForagePANEditText) = viewModelScope.launch { - * - * val response = ForageTerminalSDK().tokenizeCard( + * val response = forageTerminalSdk.tokenizeCard( * foragePanEditText = foragePanEditText, * reusable = true * ) @@ -119,18 +202,26 @@ class ForageTerminalSDK( foragePanEditText: ForagePANEditText, reusable: Boolean = true ): ForageApiResponse { - val logger = createLogger() + val logger = createLogger(posTerminalId) logger.addAttribute("reusable", reusable) logger.i("[POS] Tokenizing Payment Method via UI PAN entry on Terminal $posTerminalId") - val tokenizationResponse = forageSdk.tokenizeEBTCard( - TokenizeEBTCardParams( - foragePanEditText = foragePanEditText, - reusable = reusable + 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]}") + logger.e( + "[POS] tokenizeCard failed on Terminal $posTerminalId: ${tokenizationResponse.errors[0]}" + ) } return tokenizationResponse } @@ -152,7 +243,7 @@ class ForageTerminalSDK( * val sessionToken = "" * * fun tokenizePosCard(foragePinEditText: ForagePINEditText) = viewModelScope.launch { - * val response = ForageTerminalSDK().tokenizeCard( + * val response = forageTerminalSdk.tokenizeCard( * PosTokenizeCardParams( * forageConfig = ForageConfig( * merchantId = merchantId, @@ -185,20 +276,24 @@ class ForageTerminalSDK( */ suspend fun tokenizeCard(params: PosTokenizeCardParams): ForageApiResponse { val (posForageConfig, track2Data, reusable) = params - val logger = createLogger() + 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") + 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 - ) + return tokenizeCardService.tokenizePosCard(track2Data = track2Data, reusable = reusable) } /** @@ -223,7 +318,7 @@ class ForageTerminalSDK( * val paymentMethodRef = "020xlaldfh" * * fun checkBalance(foragePinEditText: ForagePINEditText) = viewModelScope.launch { - * val response = ForageTerminalSDK().checkBalance( + * val response = forageTerminalSdk.checkBalance( * CheckBalanceParams( * foragePinEditText = foragePinEditText, * paymentMethodRef = paymentMethodRef @@ -255,10 +350,8 @@ class ForageTerminalSDK( * to trigger balance inquiry exceptions during testing. * @return A [ForageApiResponse] object. */ - override suspend fun checkBalance( - params: CheckBalanceParams - ): ForageApiResponse { - val logger = createLogger() + override suspend fun checkBalance(params: CheckBalanceParams): ForageApiResponse { + val logger = createLogger(posTerminalId) val (foragePinEditText, paymentMethodRef) = params val illegalVaultException = isIllegalVaultExceptionOrNull(foragePinEditText, logger) @@ -271,32 +364,42 @@ class ForageTerminalSDK( logger.addAttribute("merchant_ref", merchantId) .addAttribute("payment_method_ref", paymentMethodRef) - logger.i("[POS] Called checkBalance for PaymentMethod $paymentMethodRef on Terminal $posTerminalId") + 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 - ) + 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 - ) + 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( + attributes = + mapOf( "payment_method_ref" to paymentMethodRef, "pos_terminal_id" to posTerminalId ) @@ -306,8 +409,6 @@ class ForageTerminalSDK( return balanceResponse } - // ======= Same as online-only Forage SDK below ======= - /** * Immediately captures a payment via a * [ForagePINEditText][com.joinforage.forage.android.ui.ForagePINEditText] Element. @@ -330,7 +431,7 @@ class ForageTerminalSDK( * * fun capturePayment(foragePinEditText: ForagePINEditText, paymentRef: String) = * viewModelScope.launch { - * val response = ForageTerminalSDK().capturePayment( + * val response = forageTerminalSdk.capturePayment( * CapturePaymentParams( * foragePinEditText = foragePinEditText, * paymentRef = snapPaymentRef @@ -371,18 +472,26 @@ class ForageTerminalSDK( * to trigger payment capture exceptions during testing. * @return A [ForageApiResponse] object. */ - override suspend fun capturePayment( - params: CapturePaymentParams - ): ForageApiResponse { - val (_, paymentRef) = params - - val logger = createLogger().addAttribute("payment_ref", paymentRef) + 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]}") + logger.e( + "[POS] capturePayment failed for payment $paymentRef on Terminal $posTerminalId: ${captureResponse.errors[0]}" + ) } return captureResponse } @@ -407,7 +516,7 @@ class ForageTerminalSDK( * * fun deferPaymentCapture(foragePinEditText: ForagePINEditText, paymentRef: String) = * viewModelScope.launch { - * val response = ForageTerminalSDK().deferPaymentCapture( + * val response = forageTerminalSdk.deferPaymentCapture( * DeferPaymentCaptureParams( * foragePinEditText = foragePinEditText, * paymentRef = snapPaymentRef @@ -443,16 +552,24 @@ class ForageTerminalSDK( * on error handling. * @return A [ForageApiResponse] object. */ - override suspend fun deferPaymentCapture(params: DeferPaymentCaptureParams): ForageApiResponse { - val (_, paymentRef) = params - - val logger = createLogger().addAttribute("payment_ref", paymentRef) + 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]}") + logger.e( + "[POS] deferPaymentCapture failed for Payment $paymentRef on Terminal $posTerminalId: ${deferCaptureResponse.errors[0]}" + ) } return deferCaptureResponse } @@ -477,7 +594,7 @@ class ForageTerminalSDK( * var metadata: HashMap? = null * * fun refundPayment(foragePinEditText: ForagePINEditText) = viewModelScope.launch { - * val forage = ForageTerminalSDK(posTerminalId) + * val forageTerminalSdk = ForageTerminalSDK.init(...) // may throw! * val refundParams = PosRefundPaymentParams( * foragePinEditText, * paymentRef, @@ -509,7 +626,7 @@ class ForageTerminalSDK( * @return A [ForageApiResponse] object. */ suspend fun refundPayment(params: PosRefundPaymentParams): ForageApiResponse { - val logger = createLogger() + val logger = createLogger(posTerminalId) val (foragePinEditText, paymentRef, amount, reason) = params val (merchantId, sessionToken) = forageSdk._getForageConfigOrThrow(foragePinEditText) @@ -518,9 +635,7 @@ class ForageTerminalSDK( return illegalVaultException } - logger - .addAttribute("payment_ref", paymentRef) - .addAttribute("merchant_ref", merchantId) + logger.addAttribute("payment_ref", paymentRef).addAttribute("merchant_ref", merchantId) logger.i( """ [POS] Called refundPayment for Payment $paymentRef @@ -530,28 +645,37 @@ class ForageTerminalSDK( """.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 - ) + 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 - ) + 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]}") + logger.e( + "[POS] refundPayment failed for Payment $paymentRef on Terminal $posTerminalId: ${refund.errors[0]}" + ) } return refund @@ -566,7 +690,7 @@ class ForageTerminalSDK( * var paymentRef: String = "" * * fun deferPaymentRefund(foragePinEditText: ForagePINEditText) = viewModelScope.launch { - * val forage = ForageTerminalSDK(posTerminalId) + * val forageTerminalSdk = ForageTerminalSDK.init(...) // may throw! * val deferPaymentRefundParams = PosDeferPaymentRefundParams( * foragePinEditText, * paymentRef @@ -598,7 +722,7 @@ class ForageTerminalSDK( * hasn't had its ForageConfig set via .setForageConfig(). */ suspend fun deferPaymentRefund(params: PosDeferPaymentRefundParams): ForageApiResponse { - val logger = createLogger() + val logger = createLogger(posTerminalId) val (foragePinEditText, paymentRef) = params val (merchantId, sessionToken) = forageSdk._getForageConfigOrThrow(foragePinEditText) @@ -607,9 +731,7 @@ class ForageTerminalSDK( return illegalVaultException } - logger - .addAttribute("payment_ref", paymentRef) - .addAttribute("merchant_ref", merchantId) + logger.addAttribute("payment_ref", paymentRef).addAttribute("merchant_ref", merchantId) logger.i( """ [POS] Called deferPaymentRefund for Payment $paymentRef @@ -617,27 +739,36 @@ class ForageTerminalSDK( """.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 - ) + 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 - ) + 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]}") + logger.e( + "[POS] deferPaymentRefund failed for Payment $paymentRef on Terminal $posTerminalId: ${refund.errors[0]}" + ) } return refund @@ -660,13 +791,23 @@ class ForageTerminalSDK( 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.", + 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 { 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 index 49b86534..572b0cb2 100644 --- 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 @@ -70,19 +70,15 @@ class ForageTerminalSDKTest : MockServerSuite() { mockForagePanEditText = mock(ForagePANEditText::class.java) mockForagePinEditText = mock(ForagePINEditText::class.java) mockForageSdk = mock(ForageSDK::class.java) - `when`(mockForagePinEditText.getForageConfig()).thenReturn( - ForageConfig( - merchantId = expectedData.merchantId, - sessionToken = expectedData.sessionToken - ) + + val forageConfig = ForageConfig( + merchantId = expectedData.merchantId, + sessionToken = expectedData.sessionToken ) + `when`(mockForagePinEditText.getForageConfig()).thenReturn(forageConfig) `when`(mockForagePinEditText.getVaultType()).thenReturn(vaultSubmitter.getVaultType()) - terminalSdk = ForageTerminalSDK( - posTerminalId = expectedData.posTerminalId, - forageSdk = mockForageSdk, - createLogger = { mockLogger } - ) + terminalSdk = createMockTerminalSdk() } @Test @@ -160,6 +156,8 @@ class ForageTerminalSDKTest : MockServerSuite() { ForageApiResponse.Success("Success") ) + val terminalSdk = createMockTerminalSdk(false) + val response = terminalSdk.tokenizeCard( foragePanEditText = mockForagePanEditText ) @@ -318,6 +316,7 @@ class ForageTerminalSDKTest : MockServerSuite() { ForageApiResponse.Success("Success") ) + val terminalSdk = createMockTerminalSdk(false) val params = CapturePaymentParams( foragePinEditText = mockForagePinEditText, paymentRef = "payment1234" @@ -375,6 +374,7 @@ class ForageTerminalSDKTest : MockServerSuite() { foragePinEditText = mockForagePinEditText, paymentRef = "payment1234" ) + val terminalSdk = createMockTerminalSdk(false) val response = terminalSdk.deferPaymentCapture(params) assertTrue(response is ForageApiResponse.Success) assertTrue((response as ForageApiResponse.Success).data == "") @@ -487,18 +487,29 @@ class ForageTerminalSDKTest : MockServerSuite() { ) } - private fun createMockTerminalSdk() = ForageTerminalSDK( - posTerminalId = expectedData.posVaultRequestParams.posTerminalId, - forageSdk = ForageSDK(), - createLogger = { mockLogger }, - createServiceFactory = { _: String, _: String, logger: Log -> - MockServiceFactory( - mockVaultSubmitter = vaultSubmitter, - logger = logger, - server = server + 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() 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 index 32c918ab..1accc739 100644 --- 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 @@ -214,6 +214,7 @@ fun POSComposeApp( if (panElement != null) { panElement!!.clearFocus() viewModel.tokenizeEBTCard( + context = context, panElement as ForagePANEditText, k9SDK.terminalId, onSuccess = { @@ -235,7 +236,7 @@ fun POSComposeApp( MagSwipePANEntryScreen( onLaunch = { k9SDK.listenForMagneticCardSwipe { track2Data -> - viewModel.tokenizeEBTCard(track2Data, k9SDK.terminalId) { + viewModel.tokenizeEBTCard(context, track2Data, k9SDK.terminalId) { if (it?.ref != null) { Log.i("POSComposeApp", "Successfully tokenized EBT card with ref: $it.ref") viewModel.resetPinActionErrors() @@ -256,6 +257,7 @@ fun POSComposeApp( if (pinElement != null && uiState.tokenizedPaymentMethod?.ref != null) { pinElement!!.clearFocus() viewModel.checkEBTCardBalance( + context = context, pinElement as ForagePINEditText, paymentMethodRef = uiState.tokenizedPaymentMethod!!.ref, k9SDK.terminalId, @@ -396,6 +398,7 @@ fun POSComposeApp( if (panElement != null) { panElement!!.clearFocus() viewModel.tokenizeEBTCard( + context, panElement as ForagePANEditText, k9SDK.terminalId, onSuccess = { tokenizedCard -> @@ -423,7 +426,7 @@ fun POSComposeApp( MagSwipePANEntryScreen( onLaunch = { k9SDK.listenForMagneticCardSwipe { track2Data -> - viewModel.tokenizeEBTCard(track2Data, k9SDK.terminalId) { tokenizedCard -> + 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) @@ -449,6 +452,7 @@ fun POSComposeApp( if (pinElement != null && uiState.createPaymentResponse?.ref != null) { pinElement!!.clearFocus() viewModel.capturePayment( + context = context, foragePinEditText = pinElement as ForagePINEditText, terminalId = k9SDK.terminalId, paymentRef = uiState.createPaymentResponse!!.ref!!, @@ -547,6 +551,7 @@ fun POSComposeApp( if (pinElement != null && uiState.localRefundState != null) { pinElement!!.clearFocus() viewModel.refundPayment( + context = context, foragePinEditText = pinElement as ForagePINEditText, terminalId = k9SDK.terminalId, paymentRef = uiState.localRefundState!!.paymentRef, 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 index 9ae3638b..89877961 100644 --- 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 @@ -1,5 +1,7 @@ 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 @@ -19,6 +21,7 @@ import com.joinforage.forage.android.CapturePaymentParams import com.joinforage.forage.android.CheckBalanceParams import com.joinforage.forage.android.network.model.ForageApiResponse import com.joinforage.forage.android.pos.ForageTerminalSDK +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 @@ -34,6 +37,7 @@ 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() @@ -136,9 +140,10 @@ class POSViewModel : ViewModel() { } } - fun tokenizeEBTCard(foragePanEditText: ForagePANEditText, terminalId: String, onSuccess: (data: PosPaymentMethod?) -> Unit) { + fun tokenizeEBTCard(context: Context, foragePanEditText: ForagePANEditText, terminalId: String, onSuccess: (data: PosPaymentMethod?) -> Unit) { viewModelScope.launch { - val response = ForageTerminalSDK(terminalId).tokenizeCard( + val forageTerminalSdk = initForageTerminalSDK(context, terminalId) + val response = forageTerminalSdk.tokenizeCard( foragePanEditText = foragePanEditText, reusable = true ) @@ -159,10 +164,10 @@ class POSViewModel : ViewModel() { } } - fun tokenizeEBTCard(track2Data: String, terminalId: String, onSuccess: (data: PosPaymentMethod?) -> Unit) { + fun tokenizeEBTCard(context: Context, track2Data: String, terminalId: String, onSuccess: (data: PosPaymentMethod?) -> Unit) { viewModelScope.launch { - val forage = ForageTerminalSDK(terminalId) - val response = forage.tokenizeCard( + val forageTerminalSdk = initForageTerminalSDK(context, terminalId) + val response = forageTerminalSdk.tokenizeCard( PosTokenizeCardParams( uiState.value.posForageConfig, track2Data @@ -185,9 +190,10 @@ class POSViewModel : ViewModel() { } } - fun checkEBTCardBalance(foragePinEditText: ForagePINEditText, paymentMethodRef: String, terminalId: String, onSuccess: (response: BalanceCheck?) -> Unit) { + fun checkEBTCardBalance(context: Context, foragePinEditText: ForagePINEditText, paymentMethodRef: String, terminalId: String, onSuccess: (response: BalanceCheck?) -> Unit) { viewModelScope.launch { - val response = ForageTerminalSDK(terminalId).checkBalance( + val forageTerminalSdk = initForageTerminalSDK(context, terminalId) + val response = forageTerminalSdk.checkBalance( CheckBalanceParams( foragePinEditText = foragePinEditText, paymentMethodRef = paymentMethodRef @@ -220,9 +226,10 @@ class POSViewModel : ViewModel() { } } - fun capturePayment(foragePinEditText: ForagePINEditText, terminalId: String, paymentRef: String, onSuccess: () -> Unit, onFailure: (sequenceNumber: String?) -> Unit) { + fun capturePayment(context: Context, foragePinEditText: ForagePINEditText, terminalId: String, paymentRef: String, onSuccess: () -> Unit, onFailure: (sequenceNumber: String?) -> Unit) { viewModelScope.launch { - val response = ForageTerminalSDK(terminalId).capturePayment( + val forageTerminalSdk = initForageTerminalSDK(context, terminalId) + val response = forageTerminalSdk.capturePayment( CapturePaymentParams( foragePinEditText = foragePinEditText, paymentRef = paymentRef @@ -261,9 +268,10 @@ class POSViewModel : ViewModel() { } } - fun refundPayment(foragePinEditText: ForagePINEditText, terminalId: String, amount: Float, paymentRef: String, reason: String, onSuccess: () -> Unit, onFailure: () -> Unit) { + fun refundPayment(context: Context, foragePinEditText: ForagePINEditText, terminalId: String, amount: Float, paymentRef: String, reason: String, onSuccess: () -> Unit, onFailure: () -> Unit) { viewModelScope.launch { - val response = ForageTerminalSDK(terminalId).refundPayment( + val forageTerminalSdk = initForageTerminalSDK(context, terminalId) + val response = forageTerminalSdk.refundPayment( PosRefundPaymentParams( foragePinEditText = foragePinEditText, amount = amount, @@ -361,4 +369,18 @@ class POSViewModel : ViewModel() { } } } + + 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 + ) + ) + } }