diff --git a/.editorconfig b/.editorconfig index 5aa737404a..95d358222f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -472,10 +472,11 @@ ij_groovy_wrap_long_lines = false # noinspection EditorConfigKeyCorrectness [{*.kt,*.kts}] -ktlint_standard_import-ordering = disabled -ktlint_standard_multiline-if-else = disabled +ktlint_code_style = android_studio ktlint_standard_trailing-comma-on-call-site = disabled ktlint_standard_trailing-comma-on-declaration-site = disabled +ktlint_standard_function-signature = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable max_line_length = 120 ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_multiline_binary_operation = false @@ -483,8 +484,8 @@ ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = false -ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true ij_kotlin_assignment_wrap = normal ij_kotlin_blank_lines_after_class_header = 0 ij_kotlin_blank_lines_around_block_when_branches = 0 diff --git a/README.md b/README.md index ce91bc2ff9..737d927f57 100644 --- a/README.md +++ b/README.md @@ -31,23 +31,23 @@ Import the corresponding module in your `build.gradle` file. For Drop-in: ```groovy -implementation "com.adyen.checkout:drop-in-compose:5.0.1" +implementation "com.adyen.checkout:drop-in-compose:5.1.0" ``` For the Credit Card component: ```groovy -implementation "com.adyen.checkout:card:5.0.1" -implementation "com.adyen.checkout:components-compose:5.0.1" +implementation "com.adyen.checkout:card:5.1.0" +implementation "com.adyen.checkout:components-compose:5.1.0" ``` ### Without Jetpack Compose For Drop-in: ```groovy -implementation "com.adyen.checkout:drop-in:5.0.1" +implementation "com.adyen.checkout:drop-in:5.1.0" ``` For the Credit Card component: ```groovy -implementation "com.adyen.checkout:card:5.0.1" +implementation "com.adyen.checkout:card:5.1.0" ``` The library is available on [Maven Central][mavenRepo]. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5ba0f46de0..102bce94d9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,21 +1,35 @@ [//]: # (This file will be used for the release notes on GitHub when publishing.) -[//]: # (Types of changes: `Added` `Changed` `Deprecated` `Removed` `Fixed` `Security`) +[//]: # (Types of changes: `Breaking changes` `New` `Added` `Changed` `Deprecated` `Removed` `Fixed`) [//]: # (Example:) [//]: # (## Added) [//]: # ( - New payment method) [//]: # (## Changed) [//]: # ( - DropIn service's package changed from `com.adyen.dropin` to `com.adyen.dropin.services`) -[//]: # ( # Deprecated) +[//]: # (## Deprecated) [//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object) +## New +- The [BcmcComponent](https://adyen.github.io/adyen-android/bcmc/com.adyen.checkout.bcmc/-bcmc-component/index.html) now supports co-badged Bancontact cards and card brand detection. + - The [BcmcComponentState](https://adyen.github.io/adyen-android/bcmc/com.adyen.checkout.bcmc/-bcmc-component-state/index.html) now contains 3 extra fields: `cardBrand`, `binValue` and `lastFourDigits`. +- You can now override payment method names in Drop-in by using [DropInConfiguration.Builder.overridePaymentMethodName(type, name)](https://adyen.github.io/adyen-android/drop-in/com.adyen.checkout.dropin/-drop-in-configuration/-builder/override-payment-method-name.html). +- For stored cards, Drop-in now shows the card name (for example **Visa** or **Mastercard**) instead of **Credit Card**. +- Now it is possible to show installment amounts for card payments using [InstallmentConfiguration.showInstallmentAmount](https://adyen.github.io/adyen-android/card/com.adyen.checkout.card/-installment-configuration/show-installment-amount.html) in [CardConfiguration.Builder.setInstallmentConfigurations()](https://adyen.github.io/adyen-android/card/com.adyen.checkout.card/-card-configuration/-builder/set-installment-configurations.html). +- For gift cards, you can now hide the PIN text field by setting [GiftCardConfiguration.Builder.setPinRequired()](https://adyen.github.io/adyen-android/giftcard/com.adyen.checkout.giftcard/-gift-card-configuration/-builder/set-pin-required.html) to **false**. +- For Google Pay: + - When initializing the [Google Pay button](https://docs.adyen.com/payment-methods/google-pay/android-component/#2-show-the-google-pay-button), you can now use [GooglePayComponent.getGooglePayButtonParameters()](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-google-pay-component/get-google-pay-button-parameters.html) to get the `allowedPaymentMethods` attribute. + - You can now use [AllowedAuthMethods](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-allowed-auth-methods/index.html) and [AllowedCardNetworks](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-allowed-card-networks/index.html) to easily access to the possible values for [GooglePayConfiguration.Builder.setAllowedAuthMethods()](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-google-pay-configuration/-builder/set-allowed-auth-methods.html) and [GooglePayConfiguration.Builder.setAllowedCardNetworks()](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-google-pay-configuration/-builder/set-allowed-card-networks.html). + ## Fixed -- `@RestrictTo` annotations no longer cause false errors with Android Studio and Lint. -- Using the layout inspector or having view attribute inspection enabled in the developer options no longer causes a crash when viewing a payment method. -- Implementing the `:action` module no longer gives a duplicate class error caused by a duplicate namespace. -- For Drop-in, dismissing the gift card payment method no longer prevents further interaction. +- Fixed a bug where components would not be displayed in Jetpack Compose lazy lists. ## Changed - Dependency versions: | Name | Version | |--------------------------------------------------------------------------------------------------------|-------------------------------| - | [AndroidX Compose BoM](https://developer.android.com/jetpack/compose/bom/bom-mapping) | **2023.09.01** | + | [AndroidX Compose Activity](https://developer.android.com/jetpack/androidx/releases/activity#1.8.0) | **1.8.0** | + | [Material Design](https://m2.material.io/) | **1.10.0** | + | [Gradle](https://docs.gradle.org/8.4/release-notes.html) | **8.4** | + | [Android Gradle plugin](https://developer.android.com/build/releases/gradle-plugin) | **8.1.2** | + | [AndroidX Compose BoM](https://developer.android.com/jetpack/compose/bom/bom-mapping) | **2023.10.01** | + | [AndroidX Recyclerview](https://developer.android.com/jetpack/androidx/releases/recyclerview#1.3.2) | **1.3.2** | + | [AndroidX Fragment](https://developer.android.com/jetpack/androidx/releases/fragment#1.6.2) | **1.6.2** | diff --git a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapperTest.kt b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapperTest.kt index 4bdd0f2bc9..c8889045fb 100644 --- a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapperTest.kt +++ b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapperTest.kt @@ -163,7 +163,7 @@ internal class ACHDirectDebitComponentParamsMapperTest { val sessionParams = SessionParams( enableStoreDetails = sessionsValue, - installmentOptions = null, + installmentConfiguration = null, amount = null, returnUrl = "", ) @@ -212,7 +212,7 @@ internal class ACHDirectDebitComponentParamsMapperTest { achConfiguration, sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", ) diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt index befec4b98f..c80da43eca 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt @@ -22,7 +22,11 @@ import com.adyen.checkout.voucher.VoucherConfiguration import com.adyen.checkout.wechatpay.WeChatPayActionConfiguration import java.util.Locale -@Suppress("UNCHECKED_CAST") +@Suppress( + "UNCHECKED_CAST", + "ktlint:standard:discouraged-comment-location", + "ktlint:standard:type-parameter-list-spacing", +) abstract class ActionHandlingPaymentMethodConfigurationBuilder< ConfigurationT : Configuration, BuilderT : BaseConfigurationBuilder diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt index e784956fdf..7f1a786332 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt @@ -1,106 +1,36 @@ /* - * Copyright (c) 2019 Adyen N.V. + * Copyright (c) 2023 Adyen N.V. * * This file is open source and available under the MIT license. See the LICENSE file for more info. * - * Created by arman on 18/9/2019. + * Created by ozgur on 22/8/2023. */ + package com.adyen.checkout.bcmc -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.adyen.checkout.action.core.internal.ActionHandlingComponent import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.bcmc.internal.provider.BcmcComponentProvider -import com.adyen.checkout.bcmc.internal.ui.BcmcDelegate -import com.adyen.checkout.card.CardBrand -import com.adyen.checkout.card.CardType +import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.card.internal.ui.CardDelegate import com.adyen.checkout.components.core.PaymentMethodTypes -import com.adyen.checkout.components.core.internal.ButtonComponent import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponent -import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.components.core.internal.toActionCallback -import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger -import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate -import com.adyen.checkout.ui.core.internal.ui.ComponentViewType -import com.adyen.checkout.ui.core.internal.ui.ViewableComponent -import com.adyen.checkout.ui.core.internal.util.mergeViewFlows -import kotlinx.coroutines.flow.Flow /** * A [PaymentComponent] that supports the [PaymentMethodTypes.BCMC] payment method. */ -class BcmcComponent internal constructor( - private val bcmcDelegate: BcmcDelegate, - private val genericActionDelegate: GenericActionDelegate, - private val actionHandlingComponent: DefaultActionHandlingComponent, +class BcmcComponent( + cardDelegate: CardDelegate, + genericActionDelegate: GenericActionDelegate, + actionHandlingComponent: DefaultActionHandlingComponent, internal val componentEventHandler: ComponentEventHandler, -) : ViewModel(), - PaymentComponent, - ViewableComponent, - ButtonComponent, - ActionHandlingComponent by actionHandlingComponent { - - override val delegate: ComponentDelegate get() = actionHandlingComponent.activeDelegate - - override val viewFlow: Flow = mergeViewFlows( - viewModelScope, - bcmcDelegate.viewFlow, - genericActionDelegate.viewFlow, - ) - - init { - bcmcDelegate.initialize(viewModelScope) - genericActionDelegate.initialize(viewModelScope) - componentEventHandler.initialize(viewModelScope) - } - - internal fun observe( - lifecycleOwner: LifecycleOwner, - callback: (PaymentComponentEvent) -> Unit - ) { - bcmcDelegate.observe(lifecycleOwner, viewModelScope, callback) - genericActionDelegate.observe(lifecycleOwner, viewModelScope, callback.toActionCallback()) - } - - internal fun removeObserver() { - bcmcDelegate.removeObserver() - genericActionDelegate.removeObserver() - } - - override fun isConfirmationRequired(): Boolean = bcmcDelegate.isConfirmationRequired() - - override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") - } - - override fun setInteractionBlocked(isInteractionBlocked: Boolean) { - (delegate as? BcmcDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") - } - - override fun onCleared() { - super.onCleared() - Logger.d(TAG, "onCleared") - bcmcDelegate.onCleared() - genericActionDelegate.onCleared() - componentEventHandler.onCleared() - } - +) : CardComponent(cardDelegate, genericActionDelegate, actionHandlingComponent, componentEventHandler) { companion object { - private val TAG = LogUtil.getTag() - @JvmField val PROVIDER = BcmcComponentProvider() @JvmField val PAYMENT_METHOD_TYPES = listOf(PaymentMethodTypes.BCMC) - - internal val SUPPORTED_CARD_TYPE = CardBrand(cardType = CardType.BCMC) } } diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponentState.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponentState.kt index 53e71d76ba..1cfdb7d0d6 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponentState.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponentState.kt @@ -3,20 +3,11 @@ * * This file is open source and available under the MIT license. See the LICENSE file for more info. * - * Created by ozgur on 20/2/2023. + * Created by ozgur on 27/9/2023. */ package com.adyen.checkout.bcmc -import com.adyen.checkout.components.core.PaymentComponentData -import com.adyen.checkout.components.core.PaymentComponentState -import com.adyen.checkout.components.core.paymentmethod.CardPaymentMethod +import com.adyen.checkout.card.CardComponentState -/** - * Represents the state of [BcmcComponent]. - */ -data class BcmcComponentState( - override val data: PaymentComponentData, - override val isInputValid: Boolean, - override val isReady: Boolean -) : PaymentComponentState +typealias BcmcComponentState = CardComponentState diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/provider/BcmcComponentProvider.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/provider/BcmcComponentProvider.kt index 8badfe4aa2..b651e49510 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/provider/BcmcComponentProvider.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/provider/BcmcComponentProvider.kt @@ -1,9 +1,9 @@ /* - * Copyright (c) 2019 Adyen N.V. + * Copyright (c) 2023 Adyen N.V. * * This file is open source and available under the MIT license. See the LICENSE file for more info. * - * Created by arman on 18/9/2019. + * Created by ozgur on 22/8/2023. */ package com.adyen.checkout.bcmc.internal.provider @@ -19,9 +19,11 @@ import com.adyen.checkout.action.core.internal.provider.GenericActionComponentPr import com.adyen.checkout.bcmc.BcmcComponent import com.adyen.checkout.bcmc.BcmcComponentState import com.adyen.checkout.bcmc.BcmcConfiguration -import com.adyen.checkout.bcmc.internal.ui.DefaultBcmcDelegate import com.adyen.checkout.bcmc.internal.ui.model.BcmcComponentParamsMapper +import com.adyen.checkout.card.internal.data.api.BinLookupService +import com.adyen.checkout.card.internal.data.api.DefaultDetectCardTypeRepository import com.adyen.checkout.card.internal.ui.CardValidationMapper +import com.adyen.checkout.card.internal.ui.DefaultCardDelegate import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -54,6 +56,8 @@ import com.adyen.checkout.sessions.core.internal.data.api.SessionRepository import com.adyen.checkout.sessions.core.internal.data.api.SessionService import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponentProvider import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory +import com.adyen.checkout.ui.core.internal.data.api.AddressService +import com.adyen.checkout.ui.core.internal.data.api.DefaultAddressRepository import com.adyen.checkout.ui.core.internal.ui.SubmitHandler class BcmcComponentProvider @@ -78,6 +82,7 @@ constructor( private val componentParamsMapper = BcmcComponentParamsMapper(overrideComponentParams, overrideSessionParams) + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, @@ -90,39 +95,45 @@ constructor( key: String?, ): BcmcComponent { assertSupported(paymentMethod) + val bcmcFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> + val componentParams = componentParamsMapper.mapToParams(configuration, null, paymentMethod) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) + val publicKeyService = PublicKeyService(httpClient) + val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) + val cardValidationMapper = CardValidationMapper() + val dateGenerator = DateGenerator() + val clientSideEncrypter = ClientSideEncrypter() + val genericEncrypter = DefaultGenericEncrypter(clientSideEncrypter, dateGenerator) + val cardEncrypter = DefaultCardEncrypter(genericEncrypter) + val addressService = AddressService(httpClient) + val addressRepository = DefaultAddressRepository(addressService) + val binLookupService = BinLookupService(httpClient) + val detectCardTypeRepository = DefaultDetectCardTypeRepository(cardEncrypter, binLookupService) - val componentParams = componentParamsMapper.mapToParams(configuration, null) - val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) - val publicKeyService = PublicKeyService(httpClient) - val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) - val cardValidationMapper = CardValidationMapper() - val dateGenerator = DateGenerator() - val clientSideEncrypter = ClientSideEncrypter() - val genericEncrypter = DefaultGenericEncrypter(clientSideEncrypter, dateGenerator) - val cardEncrypter = DefaultCardEncrypter(genericEncrypter) - - val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( - analyticsRepositoryData = AnalyticsRepositoryData( - application = application, - componentParams = componentParams, - paymentMethod = paymentMethod, - ), - analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) - ), - analyticsMapper = AnalyticsMapper(), - ) + val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( + analyticsRepositoryData = AnalyticsRepositoryData( + application = application, + componentParams = componentParams, + paymentMethod = paymentMethod, + ), + analyticsService = AnalyticsService( + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + ), + analyticsMapper = AnalyticsMapper(), + ) - val bcmcFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val bcmcDelegate = DefaultBcmcDelegate( + val cardDelegate = DefaultCardDelegate( observerRepository = PaymentObserverRepository(), - paymentMethod = paymentMethod, - order = order, publicKeyRepository = publicKeyRepository, componentParams = componentParams, + paymentMethod = paymentMethod, + order = order, + analyticsRepository = analyticsRepository, + addressRepository = addressRepository, + detectCardTypeRepository = detectCardTypeRepository, cardValidationMapper = cardValidationMapper, cardEncrypter = cardEncrypter, - analyticsRepository = analyticsRepository, + genericEncrypter = genericEncrypter, submitHandler = SubmitHandler(savedStateHandle) ) @@ -133,13 +144,16 @@ constructor( ) BcmcComponent( - bcmcDelegate = bcmcDelegate, + cardDelegate = cardDelegate, genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, bcmcDelegate), + actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cardDelegate), componentEventHandler = DefaultComponentEventHandler(), ) } - return ViewModelProvider(viewModelStoreOwner, bcmcFactory)[key, BcmcComponent::class.java].also { component -> + return ViewModelProvider( + viewModelStoreOwner, + bcmcFactory + )[key, BcmcComponent::class.java].also { component -> component.observe(lifecycleOwner) { component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) } @@ -159,43 +173,50 @@ constructor( key: String? ): BcmcComponent { assertSupported(paymentMethod) + val bcmcFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> + val componentParams = componentParamsMapper.mapToParams( + bcmcConfiguration = configuration, + sessionParams = SessionParamsFactory.create(checkoutSession), + paymentMethod = paymentMethod + ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) + val publicKeyService = PublicKeyService(httpClient) + val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) + val cardValidationMapper = CardValidationMapper() + val dateGenerator = DateGenerator() + val clientSideEncrypter = ClientSideEncrypter() + val genericEncrypter = DefaultGenericEncrypter(clientSideEncrypter, dateGenerator) + val cardEncrypter = DefaultCardEncrypter(genericEncrypter) + val addressService = AddressService(httpClient) + val addressRepository = DefaultAddressRepository(addressService) + val binLookupService = BinLookupService(httpClient) + val detectCardTypeRepository = DefaultDetectCardTypeRepository(cardEncrypter, binLookupService) - val componentParams = componentParamsMapper.mapToParams( - bcmcConfiguration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession) - ) - val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) - val publicKeyService = PublicKeyService(httpClient) - val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) - val cardValidationMapper = CardValidationMapper() - val dateGenerator = DateGenerator() - val clientSideEncrypter = ClientSideEncrypter() - val genericEncrypter = DefaultGenericEncrypter(clientSideEncrypter, dateGenerator) - val cardEncrypter = DefaultCardEncrypter(genericEncrypter) - - val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( - analyticsRepositoryData = AnalyticsRepositoryData( - application = application, - componentParams = componentParams, - paymentMethod = paymentMethod, - sessionId = checkoutSession.sessionSetupResponse.id, - ), - analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) - ), - analyticsMapper = AnalyticsMapper(), - ) + val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( + analyticsRepositoryData = AnalyticsRepositoryData( + application = application, + componentParams = componentParams, + paymentMethod = paymentMethod, + sessionId = checkoutSession.sessionSetupResponse.id, + ), + analyticsService = AnalyticsService( + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + ), + analyticsMapper = AnalyticsMapper(), + ) - val bcmcFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val bcmcDelegate = DefaultBcmcDelegate( + val cardDelegate = DefaultCardDelegate( observerRepository = PaymentObserverRepository(), - paymentMethod = paymentMethod, - order = checkoutSession.order, publicKeyRepository = publicKeyRepository, componentParams = componentParams, + paymentMethod = paymentMethod, + order = checkoutSession.order, + analyticsRepository = analyticsRepository, + addressRepository = addressRepository, + detectCardTypeRepository = detectCardTypeRepository, cardValidationMapper = cardValidationMapper, cardEncrypter = cardEncrypter, - analyticsRepository = analyticsRepository, + genericEncrypter = genericEncrypter, submitHandler = SubmitHandler(savedStateHandle) ) @@ -225,13 +246,17 @@ constructor( ) BcmcComponent( - bcmcDelegate = bcmcDelegate, + cardDelegate = cardDelegate, genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, bcmcDelegate), + actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cardDelegate), componentEventHandler = sessionComponentEventHandler, ) } - return ViewModelProvider(viewModelStoreOwner, bcmcFactory)[key, BcmcComponent::class.java].also { component -> + + return ViewModelProvider( + viewModelStoreOwner, + bcmcFactory + )[key, BcmcComponent::class.java].also { component -> component.observe(lifecycleOwner) { component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) } diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/BcmcDelegate.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/BcmcDelegate.kt deleted file mode 100644 index 00cb78d269..0000000000 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/BcmcDelegate.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2022 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by oscars on 8/7/2022. - */ - -package com.adyen.checkout.bcmc.internal.ui - -import com.adyen.checkout.bcmc.BcmcComponentState -import com.adyen.checkout.bcmc.internal.ui.model.BcmcComponentParams -import com.adyen.checkout.bcmc.internal.ui.model.BcmcInputData -import com.adyen.checkout.bcmc.internal.ui.model.BcmcOutputData -import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate -import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate -import com.adyen.checkout.ui.core.internal.ui.UIStateDelegate -import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate -import kotlinx.coroutines.flow.Flow - -internal interface BcmcDelegate : - PaymentComponentDelegate, - ViewProvidingDelegate, - ButtonDelegate, - UIStateDelegate { - - override val componentParams: BcmcComponentParams - - val outputData: BcmcOutputData - - val outputDataFlow: Flow - - val componentStateFlow: Flow - - val exceptionFlow: Flow - - fun isCardNumberSupported(cardNumber: String?): Boolean - - fun updateInputData(update: BcmcInputData.() -> Unit) - - fun setInteractionBlocked(isInteractionBlocked: Boolean) -} diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/BcmcViewProvider.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/BcmcViewProvider.kt index ad42c0230f..ed7b663145 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/BcmcViewProvider.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/BcmcViewProvider.kt @@ -9,7 +9,7 @@ package com.adyen.checkout.bcmc.internal.ui import android.content.Context -import com.adyen.checkout.bcmc.internal.ui.view.BcmcView +import com.adyen.checkout.card.internal.ui.view.CardView import com.adyen.checkout.ui.core.internal.ui.AmountButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ComponentView @@ -22,7 +22,7 @@ internal object BcmcViewProvider : ViewProvider { viewType: ComponentViewType, context: Context, ): ComponentView = when (viewType) { - BcmcComponentViewType -> BcmcView(context) + BcmcComponentViewType -> CardView(context) else -> throw IllegalArgumentException("Unsupported view type") } } diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegate.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegate.kt deleted file mode 100644 index 36d3877425..0000000000 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegate.kt +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright (c) 2022 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by oscars on 8/7/2022. - */ - -package com.adyen.checkout.bcmc.internal.ui - -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.LifecycleOwner -import com.adyen.checkout.bcmc.BcmcComponent -import com.adyen.checkout.bcmc.BcmcComponentState -import com.adyen.checkout.bcmc.internal.ui.model.BcmcComponentParams -import com.adyen.checkout.bcmc.internal.ui.model.BcmcInputData -import com.adyen.checkout.bcmc.internal.ui.model.BcmcOutputData -import com.adyen.checkout.card.CardBrand -import com.adyen.checkout.card.R -import com.adyen.checkout.card.internal.data.model.Brand -import com.adyen.checkout.card.internal.ui.CardValidationMapper -import com.adyen.checkout.card.internal.ui.model.ExpiryDate -import com.adyen.checkout.card.internal.util.CardValidationUtils -import com.adyen.checkout.components.core.Order -import com.adyen.checkout.components.core.PaymentComponentData -import com.adyen.checkout.components.core.PaymentMethod -import com.adyen.checkout.components.core.PaymentMethodTypes -import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.components.core.internal.PaymentObserverRepository -import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.data.api.PublicKeyRepository -import com.adyen.checkout.components.core.internal.ui.model.FieldState -import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.components.core.internal.util.bufferedChannel -import com.adyen.checkout.components.core.paymentmethod.CardPaymentMethod -import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger -import com.adyen.checkout.core.internal.util.runCompileOnly -import com.adyen.checkout.cse.EncryptedCard -import com.adyen.checkout.cse.EncryptionException -import com.adyen.checkout.cse.UnencryptedCard -import com.adyen.checkout.cse.internal.BaseCardEncrypter -import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType -import com.adyen.checkout.ui.core.internal.ui.ComponentViewType -import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIEvent -import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIState -import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -import com.adyen.threeds2.ThreeDS2Service -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -@Suppress("TooManyFunctions", "LongParameterList") -internal class DefaultBcmcDelegate( - private val observerRepository: PaymentObserverRepository, - private val paymentMethod: PaymentMethod, - private val order: Order?, - private val analyticsRepository: AnalyticsRepository, - private val publicKeyRepository: PublicKeyRepository, - override val componentParams: BcmcComponentParams, - private val cardValidationMapper: CardValidationMapper, - private val cardEncrypter: BaseCardEncrypter, - private val submitHandler: SubmitHandler -) : BcmcDelegate { - - private val inputData = BcmcInputData() - - private val _outputDataFlow = MutableStateFlow(createOutputData()) - override val outputDataFlow: Flow = _outputDataFlow - - private val _componentStateFlow = MutableStateFlow(createComponentState()) - override val componentStateFlow: Flow = _componentStateFlow - - private val exceptionChannel: Channel = bufferedChannel() - override val exceptionFlow: Flow = exceptionChannel.receiveAsFlow() - - override val outputData get() = _outputDataFlow.value - - private val _viewFlow: MutableStateFlow = MutableStateFlow(BcmcComponentViewType) - override val viewFlow: Flow = _viewFlow - - override val submitFlow: Flow = submitHandler.submitFlow - - override val uiStateFlow: Flow = submitHandler.uiStateFlow - - override val uiEventFlow: Flow = submitHandler.uiEventFlow - - private var publicKey: String? = null - - override fun initialize(coroutineScope: CoroutineScope) { - submitHandler.initialize(coroutineScope, componentStateFlow) - setupAnalytics(coroutineScope) - fetchPublicKey(coroutineScope) - } - - private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") - coroutineScope.launch { - analyticsRepository.setupAnalytics() - } - } - - private fun fetchPublicKey(coroutineScope: CoroutineScope) { - Logger.d(TAG, "fetchPublicKey") - coroutineScope.launch { - publicKeyRepository.fetchPublicKey( - environment = componentParams.environment, - clientKey = componentParams.clientKey - ).fold( - onSuccess = { key -> - Logger.d(TAG, "Public key fetched") - publicKey = key - updateComponentState(outputData) - }, - onFailure = { e -> - Logger.e(TAG, "Unable to fetch public key") - exceptionChannel.trySend(ComponentException("Unable to fetch publicKey.", e)) - } - ) - } - } - - override fun observe( - lifecycleOwner: LifecycleOwner, - coroutineScope: CoroutineScope, - callback: (PaymentComponentEvent) -> Unit - ) { - observerRepository.addObservers( - stateFlow = componentStateFlow, - exceptionFlow = exceptionFlow, - submitFlow = submitFlow, - lifecycleOwner = lifecycleOwner, - coroutineScope = coroutineScope, - callback = callback - ) - } - - override fun removeObserver() { - observerRepository.removeObservers() - } - - override fun updateInputData(update: BcmcInputData.() -> Unit) { - inputData.update() - onInputDataChanged() - } - - private fun onInputDataChanged() { - val outputData = createOutputData() - - _outputDataFlow.tryEmit(outputData) - - updateComponentState(outputData) - } - - private fun createOutputData() = BcmcOutputData( - cardNumberField = validateCardNumber(inputData.cardNumber), - expiryDateField = CardValidationUtils.validateExpiryDate(inputData.expiryDate, Brand.FieldPolicy.REQUIRED), - cardHolderNameField = validateHolderName(inputData.cardHolderName), - shouldStorePaymentMethod = inputData.isStorePaymentMethodSwitchChecked, - showStorePaymentField = showStorePaymentField(), - ) - - private fun validateCardNumber(cardNumber: String): FieldState { - val validation = - CardValidationUtils.validateCardNumber(cardNumber, enableLuhnCheck = true, isBrandSupported = true) - return cardValidationMapper.mapCardNumberValidation(cardNumber, validation) - } - - private fun validateHolderName(holderName: String): FieldState { - return if (componentParams.isHolderNameRequired && holderName.isBlank()) { - FieldState( - holderName, - Validation.Invalid(R.string.checkout_holder_name_not_valid) - ) - } else { - FieldState( - holderName, - Validation.Valid - ) - } - } - - private fun showStorePaymentField(): Boolean { - return componentParams.isStorePaymentFieldVisible - } - - @VisibleForTesting - internal fun updateComponentState(outputData: BcmcOutputData) { - Logger.v(TAG, "updateComponentState") - val componentState = createComponentState(outputData) - _componentStateFlow.tryEmit(componentState) - } - - @Suppress("ReturnCount") - private fun createComponentState( - outputData: BcmcOutputData = this.outputData - ): BcmcComponentState { - val publicKey = publicKey - - // If data is not valid we just return empty object, encryption would fail and we don't pass unencrypted data. - if (!outputData.isValid || publicKey == null) { - return BcmcComponentState( - data = PaymentComponentData(null, null, null), - isInputValid = outputData.isValid, - isReady = publicKey != null, - ) - } - - val encryptedCard = encryptCardData(outputData, publicKey) ?: return BcmcComponentState( - data = PaymentComponentData(null, null, null), - isInputValid = false, - isReady = true, - ) - - // BCMC payment method is scheme type. - val cardPaymentMethod = CardPaymentMethod( - type = CardPaymentMethod.PAYMENT_METHOD_TYPE, - checkoutAttemptId = analyticsRepository.getCheckoutAttemptId(), - encryptedCardNumber = encryptedCard.encryptedCardNumber, - encryptedExpiryMonth = encryptedCard.encryptedExpiryMonth, - encryptedExpiryYear = encryptedCard.encryptedExpiryYear, - threeDS2SdkVersion = runCompileOnly { ThreeDS2Service.INSTANCE.sdkVersion }, - brand = PaymentMethodTypes.BCMC - ).apply { - if (componentParams.isHolderNameRequired) { - holderName = outputData.cardHolderNameField.value - } - } - - val paymentComponentData = PaymentComponentData( - order = order, - paymentMethod = cardPaymentMethod, - storePaymentMethod = if (showStorePaymentField()) outputData.shouldStorePaymentMethod else null, - shopperReference = componentParams.shopperReference, - amount = componentParams.amount, - ) - - return BcmcComponentState(paymentComponentData, isInputValid = true, isReady = true) - } - - override fun onSubmit() { - val state = _componentStateFlow.value - submitHandler.onSubmit(state) - } - - private fun encryptCardData( - outputData: BcmcOutputData, - publicKey: String, - ): EncryptedCard? = try { - val unencryptedCardBuilder = UnencryptedCard.Builder() - .setNumber(outputData.cardNumberField.value) - - val expiryDateResult = outputData.expiryDateField.value - if (expiryDateResult != ExpiryDate.EMPTY_DATE) { - unencryptedCardBuilder.setExpiryDate( - expiryMonth = expiryDateResult.expiryMonth.toString(), - expiryYear = expiryDateResult.expiryYear.toString() - ) - } - - cardEncrypter.encryptFields(unencryptedCardBuilder.build(), publicKey) - } catch (e: EncryptionException) { - exceptionChannel.trySend(e) - null - } - - override fun getPaymentMethodType(): String { - return paymentMethod.type ?: PaymentMethodTypes.UNKNOWN - } - - override fun isCardNumberSupported(cardNumber: String?): Boolean { - if (cardNumber.isNullOrEmpty()) return false - return CardBrand.estimate(cardNumber).contains(BcmcComponent.SUPPORTED_CARD_TYPE) - } - - override fun isConfirmationRequired(): Boolean = _viewFlow.value is ButtonComponentViewType - - override fun shouldShowSubmitButton(): Boolean = isConfirmationRequired() && componentParams.isSubmitButtonVisible - - override fun setInteractionBlocked(isInteractionBlocked: Boolean) { - submitHandler.setInteractionBlocked(isInteractionBlocked) - } - - override fun onCleared() { - removeObserver() - } - - companion object { - private val TAG = LogUtil.getTag() - } -} diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapper.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapper.kt index 440c775bb6..71b9c516a7 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapper.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapper.kt @@ -1,17 +1,26 @@ /* - * Copyright (c) 2022 Adyen N.V. + * Copyright (c) 2023 Adyen N.V. * * This file is open source and available under the MIT license. See the LICENSE file for more info. * - * Created by josephj on 17/11/2022. + * Created by ozgur on 22/8/2023. */ package com.adyen.checkout.bcmc.internal.ui.model import com.adyen.checkout.bcmc.BcmcConfiguration +import com.adyen.checkout.card.CardBrand +import com.adyen.checkout.card.CardType +import com.adyen.checkout.card.KCPAuthVisibility +import com.adyen.checkout.card.SocialSecurityNumberVisibility +import com.adyen.checkout.card.internal.ui.model.CVCVisibility +import com.adyen.checkout.card.internal.ui.model.CardComponentParams +import com.adyen.checkout.card.internal.ui.model.StoredCVCVisibility +import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.ComponentParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.ui.core.internal.ui.model.AddressParams internal class BcmcComponentParamsMapper( private val overrideComponentParams: ComponentParams?, @@ -21,15 +30,18 @@ internal class BcmcComponentParamsMapper( fun mapToParams( bcmcConfiguration: BcmcConfiguration, sessionParams: SessionParams?, - ): BcmcComponentParams { + paymentMethod: PaymentMethod + ): CardComponentParams { return bcmcConfiguration - .mapToParamsInternal() + .mapToParamsInternal( + supportedCardBrands = paymentMethod.brands?.map { CardBrand(it) } + ) .override(overrideComponentParams) .override(sessionParams ?: overrideSessionParams) } - private fun BcmcConfiguration.mapToParamsInternal(): BcmcComponentParams { - return BcmcComponentParams( + private fun BcmcConfiguration.mapToParamsInternal(supportedCardBrands: List?): CardComponentParams { + return CardComponentParams( shopperLocale = shopperLocale, environment = environment, clientKey = clientKey, @@ -40,12 +52,19 @@ internal class BcmcComponentParamsMapper( isHolderNameRequired = isHolderNameRequired ?: false, shopperReference = shopperReference, isStorePaymentFieldVisible = isStorePaymentFieldVisible ?: false, + addressParams = AddressParams.None, + installmentParams = null, + kcpAuthVisibility = KCPAuthVisibility.HIDE, + socialSecurityNumberVisibility = SocialSecurityNumberVisibility.HIDE, + cvcVisibility = CVCVisibility.HIDE_FIRST, + storedCVCVisibility = StoredCVCVisibility.HIDE, + supportedCardBrands = supportedCardBrands ?: DEFAULT_SUPPORTED_CARD_BRANDS ) } - private fun BcmcComponentParams.override( + private fun CardComponentParams.override( overrideComponentParams: ComponentParams? - ): BcmcComponentParams { + ): CardComponentParams { if (overrideComponentParams == null) return this return copy( shopperLocale = overrideComponentParams.shopperLocale, @@ -57,13 +76,21 @@ internal class BcmcComponentParamsMapper( ) } - private fun BcmcComponentParams.override( + private fun CardComponentParams.override( sessionParams: SessionParams? = null - ): BcmcComponentParams { + ): CardComponentParams { if (sessionParams == null) return this return copy( isStorePaymentFieldVisible = sessionParams.enableStoreDetails ?: isStorePaymentFieldVisible, amount = sessionParams.amount ?: amount, ) } + + companion object { + private val DEFAULT_SUPPORTED_CARD_BRANDS = listOf( + CardBrand(cardType = CardType.BCMC), + CardBrand(cardType = CardType.MAESTRO), + CardBrand(cardType = CardType.VISA) + ) + } } diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcInputData.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcInputData.kt deleted file mode 100644 index d8768ef55c..0000000000 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcInputData.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2019 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by caiof on 27/8/2020. - */ -package com.adyen.checkout.bcmc.internal.ui.model - -import com.adyen.checkout.card.internal.ui.model.ExpiryDate -import com.adyen.checkout.components.core.internal.ui.model.InputData - -internal data class BcmcInputData( - var cardNumber: String = "", - var expiryDate: ExpiryDate = ExpiryDate.EMPTY_DATE, - var cardHolderName: String = "", - var isStorePaymentMethodSwitchChecked: Boolean = false, -) : InputData diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcOutputData.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcOutputData.kt deleted file mode 100644 index a8dcc44ba5..0000000000 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcOutputData.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2023 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by oscars on 15/2/2023. - */ -package com.adyen.checkout.bcmc.internal.ui.model - -import com.adyen.checkout.card.internal.ui.model.ExpiryDate -import com.adyen.checkout.components.core.internal.ui.model.FieldState -import com.adyen.checkout.components.core.internal.ui.model.OutputData - -internal data class BcmcOutputData internal constructor( - val cardNumberField: FieldState, - val expiryDateField: FieldState, - val cardHolderNameField: FieldState, - val showStorePaymentField: Boolean, - val shouldStorePaymentMethod: Boolean, -) : OutputData { - override val isValid: Boolean - get() = ( - cardNumberField.validation.isValid() && - expiryDateField.validation.isValid() && - cardHolderNameField.validation.isValid() - ) -} diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/view/BcmcView.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/view/BcmcView.kt deleted file mode 100644 index 160659964c..0000000000 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/view/BcmcView.kt +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2022 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by oscars on 29/9/2022. - */ - -package com.adyen.checkout.bcmc.internal.ui.view - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.view.View.OnFocusChangeListener -import android.widget.CompoundButton -import android.widget.LinearLayout -import androidx.annotation.StringRes -import androidx.core.view.isVisible -import com.adyen.checkout.bcmc.R -import com.adyen.checkout.bcmc.databinding.BcmcViewBinding -import com.adyen.checkout.bcmc.internal.ui.BcmcDelegate -import com.adyen.checkout.bcmc.internal.ui.model.BcmcOutputData -import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.ui.core.internal.ui.ComponentView -import com.adyen.checkout.ui.core.internal.util.hideError -import com.adyen.checkout.ui.core.internal.util.isVisible -import com.adyen.checkout.ui.core.internal.util.setLocalizedHintFromStyle -import com.adyen.checkout.ui.core.internal.util.setLocalizedTextFromStyle -import com.adyen.checkout.ui.core.internal.util.showError -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -@Suppress("TooManyFunctions") -internal class BcmcView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr), - ComponentView { - - private val binding = BcmcViewBinding.inflate(LayoutInflater.from(context), this) - - private lateinit var localizedContext: Context - - private lateinit var delegate: BcmcDelegate - - init { - orientation = VERTICAL - val padding = resources.getDimension(R.dimen.standard_margin).toInt() - setPadding(padding, padding, padding, 0) - } - - override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) { - require(delegate is BcmcDelegate) { "Unsupported delegate type" } - this.delegate = delegate - - this.localizedContext = localizedContext - initLocalizedStrings(localizedContext) - - observeDelegate(delegate, coroutineScope) - - initCardNumberInput() - initExpiryDateInput() - initCardHolderInput() - initStorePaymentMethodSwitch() - } - - private fun initLocalizedStrings(localizedContext: Context) { - with(binding) { - textInputLayoutCardNumber.setLocalizedHintFromStyle( - R.style.AdyenCheckout_Card_CardNumberInput, - localizedContext - ) - textInputLayoutExpiryDate.setLocalizedHintFromStyle( - R.style.AdyenCheckout_Card_ExpiryDateInput, - localizedContext - ) - binding.textInputLayoutCardHolder.setLocalizedHintFromStyle( - R.style.AdyenCheckout_Card_HolderNameInput, - localizedContext - ) - switchStorePaymentMethod.setLocalizedTextFromStyle( - R.style.AdyenCheckout_Card_StorePaymentSwitch, - localizedContext - ) - } - } - - private fun observeDelegate(delegate: BcmcDelegate, coroutineScope: CoroutineScope) { - delegate.outputDataFlow - .onEach { outputDataChanged(it) } - .launchIn(coroutineScope) - } - - private fun outputDataChanged(bcmcOutputData: BcmcOutputData) { - setStorePaymentSwitchVisibility(bcmcOutputData.showStorePaymentField) - } - - private fun setStorePaymentSwitchVisibility(showStorePaymentField: Boolean) { - binding.switchStorePaymentMethod.isVisible = showStorePaymentField - } - - private fun initExpiryDateInput() { - binding.editTextExpiryDate.setOnChangeListener { - delegate.updateInputData { expiryDate = binding.editTextExpiryDate.date } - binding.textInputLayoutExpiryDate.hideError() - } - - binding.editTextExpiryDate.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean -> - val expiryDateValidation = delegate.outputData.expiryDateField.validation - if (hasFocus) { - binding.textInputLayoutExpiryDate.hideError() - } else if (expiryDateValidation is Validation.Invalid) { - val errorReasonResId = expiryDateValidation.reason - binding.textInputLayoutExpiryDate.showError(localizedContext.getString(errorReasonResId)) - } - } - } - - private fun initCardNumberInput() { - binding.editTextCardNumber.setOnChangeListener { - delegate.updateInputData { cardNumber = binding.editTextCardNumber.rawValue } - setCardNumberError(null) - } - - binding.editTextCardNumber.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean -> - val cardNumberValidation = delegate.outputData.cardNumberField.validation - if (hasFocus) { - setCardNumberError(null) - } else if (cardNumberValidation is Validation.Invalid) { - val errorReasonResId = cardNumberValidation.reason - setCardNumberError(errorReasonResId) - } - } - } - - private fun initCardHolderInput() { - binding.textInputLayoutCardHolder.isVisible = delegate.componentParams.isHolderNameRequired - binding.editTextCardHolder.setOnChangeListener { - delegate.updateInputData { cardHolderName = binding.editTextCardHolder.rawValue } - binding.textInputLayoutCardHolder.hideError() - } - - binding.editTextCardHolder.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> - val cardHolderValidation = delegate.outputData.cardHolderNameField.validation - if (hasFocus) { - binding.textInputLayoutCardHolder.hideError() - } else if (cardHolderValidation is Validation.Invalid) { - val errorReasonResId = cardHolderValidation.reason - binding.textInputLayoutCardHolder.showError(localizedContext.getString(errorReasonResId)) - } - } - } - - private fun initStorePaymentMethodSwitch() { - binding.switchStorePaymentMethod.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> - delegate.updateInputData { isStorePaymentMethodSwitchChecked = isChecked } - } - } - - override fun highlightValidationErrors() { - val outputData = delegate.outputData - - var isErrorFocused = false - val cardNumberValidation = outputData.cardNumberField.validation - if (cardNumberValidation is Validation.Invalid) { - isErrorFocused = true - binding.editTextCardNumber.requestFocus() - val errorReasonResId = cardNumberValidation.reason - setCardNumberError(errorReasonResId) - } - - val expiryFieldValidation = outputData.expiryDateField.validation - if (expiryFieldValidation is Validation.Invalid) { - if (!isErrorFocused) { - binding.textInputLayoutExpiryDate.requestFocus() - } - val errorReasonResId = expiryFieldValidation.reason - binding.textInputLayoutExpiryDate.showError(localizedContext.getString(errorReasonResId)) - } - - val cardHolderNameValidation = outputData.cardHolderNameField.validation - if (cardHolderNameValidation is Validation.Invalid) { - if (!isErrorFocused) { - binding.textInputLayoutCardHolder.requestFocus() - } - val errorReasonResId = cardHolderNameValidation.reason - binding.textInputLayoutCardHolder.showError(localizedContext.getString(errorReasonResId)) - } - } - - private fun setCardNumberError(@StringRes stringResId: Int?) { - if (stringResId == null) { - binding.textInputLayoutCardNumber.hideError() - binding.cardBrandLogoImageView.isVisible = true - } else { - binding.textInputLayoutCardNumber.showError(localizedContext.getString(stringResId)) - binding.cardBrandLogoImageView.isVisible = false - } - } - - override fun getView(): View = this -} diff --git a/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt b/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt index 92cacce9c3..e28059bd49 100644 --- a/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt +++ b/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt @@ -14,7 +14,8 @@ import app.cash.turbine.test import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.bcmc.internal.ui.BcmcComponentViewType -import com.adyen.checkout.bcmc.internal.ui.BcmcDelegate +import com.adyen.checkout.card.CardComponentState +import com.adyen.checkout.card.internal.ui.CardDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.core.AdyenLogger @@ -42,7 +43,7 @@ import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) internal class BcmcComponentTest( - @Mock private val bcmcDelegate: BcmcDelegate, + @Mock private val cardDelegate: CardDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @Mock private val actionHandlingComponent: DefaultActionHandlingComponent, @Mock private val componentEventHandler: ComponentEventHandler, @@ -52,11 +53,11 @@ internal class BcmcComponentTest( @BeforeEach fun before() { - whenever(bcmcDelegate.viewFlow) doReturn MutableStateFlow(BcmcComponentViewType) + whenever(cardDelegate.viewFlow) doReturn MutableStateFlow(BcmcComponentViewType) whenever(genericActionDelegate.viewFlow) doReturn MutableStateFlow(null) component = BcmcComponent( - bcmcDelegate, + cardDelegate, genericActionDelegate, actionHandlingComponent, componentEventHandler, @@ -66,7 +67,7 @@ internal class BcmcComponentTest( @Test fun `when component is created then delegates are initialized`() { - verify(bcmcDelegate).initialize(component.viewModelScope) + verify(cardDelegate).initialize(component.viewModelScope) verify(genericActionDelegate).initialize(component.viewModelScope) verify(componentEventHandler).initialize(component.viewModelScope) } @@ -75,7 +76,7 @@ internal class BcmcComponentTest( fun `when component is cleared then delegates are cleared`() { component.invokeOnCleared() - verify(bcmcDelegate).onCleared() + verify(cardDelegate).onCleared() verify(genericActionDelegate).onCleared() verify(componentEventHandler).onCleared() } @@ -83,11 +84,11 @@ internal class BcmcComponentTest( @Test fun `when observe is called then observe in delegates is called`() { val lifecycleOwner = mock() - val callback: (PaymentComponentEvent) -> Unit = {} + val callback: (PaymentComponentEvent) -> Unit = {} component.observe(lifecycleOwner, callback) - verify(bcmcDelegate).observe(lifecycleOwner, component.viewModelScope, callback) + verify(cardDelegate).observe(lifecycleOwner, component.viewModelScope, callback) verify(genericActionDelegate).observe(eq(lifecycleOwner), eq(component.viewModelScope), any()) } @@ -95,7 +96,7 @@ internal class BcmcComponentTest( fun `when removeObserver is called then removeObserver in delegates is called`() { component.removeObserver() - verify(bcmcDelegate).removeObserver() + verify(cardDelegate).removeObserver() verify(genericActionDelegate).removeObserver() } @@ -110,8 +111,13 @@ internal class BcmcComponentTest( @Test fun `when bcmc delegate view flow emits a value then component view flow should match that value`() = runTest { val bcmcDelegateViewFlow = MutableStateFlow(TestComponentViewType.VIEW_TYPE_1) - whenever(bcmcDelegate.viewFlow) doReturn bcmcDelegateViewFlow - component = BcmcComponent(bcmcDelegate, genericActionDelegate, actionHandlingComponent, componentEventHandler) + whenever(cardDelegate.viewFlow) doReturn bcmcDelegateViewFlow + component = BcmcComponent( + cardDelegate = cardDelegate, + genericActionDelegate = genericActionDelegate, + actionHandlingComponent = actionHandlingComponent, + componentEventHandler = componentEventHandler + ) component.viewFlow.test { assertEquals(TestComponentViewType.VIEW_TYPE_1, awaitItem()) @@ -127,7 +133,12 @@ internal class BcmcComponentTest( fun `when action delegate view flow emits a value then component view flow should match that value`() = runTest { val actionDelegateViewFlow = MutableStateFlow(TestComponentViewType.VIEW_TYPE_1) whenever(genericActionDelegate.viewFlow) doReturn actionDelegateViewFlow - component = BcmcComponent(bcmcDelegate, genericActionDelegate, actionHandlingComponent, componentEventHandler) + component = BcmcComponent( + cardDelegate = cardDelegate, + genericActionDelegate = genericActionDelegate, + actionHandlingComponent = actionHandlingComponent, + componentEventHandler = componentEventHandler + ) component.viewFlow.test { // this value should match the value of the main delegate and not the action delegate @@ -144,20 +155,20 @@ internal class BcmcComponentTest( @Test fun `when isConfirmationRequired, then delegate is called`() { component.isConfirmationRequired() - verify(bcmcDelegate).isConfirmationRequired() + verify(cardDelegate).isConfirmationRequired() } @Test fun `when submit is called and active delegate is the payment delegate, then delegate onSubmit is called`() { - whenever(component.delegate).thenReturn(bcmcDelegate) + whenever(component.delegate).thenReturn(cardDelegate) component.submit() - verify(bcmcDelegate).onSubmit() + verify(cardDelegate).onSubmit() } @Test fun `when submit is called and active delegate is the action delegate, then delegate onSubmit is not called`() { whenever(component.delegate).thenReturn(genericActionDelegate) component.submit() - verify(bcmcDelegate, never()).onSubmit() + verify(cardDelegate, never()).onSubmit() } } diff --git a/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegateTest.kt b/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegateTest.kt deleted file mode 100644 index 5c40543c2b..0000000000 --- a/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegateTest.kt +++ /dev/null @@ -1,487 +0,0 @@ -/* - * Copyright (c) 2022 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by atef on 22/8/2022. - */ - -package com.adyen.checkout.bcmc.internal.ui - -import app.cash.turbine.test -import com.adyen.checkout.bcmc.BcmcComponentState -import com.adyen.checkout.bcmc.BcmcConfiguration -import com.adyen.checkout.bcmc.internal.ui.model.BcmcComponentParamsMapper -import com.adyen.checkout.bcmc.internal.ui.model.BcmcOutputData -import com.adyen.checkout.card.R -import com.adyen.checkout.card.internal.ui.CardValidationMapper -import com.adyen.checkout.card.internal.ui.model.ExpiryDate -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.OrderRequest -import com.adyen.checkout.components.core.PaymentMethod -import com.adyen.checkout.components.core.PaymentMethodTypes -import com.adyen.checkout.components.core.internal.PaymentObserverRepository -import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.test.TestPublicKeyRepository -import com.adyen.checkout.components.core.internal.ui.model.FieldState -import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.core.Environment -import com.adyen.checkout.cse.internal.test.TestCardEncrypter -import com.adyen.checkout.test.TestDispatcherExtension -import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments.arguments -import org.junit.jupiter.params.provider.MethodSource -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import java.util.Locale - -@OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) -internal class DefaultBcmcDelegateTest( - @Mock private val analyticsRepository: AnalyticsRepository, - @Mock private val submitHandler: SubmitHandler, -) { - - private lateinit var testPublicKeyRepository: TestPublicKeyRepository - private lateinit var cardEncrypter: TestCardEncrypter - private lateinit var cardValidationMapper: CardValidationMapper - private lateinit var delegate: DefaultBcmcDelegate - - @BeforeEach - fun setup() { - testPublicKeyRepository = TestPublicKeyRepository() - cardEncrypter = TestCardEncrypter() - cardValidationMapper = CardValidationMapper() - delegate = createBcmcDelegate() - } - - @Test - fun `when fetching the public key fails, then an error is propagated`() = runTest { - testPublicKeyRepository.shouldReturnError = true - - delegate.exceptionFlow.test { - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - - val exception = expectMostRecentItem() - assertEquals(testPublicKeyRepository.errorResult.exceptionOrNull(), exception.cause) - - cancelAndIgnoreRemainingEvents() - } - } - - @Nested - @DisplayName("when input data changes and") - inner class InputDataChangedTest { - @Test - fun `card number is empty, then output should be invalid`() = runTest { - delegate.outputDataFlow.test { - delegate.updateInputData { - cardNumber = "" - expiryDate = TEST_EXPIRY_DATE - } - - with(expectMostRecentItem()) { - assertTrue(cardNumberField.validation is Validation.Invalid) - assertTrue(expiryDateField.validation is Validation.Valid) - assertFalse(isValid) - } - } - } - - @Test - fun `card number is invalid, then output should be invalid`() = runTest { - delegate.outputDataFlow.test { - delegate.updateInputData { - cardNumber = "12345678" - expiryDate = TEST_EXPIRY_DATE - } - - with(expectMostRecentItem()) { - assertTrue(cardNumberField.validation is Validation.Invalid) - assertTrue(expiryDateField.validation is Validation.Valid) - assertFalse(isValid) - } - } - } - - @Test - fun `expiry date is invalid, then output should be invalid`() = - runTest { - delegate.outputDataFlow.test { - delegate.updateInputData { - cardNumber = TEST_CARD_NUMBER - expiryDate = ExpiryDate.INVALID_DATE - } - - with(expectMostRecentItem()) { - assertTrue(cardNumberField.validation is Validation.Valid) - assertTrue(expiryDateField.validation is Validation.Invalid) - assertFalse(isValid) - } - } - } - - @Test - fun `expiry date is empty, then output should be invalid`() = runTest { - delegate.outputDataFlow.test { - delegate.updateInputData { - cardNumber = TEST_CARD_NUMBER - expiryDate = ExpiryDate.EMPTY_DATE - } - - with(expectMostRecentItem()) { - assertTrue(cardNumberField.validation is Validation.Valid) - assertTrue(expiryDateField.validation is Validation.Invalid) - assertFalse(isValid) - } - } - } - - @Test - fun `input is valid, then output should be valid`() = runTest { - delegate.outputDataFlow.test { - delegate.updateInputData { - cardNumber = TEST_CARD_NUMBER - expiryDate = TEST_EXPIRY_DATE - } - - with(expectMostRecentItem()) { - assertTrue(cardNumberField.validation is Validation.Valid) - assertTrue(expiryDateField.validation is Validation.Valid) - assertTrue(isValid) - } - } - } - } - - @Nested - @DisplayName("when creating component state and") - inner class CreateComponentStateTest { - - @Test - fun `component is not initialized, then component state should not be ready`() = runTest { - delegate.componentStateFlow.test { - delegate.updateComponentState( - createOutputData( - cardNumber = FieldState(TEST_CARD_NUMBER, Validation.Valid), - expiryDate = FieldState(TEST_EXPIRY_DATE, Validation.Valid), - cardHolder = FieldState("Name", Validation.Valid) - ) - ) - - with(expectMostRecentItem()) { - assertFalse(isReady) - } - } - } - - @Test - fun `encryption fails, then component state should be invalid`() = runTest { - cardEncrypter.shouldThrowException = true - - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - - delegate.componentStateFlow.test { - delegate.updateComponentState( - createOutputData( - cardNumber = FieldState(TEST_CARD_NUMBER, Validation.Valid), - expiryDate = FieldState(TEST_EXPIRY_DATE, Validation.Valid), - cardHolder = FieldState("Name", Validation.Valid) - ) - ) - - with(expectMostRecentItem()) { - assertTrue(isReady) - assertFalse(isInputValid) - } - } - } - - @Test - fun `card number in output data is invalid, then component state should be invalid`() = runTest { - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - delegate.componentStateFlow.test { - delegate.updateComponentState( - createOutputData( - cardNumber = FieldState( - "12345678", - Validation.Invalid(R.string.checkout_card_number_not_valid) - ), - expiryDate = FieldState(TEST_EXPIRY_DATE, Validation.Valid), - cardHolder = FieldState("Name", Validation.Valid) - ) - ) - - with(expectMostRecentItem()) { - assertFalse(isValid) - assertFalse(isInputValid) - } - } - } - - @Test - fun `expiry date in output is invalid, then component state should be invalid`() = runTest { - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - delegate.componentStateFlow.test { - delegate.updateComponentState( - createOutputData( - cardNumber = FieldState(TEST_CARD_NUMBER, Validation.Valid), - expiryDate = FieldState( - ExpiryDate.INVALID_DATE, - Validation.Invalid(R.string.checkout_expiry_date_not_valid) - ), - cardHolder = FieldState("Name", Validation.Valid), - ) - ) - - with(expectMostRecentItem()) { - assertFalse(isValid) - assertFalse(isInputValid) - } - } - } - - @Test - fun `holder name in output is invalid, then component state should be invalid`() = runTest { - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - delegate.componentStateFlow.test { - delegate.updateComponentState( - createOutputData( - cardNumber = FieldState(TEST_CARD_NUMBER, Validation.Valid), - expiryDate = FieldState(TEST_EXPIRY_DATE, Validation.Valid), - cardHolder = FieldState("", Validation.Invalid(R.string.checkout_holder_name_not_valid)), - ) - ) - - with(expectMostRecentItem()) { - assertFalse(isValid) - assertFalse(isInputValid) - } - } - } - - @Test - fun `output data is valid, then component state should be valid`() = runTest { - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - delegate.componentStateFlow.test { - delegate.updateComponentState( - createOutputData( - cardNumber = FieldState(TEST_CARD_NUMBER, Validation.Valid), - expiryDate = FieldState(TEST_EXPIRY_DATE, Validation.Valid), - cardHolder = FieldState("Name", Validation.Valid) - ) - ) - with(expectMostRecentItem()) { - assertTrue(isValid) - assertTrue(isInputValid) - assertEquals(TEST_ORDER, data.order) - assertEquals(PaymentMethodTypes.BCMC, data.paymentMethod?.brand) - } - } - } - - @ParameterizedTest - @MethodSource("com.adyen.checkout.bcmc.internal.ui.DefaultBcmcDelegateTest#shouldStorePaymentMethodSource") - fun `storePaymentMethod in component state should match store switch visibility and state`( - isStorePaymentMethodSwitchVisible: Boolean, - isStorePaymentMethodSwitchChecked: Boolean, - expectedStorePaymentMethod: Boolean?, - ) = runTest { - val configuration = getDefaultBcmcConfigurationBuilder() - .setShowStorePaymentField(isStorePaymentMethodSwitchVisible) - .build() - delegate = createBcmcDelegate(configuration = configuration) - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - - delegate.componentStateFlow.test { - delegate.updateInputData { - cardNumber = TEST_CARD_NUMBER - expiryDate = TEST_EXPIRY_DATE - this.isStorePaymentMethodSwitchChecked = isStorePaymentMethodSwitchChecked - } - - val componentState = expectMostRecentItem() - assertEquals(expectedStorePaymentMethod, componentState.data.storePaymentMethod) - } - } - - @ParameterizedTest - @MethodSource("com.adyen.checkout.bcmc.internal.ui.DefaultBcmcDelegateTest#amountSource") - fun `when input data is valid then amount is propagated in component state if set`( - configurationValue: Amount?, - expectedComponentStateValue: Amount?, - ) = runTest { - if (configurationValue != null) { - val configuration = getDefaultBcmcConfigurationBuilder() - .setAmount(configurationValue) - .build() - delegate = createBcmcDelegate(configuration = configuration) - } - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - delegate.componentStateFlow.test { - delegate.updateInputData { - cardNumber = TEST_CARD_NUMBER - expiryDate = TEST_EXPIRY_DATE - } - assertEquals(expectedComponentStateValue, expectMostRecentItem().data.amount) - } - } - } - - @Test - fun `when delegate is initialized then analytics event is sent`() = runTest { - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - verify(analyticsRepository).setupAnalytics() - } - - @Nested - inner class SubmitButtonVisibilityTest { - - @Test - fun `when submit button is configured to be hidden, then it should not show`() { - delegate = createBcmcDelegate( - configuration = getDefaultBcmcConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() - ) - - assertFalse(delegate.shouldShowSubmitButton()) - } - - @Test - fun `when submit button is configured to be visible, then it should show`() { - delegate = createBcmcDelegate( - configuration = getDefaultBcmcConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() - ) - - assertTrue(delegate.shouldShowSubmitButton()) - } - } - - @Nested - inner class SubmitHandlerTest { - - @Test - fun `when delegate is initialized then submit handler event is initialized`() = runTest { - val coroutineScope = CoroutineScope(UnconfinedTestDispatcher()) - delegate.initialize(coroutineScope) - verify(submitHandler).initialize(coroutineScope, delegate.componentStateFlow) - } - - @Test - fun `when delegate setInteractionBlocked is called then submit handler setInteractionBlocked is called`() = - runTest { - delegate.setInteractionBlocked(true) - verify(submitHandler).setInteractionBlocked(true) - } - - @Test - fun `when delegate onSubmit is called then submit handler onSubmit is called`() = runTest { - delegate.componentStateFlow.test { - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - delegate.onSubmit() - verify(submitHandler).onSubmit(expectMostRecentItem()) - } - } - } - - @Nested - inner class AnalyticsTest { - - @Test - fun `when component state is valid then PaymentMethodDetails should contain checkoutAttemptId`() = runTest { - whenever(analyticsRepository.getCheckoutAttemptId()) doReturn TEST_CHECKOUT_ATTEMPT_ID - - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - - delegate.componentStateFlow.test { - delegate.updateInputData { - cardNumber = TEST_CARD_NUMBER - expiryDate = TEST_EXPIRY_DATE - } - - assertEquals(TEST_CHECKOUT_ATTEMPT_ID, expectMostRecentItem().data.paymentMethod?.checkoutAttemptId) - } - } - } - - private fun createOutputData( - cardNumber: FieldState, - expiryDate: FieldState, - cardHolder: FieldState, - showStorePaymentField: Boolean = false, - shouldStorePaymentMethod: Boolean = false - ): BcmcOutputData { - return BcmcOutputData( - cardNumberField = cardNumber, - expiryDateField = expiryDate, - cardHolderNameField = cardHolder, - showStorePaymentField = showStorePaymentField, - shouldStorePaymentMethod = shouldStorePaymentMethod, - ) - } - - private fun createBcmcDelegate( - configuration: BcmcConfiguration = getDefaultBcmcConfigurationBuilder().build() - ) = DefaultBcmcDelegate( - observerRepository = PaymentObserverRepository(), - paymentMethod = PaymentMethod(), - order = TEST_ORDER, - publicKeyRepository = testPublicKeyRepository, - componentParams = BcmcComponentParamsMapper(null, null).mapToParams(configuration, null), - cardValidationMapper = cardValidationMapper, - cardEncrypter = cardEncrypter, - analyticsRepository = analyticsRepository, - submitHandler = submitHandler, - ) - - private fun getDefaultBcmcConfigurationBuilder() = BcmcConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ) - - companion object { - private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" - private const val TEST_CARD_NUMBER = "5555444433331111" - private val TEST_EXPIRY_DATE = ExpiryDate(3, 2030) - private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA") - private const val TEST_CHECKOUT_ATTEMPT_ID = "TEST_CHECKOUT_ATTEMPT_ID" - - @JvmStatic - fun shouldStorePaymentMethodSource() = listOf( - // isStorePaymentMethodSwitchVisible, isStorePaymentMethodSwitchChecked, expectedStorePaymentMethod - arguments(false, false, null), - arguments(false, true, null), - arguments(true, false, false), - arguments(true, true, true), - ) - - @JvmStatic - fun amountSource() = listOf( - // configurationValue, expectedComponentStateValue - arguments(Amount("EUR", 100), Amount("EUR", 100)), - arguments(Amount("USD", 0), Amount("USD", 0)), - arguments(null, null), - arguments(null, null), - ) - } -} diff --git a/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapperTest.kt b/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapperTest.kt index 88784bed80..132f5e05f2 100644 --- a/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapperTest.kt +++ b/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapperTest.kt @@ -9,12 +9,21 @@ package com.adyen.checkout.bcmc.internal.ui.model import com.adyen.checkout.bcmc.BcmcConfiguration +import com.adyen.checkout.card.CardBrand +import com.adyen.checkout.card.CardType +import com.adyen.checkout.card.KCPAuthVisibility +import com.adyen.checkout.card.SocialSecurityNumberVisibility +import com.adyen.checkout.card.internal.ui.model.CVCVisibility +import com.adyen.checkout.card.internal.ui.model.CardComponentParams +import com.adyen.checkout.card.internal.ui.model.StoredCVCVisibility import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.core.Environment +import com.adyen.checkout.ui.core.internal.ui.model.AddressParams import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -29,9 +38,10 @@ internal class BcmcComponentParamsMapperTest { val bcmcConfiguration = getBcmcConfigurationBuilder() .build() - val params = BcmcComponentParamsMapper(null, null).mapToParams(bcmcConfiguration, null) + val params = BcmcComponentParamsMapper(null, null) + .mapToParams(bcmcConfiguration, null, PaymentMethod()) - val expected = getBcmcComponentParams() + val expected = getCardComponentParams() assertEquals(expected, params) } @@ -47,13 +57,15 @@ internal class BcmcComponentParamsMapperTest { .setSubmitButtonVisible(false) .build() - val params = BcmcComponentParamsMapper(null, null).mapToParams(bcmcConfiguration, null) + val params = BcmcComponentParamsMapper(null, null) + .mapToParams(bcmcConfiguration, null, PaymentMethod()) - val expected = getBcmcComponentParams( + val expected = getCardComponentParams( isHolderNameRequired = true, shopperReference = shopperReference, isStorePaymentFieldVisible = true, - isSubmitButtonVisible = false + isSubmitButtonVisible = false, + cvcVisibility = CVCVisibility.HIDE_FIRST ) assertEquals(expected, params) @@ -78,9 +90,10 @@ internal class BcmcComponentParamsMapperTest { ) ) - val params = BcmcComponentParamsMapper(overrideParams, null).mapToParams(bcmcConfiguration, null) + val params = BcmcComponentParamsMapper(overrideParams, null) + .mapToParams(bcmcConfiguration, null, PaymentMethod()) - val expected = getBcmcComponentParams( + val expected = getCardComponentParams( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, @@ -111,13 +124,14 @@ internal class BcmcComponentParamsMapperTest { bcmcConfiguration = bcmcConfiguration, sessionParams = SessionParams( enableStoreDetails = sessionsValue, - installmentOptions = null, + installmentConfiguration = null, amount = null, returnUrl = "", - ) + ), + PaymentMethod() ) - val expected = getBcmcComponentParams(isStorePaymentFieldVisible = expectedValue) + val expected = getCardComponentParams(isStorePaymentFieldVisible = expectedValue) assertEquals(expected, params) } @@ -136,19 +150,20 @@ internal class BcmcComponentParamsMapperTest { // this is in practice DropInComponentParams, but we don't have access to it in this module and any // ComponentParams class can work - val overrideParams = dropInValue?.let { getBcmcComponentParams(amount = it) } + val overrideParams = dropInValue?.let { getCardComponentParams(amount = it) } val params = BcmcComponentParamsMapper(overrideParams, null).mapToParams( bcmcConfiguration, sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", - ) + ), + PaymentMethod() ) - val expected = getBcmcComponentParams( + val expected = getCardComponentParams( amount = expectedValue ) @@ -162,7 +177,7 @@ internal class BcmcComponentParamsMapperTest { ) @Suppress("LongParameterList") - private fun getBcmcComponentParams( + private fun getCardComponentParams( shopperLocale: Locale = Locale.US, environment: Environment = Environment.TEST, clientKey: String = TEST_CLIENT_KEY_1, @@ -173,7 +188,8 @@ internal class BcmcComponentParamsMapperTest { isHolderNameRequired: Boolean = false, shopperReference: String? = null, isStorePaymentFieldVisible: Boolean = false, - ) = BcmcComponentParams( + cvcVisibility: CVCVisibility = CVCVisibility.HIDE_FIRST, + ) = CardComponentParams( shopperLocale = shopperLocale, environment = environment, clientKey = clientKey, @@ -183,7 +199,18 @@ internal class BcmcComponentParamsMapperTest { isSubmitButtonVisible = isSubmitButtonVisible, isHolderNameRequired = isHolderNameRequired, shopperReference = shopperReference, - isStorePaymentFieldVisible = isStorePaymentFieldVisible + isStorePaymentFieldVisible = isStorePaymentFieldVisible, + cvcVisibility = cvcVisibility, + addressParams = AddressParams.None, + installmentParams = null, + socialSecurityNumberVisibility = SocialSecurityNumberVisibility.HIDE, + kcpAuthVisibility = KCPAuthVisibility.HIDE, + storedCVCVisibility = StoredCVCVisibility.HIDE, + supportedCardBrands = listOf( + CardBrand(cardType = CardType.BCMC), + CardBrand(cardType = CardType.MAESTRO), + CardBrand(cardType = CardType.VISA) + ) ) companion object { diff --git a/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapperTest.kt b/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapperTest.kt index 7f663adbd6..3c9f14dc66 100644 --- a/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapperTest.kt +++ b/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapperTest.kt @@ -126,7 +126,7 @@ internal class BoletoComponentParamsMapperTest { boletoConfiguration, sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", ) diff --git a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt index 699046e406..108bc5fa14 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.card +import androidx.annotation.RestrictTo import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -34,7 +35,7 @@ import kotlinx.coroutines.flow.Flow /** * A [PaymentComponent] that supports the [PaymentMethodTypes.SCHEME] payment method. */ -class CardComponent internal constructor( +open class CardComponent constructor( private val cardDelegate: CardDelegate, private val genericActionDelegate: GenericActionDelegate, private val actionHandlingComponent: DefaultActionHandlingComponent, @@ -60,7 +61,8 @@ class CardComponent internal constructor( componentEventHandler.initialize(viewModelScope) } - internal fun observe( + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun observe( lifecycleOwner: LifecycleOwner, callback: (PaymentComponentEvent) -> Unit ) { @@ -73,7 +75,8 @@ class CardComponent internal constructor( ) } - internal fun removeObserver() { + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun removeObserver() { cardDelegate.removeObserver() genericActionDelegate.removeObserver() } diff --git a/card/src/main/java/com/adyen/checkout/card/InstallmentConfiguration.kt b/card/src/main/java/com/adyen/checkout/card/InstallmentConfiguration.kt index 8ce1392775..772177514d 100644 --- a/card/src/main/java/com/adyen/checkout/card/InstallmentConfiguration.kt +++ b/card/src/main/java/com/adyen/checkout/card/InstallmentConfiguration.kt @@ -28,7 +28,8 @@ import kotlinx.parcelize.Parcelize @Parcelize data class InstallmentConfiguration( val defaultOptions: InstallmentOptions.DefaultInstallmentOptions? = null, - val cardBasedOptions: List = emptyList() + val cardBasedOptions: List = emptyList(), + val showInstallmentAmount: Boolean = false ) : Parcelable { init { diff --git a/card/src/main/java/com/adyen/checkout/card/KCPAuthVisibility.kt b/card/src/main/java/com/adyen/checkout/card/KCPAuthVisibility.kt index 86822a852e..6e298ec995 100644 --- a/card/src/main/java/com/adyen/checkout/card/KCPAuthVisibility.kt +++ b/card/src/main/java/com/adyen/checkout/card/KCPAuthVisibility.kt @@ -4,5 +4,6 @@ package com.adyen.checkout.card * Used in [CardConfiguration.Builder.kcpAuthVisibility] to show or hide the KCP authentication input field. */ enum class KCPAuthVisibility { - SHOW, HIDE + SHOW, + HIDE, } diff --git a/card/src/main/java/com/adyen/checkout/card/SocialSecurityNumberVisibility.kt b/card/src/main/java/com/adyen/checkout/card/SocialSecurityNumberVisibility.kt index 690eb155eb..324cf1849b 100644 --- a/card/src/main/java/com/adyen/checkout/card/SocialSecurityNumberVisibility.kt +++ b/card/src/main/java/com/adyen/checkout/card/SocialSecurityNumberVisibility.kt @@ -5,5 +5,6 @@ package com.adyen.checkout.card * field. */ enum class SocialSecurityNumberVisibility { - SHOW, HIDE + SHOW, + HIDE, } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/data/api/BinLookupService.kt b/card/src/main/java/com/adyen/checkout/card/internal/data/api/BinLookupService.kt index e479d517fa..47affe25db 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/data/api/BinLookupService.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/data/api/BinLookupService.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.card.internal.data.api +import androidx.annotation.RestrictTo import com.adyen.checkout.card.internal.data.model.BinLookupRequest import com.adyen.checkout.card.internal.data.model.BinLookupResponse import com.adyen.checkout.core.internal.data.api.HttpClient @@ -15,7 +16,8 @@ import com.adyen.checkout.core.internal.data.api.post import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -internal class BinLookupService( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class BinLookupService( private val httpClient: HttpClient, ) { diff --git a/card/src/main/java/com/adyen/checkout/card/internal/data/api/DefaultDetectCardTypeRepository.kt b/card/src/main/java/com/adyen/checkout/card/internal/data/api/DefaultDetectCardTypeRepository.kt index 25f7e55c92..a0ba09c501 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/data/api/DefaultDetectCardTypeRepository.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/data/api/DefaultDetectCardTypeRepository.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.card.internal.data.api +import androidx.annotation.RestrictTo import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.CardType import com.adyen.checkout.card.internal.data.model.BinLookupRequest @@ -28,7 +29,8 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import java.util.UUID -internal class DefaultDetectCardTypeRepository( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class DefaultDetectCardTypeRepository( private val cardEncrypter: BaseCardEncrypter, private val binLookupService: BinLookupService, ) : DetectCardTypeRepository { @@ -45,6 +47,7 @@ internal class DefaultDetectCardTypeRepository( supportedCardBrands: List, clientKey: String, coroutineScope: CoroutineScope, + type: String? ) { Logger.d(TAG, "detectCardType") if (shouldFetchReliableTypes(cardNumber)) { @@ -64,7 +67,8 @@ internal class DefaultDetectCardTypeRepository( publicKey, supportedCardBrands, clientKey, - coroutineScope + coroutineScope, + type ) } } @@ -79,6 +83,7 @@ internal class DefaultDetectCardTypeRepository( supportedCardBrands: List, clientKey: String, coroutineScope: CoroutineScope, + type: String? ) { if (publicKey != null) { Logger.d(TAG, "Launching Bin Lookup") @@ -89,7 +94,8 @@ internal class DefaultDetectCardTypeRepository( cardNumber, publicKey, supportedCardBrands, - clientKey + clientKey, + type )?.let { _detectedCardTypesFlow.send(it) } @@ -139,10 +145,11 @@ internal class DefaultDetectCardTypeRepository( publicKey: String, supportedCardBrands: List, clientKey: String, + type: String? ): List? { val key = hashBin(cardNumber) cachedBinLookup[key] = BinLookupResult.Loading - val binLookupResponse = makeBinLookup(cardNumber, publicKey, supportedCardBrands, clientKey) + val binLookupResponse = makeBinLookup(cardNumber, publicKey, supportedCardBrands, clientKey, type) return if (binLookupResponse == null) { cachedBinLookup.remove(key) @@ -159,11 +166,12 @@ internal class DefaultDetectCardTypeRepository( publicKey: String, supportedCardBrands: List, clientKey: String, + type: String? ): BinLookupResponse? { return runSuspendCatching { val encryptedBin = cardEncrypter.encryptBin(cardNumber, publicKey) val cardBrands = supportedCardBrands.map { it.txVariant } - val request = BinLookupRequest(encryptedBin, UUID.randomUUID().toString(), cardBrands) + val request = BinLookupRequest(encryptedBin, UUID.randomUUID().toString(), cardBrands, type) binLookupService.makeBinLookup( request = request, diff --git a/card/src/main/java/com/adyen/checkout/card/internal/data/api/DetectCardTypeRepository.kt b/card/src/main/java/com/adyen/checkout/card/internal/data/api/DetectCardTypeRepository.kt index d86fec995e..ea81cf6544 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/data/api/DetectCardTypeRepository.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/data/api/DetectCardTypeRepository.kt @@ -8,12 +8,14 @@ package com.adyen.checkout.card.internal.data.api +import androidx.annotation.RestrictTo import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.internal.data.model.DetectedCardType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -internal interface DetectCardTypeRepository { +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface DetectCardTypeRepository { val detectedCardTypesFlow: Flow> @@ -24,5 +26,6 @@ internal interface DetectCardTypeRepository { supportedCardBrands: List, clientKey: String, coroutineScope: CoroutineScope, + type: String? = null ) } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/data/model/BinLookupRequest.kt b/card/src/main/java/com/adyen/checkout/card/internal/data/model/BinLookupRequest.kt index 5c297542b5..e36fbc5694 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/data/model/BinLookupRequest.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/data/model/BinLookupRequest.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.card.internal.data.model +import androidx.annotation.RestrictTo import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.JsonUtils import com.adyen.checkout.core.internal.data.model.ModelObject @@ -18,16 +19,19 @@ import org.json.JSONException import org.json.JSONObject @Parcelize -internal data class BinLookupRequest( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class BinLookupRequest( val encryptedBin: String? = null, val requestId: String? = null, - val supportedBrands: List? = null + val supportedBrands: List? = null, + val type: String? = null, ) : ModelObject() { companion object { private const val ENCRYPTED_BIN = "encryptedBin" private const val REQUEST_ID = "requestId" private const val SUPPORTED_BRANDS = "supportedBrands" + private const val TYPE = "type" @JvmField val SERIALIZER: Serializer = object : Serializer { @@ -37,6 +41,7 @@ internal data class BinLookupRequest( jsonObject.putOpt(ENCRYPTED_BIN, modelObject.encryptedBin) jsonObject.putOpt(REQUEST_ID, modelObject.requestId) jsonObject.putOpt(SUPPORTED_BRANDS, JsonUtils.serializeOptStringList(modelObject.supportedBrands)) + jsonObject.putOpt(TYPE, modelObject.type) } catch (e: JSONException) { throw ModelSerializationException(BinLookupRequest::class.java, e) } @@ -48,7 +53,8 @@ internal data class BinLookupRequest( BinLookupRequest( encryptedBin = jsonObject.getStringOrNull(ENCRYPTED_BIN), requestId = jsonObject.getStringOrNull(REQUEST_ID), - supportedBrands = jsonObject.optStringList(SUPPORTED_BRANDS) + supportedBrands = jsonObject.optStringList(SUPPORTED_BRANDS), + type = jsonObject.getStringOrNull(TYPE), ) } catch (e: JSONException) { throw ModelSerializationException(BinLookupRequest::class.java, e) diff --git a/card/src/main/java/com/adyen/checkout/card/internal/data/model/BinLookupResponse.kt b/card/src/main/java/com/adyen/checkout/card/internal/data/model/BinLookupResponse.kt index 0bb04e0218..5908a98da4 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/data/model/BinLookupResponse.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/data/model/BinLookupResponse.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.card.internal.data.model +import androidx.annotation.RestrictTo import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.ModelObject import com.adyen.checkout.core.internal.data.model.ModelUtils @@ -17,7 +18,8 @@ import org.json.JSONException import org.json.JSONObject @Parcelize -internal data class BinLookupResponse( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class BinLookupResponse( val brands: List? = null, val issuingCountryCode: String? = null, val requestId: String? = null diff --git a/card/src/main/java/com/adyen/checkout/card/internal/data/model/DetectedCardType.kt b/card/src/main/java/com/adyen/checkout/card/internal/data/model/DetectedCardType.kt index faf99e221e..f2fdfe5c1c 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/data/model/DetectedCardType.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/data/model/DetectedCardType.kt @@ -8,9 +8,11 @@ package com.adyen.checkout.card.internal.data.model +import androidx.annotation.RestrictTo import com.adyen.checkout.card.CardBrand -internal data class DetectedCardType( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class DetectedCardType( val cardBrand: CardBrand, val isReliable: Boolean, val enableLuhnCheck: Boolean, diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/CardDelegate.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/CardDelegate.kt index db4d5669f1..5d3a8b0c8d 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/CardDelegate.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/CardDelegate.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.card.internal.ui +import androidx.annotation.RestrictTo import com.adyen.checkout.card.BinLookupData import com.adyen.checkout.card.CardComponentState import com.adyen.checkout.card.internal.ui.model.CardInputData @@ -20,7 +21,8 @@ import com.adyen.checkout.ui.core.internal.ui.UIStateDelegate import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate import kotlinx.coroutines.flow.Flow -internal interface CardDelegate : +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface CardDelegate : PaymentComponentDelegate, ViewProvidingDelegate, ButtonDelegate, diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegate.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegate.kt index 786f81cf77..b247f24cb1 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegate.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegate.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.card.internal.ui +import androidx.annotation.RestrictTo import androidx.annotation.VisibleForTesting import androidx.lifecycle.LifecycleOwner import com.adyen.checkout.card.BinLookupData @@ -19,6 +20,7 @@ import com.adyen.checkout.card.SocialSecurityNumberVisibility import com.adyen.checkout.card.internal.data.api.DetectCardTypeRepository import com.adyen.checkout.card.internal.data.model.Brand import com.adyen.checkout.card.internal.data.model.DetectedCardType +import com.adyen.checkout.card.internal.ui.model.CVCVisibility import com.adyen.checkout.card.internal.ui.model.CardComponentParams import com.adyen.checkout.card.internal.ui.model.CardInputData import com.adyen.checkout.card.internal.ui.model.CardListItem @@ -83,8 +85,9 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -@Suppress("LongParameterList", "TooManyFunctions") -internal class DefaultCardDelegate( +@Suppress("LongParameterList", "TooManyFunctions", "LargeClass") +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class DefaultCardDelegate( private val observerRepository: PaymentObserverRepository, private val publicKeyRepository: PublicKeyRepository, override val componentParams: CardComponentParams, @@ -222,7 +225,8 @@ internal class DefaultCardDelegate( publicKey = publicKey, supportedCardBrands = componentParams.supportedCardBrands, clientKey = componentParams.clientKey, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, + type = paymentMethod.type ) requestStateList(inputData.address.country) } @@ -240,6 +244,9 @@ internal class DefaultCardDelegate( } updateOutputData(detectedCardTypes = detectedCardTypes) } + .map { detectedCardTypes -> detectedCardTypes.map { it.cardBrand } } + .distinctUntilChanged() + .onEach { inputData.selectedCardIndex = -1 } .launchIn(coroutineScope) } @@ -308,8 +315,6 @@ internal class DefaultCardDelegate( detectedCardTypes = filteredDetectedCardTypes ) - val reliableSelectedCard = if (isReliable) selectedOrFirstCardType else null - // perform a Luhn Check if no brands are detected val enableLuhnCheck = selectedOrFirstCardType?.enableLuhnCheck ?: true @@ -333,13 +338,13 @@ internal class DefaultCardDelegate( addressState = validateAddress( inputData.address, addressFormUIState, - reliableSelectedCard, + selectedOrFirstCardType, updatedCountryOptions, updatedStateOptions ), installmentState = makeInstallmentFieldState(inputData.installmentOption), shouldStorePaymentMethod = inputData.isStorePaymentMethodSwitchChecked, - cvcUIState = makeCvcUIState(selectedOrFirstCardType?.cvcPolicy), + cvcUIState = makeCvcUIState(selectedOrFirstCardType), expiryDateUIState = makeExpiryDateUIState(selectedOrFirstCardType?.expiryDatePolicy), holderNameUIState = getHolderNameUIState(), showStorePaymentField = showStorePaymentField(), @@ -362,7 +367,9 @@ internal class DefaultCardDelegate( private fun isCardListVisible( cardBrands: List, detectedCardTypes: List - ): Boolean = cardBrands.isNotEmpty() && detectedCardTypes.isEmpty() + ): Boolean = cardBrands.isNotEmpty() && + detectedCardTypes.isEmpty() && + paymentMethod.type == PaymentMethodTypes.SCHEME override fun getPaymentMethodType(): String { return paymentMethod.type ?: PaymentMethodTypes.UNKNOWN @@ -477,14 +484,8 @@ internal class DefaultCardDelegate( securityCode: String, cardType: DetectedCardType? ): FieldState { - return if (componentParams.isHideCvc) { - FieldState( - securityCode, - Validation.Valid - ) - } else { - CardValidationUtils.validateSecurityCode(securityCode, cardType) - } + val cvcUIState = makeCvcUIState(cardType) + return CardValidationUtils.validateSecurityCode(securityCode, cardType, cvcUIState) } private fun validateHolderName(holderName: String): FieldState { @@ -547,8 +548,8 @@ internal class DefaultCardDelegate( ) } - private fun isCvcHidden(): Boolean { - return componentParams.isHideCvc + private fun isCvcHidden(cvcUIState: InputFieldUIState = outputData.cvcUIState): Boolean { + return cvcUIState == InputFieldUIState.HIDDEN } private fun isSocialSecurityNumberRequired(): Boolean { @@ -603,18 +604,42 @@ internal class DefaultCardDelegate( ) } - private fun makeCvcUIState(cvcPolicy: Brand.FieldPolicy?): InputFieldUIState { - Logger.d(TAG, "makeCvcUIState: $cvcPolicy") - return when { - isCvcHidden() -> InputFieldUIState.HIDDEN - cvcPolicy?.isRequired() == false -> InputFieldUIState.OPTIONAL - else -> InputFieldUIState.REQUIRED + private fun makeCvcUIState(detectedCardType: DetectedCardType?): InputFieldUIState { + Logger.d(TAG, "makeCvcUIState: ${detectedCardType?.cvcPolicy}") + + return if (detectedCardType?.isReliable == true) { + when (componentParams.cvcVisibility) { + CVCVisibility.ALWAYS_SHOW -> { + when (detectedCardType.cvcPolicy) { + Brand.FieldPolicy.OPTIONAL -> InputFieldUIState.OPTIONAL + Brand.FieldPolicy.HIDDEN -> InputFieldUIState.HIDDEN + else -> InputFieldUIState.REQUIRED + } + } + + CVCVisibility.HIDE_FIRST -> { + when (detectedCardType.cvcPolicy) { + Brand.FieldPolicy.REQUIRED -> InputFieldUIState.REQUIRED + Brand.FieldPolicy.OPTIONAL -> InputFieldUIState.OPTIONAL + else -> InputFieldUIState.HIDDEN + } + } + + CVCVisibility.ALWAYS_HIDE -> InputFieldUIState.HIDDEN + } + } else { + when (componentParams.cvcVisibility) { + CVCVisibility.ALWAYS_SHOW -> InputFieldUIState.REQUIRED + CVCVisibility.HIDE_FIRST -> InputFieldUIState.HIDDEN + CVCVisibility.ALWAYS_HIDE -> InputFieldUIState.HIDDEN + } } } private fun makeExpiryDateUIState(expiryDatePolicy: Brand.FieldPolicy?): InputFieldUIState { - return when { - expiryDatePolicy?.isRequired() == false -> InputFieldUIState.OPTIONAL + return when (expiryDatePolicy) { + Brand.FieldPolicy.OPTIONAL -> InputFieldUIState.OPTIONAL + Brand.FieldPolicy.HIDDEN -> InputFieldUIState.HIDDEN else -> InputFieldUIState.REQUIRED } } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/StoredCardDelegate.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/StoredCardDelegate.kt index 238c6175c2..5f9919b0e2 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/StoredCardDelegate.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/StoredCardDelegate.kt @@ -21,6 +21,7 @@ import com.adyen.checkout.card.internal.ui.model.CardInputData import com.adyen.checkout.card.internal.ui.model.CardOutputData import com.adyen.checkout.card.internal.ui.model.ExpiryDate import com.adyen.checkout.card.internal.ui.model.InputFieldUIState +import com.adyen.checkout.card.internal.ui.model.StoredCVCVisibility import com.adyen.checkout.card.internal.util.CardValidationUtils import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentComponentData @@ -82,7 +83,8 @@ internal class StoredCardDelegate( isReliable = true, enableLuhnCheck = true, cvcPolicy = when { - componentParams.isHideCvcStoredCard || noCvcBrands.contains(cardType) -> Brand.FieldPolicy.HIDDEN + componentParams.storedCVCVisibility == StoredCVCVisibility.HIDE || + noCvcBrands.contains(cardType) -> Brand.FieldPolicy.HIDDEN else -> Brand.FieldPolicy.REQUIRED }, expiryDatePolicy = Brand.FieldPolicy.REQUIRED, @@ -306,18 +308,12 @@ internal class StoredCardDelegate( } private fun validateSecurityCode(securityCode: String, detectedCardType: DetectedCardType): FieldState { - return if (componentParams.isHideCvcStoredCard || noCvcBrands.contains(detectedCardType.cardBrand)) { - FieldState( - securityCode, - Validation.Valid - ) - } else { - CardValidationUtils.validateSecurityCode(securityCode, detectedCardType) - } + val cvcUiState = makeCvcUIState(detectedCardType.cvcPolicy) + return CardValidationUtils.validateSecurityCode(securityCode, detectedCardType, cvcUiState) } private fun isCvcHidden(): Boolean { - return componentParams.isHideCvcStoredCard || noCvcBrands.contains(cardType) + return outputData.cvcUIState == InputFieldUIState.HIDDEN } private fun mapComponentState( @@ -382,10 +378,10 @@ internal class StoredCardDelegate( private fun makeCvcUIState(cvcPolicy: Brand.FieldPolicy): InputFieldUIState { Logger.d(TAG, "makeCvcUIState: $cvcPolicy") - return when { - isCvcHidden() -> InputFieldUIState.HIDDEN - !cvcPolicy.isRequired() -> InputFieldUIState.OPTIONAL - else -> InputFieldUIState.REQUIRED + return when (cvcPolicy) { + Brand.FieldPolicy.REQUIRED -> InputFieldUIState.REQUIRED + Brand.FieldPolicy.OPTIONAL -> InputFieldUIState.OPTIONAL + Brand.FieldPolicy.HIDDEN -> InputFieldUIState.HIDDEN } } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CVCVisibility.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CVCVisibility.kt new file mode 100644 index 0000000000..fea2fec269 --- /dev/null +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CVCVisibility.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 5/9/2023. + */ + +package com.adyen.checkout.card.internal.ui.model + +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +enum class CVCVisibility { + ALWAYS_SHOW, + HIDE_FIRST, + ALWAYS_HIDE +} + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +enum class StoredCVCVisibility { + SHOW, + HIDE +} diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParams.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParams.kt index 13c47c9467..36890caba5 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParams.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParams.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.card.internal.ui.model +import androidx.annotation.RestrictTo import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.KCPAuthVisibility import com.adyen.checkout.card.SocialSecurityNumberVisibility @@ -19,7 +20,8 @@ import com.adyen.checkout.core.Environment import com.adyen.checkout.ui.core.internal.ui.model.AddressParams import java.util.Locale -internal data class CardComponentParams( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class CardComponentParams( override val shopperLocale: Locale, override val environment: Environment, override val clientKey: String, @@ -31,10 +33,10 @@ internal data class CardComponentParams( val supportedCardBrands: List, val shopperReference: String?, val isStorePaymentFieldVisible: Boolean, - val isHideCvc: Boolean, - val isHideCvcStoredCard: Boolean, val socialSecurityNumberVisibility: SocialSecurityNumberVisibility, val kcpAuthVisibility: KCPAuthVisibility, val installmentParams: InstallmentParams?, val addressParams: AddressParams, + val cvcVisibility: CVCVisibility, + val storedCVCVisibility: StoredCVCVisibility ) : ComponentParams, ButtonParams diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt index 7a4ffbf88d..726a6f57c6 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt @@ -80,12 +80,24 @@ internal class CardComponentParamsMapper( supportedCardBrands = supportedCardBrands, shopperReference = shopperReference, isStorePaymentFieldVisible = isStorePaymentFieldVisible ?: true, - isHideCvc = isHideCvc ?: false, - isHideCvcStoredCard = isHideCvcStoredCard ?: false, socialSecurityNumberVisibility = socialSecurityNumberVisibility ?: SocialSecurityNumberVisibility.HIDE, kcpAuthVisibility = kcpAuthVisibility ?: KCPAuthVisibility.HIDE, - installmentParams = installmentsParamsMapper.mapToInstallmentParams(installmentConfiguration), - addressParams = addressConfiguration?.mapToAddressParam() ?: AddressParams.None + installmentParams = installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = installmentConfiguration, + amount = amount, + shopperLocale = shopperLocale + ), + addressParams = addressConfiguration?.mapToAddressParam() ?: AddressParams.None, + cvcVisibility = if (isHideCvc == true) { + CVCVisibility.ALWAYS_HIDE + } else { + CVCVisibility.ALWAYS_SHOW + }, + storedCVCVisibility = if (isHideCvcStoredCard == true) { + StoredCVCVisibility.HIDE + } else { + StoredCVCVisibility.SHOW + } ) } @@ -102,12 +114,14 @@ internal class CardComponentParamsMapper( Logger.v(TAG, "Reading supportedCardTypes from configuration") supportedCardBrands } + paymentMethod.brands.orEmpty().isNotEmpty() -> { Logger.v(TAG, "Reading supportedCardTypes from API brands") paymentMethod.brands.orEmpty().map { CardBrand(txVariant = it) } } + else -> { Logger.v(TAG, "Falling back to CardConfiguration.DEFAULT_SUPPORTED_CARDS_LIST") CardConfiguration.DEFAULT_SUPPORTED_CARDS_LIST @@ -146,9 +160,11 @@ internal class CardComponentParamsMapper( addressFieldPolicy.mapToAddressParamFieldPolicy() ) } + AddressConfiguration.None -> { AddressParams.None } + is AddressConfiguration.PostalCode -> { AddressParams.PostalCode(addressFieldPolicy.mapToAddressParamFieldPolicy()) } @@ -160,9 +176,11 @@ internal class CardComponentParamsMapper( is AddressConfiguration.CardAddressFieldPolicy.Optional -> { AddressFieldPolicyParams.Optional } + is AddressConfiguration.CardAddressFieldPolicy.OptionalForCardTypes -> { AddressFieldPolicyParams.OptionalForCardTypes(brands) } + is AddressConfiguration.CardAddressFieldPolicy.Required -> { AddressFieldPolicyParams.Required } @@ -178,7 +196,11 @@ internal class CardComponentParamsMapper( // we don't fall back to the original value of installmentParams value on purpose // if sessionParams.installmentOptions is null we want installmentParams to be also null regardless of what // InstallmentConfiguration is passed to the mapper - installmentParams = installmentsParamsMapper.mapToInstallmentParams(sessionParams.installmentOptions), + installmentParams = installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = sessionParams.installmentConfiguration, + amount = sessionParams.amount ?: amount, + shopperLocale = shopperLocale + ), amount = sessionParams.amount ?: amount, ) } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardInputData.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardInputData.kt index 0c57b4568c..52f0d917d0 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardInputData.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardInputData.kt @@ -7,11 +7,13 @@ */ package com.adyen.checkout.card.internal.ui.model +import androidx.annotation.RestrictTo import com.adyen.checkout.card.internal.ui.view.InstallmentModel import com.adyen.checkout.components.core.internal.ui.model.InputData import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel -internal data class CardInputData( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class CardInputData( var cardNumber: String = "", var expiryDate: ExpiryDate = ExpiryDate.EMPTY_DATE, var securityCode: String = "", diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardListItem.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardListItem.kt index e8e50c3503..f0497afe5b 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardListItem.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardListItem.kt @@ -8,10 +8,12 @@ package com.adyen.checkout.card.internal.ui.model +import androidx.annotation.RestrictTo import com.adyen.checkout.card.CardBrand import com.adyen.checkout.core.Environment -internal data class CardListItem( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class CardListItem( val cardBrand: CardBrand, val isDetected: Boolean, // We need the environment to load the logo diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardOutputData.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardOutputData.kt index 703b1659c6..4a5858f99c 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardOutputData.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardOutputData.kt @@ -7,6 +7,7 @@ */ package com.adyen.checkout.card.internal.ui.model +import androidx.annotation.RestrictTo import androidx.annotation.StringRes import com.adyen.checkout.card.internal.data.model.DetectedCardType import com.adyen.checkout.card.internal.ui.view.InstallmentModel @@ -15,7 +16,8 @@ import com.adyen.checkout.components.core.internal.ui.model.OutputData import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData -internal data class CardOutputData( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class CardOutputData( val cardNumberState: FieldState, val expiryDateState: FieldState, val securityCodeState: FieldState, diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InputFieldUIState.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InputFieldUIState.kt index fa9a69c40a..513e0683f8 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InputFieldUIState.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InputFieldUIState.kt @@ -8,6 +8,11 @@ package com.adyen.checkout.card.internal.ui.model -internal enum class InputFieldUIState { - REQUIRED, OPTIONAL, HIDDEN +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +enum class InputFieldUIState { + REQUIRED, + OPTIONAL, + HIDDEN } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentOption.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentOption.kt index ac7c2899af..c42e6a0e89 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentOption.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentOption.kt @@ -8,7 +8,10 @@ package com.adyen.checkout.card.internal.ui.model -internal enum class InstallmentOption(val type: String?) { +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +enum class InstallmentOption(val type: String?) { ONE_TIME(null), REGULAR("regular"), REVOLVING("revolving") diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentOptionParams.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentOptionParams.kt index b020847632..663a7aa753 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentOptionParams.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentOptionParams.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.card.internal.ui.model +import androidx.annotation.RestrictTo import com.adyen.checkout.card.CardBrand /** @@ -15,7 +16,8 @@ import com.adyen.checkout.card.CardBrand * * Note: All values specified in [values] must be greater than 1. */ -internal sealed class InstallmentOptionParams { +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +sealed class InstallmentOptionParams { abstract val values: List abstract val includeRevolving: Boolean diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentParams.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentParams.kt index b4a3878930..c6faf57113 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentParams.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentParams.kt @@ -8,7 +8,10 @@ package com.adyen.checkout.card.internal.ui.model +import androidx.annotation.RestrictTo import com.adyen.checkout.card.CardBrand +import com.adyen.checkout.components.core.Amount +import java.util.Locale /** * Component params class for Installments in Card Component. This class can be used @@ -21,8 +24,15 @@ import com.adyen.checkout.card.CardBrand * * @param defaultOptions Installment Options to be used for all card types. * @param cardBasedOptions Installment Options to be used for specific card types. + * @param amount Amount of the transaction. + * @param shopperLocale The [Locale] of the shopper. + * @param showInstallmentAmount A flag to show the installment amount. */ -internal data class InstallmentParams( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class InstallmentParams( val defaultOptions: InstallmentOptionParams.DefaultInstallmentOptions? = null, - val cardBasedOptions: List = emptyList() + val cardBasedOptions: List = emptyList(), + val amount: Amount? = null, + val shopperLocale: Locale, + val showInstallmentAmount: Boolean = false ) diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentsParamsMapper.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentsParamsMapper.kt index dba72902bd..b83f9bc76b 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentsParamsMapper.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentsParamsMapper.kt @@ -11,35 +11,54 @@ package com.adyen.checkout.card.internal.ui.model import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.InstallmentConfiguration import com.adyen.checkout.card.InstallmentOptions +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentOptionsParams +import java.util.Locale internal class InstallmentsParamsMapper { internal fun mapToInstallmentParams( - sessionInstallmentOptions: Map? + installmentConfiguration: SessionInstallmentConfiguration?, + amount: Amount?, + shopperLocale: Locale ): InstallmentParams? { - sessionInstallmentOptions ?: return null + installmentConfiguration?.installmentOptions ?: return null + + val showInstallmentAmount = installmentConfiguration.showInstallmentAmount ?: false var defaultOptions: InstallmentOptionParams.DefaultInstallmentOptions? = null val cardBasedOptionsList = mutableListOf() - sessionInstallmentOptions.forEach { (key, value) -> + installmentConfiguration.installmentOptions?.forEach { (key, value) -> if (key == DEFAULT_INSTALLMENT_OPTION) { defaultOptions = value.mapToDefaultInstallmentOptions() } else { cardBasedOptionsList.add(value.mapToCardBasedInstallmentOptions(key)) } } - return InstallmentParams(defaultOptions, cardBasedOptionsList) + + return InstallmentParams( + defaultOptions = defaultOptions, + cardBasedOptions = cardBasedOptionsList, + amount = amount, + shopperLocale = shopperLocale, + showInstallmentAmount = showInstallmentAmount + ) } internal fun mapToInstallmentParams( - installmentConfiguration: InstallmentConfiguration? + installmentConfiguration: InstallmentConfiguration?, + amount: Amount?, + shopperLocale: Locale ): InstallmentParams? { installmentConfiguration ?: return null return InstallmentParams( defaultOptions = installmentConfiguration.defaultOptions?.mapToDefaultInstallmentOptionsParam(), cardBasedOptions = installmentConfiguration.cardBasedOptions.map { option -> option.mapToCardBasedInstallmentOptionsParams() - } + }, + amount = amount, + shopperLocale = shopperLocale, + showInstallmentAmount = installmentConfiguration.showInstallmentAmount ) } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/CardView.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/CardView.kt index c9bd84b4e8..ba178b1cff 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/CardView.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/CardView.kt @@ -18,6 +18,7 @@ import android.view.View.OnFocusChangeListener import android.view.WindowManager import android.widget.AdapterView import android.widget.LinearLayout +import androidx.annotation.RestrictTo import androidx.annotation.StringRes import androidx.core.view.isVisible import com.adyen.checkout.card.CardBrand @@ -55,7 +56,8 @@ import kotlinx.coroutines.flow.onEach * CardView for [CardComponent]. */ @Suppress("TooManyFunctions", "LargeClass") -internal class CardView @JvmOverloads constructor( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class CardView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -596,12 +598,14 @@ internal class CardView @JvmOverloads constructor( localizedContext ) } + InputFieldUIState.OPTIONAL -> { binding.textInputLayoutSecurityCode.isVisible = true binding.textInputLayoutSecurityCode.hint = localizedContext.getString( R.string.checkout_card_security_code_optional_hint ) } + InputFieldUIState.HIDDEN -> { binding.textInputLayoutSecurityCode.isVisible = false // We don't expect the hidden status to change back to isVisible, so we don't worry about putting the @@ -622,12 +626,14 @@ internal class CardView @JvmOverloads constructor( localizedContext ) } + InputFieldUIState.OPTIONAL -> { binding.textInputLayoutExpiryDate.isVisible = true binding.textInputLayoutExpiryDate.hint = localizedContext.getString( R.string.checkout_card_expiry_date_optional_hint ) } + InputFieldUIState.HIDDEN -> { binding.textInputLayoutExpiryDate.isVisible = false val params = binding.textInputLayoutSecurityCode.layoutParams as LayoutParams @@ -665,10 +671,12 @@ internal class CardView @JvmOverloads constructor( binding.addressFormInput.isVisible = true binding.textInputLayoutPostalCode.isVisible = false } + AddressFormUIState.POSTAL_CODE -> { binding.addressFormInput.isVisible = false binding.textInputLayoutPostalCode.isVisible = true } + AddressFormUIState.NONE -> { binding.addressFormInput.isVisible = false binding.textInputLayoutPostalCode.isVisible = false @@ -687,6 +695,7 @@ internal class CardView @JvmOverloads constructor( } binding.textInputLayoutPostalCode.setLocalizedHintFromStyle(postalCodeStyleResId, localizedContext) } + else -> { // no ops } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/InstallmentListAdapter.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/InstallmentListAdapter.kt index 68c45ec2a0..64ec510ab8 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/InstallmentListAdapter.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/InstallmentListAdapter.kt @@ -15,13 +15,16 @@ import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.Filter import android.widget.Filterable -import androidx.annotation.StringRes +import androidx.annotation.RestrictTo import androidx.recyclerview.widget.RecyclerView import com.adyen.checkout.card.databinding.InstallmentViewBinding import com.adyen.checkout.card.internal.ui.model.InstallmentOption import com.adyen.checkout.card.internal.util.InstallmentUtils +import com.adyen.checkout.components.core.Amount +import java.util.Locale // We need context to inflate the views and localizedContext to fetch the strings +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) internal class InstallmentListAdapter( private val context: Context, private val localizedContext: Context @@ -64,10 +67,13 @@ internal class InstallmentListAdapter( } } -internal data class InstallmentModel( - @StringRes val textResId: Int, - val value: Int?, - val option: InstallmentOption +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class InstallmentModel( + val numberOfInstallments: Int?, + val option: InstallmentOption, + val amount: Amount?, + val shopperLocale: Locale, + val showAmount: Boolean ) internal class InstallmentFilter( diff --git a/card/src/main/java/com/adyen/checkout/card/internal/util/CardValidationUtils.kt b/card/src/main/java/com/adyen/checkout/card/internal/util/CardValidationUtils.kt index cf19c3a052..4bd3ea891c 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/util/CardValidationUtils.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/util/CardValidationUtils.kt @@ -15,6 +15,7 @@ import com.adyen.checkout.card.R import com.adyen.checkout.card.internal.data.model.Brand import com.adyen.checkout.card.internal.data.model.DetectedCardType import com.adyen.checkout.card.internal.ui.model.ExpiryDate +import com.adyen.checkout.card.internal.ui.model.InputFieldUIState import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.core.internal.util.StringUtil @@ -137,13 +138,18 @@ object CardValidationUtils { /** * Validate Security Code. */ - internal fun validateSecurityCode(securityCode: String, detectedCardType: DetectedCardType?): FieldState { + internal fun validateSecurityCode( + securityCode: String, + detectedCardType: DetectedCardType?, + cvcUIState: InputFieldUIState + ): FieldState { val normalizedSecurityCode = StringUtil.normalize(securityCode) val length = normalizedSecurityCode.length val invalidState = Validation.Invalid(R.string.checkout_security_code_not_valid) val validation = when { + cvcUIState == InputFieldUIState.HIDDEN -> Validation.Valid !StringUtil.isDigitsAndSeparatorsOnly(normalizedSecurityCode) -> invalidState - detectedCardType?.cvcPolicy?.isRequired() == false && length == 0 -> Validation.Valid + cvcUIState == InputFieldUIState.OPTIONAL && length == 0 -> Validation.Valid detectedCardType?.cardBrand == CardBrand(cardType = CardType.AMERICAN_EXPRESS) && length == AMEX_SECURITY_CODE_SIZE -> Validation.Valid diff --git a/card/src/main/java/com/adyen/checkout/card/internal/util/DetectedCardTypesUtils.kt b/card/src/main/java/com/adyen/checkout/card/internal/util/DetectedCardTypesUtils.kt index d0c351610e..33493c2c5f 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/util/DetectedCardTypesUtils.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/util/DetectedCardTypesUtils.kt @@ -22,7 +22,8 @@ internal object DetectedCardTypesUtils { } fun getSelectedOrFirstDetectedCardType(detectedCardTypes: List): DetectedCardType? { - return getSelectedCardType(detectedCardTypes) ?: detectedCardTypes.firstOrNull() + val selectedCardType = getSelectedCardType(detectedCardTypes) + return selectedCardType ?: detectedCardTypes.firstOrNull() } fun getSelectedCardType(detectedCardTypes: List): DetectedCardType? { diff --git a/card/src/main/java/com/adyen/checkout/card/internal/util/InstallmentUtils.kt b/card/src/main/java/com/adyen/checkout/card/internal/util/InstallmentUtils.kt index 2e0f33767d..89b17a7649 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/util/InstallmentUtils.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/util/InstallmentUtils.kt @@ -17,7 +17,11 @@ import com.adyen.checkout.card.internal.ui.model.InstallmentOption import com.adyen.checkout.card.internal.ui.model.InstallmentOptionParams import com.adyen.checkout.card.internal.ui.model.InstallmentParams import com.adyen.checkout.card.internal.ui.view.InstallmentModel +import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.Installments +import com.adyen.checkout.components.core.internal.util.CurrencyUtils +import com.adyen.checkout.components.core.internal.util.formatToLocalizedString +import java.util.Locale private const val REVOLVING_INSTALLMENT_VALUE = 1 @@ -27,53 +31,76 @@ internal object InstallmentUtils { * Create a list of installment options from [InstallmentParams]. */ fun makeInstallmentOptions( - params: InstallmentParams?, + installmentParams: InstallmentParams?, cardBrand: CardBrand?, isCardTypeReliable: Boolean - ): List { - val hasCardBasedInstallmentOptions = params?.cardBasedOptions != null - val hasDefaultInstallmentOptions = params?.defaultOptions != null + ): List = installmentParams?.let { params -> + val hasCardBasedInstallmentOptions = params.cardBasedOptions.isNotEmpty() + val hasDefaultInstallmentOptions = !params.defaultOptions?.values.isNullOrEmpty() val hasOptionsForCardType = hasCardBasedInstallmentOptions && isCardTypeReliable && - (params?.cardBasedOptions?.any { it.cardBrand == cardBrand } ?: false) + params.cardBasedOptions.any { it.cardBrand == cardBrand } return when { hasOptionsForCardType -> { - makeInstallmentModelList(params?.cardBasedOptions?.firstOrNull { it.cardBrand == cardBrand }) + makeInstallmentModelList( + installmentOptions = params.cardBasedOptions.firstOrNull { it.cardBrand == cardBrand }, + amount = params.amount, + shopperLocale = params.shopperLocale, + showAmount = params.showInstallmentAmount + ) } + hasDefaultInstallmentOptions -> { - makeInstallmentModelList(params?.defaultOptions) + makeInstallmentModelList( + installmentOptions = params.defaultOptions, + amount = params.amount, + shopperLocale = params.shopperLocale, + showAmount = params.showInstallmentAmount + ) } + else -> { emptyList() } } - } + } ?: emptyList() - private fun makeInstallmentModelList(installmentOptions: InstallmentOptionParams?): List { + private fun makeInstallmentModelList( + installmentOptions: InstallmentOptionParams?, + amount: Amount?, + shopperLocale: Locale, + showAmount: Boolean + ): List { if (installmentOptions == null) return emptyList() val installmentOptionsList = mutableListOf() val oneTimeOption = InstallmentModel( - textResId = R.string.checkout_card_installments_option_one_time, - value = null, - option = InstallmentOption.ONE_TIME + numberOfInstallments = null, + option = InstallmentOption.ONE_TIME, + amount = amount, + shopperLocale = shopperLocale, + showAmount = showAmount ) installmentOptionsList.add(oneTimeOption) if (installmentOptions.includeRevolving) { val revolvingOption = InstallmentModel( - textResId = R.string.checkout_card_installments_option_revolving, - value = REVOLVING_INSTALLMENT_VALUE, - option = InstallmentOption.REVOLVING + numberOfInstallments = REVOLVING_INSTALLMENT_VALUE, + option = InstallmentOption.REVOLVING, + amount = amount, + shopperLocale = shopperLocale, + showAmount = showAmount ) installmentOptionsList.add(revolvingOption) } - val regularOptions = installmentOptions.values.map { + val regularOptions = installmentOptions.values.map { numberOfInstallments -> InstallmentModel( - textResId = R.string.checkout_card_installments_option_regular, - value = it, - option = InstallmentOption.REGULAR + numberOfInstallments = numberOfInstallments, + option = InstallmentOption.REGULAR, + amount = amount, + shopperLocale = shopperLocale, + showAmount = showAmount ) } installmentOptionsList.addAll(regularOptions) @@ -83,24 +110,44 @@ internal object InstallmentUtils { /** * Get the text to be shown for different types of [InstallmentOption]. */ - fun getTextForInstallmentOption(context: Context, installmentModel: InstallmentModel?): String { - return when (installmentModel?.option) { - InstallmentOption.REGULAR -> context.getString(installmentModel.textResId, installmentModel.value) - InstallmentOption.REVOLVING, InstallmentOption.ONE_TIME -> context.getString(installmentModel.textResId) - else -> "" + fun getTextForInstallmentOption(context: Context, installmentModel: InstallmentModel?): String = + with(installmentModel) { + return when (this?.option) { + InstallmentOption.ONE_TIME -> context.getString(R.string.checkout_card_installments_option_one_time) + InstallmentOption.REVOLVING -> context.getString(R.string.checkout_card_installments_option_revolving) + InstallmentOption.REGULAR -> { + val numberOfInstallments = numberOfInstallments ?: 1 + val installmentAmount = amount?.copy(value = amount.value / numberOfInstallments) + val formattedNumberOfInstallments = numberOfInstallments.formatToLocalizedString(shopperLocale) + + if (showAmount && installmentAmount != null) { + val formattedInstallmentAmount = CurrencyUtils.formatAmount(installmentAmount, shopperLocale) + context.getString( + R.string.checkout_card_installments_option_regular_with_price, + formattedNumberOfInstallments, + formattedInstallmentAmount + ) + } else { + context.getString( + R.string.checkout_card_installments_option_regular, + formattedNumberOfInstallments + ) + } + } + + else -> "" + } } - } /** * Populate the [Installments] model object from [InstallmentModel]. */ - fun makeInstallmentModelObject(installmentModel: InstallmentModel?): Installments? { - return when (installmentModel?.option) { - InstallmentOption.REGULAR, InstallmentOption.REVOLVING -> { - Installments(installmentModel.option.type, installmentModel.value) - } - else -> null + fun makeInstallmentModelObject(installmentModel: InstallmentModel?) = when (installmentModel?.option) { + InstallmentOption.REGULAR, InstallmentOption.REVOLVING -> { + Installments(installmentModel.option.type, installmentModel.numberOfInstallments) } + + else -> null } /** @@ -112,7 +159,7 @@ internal object InstallmentUtils { val hasMultipleOptionsForSameCard = cardBasedInstallmentOptions ?.groupBy { it.cardBrand } ?.values - ?.any { it.size > 1 } ?: false + ?.any { value -> value.size > 1 } ?: false return !hasMultipleOptionsForSameCard } @@ -124,7 +171,9 @@ internal object InstallmentUtils { val installmentOptions = mutableListOf() installmentOptions.add(installmentConfiguration.defaultOptions) installmentOptions.addAll(installmentConfiguration.cardBasedOptions) - val hasInvalidValue = installmentOptions.filterNotNull().any { it.values.any { it <= 1 } } + val hasInvalidValue = installmentOptions.filterNotNull().any { installmentOption -> + installmentOption.values.any { value -> value <= 1 } + } return !hasInvalidValue } } diff --git a/card/src/main/res/template/values/strings.xml.tt b/card/src/main/res/template/values/strings.xml.tt index a2eab929ca..95bf142c5b 100644 --- a/card/src/main/res/template/values/strings.xml.tt +++ b/card/src/main/res/template/values/strings.xml.tt @@ -32,6 +32,7 @@ %%installments%% %%installmentOptionMonths%% + %%installmentOption%% %%installments.oneTime%% %%installments.revolving%% diff --git a/card/src/main/res/values-ar/strings.xml b/card/src/main/res/values-ar/strings.xml index 41e8ec3254..2e4c2f3497 100644 --- a/card/src/main/res/values-ar/strings.xml +++ b/card/src/main/res/values-ar/strings.xml @@ -32,6 +32,7 @@ عدد الأقساط %s أشهر + %s × %s الدفع مرة واحدة الدفع الدوري diff --git a/card/src/main/res/values-cs-rCZ/strings.xml b/card/src/main/res/values-cs-rCZ/strings.xml index 973f7b0eb9..6cfb3dc18a 100644 --- a/card/src/main/res/values-cs-rCZ/strings.xml +++ b/card/src/main/res/values-cs-rCZ/strings.xml @@ -32,6 +32,7 @@ Počet splátek %s měsíců + %s× %s Jednorázová platba Opakující se platba diff --git a/card/src/main/res/values-da-rDK/strings.xml b/card/src/main/res/values-da-rDK/strings.xml index acc143a7f0..a4e6fc42bd 100644 --- a/card/src/main/res/values-da-rDK/strings.xml +++ b/card/src/main/res/values-da-rDK/strings.xml @@ -32,6 +32,7 @@ Antal rater %s måneder + %sx %s Engangsbetaling Løbende betaling diff --git a/card/src/main/res/values-de-rDE/strings.xml b/card/src/main/res/values-de-rDE/strings.xml index e7f57401b3..ac0f7e6754 100644 --- a/card/src/main/res/values-de-rDE/strings.xml +++ b/card/src/main/res/values-de-rDE/strings.xml @@ -32,6 +32,7 @@ Anzahl der Raten %s Monate + %sx %s Einmalige Zahlung Ratenzahlung diff --git a/card/src/main/res/values-el-rGR/strings.xml b/card/src/main/res/values-el-rGR/strings.xml index bcc6bf15d6..99982e9f97 100644 --- a/card/src/main/res/values-el-rGR/strings.xml +++ b/card/src/main/res/values-el-rGR/strings.xml @@ -32,6 +32,7 @@ Αριθμός δόσεων %s μήνες + %sx %s Εφάπαξ πληρωμή Ανακυκλούμενη πληρωμή diff --git a/card/src/main/res/values-es-rES/strings.xml b/card/src/main/res/values-es-rES/strings.xml index 63bb278e97..290dbaaaae 100644 --- a/card/src/main/res/values-es-rES/strings.xml +++ b/card/src/main/res/values-es-rES/strings.xml @@ -32,6 +32,7 @@ Número de plazos %s meses + %sx %s Pago único Pago rotativo diff --git a/card/src/main/res/values-fi-rFI/strings.xml b/card/src/main/res/values-fi-rFI/strings.xml index 3814456402..eccc53f258 100644 --- a/card/src/main/res/values-fi-rFI/strings.xml +++ b/card/src/main/res/values-fi-rFI/strings.xml @@ -32,6 +32,7 @@ Asennusten määrä %s kuukautta + %s x%s Kertamaksu Toistuva maksu diff --git a/card/src/main/res/values-fr-rFR/strings.xml b/card/src/main/res/values-fr-rFR/strings.xml index e0f2e05ef7..3a909f9afd 100644 --- a/card/src/main/res/values-fr-rFR/strings.xml +++ b/card/src/main/res/values-fr-rFR/strings.xml @@ -32,6 +32,7 @@ Nombre de versements %s mois + %sx %s Paiement unique Paiement en plusieurs fois diff --git a/card/src/main/res/values-hr-rHR/strings.xml b/card/src/main/res/values-hr-rHR/strings.xml index fbc80dcdd4..06578c52a3 100644 --- a/card/src/main/res/values-hr-rHR/strings.xml +++ b/card/src/main/res/values-hr-rHR/strings.xml @@ -32,6 +32,7 @@ Broj rata Mjeseci: %s + %s x %s Jednokratno plaćanje Obnovljivo plaćanje diff --git a/card/src/main/res/values-hu-rHU/strings.xml b/card/src/main/res/values-hu-rHU/strings.xml index ef752508c7..278a73c72b 100644 --- a/card/src/main/res/values-hu-rHU/strings.xml +++ b/card/src/main/res/values-hu-rHU/strings.xml @@ -32,6 +32,7 @@ Részletek száma %s hónap + %s x %s Egyösszegű fizetés Többösszegű fizetés diff --git a/card/src/main/res/values-it-rIT/strings.xml b/card/src/main/res/values-it-rIT/strings.xml index 6cbffb9618..02922571c2 100644 --- a/card/src/main/res/values-it-rIT/strings.xml +++ b/card/src/main/res/values-it-rIT/strings.xml @@ -32,6 +32,7 @@ Numero di rate %s mesi + %s x%s Pagamento una tantum Pagamento ricorrente diff --git a/card/src/main/res/values-ja-rJP/strings.xml b/card/src/main/res/values-ja-rJP/strings.xml index 0c00ad57d9..bc993528b8 100644 --- a/card/src/main/res/values-ja-rJP/strings.xml +++ b/card/src/main/res/values-ja-rJP/strings.xml @@ -32,6 +32,7 @@ 分割回数 %sか月 + %sx %s 一括払い リボ払い diff --git a/card/src/main/res/values-ko-rKR/strings.xml b/card/src/main/res/values-ko-rKR/strings.xml index b1be2b26e2..bd87e5cb0a 100644 --- a/card/src/main/res/values-ko-rKR/strings.xml +++ b/card/src/main/res/values-ko-rKR/strings.xml @@ -32,6 +32,7 @@ 할부 개월 수 %s개월 + %sx %s 일시불 결제 리볼빙 결제 diff --git a/card/src/main/res/values-nb-rNO/strings.xml b/card/src/main/res/values-nb-rNO/strings.xml index 2bf0eb7875..98f43a4f72 100644 --- a/card/src/main/res/values-nb-rNO/strings.xml +++ b/card/src/main/res/values-nb-rNO/strings.xml @@ -32,6 +32,7 @@ Antall avdrag %s måneder + %sx %s Engangsbetaling Gjentakende betaling diff --git a/card/src/main/res/values-nl-rNL/strings.xml b/card/src/main/res/values-nl-rNL/strings.xml index 2efeccd7cc..042cbd8e3c 100644 --- a/card/src/main/res/values-nl-rNL/strings.xml +++ b/card/src/main/res/values-nl-rNL/strings.xml @@ -32,6 +32,7 @@ Aantal termijnen %s maanden + %sx %s Eenmalige betaling Terugkerende betaling diff --git a/card/src/main/res/values-pl-rPL/strings.xml b/card/src/main/res/values-pl-rPL/strings.xml index 9c97823fa3..b9083d4ef2 100644 --- a/card/src/main/res/values-pl-rPL/strings.xml +++ b/card/src/main/res/values-pl-rPL/strings.xml @@ -32,6 +32,7 @@ Liczba rat %s miesięcy + %sx %s Płatność jednorazowa Płatność odnawialna diff --git a/card/src/main/res/values-pt-rBR/strings.xml b/card/src/main/res/values-pt-rBR/strings.xml index 20b028d6aa..4b58ce9c90 100644 --- a/card/src/main/res/values-pt-rBR/strings.xml +++ b/card/src/main/res/values-pt-rBR/strings.xml @@ -32,6 +32,7 @@ Opções de Parcelamento %s meses + %sx %s Pagamento à vista Pagamento rotativo diff --git a/card/src/main/res/values-pt-rPT/strings.xml b/card/src/main/res/values-pt-rPT/strings.xml index b019b7fab5..ef6e04208e 100644 --- a/card/src/main/res/values-pt-rPT/strings.xml +++ b/card/src/main/res/values-pt-rPT/strings.xml @@ -32,6 +32,7 @@ Número de prestações %s meses + %sx %s Pagamento único Pagamento rotativo diff --git a/card/src/main/res/values-ro-rRO/strings.xml b/card/src/main/res/values-ro-rRO/strings.xml index be47ceade3..17817225aa 100644 --- a/card/src/main/res/values-ro-rRO/strings.xml +++ b/card/src/main/res/values-ro-rRO/strings.xml @@ -32,6 +32,7 @@ Număr de rate %s luni + %sx %s Plată unică Plată recurentă diff --git a/card/src/main/res/values-ru-rRU/strings.xml b/card/src/main/res/values-ru-rRU/strings.xml index 24a8c96825..a63e095fdd 100644 --- a/card/src/main/res/values-ru-rRU/strings.xml +++ b/card/src/main/res/values-ru-rRU/strings.xml @@ -32,6 +32,7 @@ Количество платежей %s мес. + %s× %s Одноразовый платеж Повторяющаяся оплата diff --git a/card/src/main/res/values-sk-rSK/strings.xml b/card/src/main/res/values-sk-rSK/strings.xml index 7058d76ddc..cf3eee1987 100644 --- a/card/src/main/res/values-sk-rSK/strings.xml +++ b/card/src/main/res/values-sk-rSK/strings.xml @@ -32,6 +32,7 @@ Počet splátok %s mesiace/-ov + %s x %s Jednorazová platba Revolvingová platba diff --git a/card/src/main/res/values-sl-rSI/strings.xml b/card/src/main/res/values-sl-rSI/strings.xml index f58722c587..7d6713fb48 100644 --- a/card/src/main/res/values-sl-rSI/strings.xml +++ b/card/src/main/res/values-sl-rSI/strings.xml @@ -32,6 +32,7 @@ Število obrokov Št. mesecev: %s + %s × %s Enkratno plačilo Revolving plačilo diff --git a/card/src/main/res/values-sv-rSE/strings.xml b/card/src/main/res/values-sv-rSE/strings.xml index ba0bb66367..9f0ab5fead 100644 --- a/card/src/main/res/values-sv-rSE/strings.xml +++ b/card/src/main/res/values-sv-rSE/strings.xml @@ -32,6 +32,7 @@ Antal delbetalningar %s månader + %s x %s Engångsbetalning Uppdelad betalning diff --git a/card/src/main/res/values-zh-rCN/strings.xml b/card/src/main/res/values-zh-rCN/strings.xml index 222af27640..556a84c387 100644 --- a/card/src/main/res/values-zh-rCN/strings.xml +++ b/card/src/main/res/values-zh-rCN/strings.xml @@ -32,6 +32,7 @@ 分期付款期数 %s 个月 + %sx %s 全款支付 循环支付 diff --git a/card/src/main/res/values-zh-rTW/strings.xml b/card/src/main/res/values-zh-rTW/strings.xml index 3c9641731e..f216da2218 100644 --- a/card/src/main/res/values-zh-rTW/strings.xml +++ b/card/src/main/res/values-zh-rTW/strings.xml @@ -32,6 +32,7 @@ 分期付款的期數 %s 個月 + %sx %s 一次性付款 延期付款 diff --git a/card/src/main/res/values/strings.xml b/card/src/main/res/values/strings.xml index 40af545e9f..29efdc76a4 100644 --- a/card/src/main/res/values/strings.xml +++ b/card/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ Number of installments %s months + %sx %s One time payment Revolving payment diff --git a/card/src/test/java/com/adyen/checkout/card/internal/data/api/TestDetectCardTypeRepository.kt b/card/src/test/java/com/adyen/checkout/card/internal/data/api/TestDetectCardTypeRepository.kt index 832ad9677b..cc3d20ee22 100644 --- a/card/src/test/java/com/adyen/checkout/card/internal/data/api/TestDetectCardTypeRepository.kt +++ b/card/src/test/java/com/adyen/checkout/card/internal/data/api/TestDetectCardTypeRepository.kt @@ -34,6 +34,7 @@ internal class TestDetectCardTypeRepository : DetectCardTypeRepository { supportedCardBrands: List, clientKey: String, coroutineScope: CoroutineScope, + type: String? ) { val detectedCardTypes = when (detectionResult) { TestDetectedCardType.ERROR -> null diff --git a/card/src/test/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegateTest.kt b/card/src/test/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegateTest.kt index de44d012c0..3b941ed63f 100644 --- a/card/src/test/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegateTest.kt +++ b/card/src/test/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegateTest.kt @@ -426,6 +426,10 @@ internal class DefaultCardDelegateTest( delegate.updateInputData { cardNumber = invalidLuhnCardNumber + } + + // we need to update selectedCardIndex separate from cardNumber to simulate the actual use case + delegate.updateInputData { selectedCardIndex = 1 } @@ -447,7 +451,7 @@ internal class DefaultCardDelegateTest( assertTrue(expiryDateState.validation is Validation.Valid) assertTrue(securityCodeState.validation is Validation.Valid) assertEquals(InputFieldUIState.OPTIONAL, cvcUIState) - assertEquals(InputFieldUIState.OPTIONAL, expiryDateUIState) + assertEquals(InputFieldUIState.HIDDEN, expiryDateUIState) assertTrue(isDualBranded) } } @@ -537,7 +541,8 @@ internal class DefaultCardDelegateTest( InstallmentOptionParams.DefaultInstallmentOptions( values = listOf(2, 3), includeRevolving = true - ) + ), + shopperLocale = Locale.US ) val addressConfiguration = AddressConfiguration.FullAddress() @@ -561,9 +566,11 @@ internal class DefaultCardDelegateTest( delegate.outputDataFlow.test { val installmentModel = InstallmentModel( - textResId = R.string.checkout_card_installments_option_revolving, - value = 1, - option = InstallmentOption.REVOLVING + numberOfInstallments = 1, + option = InstallmentOption.REVOLVING, + amount = null, + shopperLocale = Locale.US, + showAmount = false ) delegate.updateInputData { @@ -801,9 +808,11 @@ internal class DefaultCardDelegateTest( val addressUIState = AddressFormUIState.FULL_ADDRESS val installmentModel = InstallmentModel( - textResId = R.string.checkout_card_installments_option_revolving, - value = 1, - option = InstallmentOption.REVOLVING + numberOfInstallments = 1, + option = InstallmentOption.REVOLVING, + amount = null, + shopperLocale = Locale.US, + showAmount = false ) val detectedCardTypes = listOf( @@ -1105,23 +1114,24 @@ internal class DefaultCardDelegateTest( } @Test - fun `when card number is detected over network, then callback should be called with reliable result`() = runTest { - detectCardTypeRepository.detectionResult = TestDetectedCardType.FETCHED_FROM_NETWORK + fun `when card number is detected over network, then callback should be called with reliable result`() = + runTest { + detectCardTypeRepository.detectionResult = TestDetectedCardType.FETCHED_FROM_NETWORK - delegate.setOnBinLookupListener { data -> - launch(this.coroutineContext) { - with(data.first()) { - assertEquals("mc", brand) - assertEquals("mccredit", paymentMethodVariant) - assertTrue(isReliable) + delegate.setOnBinLookupListener { data -> + launch(this.coroutineContext) { + with(data.first()) { + assertEquals("mc", brand) + assertEquals("mccredit", paymentMethodVariant) + assertTrue(isReliable) + } } } - } - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) - delegate.updateInputData { cardNumber = "5555444" } - } + delegate.updateInputData { cardNumber = "5555444" } + } @Test fun `when callback is called multiple times, then it should only trigger if the data changed`() = runTest { @@ -1155,7 +1165,7 @@ internal class DefaultCardDelegateTest( cardEncrypter: BaseCardEncrypter = this.cardEncrypter, genericEncrypter: BaseGenericEncrypter = this.genericEncrypter, configuration: CardConfiguration = getDefaultCardConfigurationBuilder().build(), - paymentMethod: PaymentMethod = PaymentMethod(), + paymentMethod: PaymentMethod = PaymentMethod(type = PaymentMethodTypes.SCHEME), analyticsRepository: AnalyticsRepository = this.analyticsRepository, submitHandler: SubmitHandler = this.submitHandler, order: OrderRequest? = TEST_ORDER, diff --git a/card/src/test/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapperTest.kt b/card/src/test/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapperTest.kt index 83e65786a0..73da3b7145 100644 --- a/card/src/test/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapperTest.kt +++ b/card/src/test/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapperTest.kt @@ -21,6 +21,7 @@ import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentOptionsParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.core.Environment @@ -59,10 +60,11 @@ internal class CardComponentParamsMapperTest { ) ) val expectedInstallmentParams = InstallmentParams( - InstallmentOptionParams.DefaultInstallmentOptions( + defaultOptions = InstallmentOptionParams.DefaultInstallmentOptions( values = listOf(2, 3), includeRevolving = true - ) + ), + shopperLocale = Locale.FRANCE ) val addressConfiguration = AddressConfiguration.FullAddress(supportedCountryCodes = listOf("CA", "GB")) @@ -107,8 +109,8 @@ internal class CardComponentParamsMapperTest { shopperReference = shopperReference, isStorePaymentFieldVisible = false, isSubmitButtonVisible = false, - isHideCvc = true, - isHideCvcStoredCard = true, + cvcVisibility = CVCVisibility.ALWAYS_HIDE, + storedCVCVisibility = StoredCVCVisibility.HIDE, socialSecurityNumberVisibility = SocialSecurityNumberVisibility.SHOW, kcpAuthVisibility = KCPAuthVisibility.SHOW, installmentParams = expectedInstallmentParams, @@ -270,7 +272,7 @@ internal class CardComponentParamsMapperTest { PaymentMethod(), sessionParams = SessionParams( enableStoreDetails = sessionsValue, - installmentOptions = null, + installmentConfiguration = null, amount = null, returnUrl = "", ) @@ -301,7 +303,7 @@ internal class CardComponentParamsMapperTest { PaymentMethod(), sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = null, returnUrl = "", ) @@ -316,10 +318,21 @@ internal class CardComponentParamsMapperTest { @Test fun `installmentParams should match value set in sessions`() { + val installmentOptions = mapOf( + "card" to SessionInstallmentOptionsParams( + plans = listOf("regular"), + preselectedValue = 2, + values = listOf(2) + ) + ) + val installmentConfiguration = SessionInstallmentConfiguration( + installmentOptions = installmentOptions, + showInstallmentAmount = false + ) val cardConfiguration = getCardConfigurationBuilder() .setInstallmentConfigurations( InstallmentConfiguration( - InstallmentOptions.DefaultInstallmentOptions( + defaultOptions = InstallmentOptions.DefaultInstallmentOptions( maxInstallments = 3, includeRevolving = true ) @@ -327,13 +340,6 @@ internal class CardComponentParamsMapperTest { ) .build() - val installmentOptions = mapOf( - "card" to SessionInstallmentOptionsParams( - plans = listOf("regular"), - preselectedValue = 2, - values = listOf(2) - ) - ) val mapper = InstallmentsParamsMapper() val params = CardComponentParamsMapper(mapper, null, null).mapToParamsDefault( @@ -341,14 +347,18 @@ internal class CardComponentParamsMapperTest { PaymentMethod(), sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = installmentOptions, + installmentConfiguration = installmentConfiguration, amount = null, returnUrl = "", ) ) val expected = getCardComponentParams( - installmentParams = mapper.mapToInstallmentParams(installmentOptions) + installmentParams = mapper.mapToInstallmentParams( + installmentConfiguration = installmentConfiguration, + amount = cardConfiguration.amount, + shopperLocale = cardConfiguration.shopperLocale + ) ) assertEquals(expected, params) @@ -375,7 +385,11 @@ internal class CardComponentParamsMapperTest { ) val expected = getCardComponentParams( - installmentParams = mapper.mapToInstallmentParams(installmentConfiguration) + installmentParams = mapper.mapToInstallmentParams( + installmentConfiguration = installmentConfiguration, + amount = cardConfiguration.amount, + shopperLocale = cardConfiguration.shopperLocale + ) ) assertEquals(expected, params) @@ -419,7 +433,7 @@ internal class CardComponentParamsMapperTest { PaymentMethod(), sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", ) @@ -451,12 +465,12 @@ internal class CardComponentParamsMapperTest { supportedCardBrands: List = CardConfiguration.DEFAULT_SUPPORTED_CARDS_LIST, shopperReference: String? = null, isStorePaymentFieldVisible: Boolean = true, - isHideCvc: Boolean = false, - isHideCvcStoredCard: Boolean = false, socialSecurityNumberVisibility: SocialSecurityNumberVisibility = SocialSecurityNumberVisibility.HIDE, kcpAuthVisibility: KCPAuthVisibility = KCPAuthVisibility.HIDE, installmentParams: InstallmentParams? = null, addressParams: AddressParams = AddressParams.None, + cvcVisibility: CVCVisibility = CVCVisibility.ALWAYS_SHOW, + storedCVCVisibility: StoredCVCVisibility = StoredCVCVisibility.SHOW ) = CardComponentParams( shopperLocale = shopperLocale, environment = environment, @@ -468,13 +482,13 @@ internal class CardComponentParamsMapperTest { supportedCardBrands = supportedCardBrands, shopperReference = shopperReference, isStorePaymentFieldVisible = isStorePaymentFieldVisible, - isHideCvc = isHideCvc, - isHideCvcStoredCard = isHideCvcStoredCard, socialSecurityNumberVisibility = socialSecurityNumberVisibility, kcpAuthVisibility = kcpAuthVisibility, installmentParams = installmentParams, addressParams = addressParams, - amount = amount + amount = amount, + cvcVisibility = cvcVisibility, + storedCVCVisibility = storedCVCVisibility, ) companion object { diff --git a/card/src/test/java/com/adyen/checkout/card/internal/ui/model/InstallmentParamsMapperTest.kt b/card/src/test/java/com/adyen/checkout/card/internal/ui/model/InstallmentParamsMapperTest.kt index 478da532b5..030ddda053 100644 --- a/card/src/test/java/com/adyen/checkout/card/internal/ui/model/InstallmentParamsMapperTest.kt +++ b/card/src/test/java/com/adyen/checkout/card/internal/ui/model/InstallmentParamsMapperTest.kt @@ -12,43 +12,62 @@ import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.CardType import com.adyen.checkout.card.InstallmentConfiguration import com.adyen.checkout.card.InstallmentOptions +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentOptionsParams import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import java.util.Locale internal class InstallmentParamsMapperTest { private val installmentsParamsMapper: InstallmentsParamsMapper = InstallmentsParamsMapper() + private val amount = Amount("EUR", 100) + private val shopperLocale = Locale.US + private val showInstallmentAmount = true @Test fun `when session setup installment option is default then installment params should be the same `() { - val sessionSetupInstallmentOptionsMap = mapOf( - DEFAULT_INSTALLMENT_OPTION to SessionInstallmentOptionsParams( - plans = listOf(INSTALLMENT_PLAN), - preselectedValue = 2, - values = listOf(2) - ) + val sessionSetupInstallmentOptionsMap = SessionInstallmentConfiguration( + installmentOptions = mapOf( + DEFAULT_INSTALLMENT_OPTION to SessionInstallmentOptionsParams( + plans = listOf(INSTALLMENT_PLAN), + preselectedValue = 2, + values = listOf(2) + ) + ), + showInstallmentAmount = showInstallmentAmount ) val expectedInstallmentParams = InstallmentParams( - InstallmentOptionParams.DefaultInstallmentOptions( + defaultOptions = InstallmentOptionParams.DefaultInstallmentOptions( values = listOf(2), includeRevolving = false - ) + ), + amount = amount, + shopperLocale = shopperLocale, + showInstallmentAmount = showInstallmentAmount ) - val actualInstallmentParams = installmentsParamsMapper.mapToInstallmentParams(sessionSetupInstallmentOptionsMap) + val actualInstallmentParams = installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = sessionSetupInstallmentOptionsMap, + amount = amount, + shopperLocale = shopperLocale + ) assertEquals(expectedInstallmentParams, actualInstallmentParams) } @Test fun `when session setup installment option is card based then installment params should be the same `() { - val sessionSetupInstallmentOptionsMap = mapOf( - CardType.VISA.txVariant to SessionInstallmentOptionsParams( - plans = listOf(INSTALLMENT_PLAN), - preselectedValue = 2, - values = listOf(2) - ) + val sessionSetupInstallmentOptionsMap = SessionInstallmentConfiguration( + installmentOptions = mapOf( + CardType.VISA.txVariant to SessionInstallmentOptionsParams( + plans = listOf(INSTALLMENT_PLAN), + preselectedValue = 2, + values = listOf(2) + ) + ), + showInstallmentAmount = showInstallmentAmount ) val expectedInstallmentParams = InstallmentParams( cardBasedOptions = listOf( @@ -57,10 +76,17 @@ internal class InstallmentParamsMapperTest { includeRevolving = false, cardBrand = CardBrand(CardType.VISA) ) - ) + ), + amount = amount, + shopperLocale = shopperLocale, + showInstallmentAmount = showInstallmentAmount ) - val actualInstallmentParams = installmentsParamsMapper.mapToInstallmentParams(sessionSetupInstallmentOptionsMap) + val actualInstallmentParams = installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = sessionSetupInstallmentOptionsMap, + amount = amount, + shopperLocale = shopperLocale + ) assertEquals(expectedInstallmentParams, actualInstallmentParams) } @@ -71,17 +97,25 @@ internal class InstallmentParamsMapperTest { defaultOptions = InstallmentOptions.DefaultInstallmentOptions( values = listOf(2), includeRevolving = false - ) + ), + showInstallmentAmount = showInstallmentAmount ) val expectedInstallmentParams = InstallmentParams( defaultOptions = InstallmentOptionParams.DefaultInstallmentOptions( values = listOf(2), includeRevolving = false - ) + ), + amount = amount, + shopperLocale = shopperLocale, + showInstallmentAmount = showInstallmentAmount ) - val actualInstallmentParams = installmentsParamsMapper.mapToInstallmentParams(installmentConfiguration) + val actualInstallmentParams = installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = installmentConfiguration, + amount = amount, + shopperLocale = shopperLocale + ) assertEquals(expectedInstallmentParams, actualInstallmentParams) } @@ -95,7 +129,8 @@ internal class InstallmentParamsMapperTest { includeRevolving = false, cardBrand = CardBrand(CardType.VISA) ) - ) + ), + showInstallmentAmount = showInstallmentAmount ) val expectedInstallmentParams = InstallmentParams( @@ -105,10 +140,17 @@ internal class InstallmentParamsMapperTest { includeRevolving = false, cardBrand = CardBrand(CardType.VISA) ) - ) + ), + amount = amount, + shopperLocale = shopperLocale, + showInstallmentAmount = showInstallmentAmount ) - val actualInstallmentParams = installmentsParamsMapper.mapToInstallmentParams(installmentConfiguration) + val actualInstallmentParams = installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = installmentConfiguration, + amount = amount, + shopperLocale = shopperLocale + ) assertEquals(expectedInstallmentParams, actualInstallmentParams) } diff --git a/card/src/test/java/com/adyen/checkout/card/internal/util/CardValidationUtilsTest.kt b/card/src/test/java/com/adyen/checkout/card/internal/util/CardValidationUtilsTest.kt index 02b4b3c6fc..c2c1f1d68d 100644 --- a/card/src/test/java/com/adyen/checkout/card/internal/util/CardValidationUtilsTest.kt +++ b/card/src/test/java/com/adyen/checkout/card/internal/util/CardValidationUtilsTest.kt @@ -14,6 +14,7 @@ import com.adyen.checkout.card.R import com.adyen.checkout.card.internal.data.model.Brand import com.adyen.checkout.card.internal.data.model.DetectedCardType import com.adyen.checkout.card.internal.ui.model.ExpiryDate +import com.adyen.checkout.card.internal.ui.model.InputFieldUIState import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import org.junit.jupiter.api.Assertions.assertEquals @@ -353,42 +354,51 @@ internal class CardValidationUtilsTest { @Test fun `cvc is empty then result should be invalid`() { val cvc = "" - val actual = CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType()) + val actual = + CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType(), InputFieldUIState.REQUIRED) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @Test fun `cvc is 1 digit then result should be invalid`() { val cvc = "7" - val actual = CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType()) + val actual = + CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType(), InputFieldUIState.REQUIRED) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @Test fun `cvc is 2 digits then result should be invalid`() { val cvc = "12" - val actual = CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType()) + val actual = + CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType(), InputFieldUIState.REQUIRED) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @Test fun `cvc is 3 digits then result should be valid`() { val cvc = "737" - val actual = CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType()) + val actual = + CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType(), InputFieldUIState.REQUIRED) assertEquals(FieldState(cvc, Validation.Valid), actual) } @Test fun `cvc is 4 digits then result should be invalid`() { val cvc = "8689" - val actual = CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType()) + val actual = + CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType(), InputFieldUIState.REQUIRED) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @Test fun `cvc is 6 digits then result should be invalid`() { val cvc = "457835" - val actual = CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType()) + val actual = CardValidationUtils.validateSecurityCode( + cvc, + getDetectedCardType(), + InputFieldUIState.REQUIRED + ) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @@ -397,7 +407,8 @@ internal class CardValidationUtilsTest { val cvc = "737" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cardBrand = CardBrand(CardType.AMERICAN_EXPRESS)) + getDetectedCardType(cardBrand = CardBrand(CardType.AMERICAN_EXPRESS)), + cvcUIState = InputFieldUIState.REQUIRED ) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @@ -407,7 +418,8 @@ internal class CardValidationUtilsTest { val cvc = "8689" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cardBrand = CardBrand(CardType.AMERICAN_EXPRESS)) + getDetectedCardType(cardBrand = CardBrand(CardType.AMERICAN_EXPRESS)), + cvcUIState = InputFieldUIState.REQUIRED ) assertEquals(FieldState(cvc, Validation.Valid), actual) } @@ -415,7 +427,8 @@ internal class CardValidationUtilsTest { @Test fun `cvc has invalid characters then result should be invalid`() { val cvc = "1%y" - val actual = CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType()) + val actual = + CardValidationUtils.validateSecurityCode(cvc, getDetectedCardType(), InputFieldUIState.REQUIRED) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @@ -424,7 +437,8 @@ internal class CardValidationUtilsTest { val cvc = "546" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cvcPolicy = Brand.FieldPolicy.REQUIRED) + getDetectedCardType(cvcPolicy = Brand.FieldPolicy.REQUIRED), + cvcUIState = InputFieldUIState.REQUIRED ) assertEquals(FieldState(cvc, Validation.Valid), actual) } @@ -434,7 +448,8 @@ internal class CardValidationUtilsTest { val cvc = "345" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cvcPolicy = Brand.FieldPolicy.OPTIONAL) + getDetectedCardType(cvcPolicy = Brand.FieldPolicy.OPTIONAL), + cvcUIState = InputFieldUIState.OPTIONAL ) assertEquals(FieldState(cvc, Validation.Valid), actual) } @@ -444,7 +459,8 @@ internal class CardValidationUtilsTest { val cvc = "156" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cvcPolicy = Brand.FieldPolicy.HIDDEN) + getDetectedCardType(cvcPolicy = Brand.FieldPolicy.HIDDEN), + cvcUIState = InputFieldUIState.HIDDEN ) assertEquals(FieldState(cvc, Validation.Valid), actual) } @@ -454,7 +470,8 @@ internal class CardValidationUtilsTest { val cvc = "77" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cvcPolicy = Brand.FieldPolicy.REQUIRED) + getDetectedCardType(cvcPolicy = Brand.FieldPolicy.REQUIRED), + InputFieldUIState.REQUIRED ) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @@ -464,19 +481,21 @@ internal class CardValidationUtilsTest { val cvc = "9" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cvcPolicy = Brand.FieldPolicy.OPTIONAL) + getDetectedCardType(cvcPolicy = Brand.FieldPolicy.OPTIONAL), + InputFieldUIState.OPTIONAL ) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @Test - fun `cvc is invalid with field policy hidden then result should be invalid`() { + fun `cvc is invalid with field policy hidden then result should be valid`() { val cvc = "1358" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cvcPolicy = Brand.FieldPolicy.HIDDEN) + getDetectedCardType(cvcPolicy = Brand.FieldPolicy.HIDDEN), + cvcUIState = InputFieldUIState.HIDDEN ) - assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) + assertEquals(FieldState(cvc, Validation.Valid), actual) } @Test @@ -484,7 +503,8 @@ internal class CardValidationUtilsTest { val cvc = "" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cvcPolicy = Brand.FieldPolicy.REQUIRED) + getDetectedCardType(cvcPolicy = Brand.FieldPolicy.REQUIRED), + cvcUIState = InputFieldUIState.REQUIRED ) assertEquals(FieldState(cvc, Validation.Invalid(R.string.checkout_security_code_not_valid)), actual) } @@ -494,7 +514,8 @@ internal class CardValidationUtilsTest { val cvc = "" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cvcPolicy = Brand.FieldPolicy.OPTIONAL) + getDetectedCardType(cvcPolicy = Brand.FieldPolicy.OPTIONAL), + cvcUIState = InputFieldUIState.OPTIONAL ) assertEquals(FieldState(cvc, Validation.Valid), actual) } @@ -504,7 +525,8 @@ internal class CardValidationUtilsTest { val cvc = "" val actual = CardValidationUtils.validateSecurityCode( cvc, - getDetectedCardType(cvcPolicy = Brand.FieldPolicy.HIDDEN) + getDetectedCardType(cvcPolicy = Brand.FieldPolicy.HIDDEN), + cvcUIState = InputFieldUIState.HIDDEN ) assertEquals(FieldState(cvc, Validation.Valid), actual) } diff --git a/card/src/test/java/com/adyen/checkout/card/internal/util/InstallmentUtilsTest.kt b/card/src/test/java/com/adyen/checkout/card/internal/util/InstallmentUtilsTest.kt new file mode 100644 index 0000000000..2e7f632728 --- /dev/null +++ b/card/src/test/java/com/adyen/checkout/card/internal/util/InstallmentUtilsTest.kt @@ -0,0 +1,572 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 17/11/2023. + */ + +package com.adyen.checkout.card.internal.util + +import android.content.Context +import androidx.annotation.StringRes +import com.adyen.checkout.card.CardBrand +import com.adyen.checkout.card.CardType +import com.adyen.checkout.card.InstallmentConfiguration +import com.adyen.checkout.card.InstallmentOptions +import com.adyen.checkout.card.R +import com.adyen.checkout.card.internal.ui.model.InstallmentOption +import com.adyen.checkout.card.internal.ui.model.InstallmentOptionParams +import com.adyen.checkout.card.internal.ui.model.InstallmentParams +import com.adyen.checkout.card.internal.ui.view.InstallmentModel +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.internal.util.formatToLocalizedString +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.Locale + +internal class InstallmentUtilsTest { + + private val context = mock().apply { + whenever(getString(any())).thenReturn("Some text") + whenever(getString(any(), any())).thenReturn("Some text") + whenever(getString(any(), any(), any())).thenReturn("Some text") + } + + @ParameterizedTest + @MethodSource("noValidInstallmentsSourceForMakeInstallmentOptions") + fun `make installment options returns empty list, when there are no valid installment options`( + params: InstallmentParams?, + cardBrand: CardBrand?, + isCardTypeReliable: Boolean + ) { + val installmentOptions = InstallmentUtils.makeInstallmentOptions(params, cardBrand, isCardTypeReliable) + assertTrue(installmentOptions.isEmpty()) + } + + @Test + fun `make installment options returns installment models, when there are valid default installment options`() { + val installmentOptionValues = listOf(1, 3, 5, 10) + val installmentParams = InstallmentParams( + defaultOptions = InstallmentOptionParams.DefaultInstallmentOptions( + values = installmentOptionValues, + includeRevolving = false + ), + shopperLocale = Locale.US + ) + + val installmentOptions = InstallmentUtils.makeInstallmentOptions(installmentParams, null, false) + + val regularInstallmentOptions = installmentOptions.filter { model -> model.option == InstallmentOption.REGULAR } + installmentOptionValues.forEachIndexed { index, optionValue -> + assertEquals(optionValue, regularInstallmentOptions[index].numberOfInstallments) + } + } + + @Test + fun `make installment options returns installment models, when there are valid card installment options`() { + val installmentOptionValues = listOf(1, 3, 5, 10) + val cardBrand = CardBrand(CardType.MASTERCARD) + val installmentParams = InstallmentParams( + cardBasedOptions = listOf( + InstallmentOptionParams.CardBasedInstallmentOptions( + values = installmentOptionValues, + cardBrand = cardBrand, + includeRevolving = false + ), + InstallmentOptionParams.CardBasedInstallmentOptions( + values = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9), + cardBrand = cardBrand, + includeRevolving = false + ) + ), + shopperLocale = Locale.US + ) + + val installmentOptions = InstallmentUtils.makeInstallmentOptions( + installmentParams = installmentParams, + cardBrand = cardBrand, + isCardTypeReliable = true + ) + + val regularInstallmentOptions = installmentOptions.filter { model -> model.option == InstallmentOption.REGULAR } + installmentOptionValues.forEachIndexed { index, optionValue -> + assertEquals(optionValue, regularInstallmentOptions[index].numberOfInstallments) + } + } + + @Test + fun `make installment options returns one time installment model, when there are valid installment options`() { + val installmentParams = InstallmentParams( + defaultOptions = InstallmentOptionParams.DefaultInstallmentOptions( + values = listOf(1, 3, 5, 10), + includeRevolving = false + ), + shopperLocale = Locale.US + ) + + val installmentOptions = InstallmentUtils.makeInstallmentOptions(installmentParams, null, false) + + val oneTimeInstallmentOptions = + installmentOptions.filter { model -> model.option == InstallmentOption.ONE_TIME } + assertEquals(1, oneTimeInstallmentOptions.size) + } + + @Test + fun `make installment options returns revolving installment model, when is revolving`() { + val installmentParams = InstallmentParams( + defaultOptions = InstallmentOptionParams.DefaultInstallmentOptions( + values = listOf(1, 3, 5, 10), + includeRevolving = true + ), + shopperLocale = Locale.US + ) + + val installmentOptions = InstallmentUtils.makeInstallmentOptions(installmentParams, null, false) + + val revolvingInstallmentOptions = + installmentOptions.filter { model -> model.option == InstallmentOption.REVOLVING } + assertEquals(1, revolvingInstallmentOptions.size) + } + + @Test + fun `make installment options does not return revolving installment model, when is not revolving`() { + val installmentParams = InstallmentParams( + defaultOptions = InstallmentOptionParams.DefaultInstallmentOptions( + values = listOf(1, 3, 5, 10), + includeRevolving = false + ), + shopperLocale = Locale.US + ) + + val installmentOptions = InstallmentUtils.makeInstallmentOptions(installmentParams, null, false) + + val revolvingInstallmentOptions = + installmentOptions.filter { model -> model.option == InstallmentOption.REVOLVING } + assertTrue(revolvingInstallmentOptions.isEmpty()) + } + + @Test + fun `get text for installment option provides empty text, if installment option is null`() { + val installmentOptionText = InstallmentUtils.getTextForInstallmentOption(mock(), null) + assertTrue(installmentOptionText.isEmpty()) + } + + @ParameterizedTest + @MethodSource("noStringArgumentInstallmentSourceForGetTextForInstallmentOption") + fun `get text for installment option gets a string, if installment option is one time`( + installmentModel: InstallmentModel, + @StringRes textResourceId: Int + ) { + InstallmentUtils.getTextForInstallmentOption(context, installmentModel) + + verify(context).getString(textResourceId) + } + + @ParameterizedTest + @MethodSource("numberOfInstallmentsStringSourceForGetTextForInstallmentOption") + fun `get text for installment option gets a string, if installment option is regular and amount is not shown`( + installmentModel: InstallmentModel + ) { + val textResourceId = R.string.checkout_card_installments_option_regular + val formattedNumberOfInstallments = + installmentModel.numberOfInstallments?.formatToLocalizedString(installmentModel.shopperLocale) + + InstallmentUtils.getTextForInstallmentOption(context, installmentModel) + + verify(context).getString(textResourceId, formattedNumberOfInstallments) + } + + @ParameterizedTest + @MethodSource("amountShownStringSourceForGetTextForInstallmentOption") + fun `get text for installment option gets a string, if installment option is regular and amount is shown`( + installmentModel: InstallmentModel, + installmentAmount: String + ) { + val textResourceId = R.string.checkout_card_installments_option_regular_with_price + val formattedNumberOfInstallments = + installmentModel.numberOfInstallments?.formatToLocalizedString(installmentModel.shopperLocale) + + InstallmentUtils.getTextForInstallmentOption(context, installmentModel) + + verify(context).getString(textResourceId, formattedNumberOfInstallments, installmentAmount) + } + + @ParameterizedTest + @MethodSource("noValidInstallmentOptionForMakeInstallmentModelObject") + fun `make installment model object returns null, if installment option does not have valid type`( + installmentModel: InstallmentModel? + ) { + assertNull(InstallmentUtils.makeInstallmentModelObject(installmentModel)) + } + + @ParameterizedTest + @MethodSource("validInstallmentOptionForMakeInstallmentModelObject") + fun `make installment model object returns installments object, if installment option is regular`( + installmentModel: InstallmentModel + ) { + val installments = InstallmentUtils.makeInstallmentModelObject(installmentModel) + + assertEquals(installmentModel.option.type, installments?.plan) + assertEquals(installmentModel.numberOfInstallments, installments?.value) + } + + @ParameterizedTest + @MethodSource("validInstallmentOptionsForIsCardBasedOptionsValid") + fun `is card based options valid returns true`( + installmentOptions: List? + ) { + assertTrue(InstallmentUtils.isCardBasedOptionsValid(installmentOptions)) + } + + @ParameterizedTest + @MethodSource("invalidInstallmentOptionsForIsCardBasedOptionsValid") + fun `is card based options valid returns false`( + installmentOptions: List? + ) { + assertFalse(InstallmentUtils.isCardBasedOptionsValid(installmentOptions)) + } + + @ParameterizedTest + @MethodSource("validInstallmentConfigurationForAreInstallmentValuesValid") + fun `are installment values valid returns true`( + installmentConfiguration: InstallmentConfiguration + ) { + assertTrue(InstallmentUtils.areInstallmentValuesValid(installmentConfiguration)) + } + + @ParameterizedTest + @MethodSource("invalidInstallmentConfigurationForAreInstallmentValuesValid") + fun `are installment values valid returns false`( + installmentConfiguration: InstallmentConfiguration + ) { + assertFalse(InstallmentUtils.areInstallmentValuesValid(installmentConfiguration)) + } + + companion object { + @JvmStatic + fun noValidInstallmentsSourceForMakeInstallmentOptions() = listOf( + arguments(InstallmentParams(shopperLocale = Locale.US), CardBrand(CardType.MASTERCARD), true), + arguments( + InstallmentParams( + defaultOptions = InstallmentOptionParams.DefaultInstallmentOptions( + values = listOf(), + includeRevolving = true + ), + amount = Amount("EUR", 100L), + shopperLocale = Locale.US, + showInstallmentAmount = true + ), + CardBrand(CardType.VISA), + true + ), + arguments( + InstallmentParams( + defaultOptions = null, + cardBasedOptions = listOf(), + amount = Amount("EUR", 100L), + shopperLocale = Locale.US, + showInstallmentAmount = true + ), + CardBrand(CardType.VISA), + true + ), + arguments( + InstallmentParams( + cardBasedOptions = listOf( + InstallmentOptionParams.CardBasedInstallmentOptions( + values = listOf(), + includeRevolving = false, + cardBrand = CardBrand(CardType.MASTERCARD) + ) + ), + shopperLocale = Locale.US + ), + CardBrand(CardType.VISA), + true + ), + arguments( + InstallmentParams( + cardBasedOptions = listOf( + InstallmentOptionParams.CardBasedInstallmentOptions( + values = listOf(), + includeRevolving = false, + cardBrand = CardBrand(CardType.MASTERCARD) + ) + ), + shopperLocale = Locale.US + ), + CardBrand(CardType.MASTERCARD), + false + ), + arguments(null, null, false), + ) + + @JvmStatic + fun noStringArgumentInstallmentSourceForGetTextForInstallmentOption() = listOf( + arguments( + InstallmentModel( + numberOfInstallments = null, + option = InstallmentOption.ONE_TIME, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ), + R.string.checkout_card_installments_option_one_time + ), + arguments( + InstallmentModel( + numberOfInstallments = null, + option = InstallmentOption.REVOLVING, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ), + R.string.checkout_card_installments_option_revolving + ) + ) + + @JvmStatic + fun numberOfInstallmentsStringSourceForGetTextForInstallmentOption() = listOf( + arguments( + InstallmentModel( + numberOfInstallments = 2, + option = InstallmentOption.REGULAR, + amount = Amount("USD", 100L), + shopperLocale = Locale.US, + showAmount = false + ) + ), + arguments( + InstallmentModel( + numberOfInstallments = 2, + option = InstallmentOption.REGULAR, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ) + ) + ) + + @JvmStatic + fun amountShownStringSourceForGetTextForInstallmentOption() = listOf( + arguments( + InstallmentModel( + numberOfInstallments = 2, + option = InstallmentOption.REGULAR, + amount = Amount("USD", 10000L), + shopperLocale = Locale.US, + showAmount = true + ), + "$50.00" + ), + arguments( + InstallmentModel( + numberOfInstallments = 3, + option = InstallmentOption.REGULAR, + amount = Amount("USD", 10000L), + shopperLocale = Locale.US, + showAmount = true + ), + "$33.33" + ), + arguments( + InstallmentModel( + numberOfInstallments = 4, + option = InstallmentOption.REGULAR, + amount = Amount("USD", 10000L), + shopperLocale = Locale.US, + showAmount = true + ), + "$25.00" + ) + ) + + @JvmStatic + fun noValidInstallmentOptionForMakeInstallmentModelObject() = listOf( + arguments(null), + arguments( + InstallmentModel( + numberOfInstallments = null, + option = InstallmentOption.ONE_TIME, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ) + ) + ) + + @JvmStatic + fun validInstallmentOptionForMakeInstallmentModelObject() = listOf( + arguments( + InstallmentModel( + numberOfInstallments = null, + option = InstallmentOption.REGULAR, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ) + ), + arguments( + InstallmentModel( + numberOfInstallments = null, + option = InstallmentOption.REVOLVING, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ) + ) + ) + + @JvmStatic + fun validInstallmentOptionsForIsCardBasedOptionsValid() = listOf( + arguments(null), + arguments( + listOf( + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.MASTERCARD)), + ) + ), + arguments( + listOf( + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.MASTERCARD)), + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.VISA)), + ) + ), + arguments( + listOf( + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.MASTERCARD)), + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.VISA)), + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.AMERICAN_EXPRESS)), + ) + ) + ) + + @JvmStatic + fun invalidInstallmentOptionsForIsCardBasedOptionsValid() = listOf( + arguments( + listOf( + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.MASTERCARD)), + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.MASTERCARD)), + ) + ), + arguments( + listOf( + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.VISA)), + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.MASTERCARD)), + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.VISA)), + InstallmentOptions.CardBasedInstallmentOptions(10, false, CardBrand(CardType.AMERICAN_EXPRESS)), + ) + ) + ) + + @JvmStatic + fun validInstallmentConfigurationForAreInstallmentValuesValid() = listOf( + arguments(InstallmentConfiguration()), + arguments( + InstallmentConfiguration( + defaultOptions = InstallmentOptions.DefaultInstallmentOptions( + values = listOf(2, 6, 10), + includeRevolving = false + ) + ) + ), + arguments( + InstallmentConfiguration( + cardBasedOptions = listOf( + InstallmentOptions.CardBasedInstallmentOptions( + values = listOf(2, 6, 10), + includeRevolving = false, + cardBrand = CardBrand(CardType.MASTERCARD) + ), + InstallmentOptions.CardBasedInstallmentOptions( + values = listOf(2, 6), + includeRevolving = false, + cardBrand = CardBrand(CardType.VISA) + ) + ) + ) + ), + arguments( + InstallmentConfiguration( + defaultOptions = InstallmentOptions.DefaultInstallmentOptions( + values = listOf(2, 6), + includeRevolving = false + ), + cardBasedOptions = listOf( + InstallmentOptions.CardBasedInstallmentOptions( + values = listOf(2, 6), + includeRevolving = false, + cardBrand = CardBrand(CardType.MASTERCARD) + ) + ) + ) + ) + ) + + @JvmStatic + fun invalidInstallmentConfigurationForAreInstallmentValuesValid() = listOf( + arguments( + mock().apply { + whenever(defaultOptions).thenReturn( + InstallmentOptions.DefaultInstallmentOptions( + values = listOf(0), + includeRevolving = false + ) + ) + } + ), + arguments( + mock().apply { + whenever(defaultOptions).thenReturn( + InstallmentOptions.DefaultInstallmentOptions( + values = listOf(1), + includeRevolving = false + ) + ) + } + ), + arguments( + mock().apply { + whenever(cardBasedOptions).thenReturn( + listOf( + InstallmentOptions.CardBasedInstallmentOptions( + values = listOf(1), + includeRevolving = false, + cardBrand = CardBrand(CardType.MASTERCARD) + ) + ) + ) + } + ), + arguments( + mock().apply { + whenever(defaultOptions).thenReturn( + InstallmentOptions.DefaultInstallmentOptions( + values = listOf(3, 4), + includeRevolving = false + ) + ) + whenever(cardBasedOptions).thenReturn( + listOf( + InstallmentOptions.CardBasedInstallmentOptions( + values = listOf(2, 3, 1), + includeRevolving = false, + cardBrand = CardBrand(CardType.MASTERCARD) + ) + ) + ) + } + ) + ) + } +} diff --git a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapperTest.kt b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapperTest.kt index 52509fed20..2f42582785 100644 --- a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapperTest.kt +++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapperTest.kt @@ -138,7 +138,7 @@ internal class CashAppPayComponentParamsMapperTest { configuration = cardConfiguration, sessionParams = SessionParams( enableStoreDetails = sessionsValue, - installmentOptions = null, + installmentConfiguration = null, amount = null, returnUrl = TEST_RETURN_URL, ), @@ -173,7 +173,7 @@ internal class CashAppPayComponentParamsMapperTest { cardConfiguration, sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = TEST_RETURN_URL, ), diff --git a/components-compose/build.gradle b/components-compose/build.gradle index 6bf81b06ee..fe541590e6 100644 --- a/components-compose/build.gradle +++ b/components-compose/build.gradle @@ -36,13 +36,6 @@ android { compose true } - kotlinOptions { - freeCompilerArgs += [ - '-P', - 'plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=1.9.10' - ] - } - composeOptions { kotlinCompilerExtensionVersion = compose_compiler_version } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodsApiResponse.kt b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodsApiResponse.kt index 291ceebbee..8db09e5207 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodsApiResponse.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodsApiResponse.kt @@ -20,7 +20,7 @@ import org.json.JSONObject * Use [PaymentMethodsApiResponse.SERIALIZER] to deserialize this class from your JSON response. */ @Parcelize -class PaymentMethodsApiResponse( +data class PaymentMethodsApiResponse( var storedPaymentMethods: List? = null, var paymentMethods: List? = null, ) : ModelObject() { diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/BaseConfigurationBuilder.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/BaseConfigurationBuilder.kt index d6cef5a123..ff7e09e989 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/BaseConfigurationBuilder.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/BaseConfigurationBuilder.kt @@ -10,6 +10,7 @@ import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.internal.util.LocaleUtil import java.util.Locale +@Suppress("ktlint:standard:discouraged-comment-location", "ktlint:standard:type-parameter-list-spacing") abstract class BaseConfigurationBuilder< ConfigurationT : Configuration, BuilderT : BaseConfigurationBuilder diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapper.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapper.kt index df21b258e4..138dcd8c7d 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapper.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapper.kt @@ -30,9 +30,9 @@ class AnalyticsMapper { sessionId: String?, ): AnalyticsSetupRequest { return AnalyticsSetupRequest( - version = BuildConfig.CHECKOUT_VERSION, + version = actualVersion, channel = ANDROID_CHANNEL, - platform = ANDROID_PLATFORM, + platform = actualPlatform, locale = locale.toString(), component = getComponentQueryParameter(source), flavor = getFlavorQueryParameter(source), @@ -43,7 +43,8 @@ class AnalyticsMapper { screenWidth = screenWidth, paymentMethods = paymentMethods, amount = amount, - containerWidth = null, // unused for Android, + // unused for Android + containerWidth = null, sessionId = sessionId, ) } @@ -73,7 +74,26 @@ class AnalyticsMapper { companion object { private const val DROP_IN_COMPONENT = "dropin" - private const val ANDROID_PLATFORM = "android" private const val ANDROID_CHANNEL = "android" + + // these params are prefixed with actual because cross platform SDKs will override them so they are not + // technically constants + private var actualPlatform = AnalyticsPlatform.ANDROID.value + private var actualVersion = BuildConfig.CHECKOUT_VERSION + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun overrideForCrossPlatform( + platform: AnalyticsPlatform, + version: String, + ) { + this.actualPlatform = platform.value + this.actualVersion = version + } + + @VisibleForTesting + internal fun resetToDefaults() { + actualPlatform = AnalyticsPlatform.ANDROID.value + actualVersion = BuildConfig.CHECKOUT_VERSION + } } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsPlatform.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsPlatform.kt new file mode 100644 index 0000000000..4063fbed36 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsPlatform.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 9/11/2023. + */ + +package com.adyen.checkout.components.core.internal.data.api + +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +enum class AnalyticsPlatform(val value: String) { + ANDROID("android"), + FLUTTER("flutter"), + REACT_NATIVE("react-native"), +} diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/FieldState.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/FieldState.kt index d1f93046ed..3beb920fad 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/FieldState.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/FieldState.kt @@ -11,7 +11,7 @@ package com.adyen.checkout.components.core.internal.ui.model import androidx.annotation.RestrictTo @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -data class FieldState ( +data class FieldState( val value: T, val validation: Validation ) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionInstallmentConfiguration.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionInstallmentConfiguration.kt new file mode 100644 index 0000000000..0dee0c9934 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionInstallmentConfiguration.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 15/11/2023. + */ + +package com.adyen.checkout.components.core.internal.ui.model + +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class SessionInstallmentConfiguration( + val installmentOptions: Map?, + val showInstallmentAmount: Boolean? +) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionParams.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionParams.kt index 2ac257dc31..8370d89d39 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionParams.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionParams.kt @@ -14,7 +14,7 @@ import com.adyen.checkout.components.core.Amount @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class SessionParams( val enableStoreDetails: Boolean?, - val installmentOptions: Map?, + val installmentConfiguration: SessionInstallmentConfiguration?, val amount: Amount?, val returnUrl: String?, ) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/NumberExtension.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/NumberExtension.kt new file mode 100644 index 0000000000..5b92a81319 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/NumberExtension.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 21/11/2023. + */ + +package com.adyen.checkout.components.core.internal.util + +import androidx.annotation.RestrictTo +import java.text.NumberFormat +import java.util.Locale + +/** + * Format the [Int] to be displayed to the user based on the Locale. + * + * @param locale The locale the number will be formatted with. + * @return A formatted string displaying value. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +fun Int.formatToLocalizedString(locale: Locale): String = NumberFormat.getInstance(locale).format(this) diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapperTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapperTest.kt index 2a9670e933..a860fd0db7 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapperTest.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapperTest.kt @@ -15,15 +15,24 @@ import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.data.model.AnalyticsSetupRequest import com.adyen.checkout.components.core.internal.data.model.AnalyticsSource import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.junit.jupiter.MockitoExtension import java.util.Locale +@ExtendWith(MockitoExtension::class) internal class AnalyticsMapperTest { private val analyticsMapper: AnalyticsMapper = AnalyticsMapper() + @BeforeEach + fun beforeEach() { + AnalyticsMapper.resetToDefaults() + } + @Nested @DisplayName("when getFlavorQueryParameter is called and") inner class GetFlavorQueryParameterTest { @@ -110,7 +119,7 @@ internal class AnalyticsMapperTest { ) val expected = AnalyticsSetupRequest( - version = "5.0.1", + version = "5.1.0", channel = "android", platform = "android", locale = "en_US", @@ -130,4 +139,41 @@ internal class AnalyticsMapperTest { assertEquals(expected.toString(), actual.toString()) } } + + @Test + fun `when cross platform parameters are overridden, then returned values should match expected`() { + AnalyticsMapper.overrideForCrossPlatform(AnalyticsPlatform.FLUTTER, "some test version") + val actual = analyticsMapper.getAnalyticsSetupRequest( + packageName = "PACKAGE_NAME", + locale = Locale("en", "US"), + source = AnalyticsSource.PaymentComponent( + isCreatedByDropIn = false, + PaymentMethod(type = "PAYMENT_METHOD_TYPE") + ), + amount = Amount("USD", 1337), + screenWidth = 1286, + paymentMethods = listOf("scheme", "googlepay"), + sessionId = "SESSION_ID", + ) + + val expected = AnalyticsSetupRequest( + version = "some test version", + channel = "android", + platform = "flutter", + locale = "en_US", + component = "PAYMENT_METHOD_TYPE", + flavor = "components", + deviceBrand = "null", + deviceModel = "null", + referrer = "PACKAGE_NAME", + systemVersion = Build.VERSION.SDK_INT.toString(), + containerWidth = null, + screenWidth = 1286, + paymentMethods = listOf("scheme", "googlepay"), + amount = Amount("USD", 1337), + sessionId = "SESSION_ID", + ) + + assertEquals(expected.toString(), actual.toString()) + } } diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapperTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapperTest.kt index 1f5914bc77..3936168dca 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapperTest.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapperTest.kt @@ -82,7 +82,7 @@ internal class ButtonComponentParamsMapperTest { buttonConfiguration, sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", ) diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapperTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapperTest.kt index 9bcc37b004..a5b38fa79f 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapperTest.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapperTest.kt @@ -93,7 +93,7 @@ internal class GenericComponentParamsMapperTest { testConfiguration, sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", ) diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/CurrencyUtilsTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/CurrencyUtilsTest.kt new file mode 100644 index 0000000000..3e75e411cb --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/CurrencyUtilsTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 21/11/2023. + */ + +package com.adyen.checkout.components.core.internal.util + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.core.exception.CheckoutException +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.jupiter.api.assertDoesNotThrow +import java.util.Locale + +internal class CurrencyUtilsTest { + + @Test + fun `format amount with nl-NL locale`() { + val amount = Amount("EUR", 1075L) + val locale = Locale.forLanguageTag("nl-NL") + + val formattedAmount = CurrencyUtils.formatAmount(amount, locale) + + assertEquals("€ 10,75", formattedAmount) + } + + @Test + fun `format amount with ar-LB locale`() { + val amount = Amount("LBP", 10050L) + val locale = Locale.forLanguageTag("ar-LB") + + val formattedAmount = CurrencyUtils.formatAmount(amount, locale) + + assertEquals("ل.ل.\u200F ١٠٠٫٥٠", formattedAmount) + } + + @Test + fun `format amount with en-US locale`() { + val amount = Amount("USD", 220000L) + val locale = Locale.forLanguageTag("en-US") + + val formattedAmount = CurrencyUtils.formatAmount(amount, locale) + + assertEquals("$2,200.00", formattedAmount) + } + + @Test + fun `assert currency does not throw an exception, if currency code is supported`() { + val currencyCode = "EUR" + + assertDoesNotThrow { CurrencyUtils.assertCurrency(currencyCode) } + } + + @Test + fun `assert currency throws exception, if currency code is not supported`() { + val currencyCode = "AAA" + + val thrown = assertThrows(CheckoutException::class.java) { CurrencyUtils.assertCurrency(currencyCode) } + + assertEquals("Currency $currencyCode not supported", thrown.message) + } +} diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/NumberExtensionTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/NumberExtensionTest.kt new file mode 100644 index 0000000000..3fa2fa9180 --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/NumberExtensionTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 21/11/2023. + */ + +package com.adyen.checkout.components.core.internal.util + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.Locale + +internal class NumberExtensionTest { + + @Test + fun `integer format to localized string formats with en-US locale`() { + val locale = Locale.forLanguageTag("en-US") + + assertEquals("1", 1.formatToLocalizedString(locale)) + assertEquals("5", 5.formatToLocalizedString(locale)) + assertEquals("10", 10.formatToLocalizedString(locale)) + assertEquals("15", 15.formatToLocalizedString(locale)) + } + + @Test + fun `integer format to localized string formats with ar-LB locale`() { + val locale = Locale.forLanguageTag("ar-LB") + + assertEquals("١", 1.formatToLocalizedString(locale)) + assertEquals("٥", 5.formatToLocalizedString(locale)) + assertEquals("١٠", 10.formatToLocalizedString(locale)) + assertEquals("١٥", 15.formatToLocalizedString(locale)) + } +} diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index b38822b2a8..df9d166c28 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -16,6 +16,10 @@ naming: style: active: true + MagicNumber: + active: true + ignorePropertyDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true MaxLineLength: active: true maxLineLength: 120 @@ -25,6 +29,10 @@ style: excludeRawStrings: true ignoreAnnotated: - Test + UnusedPrivateMember: + active: true + ignoreAnnotated: + - Preview formatting: active: true diff --git a/config/gradle/ktlint.gradle b/config/gradle/ktlint.gradle index 66cda5abd2..33b1d735a4 100644 --- a/config/gradle/ktlint.gradle +++ b/config/gradle/ktlint.gradle @@ -17,7 +17,7 @@ configurations { } dependencies { - ktlint "com.pinterest:ktlint:$ktlint_version" + ktlint "com.pinterest.ktlint:ktlint-cli:$ktlint_version" } task ktlint(type: JavaExec) { diff --git a/cse/src/main/java/com/adyen/checkout/cse/internal/ClientSideEncrypter.kt b/cse/src/main/java/com/adyen/checkout/cse/internal/ClientSideEncrypter.kt index f5aa2be473..272daac7fa 100644 --- a/cse/src/main/java/com/adyen/checkout/cse/internal/ClientSideEncrypter.kt +++ b/cse/src/main/java/com/adyen/checkout/cse/internal/ClientSideEncrypter.kt @@ -60,8 +60,8 @@ class ClientSideEncrypter { throw EncryptionException("RSA KeyFactory not found.", e) } val pubKeySpec = RSAPublicKeySpec( - BigInteger(keyComponents[1].lowercase(Locale.getDefault()), radix), - BigInteger(keyComponents[0].lowercase(Locale.getDefault()), radix) + BigInteger(keyComponents[1].lowercase(Locale.getDefault()), RADIX), + BigInteger(keyComponents[0].lowercase(Locale.getDefault()), RADIX) ) val pubKey: PublicKey = try { keyFactory.generatePublic(pubKeySpec) @@ -127,7 +127,7 @@ class ClientSideEncrypter { } catch (e: NoSuchAlgorithmException) { throw EncryptionException("Unable to get AES algorithm", e) } - keyGenerator.init(keySize) + keyGenerator.init(KEY_SIZE) return keyGenerator.generateKey() } @@ -138,7 +138,7 @@ class ClientSideEncrypter { */ private fun generateIV(secureRandom: SecureRandom): ByteArray { // generate random IV AES is always 16bytes, but in CCM mode this represents the NONCE - val iv = ByteArray(ivSize) + val iv = ByteArray(IV_SIZE) secureRandom.nextBytes(iv) return iv } @@ -148,8 +148,8 @@ class ClientSideEncrypter { private const val VERSION = "0_1_1" private const val SEPARATOR = "$" - private const val keySize = 256 - private const val ivSize = 12 - private const val radix = 16 + private const val KEY_SIZE = 256 + private const val IV_SIZE = 12 + private const val RADIX = 16 } } diff --git a/dependencies.gradle b/dependencies.gradle index a8ac4251d1..108cb75f97 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -16,34 +16,35 @@ ext { // just for example app, don't need to increment version_code = 1 // The version_name format is "major.minor.patch(-(alpha|beta|rc)[0-9]{2}){0,1}" (e.g. 3.0.0, 3.1.1-alpha04 or 3.1.4-rc01 etc). - version_name = "5.0.1" + version_name = "5.1.0" // Build Script - android_gradle_plugin_version = '8.1.1' + android_gradle_plugin_version = '8.1.2' kotlin_version = '1.9.10' - detekt_gradle_plugin_version = "1.23.1" - dokka_version = "1.9.0" + detekt_gradle_plugin_version = "1.23.3" + dokka_version = "1.9.10" hilt_version = "2.48.1" compose_compiler_version = '1.5.3' // Code quality - detekt_version = "1.23.1" - ktlint_version = '0.50.0' + detekt_version = "1.23.3" + ktlint_version = '1.0.1' // Android Dependencies annotation_version = "1.7.0" appcompat_version = "1.6.1" browser_version = "1.6.0" coroutines_version = "1.6.4" - fragment_version = "1.6.1" + fragment_version = "1.6.2" lifecycle_version = "2.5.1" - material_version = "1.9.0" - recyclerview_version = "1.3.1" + material_version = "1.10.0" + recyclerview_version = "1.3.2" constraintlayout_version = '2.1.4' // Compose Dependencies - compose_activity_version = '1.7.2' - compose_bom_version = '2023.09.01' + compose_activity_version = '1.8.0' + compose_bom_version = '2023.10.01' + compose_hilt_version = '1.1.0' compose_viewmodel_version = '2.6.2' // Adyen Dependencies @@ -51,7 +52,7 @@ ext { // External Dependencies cash_app_pay_version = '2.3.0' - okhttp_version = "4.11.0" + okhttp_version = "4.12.0" play_services_wallet_version = '19.2.1' wechat_pay_version = "6.8.0" @@ -59,18 +60,18 @@ ext { leak_canary_version = '2.12' moshi_adapters_version = '1.14.0' moshi_kotlin_adapter_version = '1.14.0' - okhttp_logging_version = "4.11.0" + okhttp_logging_version = "4.12.0" preference_version = "1.2.1" retrofit2_version = '2.9.0' // Tests arch_core_testing_version = "2.2.0" espresso_version = "3.5.0" - json_version = '20230618' + json_version = '20231013' junit_jupiter_version = "5.9.1" mockito_kotlin_version = "4.1.0" mockito_version = "4.9.0" - robolectric_version = "4.10.3" + robolectric_version = "4.11.1" test_ext_version = "1.1.4" test_rules_version = "1.5.0" turbine_version = "0.12.1" @@ -95,6 +96,13 @@ ext { compose : [ activity : "androidx.activity:activity-compose:$compose_activity_version", bom : "androidx.compose:compose-bom:$compose_bom_version", + hilt : "androidx.hilt:hilt-navigation-compose:$compose_hilt_version", + material : 'androidx.compose.material3:material3', + ui : [ + 'androidx.compose.ui:ui', + 'androidx.compose.ui:ui-graphics', + 'androidx.compose.ui:ui-tooling-preview', + ], viewmodel: "androidx.lifecycle:lifecycle-viewmodel-compose:$compose_viewmodel_version" ], hilt : "com.google.dagger:hilt-android:$hilt_version", diff --git a/drop-in-compose/build.gradle b/drop-in-compose/build.gradle index dc92564e30..6cefd510b4 100644 --- a/drop-in-compose/build.gradle +++ b/drop-in-compose/build.gradle @@ -36,13 +36,6 @@ android { compose true } - kotlinOptions { - freeCompilerArgs += [ - '-P', - 'plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=1.9.10' - ] - } - composeOptions { kotlinCompilerExtensionVersion = compose_compiler_version } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/BaseDropInServiceContract.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/BaseDropInServiceContract.kt index 7c364b5ac2..4783a7fa27 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/BaseDropInServiceContract.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/BaseDropInServiceContract.kt @@ -86,7 +86,7 @@ interface BaseDropInServiceContract { fun sendRecurringResult(result: RecurringDropInServiceResult) /** - * Gets the additional data that was set when starting drop-in using + * Gets the additional data that was set when starting Drop-in using * [DropInConfiguration.Builder.setAdditionalDataForDropInService] or null if nothing was set. */ fun getAdditionalData(): Bundle? diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt index 6244c8fb33..677c9c85b3 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt @@ -27,8 +27,10 @@ import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPConfiguration import com.adyen.checkout.core.Environment import com.adyen.checkout.dotpay.DotpayConfiguration import com.adyen.checkout.dropin.DropInConfiguration.Builder +import com.adyen.checkout.dropin.internal.ui.model.DropInPaymentMethodInformation import com.adyen.checkout.entercash.EntercashConfiguration import com.adyen.checkout.eps.EPSConfiguration +import com.adyen.checkout.giftcard.GiftCardConfiguration import com.adyen.checkout.googlepay.GooglePayConfiguration import com.adyen.checkout.ideal.IdealConfiguration import com.adyen.checkout.mbway.MBWayConfiguration @@ -66,6 +68,7 @@ class DropInConfiguration private constructor( val skipListWhenSinglePaymentMethod: Boolean, val isRemovingStoredPaymentMethodsEnabled: Boolean, val additionalDataForDropInService: Bundle?, + internal val overriddenPaymentMethodInformation: HashMap, ) : Configuration { internal fun getConfigurationForPaymentMethod(paymentMethod: String): T? { @@ -84,6 +87,7 @@ class DropInConfiguration private constructor( ActionHandlingPaymentMethodConfigurationBuilder { private val availablePaymentConfigs = HashMap() + private val overriddenPaymentMethodInformation = HashMap() private var showPreselectedStoredPaymentMethod: Boolean = true private var skipListWhenSinglePaymentMethod: Boolean = false @@ -376,6 +380,28 @@ class DropInConfiguration private constructor( return this } + /** + * Add configuration for gift card payment method. + */ + fun addGiftCardConfiguration(giftCardConfiguration: GiftCardConfiguration): Builder { + availablePaymentConfigs[PaymentMethodTypes.GIFTCARD] = giftCardConfiguration + return this + } + + /** + * Provide a custom name to be shown in Drop-in for payment methods with a type matching [paymentMethodType]. + * For [paymentMethodType] you can pass [PaymentMethodTypes] or any other custom value. + * + * This function can be called multiple times to set custom names for payment methods with different types. + * + * @param paymentMethodType The type of the payment method. + * @param name The name to be displayed. + */ + fun overridePaymentMethodName(paymentMethodType: String, name: String): Builder { + overriddenPaymentMethodInformation[paymentMethodType] = DropInPaymentMethodInformation(name) + return this + } + override fun buildInternal(): DropInConfiguration { return DropInConfiguration( shopperLocale = shopperLocale, @@ -389,6 +415,7 @@ class DropInConfiguration private constructor( skipListWhenSinglePaymentMethod = skipListWhenSinglePaymentMethod, isRemovingStoredPaymentMethodsEnabled = isRemovingStoredPaymentMethodsEnabled, additionalDataForDropInService = additionalDataForDropInService, + overriddenPaymentMethodInformation = overriddenPaymentMethodInformation, ) } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/ActionComponentDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/ActionComponentDialogFragment.kt index a83b0ffa44..151130e303 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/ActionComponentDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/ActionComponentDialogFragment.kt @@ -73,6 +73,10 @@ internal class ActionComponentDialogFragment : return binding.root } + override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).apply { + window?.setWindowAnimations(R.style.AdyenCheckout_BottomSheet_NoWindowEnterDialogAnimation) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Logger.d(TAG, "onViewCreated") @@ -84,7 +88,7 @@ internal class ActionComponentDialogFragment : actionComponent = GenericActionComponentProvider(componentParams).get( fragment = this, configuration = actionConfiguration, - callback = this + callback = this, ) actionComponent.setOnRedirectListener { protocol.onRedirect() } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BaseComponentDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BaseComponentDialogFragment.kt index fa9297085c..0984e20207 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BaseComponentDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BaseComponentDialogFragment.kt @@ -42,7 +42,7 @@ internal abstract class BaseComponentDialogFragment : var paymentMethod: PaymentMethod = PaymentMethod() var storedPaymentMethod: StoredPaymentMethod = StoredPaymentMethod() lateinit var component: PaymentComponent - private var isStoredPayment = false + protected var isStoredPayment = false private var navigatedFromPreselected = false open class BaseCompanion(private var classes: Class) { diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/CardComponentDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/CardComponentDialogFragment.kt index fd11afc381..4e290db11f 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/CardComponentDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/CardComponentDialogFragment.kt @@ -13,7 +13,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.adyen.checkout.card.CardComponent -import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.core.internal.util.LogUtil import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.dropin.databinding.FragmentCardComponentBinding @@ -35,9 +34,11 @@ internal class CardComponentDialogFragment : BaseComponentDialogFragment() { super.onViewCreated(view, savedInstanceState) Logger.d(TAG, "onViewCreated") - // try to get the name from the payment methods response - binding.header.text = dropInViewModel.getPaymentMethods() - .find { it.type == PaymentMethodTypes.SCHEME }?.name + binding.header.text = if (isStoredPayment) { + storedPaymentMethod.name + } else { + paymentMethod.name + } cardComponent.setOnBinValueListener(protocol::onBinValue) cardComponent.setOnBinLookupListener(protocol::onBinLookup) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactory.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactory.kt index b00eef5814..0faff2ce20 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactory.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactory.kt @@ -24,6 +24,8 @@ import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.util.screenWidthPixels import com.adyen.checkout.core.internal.data.api.HttpClientFactory import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.dropin.internal.ui.model.DropInPaymentMethodInformation +import com.adyen.checkout.dropin.internal.ui.model.overrideInformation internal class DropInViewModelFactory( activity: ComponentActivity @@ -36,6 +38,8 @@ internal class DropInViewModelFactory( val bundleHandler = DropInSavedStateHandleContainer(handle) val dropInConfiguration: DropInConfiguration = requireNotNull(bundleHandler.dropInConfiguration) + bundleHandler.overridePaymentMethodInformation(dropInConfiguration.overriddenPaymentMethodInformation) + val amount: Amount? = bundleHandler.amount val paymentMethods = bundleHandler.paymentMethodsApiResponse?.paymentMethods?.mapNotNull { it.type }.orEmpty() val session = bundleHandler.sessionDetails @@ -64,3 +68,16 @@ internal class DropInViewModelFactory( return DropInViewModel(bundleHandler, orderStatusRepository, analyticsRepository) as T } } + +internal fun DropInSavedStateHandleContainer.overridePaymentMethodInformation( + paymentMethodInformationMap: Map +) { + paymentMethodInformationMap.forEach { informationEntry -> + val type = informationEntry.key + val paymentMethodInformation = informationEntry.value + + paymentMethodsApiResponse?.paymentMethods + ?.filter { paymentMethod -> paymentMethod.type == type } + ?.forEach { paymentMethod -> paymentMethod.overrideInformation(paymentMethodInformation) } + } +} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInPaymentMethodInformation.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInPaymentMethodInformation.kt new file mode 100644 index 0000000000..29d74f44a5 --- /dev/null +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInPaymentMethodInformation.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 9/11/2023. + */ + +package com.adyen.checkout.dropin.internal.ui.model + +import android.os.Parcelable +import com.adyen.checkout.components.core.PaymentMethod +import kotlinx.parcelize.Parcelize + +@Parcelize +internal data class DropInPaymentMethodInformation( + val name: String +) : Parcelable + +internal fun PaymentMethod.overrideInformation(information: DropInPaymentMethodInformation) { + name = information.name +} diff --git a/drop-in/src/test/java/com/adyen/checkout/internal/ConfigurationProvider.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt similarity index 98% rename from drop-in/src/test/java/com/adyen/checkout/internal/ConfigurationProvider.kt rename to drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt index 23a83311db..e15189725d 100644 --- a/drop-in/src/test/java/com/adyen/checkout/internal/ConfigurationProvider.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt @@ -6,7 +6,7 @@ * Created by atef on 28/10/2022. */ -package com.adyen.checkout.internal +package com.adyen.checkout.dropin.internal import com.adyen.checkout.bcmc.BcmcConfiguration import com.adyen.checkout.card.CardConfiguration diff --git a/drop-in/src/test/java/com/adyen/checkout/internal/DataProvider.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/DataProvider.kt similarity index 98% rename from drop-in/src/test/java/com/adyen/checkout/internal/DataProvider.kt rename to drop-in/src/test/java/com/adyen/checkout/dropin/internal/DataProvider.kt index 276ae0d465..0f7203756f 100644 --- a/drop-in/src/test/java/com/adyen/checkout/internal/DataProvider.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/DataProvider.kt @@ -6,7 +6,7 @@ * Created by atef on 28/10/2022. */ -package com.adyen.checkout.internal +package com.adyen.checkout.dropin.internal import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.PaymentMethodsApiResponse diff --git a/drop-in/src/test/java/com/adyen/checkout/internal/Helpers.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/Helpers.kt similarity index 97% rename from drop-in/src/test/java/com/adyen/checkout/internal/Helpers.kt rename to drop-in/src/test/java/com/adyen/checkout/dropin/internal/Helpers.kt index d36d9137e3..17865b61c3 100644 --- a/drop-in/src/test/java/com/adyen/checkout/internal/Helpers.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/Helpers.kt @@ -6,7 +6,7 @@ * Created by atef on 28/10/2022. */ -package com.adyen.checkout.internal +package com.adyen.checkout.dropin.internal import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.PaymentMethodTypes diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactoryTest.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactoryTest.kt new file mode 100644 index 0000000000..962c7da22e --- /dev/null +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactoryTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 9/11/2023. + */ + +package com.adyen.checkout.dropin.internal.ui + +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.PaymentMethodsApiResponse +import com.adyen.checkout.components.core.StoredPaymentMethod +import com.adyen.checkout.dropin.internal.ui.model.DropInPaymentMethodInformation +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.mockito.kotlin.mock + +internal class DropInViewModelFactoryTest { + + @Test + fun `when overriding payment information for a payment method by type, updates the correct payment methods`() { + val bundleHandler = DropInSavedStateHandleContainer(mock()).apply { + paymentMethodsApiResponse = generatePaymentMethodsApiResponse() + } + val paymentMethodInformationMap = hashMapOf( + Pair("testType1", DropInPaymentMethodInformation("custom payment method")) + ) + + bundleHandler.overridePaymentMethodInformation(paymentMethodInformationMap) + + bundleHandler.paymentMethodsApiResponse?.apply { + paymentMethods?.filter { paymentMethod -> + paymentMethod.type == "testType1" + }?.forEach { paymentMethod -> + assertEquals( + "custom payment method", + paymentMethod.name + ) + } + paymentMethods?.filter { paymentMethod -> + paymentMethod.type != "testType1" + }?.forEach { paymentMethod -> + assertNotEquals( + "custom payment method", + paymentMethod.name + ) + } + storedPaymentMethods?.forEach { storedPaymentMethod -> + assertNotEquals( + "custom payment method", + storedPaymentMethod.name + ) + } + } + } + + @Test + fun `when overriding payment information for a payment method by non existing type, does not update any payment method`() { + val bundleHandler = DropInSavedStateHandleContainer(mock()).apply { + paymentMethodsApiResponse = generatePaymentMethodsApiResponse() + } + val paymentMethodInformationMap = hashMapOf( + Pair("nonExistingType", DropInPaymentMethodInformation("custom payment method")) + ) + + bundleHandler.overridePaymentMethodInformation(paymentMethodInformationMap) + + bundleHandler.paymentMethodsApiResponse?.apply { + paymentMethods?.forEach { paymentMethod -> + assertNotEquals( + "custom payment method", + paymentMethod.name + ) + } + storedPaymentMethods?.forEach { storedPaymentMethod -> + assertNotEquals( + "custom payment method", + storedPaymentMethod.name + ) + } + } + } + + private fun generatePaymentMethodsApiResponse() = PaymentMethodsApiResponse( + paymentMethods = listOf( + PaymentMethod(type = "testType1", name = "paymentMethod1"), + PaymentMethod(type = "testType1", name = "paymentMethod2"), + PaymentMethod(type = "testType2", name = "paymentMethod3"), + PaymentMethod(type = "testType3", name = "paymentMethod4"), + ), + storedPaymentMethods = listOf( + StoredPaymentMethod(type = "testType1", name = "savedPaymentMethod1"), + StoredPaymentMethod(type = "testType1", name = "savedPaymentMethod2"), + StoredPaymentMethod(type = "testType2", name = "savedPaymentMethod3"), + StoredPaymentMethod(type = "testType3", name = "savedPaymentMethod4"), + ) + ) +} diff --git a/drop-in/src/test/java/com/adyen/checkout/internal/ui/PaymentMethodsListViewModelTest.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt similarity index 95% rename from drop-in/src/test/java/com/adyen/checkout/internal/ui/PaymentMethodsListViewModelTest.kt rename to drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt index 333d332159..51a02eee66 100644 --- a/drop-in/src/test/java/com/adyen/checkout/internal/ui/PaymentMethodsListViewModelTest.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt @@ -6,7 +6,7 @@ * Created by atef on 27/10/2022. */ -package com.adyen.checkout.internal.ui +package com.adyen.checkout.dropin.internal.ui import android.app.Application import androidx.arch.core.executor.testing.InstantTaskExecutorRule @@ -15,20 +15,18 @@ import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.dropin.DropInConfiguration -import com.adyen.checkout.dropin.internal.ui.PaymentMethodsListViewModel +import com.adyen.checkout.dropin.internal.ConfigurationProvider +import com.adyen.checkout.dropin.internal.DataProvider +import com.adyen.checkout.dropin.internal.Helpers.mapToPaymentMethodModelList +import com.adyen.checkout.dropin.internal.Helpers.mapToStoredPaymentMethodsModelList import com.adyen.checkout.dropin.internal.ui.model.GiftCardPaymentMethodModel import com.adyen.checkout.dropin.internal.ui.model.OrderModel import com.adyen.checkout.dropin.internal.ui.model.PaymentMethodHeader import com.adyen.checkout.dropin.internal.ui.model.PaymentMethodModel import com.adyen.checkout.dropin.internal.ui.model.PaymentMethodNote import com.adyen.checkout.dropin.internal.ui.model.StoredPaymentMethodModel -import com.adyen.checkout.internal.ConfigurationProvider -import com.adyen.checkout.internal.DataProvider -import com.adyen.checkout.internal.Helpers.mapToPaymentMethodModelList -import com.adyen.checkout.internal.Helpers.mapToStoredPaymentMethodsModelList import com.adyen.checkout.sessions.core.internal.data.model.SessionDetails import com.adyen.checkout.test.TestDispatcherExtension -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.jupiter.api.Assertions.assertEquals @@ -45,7 +43,6 @@ import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.whenever -@OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) internal class PaymentMethodsListViewModelTest( @Mock private val application: Application diff --git a/drop-in/src/test/java/com/adyen/checkout/internal/ui/PreselectedStoredPaymentViewModelTest.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModelTest.kt similarity index 96% rename from drop-in/src/test/java/com/adyen/checkout/internal/ui/PreselectedStoredPaymentViewModelTest.kt rename to drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModelTest.kt index 3339099640..3f6988e16d 100644 --- a/drop-in/src/test/java/com/adyen/checkout/internal/ui/PreselectedStoredPaymentViewModelTest.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModelTest.kt @@ -6,7 +6,7 @@ * Created by josephj on 28/12/2022. */ -package com.adyen.checkout.internal.ui +package com.adyen.checkout.dropin.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.ActionComponentData @@ -17,9 +17,6 @@ import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.dropin.DropInConfiguration -import com.adyen.checkout.dropin.internal.ui.ButtonState -import com.adyen.checkout.dropin.internal.ui.PreselectedStoredEvent -import com.adyen.checkout.dropin.internal.ui.PreselectedStoredPaymentViewModel import com.adyen.checkout.dropin.internal.ui.model.GenericStoredModel import com.adyen.checkout.test.TestDispatcherExtension import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/drop-in/src/test/java/com/adyen/checkout/internal/ui/TestComponentState.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/TestComponentState.kt similarity index 93% rename from drop-in/src/test/java/com/adyen/checkout/internal/ui/TestComponentState.kt rename to drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/TestComponentState.kt index e8dc7628a9..dcd63b619f 100644 --- a/drop-in/src/test/java/com/adyen/checkout/internal/ui/TestComponentState.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/TestComponentState.kt @@ -6,7 +6,7 @@ * Created by ozgur on 22/2/2023. */ -package com.adyen.checkout.internal.ui +package com.adyen.checkout.dropin.internal.ui import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentComponentState diff --git a/econtext/src/main/java/com/adyen/checkout/econtext/internal/provider/EContextComponentProvider.kt b/econtext/src/main/java/com/adyen/checkout/econtext/internal/provider/EContextComponentProvider.kt index 0eb3525d42..39a2110b8c 100644 --- a/econtext/src/main/java/com/adyen/checkout/econtext/internal/provider/EContextComponentProvider.kt +++ b/econtext/src/main/java/com/adyen/checkout/econtext/internal/provider/EContextComponentProvider.kt @@ -54,6 +54,7 @@ import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponen import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.ui.SubmitHandler +@Suppress("ktlint:standard:type-parameter-list-spacing") abstract class EContextComponentProvider< ComponentT : EContextComponent, ConfigurationT : EContextConfiguration, diff --git a/example-app/build.gradle b/example-app/build.gradle index c5c8a69457..61079a8e5e 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -55,14 +55,20 @@ android { } buildFeatures { + compose true viewBinding true } + + composeOptions { + kotlinCompilerExtensionVersion = compose_compiler_version + } } dependencies { // Checkout implementation project(':drop-in') -// implementation "com.adyen.checkout:drop-in:5.0.1" + implementation project(':components-compose') +// implementation "com.adyen.checkout:drop-in:5.1.0" // Dependencies implementation libraries.kotlinCoroutines @@ -72,7 +78,12 @@ dependencies { implementation libraries.androidx.constraintlayout implementation libraries.androidx.preference - debugImplementation libraries.leakCanary + implementation platform(libraries.compose.bom) + implementation libraries.compose.ui + implementation libraries.compose.activity + implementation libraries.compose.hilt + implementation libraries.compose.material + implementation libraries.compose.viewmodel implementation libraries.material @@ -83,6 +94,10 @@ dependencies { implementation libraries.hilt kapt libraries.hiltCompiler + debugImplementation libraries.leakCanary + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + // Tests testImplementation testLibraries.junit5 testImplementation testLibraries.mockito diff --git a/example-app/src/main/AndroidManifest.xml b/example-app/src/main/AndroidManifest.xml index 3d1907b53a..0ad79d28d0 100644 --- a/example-app/src/main/AndroidManifest.xml +++ b/example-app/src/main/AndroidManifest.xml @@ -86,7 +86,7 @@ android:value=".ui.main.MainActivity" /> diff --git a/example-app/src/main/java/com/adyen/checkout/example/CheckoutExampleApplication.kt b/example-app/src/main/java/com/adyen/checkout/example/CheckoutExampleApplication.kt index fb136ae95d..8dcf2b87c6 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/CheckoutExampleApplication.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/CheckoutExampleApplication.kt @@ -11,7 +11,7 @@ package com.adyen.checkout.example import android.app.Application import android.util.Log import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.example.ui.NightThemeRepository +import com.adyen.checkout.example.ui.theme.NightThemeRepository import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject @@ -21,14 +21,12 @@ class CheckoutExampleApplication : Application() { @Inject internal lateinit var nightThemeRepository: NightThemeRepository + init { + AdyenLogger.setLogLevel(Log.VERBOSE) + } + override fun onCreate() { super.onCreate() nightThemeRepository.initialize() } - - companion object { - init { - AdyenLogger.setLogLevel(Log.VERBOSE) - } - } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt index 120661852d..40fe6f11ff 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt @@ -28,10 +28,10 @@ import retrofit2.http.Query internal interface CheckoutApiService { companion object { - private const val defaultGradleUrl = "" + private const val DEFAULT_GRADLE_SERVER_URL = "" fun isRealUrlAvailable(): Boolean { - return BuildConfig.MERCHANT_SERVER_URL != defaultGradleUrl + return BuildConfig.MERCHANT_SERVER_URL != DEFAULT_GRADLE_SERVER_URL } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/SessionRequest.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/SessionRequest.kt index 683571140f..ff1855f4c7 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/SessionRequest.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/SessionRequest.kt @@ -32,5 +32,6 @@ data class SessionRequest( val allowedPaymentMethods: List?, val storePaymentMethodMode: String?, val recurringProcessingModel: String?, - val installmentOptions: Map? + val installmentOptions: Map?, + val showInstallmentAmount: Boolean ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt b/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt index 0d2d16be04..255bd15b86 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt @@ -32,9 +32,10 @@ interface KeyValueStorage { fun getCardAddressMode(): CardAddressMode fun getInstantPaymentMethodType(): String fun getInstallmentOptionsMode(): CardInstallmentOptionsMode + fun isInstallmentAmountShown(): Boolean fun useSessions(): Boolean fun setUseSessions(useSessions: Boolean) - fun getTelemetryLevel(): AnalyticsLevel + fun getAnalyticsLevel(): AnalyticsLevel } @Suppress("TooManyFunctions") @@ -150,6 +151,12 @@ internal class DefaultKeyValueStorage( ) } + override fun isInstallmentAmountShown() = sharedPreferences.getBoolean( + appContext = appContext, + stringRes = R.string.card_installment_show_amount_key, + defaultStringRes = R.string.preferences_default_installment_amount_shown, + ) + override fun useSessions(): Boolean { return sharedPreferences.getBoolean( appContext = appContext, @@ -164,12 +171,12 @@ internal class DefaultKeyValueStorage( } } - override fun getTelemetryLevel(): AnalyticsLevel { + override fun getAnalyticsLevel(): AnalyticsLevel { return AnalyticsLevel.valueOf( sharedPreferences.getString( appContext = appContext, - stringRes = R.string.telemetry_level_key, - defaultStringRes = R.string.preferences_default_telemetry_level, + stringRes = R.string.analytics_level_key, + defaultStringRes = R.string.preferences_default_analytics_level, ) ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/di/ThemeModule.kt b/example-app/src/main/java/com/adyen/checkout/example/di/ThemeModule.kt index 5e7b55b61a..a75fa749fd 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/di/ThemeModule.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/di/ThemeModule.kt @@ -8,8 +8,8 @@ package com.adyen.checkout.example.di -import com.adyen.checkout.example.ui.DefaultNightThemeRepository -import com.adyen.checkout.example.ui.NightThemeRepository +import com.adyen.checkout.example.ui.theme.DefaultNightThemeRepository +import com.adyen.checkout.example.ui.theme.NightThemeRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/example-app/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt b/example-app/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt index 83be3db1b6..f7cdf3c256 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt @@ -63,6 +63,7 @@ fun getSessionRequest( isThreeds2Enabled: Boolean, isExecuteThreeD: Boolean, installmentOptions: Map?, + showInstallmentAmount: Boolean = false, threeDSAuthenticationOnly: Boolean = false, shopperEmail: String? = null, allowedPaymentMethods: List? = null, @@ -84,12 +85,14 @@ fun getSessionRequest( lineItems = LINE_ITEMS, threeDSAuthenticationOnly = threeDSAuthenticationOnly, // TODO check if this should be kept or removed - threeDS2RequestData = null, // if (force3DS2Challenge) ThreeDS2RequestDataRequest() else null + // previous code: if (force3DS2Challenge) ThreeDS2RequestDataRequest() else null + threeDS2RequestData = null, shopperEmail = shopperEmail, allowedPaymentMethods = allowedPaymentMethods, storePaymentMethodMode = storePaymentMethodMode, recurringProcessingModel = recurringProcessingModel, - installmentOptions = installmentOptions + installmentOptions = installmentOptions, + showInstallmentAmount = showInstallmentAmount ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardActivity.kt deleted file mode 100644 index 07c18a1676..0000000000 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardActivity.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2023 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by josephj on 4/1/2023. - */ - -package com.adyen.checkout.example.ui.card - -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.widget.Toast -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.adyen.checkout.card.CardComponent -import com.adyen.checkout.components.core.action.Action -import com.adyen.checkout.example.databinding.ActivityCardBinding -import com.adyen.checkout.example.extensions.getLogTag -import com.adyen.checkout.example.ui.configuration.CheckoutConfigurationProvider -import com.adyen.checkout.redirect.RedirectComponent -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import javax.inject.Inject - -@AndroidEntryPoint -class SessionsCardActivity : AppCompatActivity() { - - @Inject - internal lateinit var checkoutConfigurationProvider: CheckoutConfigurationProvider - - private lateinit var binding: ActivityCardBinding - - private val cardViewModel: SessionsCardViewModel by viewModels() - - private var cardComponent: CardComponent? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Insert return url in extras, so we can access it in the ViewModel through SavedStateHandle - val returnUrl = RedirectComponent.getReturnUrl(applicationContext) + "/sessions/card" - intent = (intent ?: Intent()).putExtra(RETURN_URL_EXTRA, returnUrl) - - binding = ActivityCardBinding.inflate(layoutInflater) - setContentView(binding.root) - - setSupportActionBar(binding.toolbar) - supportActionBar?.setDisplayShowTitleEnabled(false) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { cardViewModel.sessionsCardComponentDataFlow.collect(::setupCardView) } - launch { cardViewModel.cardViewState.collect(::onCardViewState) } - launch { cardViewModel.events.collect(::onCardEvent) } - } - } - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - - val data = intent.data - if (data != null && data.toString().startsWith(RedirectComponent.REDIRECT_RESULT_SCHEME)) { - cardComponent?.handleIntent(intent) - } - } - - private fun onCardViewState(cardViewState: CardViewState) { - when (cardViewState) { - CardViewState.Loading -> { - // We are hiding the CardView here to display our own loading state. If you leave the view visible - // the built in loading state will be shown. - binding.progressIndicator.isVisible = true - binding.cardContainer.isVisible = false - binding.errorView.isVisible = false - } - - is CardViewState.ShowComponent -> { - binding.cardContainer.isVisible = true - binding.progressIndicator.isVisible = false - binding.errorView.isVisible = false - } - - CardViewState.Error -> { - binding.errorView.isVisible = true - binding.progressIndicator.isVisible = false - binding.cardContainer.isVisible = false - } - } - } - - private fun setupCardView(sessionsCardComponentData: SessionsCardComponentData) { - val cardComponent = CardComponent.PROVIDER.get( - activity = this, - checkoutSession = sessionsCardComponentData.checkoutSession, - paymentMethod = sessionsCardComponentData.paymentMethod, - configuration = checkoutConfigurationProvider.getCardConfiguration(), - componentCallback = sessionsCardComponentData.callback, - ) - - cardComponent.setOnRedirectListener { - Log.d(TAG, "On redirect") - } - - this.cardComponent = cardComponent - - binding.cardView.attach(cardComponent, this) - } - - private fun onCardEvent(event: CardEvent) { - when (event) { - is CardEvent.PaymentResult -> onPaymentResult(event.result) - is CardEvent.AdditionalAction -> onAction(event.action) - } - } - - private fun onAction(action: Action) { - cardComponent?.handleAction(action, this) - } - - private fun onPaymentResult(result: String) { - Toast.makeText(this, result, Toast.LENGTH_SHORT).show() - finish() - } - - override fun onDestroy() { - super.onDestroy() - cardComponent = null - } - - companion object { - private val TAG = getLogTag() - internal const val RETURN_URL_EXTRA = "RETURN_URL_EXTRA" - } -} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverViewModel.kt index 23837213d6..04ecdcb2e9 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverViewModel.kt @@ -114,7 +114,8 @@ internal class SessionsCardTakenOverViewModel @Inject constructor( ?: error("Return url should be set"), shopperEmail = keyValueStorage.getShopperEmail(), allowedPaymentMethods = listOf(paymentMethodType), - installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()) + installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown() ) ) ?: return null diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.kt new file mode 100644 index 0000000000..26981c1eac --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 30/11/2023. + */ + +package com.adyen.checkout.example.ui.card + +import androidx.compose.runtime.Immutable +import com.adyen.checkout.card.CardConfiguration +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.example.ui.compose.ResultState + +@Immutable +internal data class SessionsCardUiState( + val cardConfiguration: CardConfiguration, + val isLoading: Boolean = false, + val oneTimeMessage: String? = null, + val componentData: SessionsCardComponentData? = null, + val action: Action? = null, + val finalResult: ResultState? = null, +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt index df7df890ad..aea2968943 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt @@ -17,11 +17,14 @@ import com.adyen.checkout.card.CardConfiguration import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.core.exception.CancellationException import com.adyen.checkout.example.data.storage.KeyValueStorage import com.adyen.checkout.example.extensions.getLogTag import com.adyen.checkout.example.repositories.PaymentsRepository import com.adyen.checkout.example.service.getSessionRequest import com.adyen.checkout.example.service.getSettingsInstallmentOptionsMode +import com.adyen.checkout.example.ui.card.compose.SessionsCardActivity +import com.adyen.checkout.example.ui.compose.ResultState import com.adyen.checkout.example.ui.configuration.CheckoutConfigurationProvider import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.CheckoutSessionProvider @@ -30,10 +33,10 @@ import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.SessionModel import com.adyen.checkout.sessions.core.SessionPaymentResult import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -46,44 +49,37 @@ internal class SessionsCardViewModel @Inject constructor( checkoutConfigurationProvider: CheckoutConfigurationProvider, ) : ViewModel(), SessionComponentCallback { - private val _sessionsCardComponentDataFlow = MutableStateFlow(null) - val sessionsCardComponentDataFlow: Flow = _sessionsCardComponentDataFlow.filterNotNull() - - private val _cardViewState = MutableStateFlow(CardViewState.Loading) - val cardViewState: Flow = _cardViewState - - private val _events = MutableSharedFlow() - val events: Flow = _events - private val cardConfiguration = checkoutConfigurationProvider.getCardConfiguration() + private val _uiState = MutableStateFlow(SessionsCardUiState(cardConfiguration)) + val uiState: StateFlow = _uiState.asStateFlow() + init { viewModelScope.launch { launchComponent() } } private suspend fun launchComponent() { + updateUiState { it.copy(isLoading = true) } val paymentMethodType = PaymentMethodTypes.SCHEME val checkoutSession = getSession(paymentMethodType) if (checkoutSession == null) { Log.e(TAG, "Failed to fetch session") - _cardViewState.emit(CardViewState.Error) + onError("Failed to fetch session") return } val paymentMethod = checkoutSession.getPaymentMethod(paymentMethodType) if (paymentMethod == null) { Log.e(TAG, "Session does not contain SCHEME payment method") - _cardViewState.emit(CardViewState.Error) + onError("Payment method is null") return } - _sessionsCardComponentDataFlow.emit( - SessionsCardComponentData( - checkoutSession = checkoutSession, - paymentMethod = paymentMethod, - callback = this - ) + val componentData = SessionsCardComponentData( + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + callback = this, ) - _cardViewState.emit(CardViewState.ShowComponent) + updateUiState { it.copy(componentData = componentData, isLoading = false) } } private suspend fun getSession(paymentMethodType: String): CheckoutSession? { @@ -101,8 +97,9 @@ internal class SessionsCardViewModel @Inject constructor( ?: error("Return url should be set"), shopperEmail = keyValueStorage.getShopperEmail(), allowedPaymentMethods = listOf(paymentMethodType), - installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()) - ) + installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), + ), ) ?: return null return getCheckoutSession(sessionModel, cardConfiguration) @@ -119,30 +116,57 @@ internal class SessionsCardViewModel @Inject constructor( } override fun onAction(action: Action) { - viewModelScope.launch { _events.emit(CardEvent.AdditionalAction(action)) } + updateUiState { it.copy(action = action) } } override fun onError(componentError: ComponentError) { - onComponentError(componentError) + if (componentError.exception is CancellationException) { + updateUiState { + it.copy( + oneTimeMessage = "Payment in progress was cancelled", + finalResult = ResultState.FAILURE, + ) + } + } else { + onError(componentError.errorMessage) + } + } + + private fun onError(message: String) { + updateUiState { it.copy(oneTimeMessage = "Error: $message") } } override fun onFinished(result: SessionPaymentResult) { - viewModelScope.launch { _events.emit(CardEvent.PaymentResult(result.resultCode.orEmpty())) } + updateUiState { + it.copy( + oneTimeMessage = "Finished: ${result.resultCode}", + finalResult = getFinalResultState(result), + ) + } } - private fun onComponentError(error: ComponentError) { - viewModelScope.launch { _events.emit(CardEvent.PaymentResult("Failed: ${error.errorMessage}")) } + private fun getFinalResultState(result: SessionPaymentResult): ResultState = when (result.resultCode) { + "Authorised" -> ResultState.SUCCESS + "Pending", + "Received" -> ResultState.PENDING + + else -> ResultState.FAILURE } override fun onLoading(isLoading: Boolean) { - val state = if (isLoading) { - Log.d(TAG, "Show loading") - CardViewState.Loading - } else { - Log.d(TAG, "Don't show loading") - CardViewState.ShowComponent - } - _cardViewState.tryEmit(state) + updateUiState { it.copy(isLoading = isLoading) } + } + + fun oneTimeMessageConsumed() { + updateUiState { it.copy(oneTimeMessage = null) } + } + + fun actionConsumed() { + updateUiState { it.copy(action = null) } + } + + private fun updateUiState(block: (SessionsCardUiState) -> SessionsCardUiState) { + _uiState.update(block) } companion object { diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardActivity.kt new file mode 100644 index 0000000000..64c7bcf984 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardActivity.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 29/11/2023. + */ + +package com.adyen.checkout.example.ui.card.compose + +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.core.view.WindowCompat +import com.adyen.checkout.example.ui.theme.ExampleTheme +import com.adyen.checkout.example.ui.theme.NightTheme +import com.adyen.checkout.example.ui.theme.NightThemeRepository +import com.adyen.checkout.redirect.RedirectComponent +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class SessionsCardActivity : AppCompatActivity() { + + @Inject + internal lateinit var nightThemeRepository: NightThemeRepository + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Helps to resize the view port when the keyboard is displayed. + WindowCompat.setDecorFitsSystemWindows(window, false) + + // Insert return url in extras, so we can access it in the ViewModel through SavedStateHandle + val returnUrl = RedirectComponent.getReturnUrl(applicationContext) + "/sessions/card" + intent = (intent ?: Intent()).putExtra(RETURN_URL_EXTRA, returnUrl) + + setContent { + val useDarkTheme = when (nightThemeRepository.theme) { + NightTheme.DAY -> false + NightTheme.NIGHT -> true + NightTheme.SYSTEM -> isSystemInDarkTheme() + } + ExampleTheme(useDarkTheme) { + SessionsCardScreen(onBackPressed = { onBackPressedDispatcher.onBackPressed() }) + } + } + } + + companion object { + internal const val RETURN_URL_EXTRA = "RETURN_URL_EXTRA" + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardScreen.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardScreen.kt new file mode 100644 index 0000000000..1c433aea44 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardScreen.kt @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 29/11/2023. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.adyen.checkout.example.ui.card.compose + +import android.app.Activity +import android.widget.Toast +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +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.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.card.CardConfiguration +import com.adyen.checkout.components.compose.AdyenComponent +import com.adyen.checkout.components.compose.get +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.example.ui.card.SessionsCardComponentData +import com.adyen.checkout.example.ui.card.SessionsCardUiState +import com.adyen.checkout.example.ui.card.SessionsCardViewModel +import com.adyen.checkout.example.ui.compose.ResultContent + +@Composable +internal fun SessionsCardScreen( + onBackPressed: () -> Unit, + viewModel: SessionsCardViewModel = hiltViewModel(), +) { + Scaffold( + modifier = Modifier.windowInsetsPadding(WindowInsets.ime), + topBar = { + TopAppBar( + title = { Text(text = "Card component with sessions") }, + navigationIcon = { + IconButton(onClick = onBackPressed) { + Icon(Icons.Filled.ArrowBack, contentDescription = "Back") + } + }, + ) + }, + ) { innerPadding -> + val uiState by viewModel.uiState.collectAsState() + SessionsCardContent( + uiState = uiState, + onOneTimeMessageConsumed = viewModel::oneTimeMessageConsumed, + onActionConsumed = viewModel::actionConsumed, + modifier = Modifier.padding(innerPadding), + ) + } +} + +@Suppress("DestructuringDeclarationWithTooManyEntries") +@Composable +private fun SessionsCardContent( + uiState: SessionsCardUiState, + onOneTimeMessageConsumed: () -> Unit, + onActionConsumed: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + val (cardConfiguration, isLoading, oneTimeMessage, componentData, action, finalResult) = uiState + + if (isLoading) { + CircularProgressIndicator() + } + + if (oneTimeMessage != null) { + val context = LocalContext.current + LaunchedEffect(oneTimeMessage) { + Toast.makeText(context, oneTimeMessage, Toast.LENGTH_SHORT).show() + onOneTimeMessageConsumed() + } + } + + if (finalResult != null) { + ResultContent(finalResult) + } else if (componentData != null) { + CardComponent( + configuration = cardConfiguration, + componentData = componentData, + action = action, + onActionConsumed = onActionConsumed, + modifier = Modifier.fillMaxSize(), + ) + } + } +} + +@Composable +private fun CardComponent( + configuration: CardConfiguration, + componentData: SessionsCardComponentData, + action: Action?, + onActionConsumed: () -> Unit, + modifier: Modifier = Modifier, +) { + val component = CardComponent.PROVIDER.get( + componentData.checkoutSession, + componentData.paymentMethod, + configuration, + componentData.callback, + componentData.hashCode().toString(), + ) + + // Enables vertical scrolling when the CardView becomes too long. + Column(modifier.verticalScroll(rememberScrollState())) { + AdyenComponent( + component, + modifier, + ) + } + + if (action != null) { + val activity = LocalContext.current as Activity + LaunchedEffect(action) { + component.handleAction(action, activity) + onActionConsumed() + } + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/compose/ResultContent.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/compose/ResultContent.kt new file mode 100644 index 0000000000..c3676375b6 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/compose/ResultContent.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 30/11/2023. + */ + +package com.adyen.checkout.example.ui.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.adyen.checkout.example.ui.theme.ExampleTheme + +@Composable +internal fun ResultContent( + resultState: ResultState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(ExampleTheme.dimensions.grid_2), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + val tint = when (resultState) { + ResultState.SUCCESS -> ExampleTheme.customColors.success + ResultState.PENDING -> ExampleTheme.customColors.warning + ResultState.FAILURE -> MaterialTheme.colorScheme.error + } + Icon( + painter = painterResource(id = resultState.drawable), + contentDescription = null, + tint = tint, + modifier = Modifier.size(100.dp), + ) + Spacer(modifier = Modifier.height(ExampleTheme.dimensions.grid_2)) + Text(text = resultState.text, style = MaterialTheme.typography.displaySmall) + } +} + +@Preview(showBackground = true) +@Composable +private fun ResultContentPreview() { + ResultContent(ResultState.SUCCESS) +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/compose/ResultState.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/compose/ResultState.kt new file mode 100644 index 0000000000..2debb57da3 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/compose/ResultState.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 30/11/2023. + */ + +package com.adyen.checkout.example.ui.compose + +import com.adyen.checkout.example.R + +enum class ResultState( + val drawable: Int, + val text: String, +) { + SUCCESS(R.drawable.ic_result_success, "Payment successful!"), + PENDING(R.drawable.ic_result_pending, "Payment pending..."), + FAILURE(R.drawable.ic_result_failure, "Payment failed..."), +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt index 9fcaa629d7..50202b5b94 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt @@ -62,6 +62,7 @@ internal class CheckoutConfigurationProvider @Inject constructor( .addGooglePayConfiguration(getGooglePayConfiguration()) .add3ds2ActionConfiguration(get3DS2Configuration()) .addRedirectActionConfiguration(getRedirectConfiguration()) + .addGiftCardConfiguration(getGiftCardConfiguration()) .setEnableRemovingStoredPaymentMethods(true) .setAmount(amount) .setAnalyticsConfiguration(getAnalyticsConfiguration()) @@ -105,6 +106,7 @@ internal class CheckoutConfigurationProvider @Inject constructor( GiftCardConfiguration.Builder(shopperLocale, environment, clientKey) .setAmount(amount) .setAnalyticsConfiguration(getAnalyticsConfiguration()) + .setPinRequired(true) .build() private fun getAddressConfiguration(): AddressConfiguration = when (keyValueStorage.getCardAddressMode()) { @@ -147,7 +149,7 @@ internal class CheckoutConfigurationProvider @Inject constructor( .build() private fun getAnalyticsConfiguration(): AnalyticsConfiguration { - val analyticsLevel = keyValueStorage.getTelemetryLevel() + val analyticsLevel = keyValueStorage.getAnalyticsLevel() return AnalyticsConfiguration(level = analyticsLevel) } @@ -163,10 +165,11 @@ internal class CheckoutConfigurationProvider @Inject constructor( maxInstallments: Int = 3, includeRevolving: Boolean = false ) = InstallmentConfiguration( - InstallmentOptions.DefaultInstallmentOptions( + defaultOptions = InstallmentOptions.DefaultInstallmentOptions( maxInstallments = maxInstallments, includeRevolving = includeRevolving - ) + ), + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown() ) private fun getCardBasedInstallmentOptions( @@ -180,6 +183,7 @@ internal class CheckoutConfigurationProvider @Inject constructor( includeRevolving = includeRevolving, cardBrand = cardBrand ) - ) + ), + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown() ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt index 0ad304e01d..1605e39614 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt @@ -15,8 +15,8 @@ import androidx.preference.DropDownPreference import androidx.preference.PreferenceFragmentCompat import com.adyen.checkout.example.R import com.adyen.checkout.example.databinding.ActivitySettingsBinding -import com.adyen.checkout.example.ui.NightTheme -import com.adyen.checkout.example.ui.NightThemeRepository +import com.adyen.checkout.example.ui.theme.NightTheme +import com.adyen.checkout.example.ui.theme.NightThemeRepository import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt index cc50957579..e733e6fa24 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt @@ -30,8 +30,8 @@ import com.adyen.checkout.example.service.ExampleSessionsDropInService import com.adyen.checkout.example.ui.bacs.BacsFragment import com.adyen.checkout.example.ui.blik.BlikActivity import com.adyen.checkout.example.ui.card.CardActivity -import com.adyen.checkout.example.ui.card.SessionsCardActivity import com.adyen.checkout.example.ui.card.SessionsCardTakenOverActivity +import com.adyen.checkout.example.ui.card.compose.SessionsCardActivity import com.adyen.checkout.example.ui.configuration.ConfigurationActivity import com.adyen.checkout.example.ui.giftcard.GiftCardActivity import com.adyen.checkout.example.ui.giftcard.SessionsGiftCardActivity @@ -49,12 +49,12 @@ class MainActivity : AppCompatActivity() { private val dropInLauncher = DropIn.registerForDropInResult( this, - DropInCallback { dropInResult -> viewModel.onDropInResult(dropInResult) } + DropInCallback { dropInResult -> viewModel.onDropInResult(dropInResult) }, ) private val sessionDropInLauncher = DropIn.registerForDropInResult( this, - SessionDropInCallback { sessionDropInResult -> viewModel.onDropInResult(sessionDropInResult) } + SessionDropInCallback { sessionDropInResult -> viewModel.onDropInResult(sessionDropInResult) }, ) private var componentItemAdapter: ComponentItemAdapter? = null @@ -75,7 +75,7 @@ class MainActivity : AppCompatActivity() { binding.switchSessions.setOnCheckedChangeListener { _, isChecked -> viewModel.onSessionsToggled(isChecked) } componentItemAdapter = ComponentItemAdapter( - viewModel::onComponentEntryClick + viewModel::onComponentEntryClick, ) binding.componentList.adapter = componentItemAdapter @@ -162,7 +162,7 @@ class MainActivity : AppCompatActivity() { sessionDropInLauncher, navigation.checkoutSession, navigation.dropInConfiguration, - ExampleSessionsDropInService::class.java + ExampleSessionsDropInService::class.java, ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt index 0309c9322a..001f068f0d 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt @@ -44,8 +44,8 @@ internal class MainViewModel @Inject constructor( private val checkoutConfigurationProvider: CheckoutConfigurationProvider, ) : ViewModel() { - private val _useSessions: MutableStateFlow = MutableStateFlow(keyValueStorage.useSessions()) - private val _showLoading: MutableStateFlow = MutableStateFlow(false) + private val useSessions: MutableStateFlow = MutableStateFlow(keyValueStorage.useSessions()) + private val showLoading: MutableStateFlow = MutableStateFlow(false) private val _mainViewState: MutableStateFlow = MutableStateFlow(getViewState()) val mainViewState: Flow = _mainViewState @@ -54,11 +54,11 @@ internal class MainViewModel @Inject constructor( val eventFlow: Flow = _eventFlow init { - _useSessions.onEach { + useSessions.onEach { loadViewState() }.launchIn(viewModelScope) - _showLoading.onEach { + showLoading.onEach { loadViewState() }.launchIn(viewModelScope) } @@ -167,7 +167,8 @@ internal class MainViewModel @Inject constructor( redirectUrl = savedStateHandle.get(MainActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), shopperEmail = keyValueStorage.getShopperEmail(), - installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()) + installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown() ) ) ?: return null @@ -194,12 +195,12 @@ internal class MainViewModel @Inject constructor( fun onSessionsToggled(enable: Boolean) { viewModelScope.launch { keyValueStorage.setUseSessions(enable) - _useSessions.emit(enable) + useSessions.emit(enable) } } private suspend fun showLoading(loading: Boolean) { - _showLoading.emit(loading) + showLoading.emit(loading) } private suspend fun loadViewState() { @@ -207,8 +208,8 @@ internal class MainViewModel @Inject constructor( } private fun getViewState(): MainViewState { - val useSessions = _useSessions.value - val showLoading = _showLoading.value + val useSessions = useSessions.value + val showLoading = showLoading.value return MainViewState( listItems = getListItems(useSessions), useSessions = useSessions, diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/theme/Color.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/Color.kt new file mode 100644 index 0000000000..cdb38197c3 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/Color.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 30/11/2023. + */ + +package com.adyen.checkout.example.ui.theme + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +// Light theme +val md_theme_light_primary = Color(0xFF0abf53) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFF0abf53) +val md_theme_light_onPrimaryContainer = Color(0xFFFFFFFF) +val md_theme_light_secondary = Color(0xFF00112c) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFF00112c) +val md_theme_light_onSecondaryContainer = Color(0xFFFFFFFF) +val md_theme_light_tertiary = Color(0xFF00112c) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFF00112c) +val md_theme_light_onTertiaryContainer = Color(0xFFFFFFFF) +val md_theme_light_error = Color(0xFFE22D2D) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFFFFFF) +val md_theme_light_onBackground = Color(0xFF00112c) +val md_theme_light_surface = Color(0xFFFFFFFF) +val md_theme_light_onSurface = Color(0xFF00112c) +val md_theme_light_surfaceVariant = Color(0xFFFFFFFF) +val md_theme_light_onSurfaceVariant = Color(0xFF00112c) + +// Not customized for now +val md_theme_light_outline = Color(0xFF00112c) +val md_theme_light_inverseOnSurface = Color(0xFFD6F6FF) +val md_theme_light_inverseSurface = Color(0xFF00363F) +val md_theme_light_inversePrimary = Color(0xFF47E270) +val md_theme_light_surfaceTint = Color(0xFF006E2C) +val md_theme_light_outlineVariant = Color(0xFFC1C9BE) +val md_theme_light_scrim = Color(0xFF000000) + +// Custom colors +val md_theme_light_success = Color(0xFF09AB4B) +val md_theme_light_warning = Color(0xFFF7BC00) + +// Dark theme +val md_theme_dark_primary = Color(0xFF0abf53) +val md_theme_dark_onPrimary = Color(0xFFFFFFFF) +val md_theme_dark_primaryContainer = Color(0xFF0abf53) +val md_theme_dark_onPrimaryContainer = Color(0xFFFFFFFF) +val md_theme_dark_secondary = Color(0xFF00112c) +val md_theme_dark_onSecondary = Color(0xFFFFFFFF) +val md_theme_dark_secondaryContainer = Color(0xFF00112c) +val md_theme_dark_onSecondaryContainer = Color(0xFFFFFFFF) +val md_theme_dark_tertiary = Color(0xFF00112c) +val md_theme_dark_onTertiary = Color(0xFFFFFFFF) +val md_theme_dark_tertiaryContainer = Color(0xFF00112c) +val md_theme_dark_onTertiaryContainer = Color(0xFFFFFFFF) +val md_theme_dark_error = Color(0xFFF66565) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF00112c) +val md_theme_dark_onBackground = Color(0xFFFFFFFF) +val md_theme_dark_surface = Color(0xFF00112c) +val md_theme_dark_onSurface = Color(0xFFFFFFFF) +val md_theme_dark_surfaceVariant = Color(0xFF00112c) +val md_theme_dark_onSurfaceVariant = Color(0xFFFFFFFF) + +// Not customized for now +val md_theme_dark_outline = Color(0xFF8B9389) +val md_theme_dark_inverseOnSurface = Color(0xFF001F25) +val md_theme_dark_inverseSurface = Color(0xFFA6EEFF) +val md_theme_dark_inversePrimary = Color(0xFF006E2C) +val md_theme_dark_surfaceTint = Color(0xFF47E270) +val md_theme_dark_outlineVariant = Color(0xFF424940) +val md_theme_dark_scrim = Color(0xFF000000) + +// Custom colors +val md_theme_dark_success = Color(0xFF09AB4B) +val md_theme_dark_warning = Color(0xFFF7BC00) + +val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, +) + +val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, +) + +val CustomLightColors = CustomColorScheme( + success = md_theme_light_success, + warning = md_theme_light_warning, +) + +val CustomDarkColors = CustomColorScheme( + success = md_theme_dark_success, + warning = md_theme_dark_warning, +) + +@Immutable +data class CustomColorScheme( + val success: Color = Color.Unspecified, + val warning: Color = Color.Unspecified, +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/theme/Dimensions.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/Dimensions.kt new file mode 100644 index 0000000000..0686ee2f04 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/Dimensions.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 6/12/2023. + */ + +package com.adyen.checkout.example.ui.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Suppress("ConstructorParameterNaming") +@Immutable +data class Dimensions( + val grid_0_25: Dp = Dp.Unspecified, + val grid_0_5: Dp = Dp.Unspecified, + val grid_1: Dp = Dp.Unspecified, + val grid_1_5: Dp = Dp.Unspecified, + val grid_2: Dp = Dp.Unspecified, + val grid_4: Dp = Dp.Unspecified, + val grid_8: Dp = Dp.Unspecified, +) { + + constructor(gridSize: Int) : this( + grid_0_25 = (gridSize * 0.25).dp, + grid_0_5 = (gridSize * 0.5).dp, + grid_1 = gridSize.dp, + grid_1_5 = (gridSize * 1.5).dp, + grid_2 = (gridSize * 2).dp, + grid_4 = (gridSize * 4).dp, + grid_8 = (gridSize * 8).dp, + ) +} + +val DefaultDimensions = Dimensions(gridSize = 8) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/theme/ExampleTheme.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/ExampleTheme.kt new file mode 100644 index 0000000000..6a03451e1b --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/ExampleTheme.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 30/11/2023. + */ + +package com.adyen.checkout.example.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.staticCompositionLocalOf + +@Composable +fun ExampleTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (!useDarkTheme) { + LightColors + } else { + DarkColors + } + + val customColors = if (!useDarkTheme) { + CustomLightColors + } else { + CustomDarkColors + } + + val dimensions = DefaultDimensions + + CompositionLocalProvider( + LocalCustomColorScheme provides customColors, + LocalDimensions provides dimensions, + ) { + MaterialTheme( + colorScheme = colors, + content = content, + ) + } +} + +object ExampleTheme { + + val customColors: CustomColorScheme + @Composable + @ReadOnlyComposable + get() = LocalCustomColorScheme.current + + val dimensions: Dimensions + @Composable + @ReadOnlyComposable + get() = LocalDimensions.current +} + +private val LocalDimensions = staticCompositionLocalOf { Dimensions() } +private val LocalCustomColorScheme = staticCompositionLocalOf { CustomColorScheme() } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/NightThemeRepository.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/NightThemeRepository.kt similarity index 97% rename from example-app/src/main/java/com/adyen/checkout/example/ui/NightThemeRepository.kt rename to example-app/src/main/java/com/adyen/checkout/example/ui/theme/NightThemeRepository.kt index 36d834f5f5..b1e443d483 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/NightThemeRepository.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/NightThemeRepository.kt @@ -6,7 +6,7 @@ * Created by oscars on 7/10/2022. */ -package com.adyen.checkout.example.ui +package com.adyen.checkout.example.ui.theme import android.content.SharedPreferences import androidx.appcompat.app.AppCompatDelegate diff --git a/example-app/src/main/res/drawable/ic_result_failure.xml b/example-app/src/main/res/drawable/ic_result_failure.xml new file mode 100644 index 0000000000..bf77201c10 --- /dev/null +++ b/example-app/src/main/res/drawable/ic_result_failure.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/example-app/src/main/res/drawable/ic_result_pending.xml b/example-app/src/main/res/drawable/ic_result_pending.xml new file mode 100644 index 0000000000..f5d8808c3a --- /dev/null +++ b/example-app/src/main/res/drawable/ic_result_pending.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + diff --git a/example-app/src/main/res/drawable/ic_result_success.xml b/example-app/src/main/res/drawable/ic_result_success.xml new file mode 100644 index 0000000000..14c176ae4a --- /dev/null +++ b/example-app/src/main/res/drawable/ic_result_success.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/example-app/src/main/res/values/arrays.xml b/example-app/src/main/res/values/arrays.xml index 4f8637f5a4..6d7621546f 100644 --- a/example-app/src/main/res/values/arrays.xml +++ b/example-app/src/main/res/values/arrays.xml @@ -46,12 +46,12 @@ @string/night_theme_night - + "All" "None" - + "ALL" "NONE" diff --git a/example-app/src/main/res/values/strings.xml b/example-app/src/main/res/values/strings.xml index 6fa57e36ca..16fe881743 100644 --- a/example-app/src/main/res/values/strings.xml +++ b/example-app/src/main/res/values/strings.xml @@ -33,7 +33,7 @@ Merchant Account Card Other payment methods - Telemetry + Analytics App Shopper Information @@ -54,12 +54,14 @@ card_address_form_mode Installment options card_installment_options_mode + card_installment_show_amount + Show installment amount Address mode instant_payment_method_type Instant Payment Method Type use_sessions - telemetry_level - Level + analytics_level + Level Theme @@ -83,8 +85,9 @@ false NONE NONE + false wechatpaySDK true - ALL + ALL diff --git a/example-app/src/main/res/xml/preferences.xml b/example-app/src/main/res/xml/preferences.xml index 5ddba40a53..88b84188df 100644 --- a/example-app/src/main/res/xml/preferences.xml +++ b/example-app/src/main/res/xml/preferences.xml @@ -88,6 +88,11 @@ android:title="@string/card_installment_options_mode_title" app:useSimpleSummaryProvider="true" /> + + - + diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardConfiguration.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardConfiguration.kt index c7f49e8363..f8dceb1f37 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardConfiguration.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardConfiguration.kt @@ -31,6 +31,7 @@ class GiftCardConfiguration private constructor( override val analyticsConfiguration: AnalyticsConfiguration?, override val amount: Amount?, override val isSubmitButtonVisible: Boolean?, + val isPinRequired: Boolean?, internal val genericActionConfiguration: GenericActionConfiguration, ) : Configuration, ButtonConfiguration { @@ -41,6 +42,7 @@ class GiftCardConfiguration private constructor( ActionHandlingPaymentMethodConfigurationBuilder, ButtonConfigurationBuilder { + private var isPinRequired: Boolean? = null private var isSubmitButtonVisible: Boolean? = null /** @@ -81,6 +83,19 @@ class GiftCardConfiguration private constructor( return this } + /** + * Set if the PIN field should be hidden from the Component and not requested to the shopper. + * Note that this might have implications for the transaction. + * + * Default is true. + * + * @param isPinRequired If PIN should be hidden or not. + */ + fun setPinRequired(isPinRequired: Boolean): Builder { + this.isPinRequired = isPinRequired + return this + } + override fun buildInternal(): GiftCardConfiguration { return GiftCardConfiguration( shopperLocale = shopperLocale, @@ -89,6 +104,7 @@ class GiftCardConfiguration private constructor( analyticsConfiguration = analyticsConfiguration, amount = amount, isSubmitButtonVisible = isSubmitButtonVisible, + isPinRequired = isPinRequired, genericActionConfiguration = genericActionConfigurationBuilder.build(), ) } diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt index e240308d15..346dcc54b4 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt @@ -26,7 +26,6 @@ import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepo import com.adyen.checkout.components.core.internal.data.api.DefaultPublicKeyRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyService import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.ComponentParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.components.core.internal.util.get @@ -46,6 +45,7 @@ import com.adyen.checkout.giftcard.internal.GiftCardComponentEventHandler import com.adyen.checkout.giftcard.internal.SessionsGiftCardComponentCallbackWrapper import com.adyen.checkout.giftcard.internal.SessionsGiftCardComponentEventHandler import com.adyen.checkout.giftcard.internal.ui.DefaultGiftCardDelegate +import com.adyen.checkout.giftcard.internal.ui.model.GiftCardComponentParamsMapper import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.internal.SessionInteractor import com.adyen.checkout.sessions.core.internal.SessionSavedStateHandleContainer @@ -75,7 +75,7 @@ constructor( SessionsGiftCardComponentCallback > { - private val componentParamsMapper = ButtonComponentParamsMapper(overrideComponentParams, overrideSessionParams) + private val componentParamsMapper = GiftCardComponentParamsMapper(overrideComponentParams, overrideSessionParams) override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt index 3ac8d96e87..73b6f200d5 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt @@ -21,7 +21,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyRepository -import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.FieldState +import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.GiftCardPaymentMethod import com.adyen.checkout.core.exception.CheckoutException @@ -35,10 +36,13 @@ import com.adyen.checkout.cse.internal.BaseCardEncrypter import com.adyen.checkout.giftcard.GiftCardAction import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.giftcard.GiftCardException +import com.adyen.checkout.giftcard.internal.ui.model.GiftCardComponentParams import com.adyen.checkout.giftcard.internal.ui.model.GiftCardInputData import com.adyen.checkout.giftcard.internal.ui.model.GiftCardOutputData import com.adyen.checkout.giftcard.internal.util.GiftCardBalanceStatus import com.adyen.checkout.giftcard.internal.util.GiftCardBalanceUtils +import com.adyen.checkout.giftcard.internal.util.GiftCardNumberUtils +import com.adyen.checkout.giftcard.internal.util.GiftCardPinUtils import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIEvent @@ -58,7 +62,7 @@ internal class DefaultGiftCardDelegate( private val order: OrderRequest?, private val analyticsRepository: AnalyticsRepository, private val publicKeyRepository: PublicKeyRepository, - override val componentParams: ButtonComponentParams, + override val componentParams: GiftCardComponentParams, private val cardEncrypter: BaseCardEncrypter, private val submitHandler: SubmitHandler, ) : GiftCardDelegate { @@ -154,7 +158,18 @@ internal class DefaultGiftCardDelegate( updateComponentState(outputData) } - private fun createOutputData() = GiftCardOutputData(cardNumber = inputData.cardNumber, pin = inputData.pin) + private fun createOutputData() = GiftCardOutputData( + numberFieldState = GiftCardNumberUtils.validateInputField(inputData.cardNumber), + pinFieldState = getPinFieldState(inputData.pin), + ) + + private fun getPinFieldState(pin: String): FieldState { + return if (isPinRequired()) { + GiftCardPinUtils.validateInputField(pin) + } else { + FieldState(pin, Validation.Valid) + } + } @VisibleForTesting internal fun updateComponentState(outputData: GiftCardOutputData) { @@ -200,7 +215,7 @@ internal class DefaultGiftCardDelegate( brand = paymentMethod.brand, ) - val lastDigits = outputData.giftcardNumberFieldState.value.takeLast(LAST_DIGITS_LENGTH) + val lastDigits = outputData.numberFieldState.value.takeLast(LAST_DIGITS_LENGTH) val paymentComponentData = PaymentComponentData( paymentMethod = giftCardPaymentMethod, @@ -226,11 +241,13 @@ internal class DefaultGiftCardDelegate( outputData: GiftCardOutputData, publicKey: String, ): EncryptedCard? = try { - val unencryptedCard = UnencryptedCard - .Builder() - .setNumber(outputData.giftcardNumberFieldState.value) - .setCvc(outputData.giftcardPinFieldState.value) - .build() + val unencryptedCard = UnencryptedCard.Builder().run { + setNumber(outputData.numberFieldState.value) + if (componentParams.isPinRequired) { + setCvc(outputData.pinFieldState.value) + } + build() + } cardEncrypter.encryptFields(unencryptedCard, publicKey) } catch (e: EncryptionException) { @@ -325,6 +342,8 @@ internal class DefaultGiftCardDelegate( submitHandler.onSubmit(updatedState) } + override fun isPinRequired(): Boolean = componentParams.isPinRequired + override fun onCleared() { removeObserver() } diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/GiftCardDelegate.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/GiftCardDelegate.kt index f3d7a46e9d..a460fd12a3 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/GiftCardDelegate.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/GiftCardDelegate.kt @@ -41,4 +41,6 @@ internal interface GiftCardDelegate : fun resolveBalanceResult(balanceResult: BalanceResult) fun resolveOrderResponse(orderResponse: OrderResponse) + + fun isPinRequired(): Boolean } diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParams.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt similarity index 73% rename from bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParams.kt rename to giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt index a888d5a2bb..4bfbaa3b0b 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParams.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt @@ -1,12 +1,12 @@ /* - * Copyright (c) 2022 Adyen N.V. + * Copyright (c) 2023 Adyen N.V. * * This file is open source and available under the MIT license. See the LICENSE file for more info. * - * Created by josephj on 15/11/2022. + * Created by oscars on 22/11/2023. */ -package com.adyen.checkout.bcmc.internal.ui.model +package com.adyen.checkout.giftcard.internal.ui.model import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams @@ -15,7 +15,7 @@ import com.adyen.checkout.components.core.internal.ui.model.ComponentParams import com.adyen.checkout.core.Environment import java.util.Locale -internal data class BcmcComponentParams( +internal data class GiftCardComponentParams( override val shopperLocale: Locale, override val environment: Environment, override val clientKey: String, @@ -23,7 +23,5 @@ internal data class BcmcComponentParams( override val isCreatedByDropIn: Boolean, override val amount: Amount?, override val isSubmitButtonVisible: Boolean, - val isHolderNameRequired: Boolean, - val shopperReference: String?, - val isStorePaymentFieldVisible: Boolean, + val isPinRequired: Boolean, ) : ComponentParams, ButtonParams diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapper.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapper.kt new file mode 100644 index 0000000000..15b823a712 --- /dev/null +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapper.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 22/11/2023. + */ + +package com.adyen.checkout.giftcard.internal.ui.model + +import com.adyen.checkout.components.core.internal.ButtonConfiguration +import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams +import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.giftcard.GiftCardConfiguration + +internal class GiftCardComponentParamsMapper( + private val overrideComponentParams: ComponentParams?, + private val overrideSessionParams: SessionParams?, +) { + + fun mapToParams( + configuration: GiftCardConfiguration, + sessionParams: SessionParams?, + ): GiftCardComponentParams { + return configuration + .mapToParamsInternal() + .override(overrideComponentParams) + .override(sessionParams ?: overrideSessionParams) + } + + private fun GiftCardConfiguration.mapToParamsInternal(): GiftCardComponentParams { + return GiftCardComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = AnalyticsParams(analyticsConfiguration), + isCreatedByDropIn = false, + amount = amount, + isSubmitButtonVisible = (this as? ButtonConfiguration)?.isSubmitButtonVisible ?: true, + isPinRequired = isPinRequired ?: true, + ) + } + + private fun GiftCardComponentParams.override( + overrideComponentParams: ComponentParams? + ): GiftCardComponentParams { + if (overrideComponentParams == null) return this + return copy( + shopperLocale = overrideComponentParams.shopperLocale, + environment = overrideComponentParams.environment, + clientKey = overrideComponentParams.clientKey, + analyticsParams = overrideComponentParams.analyticsParams, + isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, + amount = overrideComponentParams.amount + ) + } + + private fun GiftCardComponentParams.override( + sessionParams: SessionParams? = null + ): GiftCardComponentParams { + if (sessionParams == null) return this + return copy( + amount = sessionParams.amount ?: amount, + ) + } +} diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardOutputData.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardOutputData.kt index 38aafc1d47..b7dcead7a2 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardOutputData.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardOutputData.kt @@ -10,14 +10,12 @@ package com.adyen.checkout.giftcard.internal.ui.model import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.OutputData -import com.adyen.checkout.giftcard.internal.util.GiftCardNumberUtils -import com.adyen.checkout.giftcard.internal.util.GiftCardPinUtils -internal class GiftCardOutputData(cardNumber: String, pin: String) : OutputData { - - val giftcardNumberFieldState: FieldState = GiftCardNumberUtils.validateInputField(cardNumber) - val giftcardPinFieldState: FieldState = GiftCardPinUtils.validateInputField(pin) +internal data class GiftCardOutputData( + val numberFieldState: FieldState, + val pinFieldState: FieldState, +) : OutputData { override val isValid: Boolean - get() = giftcardNumberFieldState.validation.isValid() && giftcardPinFieldState.validation.isValid() + get() = numberFieldState.validation.isValid() && pinFieldState.validation.isValid() } diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/view/GiftCardView.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/view/GiftCardView.kt index d6f9a9e030..0ba8c00417 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/view/GiftCardView.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/view/GiftCardView.kt @@ -23,6 +23,7 @@ import com.adyen.checkout.giftcard.databinding.GiftcardViewBinding import com.adyen.checkout.giftcard.internal.ui.GiftCardDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentView import com.adyen.checkout.ui.core.internal.util.hideError +import com.adyen.checkout.ui.core.internal.util.isVisible import com.adyen.checkout.ui.core.internal.util.setLocalizedHintFromStyle import com.adyen.checkout.ui.core.internal.util.showError import kotlinx.coroutines.CoroutineScope @@ -56,49 +57,53 @@ internal class GiftCardView @JvmOverloads constructor( giftCardDelegate = delegate this.localizedContext = localizedContext - initLocalizedStrings(localizedContext) - - initInputs() + initCardNumberField(localizedContext) + initPinField(localizedContext) } - private fun initLocalizedStrings(localizedContext: Context) { + private fun initCardNumberField(localizedContext: Context) { binding.textInputLayoutGiftcardNumber.setLocalizedHintFromStyle( R.style.AdyenCheckout_GiftCard_GiftCardNumberInput, localizedContext ) - binding.textInputLayoutGiftcardPin.setLocalizedHintFromStyle( - R.style.AdyenCheckout_GiftCard_GiftCardPinInput, - localizedContext - ) - } - private fun initInputs() { binding.editTextGiftcardNumber.setOnChangeListener { giftCardDelegate.updateInputData { cardNumber = binding.editTextGiftcardNumber.rawValue } binding.textInputLayoutGiftcardNumber.hideError() } binding.editTextGiftcardNumber.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean -> - val cardNumberValidation = giftCardDelegate.outputData.giftcardNumberFieldState.validation + val cardNumberValidation = giftCardDelegate.outputData.numberFieldState.validation if (hasFocus) { binding.textInputLayoutGiftcardNumber.hideError() } else if (cardNumberValidation is Validation.Invalid) { binding.textInputLayoutGiftcardNumber.showError(localizedContext.getString(cardNumberValidation.reason)) } } + } - binding.editTextGiftcardPin.setOnChangeListener { editable: Editable -> - giftCardDelegate.updateInputData { pin = editable.toString() } - binding.textInputLayoutGiftcardPin.hideError() - } + private fun initPinField(localizedContext: Context) { + if (giftCardDelegate.isPinRequired()) { + binding.textInputLayoutGiftcardPin.setLocalizedHintFromStyle( + R.style.AdyenCheckout_GiftCard_GiftCardPinInput, + localizedContext + ) - binding.editTextGiftcardPin.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> - val pinValidation = giftCardDelegate.outputData.giftcardPinFieldState.validation - if (hasFocus) { + binding.editTextGiftcardPin.setOnChangeListener { editable: Editable -> + giftCardDelegate.updateInputData { pin = editable.toString() } binding.textInputLayoutGiftcardPin.hideError() - } else if (pinValidation is Validation.Invalid) { - binding.textInputLayoutGiftcardPin.showError(localizedContext.getString(pinValidation.reason)) } + + binding.editTextGiftcardPin.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> + val pinValidation = giftCardDelegate.outputData.pinFieldState.validation + if (hasFocus) { + binding.textInputLayoutGiftcardPin.hideError() + } else if (pinValidation is Validation.Invalid) { + binding.textInputLayoutGiftcardPin.showError(localizedContext.getString(pinValidation.reason)) + } + } + } else { + binding.textInputLayoutGiftcardPin.isVisible = false } } @@ -106,13 +111,13 @@ internal class GiftCardView @JvmOverloads constructor( Logger.d(TAG, "highlightValidationErrors") val outputData = giftCardDelegate.outputData var isErrorFocused = false - val cardNumberValidation = outputData.giftcardNumberFieldState.validation + val cardNumberValidation = outputData.numberFieldState.validation if (cardNumberValidation is Validation.Invalid) { isErrorFocused = true binding.textInputLayoutGiftcardNumber.requestFocus() binding.textInputLayoutGiftcardNumber.showError(localizedContext.getString(cardNumberValidation.reason)) } - val pinValidation = outputData.giftcardPinFieldState.validation + val pinValidation = outputData.pinFieldState.validation if (pinValidation is Validation.Invalid) { if (!isErrorFocused) { binding.textInputLayoutGiftcardPin.requestFocus() diff --git a/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt b/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt index 3d78fdd9cc..466902746a 100644 --- a/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt +++ b/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt @@ -16,16 +16,19 @@ import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.test.TestPublicKeyRepository -import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper import com.adyen.checkout.core.Environment import com.adyen.checkout.cse.internal.test.TestCardEncrypter import com.adyen.checkout.giftcard.GiftCardAction import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.giftcard.GiftCardConfiguration import com.adyen.checkout.giftcard.GiftCardException +import com.adyen.checkout.giftcard.internal.ui.model.GiftCardComponentParamsMapper import com.adyen.checkout.giftcard.internal.ui.model.GiftCardOutputData import com.adyen.checkout.giftcard.internal.util.GiftCardBalanceStatus +import com.adyen.checkout.giftcard.internal.util.GiftCardNumberUtils +import com.adyen.checkout.giftcard.internal.util.GiftCardPinUtils import com.adyen.checkout.test.TestDispatcherExtension +import com.adyen.checkout.test.extensions.test import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -90,7 +93,7 @@ internal class DefaultGiftCardDelegateTest( @Test fun `public key is null, then component state should not be ready`() = runTest { delegate.componentStateFlow.test { - delegate.updateComponentState(GiftCardOutputData("5555444433330000", "737")) + delegate.updateComponentState(giftCardOutputDataWith("5555444433330000", "737")) val componentState = expectMostRecentItem() @@ -105,7 +108,7 @@ internal class DefaultGiftCardDelegateTest( delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.componentStateFlow.test { - delegate.updateComponentState(GiftCardOutputData("123", "737")) + delegate.updateComponentState(giftCardOutputDataWith("123", "737")) val componentState = expectMostRecentItem() @@ -123,7 +126,7 @@ internal class DefaultGiftCardDelegateTest( delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.componentStateFlow.test { - delegate.updateComponentState(GiftCardOutputData("5555444433330000", "737")) + delegate.updateComponentState(giftCardOutputDataWith("5555444433330000", "737")) val componentState = expectMostRecentItem() @@ -139,7 +142,7 @@ internal class DefaultGiftCardDelegateTest( delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.componentStateFlow.test { - delegate.updateComponentState(GiftCardOutputData("5555444433330000", "737")) + delegate.updateComponentState(giftCardOutputDataWith("5555444433330000", "737")) val componentState = expectMostRecentItem() @@ -373,6 +376,24 @@ internal class DefaultGiftCardDelegateTest( } } + @Test + fun `when pin is not required, then does not matter for validation`() = runTest { + delegate = createGiftCardDelegate( + configuration = getDefaultGiftCardConfigurationBuilder().setPinRequired(false).build() + ) + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + val componentStateFlow = delegate.componentStateFlow.test(testScheduler) + + delegate.updateInputData { + // Valid card number + cardNumber = "5555444433330000" + // Invalid pin + pin = "" + } + + assertTrue(componentStateFlow.latestValue.isInputValid) + } + private fun createGiftCardDelegate( configuration: GiftCardConfiguration = getDefaultGiftCardConfigurationBuilder().build(), order: OrderRequest? = TEST_ORDER @@ -381,7 +402,7 @@ internal class DefaultGiftCardDelegateTest( paymentMethod = PaymentMethod(), order = order, publicKeyRepository = publicKeyRepository, - componentParams = ButtonComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = GiftCardComponentParamsMapper(null, null).mapToParams(configuration, null), cardEncrypter = cardEncrypter, analyticsRepository = analyticsRepository, submitHandler = submitHandler, @@ -393,6 +414,11 @@ internal class DefaultGiftCardDelegateTest( TEST_CLIENT_KEY ) + private fun giftCardOutputDataWith(number: String, pin: String) = GiftCardOutputData( + numberFieldState = GiftCardNumberUtils.validateInputField(number), + pinFieldState = GiftCardPinUtils.validateInputField(pin), + ) + companion object { private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA") diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/AllowedAuthMethods.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/AllowedAuthMethods.kt new file mode 100644 index 0000000000..88a3ddf6ff --- /dev/null +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/AllowedAuthMethods.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 5/12/2023. + */ +package com.adyen.checkout.googlepay + +/** + * The authentication methods accepted by Google Pay. + */ +@Suppress("MemberVisibilityCanBePrivate") +object AllowedAuthMethods { + + const val PAN_ONLY = "PAN_ONLY" + const val CRYPTOGRAM_3DS = "CRYPTOGRAM_3DS" + + internal val allAllowedAuthMethods: List = listOf(PAN_ONLY, CRYPTOGRAM_3DS) +} diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/AllowedCardNetworks.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/AllowedCardNetworks.kt new file mode 100644 index 0000000000..350e4e0955 --- /dev/null +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/AllowedCardNetworks.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 5/12/2023. + */ +package com.adyen.checkout.googlepay + +/** + * The card networks accepted by Google Pay. + */ +@Suppress("MemberVisibilityCanBePrivate") +object AllowedCardNetworks { + + const val AMEX = "AMEX" + const val DISCOVER = "DISCOVER" + const val INTERAC = "INTERAC" + const val JCB = "JCB" + const val MASTERCARD = "MASTERCARD" + const val VISA = "VISA" + + internal val allAllowedCardNetworks: List = listOf(AMEX, DISCOVER, INTERAC, JCB, MASTERCARD, VISA) +} diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayButtonParameters.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayButtonParameters.kt new file mode 100644 index 0000000000..2e2f2a50b5 --- /dev/null +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayButtonParameters.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 5/12/2023. + */ + +package com.adyen.checkout.googlepay + +/** + * Class containing some of the parameters required to initialize the Google Pay button. + */ +data class GooglePayButtonParameters( + val allowedPaymentMethods: String, +) diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.kt index fd22c13eb6..c1fe1db0e2 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.kt @@ -78,6 +78,14 @@ class GooglePayComponent internal constructor( googlePayDelegate.startGooglePayScreen(activity, requestCode) } + /** + * Returns some of the parameters required to initialize the [Google Pay button](https://docs.adyen.com/payment-methods/google-pay/android-component/#2-show-the-google-pay-button). + */ + @Suppress("MaxLineLength") + fun getGooglePayButtonParameters(): GooglePayButtonParameters { + return googlePayDelegate.getGooglePayButtonParameters() + } + /** * Handle the result from the GooglePay screen that was started by [.startGooglePayScreen]. * diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.kt index 78f48e7eaf..7bd14475f9 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.kt @@ -164,7 +164,7 @@ class GooglePayConfiguration private constructor( } /** - * Sets the supported authentication methods. + * Sets the supported authentication methods. Check [AllowedAuthMethods] for all the possible values. * * Default is ["PAN_ONLY", "CRYPTOGRAM_3DS"]. * @@ -180,7 +180,7 @@ class GooglePayConfiguration private constructor( /** * Sets the allowed card networks. The allowed networks are automatically configured based on your account - * settings, but you can override them here. + * settings, but you can override them here. Check [AllowedCardNetworks] for all the possible values. * * Default is ["AMEX", "DISCOVER", "INTERAC", "JCB", "MASTERCARD", "VISA"]. * diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegate.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegate.kt index f1f6c38d5c..7a38258542 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegate.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegate.kt @@ -22,9 +22,12 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException +import com.adyen.checkout.core.internal.data.model.ModelUtils import com.adyen.checkout.core.internal.util.LogUtil import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.googlepay.GooglePayButtonParameters import com.adyen.checkout.googlepay.GooglePayComponentState +import com.adyen.checkout.googlepay.internal.data.model.GooglePayPaymentMethodModel import com.adyen.checkout.googlepay.internal.ui.model.GooglePayComponentParams import com.adyen.checkout.googlepay.internal.util.GooglePayUtils import com.google.android.gms.wallet.AutoResolveHelper @@ -89,7 +92,7 @@ internal class DefaultGooglePayDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -124,7 +127,7 @@ internal class DefaultGooglePayDelegate( data = paymentComponentData, isInputValid = isValid, isReady = true, - paymentData = paymentData + paymentData = paymentData, ) } @@ -163,6 +166,15 @@ internal class DefaultGooglePayDelegate( } } + override fun getGooglePayButtonParameters(): GooglePayButtonParameters { + val allowedPaymentMethodsList = GooglePayUtils.getAllowedPaymentMethods(componentParams) + val allowedPaymentMethods = ModelUtils.serializeOptList( + allowedPaymentMethodsList, + GooglePayPaymentMethodModel.SERIALIZER, + )?.toString().orEmpty() + return GooglePayButtonParameters(allowedPaymentMethods) + } + override fun getPaymentMethodType(): String { return paymentMethod.type ?: PaymentMethodTypes.UNKNOWN } diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/GooglePayDelegate.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/GooglePayDelegate.kt index 72fb373fd0..2d393b6d19 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/GooglePayDelegate.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/GooglePayDelegate.kt @@ -12,6 +12,7 @@ import android.app.Activity import android.content.Intent import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.googlepay.GooglePayButtonParameters import com.adyen.checkout.googlepay.GooglePayComponentState import kotlinx.coroutines.flow.Flow @@ -24,4 +25,6 @@ internal interface GooglePayDelegate : PaymentComponentDelegate = listOf(PAN_ONLY, CRYPTOGRAM_3DS) -} diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/AllowedCardNetworks.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/AllowedCardNetworks.kt deleted file mode 100644 index e90e2d0e4b..0000000000 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/AllowedCardNetworks.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2019 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by caiof on 17/7/2019. - */ -package com.adyen.checkout.googlepay.internal.util - -@Suppress("MemberVisibilityCanBePrivate") -internal object AllowedCardNetworks { - - const val AMEX = "AMEX" - const val DISCOVER = "DISCOVER" - const val INTERAC = "INTERAC" - const val JCB = "JCB" - const val MASTERCARD = "MASTERCARD" - const val VISA = "VISA" - - /** - * A list of the allowed credit card networks accepted on Google Pay. - * - * @return The list of all allowed card networks. - */ - val allAllowedCardNetworks: List = listOf(AMEX, DISCOVER, INTERAC, JCB, MASTERCARD, VISA) -} diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtils.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtils.kt index 559105d226..7bb0b62994 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtils.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtils.kt @@ -7,7 +7,7 @@ */ package com.adyen.checkout.googlepay.internal.util -import com.adyen.checkout.components.core.internal.util.AmountFormat.toBigDecimal +import com.adyen.checkout.components.core.internal.util.AmountFormat import com.adyen.checkout.components.core.paymentmethod.GooglePayPaymentMethod import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.internal.util.LogUtil @@ -152,37 +152,37 @@ internal object GooglePayUtils { } private fun createIsReadyToPayRequestModel(params: GooglePayComponentParams): IsReadyToPayRequestModel { - val isReadyToPayRequestModel = IsReadyToPayRequestModel() - isReadyToPayRequestModel.apiVersion = MAJOR_API_VERSION - isReadyToPayRequestModel.apiVersionMinor = MINOT_API_VERSION - isReadyToPayRequestModel.isExistingPaymentMethodRequired = params.isExistingPaymentMethodRequired - val allowedPaymentMethods = ArrayList() - allowedPaymentMethods.add(createCardPaymentMethod(params)) - isReadyToPayRequestModel.allowedPaymentMethods = allowedPaymentMethods - return isReadyToPayRequestModel + return IsReadyToPayRequestModel( + apiVersion = MAJOR_API_VERSION, + apiVersionMinor = MINOT_API_VERSION, + isExistingPaymentMethodRequired = params.isExistingPaymentMethodRequired, + allowedPaymentMethods = getAllowedPaymentMethods(params), + ) } private fun createPaymentDataRequestModel(params: GooglePayComponentParams): PaymentDataRequestModel { - val paymentDataRequestModel = PaymentDataRequestModel() - paymentDataRequestModel.apiVersion = MAJOR_API_VERSION - paymentDataRequestModel.apiVersionMinor = MINOT_API_VERSION - paymentDataRequestModel.merchantInfo = params.merchantInfo - paymentDataRequestModel.transactionInfo = createTransactionInfo(params) - val allowedPaymentMethods = ArrayList() - allowedPaymentMethods.add(createCardPaymentMethod(params)) - paymentDataRequestModel.allowedPaymentMethods = allowedPaymentMethods - paymentDataRequestModel.isEmailRequired = params.isEmailRequired - paymentDataRequestModel.isShippingAddressRequired = params.isShippingAddressRequired - paymentDataRequestModel.shippingAddressParameters = params.shippingAddressParameters - return paymentDataRequestModel + return PaymentDataRequestModel( + apiVersion = MAJOR_API_VERSION, + apiVersionMinor = MINOT_API_VERSION, + merchantInfo = params.merchantInfo, + transactionInfo = createTransactionInfo(params), + allowedPaymentMethods = getAllowedPaymentMethods(params), + isEmailRequired = params.isEmailRequired, + isShippingAddressRequired = params.isShippingAddressRequired, + shippingAddressParameters = params.shippingAddressParameters, + ) + } + + internal fun getAllowedPaymentMethods(params: GooglePayComponentParams): List { + return listOf(createCardPaymentMethod(params)) } private fun createCardPaymentMethod(params: GooglePayComponentParams): GooglePayPaymentMethodModel { - val cardPaymentMethod = GooglePayPaymentMethodModel() - cardPaymentMethod.type = PAYMENT_TYPE_CARD - cardPaymentMethod.parameters = createCardParameters(params) - cardPaymentMethod.tokenizationSpecification = createTokenizationSpecification(params) - return cardPaymentMethod + return GooglePayPaymentMethodModel( + type = PAYMENT_TYPE_CARD, + parameters = createCardParameters(params), + tokenizationSpecification = createTokenizationSpecification(params), + ) } private fun createCardParameters(params: GooglePayComponentParams): CardParameters { @@ -200,31 +200,31 @@ internal object GooglePayUtils { private fun createTokenizationSpecification( params: GooglePayComponentParams ): PaymentMethodTokenizationSpecification { - val tokenizationSpecification = PaymentMethodTokenizationSpecification() - tokenizationSpecification.type = PAYMENT_GATEWAY - tokenizationSpecification.parameters = createGatewayParameters(params) - return tokenizationSpecification + return PaymentMethodTokenizationSpecification( + type = PAYMENT_GATEWAY, + parameters = createGatewayParameters(params), + ) } private fun createGatewayParameters(params: GooglePayComponentParams): TokenizationParameters { - val tokenizationParameters = TokenizationParameters() - tokenizationParameters.gateway = ADYEN_GATEWAY - tokenizationParameters.gatewayMerchantId = params.gatewayMerchantId - return tokenizationParameters + return TokenizationParameters( + gateway = ADYEN_GATEWAY, + gatewayMerchantId = params.gatewayMerchantId, + ) } private fun createTransactionInfo(params: GooglePayComponentParams): TransactionInfoModel { - var bigDecimal = toBigDecimal(params.amount) - bigDecimal = bigDecimal.setScale(GOOGLE_PAY_DECIMAL_SCALE, RoundingMode.HALF_UP) - val displayAmount = GOOGLE_PAY_DECIMAL_FORMAT.format(bigDecimal) - val transactionInfoModel = TransactionInfoModel() - // Google requires to not pass the price when the price status is NOT_CURRENTLY_KNOWN - if (params.totalPriceStatus != NOT_CURRENTLY_KNOWN) { - transactionInfoModel.totalPrice = displayAmount + return TransactionInfoModel( + countryCode = params.countryCode, + totalPriceStatus = params.totalPriceStatus, + currencyCode = params.amount.currency, + ).apply { + // Google requires to not pass the price when the price status is NOT_CURRENTLY_KNOWN + if (params.totalPriceStatus == NOT_CURRENTLY_KNOWN) return@apply + val bigDecimalAmount = AmountFormat.toBigDecimal(params.amount) + .setScale(GOOGLE_PAY_DECIMAL_SCALE, RoundingMode.HALF_UP) + val displayAmount = GOOGLE_PAY_DECIMAL_FORMAT.format(bigDecimalAmount) + totalPrice = displayAmount } - transactionInfoModel.countryCode = params.countryCode - transactionInfoModel.totalPriceStatus = params.totalPriceStatus - transactionInfoModel.currencyCode = params.amount.currency - return transactionInfoModel } } diff --git a/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapperTest.kt b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapperTest.kt index 6d55e88b43..0810627a61 100644 --- a/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapperTest.kt +++ b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapperTest.kt @@ -19,12 +19,12 @@ import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.googlepay.AllowedAuthMethods +import com.adyen.checkout.googlepay.AllowedCardNetworks import com.adyen.checkout.googlepay.BillingAddressParameters import com.adyen.checkout.googlepay.GooglePayConfiguration import com.adyen.checkout.googlepay.MerchantInfo import com.adyen.checkout.googlepay.ShippingAddressParameters -import com.adyen.checkout.googlepay.internal.util.AllowedAuthMethods -import com.adyen.checkout.googlepay.internal.util.AllowedCardNetworks import com.google.android.gms.wallet.WalletConstants import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach @@ -66,7 +66,7 @@ internal class GooglePayComponentParamsMapperTest { val googlePayConfiguration = GooglePayConfiguration.Builder( shopperLocale = Locale.FRANCE, environment = Environment.APSE, - clientKey = TEST_CLIENT_KEY_2 + clientKey = TEST_CLIENT_KEY_2, ).setAmount(amount).setGooglePayEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION) .setMerchantAccount("MERCHANT_ACCOUNT") .setAllowPrepaidCards(true) @@ -128,14 +128,14 @@ internal class GooglePayComponentParamsMapperTest { isCreatedByDropIn = true, amount = Amount( currency = "XCD", - value = 4_00L - ) + value = 4_00L, + ), ) val params = GooglePayComponentParamsMapper(overrideParams, null).mapToParams( googlePayConfiguration, PaymentMethod(), - null + null, ) val expected = getGooglePayComponentParams( @@ -146,8 +146,8 @@ internal class GooglePayComponentParamsMapperTest { isCreatedByDropIn = true, amount = Amount( currency = "XCD", - value = 4_00L - ) + value = 4_00L, + ), ) assertEquals(expected, params) @@ -158,19 +158,19 @@ internal class GooglePayComponentParamsMapperTest { val googlePayConfiguration = GooglePayConfiguration.Builder( shopperLocale = Locale.US, environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + clientKey = TEST_CLIENT_KEY_1, ).setMerchantAccount("GATEWAY_MERCHANT_ID_1").build() val paymentMethod = PaymentMethod( configuration = Configuration( - gatewayMerchantId = "GATEWAY_MERCHANT_ID_2" - ) + gatewayMerchantId = "GATEWAY_MERCHANT_ID_2", + ), ) val params = GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, paymentMethod, null) val expected = getGooglePayComponentParams( - gatewayMerchantId = "GATEWAY_MERCHANT_ID_1" + gatewayMerchantId = "GATEWAY_MERCHANT_ID_1", ) assertEquals(expected, params) @@ -181,19 +181,19 @@ internal class GooglePayComponentParamsMapperTest { val googlePayConfiguration = GooglePayConfiguration.Builder( shopperLocale = Locale.US, environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + clientKey = TEST_CLIENT_KEY_1, ).build() val paymentMethod = PaymentMethod( configuration = Configuration( - gatewayMerchantId = "GATEWAY_MERCHANT_ID_2" - ) + gatewayMerchantId = "GATEWAY_MERCHANT_ID_2", + ), ) val params = GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, paymentMethod, null) val expected = getGooglePayComponentParams( - gatewayMerchantId = "GATEWAY_MERCHANT_ID_2" + gatewayMerchantId = "GATEWAY_MERCHANT_ID_2", ) assertEquals(expected, params) @@ -204,7 +204,7 @@ internal class GooglePayComponentParamsMapperTest { val googlePayConfiguration = GooglePayConfiguration.Builder( shopperLocale = Locale.US, environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + clientKey = TEST_CLIENT_KEY_1, ).build() assertThrows { @@ -217,13 +217,13 @@ internal class GooglePayComponentParamsMapperTest { val googlePayConfiguration = getGooglePayConfigurationBuilder().build() val paymentMethod = PaymentMethod( - brands = listOf("mc", "amex", "maestro", "discover") + brands = listOf("mc", "amex", "maestro", "discover"), ) val params = GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, paymentMethod, null) val expected = getGooglePayComponentParams( - allowedCardNetworks = listOf("MASTERCARD", "AMEX", "DISCOVER") + allowedCardNetworks = listOf("MASTERCARD", "AMEX", "DISCOVER"), ) assertEquals(expected, params) @@ -238,7 +238,7 @@ internal class GooglePayComponentParamsMapperTest { GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, PaymentMethod(), null) val expected = getGooglePayComponentParams( - googlePayEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION + googlePayEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION, ) assertEquals(expected, params) @@ -252,7 +252,7 @@ internal class GooglePayComponentParamsMapperTest { GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, PaymentMethod(), null) val expected = getGooglePayComponentParams( - googlePayEnvironment = WalletConstants.ENVIRONMENT_TEST + googlePayEnvironment = WalletConstants.ENVIRONMENT_TEST, ) assertEquals(expected, params) @@ -263,7 +263,7 @@ internal class GooglePayComponentParamsMapperTest { val googlePayConfiguration = GooglePayConfiguration.Builder( shopperLocale = Locale.CHINA, environment = Environment.UNITED_STATES, - clientKey = TEST_CLIENT_KEY_2 + clientKey = TEST_CLIENT_KEY_2, ).setMerchantAccount(TEST_GATEWAY_MERCHANT_ID).build() val params = @@ -273,7 +273,7 @@ internal class GooglePayComponentParamsMapperTest { shopperLocale = Locale.CHINA, environment = Environment.UNITED_STATES, clientKey = TEST_CLIENT_KEY_2, - googlePayEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION + googlePayEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION, ) assertEquals(expected, params) @@ -291,7 +291,7 @@ internal class GooglePayComponentParamsMapperTest { clientKey = TEST_CLIENT_KEY_1, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), isCreatedByDropIn = false, - amount = null + amount = null, ) val params = GooglePayComponentParamsMapper(overrideParams, null).mapToParams( @@ -305,7 +305,7 @@ internal class GooglePayComponentParamsMapperTest { environment = Environment.TEST, clientKey = TEST_CLIENT_KEY_1, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), - isCreatedByDropIn = false + isCreatedByDropIn = false, ) assertEquals(expected, params) @@ -318,8 +318,8 @@ internal class GooglePayComponentParamsMapperTest { .setAmount( Amount( currency = "TRY", - value = 40_00L - ) + value = 40_00L, + ), ) .build() @@ -332,7 +332,7 @@ internal class GooglePayComponentParamsMapperTest { clientKey = TEST_CLIENT_KEY_1, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), isCreatedByDropIn = false, - amount = null + amount = null, ) val params = GooglePayComponentParamsMapper(overrideParams, null).mapToParams( @@ -373,14 +373,14 @@ internal class GooglePayComponentParamsMapperTest { PaymentMethod(), sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", - ) + ), ) val expected = getGooglePayComponentParams( - amount = expectedValue + amount = expectedValue, ) assertEquals(expected, params) @@ -389,7 +389,7 @@ internal class GooglePayComponentParamsMapperTest { private fun getGooglePayConfigurationBuilder() = GooglePayConfiguration.Builder( shopperLocale = Locale.US, environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + clientKey = TEST_CLIENT_KEY_1, ).setMerchantAccount(TEST_GATEWAY_MERCHANT_ID) @Suppress("LongParameterList") diff --git a/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtilsTest.kt b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtilsTest.kt new file mode 100644 index 0000000000..37e2f31e49 --- /dev/null +++ b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtilsTest.kt @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 5/12/2023. + */ + +package com.adyen.checkout.googlepay.internal.util + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams +import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel +import com.adyen.checkout.core.Environment +import com.adyen.checkout.googlepay.BillingAddressParameters +import com.adyen.checkout.googlepay.MerchantInfo +import com.adyen.checkout.googlepay.ShippingAddressParameters +import com.adyen.checkout.googlepay.internal.ui.model.GooglePayComponentParams +import com.google.android.gms.wallet.WalletConstants +import org.json.JSONObject +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import java.util.Locale + +internal class GooglePayUtilsTest { + + @Test + fun `when creating IsReadyToPayRequest with default or empty GooglePayComponentParams then results match`() { + val isReadyToPayRequest = GooglePayUtils.createIsReadyToPayRequest(getEmptyGooglePayComponentParams()) + val expectedSerializedIsReadyToPayRequest = JSONObject( + """ + { + "apiVersionMinor": 0, + "apiVersion": 2, + "allowedPaymentMethods": + [ + { + "type": "CARD", + "parameters": + { + "allowedAuthMethods": [], + "billingAddressRequired": false, + "allowedCardNetworks": [], + "allowPrepaidCards": false + }, + "tokenizationSpecification": + { + "type": "PAYMENT_GATEWAY", + "parameters": + { + "gatewayMerchantId": "", + "gateway": "adyen" + } + } + } + ], + "existingPaymentMethodRequired": false + } + """.trimIndent(), + ).toString() + + assertEquals(expectedSerializedIsReadyToPayRequest, isReadyToPayRequest.toJson()) + } + + @Test + fun `when creating IsReadyToPayRequest with custom GooglePayComponentParams then results match`() { + val isReadyToPayRequest = GooglePayUtils.createIsReadyToPayRequest(getCustomGooglePayComponentParams()) + val expectedSerializedIsReadyToPayRequest = JSONObject( + """ + { + "apiVersionMinor": 0, + "apiVersion": 2, + "allowedPaymentMethods": + [ + { + "type": "CARD", + "parameters": + { + "assuranceDetailsRequired": true, + "allowedAuthMethods": + [ + "AUTH_METHOD_1", + "AUTH_METHOD_2" + ], + "billingAddressRequired": true, + "billingAddressParameters": + { + "format": "FORMAT", + "phoneNumberRequired": true + }, + "allowedCardNetworks": + [ + "CARD_NETWORK_1", + "CARD_NETWORK_2", + "CARD_NETWORK_3" + ], + "allowCreditCards": true, + "allowPrepaidCards": true + }, + "tokenizationSpecification": + { + "type": "PAYMENT_GATEWAY", + "parameters": + { + "gatewayMerchantId": "GATEWAY_MERCHANT_ID", + "gateway": "adyen" + } + } + } + ], + "existingPaymentMethodRequired": true + } + """.trimIndent(), + ).toString() + + assertEquals(expectedSerializedIsReadyToPayRequest, isReadyToPayRequest.toJson()) + } + + @Test + fun `when creating PaymentDataRequest with default or empty GooglePayComponentParams then results match`() { + val paymentDataRequest = GooglePayUtils.createPaymentDataRequest(getEmptyGooglePayComponentParams()) + val expectedSerializedPaymentDataRequest = JSONObject( + """ + { + "apiVersionMinor": 0, + "apiVersion": 2, + "allowedPaymentMethods": + [ + { + "type": "CARD", + "parameters": + { + "allowedAuthMethods": [], + "billingAddressRequired": false, + "allowedCardNetworks": [], + "allowPrepaidCards": false + }, + "tokenizationSpecification": + { + "type": "PAYMENT_GATEWAY", + "parameters": + { + "gatewayMerchantId": "", + "gateway": "adyen" + } + } + } + ], + "shippingAddressRequired": false, + "emailRequired": false, + "transactionInfo": + { + "totalPriceStatus": "NOT_CURRENTLY_KNOWN", + "currencyCode": "USD" + } + } + """.trimIndent(), + ).toString() + + assertEquals(expectedSerializedPaymentDataRequest, paymentDataRequest.toJson()) + } + + @Test + fun `when creating PaymentDataRequest with custom GooglePayComponentParams then results match`() { + val paymentDataRequest = GooglePayUtils.createPaymentDataRequest(getCustomGooglePayComponentParams()) + val expectedSerializedPaymentDataRequest = JSONObject( + """ + { + "apiVersionMinor": 0, + "apiVersion": 2, + "merchantInfo": + { + "merchantId": "MERCHANT_ID", + "merchantName": "MERCHANT_NAME" + }, + "allowedPaymentMethods": + [ + { + "type": "CARD", + "parameters": + { + "assuranceDetailsRequired": true, + "allowedAuthMethods": + [ + "AUTH_METHOD_1", + "AUTH_METHOD_2" + ], + "billingAddressRequired": true, + "billingAddressParameters": + { + "format": "FORMAT", + "phoneNumberRequired": true + }, + "allowedCardNetworks": + [ + "CARD_NETWORK_1", + "CARD_NETWORK_2", + "CARD_NETWORK_3" + ], + "allowCreditCards": true, + "allowPrepaidCards": true + }, + "tokenizationSpecification": + { + "type": "PAYMENT_GATEWAY", + "parameters": + { + "gatewayMerchantId": "GATEWAY_MERCHANT_ID", + "gateway": "adyen" + } + } + } + ], + "shippingAddressRequired": true, + "shippingAddressParameters": + { + "allowedCountryCodes": + [ + "COUNTRY_1", + "COUNTRY_2" + ], + "phoneNumberRequired": true + }, + "emailRequired": true, + "transactionInfo": + { + "totalPrice": "13.37", + "countryCode": "COUNTRY_CODE", + "totalPriceStatus": "TOTAL_PRICE_STATUS", + "currencyCode": "EUR" + } + } + """.trimIndent(), + ).toString() + + assertEquals(expectedSerializedPaymentDataRequest, paymentDataRequest.toJson()) + } + + private fun getEmptyGooglePayComponentParams(): GooglePayComponentParams { + return GooglePayComponentParams( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = "CLIENT_KEY", + analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), + isCreatedByDropIn = false, + amount = Amount("USD", 0), + gatewayMerchantId = "", + googlePayEnvironment = WalletConstants.ENVIRONMENT_TEST, + totalPriceStatus = "NOT_CURRENTLY_KNOWN", + countryCode = null, + merchantInfo = null, + allowedAuthMethods = emptyList(), + allowedCardNetworks = emptyList(), + isAllowPrepaidCards = false, + isAllowCreditCards = null, + isAssuranceDetailsRequired = null, + isEmailRequired = false, + isExistingPaymentMethodRequired = false, + isShippingAddressRequired = false, + shippingAddressParameters = null, + isBillingAddressRequired = false, + billingAddressParameters = null, + ) + } + + private fun getCustomGooglePayComponentParams(): GooglePayComponentParams { + return GooglePayComponentParams( + shopperLocale = Locale.GERMAN, + environment = Environment.EUROPE, + clientKey = "CLIENT_KEY_CUSTOM", + analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), + isCreatedByDropIn = true, + amount = Amount("EUR", 13_37), + gatewayMerchantId = "GATEWAY_MERCHANT_ID", + googlePayEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION, + totalPriceStatus = "TOTAL_PRICE_STATUS", + countryCode = "COUNTRY_CODE", + merchantInfo = MerchantInfo(merchantName = "MERCHANT_NAME", merchantId = "MERCHANT_ID"), + allowedAuthMethods = listOf("AUTH_METHOD_1", "AUTH_METHOD_2"), + allowedCardNetworks = listOf("CARD_NETWORK_1", "CARD_NETWORK_2", "CARD_NETWORK_3"), + isAllowPrepaidCards = true, + isAllowCreditCards = true, + isAssuranceDetailsRequired = true, + isEmailRequired = true, + isExistingPaymentMethodRequired = true, + isShippingAddressRequired = true, + shippingAddressParameters = ShippingAddressParameters( + allowedCountryCodes = listOf( + "COUNTRY_1", + "COUNTRY_2", + ), + isPhoneNumberRequired = true, + ), + isBillingAddressRequired = true, + billingAddressParameters = BillingAddressParameters( + format = "FORMAT", + isPhoneNumberRequired = true, + ), + ) + } +} diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 68e710e873..46ba1da5bd 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -73,6 +73,14 @@ + + + + + + + + @@ -89,6 +97,14 @@ + + + + + + + + @@ -139,6 +155,14 @@ + + + + + + + + @@ -435,6 +459,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -467,11 +528,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -504,6 +655,14 @@ + + + + + + + + @@ -520,6 +679,14 @@ + + + + + + + + @@ -552,6 +719,14 @@ + + + + + + + + @@ -568,11 +743,24 @@ + + + + + + + + + + + + + @@ -605,6 +793,14 @@ + + + + + + + + @@ -621,6 +817,14 @@ + + + + + + + + @@ -653,6 +857,14 @@ + + + + + + + + @@ -669,6 +881,14 @@ + + + + + + + + @@ -701,6 +921,14 @@ + + + + + + + + @@ -717,6 +945,22 @@ + + + + + + + + + + + + + + + + @@ -749,6 +993,14 @@ + + + + + + + + @@ -765,6 +1017,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -797,6 +1099,14 @@ + + + + + + + + @@ -813,6 +1123,14 @@ + + + + + + + + @@ -839,6 +1157,11 @@ + + + + + @@ -855,6 +1178,14 @@ + + + + + + + + @@ -1080,6 +1411,14 @@ + + + + + + + + @@ -1112,6 +1451,14 @@ + + + + + + + + @@ -1144,6 +1491,14 @@ + + + + + + + + @@ -1184,6 +1539,14 @@ + + + + + + + + @@ -1192,6 +1555,14 @@ + + + + + + + + @@ -1253,6 +1624,14 @@ + + + + + + + + @@ -1285,6 +1664,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -1762,6 +2165,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1847,6 +2290,14 @@ + + + + + + + + @@ -2310,6 +2761,14 @@ + + + + + + + + @@ -2342,6 +2801,14 @@ + + + + + + + + @@ -2362,6 +2829,11 @@ + + + + + @@ -2394,6 +2866,14 @@ + + + + + + + + @@ -2414,6 +2894,11 @@ + + + + + @@ -2446,6 +2931,14 @@ + + + + + + + + @@ -2478,6 +2971,14 @@ + + + + + + + + @@ -2510,6 +3011,14 @@ + + + + + + + + @@ -2542,6 +3051,14 @@ + + + + + + + + @@ -2574,6 +3091,14 @@ + + + + + + + + @@ -2606,6 +3131,14 @@ + + + + + + + + @@ -2638,6 +3171,14 @@ + + + + + + + + @@ -2670,6 +3211,14 @@ + + + + + + + + @@ -2702,6 +3251,14 @@ + + + + + + + + @@ -2734,6 +3291,14 @@ + + + + + + + + @@ -2766,6 +3331,14 @@ + + + + + + + + @@ -2807,6 +3380,17 @@ + + + + + + + + + + + @@ -2839,6 +3423,14 @@ + + + + + + + + @@ -2871,6 +3463,14 @@ + + + + + + + + @@ -2903,6 +3503,14 @@ + + + + + + + + @@ -2935,6 +3543,14 @@ + + + + + + + + @@ -2967,6 +3583,14 @@ + + + + + + + + @@ -2999,6 +3623,14 @@ + + + + + + + + @@ -3031,6 +3663,14 @@ + + + + + + + + @@ -3079,6 +3719,14 @@ + + + + + + + + @@ -3111,6 +3759,14 @@ + + + + + + + + @@ -3143,6 +3799,14 @@ + + + + + + + + @@ -3175,6 +3839,14 @@ + + + + + + + + @@ -3231,6 +3903,14 @@ + + + + + + + + @@ -3263,6 +3943,14 @@ + + + + + + + + @@ -3295,6 +3983,14 @@ + + + + + + + + @@ -3327,6 +4023,14 @@ + + + + + + + + @@ -3359,6 +4063,14 @@ + + + + + + + + @@ -3391,6 +4103,14 @@ + + + + + + + + @@ -3423,6 +4143,14 @@ + + + + + + + + @@ -3455,6 +4183,14 @@ + + + + + + + + @@ -3487,6 +4223,14 @@ + + + + + + + + @@ -3519,6 +4263,14 @@ + + + + + + + + @@ -3551,6 +4303,14 @@ + + + + + + + + @@ -3583,6 +4343,14 @@ + + + + + + + + @@ -3615,6 +4383,14 @@ + + + + + + + + @@ -3647,6 +4423,14 @@ + + + + + + + + @@ -3663,6 +4447,14 @@ + + + + + + + + @@ -3695,6 +4487,14 @@ + + + + + + + + @@ -3711,6 +4511,14 @@ + + + + + + + + @@ -3743,6 +4551,14 @@ + + + + + + + + @@ -3775,6 +4591,14 @@ + + + + + + + + @@ -3807,6 +4631,14 @@ + + + + + + + + @@ -3986,6 +4818,14 @@ + + + + + + + + @@ -4023,6 +4863,14 @@ + + + + + + + + @@ -4036,6 +4884,11 @@ + + + + + @@ -4433,6 +5286,14 @@ + + + + + + + + @@ -4511,6 +5372,11 @@ + + + + + @@ -4712,44 +5578,132 @@ + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + @@ -4768,6 +5722,14 @@ + + + + + + + + @@ -4800,6 +5762,14 @@ + + + + + + + + @@ -4944,6 +5914,14 @@ + + + + + + + + @@ -4960,6 +5938,14 @@ + + + + + + + + @@ -4992,6 +5978,14 @@ + + + + + + + + @@ -5463,6 +6457,14 @@ + + + + + + + + @@ -5481,6 +6483,14 @@ + + + + + + + + @@ -5499,6 +6509,14 @@ + + + + + + + + @@ -5507,6 +6525,14 @@ + + + + + + + + @@ -5635,6 +6661,19 @@ + + + + + + + + + + + + + @@ -5651,6 +6690,14 @@ + + + + + + + + @@ -5680,6 +6727,11 @@ + + + + + @@ -5688,6 +6740,14 @@ + + + + + + + + @@ -5704,6 +6764,14 @@ + + + + + + + + @@ -5728,6 +6796,14 @@ + + + + + + + + @@ -5752,6 +6828,14 @@ + + + + + + + + @@ -5776,6 +6860,14 @@ + + + + + + + + @@ -5800,6 +6892,14 @@ + + + + + + + + @@ -5824,6 +6924,14 @@ + + + + + + + + @@ -5848,6 +6956,14 @@ + + + + + + + + @@ -5872,6 +6988,14 @@ + + + + + + + + @@ -5896,6 +7020,14 @@ + + + + + + + + @@ -5920,6 +7052,14 @@ + + + + + + + + @@ -5944,6 +7084,14 @@ + + + + + + + + @@ -5968,6 +7116,14 @@ + + + + + + + + @@ -5992,6 +7148,14 @@ + + + + + + + + @@ -6016,6 +7180,14 @@ + + + + + + + + @@ -6040,6 +7212,14 @@ + + + + + + + + @@ -6064,6 +7244,14 @@ + + + + + + + + @@ -6088,6 +7276,14 @@ + + + + + + + + @@ -6112,6 +7308,14 @@ + + + + + + + + @@ -6136,6 +7340,14 @@ + + + + + + + + @@ -6160,6 +7372,14 @@ + + + + + + + + @@ -6184,6 +7404,14 @@ + + + + + + + + @@ -6208,6 +7436,14 @@ + + + + + + + + @@ -6232,6 +7468,14 @@ + + + + + + + + @@ -6256,6 +7500,14 @@ + + + + + + + + @@ -6280,6 +7532,14 @@ + + + + + + + + @@ -6304,6 +7564,14 @@ + + + + + + + + @@ -6319,6 +7587,11 @@ + + + + + @@ -6799,6 +8072,14 @@ + + + + + + + + @@ -7057,6 +8338,14 @@ + + + + + + + + @@ -7065,6 +8354,14 @@ + + + + + + + + @@ -7073,6 +8370,14 @@ + + + + + + + + @@ -7113,6 +8418,14 @@ + + + + + + + + @@ -7137,6 +8450,14 @@ + + + + + + + + @@ -7161,6 +8482,14 @@ + + + + + + + + @@ -7185,6 +8514,14 @@ + + + + + + + + @@ -7209,6 +8546,14 @@ + + + + + + + + @@ -7233,6 +8578,14 @@ + + + + + + + + @@ -7257,6 +8610,14 @@ + + + + + + + + @@ -7281,6 +8642,14 @@ + + + + + + + + @@ -7337,6 +8706,14 @@ + + + + + + + + @@ -7352,6 +8729,11 @@ + + + + + @@ -7376,6 +8758,14 @@ + + + + + + + + @@ -7466,6 +8856,11 @@ + + + + + @@ -8024,6 +9419,14 @@ + + + + + + + + @@ -8032,6 +9435,14 @@ + + + + + + + + @@ -8938,6 +10349,14 @@ + + + + + + + + @@ -9110,6 +10529,14 @@ + + + + + + + + @@ -9126,6 +10553,14 @@ + + + + + + + + @@ -9142,6 +10577,14 @@ + + + + + + + + @@ -9158,6 +10601,14 @@ + + + + + + + + @@ -9174,6 +10625,14 @@ + + + + + + + + @@ -9190,6 +10649,14 @@ + + + + + + + + @@ -9206,6 +10673,14 @@ + + + + + + + + @@ -9222,6 +10697,14 @@ + + + + + + + + @@ -9230,6 +10713,14 @@ + + + + + + + + @@ -9246,6 +10737,14 @@ + + + + + + + + @@ -9262,6 +10761,14 @@ + + + + + + + + @@ -9278,6 +10785,14 @@ + + + + + + + + @@ -9294,6 +10809,14 @@ + + + + + + + + @@ -9310,6 +10833,14 @@ + + + + + + + + @@ -9326,6 +10857,14 @@ + + + + + + + + @@ -9342,6 +10881,22 @@ + + + + + + + + + + + + + + + + @@ -9358,6 +10913,14 @@ + + + + + + + + @@ -9374,6 +10937,14 @@ + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34e8a..3fa8f862f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1a53..1aa94a4269 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/provider/IssuerListComponentProvider.kt b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/provider/IssuerListComponentProvider.kt index 030570b27d..79cc79041f 100644 --- a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/provider/IssuerListComponentProvider.kt +++ b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/provider/IssuerListComponentProvider.kt @@ -54,6 +54,7 @@ import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponen import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.ui.SubmitHandler +@Suppress("ktlint:standard:type-parameter-list-spacing") abstract class IssuerListComponentProvider< ComponentT : IssuerListComponent, ConfigurationT : IssuerListConfiguration, diff --git a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapperTest.kt b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapperTest.kt index f770d1ff30..cbdef97afb 100644 --- a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapperTest.kt +++ b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapperTest.kt @@ -130,7 +130,7 @@ internal class IssuerListComponentParamsMapperTest { issuerListConfiguration, sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", ) diff --git a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/provider/OnlineBankingComponentProvider.kt b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/provider/OnlineBankingComponentProvider.kt index 51c0dafbb7..10dc9eacb3 100644 --- a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/provider/OnlineBankingComponentProvider.kt +++ b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/provider/OnlineBankingComponentProvider.kt @@ -55,6 +55,7 @@ import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import com.adyen.checkout.ui.core.internal.util.PdfOpener +@Suppress("ktlint:standard:type-parameter-list-spacing") abstract class OnlineBankingComponentProvider< ComponentT : OnlineBankingComponent, ConfigurationT : OnlineBankingConfiguration, diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/QrCodeViewProvider.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/QrCodeViewProvider.kt index c1ee33ad32..5597a0185c 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/QrCodeViewProvider.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/QrCodeViewProvider.kt @@ -30,7 +30,9 @@ internal object QrCodeViewProvider : ViewProvider { } internal enum class QrCodeComponentViewType : ComponentViewType { - SIMPLE_QR_CODE, FULL_QR_CODE, REDIRECT; + SIMPLE_QR_CODE, + FULL_QR_CODE, + REDIRECT; override val viewProvider: ViewProvider = QrCodeViewProvider } diff --git a/renovate.json b/renovate.json index 6d814dd7d1..9da6203e07 100644 --- a/renovate.json +++ b/renovate.json @@ -10,5 +10,6 @@ "minimumReleaseAge" : "30 days", "schedule" : ["on the first day of the month"] } - ] + ], + "rebaseWhen" : "never" } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupConfiguration.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupConfiguration.kt index 487a0322fb..ecb2a61334 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupConfiguration.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupConfiguration.kt @@ -18,11 +18,13 @@ import org.json.JSONObject @Parcelize data class SessionSetupConfiguration( val enableStoreDetails: Boolean? = null, + val showInstallmentAmount: Boolean = false, val installmentOptions: Map? = null ) : ModelObject() { companion object { private const val ENABLE_STORE_DETAILS = "enableStoreDetails" + private const val SHOW_INSTALLMENT_AMOUNT = "showInstallmentAmount" private const val INSTALLMENT_OPTIONS = "installmentOptions" @JvmField @@ -31,6 +33,7 @@ data class SessionSetupConfiguration( return try { JSONObject().apply { putOpt(ENABLE_STORE_DETAILS, modelObject.enableStoreDetails) + putOpt(SHOW_INSTALLMENT_AMOUNT, modelObject.showInstallmentAmount) putOpt( INSTALLMENT_OPTIONS, modelObject.installmentOptions?.let { JSONObject(it) } @@ -45,6 +48,7 @@ data class SessionSetupConfiguration( return try { SessionSetupConfiguration( enableStoreDetails = jsonObject.optBoolean(ENABLE_STORE_DETAILS), + showInstallmentAmount = jsonObject.optBoolean(SHOW_INSTALLMENT_AMOUNT), installmentOptions = jsonObject.optJSONObject(INSTALLMENT_OPTIONS) ?.jsonToMap(SessionSetupInstallmentOptions.SERIALIZER) ) diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/ui/model/SessionParamsFactory.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/ui/model/SessionParamsFactory.kt index 5fcf5163c9..0bd517168e 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/ui/model/SessionParamsFactory.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/ui/model/SessionParamsFactory.kt @@ -10,6 +10,7 @@ package com.adyen.checkout.sessions.core.internal.ui.model import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentOptionsParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.sessions.core.CheckoutSession @@ -39,13 +40,16 @@ object SessionParamsFactory { ): SessionParams { return SessionParams( enableStoreDetails = sessionSetupConfiguration?.enableStoreDetails, - installmentOptions = sessionSetupConfiguration?.installmentOptions?.map { - it.key to SessionInstallmentOptionsParams( - plans = it.value?.plans, - preselectedValue = it.value?.preselectedValue, - values = it.value?.values, - ) - }?.toMap(), + installmentConfiguration = SessionInstallmentConfiguration( + installmentOptions = sessionSetupConfiguration?.installmentOptions?.map { + it.key to SessionInstallmentOptionsParams( + plans = it.value?.plans, + preselectedValue = it.value?.preselectedValue, + values = it.value?.values, + ) + }?.toMap(), + showInstallmentAmount = sessionSetupConfiguration?.showInstallmentAmount + ), amount = amount, returnUrl = returnUrl, ) diff --git a/test-core/src/main/java/com/adyen/checkout/test/extensions/ViewModelExtensions.kt b/test-core/src/main/java/com/adyen/checkout/test/extensions/ViewModelExtensions.kt index 032905614a..6897897300 100644 --- a/test-core/src/main/java/com/adyen/checkout/test/extensions/ViewModelExtensions.kt +++ b/test-core/src/main/java/com/adyen/checkout/test/extensions/ViewModelExtensions.kt @@ -18,7 +18,11 @@ import androidx.lifecycle.ViewModel */ @RestrictTo(RestrictTo.Scope.TESTS) fun ViewModel.invokeOnCleared() { - with(javaClass.getDeclaredMethod("onCleared")) { + var clazz = javaClass as Class + while (clazz.declaredMethods.toList().none { it.name == "onCleared" }) { + clazz = clazz.superclass as Class + } + with(clazz.getDeclaredMethod("onCleared")) { isAccessible = true invoke(this@invokeOnCleared) } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/AdyenComponentView.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/AdyenComponentView.kt index 94545790b7..0ddb4dd73e 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/AdyenComponentView.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/AdyenComponentView.kt @@ -13,9 +13,7 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.widget.LinearLayout import androidx.core.view.children -import androidx.core.view.doOnNextLayout import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.adyen.checkout.components.core.internal.Component @@ -56,7 +54,7 @@ class AdyenComponentView @JvmOverloads constructor( private val binding: AdyenComponentViewBinding = AdyenComponentViewBinding.inflate( LayoutInflater.from(context), - this + this, ) /** @@ -125,12 +123,7 @@ class AdyenComponentView @JvmOverloads constructor( val localizedContext = context.createLocalizedContext(componentParams.shopperLocale) - binding.frameLayoutComponentContainer.doOnNextLayout { - val view = componentView.getView() - binding.frameLayoutComponentContainer.addView(view) - view.updateLayoutParams { width = LayoutParams.MATCH_PARENT } - } - + binding.frameLayoutComponentContainer.addView(componentView.getView()) componentView.initView(delegate, coroutineScope, localizedContext) val buttonDelegate = (delegate as? ButtonDelegate) @@ -181,7 +174,7 @@ class AdyenComponentView @JvmOverloads constructor( amount = componentParams.amount, locale = componentParams.shopperLocale, localizedContext = localizedContext, - emptyAmountStringResId = viewType.buttonTextResId + emptyAmountStringResId = viewType.buttonTextResId, ) } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressFormUIState.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressFormUIState.kt index bf9b250e2e..a02c988215 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressFormUIState.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressFormUIState.kt @@ -13,7 +13,9 @@ import com.adyen.checkout.ui.core.internal.ui.model.AddressParams @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) enum class AddressFormUIState { - NONE, POSTAL_CODE, FULL_ADDRESS; + NONE, + POSTAL_CODE, + FULL_ADDRESS; companion object { /** diff --git a/ui-core/src/main/res/values/styles_bottom_sheet.xml b/ui-core/src/main/res/values/styles_bottom_sheet.xml index b55130bd36..879457c83f 100644 --- a/ui-core/src/main/res/values/styles_bottom_sheet.xml +++ b/ui-core/src/main/res/values/styles_bottom_sheet.xml @@ -19,4 +19,9 @@ 0dp + + + diff --git a/voucher/src/main/java/com/adyen/checkout/voucher/internal/ui/VoucherViewProvider.kt b/voucher/src/main/java/com/adyen/checkout/voucher/internal/ui/VoucherViewProvider.kt index ef939226bb..116009c189 100644 --- a/voucher/src/main/java/com/adyen/checkout/voucher/internal/ui/VoucherViewProvider.kt +++ b/voucher/src/main/java/com/adyen/checkout/voucher/internal/ui/VoucherViewProvider.kt @@ -30,7 +30,8 @@ internal object VoucherViewProvider : ViewProvider { } internal enum class VoucherComponentViewType : ComponentViewType { - SIMPLE_VOUCHER, FULL_VOUCHER; + SIMPLE_VOUCHER, + FULL_VOUCHER; override val viewProvider: ViewProvider = VoucherViewProvider }