diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt index 51bf6af76..6cdcc85ad 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt @@ -30,6 +30,7 @@ import com.processout.sdk.netcetera.threeds.PONetcetera3DS2ServiceConfiguration import com.processout.sdk.ui.card.tokenization.POCardTokenizationActivity import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.CardScannerConfiguration +import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.SavingConfiguration import com.processout.sdk.ui.card.tokenization.POCardTokenizationLauncher import com.processout.sdk.ui.shared.view.dialog.POAlertDialog import com.processout.sdk.ui.threeds.PO3DSRedirectCustomTabLauncher @@ -95,9 +96,9 @@ class CardPaymentFragment : BaseFragment( private fun launchTokenization() { launcher.launch( - POCardTokenizationConfiguration( + configuration = POCardTokenizationConfiguration( cardScanner = CardScannerConfiguration(), - savingAllowed = true + saving = SavingConfiguration() ) ) } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceAuthorizationResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceAuthorizationResponse.kt index 4d0499568..7d04a8905 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceAuthorizationResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceAuthorizationResponse.kt @@ -1,10 +1,51 @@ package com.processout.sdk.api.model.response +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.processout.sdk.core.util.findBy import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class InvoiceAuthorizationResponse( + @Json(name = "outcome") + val rawOutcome: String, @Json(name = "customer_action") - val customerAction: CustomerAction? + val customerAction: CustomerAction?, + @Json(name = "customer_token_id") + val customerTokenId: String? +) { + + val outcome: POInvoiceAuthorizationOutcome + get() = POInvoiceAuthorizationOutcome::rawValue.findBy(rawOutcome) ?: POInvoiceAuthorizationOutcome.UNKNOWN +} + +/** + * Invoice authorization response. + * + * @param[outcome] Invoice authorization outcome. + * @param[customerTokenId] Optional customer token ID. + * Available if invoice was authorized with `saveSource` flag set to `true` and implementation was able to tokenize the source. + */ +data class POInvoiceAuthorizationResponse( + val outcome: POInvoiceAuthorizationOutcome, + val customerTokenId: String? ) + +/** + * Invoice authorization outcome determined by the request and the current transaction status. + */ +@JsonClass(generateAdapter = false) +enum class POInvoiceAuthorizationOutcome(val rawValue: String) { + /** Pending operation. */ + PENDING("pending"), + + /** Successful operation. */ + SUCCESS("success"), + + /** + * Placeholder that allows adding additional cases while staying backward compatible. + * __Warning:__ Do not match this case directly, use _when-else_ instead. + */ + @ProcessOutInternalApi + UNKNOWN(String()) +} diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt index 452a0b220..a6e561b89 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt @@ -156,6 +156,10 @@ sealed class PODynamicCheckoutPaymentMethod { * @param[cvcRequired] Defines whether the card CVC should be collected. * @param[cardholderNameRequired] Defines whether the cardholder name should be collected. * @param[schemeSelectionAllowed] Defines whether the user will be asked to select the scheme if co-scheme is available. + * @param[schemeSelectionDefaultOrder] Schemes selection preferred order. + * When cards with multiple supported schemes are detected, the first matching scheme is preselected. + * @param[restrictToIins] Restrict card schemes to the ones with IINs in the set. + * @param[restrictToSchemes] Restrict card schemes to the ones in the set. * @param[billingAddress] Card billing address configuration. * @param[savingAllowed] Defines whether saving of the payment method is allowed for future payments. */ @@ -167,6 +171,12 @@ sealed class PODynamicCheckoutPaymentMethod { val cardholderNameRequired: Boolean, @Json(name = "scheme_selection_allowed") val schemeSelectionAllowed: Boolean, + @Json(name = "scheme_selection_default_order") + val schemeSelectionDefaultOrder: List?, + @Json(name = "restrict_to_iins") + val restrictToIins: Set?, + @Json(name = "restrict_to_schemes") + val restrictToSchemes: Set?, @Json(name = "billing_address") val billingAddress: BillingAddressConfiguration, @Json(name = "saving_allowed") diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt index 82b320578..4a08e9d64 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt @@ -8,10 +8,7 @@ import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest -import com.processout.sdk.api.model.response.POInvoice -import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethod -import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodCapture -import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails +import com.processout.sdk.api.model.response.* import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentUrlResolutionResponse import com.processout.sdk.api.repository.InvoicesRepository @@ -166,6 +163,68 @@ internal class DefaultInvoicesService( } } + override suspend fun authorizeV2( + request: POInvoiceAuthorizationRequest, + threeDSService: PO3DSService + ): ProcessOutResult { + val logAttributes = mapOf(POLogAttribute.INVOICE_ID to request.invoiceId) + return try { + repository.authorizeInvoice(request) + .fold( + onSuccess = { response -> + if (response.customerAction == null) { + threeDSService.cleanup() + val authorizationResponse = POInvoiceAuthorizationResponse( + outcome = response.outcome, + customerTokenId = response.customerTokenId + ) + return@fold ProcessOutResult.Success(authorizationResponse) + } + customerActionsService.handle(response.customerAction, threeDSService) + .fold( + onSuccess = { newSource -> + authorizeV2( + request.copy(source = newSource), + threeDSService + ) + }, + onFailure = { failure -> + POLogger.warn( + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + failure + } + ) + }, + onFailure = { failure -> + POLogger.warn( + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + failure + } + ) + } catch (e: CancellationException) { + coroutineScope { + val failure = ProcessOutResult.Failure( + code = Cancelled, + message = e.message, + cause = e + ) + POLogger.info( + message = "Invoice authorization has been cancelled: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + ensureActive() + failure + } + } + } + override suspend fun authorize( request: PONativeAlternativePaymentAuthorizationRequest ): ProcessOutResult = diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt index 7dafd0432..389c5040e 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt @@ -6,10 +6,7 @@ import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest -import com.processout.sdk.api.model.response.POInvoice -import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethod -import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodCapture -import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails +import com.processout.sdk.api.model.response.* import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentUrlResolutionResponse import com.processout.sdk.core.ProcessOutCallback @@ -67,6 +64,15 @@ interface POInvoicesService { threeDSService: PO3DSService ): ProcessOutResult + /** + * Authorize invoice with the given request and 3DS service implementation. + * Provides the result with [POInvoiceAuthorizationResponse]. + */ + suspend fun authorizeV2( + request: POInvoiceAuthorizationRequest, + threeDSService: PO3DSService + ): ProcessOutResult + /** * Authorize invoice with the given request. */ diff --git a/sdk/src/main/res/values-ar/strings.xml b/sdk/src/main/res/values-ar/strings.xml index 7f8ef340f..71c2b4cf2 100644 --- a/sdk/src/main/res/values-ar/strings.xml +++ b/sdk/src/main/res/values-ar/strings.xml @@ -5,6 +5,7 @@ الدفع السريع + طرق دفع أخرى ادفع إلغاء حِفْظُ هذِهِ الطَّريقَةِ لِلدَّفْعِ في المُستَقبَلِ diff --git a/sdk/src/main/res/values-fr/strings.xml b/sdk/src/main/res/values-fr/strings.xml index 9b6f1925a..8101b0171 100644 --- a/sdk/src/main/res/values-fr/strings.xml +++ b/sdk/src/main/res/values-fr/strings.xml @@ -5,6 +5,7 @@ Paiement express + Autres modes de paiement Payer Annuler Enregistrer cette méthode pour les paiements futurs diff --git a/sdk/src/main/res/values-pl/strings.xml b/sdk/src/main/res/values-pl/strings.xml index de82d9152..197c44fea 100644 --- a/sdk/src/main/res/values-pl/strings.xml +++ b/sdk/src/main/res/values-pl/strings.xml @@ -5,6 +5,7 @@ Szybka płatność + Inne metody płatności Zapłać Anuluj Zapisz tę metodę płatności na przyszłe płatności diff --git a/sdk/src/main/res/values-pt/strings.xml b/sdk/src/main/res/values-pt/strings.xml index 681e107d3..d3797a669 100644 --- a/sdk/src/main/res/values-pt/strings.xml +++ b/sdk/src/main/res/values-pt/strings.xml @@ -5,6 +5,7 @@ Pagamento expresso + Outras formas de pagamento Pagar Cancelar Guardar este método para futuros pagamentos diff --git a/sdk/src/main/res/values/strings.xml b/sdk/src/main/res/values/strings.xml index d19e4fce6..59945f283 100644 --- a/sdk/src/main/res/values/strings.xml +++ b/sdk/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ Express checkout + Other payment methods Pay Cancel Save this method for future payments diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt index b69ad7f1f..9012f5541 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt @@ -25,7 +25,6 @@ import com.processout.sdk.ui.card.tokenization.CardTokenizationCompletion.Succes import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent.CardScannerResult import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent.Dismiss import com.processout.sdk.ui.card.tokenization.CardTokenizationSideEffect.CardScanner -import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.Button import com.processout.sdk.ui.card.tokenization.screen.CardTokenizationScreen import com.processout.sdk.ui.core.theme.ProcessOutTheme import com.processout.sdk.ui.shared.component.displayCutoutHeight @@ -60,7 +59,7 @@ internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index fd3bf2d14..af6e3ab34 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -139,24 +139,30 @@ internal class CardTokenizationInteractor( latestShouldContinueRequest = null } - private fun initState() = CardTokenizationInteractorState( - cardFields = cardFields(), - addressFields = emptyList(), - preferredSchemeField = Field( - id = FieldId.PREFERRED_SCHEME, - shouldCollect = false - ), - saveCardField = Field( - id = FieldId.SAVE_CARD, - value = TextFieldValue(text = "false"), - shouldCollect = configuration.savingAllowed - ), - focusedFieldId = CardFieldId.NUMBER, - pendingFocusedFieldId = null, - primaryActionId = ActionId.SUBMIT, - secondaryActionId = ActionId.CANCEL, - cardScannerActionId = ActionId.CARD_SCANNER - ) + private fun initState(): CardTokenizationInteractorState { + val saveCardFieldValue: Boolean = configuration.saving?.let { + it.required || it.enabledByDefault + } ?: false + return CardTokenizationInteractorState( + cardFields = cardFields(), + addressFields = emptyList(), + preferredSchemeField = Field( + id = FieldId.PREFERRED_SCHEME, + shouldCollect = false + ), + saveCardField = Field( + id = FieldId.SAVE_CARD, + value = TextFieldValue(text = saveCardFieldValue.toString()), + enabled = configuration.saving?.required != true, + shouldCollect = configuration.saving != null + ), + focusedFieldId = CardFieldId.NUMBER, + pendingFocusedFieldId = null, + primaryActionId = ActionId.SUBMIT, + secondaryActionId = ActionId.CANCEL, + cardScannerActionId = ActionId.CARD_SCANNER + ) + } private fun cardFields(): List = mutableListOf( Field(id = CardFieldId.NUMBER), @@ -303,7 +309,7 @@ internal class CardTokenizationInteractor( field.copy(enabled = enabled) }, preferredSchemeField = it.preferredSchemeField.copy(enabled = enabled), - saveCardField = it.saveCardField.copy(enabled = enabled) + saveCardField = it.saveCardField.copy(enabled = configuration.saving?.required != true) ) } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt index 0ec8aba8e..6748bfe2c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt @@ -25,7 +25,8 @@ import kotlinx.parcelize.Parcelize * @param[preferredScheme] Preferred scheme selection configuration. * Shows scheme selection if co-scheme is available. Use _null_ to hide. * @param[billingAddress] Allows to customize the collection of billing address. - * @param[savingAllowed] Displays checkbox that allows to save the card details for future payments. + * @param[saving] Card saving configuration. Displays checkbox that allows to save the card details for future payments. + * Use _null_ to hide, this is a default behaviour. * @param[submitButton] Submit button configuration. * @param[cancelButton] Cancel button configuration. Use _null_ to hide. * @param[bottomSheet] Specifies bottom sheet configuration. By default is [WrapContent] and non-expandable. @@ -40,7 +41,7 @@ data class POCardTokenizationConfiguration( val cardScanner: CardScannerConfiguration? = null, val preferredScheme: PreferredSchemeConfiguration? = PreferredSchemeConfiguration(), val billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(), - val savingAllowed: Boolean = false, + val saving: SavingConfiguration? = null, val submitButton: Button = Button(), val cancelButton: CancelButton? = CancelButton(), val bottomSheet: POBottomSheetConfiguration = POBottomSheetConfiguration( @@ -52,6 +53,53 @@ data class POCardTokenizationConfiguration( internal val _submitButton: Button? = submitButton ) : Parcelable { + /** + * Defines card tokenization configuration. + * + * @param[title] Custom title. + * @param[cvcRequired] Specifies whether the CVC field should be displayed. Default value is _true_. + * @param[cardholderNameRequired] Specifies whether the cardholder name field should be displayed. Default value is _true_. + * @param[cardScanner] Card scanner configuration. Use _null_ to hide, this is a default behaviour. + * @param[preferredScheme] Preferred scheme selection configuration. + * Shows scheme selection if co-scheme is available. Use _null_ to hide. + * @param[billingAddress] Allows to customize the collection of billing address. + * @param[savingAllowed] Displays checkbox that allows to save the card details for future payments. + * @param[submitButton] Submit button configuration. + * @param[cancelButton] Cancel button configuration. Use _null_ to hide. + * @param[bottomSheet] Specifies bottom sheet configuration. By default is [WrapContent] and non-expandable. + * @param[metadata] Metadata related to the card. + * @param[style] Custom style. + */ + @Deprecated(message = "Use alternative constructor.") + constructor( + title: String? = null, + cvcRequired: Boolean = true, + cardholderNameRequired: Boolean = true, + cardScanner: CardScannerConfiguration? = null, + preferredScheme: PreferredSchemeConfiguration? = PreferredSchemeConfiguration(), + billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(), + savingAllowed: Boolean = false, + submitButton: Button = Button(), + cancelButton: CancelButton? = CancelButton(), + bottomSheet: POBottomSheetConfiguration = POBottomSheetConfiguration( + height = WrapContent, + expandable = false + ), + metadata: Map? = null, + style: Style? = null + ) : this( + title = title, + cvcRequired = cvcRequired, + cardholderNameRequired = cardholderNameRequired, + billingAddress = billingAddress, + saving = if (savingAllowed) SavingConfiguration() else null, + submitButton = submitButton, + cancelButton = cancelButton, + bottomSheet = bottomSheet, + metadata = metadata, + style = style + ) + /** * Defines card tokenization configuration. * @@ -153,6 +201,18 @@ data class POCardTokenizationConfiguration( } } + /** + * Card saving configuration. + * + * @param[enabledByDefault] Initial selection state. + * @param[required] If `true`, saving is enforced and cannot be disabled by the user. + */ + @Parcelize + data class SavingConfiguration( + val enabledByDefault: Boolean = false, + val required: Boolean = false + ) : Parcelable + /** * Button configuration. * diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/component/POCardTokenizationViewComponent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/component/POCardTokenizationViewComponent.kt index 3ab878295..7ca047ae2 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/component/POCardTokenizationViewComponent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/component/POCardTokenizationViewComponent.kt @@ -147,7 +147,7 @@ class POCardTokenizationViewComponent private constructor( cardScanner = cardScanner, preferredScheme = preferredScheme, billingAddress = billingAddress, - savingAllowed = savingAllowed, + saving = saving, submitButton = Button(), cancelButton = cancelButton, bottomSheet = POBottomSheetConfiguration( diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/component/POCardTokenizationViewComponentConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/component/POCardTokenizationViewComponentConfiguration.kt index ad626d5b1..1babaf54a 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/component/POCardTokenizationViewComponentConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/component/POCardTokenizationViewComponentConfiguration.kt @@ -11,7 +11,8 @@ import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.* * @param[preferredScheme] Preferred scheme selection configuration. * Shows scheme selection if co-scheme is available. Use _null_ to hide. * @param[billingAddress] Allows to customize the collection of billing address. - * @param[savingAllowed] Displays checkbox that allows to save the card details for future payments. + * @param[saving] Card saving configuration. Displays checkbox that allows to save the card details for future payments. + * Use _null_ to hide, this is a default behaviour. * @param[submitButton] Submit button configuration. Use _null_ to hide. * @param[cancelButton] Cancel button configuration. Use _null_ to hide. * @param[metadata] Metadata related to the card. @@ -23,9 +24,50 @@ data class POCardTokenizationViewComponentConfiguration( val cardScanner: CardScannerConfiguration? = null, val preferredScheme: PreferredSchemeConfiguration? = PreferredSchemeConfiguration(), val billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(), - val savingAllowed: Boolean = false, + val saving: SavingConfiguration? = null, val submitButton: Button? = Button(), val cancelButton: CancelButton? = CancelButton(), val metadata: Map? = null, val style: Style? = null -) +) { + + /** + * Defines card tokenization view component configuration. + * + * @param[cvcRequired] Specifies whether the CVC field should be displayed. Default value is _true_. + * @param[cardholderNameRequired] Specifies whether the cardholder name field should be displayed. Default value is _true_. + * @param[cardScanner] Card scanner configuration. Use _null_ to hide, this is a default behaviour. + * @param[preferredScheme] Preferred scheme selection configuration. + * Shows scheme selection if co-scheme is available. Use _null_ to hide. + * @param[billingAddress] Allows to customize the collection of billing address. + * @param[savingAllowed] Displays checkbox that allows to save the card details for future payments. + * @param[submitButton] Submit button configuration. Use _null_ to hide. + * @param[cancelButton] Cancel button configuration. Use _null_ to hide. + * @param[metadata] Metadata related to the card. + * @param[style] Allows to customize the look and feel. + */ + @Deprecated(message = "Use alternative constructor.") + constructor( + cvcRequired: Boolean = true, + cardholderNameRequired: Boolean = true, + cardScanner: CardScannerConfiguration? = null, + preferredScheme: PreferredSchemeConfiguration? = PreferredSchemeConfiguration(), + billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(), + savingAllowed: Boolean = false, + submitButton: Button? = Button(), + cancelButton: CancelButton? = CancelButton(), + metadata: Map? = null, + style: Style? = null + ) : this( + cvcRequired = cvcRequired, + cardholderNameRequired = cardholderNameRequired, + cardScanner = cardScanner, + preferredScheme = preferredScheme, + billingAddress = billingAddress, + saving = if (savingAllowed) SavingConfiguration() else null, + submitButton = submitButton, + cancelButton = cancelButton, + metadata = metadata, + style = style + ) +} diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/screen/CardTokenizationContent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/screen/CardTokenizationContent.kt index ab1523647..f884f41eb 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/screen/CardTokenizationContent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/screen/CardTokenizationContent.kt @@ -355,6 +355,7 @@ private fun CheckboxField( }, modifier = modifier, checkboxStyle = style, + enabled = state.enabled, isError = state.isError ) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt index 632e7edbe..bd75a53f6 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt @@ -39,9 +39,10 @@ import com.processout.sdk.core.onSuccess import com.processout.sdk.ui.base.BaseInteractor import com.processout.sdk.ui.card.tokenization.* import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.BillingAddressConfiguration.CollectionMode -import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationProcessingRequest -import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationShouldContinueRequest -import com.processout.sdk.ui.card.tokenization.delegate.toResponse +import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.SavingConfiguration +import com.processout.sdk.ui.card.tokenization.delegate.* +import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility.Eligible +import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility.NotEligible import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.* import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.* import com.processout.sdk.ui.checkout.DynamicCheckoutInteractorState.* @@ -64,7 +65,6 @@ import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.* import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow import com.processout.sdk.ui.napm.delegate.v2.NativeAlternativePaymentDefaultValuesRequest import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentEvent -import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentEvent.WillSubmitParameters import com.processout.sdk.ui.savedpaymentmethods.POSavedPaymentMethodsConfiguration import com.processout.sdk.ui.shared.extension.orElse import com.processout.sdk.ui.shared.state.FieldValue @@ -291,9 +291,13 @@ internal class DynamicCheckoutInteractor( gatewayConfigurationId = paymentMethod.configuration.gatewayConfigurationId, redirectUrl = redirectUrl, savePaymentMethodField = if (paymentMethod.configuration.savingAllowed) { + val value: Boolean = configuration.saving?.let { + it.required || it.enabledByDefault + } ?: false Field( id = FieldId.SAVE_PAYMENT_METHOD, - value = TextFieldValue(text = "false") + value = TextFieldValue(text = value.toString()), + enabled = configuration.saving?.required != true ) } else null, display = paymentMethod.display, @@ -503,7 +507,11 @@ internal class DynamicCheckoutInteractor( mode = configuration.billingAddress.collectionMode.map(), countryCodes = configuration.billingAddress.restrictToCountryCodes ), - savingAllowed = configuration.savingAllowed + saving = if (configuration.savingAllowed) + SavingConfiguration( + enabledByDefault = saving?.enabledByDefault ?: false, + required = saving?.required ?: false + ) else null ) private fun POBillingAddressCollectionMode.map(): CollectionMode = @@ -989,7 +997,7 @@ internal class DynamicCheckoutInteractor( POLogger.info("Authorizing the invoice.", attributes = logAttributes) val threeDSService = PODefaultProxy3DSService() val job = interactorScope.launch { - val result = invoicesService.authorize( + val result = invoicesService.authorizeV2( request = response.request, threeDSService = threeDSService ) @@ -1009,11 +1017,22 @@ internal class DynamicCheckoutInteractor( private fun handleInvoiceAuthorization( state: DynamicCheckoutInteractorState, invoiceId: String, - result: ProcessOutResult + result: ProcessOutResult ) { if (invoiceId != state.invoice?.id) { return } + result.onSuccess { response -> + val customerTokenId = response.customerTokenId + val paymentMethod = state.processingPaymentMethod + if (customerTokenId != null && paymentMethod != null) { + val didTokenizeEvent = DidTokenizePaymentMethod( + paymentMethod = paymentMethod.original, + customerTokenId = customerTokenId + ) + dispatch(didTokenizeEvent) + } + } when (state.processingPaymentMethod) { is Card -> latestCardProcessingRequest?.let { request -> interactorScope.launch { @@ -1089,6 +1108,74 @@ internal class DynamicCheckoutInteractor( } private fun dispatchEvents() { + dispatchCardTokenizationEvents() + dispatchNativeAlternativePaymentEvents() + } + + private fun dispatchCardTokenizationEvents() { + eventDispatcher.subscribeForResponse( + coroutineScope = interactorScope + ) { response -> + activePaymentMethod()?.let { paymentMethod -> + if (paymentMethod is Card) { + interactorScope.launch { + val eligibilityByIins: POCardTokenizationEligibility? = + paymentMethod.configuration.restrictToIins?.let { eligibleIins -> + val isEligible = eligibleIins.any { eligibleIin -> + response.iin.startsWith(eligibleIin) + } + if (isEligible) Eligible() else NotEligible() + } + val eligibilityBySchemes: POCardTokenizationEligibility? = + paymentMethod.configuration.restrictToSchemes?.let { eligibleSchemes -> + val scheme = response.issuerInformation.scheme + val coScheme = response.issuerInformation.coScheme + val isSchemeEligible = scheme in eligibleSchemes + val isCoSchemeEligible = coScheme in eligibleSchemes + when { + coScheme == null -> if (isSchemeEligible) Eligible(scheme) else NotEligible() + !isSchemeEligible && !isCoSchemeEligible -> NotEligible() + isSchemeEligible && !isCoSchemeEligible -> Eligible(scheme) + !isSchemeEligible -> Eligible(coScheme) + else -> Eligible() + } + } + val eligibility = response.eligibility + ?: eligibilityByIins + ?: eligibilityBySchemes + ?: Eligible() + val eligibilityResponse = CardTokenizationEligibilityResponse( + uuid = response.uuid, + eligibility = eligibility + ) + eventDispatcher.send(eligibilityResponse) + } + } + } + } + + eventDispatcher.subscribeForResponse( + coroutineScope = interactorScope + ) { response -> + activePaymentMethod()?.let { paymentMethod -> + if (paymentMethod is Card) { + interactorScope.launch { + val schemeSelectionDefaultOrder = paymentMethod.configuration.schemeSelectionDefaultOrder + val issuerInformation = response.issuerInformation + val preferredScheme = response.preferredScheme + ?: schemeSelectionDefaultOrder?.firstOrNull { scheme -> + scheme == issuerInformation.scheme || scheme == issuerInformation.coScheme + } ?: issuerInformation.scheme + val preferredSchemeResponse = CardTokenizationPreferredSchemeResponse( + uuid = response.uuid, + preferredScheme = preferredScheme + ) + eventDispatcher.send(preferredSchemeResponse) + } + } + } + } + eventDispatcher.subscribeForRequest( coroutineScope = interactorScope ) { request -> @@ -1097,6 +1184,17 @@ internal class DynamicCheckoutInteractor( eventDispatcher.send(request.toResponse(shouldContinue)) } } + } + + private fun dispatchNativeAlternativePaymentEvents() { + eventDispatcher.subscribe( + coroutineScope = interactorScope + ) { event -> + if (event is PONativeAlternativePaymentEvent.WillStart) { + _state.update { it.copy(processingPaymentMethod = _state.value.selectedPaymentMethod) } + } + } + eventDispatcher.subscribeForRequest( coroutineScope = interactorScope ) { request -> @@ -1113,13 +1211,6 @@ internal class DynamicCheckoutInteractor( } } } - eventDispatcher.subscribe( - coroutineScope = interactorScope - ) { event -> - if (event is WillSubmitParameters) { - _state.update { it.copy(processingPaymentMethod = _state.value.selectedPaymentMethod) } - } - } } private fun dispatchSideEffects() { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt index bbb71d0d4..48b143316 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt @@ -65,7 +65,8 @@ internal data class DynamicCheckoutInteractorState( data class Field( val id: String, - val value: TextFieldValue = TextFieldValue() + val value: TextFieldValue = TextFieldValue(), + val enabled: Boolean = true ) data class Actions( diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt index 3da34761c..14be973db 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt @@ -183,9 +183,9 @@ private fun Content( isLightTheme = isLightTheme ) } - if (state.regularPayments.elements.isNotEmpty()) { - RegularPayments( - payments = state.regularPayments, + state.regularCheckout?.let { + RegularCheckout( + state = it, onEvent = onEvent, style = style, isLightTheme = isLightTheme @@ -195,27 +195,7 @@ private fun Content( } @Composable -private fun ExpressCheckout( - state: ExpressCheckout, - onEvent: (DynamicCheckoutEvent) -> Unit, - style: DynamicCheckoutScreen.Style, - isLightTheme: Boolean -) { - ExpressCheckoutHeader( - state = state.header, - onEvent = onEvent, - style = style.sectionHeader - ) - ExpressPayments( - payments = state.expressPayments, - onEvent = onEvent, - style = style, - isLightTheme = isLightTheme - ) -} - -@Composable -private fun ExpressCheckoutHeader( +private fun SectionHeader( state: SectionHeader, onEvent: (DynamicCheckoutEvent) -> Unit, style: SectionHeaderStyle, @@ -259,6 +239,28 @@ private fun ExpressCheckoutHeader( } } +@Composable +private fun ExpressCheckout( + state: ExpressCheckout, + onEvent: (DynamicCheckoutEvent) -> Unit, + style: DynamicCheckoutScreen.Style, + isLightTheme: Boolean +) { + state.header?.let { + SectionHeader( + state = it, + onEvent = onEvent, + style = style.sectionHeader + ) + } + ExpressPayments( + payments = state.expressPayments, + onEvent = onEvent, + style = style, + isLightTheme = isLightTheme + ) +} + @Composable private fun ExpressPayments( payments: POImmutableList, @@ -356,6 +358,28 @@ private fun ExpressPayment( } } +@Composable +private fun RegularCheckout( + state: RegularCheckout, + onEvent: (DynamicCheckoutEvent) -> Unit, + style: DynamicCheckoutScreen.Style, + isLightTheme: Boolean +) { + state.header?.let { + SectionHeader( + state = it, + onEvent = onEvent, + style = style.sectionHeader + ) + } + RegularPayments( + payments = state.regularPayments, + onEvent = onEvent, + style = style, + isLightTheme = isLightTheme + ) +} + @Composable private fun RegularPayments( payments: POImmutableList, @@ -579,13 +603,15 @@ private fun CheckboxField( text = state.label ?: String(), checked = state.value.text.toBooleanStrictOrNull() ?: false, onCheckedChange = { - onEvent( - FieldValueChanged( - paymentMethodId = id, - fieldId = state.id, - value = FieldValue.Text(value = TextFieldValue(text = it.toString())) + if (state.enabled) { + onEvent( + FieldValueChanged( + paymentMethodId = id, + fieldId = state.id, + value = FieldValue.Text(value = TextFieldValue(text = it.toString())) + ) ) - ) + } }, modifier = modifier, style = style, diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index f4321a73c..cf4d30bc4 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -104,7 +104,7 @@ internal class DynamicCheckoutViewModel private constructor( } else { Loaded( expressCheckout = expressCheckout(interactorState), - regularPayments = regularPayments(interactorState, cardTokenizationState, nativeAlternativePaymentState), + regularCheckout = regularCheckout(interactorState, cardTokenizationState, nativeAlternativePaymentState), cancelAction = cancelAction, errorMessage = interactorState.errorMessage ) @@ -189,34 +189,49 @@ internal class DynamicCheckoutViewModel private constructor( private fun expressCheckout( interactorState: DynamicCheckoutInteractorState ): ExpressCheckout? { + val configuration = configuration.expressCheckout ?: return null val expressPayments = expressPayments(interactorState) if (expressPayments.isEmpty()) { return null } return ExpressCheckout( - header = SectionHeader( - title = configuration.expressCheckout.title - ?: app.getString(R.string.po_dynamic_checkout_express_checkout), - action = savedPaymentMethodsAction(interactorState) - ), + header = expressCheckoutSectionHeader(interactorState, configuration), expressPayments = POImmutableList(expressPayments) ) } + private fun expressCheckoutSectionHeader( + interactorState: DynamicCheckoutInteractorState, + configuration: PODynamicCheckoutConfiguration.ExpressCheckout + ): SectionHeader? { + val savedPaymentMethodsAction = savedPaymentMethodsAction(interactorState, configuration) + if (configuration.title?.isBlank() == true && savedPaymentMethodsAction == null) { + return null + } + return SectionHeader( + title = configuration.title + ?: app.getString(R.string.po_dynamic_checkout_express_checkout), + action = savedPaymentMethodsAction + ) + } + private fun savedPaymentMethodsAction( - interactorState: DynamicCheckoutInteractorState - ): POActionState? = with(configuration.expressCheckout) { - if (interactorState.paymentMethods.deletingAllowed) - POActionState( - id = interactorState.actions.savedPaymentMethodsId, - text = settingsButton?.text ?: String(), - primary = false, - icon = settingsButton?.icon - ?: PODrawableImage( - resId = com.processout.sdk.ui.R.drawable.po_icon_settings, - renderingMode = POImageRenderingMode.ORIGINAL - ) - ) else null + interactorState: DynamicCheckoutInteractorState, + configuration: PODynamicCheckoutConfiguration.ExpressCheckout + ): POActionState? { + if (!interactorState.paymentMethods.deletingAllowed) { + return null + } + return POActionState( + id = interactorState.actions.savedPaymentMethodsId, + text = configuration.settingsButton?.text ?: String(), + primary = false, + icon = configuration.settingsButton?.icon + ?: PODrawableImage( + resId = com.processout.sdk.ui.R.drawable.po_icon_settings, + renderingMode = POImageRenderingMode.ORIGINAL + ) + ) } private val List.deletingAllowed: Boolean @@ -284,11 +299,40 @@ internal class DynamicCheckoutViewModel private constructor( ) } + private fun regularCheckout( + interactorState: DynamicCheckoutInteractorState, + cardTokenizationState: CardTokenizationViewModelState, + nativeAlternativePaymentState: NativeAlternativePaymentViewModelState + ): RegularCheckout? { + val configuration = configuration.regularCheckout ?: return null + val regularPayments = regularPayments(interactorState, cardTokenizationState, nativeAlternativePaymentState) + if (regularPayments.isEmpty()) { + return null + } + return RegularCheckout( + header = regularCheckoutSectionHeader(configuration), + regularPayments = POImmutableList(regularPayments) + ) + } + + private fun regularCheckoutSectionHeader( + configuration: PODynamicCheckoutConfiguration.RegularCheckout + ): SectionHeader? { + if (configuration.title?.isBlank() == true) { + return null + } + return SectionHeader( + title = configuration.title + ?: app.getString(R.string.po_dynamic_checkout_regular_checkout), + action = null + ) + } + private fun regularPayments( interactorState: DynamicCheckoutInteractorState, cardTokenizationState: CardTokenizationViewModelState, nativeAlternativePaymentState: NativeAlternativePaymentViewModelState - ): POImmutableList = + ): List = interactorState.paymentMethods.mapNotNull { paymentMethod -> val id = paymentMethod.id val selected = id == interactorState.selectedPaymentMethod?.id @@ -342,7 +386,7 @@ internal class DynamicCheckoutViewModel private constructor( ) else -> null } - }.let { POImmutableList(it) } + } private fun regularPaymentState( display: Display, @@ -375,7 +419,8 @@ internal class DynamicCheckoutViewModel private constructor( FieldState( id = id, value = value, - label = title + label = title, + enabled = enabled ) ) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt index cac2df5dd..870d478aa 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt @@ -22,7 +22,7 @@ internal sealed interface DynamicCheckoutViewModelState { @Immutable data class Loaded( val expressCheckout: ExpressCheckout?, - val regularPayments: POImmutableList, + val regularCheckout: RegularCheckout?, val cancelAction: POActionState?, val errorMessage: String? = null ) : DynamicCheckoutViewModelState @@ -42,10 +42,16 @@ internal sealed interface DynamicCheckoutViewModelState { @Immutable data class ExpressCheckout( - val header: SectionHeader, + val header: SectionHeader?, val expressPayments: POImmutableList ) + @Immutable + data class RegularCheckout( + val header: SectionHeader?, + val regularPayments: POImmutableList + ) + @Immutable sealed interface ExpressPayment { @Immutable diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutActivity.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutActivity.kt index 315b601fc..bdf04bade 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutActivity.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutActivity.kt @@ -114,6 +114,12 @@ class PODynamicCheckoutActivity : POBaseTransparentPortraitActivity() { defaultAddress = billingAddress.defaultAddress, attachDefaultsToPaymentMethod = billingAddress.attachDefaultsToPaymentMethod ), + saving = configuration.saving?.let { + POCardTokenizationConfiguration.SavingConfiguration( + enabledByDefault = it.enabledByDefault, + required = it.required + ) + }, submitButton = configuration.submitButton.let { POCardTokenizationConfiguration.Button( text = it.text, diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt index 4b7beb247..a8812d28d 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt @@ -23,9 +23,17 @@ import kotlinx.parcelize.Parcelize * * @param[invoiceRequest] Request to fetch invoice for payment. * @param[expressCheckout] Express checkout section configuration. + * Controls the visibility and appearance of the saved payment methods. + * Set this value to `null` to hide the entire "Express Checkout" section and omit saved payments from the flow. + * This setting does not affect the availability of other payment options, nor does it affect whether the user can save a payment method. + * @param[regularCheckout] Regular checkout section configuration. + * Controls the visibility and appearance of the regular payment methods section. + * Set this value to `null` to hide the entire "Regular Checkout" section. + * This setting does not affect the availability of saved payment options. * @param[card] Card payment configuration. * @param[googlePay] Google Pay configuration. * @param[alternativePayment] Alternative payment configuration. + * @param[saving] Saving configuration for supported payment methods. * @param[submitButton] Submit button configuration. * @param[cancelButton] Cancel button configuration. * @param[cancelOnBackPressed] Specifies whether the screen should be cancelled on back button press or back gesture. @@ -40,10 +48,12 @@ import kotlinx.parcelize.Parcelize @Parcelize data class PODynamicCheckoutConfiguration( val invoiceRequest: POInvoiceRequest, - val expressCheckout: ExpressCheckout = ExpressCheckout(), + val expressCheckout: ExpressCheckout? = ExpressCheckout(), + val regularCheckout: RegularCheckout? = RegularCheckout(), val card: CardConfiguration = CardConfiguration(), val googlePay: GooglePayConfiguration = GooglePayConfiguration(), val alternativePayment: AlternativePaymentConfiguration = AlternativePaymentConfiguration(), + val saving: SavingConfiguration? = SavingConfiguration(), val submitButton: Button = Button(), val cancelButton: CancelButton? = CancelButton(), val cancelOnBackPressed: Boolean = true, @@ -55,7 +65,7 @@ data class PODynamicCheckoutConfiguration( /** * Specifies express checkout section configuration. * - * @param[title] Custom section title. + * @param[title] Custom section title. Set `null` to use the default value, or an empty string to remove the title. * @param[settingsButton] Settings button configuration. */ @Parcelize @@ -64,6 +74,16 @@ data class PODynamicCheckoutConfiguration( val settingsButton: Button? = null ) : Parcelable + /** + * Specifies regular checkout section configuration. + * + * @param[title] Custom section title. Set `null` to use the default value, or an empty string to remove the title. + */ + @Parcelize + data class RegularCheckout( + val title: String? = String() + ) : Parcelable + /** * Specifies card payment configuration. * @@ -282,6 +302,18 @@ data class PODynamicCheckoutConfiguration( ) : Parcelable } + /** + * Payment method saving configuration. + * + * @param[enabledByDefault] Initial selection state. + * @param[required] If `true`, saving is enforced and cannot be disabled by the user. + */ + @Parcelize + data class SavingConfiguration( + val enabledByDefault: Boolean = false, + val required: Boolean = false + ) : Parcelable + /** * Button configuration. * diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutLauncher.kt index 240103b89..45170eee7 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutLauncher.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutLauncher.kt @@ -17,8 +17,6 @@ import com.processout.sdk.core.POUnit import com.processout.sdk.core.ProcessOutActivityResult import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationEligibilityRequest import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationPreferredSchemeRequest -import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility.Eligible -import com.processout.sdk.ui.card.tokenization.delegate.toResponse import com.processout.sdk.ui.checkout.delegate.* import com.processout.sdk.ui.core.annotation.ProcessOutInternalApi import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentEvent @@ -148,7 +146,11 @@ class PODynamicCheckoutLauncher private constructor( coroutineScope = scope ) { request -> scope.launch { - eventDispatcher.send(request.toResponse(eligibility = Eligible())) + val eligibility = delegate.evaluateEligibility( + iin = request.iin, + issuerInformation = request.issuerInformation + ) + eventDispatcher.send(request.toDynamicCheckoutResponse(eligibility)) } } } @@ -159,7 +161,7 @@ class PODynamicCheckoutLauncher private constructor( ) { request -> scope.launch { val preferredScheme = delegate.preferredScheme(request.issuerInformation) - eventDispatcher.send(request.toResponse(preferredScheme)) + eventDispatcher.send(request.toDynamicCheckoutResponse(preferredScheme)) } } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/DynamicCheckoutCardEligibility.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/DynamicCheckoutCardEligibility.kt new file mode 100644 index 000000000..349a7e538 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/DynamicCheckoutCardEligibility.kt @@ -0,0 +1,18 @@ +package com.processout.sdk.ui.checkout.delegate + +import com.processout.sdk.api.dispatcher.POEventDispatcher +import com.processout.sdk.api.model.response.POCardIssuerInformation +import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationEligibilityRequest +import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility +import java.util.UUID + +internal data class DynamicCheckoutCardEligibilityResponse( + override val uuid: UUID, + val iin: String, + val issuerInformation: POCardIssuerInformation, + val eligibility: POCardTokenizationEligibility? +) : POEventDispatcher.Response + +internal fun CardTokenizationEligibilityRequest.toDynamicCheckoutResponse( + eligibility: POCardTokenizationEligibility? +) = DynamicCheckoutCardEligibilityResponse(uuid, iin, issuerInformation, eligibility) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/DynamicCheckoutCardPreferredScheme.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/DynamicCheckoutCardPreferredScheme.kt new file mode 100644 index 000000000..a0adfdcd4 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/DynamicCheckoutCardPreferredScheme.kt @@ -0,0 +1,16 @@ +package com.processout.sdk.ui.checkout.delegate + +import com.processout.sdk.api.dispatcher.POEventDispatcher +import com.processout.sdk.api.model.response.POCardIssuerInformation +import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationPreferredSchemeRequest +import java.util.UUID + +internal data class DynamicCheckoutCardPreferredSchemeResponse( + override val uuid: UUID, + val issuerInformation: POCardIssuerInformation, + val preferredScheme: String? +) : POEventDispatcher.Response + +internal fun CardTokenizationPreferredSchemeRequest.toDynamicCheckoutResponse( + preferredScheme: String? +) = DynamicCheckoutCardPreferredSchemeResponse(uuid, issuerInformation, preferredScheme) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/PODynamicCheckoutDelegate.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/PODynamicCheckoutDelegate.kt index 28ebe416a..faea776c7 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/PODynamicCheckoutDelegate.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/PODynamicCheckoutDelegate.kt @@ -7,6 +7,7 @@ import com.processout.sdk.api.model.response.POCardIssuerInformation import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod import com.processout.sdk.api.model.response.POInvoice import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement +import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration import com.processout.sdk.ui.core.annotation.ProcessOutInternalApi import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentEvent @@ -61,13 +62,24 @@ interface PODynamicCheckoutDelegate { request: POInvoiceAuthorizationRequest ): POInvoiceAuthorizationRequest = request + /** + * Allows to evaluate card eligibility for tokenization based on issuer information. + * + * @param[iin] Issuer identification number. + * @param[issuerInformation] Resolved issuer information. + */ + suspend fun evaluateEligibility( + iin: String, + issuerInformation: POCardIssuerInformation + ): POCardTokenizationEligibility? = null + /** * Allows to choose default preferred card scheme based on issuer information. * Primary card scheme is used by default. */ fun preferredScheme( issuerInformation: POCardIssuerInformation - ): String? = issuerInformation.scheme + ): String? = null /** * Allows to prefill default values for the given [parameters] during native alternative payment. diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/PODynamicCheckoutEvent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/PODynamicCheckoutEvent.kt index 1dcfdf9ae..da708036f 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/PODynamicCheckoutEvent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/delegate/PODynamicCheckoutEvent.kt @@ -43,6 +43,14 @@ sealed class PODynamicCheckoutEvent { val paymentMethod: PODynamicCheckoutPaymentMethod ) : PODynamicCheckoutEvent() + /** + * Event is sent after the payment method is tokenized. + */ + data class DidTokenizePaymentMethod( + val paymentMethod: PODynamicCheckoutPaymentMethod, + val customerTokenId: String + ) : PODynamicCheckoutEvent() + /** * Event is sent after payment was confirmed to be captured. This is a final event. */ diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index 3c241b2ea..d1625e916 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -742,6 +742,7 @@ internal class NativeAlternativePaymentInteractor( stateValue: NextStepStateValue, redirect: PONativeAlternativePaymentRedirect ) { + dispatch(WillStartRedirect(redirect)) when (redirect.type) { RedirectType.WEB -> webRedirect( stateValue = stateValue, diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/PONativeAlternativePaymentEvent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/PONativeAlternativePaymentEvent.kt index c29979a04..67cd58ad1 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/PONativeAlternativePaymentEvent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/PONativeAlternativePaymentEvent.kt @@ -1,6 +1,7 @@ package com.processout.sdk.ui.napm.delegate.v2 import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.annotation.ProcessOutInternalApi @@ -53,11 +54,22 @@ sealed class PONativeAlternativePaymentEvent { /** * Event is sent when parameters submission failed and the error is retriable, otherwise expect [DidFail] event. + * + * @param[failure] Failure details. */ data class DidFailToSubmitParameters( val failure: ProcessOutResult.Failure ) : PONativeAlternativePaymentEvent() + /** + * Event is sent just before redirecting to a web or a deep/app link. + * + * @param[redirect] Redirect details. + */ + data class WillStartRedirect( + val redirect: PONativeAlternativePaymentRedirect + ) : PONativeAlternativePaymentEvent() + /** * Event is sent when user asked to confirm cancellation, e.g. via dialog. */ @@ -82,6 +94,7 @@ sealed class PONativeAlternativePaymentEvent { /** * Event is sent when unretryable error occurs. This is a final event. * + * @param[failure] Failure details. * @param[paymentState] The payment state provides additional context about where in the payment process the failure occurred. * For example, in the event of a user-initiated cancellation, * this state can be used to determine which step the user was on when they canceled.