From b30a642d3a60f067de6c035de3ef8c1b268ef8e6 Mon Sep 17 00:00:00 2001 From: josephj Date: Fri, 10 Nov 2023 16:54:09 +0100 Subject: [PATCH 01/60] Do not rebase renovate PRs automatically To reduce notification noise --- renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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" } From 2de5c551687878998e1cc82c4db1b7a332b79db8 Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Thu, 9 Nov 2023 15:14:15 +0100 Subject: [PATCH 02/60] Show card name instead of "Credit Card" on stored card payment component screen COAND-804 --- .../dropin/internal/ui/BaseComponentDialogFragment.kt | 2 +- .../dropin/internal/ui/CardComponentDialogFragment.kt | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) 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) From 39ecc10cbac1018afe9f211d86eea4bda9d3f2ee Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Thu, 9 Nov 2023 15:30:13 +0100 Subject: [PATCH 03/60] Add overridePaymentMethodName method in DropInConfiguration Add the necessary logic to override the name field of payment methods COAND-804 --- .../checkout/dropin/DropInConfiguration.kt | 9 ++++++++ .../internal/ui/DropInViewModelFactory.kt | 20 ++++++++++++++++- .../model/DropInPaymentMethodInformation.kt | 22 +++++++++++++++++++ .../internal/ui/DropInViewModelFactoryTest.kt | 13 +++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInPaymentMethodInformation.kt create mode 100644 drop-in/src/test/java/com/adyen/checkout/internal/ui/DropInViewModelFactoryTest.kt 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..e78cf9fd90 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,6 +27,7 @@ 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.googlepay.GooglePayConfiguration @@ -66,6 +67,7 @@ class DropInConfiguration private constructor( val skipListWhenSinglePaymentMethod: Boolean, val isRemovingStoredPaymentMethodsEnabled: Boolean, val additionalDataForDropInService: Bundle?, + val overriddenPaymentMethodInformation: HashMap, ) : Configuration { internal fun getConfigurationForPaymentMethod(paymentMethod: String): T? { @@ -84,6 +86,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 +379,11 @@ class DropInConfiguration private constructor( return this } + fun overridePaymentMethodName(paymentMethodType: String, name: String): Builder { + overriddenPaymentMethodInformation[paymentMethodType] = DropInPaymentMethodInformation(name) + return this + } + override fun buildInternal(): DropInConfiguration { return DropInConfiguration( shopperLocale = shopperLocale, @@ -389,6 +397,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/DropInViewModelFactory.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactory.kt index b00eef5814..43e9451c34 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 @@ -13,6 +13,7 @@ import androidx.lifecycle.AbstractSavedStateViewModelFactory import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.PaymentMethodsApiResponse import com.adyen.checkout.components.core.internal.data.api.AnalyticsMapper import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryData import com.adyen.checkout.components.core.internal.data.api.AnalyticsService @@ -24,6 +25,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 @@ -35,7 +38,9 @@ internal class DropInViewModelFactory( override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { val bundleHandler = DropInSavedStateHandleContainer(handle) - val dropInConfiguration: DropInConfiguration = requireNotNull(bundleHandler.dropInConfiguration) + val dropInConfiguration: DropInConfiguration = requireNotNull(bundleHandler.dropInConfiguration).apply { + bundleHandler.paymentMethodsApiResponse?.overridePaymentMethodInformation(overriddenPaymentMethodInformation) + } val amount: Amount? = bundleHandler.amount val paymentMethods = bundleHandler.paymentMethodsApiResponse?.paymentMethods?.mapNotNull { it.type }.orEmpty() val session = bundleHandler.sessionDetails @@ -63,4 +68,17 @@ internal class DropInViewModelFactory( @Suppress("UNCHECKED_CAST") return DropInViewModel(bundleHandler, orderStatusRepository, analyticsRepository) as T } + + internal fun PaymentMethodsApiResponse.overridePaymentMethodInformation( + paymentMethodInformationMap: Map + ) { + paymentMethodInformationMap.forEach { informationEntry -> + val type = informationEntry.key + val paymentMethodInformation = informationEntry.value + + 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..00bdd98fe7 --- /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 +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/ui/DropInViewModelFactoryTest.kt b/drop-in/src/test/java/com/adyen/checkout/internal/ui/DropInViewModelFactoryTest.kt new file mode 100644 index 0000000000..95e598ff01 --- /dev/null +++ b/drop-in/src/test/java/com/adyen/checkout/internal/ui/DropInViewModelFactoryTest.kt @@ -0,0 +1,13 @@ +/* + * 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.internal.ui + +class DropInViewModelFactoryTest { + // TODO: Tests to be added +} From 78daf0896de8c07db2b7b39fe356f5f9d7453cc6 Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Fri, 10 Nov 2023 09:53:02 +0100 Subject: [PATCH 04/60] Move test classes to the correct package for drop-in COAND-804 --- .../{ => dropin}/internal/ConfigurationProvider.kt | 6 +++--- .../checkout/{ => dropin}/internal/DataProvider.kt | 6 +++--- .../adyen/checkout/{ => dropin}/internal/Helpers.kt | 6 +++--- .../internal/ui/PaymentMethodsListViewModelTest.kt | 11 +++++------ .../ui/PreselectedStoredPaymentViewModelTest.kt | 5 +---- .../{ => dropin}/internal/ui/TestComponentState.kt | 2 +- 6 files changed, 16 insertions(+), 20 deletions(-) rename drop-in/src/test/java/com/adyen/checkout/{ => dropin}/internal/ConfigurationProvider.kt (95%) rename drop-in/src/test/java/com/adyen/checkout/{ => dropin}/internal/DataProvider.kt (94%) rename drop-in/src/test/java/com/adyen/checkout/{ => dropin}/internal/Helpers.kt (94%) rename drop-in/src/test/java/com/adyen/checkout/{ => dropin}/internal/ui/PaymentMethodsListViewModelTest.kt (96%) rename drop-in/src/test/java/com/adyen/checkout/{ => dropin}/internal/ui/PreselectedStoredPaymentViewModelTest.kt (96%) rename drop-in/src/test/java/com/adyen/checkout/{ => dropin}/internal/ui/TestComponentState.kt (93%) 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 95% 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..724d2940d3 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 @@ -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 atef on 28/10/2022. + * Created by ararat on 9/11/2023. */ -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 94% 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..4a23e69a3b 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 @@ -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 atef on 28/10/2022. + * Created by ararat on 9/11/2023. */ -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 94% 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..5744df80bd 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 @@ -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 atef on 28/10/2022. + * Created by ararat on 9/11/2023. */ -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/internal/ui/PaymentMethodsListViewModelTest.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt similarity index 96% 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..f2215b0afc 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,17 +15,16 @@ 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.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.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.sessions.core.internal.data.model.SessionDetails import com.adyen.checkout.test.TestDispatcherExtension import kotlinx.coroutines.ExperimentalCoroutinesApi 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 From 3e7d66a55628aac6267ba82eef03bc7584499b1b Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Fri, 10 Nov 2023 13:50:25 +0100 Subject: [PATCH 05/60] Modify overridePaymentMethodInformation implementation. Add tests for overridePaymentMethodInformation function. COAND-804 --- .../core/PaymentMethodsApiResponse.kt | 2 +- .../checkout/dropin/DropInConfiguration.kt | 12 ++- .../internal/ui/DropInViewModelFactory.kt | 23 ++-- .../model/DropInPaymentMethodInformation.kt | 2 +- .../internal/ui/DropInViewModelFactoryTest.kt | 100 ++++++++++++++++++ 5 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactoryTest.kt 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/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt index e78cf9fd90..09a9aa625e 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 @@ -67,7 +67,7 @@ class DropInConfiguration private constructor( val skipListWhenSinglePaymentMethod: Boolean, val isRemovingStoredPaymentMethodsEnabled: Boolean, val additionalDataForDropInService: Bundle?, - val overriddenPaymentMethodInformation: HashMap, + internal val overriddenPaymentMethodInformation: HashMap, ) : Configuration { internal fun getConfigurationForPaymentMethod(paymentMethod: String): T? { @@ -379,6 +379,16 @@ class DropInConfiguration private constructor( return this } + /** + * Override payment method name, filtering by [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. + * Only the latest value will be shown if calling this function multiple times for the same [paymentMethodType]. + * + * @param paymentMethodType Updates payment methods matching the given type. + * @param name Custom name to be shown for payment methods filtered by given [paymentMethodType]. + */ fun overridePaymentMethodName(paymentMethodType: String, name: String): Builder { overriddenPaymentMethodInformation[paymentMethodType] = DropInPaymentMethodInformation(name) return this 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 43e9451c34..22356a2a89 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 @@ -13,7 +13,6 @@ import androidx.lifecycle.AbstractSavedStateViewModelFactory import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.PaymentMethodsApiResponse import com.adyen.checkout.components.core.internal.data.api.AnalyticsMapper import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryData import com.adyen.checkout.components.core.internal.data.api.AnalyticsService @@ -39,7 +38,7 @@ internal class DropInViewModelFactory( val bundleHandler = DropInSavedStateHandleContainer(handle) val dropInConfiguration: DropInConfiguration = requireNotNull(bundleHandler.dropInConfiguration).apply { - bundleHandler.paymentMethodsApiResponse?.overridePaymentMethodInformation(overriddenPaymentMethodInformation) + bundleHandler.overridePaymentMethodInformation(overriddenPaymentMethodInformation) } val amount: Amount? = bundleHandler.amount val paymentMethods = bundleHandler.paymentMethodsApiResponse?.paymentMethods?.mapNotNull { it.type }.orEmpty() @@ -68,17 +67,17 @@ internal class DropInViewModelFactory( @Suppress("UNCHECKED_CAST") return DropInViewModel(bundleHandler, orderStatusRepository, analyticsRepository) as T } +} - internal fun PaymentMethodsApiResponse.overridePaymentMethodInformation( - paymentMethodInformationMap: Map - ) { - paymentMethodInformationMap.forEach { informationEntry -> - val type = informationEntry.key - val paymentMethodInformation = informationEntry.value +internal fun DropInSavedStateHandleContainer.overridePaymentMethodInformation( + paymentMethodInformationMap: Map +) { + paymentMethodInformationMap.forEach { informationEntry -> + val type = informationEntry.key + val paymentMethodInformation = informationEntry.value - paymentMethods - ?.filter { paymentMethod -> paymentMethod.type == type } - ?.forEach { paymentMethod -> paymentMethod.overrideInformation(paymentMethodInformation) } - } + 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 index 00bdd98fe7..29d74f44a5 100644 --- 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 @@ -13,7 +13,7 @@ import com.adyen.checkout.components.core.PaymentMethod import kotlinx.parcelize.Parcelize @Parcelize -class DropInPaymentMethodInformation( +internal data class DropInPaymentMethodInformation( val name: String ) : Parcelable 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..b7b1b81ffb --- /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 + +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 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"), + ) + ) +} From aaba2de9fc53a45808b03ede9a0d0f720e1251de Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Fri, 10 Nov 2023 14:36:44 +0100 Subject: [PATCH 06/60] Remove redundant DropInViewModelFactoryTest Clean up code COAND-804 --- .../adyen/checkout/dropin/DropInConfiguration.kt | 4 ++-- .../dropin/internal/ConfigurationProvider.kt | 4 ++-- .../adyen/checkout/dropin/internal/DataProvider.kt | 4 ++-- .../com/adyen/checkout/dropin/internal/Helpers.kt | 4 ++-- .../internal/ui/DropInViewModelFactoryTest.kt | 2 +- .../internal/ui/PaymentMethodsListViewModelTest.kt | 10 ++++------ .../internal/ui/DropInViewModelFactoryTest.kt | 13 ------------- 7 files changed, 13 insertions(+), 28 deletions(-) delete mode 100644 drop-in/src/test/java/com/adyen/checkout/internal/ui/DropInViewModelFactoryTest.kt 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 09a9aa625e..89b06babd7 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 @@ -380,8 +380,8 @@ class DropInConfiguration private constructor( } /** - * Override payment method name, filtering by [paymentMethodType]. - * You can pass [PaymentMethodTypes] or any other custom value.] + * Provide a custom name to be shown in drop in for payment methods, filtering by [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. * Only the latest value will be shown if calling this function multiple times for the same [paymentMethodType]. diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt index 724d2940d3..e15189725d 100644 --- a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt @@ -1,9 +1,9 @@ /* - * Copyright (c) 2023 Adyen N.V. + * 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 ararat on 9/11/2023. + * Created by atef on 28/10/2022. */ package com.adyen.checkout.dropin.internal diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/DataProvider.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/DataProvider.kt index 4a23e69a3b..0f7203756f 100644 --- a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/DataProvider.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/DataProvider.kt @@ -1,9 +1,9 @@ /* - * Copyright (c) 2023 Adyen N.V. + * 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 ararat on 9/11/2023. + * Created by atef on 28/10/2022. */ package com.adyen.checkout.dropin.internal diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/Helpers.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/Helpers.kt index 5744df80bd..17865b61c3 100644 --- a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/Helpers.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/Helpers.kt @@ -1,9 +1,9 @@ /* - * Copyright (c) 2023 Adyen N.V. + * 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 ararat on 9/11/2023. + * Created by atef on 28/10/2022. */ package com.adyen.checkout.dropin.internal 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 index b7b1b81ffb..5339345cc9 100644 --- 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 @@ -57,7 +57,7 @@ class DropInViewModelFactoryTest { } @Test - fun `when overriding payment information for a payment method by type, does not update any payment method`() { + 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() } diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt index f2215b0afc..51a02eee66 100644 --- a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt @@ -15,19 +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.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.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.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 @@ -44,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/DropInViewModelFactoryTest.kt b/drop-in/src/test/java/com/adyen/checkout/internal/ui/DropInViewModelFactoryTest.kt deleted file mode 100644 index 95e598ff01..0000000000 --- a/drop-in/src/test/java/com/adyen/checkout/internal/ui/DropInViewModelFactoryTest.kt +++ /dev/null @@ -1,13 +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 ararat on 9/11/2023. - */ - -package com.adyen.checkout.internal.ui - -class DropInViewModelFactoryTest { - // TODO: Tests to be added -} From a4a090ba2fafc7e4d16906d7641a6b332f54d8c0 Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Fri, 10 Nov 2023 15:58:05 +0100 Subject: [PATCH 07/60] Add release notes COAND-804 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5ba0f46de..04bbf7767 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,21 +1,12 @@ [//]: # (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) -## 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. - -## Changed -- Dependency versions: - | Name | Version | - |--------------------------------------------------------------------------------------------------------|-------------------------------| - | [AndroidX Compose BoM](https://developer.android.com/jetpack/compose/bom/bom-mapping) | **2023.09.01** | +## New +- You can now override payment method names in drop in by using `DropInConfiguration.Builder.overridePaymentMethodName(type, name)` --- RELEASE_NOTES.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5ba0f46de0..04bbf77675 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,21 +1,12 @@ [//]: # (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) -## 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. - -## Changed -- Dependency versions: - | Name | Version | - |--------------------------------------------------------------------------------------------------------|-------------------------------| - | [AndroidX Compose BoM](https://developer.android.com/jetpack/compose/bom/bom-mapping) | **2023.09.01** | +## New +- You can now override payment method names in drop in by using `DropInConfiguration.Builder.overridePaymentMethodName(type, name)` From e44f359b62dc4d659c476a8c937d4ec7625523bb Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Mon, 13 Nov 2023 16:17:10 +0100 Subject: [PATCH 08/60] Address CR comments COAND-804 --- RELEASE_NOTES.md | 1 + .../adyen/checkout/dropin/BaseDropInServiceContract.kt | 2 +- .../com/adyen/checkout/dropin/DropInConfiguration.kt | 9 ++++----- .../dropin/internal/ui/DropInViewModelFactory.kt | 6 +++--- .../dropin/internal/ui/DropInViewModelFactoryTest.kt | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 04bbf77675..eb455388be 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -10,3 +10,4 @@ ## New - You can now override payment method names in drop in by using `DropInConfiguration.Builder.overridePaymentMethodName(type, name)` +- For stored cards, Drop-in will show the card name ("Visa", "Mastercard"...), instead of "Credit Card" 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 89b06babd7..3bf9676563 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 @@ -380,14 +380,13 @@ class DropInConfiguration private constructor( } /** - * Provide a custom name to be shown in drop in for payment methods, filtering by [paymentMethodType]. - * You can pass [PaymentMethodTypes] or any other custom value. + * 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. - * Only the latest value will be shown if calling this function multiple times for the same [paymentMethodType]. * - * @param paymentMethodType Updates payment methods matching the given type. - * @param name Custom name to be shown for payment methods filtered by given [paymentMethodType]. + * @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) 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 22356a2a89..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 @@ -37,9 +37,9 @@ internal class DropInViewModelFactory( override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { val bundleHandler = DropInSavedStateHandleContainer(handle) - val dropInConfiguration: DropInConfiguration = requireNotNull(bundleHandler.dropInConfiguration).apply { - bundleHandler.overridePaymentMethodInformation(overriddenPaymentMethodInformation) - } + 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 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 index 5339345cc9..962c7da22e 100644 --- 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 @@ -17,7 +17,7 @@ import org.junit.Assert.assertNotEquals import org.junit.Test import org.mockito.kotlin.mock -class DropInViewModelFactoryTest { +internal class DropInViewModelFactoryTest { @Test fun `when overriding payment information for a payment method by type, updates the correct payment methods`() { From cf907370ff2f7de6929c7b539fbf5805a34f5789 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:03:12 +0000 Subject: [PATCH 09/60] Update dependency androidx.activity:activity-compose to v1.8.0 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index a8ac4251d1..621410ea29 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -42,7 +42,7 @@ ext { constraintlayout_version = '2.1.4' // Compose Dependencies - compose_activity_version = '1.7.2' + compose_activity_version = '1.8.0' compose_bom_version = '2023.09.01' compose_viewmodel_version = '2.6.2' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 68e710e873..58eaad2cf3 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -73,6 +73,14 @@ + + + + + + + + @@ -89,6 +97,14 @@ + + + + + + + + @@ -139,6 +155,14 @@ + + + + + + + + From aeba1383da329325e38a0360152d4ddaaa945404 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:32:44 +0000 Subject: [PATCH 10/60] Update dependency org.json:json to v20231013 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 621410ea29..c4ed9c3a1e 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -66,7 +66,7 @@ ext { // 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" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 58eaad2cf3..f4f22a5859 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -8962,6 +8962,14 @@ + + + + + + + + From 6188fd7d16ee8bb6b7c9629ca10e7103cfdb20d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:01:23 +0000 Subject: [PATCH 11/60] Update dependency com.google.android.material:material to v1.10.0 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index c4ed9c3a1e..9aaf18cd47 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -37,7 +37,7 @@ ext { coroutines_version = "1.6.4" fragment_version = "1.6.1" lifecycle_version = "2.5.1" - material_version = "1.9.0" + material_version = "1.10.0" recyclerview_version = "1.3.1" constraintlayout_version = '2.1.4' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f4f22a5859..6d86a9900a 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -4010,6 +4010,14 @@ + + + + + + + + @@ -7490,6 +7498,11 @@ + + + + + @@ -8048,6 +8061,11 @@ + + + + + From 8048b38a9df67674ed882c5aca389ffc8b72a19f Mon Sep 17 00:00:00 2001 From: josephj Date: Tue, 14 Nov 2023 15:46:16 +0100 Subject: [PATCH 12/60] Update verification-metadata.xml --- gradle/verification-metadata.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6d86a9900a..1658eaf33e 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -8062,6 +8062,9 @@ + + + From 1cb0e30c2c8c942582592f2eae57612718461cc7 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Tue, 7 Nov 2023 17:18:16 +0100 Subject: [PATCH 13/60] Make it possible to override version and platform for analytics COAND-815 --- .../core/internal/data/api/AnalyticsMapper.kt | 22 ++++++++- .../internal/data/api/AnalyticsMapperTest.kt | 46 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) 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..62ffe9b7b6 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 = VERSION, channel = ANDROID_CHANNEL, - platform = ANDROID_PLATFORM, + platform = PLATFORM, locale = locale.toString(), component = getComponentQueryParameter(source), flavor = getFlavorQueryParameter(source), @@ -75,5 +75,23 @@ class AnalyticsMapper { private const val DROP_IN_COMPONENT = "dropin" private const val ANDROID_PLATFORM = "android" private const val ANDROID_CHANNEL = "android" + + private var PLATFORM = ANDROID_PLATFORM + private var VERSION = BuildConfig.CHECKOUT_VERSION + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun overrideForCrossPlatform( + platform: String, + version: String, + ) { + PLATFORM = platform + VERSION = version + } + + @VisibleForTesting + internal fun resetToDefaults() { + PLATFORM = ANDROID_PLATFORM + VERSION = BuildConfig.CHECKOUT_VERSION + } } } 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..dfb55861cc 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 { @@ -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("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()) + } } From fec91158512d2954e1a1d4f98cf2629b168f88f4 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Thu, 9 Nov 2023 15:58:38 +0100 Subject: [PATCH 14/60] Use an enum for the platform value COAND-815 --- .../core/internal/data/api/AnalyticsMapper.kt | 9 ++++----- .../internal/data/api/AnalyticsPlatform.kt | 18 ++++++++++++++++++ .../internal/data/api/AnalyticsMapperTest.kt | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsPlatform.kt 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 62ffe9b7b6..70ce5befed 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 @@ -73,24 +73,23 @@ class AnalyticsMapper { companion object { private const val DROP_IN_COMPONENT = "dropin" - private const val ANDROID_PLATFORM = "android" private const val ANDROID_CHANNEL = "android" - private var PLATFORM = ANDROID_PLATFORM + private var PLATFORM = AnalyticsPlatform.ANDROID.value private var VERSION = BuildConfig.CHECKOUT_VERSION @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun overrideForCrossPlatform( - platform: String, + platform: AnalyticsPlatform, version: String, ) { - PLATFORM = platform + PLATFORM = platform.value VERSION = version } @VisibleForTesting internal fun resetToDefaults() { - PLATFORM = ANDROID_PLATFORM + PLATFORM = AnalyticsPlatform.ANDROID.value VERSION = 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/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 dfb55861cc..d03a7c9669 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 @@ -142,7 +142,7 @@ internal class AnalyticsMapperTest { @Test fun `when cross platform parameters are overridden, then returned values should match expected`() { - AnalyticsMapper.overrideForCrossPlatform("flutter", "some test version") + AnalyticsMapper.overrideForCrossPlatform(AnalyticsPlatform.FLUTTER, "some test version") val actual = analyticsMapper.getAnalyticsSetupRequest( packageName = "PACKAGE_NAME", locale = Locale("en", "US"), From 57bdb02004e1ab89e95d2b88371d7a1e37cb1709 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 09:00:48 +0000 Subject: [PATCH 15/60] Update dependency gradle to v8.4 --- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) 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" \ From cd9ee7a763f8b7ef6bd2492e37e51ff0216971ff Mon Sep 17 00:00:00 2001 From: josephj Date: Wed, 15 Nov 2023 10:05:55 +0100 Subject: [PATCH 16/60] Rename telemetry to analytics for consistency with the SDK and because analytics is a broader term than telemetry COAND-481 --- .../checkout/example/data/storage/KeyValueStorage.kt | 8 ++++---- .../configuration/CheckoutConfigurationProvider.kt | 2 +- example-app/src/main/res/values/arrays.xml | 4 ++-- example-app/src/main/res/values/strings.xml | 8 ++++---- example-app/src/main/res/xml/preferences.xml | 12 ++++++------ 5 files changed, 17 insertions(+), 17 deletions(-) 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..4c783f09ad 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 @@ -34,7 +34,7 @@ interface KeyValueStorage { fun getInstallmentOptionsMode(): CardInstallmentOptionsMode fun useSessions(): Boolean fun setUseSessions(useSessions: Boolean) - fun getTelemetryLevel(): AnalyticsLevel + fun getAnalyticsLevel(): AnalyticsLevel } @Suppress("TooManyFunctions") @@ -164,12 +164,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/ui/configuration/CheckoutConfigurationProvider.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt index 9fcaa629d7..d06d56ee7f 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 @@ -147,7 +147,7 @@ internal class CheckoutConfigurationProvider @Inject constructor( .build() private fun getAnalyticsConfiguration(): AnalyticsConfiguration { - val analyticsLevel = keyValueStorage.getTelemetryLevel() + val analyticsLevel = keyValueStorage.getAnalyticsLevel() return AnalyticsConfiguration(level = analyticsLevel) } 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..37ccb9d452 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 @@ -58,8 +58,8 @@ instant_payment_method_type Instant Payment Method Type use_sessions - telemetry_level - Level + analytics_level + Level Theme @@ -85,6 +85,6 @@ NONE 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..f8ca721d19 100644 --- a/example-app/src/main/res/xml/preferences.xml +++ b/example-app/src/main/res/xml/preferences.xml @@ -105,14 +105,14 @@ - + From 34f84356ad2e1d4271f5fe3d1cd9d885b21e74e8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 10:59:19 +0000 Subject: [PATCH 17/60] Update android_gradle_plugin_version to v8.1.2 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 330 +++++++++++++++++++++++++++++++ 2 files changed, 331 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 9aaf18cd47..17517b2870 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -19,7 +19,7 @@ ext { version_name = "5.0.1" // 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" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 1658eaf33e..08d9550d34 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1104,6 +1104,14 @@ + + + + + + + + @@ -1136,6 +1144,14 @@ + + + + + + + + @@ -1168,6 +1184,14 @@ + + + + + + + + @@ -2334,6 +2358,14 @@ + + + + + + + + @@ -2366,6 +2398,14 @@ + + + + + + + + @@ -2386,6 +2426,11 @@ + + + + + @@ -2418,6 +2463,14 @@ + + + + + + + + @@ -2438,6 +2491,11 @@ + + + + + @@ -2470,6 +2528,14 @@ + + + + + + + + @@ -2502,6 +2568,14 @@ + + + + + + + + @@ -2534,6 +2608,14 @@ + + + + + + + + @@ -2598,6 +2680,14 @@ + + + + + + + + @@ -2630,6 +2720,14 @@ + + + + + + + + @@ -2662,6 +2760,14 @@ + + + + + + + + @@ -2694,6 +2800,14 @@ + + + + + + + + @@ -2726,6 +2840,14 @@ + + + + + + + + @@ -2758,6 +2880,14 @@ + + + + + + + + @@ -2790,6 +2920,14 @@ + + + + + + + + @@ -2863,6 +3001,14 @@ + + + + + + + + @@ -2895,6 +3041,14 @@ + + + + + + + + @@ -2927,6 +3081,14 @@ + + + + + + + + @@ -2959,6 +3121,14 @@ + + + + + + + + @@ -2991,6 +3161,14 @@ + + + + + + + + @@ -3023,6 +3201,14 @@ + + + + + + + + @@ -3055,6 +3241,14 @@ + + + + + + + + @@ -3103,6 +3297,14 @@ + + + + + + + + @@ -3135,6 +3337,14 @@ + + + + + + + + @@ -3167,6 +3377,14 @@ + + + + + + + + @@ -3199,6 +3417,14 @@ + + + + + + + + @@ -3255,6 +3481,14 @@ + + + + + + + + @@ -3383,6 +3617,14 @@ + + + + + + + + @@ -3543,6 +3785,14 @@ + + + + + + + + @@ -3575,6 +3825,14 @@ + + + + + + + + @@ -3607,6 +3865,14 @@ + + + + + + + + @@ -3639,6 +3905,14 @@ + + + + + + + + @@ -3671,6 +3945,14 @@ + + + + + + + + @@ -3687,6 +3969,14 @@ + + + + + + + + @@ -3719,6 +4009,14 @@ + + + + + + + + @@ -3735,6 +4033,14 @@ + + + + + + + + @@ -3767,6 +4073,14 @@ + + + + + + + + @@ -3799,6 +4113,14 @@ + + + + + + + + @@ -3831,6 +4153,14 @@ + + + + + + + + From 7eee68d0127ed3d6a76c98492a90989f1fb9a464 Mon Sep 17 00:00:00 2001 From: josephj Date: Wed, 15 Nov 2023 14:31:21 +0100 Subject: [PATCH 18/60] Update verification-metadata.xml --- gradle/verification-metadata.xml | 75 ++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 08d9550d34..9ccfa66a12 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2648,6 +2648,14 @@ + + + + + + + + @@ -2969,6 +2977,17 @@ + + + + + + + + + + + @@ -3521,6 +3540,14 @@ + + + + + + + + @@ -3553,6 +3580,14 @@ + + + + + + + + @@ -3585,6 +3620,14 @@ + + + + + + + + @@ -3657,6 +3700,14 @@ + + + + + + + + @@ -3689,6 +3740,14 @@ + + + + + + + + @@ -3721,6 +3780,14 @@ + + + + + + + + @@ -3753,6 +3820,14 @@ + + + + + + + + From 8a951a153dfaf870d3a74820313a78c3510d3033 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 09:11:38 +0000 Subject: [PATCH 19/60] Update plugin org.jetbrains.dokka to v1.9.10 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 117 +++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 17517b2870..88f7c999e9 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -22,7 +22,7 @@ ext { android_gradle_plugin_version = '8.1.2' kotlin_version = '1.9.10' detekt_gradle_plugin_version = "1.23.1" - dokka_version = "1.9.0" + dokka_version = "1.9.10" hilt_version = "2.48.1" compose_compiler_version = '1.5.3' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9ccfa66a12..6ced2953b6 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -7494,6 +7494,14 @@ + + + + + + + + @@ -7502,6 +7510,14 @@ + + + + + + + + @@ -7510,6 +7526,14 @@ + + + + + + + + @@ -7550,6 +7574,14 @@ + + + + + + + + @@ -7574,6 +7606,14 @@ + + + + + + + + @@ -7598,6 +7638,14 @@ + + + + + + + + @@ -7622,6 +7670,14 @@ + + + + + + + + @@ -7646,6 +7702,14 @@ + + + + + + + + @@ -7670,6 +7734,14 @@ + + + + + + + + @@ -7694,6 +7766,14 @@ + + + + + + + + @@ -7718,6 +7798,14 @@ + + + + + + + + @@ -7774,6 +7862,14 @@ + + + + + + + + @@ -7789,6 +7885,11 @@ + + + + + @@ -7813,6 +7914,14 @@ + + + + + + + + @@ -8482,6 +8591,14 @@ + + + + + + + + From 8aadbd2ced0526774c340364e77a3f9a0cb6e671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zg=C3=BCr=20Ta=C5=9Foluk?= <6615094+ozgur00@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:36:22 +0100 Subject: [PATCH 20/60] Revert "Revert "Refactor BCMC"" --- .../com/adyen/checkout/bcmc/BcmcComponent.kt | 90 +--- .../adyen/checkout/bcmc/BcmcComponentState.kt | 15 +- .../provider/BcmcComponentProvider.kt | 155 +++--- .../checkout/bcmc/internal/ui/BcmcDelegate.kt | 43 -- .../bcmc/internal/ui/BcmcViewProvider.kt | 4 +- .../bcmc/internal/ui/DefaultBcmcDelegate.kt | 296 ----------- .../internal/ui/model/BcmcComponentParams.kt | 29 -- .../ui/model/BcmcComponentParamsMapper.kt | 47 +- .../bcmc/internal/ui/model/BcmcInputData.kt | 18 - .../bcmc/internal/ui/model/BcmcOutputData.kt | 27 - .../bcmc/internal/ui/view/BcmcView.kt | 206 -------- .../adyen/checkout/bcmc/BcmcComponentTest.kt | 43 +- .../internal/ui/DefaultBcmcDelegateTest.kt | 487 ------------------ .../ui/model/BcmcComponentParamsMapperTest.kt | 57 +- .../com/adyen/checkout/card/CardComponent.kt | 9 +- .../internal/data/api/BinLookupService.kt | 4 +- .../api/DefaultDetectCardTypeRepository.kt | 18 +- .../data/api/DetectCardTypeRepository.kt | 5 +- .../internal/data/model/BinLookupRequest.kt | 12 +- .../internal/data/model/BinLookupResponse.kt | 4 +- .../internal/data/model/DetectedCardType.kt | 4 +- .../checkout/card/internal/ui/CardDelegate.kt | 4 +- .../card/internal/ui/DefaultCardDelegate.kt | 74 ++- .../card/internal/ui/StoredCardDelegate.kt | 24 +- .../card/internal/ui/model/CVCVisibility.kt | 21 + .../internal/ui/model/CardComponentParams.kt | 8 +- .../ui/model/CardComponentParamsMapper.kt | 20 +- .../card/internal/ui/model/CardInputData.kt | 4 +- .../card/internal/ui/model/CardListItem.kt | 4 +- .../card/internal/ui/model/CardOutputData.kt | 4 +- .../internal/ui/model/InputFieldUIState.kt | 5 +- .../internal/ui/model/InstallmentOption.kt | 5 +- .../ui/model/InstallmentOptionParams.kt | 4 +- .../internal/ui/model/InstallmentParams.kt | 4 +- .../card/internal/ui/view/CardView.kt | 11 +- .../ui/view/InstallmentListAdapter.kt | 5 +- .../card/internal/util/CardValidationUtils.kt | 10 +- .../internal/util/DetectedCardTypesUtils.kt | 3 +- .../data/api/TestDetectCardTypeRepository.kt | 1 + .../internal/ui/DefaultCardDelegateTest.kt | 29 +- .../ui/model/CardComponentParamsMapperTest.kt | 14 +- .../internal/util/CardValidationUtilsTest.kt | 62 ++- .../test/extensions/ViewModelExtensions.kt | 6 +- 43 files changed, 473 insertions(+), 1422 deletions(-) delete mode 100644 bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/BcmcDelegate.kt delete mode 100644 bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegate.kt delete mode 100644 bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParams.kt delete mode 100644 bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcInputData.kt delete mode 100644 bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcOutputData.kt delete mode 100644 bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/view/BcmcView.kt delete mode 100644 bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegateTest.kt create mode 100644 card/src/main/java/com/adyen/checkout/card/internal/ui/model/CVCVisibility.kt 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/BcmcComponentParams.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParams.kt deleted file mode 100644 index a888d5a2bb..0000000000 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParams.kt +++ /dev/null @@ -1,29 +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 josephj on 15/11/2022. - */ - -package com.adyen.checkout.bcmc.internal.ui.model - -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.ButtonParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.core.Environment -import java.util.Locale - -internal data class BcmcComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, - override val isSubmitButtonVisible: Boolean, - val isHolderNameRequired: Boolean, - val shopperReference: String?, - val isStorePaymentFieldVisible: Boolean, -) : ComponentParams, ButtonParams 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..b2f4ca0abe 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, @@ -114,10 +127,11 @@ internal class BcmcComponentParamsMapperTest { installmentOptions = null, amount = null, returnUrl = "", - ) + ), + PaymentMethod() ) - val expected = getBcmcComponentParams(isStorePaymentFieldVisible = expectedValue) + val expected = getCardComponentParams(isStorePaymentFieldVisible = expectedValue) assertEquals(expected, params) } @@ -136,7 +150,7 @@ 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, @@ -145,10 +159,11 @@ internal class BcmcComponentParamsMapperTest { installmentOptions = 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/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/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..58aaca5762 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) } @@ -308,8 +312,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 +335,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 +364,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 +481,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 +545,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 +601,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..1a975b9fed --- /dev/null +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CVCVisibility.kt @@ -0,0 +1,21 @@ +/* + * 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..161279da93 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,20 @@ 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 + 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 +110,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 +156,11 @@ internal class CardComponentParamsMapper( addressFieldPolicy.mapToAddressParamFieldPolicy() ) } + AddressConfiguration.None -> { AddressParams.None } + is AddressConfiguration.PostalCode -> { AddressParams.PostalCode(addressFieldPolicy.mapToAddressParamFieldPolicy()) } @@ -160,9 +172,11 @@ internal class CardComponentParamsMapper( is AddressConfiguration.CardAddressFieldPolicy.Optional -> { AddressFieldPolicyParams.Optional } + is AddressConfiguration.CardAddressFieldPolicy.OptionalForCardTypes -> { AddressFieldPolicyParams.OptionalForCardTypes(brands) } + is AddressConfiguration.CardAddressFieldPolicy.Required -> { AddressFieldPolicyParams.Required } 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..852a0bf8c6 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,9 @@ package com.adyen.checkout.card.internal.ui.model -internal enum class InputFieldUIState { +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..972ad84762 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,6 +8,7 @@ package com.adyen.checkout.card.internal.ui.model +import androidx.annotation.RestrictTo import com.adyen.checkout.card.CardBrand /** @@ -22,7 +23,8 @@ 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. */ -internal data class InstallmentParams( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class InstallmentParams( val defaultOptions: InstallmentOptionParams.DefaultInstallmentOptions? = null, val cardBasedOptions: List = emptyList() ) 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..74415926a3 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,6 +15,7 @@ import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.Filter import android.widget.Filterable +import androidx.annotation.RestrictTo import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import com.adyen.checkout.card.databinding.InstallmentViewBinding @@ -22,6 +23,7 @@ import com.adyen.checkout.card.internal.ui.model.InstallmentOption import com.adyen.checkout.card.internal.util.InstallmentUtils // 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,7 +66,8 @@ internal class InstallmentListAdapter( } } -internal data class InstallmentModel( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class InstallmentModel( @StringRes val textResId: Int, val value: Int?, val option: InstallmentOption 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/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..80e0655a82 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 @@ -447,7 +447,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) } } @@ -1105,23 +1105,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 +1156,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..55415eaf44 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 @@ -107,8 +107,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, @@ -451,12 +451,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 +468,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/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/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) } From f722dc13ae04c007ea2ca8ed072f8f982fc4a7bc Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:43:20 +0100 Subject: [PATCH 21/60] Fix selected card index not being reset when a new card is detected COAND-795 --- .../adyen/checkout/card/internal/ui/DefaultCardDelegate.kt | 3 +++ .../checkout/card/internal/ui/DefaultCardDelegateTest.kt | 4 ++++ 2 files changed, 7 insertions(+) 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 58aaca5762..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 @@ -244,6 +244,9 @@ class DefaultCardDelegate( } updateOutputData(detectedCardTypes = detectedCardTypes) } + .map { detectedCardTypes -> detectedCardTypes.map { it.cardBrand } } + .distinctUntilChanged() + .onEach { inputData.selectedCardIndex = -1 } .launchIn(coroutineScope) } 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 80e0655a82..67c472432b 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 } From bb4dce2d49ccec509fa16c8ef832712d8a55ed5f Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Thu, 16 Nov 2023 13:12:59 +0100 Subject: [PATCH 22/60] Allow showing amounts as part of installment options COAND-802 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 8ce139277..772177514 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/internal/ui/model/CardComponentParamsMapper.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt index 161279da9..726a6f57c 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 @@ -82,7 +82,11 @@ internal class CardComponentParamsMapper( isStorePaymentFieldVisible = isStorePaymentFieldVisible ?: true, socialSecurityNumberVisibility = socialSecurityNumberVisibility ?: SocialSecurityNumberVisibility.HIDE, kcpAuthVisibility = kcpAuthVisibility ?: KCPAuthVisibility.HIDE, - installmentParams = installmentsParamsMapper.mapToInstallmentParams(installmentConfiguration), + installmentParams = installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = installmentConfiguration, + amount = amount, + shopperLocale = shopperLocale + ), addressParams = addressConfiguration?.mapToAddressParam() ?: AddressParams.None, cvcVisibility = if (isHideCvc == true) { CVCVisibility.ALWAYS_HIDE @@ -192,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/InstallmentParams.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentParams.kt index 972ad8476..f08be8860 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 @@ -10,6 +10,8 @@ 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 @@ -26,5 +28,8 @@ import com.adyen.checkout.card.CardBrand @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? = null, + 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 dba72902b..bf942d01b 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/InstallmentListAdapter.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/InstallmentListAdapter.kt index 74415926a..1b34b4f23 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 @@ -21,6 +21,8 @@ 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) @@ -69,8 +71,11 @@ internal class InstallmentListAdapter( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class InstallmentModel( @StringRes val textResId: Int, - val value: Int?, - val option: InstallmentOption + val monthValue: 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/InstallmentUtils.kt b/card/src/main/java/com/adyen/checkout/card/internal/util/InstallmentUtils.kt index 2e0f33767..9fc6ee733 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,10 +17,14 @@ 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 java.util.Locale private const val REVOLVING_INSTALLMENT_VALUE = 1 +// TODO: Add tests internal object InstallmentUtils { /** @@ -39,41 +43,72 @@ internal object InstallmentUtils { 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 ?: false + ) } + hasDefaultInstallmentOptions -> { - makeInstallmentModelList(params?.defaultOptions) + makeInstallmentModelList( + installmentOptions = params?.defaultOptions, + amount = params?.amount, + shopperLocale = params?.shopperLocale, + showAmount = params?.showInstallmentAmount ?: false + ) } + else -> { 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 + monthValue = 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 + monthValue = REVOLVING_INSTALLMENT_VALUE, + option = InstallmentOption.REVOLVING, + amount = amount, + shopperLocale = shopperLocale, + showAmount = showAmount ) installmentOptionsList.add(revolvingOption) } + val regularOptionTextResId = if (showAmount && amount != null && shopperLocale != null) { + R.string.checkout_card_installments_option_regular_with_price + } else { + R.string.checkout_card_installments_option_regular + } val regularOptions = installmentOptions.values.map { InstallmentModel( - textResId = R.string.checkout_card_installments_option_regular, - value = it, - option = InstallmentOption.REGULAR + textResId = regularOptionTextResId, + monthValue = it, + option = InstallmentOption.REGULAR, + amount = amount, + shopperLocale = shopperLocale, + showAmount = showAmount ) } installmentOptionsList.addAll(regularOptions) @@ -83,13 +118,24 @@ 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.REGULAR -> { + val monthValue = monthValue ?: 1 + val installmentAmount = amount?.copy(value = amount.value / monthValue) + if (installmentAmount != null && shopperLocale != null) { + val formattedAmount = CurrencyUtils.formatAmount(installmentAmount, shopperLocale) + context.getString(textResId, monthValue, formattedAmount) + } else { + context.getString(textResId, monthValue) + } + } + + InstallmentOption.REVOLVING, InstallmentOption.ONE_TIME -> context.getString(textResId) + else -> "" + } } - } /** * Populate the [Installments] model object from [InstallmentModel]. @@ -97,8 +143,9 @@ internal object InstallmentUtils { fun makeInstallmentModelObject(installmentModel: InstallmentModel?): Installments? { return when (installmentModel?.option) { InstallmentOption.REGULAR, InstallmentOption.REVOLVING -> { - Installments(installmentModel.option.type, installmentModel.value) + Installments(installmentModel.option.type, installmentModel.monthValue) } + else -> null } } diff --git a/card/src/main/res/values/strings.xml b/card/src/main/res/values/strings.xml index 40af545e9..6bac9728e 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 + %1$sx %2$s One time payment Revolving payment 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 67c472432..5ba566b94 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 @@ -566,8 +566,11 @@ internal class DefaultCardDelegateTest( delegate.outputDataFlow.test { val installmentModel = InstallmentModel( textResId = R.string.checkout_card_installments_option_revolving, - value = 1, - option = InstallmentOption.REVOLVING + monthValue = 1, + option = InstallmentOption.REVOLVING, + amount = null, + shopperLocale = null, + showAmount = false ) delegate.updateInputData { @@ -806,8 +809,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 + monthValue = 1, + option = InstallmentOption.REVOLVING, + amount = null, + shopperLocale = null, + showAmount = false ) val detectedCardTypes = listOf( 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 55415eaf4..73da3b714 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")) @@ -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 = "", ) 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 478da532b..030ddda05 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/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 000000000..0dee0c993 --- /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 2ac257dc3..8370d89d3 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/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 683571140..ff1855f4c 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 4c783f09a..255bd15b8 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,6 +32,7 @@ interface KeyValueStorage { fun getCardAddressMode(): CardAddressMode fun getInstantPaymentMethodType(): String fun getInstallmentOptionsMode(): CardInstallmentOptionsMode + fun isInstallmentAmountShown(): Boolean fun useSessions(): Boolean fun setUseSessions(useSessions: Boolean) fun getAnalyticsLevel(): AnalyticsLevel @@ -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, 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 83be3db1b..53a40a0ae 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, @@ -89,7 +90,8 @@ fun getSessionRequest( 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/SessionsCardTakenOverViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverViewModel.kt index 23837213d..04ecdcb2e 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/SessionsCardViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt index df7df890a..df35a851e 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 @@ -101,7 +101,8 @@ 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 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 d06d56ee7..7ddf84f76 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 @@ -163,10 +163,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 +181,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/main/MainViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt index 0309c9322..a0742596e 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 @@ -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 diff --git a/example-app/src/main/res/values/strings.xml b/example-app/src/main/res/values/strings.xml index 37ccb9d45..16fe88174 100644 --- a/example-app/src/main/res/values/strings.xml +++ b/example-app/src/main/res/values/strings.xml @@ -54,6 +54,8 @@ 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 @@ -83,6 +85,7 @@ false NONE NONE + false wechatpaySDK true ALL diff --git a/example-app/src/main/res/xml/preferences.xml b/example-app/src/main/res/xml/preferences.xml index f8ca721d1..88b84188d 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" /> + + ? = 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 5fcf5163c..0bd517168 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, ) --- .../checkout/card/InstallmentConfiguration.kt | 3 +- .../ui/model/CardComponentParamsMapper.kt | 12 ++- .../internal/ui/model/InstallmentParams.kt | 7 +- .../ui/model/InstallmentsParamsMapper.kt | 31 +++++-- .../ui/view/InstallmentListAdapter.kt | 9 +- .../card/internal/util/InstallmentUtils.kt | 81 +++++++++++++---- card/src/main/res/values/strings.xml | 1 + .../internal/ui/DefaultCardDelegateTest.kt | 14 ++- .../ui/model/CardComponentParamsMapperTest.kt | 46 ++++++---- .../ui/model/InstallmentParamsMapperTest.kt | 88 ++++++++++++++----- .../model/SessionInstallmentConfiguration.kt | 17 ++++ .../core/internal/ui/model/SessionParams.kt | 2 +- .../example/data/api/model/SessionRequest.kt | 3 +- .../example/data/storage/KeyValueStorage.kt | 7 ++ .../checkout/example/service/RequestUtils.kt | 4 +- .../ui/card/SessionsCardTakenOverViewModel.kt | 3 +- .../example/ui/card/SessionsCardViewModel.kt | 3 +- .../CheckoutConfigurationProvider.kt | 8 +- .../checkout/example/ui/main/MainViewModel.kt | 3 +- example-app/src/main/res/values/strings.xml | 3 + example-app/src/main/res/xml/preferences.xml | 5 ++ .../core/SessionSetupConfiguration.kt | 4 + .../internal/ui/model/SessionParamsFactory.kt | 18 ++-- 23 files changed, 284 insertions(+), 88 deletions(-) create mode 100644 components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionInstallmentConfiguration.kt 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/internal/ui/model/CardComponentParamsMapper.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt index 161279da93..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 @@ -82,7 +82,11 @@ internal class CardComponentParamsMapper( isStorePaymentFieldVisible = isStorePaymentFieldVisible ?: true, socialSecurityNumberVisibility = socialSecurityNumberVisibility ?: SocialSecurityNumberVisibility.HIDE, kcpAuthVisibility = kcpAuthVisibility ?: KCPAuthVisibility.HIDE, - installmentParams = installmentsParamsMapper.mapToInstallmentParams(installmentConfiguration), + installmentParams = installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = installmentConfiguration, + amount = amount, + shopperLocale = shopperLocale + ), addressParams = addressConfiguration?.mapToAddressParam() ?: AddressParams.None, cvcVisibility = if (isHideCvc == true) { CVCVisibility.ALWAYS_HIDE @@ -192,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/InstallmentParams.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/InstallmentParams.kt index 972ad84762..f08be88608 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 @@ -10,6 +10,8 @@ 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 @@ -26,5 +28,8 @@ import com.adyen.checkout.card.CardBrand @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? = null, + 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..bf942d01b9 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/InstallmentListAdapter.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/InstallmentListAdapter.kt index 74415926a3..1b34b4f231 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 @@ -21,6 +21,8 @@ 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) @@ -69,8 +71,11 @@ internal class InstallmentListAdapter( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class InstallmentModel( @StringRes val textResId: Int, - val value: Int?, - val option: InstallmentOption + val monthValue: 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/InstallmentUtils.kt b/card/src/main/java/com/adyen/checkout/card/internal/util/InstallmentUtils.kt index 2e0f33767d..9fc6ee7338 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,10 +17,14 @@ 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 java.util.Locale private const val REVOLVING_INSTALLMENT_VALUE = 1 +// TODO: Add tests internal object InstallmentUtils { /** @@ -39,41 +43,72 @@ internal object InstallmentUtils { 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 ?: false + ) } + hasDefaultInstallmentOptions -> { - makeInstallmentModelList(params?.defaultOptions) + makeInstallmentModelList( + installmentOptions = params?.defaultOptions, + amount = params?.amount, + shopperLocale = params?.shopperLocale, + showAmount = params?.showInstallmentAmount ?: false + ) } + else -> { 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 + monthValue = 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 + monthValue = REVOLVING_INSTALLMENT_VALUE, + option = InstallmentOption.REVOLVING, + amount = amount, + shopperLocale = shopperLocale, + showAmount = showAmount ) installmentOptionsList.add(revolvingOption) } + val regularOptionTextResId = if (showAmount && amount != null && shopperLocale != null) { + R.string.checkout_card_installments_option_regular_with_price + } else { + R.string.checkout_card_installments_option_regular + } val regularOptions = installmentOptions.values.map { InstallmentModel( - textResId = R.string.checkout_card_installments_option_regular, - value = it, - option = InstallmentOption.REGULAR + textResId = regularOptionTextResId, + monthValue = it, + option = InstallmentOption.REGULAR, + amount = amount, + shopperLocale = shopperLocale, + showAmount = showAmount ) } installmentOptionsList.addAll(regularOptions) @@ -83,13 +118,24 @@ 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.REGULAR -> { + val monthValue = monthValue ?: 1 + val installmentAmount = amount?.copy(value = amount.value / monthValue) + if (installmentAmount != null && shopperLocale != null) { + val formattedAmount = CurrencyUtils.formatAmount(installmentAmount, shopperLocale) + context.getString(textResId, monthValue, formattedAmount) + } else { + context.getString(textResId, monthValue) + } + } + + InstallmentOption.REVOLVING, InstallmentOption.ONE_TIME -> context.getString(textResId) + else -> "" + } } - } /** * Populate the [Installments] model object from [InstallmentModel]. @@ -97,8 +143,9 @@ internal object InstallmentUtils { fun makeInstallmentModelObject(installmentModel: InstallmentModel?): Installments? { return when (installmentModel?.option) { InstallmentOption.REGULAR, InstallmentOption.REVOLVING -> { - Installments(installmentModel.option.type, installmentModel.value) + Installments(installmentModel.option.type, installmentModel.monthValue) } + else -> null } } diff --git a/card/src/main/res/values/strings.xml b/card/src/main/res/values/strings.xml index 40af545e9f..6bac9728ed 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 + %1$sx %2$s One time payment Revolving payment 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 67c472432b..5ba566b942 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 @@ -566,8 +566,11 @@ internal class DefaultCardDelegateTest( delegate.outputDataFlow.test { val installmentModel = InstallmentModel( textResId = R.string.checkout_card_installments_option_revolving, - value = 1, - option = InstallmentOption.REVOLVING + monthValue = 1, + option = InstallmentOption.REVOLVING, + amount = null, + shopperLocale = null, + showAmount = false ) delegate.updateInputData { @@ -806,8 +809,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 + monthValue = 1, + option = InstallmentOption.REVOLVING, + amount = null, + shopperLocale = null, + showAmount = false ) val detectedCardTypes = listOf( 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 55415eaf44..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")) @@ -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 = "", ) 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/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/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 4c783f09ad..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,6 +32,7 @@ interface KeyValueStorage { fun getCardAddressMode(): CardAddressMode fun getInstantPaymentMethodType(): String fun getInstallmentOptionsMode(): CardInstallmentOptionsMode + fun isInstallmentAmountShown(): Boolean fun useSessions(): Boolean fun setUseSessions(useSessions: Boolean) fun getAnalyticsLevel(): AnalyticsLevel @@ -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, 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..53a40a0aed 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, @@ -89,7 +90,8 @@ fun getSessionRequest( 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/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/SessionsCardViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt index df7df890ad..df35a851e2 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 @@ -101,7 +101,8 @@ 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 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 d06d56ee7f..7ddf84f76f 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 @@ -163,10 +163,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 +181,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/main/MainViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt index 0309c9322a..a0742596ea 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 @@ -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 diff --git a/example-app/src/main/res/values/strings.xml b/example-app/src/main/res/values/strings.xml index 37ccb9d452..16fe881743 100644 --- a/example-app/src/main/res/values/strings.xml +++ b/example-app/src/main/res/values/strings.xml @@ -54,6 +54,8 @@ 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 @@ -83,6 +85,7 @@ false NONE NONE + false wechatpaySDK true ALL diff --git a/example-app/src/main/res/xml/preferences.xml b/example-app/src/main/res/xml/preferences.xml index f8ca721d19..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" /> + + ? = 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, ) From 03242f502162fdeb8afd8f81595ba4c767f9f876 Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Mon, 20 Nov 2023 12:57:01 +0100 Subject: [PATCH 23/60] Add translations COAND-802 --- card/src/main/res/template/values/strings.xml.tt | 1 + card/src/main/res/values-ar/strings.xml | 1 + card/src/main/res/values-cs-rCZ/strings.xml | 1 + card/src/main/res/values-da-rDK/strings.xml | 1 + card/src/main/res/values-de-rDE/strings.xml | 1 + card/src/main/res/values-el-rGR/strings.xml | 1 + card/src/main/res/values-es-rES/strings.xml | 1 + card/src/main/res/values-fi-rFI/strings.xml | 1 + card/src/main/res/values-fr-rFR/strings.xml | 1 + card/src/main/res/values-hr-rHR/strings.xml | 1 + card/src/main/res/values-hu-rHU/strings.xml | 1 + card/src/main/res/values-it-rIT/strings.xml | 1 + card/src/main/res/values-ja-rJP/strings.xml | 1 + card/src/main/res/values-ko-rKR/strings.xml | 1 + card/src/main/res/values-nb-rNO/strings.xml | 1 + card/src/main/res/values-nl-rNL/strings.xml | 1 + card/src/main/res/values-pl-rPL/strings.xml | 1 + card/src/main/res/values-pt-rBR/strings.xml | 1 + card/src/main/res/values-pt-rPT/strings.xml | 1 + card/src/main/res/values-ro-rRO/strings.xml | 1 + card/src/main/res/values-ru-rRU/strings.xml | 1 + card/src/main/res/values-sk-rSK/strings.xml | 1 + card/src/main/res/values-sl-rSI/strings.xml | 1 + card/src/main/res/values-sv-rSE/strings.xml | 1 + card/src/main/res/values-zh-rCN/strings.xml | 1 + card/src/main/res/values-zh-rTW/strings.xml | 1 + card/src/main/res/values/strings.xml | 2 +- 27 files changed, 27 insertions(+), 1 deletion(-) 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 6bac9728ed..29efdc76a4 100644 --- a/card/src/main/res/values/strings.xml +++ b/card/src/main/res/values/strings.xml @@ -32,7 +32,7 @@ Number of installments %s months - %1$sx %2$s + %sx %s One time payment Revolving payment From 99c8da00b4c1ec8a4202aded1ff6c572f85e8716 Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Mon, 20 Nov 2023 14:24:58 +0100 Subject: [PATCH 24/60] Format numberOfInstallments options based on locale COAND-802 --- .../internal/ui/model/InstallmentParams.kt | 5 +- .../ui/model/InstallmentsParamsMapper.kt | 4 +- .../ui/view/InstallmentListAdapter.kt | 4 +- .../card/internal/util/InstallmentUtils.kt | 56 ++++++++++--------- .../internal/ui/DefaultCardDelegateTest.kt | 11 ++-- 5 files changed, 43 insertions(+), 37 deletions(-) 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 f08be88608..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 @@ -24,12 +24,15 @@ import java.util.Locale * * @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. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class InstallmentParams( val defaultOptions: InstallmentOptionParams.DefaultInstallmentOptions? = null, val cardBasedOptions: List = emptyList(), val amount: Amount? = null, - val shopperLocale: Locale? = 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 bf942d01b9..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 @@ -21,7 +21,7 @@ internal class InstallmentsParamsMapper { internal fun mapToInstallmentParams( installmentConfiguration: SessionInstallmentConfiguration?, amount: Amount?, - shopperLocale: Locale? + shopperLocale: Locale ): InstallmentParams? { installmentConfiguration?.installmentOptions ?: return null @@ -48,7 +48,7 @@ internal class InstallmentsParamsMapper { internal fun mapToInstallmentParams( installmentConfiguration: InstallmentConfiguration?, amount: Amount?, - shopperLocale: Locale? + shopperLocale: Locale ): InstallmentParams? { installmentConfiguration ?: return null return InstallmentParams( 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 1b34b4f231..9518eb8964 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 @@ -71,10 +71,10 @@ internal class InstallmentListAdapter( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class InstallmentModel( @StringRes val textResId: Int, - val monthValue: Int?, + val numberOfInstallments: Int?, val option: InstallmentOption, val amount: Amount?, - val shopperLocale: Locale?, + val shopperLocale: Locale, val showAmount: Boolean ) 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 9fc6ee7338..2712cb5216 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 @@ -20,43 +20,43 @@ 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 java.text.NumberFormat import java.util.Locale private const val REVOLVING_INSTALLMENT_VALUE = 1 -// TODO: Add tests 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 != null val hasOptionsForCardType = hasCardBasedInstallmentOptions && isCardTypeReliable && - (params?.cardBasedOptions?.any { it.cardBrand == cardBrand } ?: false) + params.cardBasedOptions.any { it.cardBrand == cardBrand } return when { hasOptionsForCardType -> { makeInstallmentModelList( - installmentOptions = params?.cardBasedOptions?.firstOrNull { it.cardBrand == cardBrand }, - amount = params?.amount, - shopperLocale = params?.shopperLocale, - showAmount = params?.showInstallmentAmount ?: false + installmentOptions = params.cardBasedOptions.firstOrNull { it.cardBrand == cardBrand }, + amount = params.amount, + shopperLocale = params.shopperLocale, + showAmount = params.showInstallmentAmount ) } hasDefaultInstallmentOptions -> { makeInstallmentModelList( - installmentOptions = params?.defaultOptions, - amount = params?.amount, - shopperLocale = params?.shopperLocale, - showAmount = params?.showInstallmentAmount ?: false + installmentOptions = params.defaultOptions, + amount = params.amount, + shopperLocale = params.shopperLocale, + showAmount = params.showInstallmentAmount ) } @@ -64,19 +64,19 @@ internal object InstallmentUtils { emptyList() } } - } + } ?: emptyList() private fun makeInstallmentModelList( installmentOptions: InstallmentOptionParams?, amount: Amount?, - shopperLocale: Locale?, + 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, - monthValue = null, + numberOfInstallments = null, option = InstallmentOption.ONE_TIME, amount = amount, shopperLocale = shopperLocale, @@ -87,7 +87,7 @@ internal object InstallmentUtils { if (installmentOptions.includeRevolving) { val revolvingOption = InstallmentModel( textResId = R.string.checkout_card_installments_option_revolving, - monthValue = REVOLVING_INSTALLMENT_VALUE, + numberOfInstallments = REVOLVING_INSTALLMENT_VALUE, option = InstallmentOption.REVOLVING, amount = amount, shopperLocale = shopperLocale, @@ -96,15 +96,15 @@ internal object InstallmentUtils { installmentOptionsList.add(revolvingOption) } - val regularOptionTextResId = if (showAmount && amount != null && shopperLocale != null) { + val regularOptionTextResId = if (showAmount && amount != null) { R.string.checkout_card_installments_option_regular_with_price } else { R.string.checkout_card_installments_option_regular } - val regularOptions = installmentOptions.values.map { + val regularOptions = installmentOptions.values.map { numberOfInstallments -> InstallmentModel( textResId = regularOptionTextResId, - monthValue = it, + numberOfInstallments = numberOfInstallments, option = InstallmentOption.REGULAR, amount = amount, shopperLocale = shopperLocale, @@ -122,13 +122,15 @@ internal object InstallmentUtils { with(installmentModel) { return when (this?.option) { InstallmentOption.REGULAR -> { - val monthValue = monthValue ?: 1 - val installmentAmount = amount?.copy(value = amount.value / monthValue) - if (installmentAmount != null && shopperLocale != null) { + val numberOfInstallments = numberOfInstallments ?: 1 + val installmentAmount = amount?.copy(value = amount.value / numberOfInstallments) + val formattedNumberOfInstallments = + NumberFormat.getInstance(shopperLocale).format(numberOfInstallments) + if (installmentAmount != null) { val formattedAmount = CurrencyUtils.formatAmount(installmentAmount, shopperLocale) - context.getString(textResId, monthValue, formattedAmount) + context.getString(textResId, formattedNumberOfInstallments, formattedAmount) } else { - context.getString(textResId, monthValue) + context.getString(textResId, formattedNumberOfInstallments) } } @@ -143,7 +145,7 @@ internal object InstallmentUtils { fun makeInstallmentModelObject(installmentModel: InstallmentModel?): Installments? { return when (installmentModel?.option) { InstallmentOption.REGULAR, InstallmentOption.REVOLVING -> { - Installments(installmentModel.option.type, installmentModel.monthValue) + Installments(installmentModel.option.type, installmentModel.numberOfInstallments) } else -> 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 5ba566b942..1887b9c74e 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 @@ -541,7 +541,8 @@ internal class DefaultCardDelegateTest( InstallmentOptionParams.DefaultInstallmentOptions( values = listOf(2, 3), includeRevolving = true - ) + ), + shopperLocale = Locale.US ) val addressConfiguration = AddressConfiguration.FullAddress() @@ -566,10 +567,10 @@ internal class DefaultCardDelegateTest( delegate.outputDataFlow.test { val installmentModel = InstallmentModel( textResId = R.string.checkout_card_installments_option_revolving, - monthValue = 1, + numberOfInstallments = 1, option = InstallmentOption.REVOLVING, amount = null, - shopperLocale = null, + shopperLocale = Locale.US, showAmount = false ) @@ -809,10 +810,10 @@ internal class DefaultCardDelegateTest( val addressUIState = AddressFormUIState.FULL_ADDRESS val installmentModel = InstallmentModel( textResId = R.string.checkout_card_installments_option_revolving, - monthValue = 1, + numberOfInstallments = 1, option = InstallmentOption.REVOLVING, amount = null, - shopperLocale = null, + shopperLocale = Locale.US, showAmount = false ) From 5fa0a21e199f80fab58993339de8ebcd029b59b4 Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Tue, 21 Nov 2023 15:06:44 +0100 Subject: [PATCH 25/60] Add unit tests for NumberExtension, InstallmentUtils and CurrencyUtils COAND-802 --- ...ACHDirectDebitComponentParamsMapperTest.kt | 4 +- .../ui/model/BcmcComponentParamsMapperTest.kt | 4 +- .../model/BoletoComponentParamsMapperTest.kt | 2 +- .../card/internal/util/InstallmentUtils.kt | 32 +- .../internal/util/InstallmentUtilsTest.kt | 537 ++++++++++++++++++ .../CashAppPayComponentParamsMapperTest.kt | 4 +- .../core/internal/util/NumberExtension.kt | 22 + .../model/ButtonComponentParamsMapperTest.kt | 2 +- .../model/GenericComponentParamsMapperTest.kt | 2 +- .../core/internal/util/CurrencyUtilsTest.kt | 67 +++ .../core/internal/util/NumberExtensionTest.kt | 36 ++ .../GooglePayComponentParamsMapperTest.kt | 2 +- .../IssuerListComponentParamsMapperTest.kt | 2 +- 13 files changed, 689 insertions(+), 27 deletions(-) create mode 100644 card/src/test/java/com/adyen/checkout/card/internal/util/InstallmentUtilsTest.kt create mode 100644 components-core/src/main/java/com/adyen/checkout/components/core/internal/util/NumberExtension.kt create mode 100644 components-core/src/test/java/com/adyen/checkout/components/core/internal/util/CurrencyUtilsTest.kt create mode 100644 components-core/src/test/java/com/adyen/checkout/components/core/internal/util/NumberExtensionTest.kt 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/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 b2f4ca0abe..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 @@ -124,7 +124,7 @@ internal class BcmcComponentParamsMapperTest { bcmcConfiguration = bcmcConfiguration, sessionParams = SessionParams( enableStoreDetails = sessionsValue, - installmentOptions = null, + installmentConfiguration = null, amount = null, returnUrl = "", ), @@ -156,7 +156,7 @@ internal class BcmcComponentParamsMapperTest { bcmcConfiguration, sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", ), 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/internal/util/InstallmentUtils.kt b/card/src/main/java/com/adyen/checkout/card/internal/util/InstallmentUtils.kt index 2712cb5216..b65d6feb47 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 @@ -20,7 +20,7 @@ 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 java.text.NumberFormat +import com.adyen.checkout.components.core.internal.util.format import java.util.Locale private const val REVOLVING_INSTALLMENT_VALUE = 1 @@ -36,7 +36,7 @@ internal object InstallmentUtils { isCardTypeReliable: Boolean ): List = installmentParams?.let { params -> val hasCardBasedInstallmentOptions = params.cardBasedOptions.isNotEmpty() - val hasDefaultInstallmentOptions = params.defaultOptions != null + val hasDefaultInstallmentOptions = !params.defaultOptions?.values.isNullOrEmpty() val hasOptionsForCardType = hasCardBasedInstallmentOptions && isCardTypeReliable && params.cardBasedOptions.any { it.cardBrand == cardBrand } @@ -124,11 +124,11 @@ internal object InstallmentUtils { InstallmentOption.REGULAR -> { val numberOfInstallments = numberOfInstallments ?: 1 val installmentAmount = amount?.copy(value = amount.value / numberOfInstallments) - val formattedNumberOfInstallments = - NumberFormat.getInstance(shopperLocale).format(numberOfInstallments) - if (installmentAmount != null) { - val formattedAmount = CurrencyUtils.formatAmount(installmentAmount, shopperLocale) - context.getString(textResId, formattedNumberOfInstallments, formattedAmount) + val formattedNumberOfInstallments = numberOfInstallments.format(shopperLocale) + + if (showAmount && installmentAmount != null) { + val formattedInstallmentAmount = CurrencyUtils.formatAmount(installmentAmount, shopperLocale) + context.getString(textResId, formattedNumberOfInstallments, formattedInstallmentAmount) } else { context.getString(textResId, formattedNumberOfInstallments) } @@ -142,14 +142,12 @@ internal object InstallmentUtils { /** * 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.numberOfInstallments) - } - - else -> null + fun makeInstallmentModelObject(installmentModel: InstallmentModel?) = when (installmentModel?.option) { + InstallmentOption.REGULAR, InstallmentOption.REVOLVING -> { + Installments(installmentModel.option.type, installmentModel.numberOfInstallments) } + + else -> null } /** @@ -161,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 } @@ -173,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/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..31b5195559 --- /dev/null +++ b/card/src/test/java/com/adyen/checkout/card/internal/util/InstallmentUtilsTest.kt @@ -0,0 +1,537 @@ +/* + * 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 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.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 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()) + } + + @Test + fun `get text for installment option gets a string, if installment option is one time`() { + val textResourceId = Int.MAX_VALUE + val installmentModel = InstallmentModel( + textResId = textResourceId, + numberOfInstallments = null, + option = InstallmentOption.ONE_TIME, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ) + + InstallmentUtils.getTextForInstallmentOption(context, installmentModel) + + verify(context).getString(textResourceId) + } + + @Test + fun `get text for installment option gets a string, if installment option is revolving`() { + val textResourceId = Int.MAX_VALUE + val installmentModel = InstallmentModel( + textResId = textResourceId, + numberOfInstallments = null, + option = InstallmentOption.REVOLVING, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ) + + InstallmentUtils.getTextForInstallmentOption(context, installmentModel) + + verify(context).getString(textResourceId) + } + + @Test + fun `get text for installment option gets a string, if installment option is regular and amount is not shown`() { + val textResourceId = Int.MAX_VALUE + val installmentModel = InstallmentModel( + textResId = textResourceId, + numberOfInstallments = 2, + option = InstallmentOption.REGULAR, + amount = Amount("USD", 100L), + shopperLocale = Locale.US, + showAmount = false + ) + + InstallmentUtils.getTextForInstallmentOption(context, installmentModel) + + verify(context).getString(textResourceId, "2") + } + + @Test + fun `get text for installment option gets a string, if installment option is regular and amount is null`() { + val textResourceId = Int.MAX_VALUE + val installmentModel = InstallmentModel( + textResId = textResourceId, + numberOfInstallments = 2, + option = InstallmentOption.REGULAR, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ) + + InstallmentUtils.getTextForInstallmentOption(context, installmentModel) + + verify(context).getString(textResourceId, "2") + } + + @Test + fun `get text for installment option gets a string, if installment option is regular and amount is shown`() { + val textResourceId = Int.MAX_VALUE + val installmentModel = InstallmentModel( + textResId = textResourceId, + numberOfInstallments = 3, + option = InstallmentOption.REGULAR, + amount = Amount("USD", 10000L), + shopperLocale = Locale.US, + showAmount = true + ) + + InstallmentUtils.getTextForInstallmentOption(context, installmentModel) + + verify(context).getString(textResourceId, "3", "$33.33") + } + + @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 noValidInstallmentOptionForMakeInstallmentModelObject() = listOf( + arguments(null), + arguments( + InstallmentModel( + textResId = Int.MAX_VALUE, + numberOfInstallments = null, + option = InstallmentOption.ONE_TIME, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ) + ) + ) + + @JvmStatic + fun validInstallmentOptionForMakeInstallmentModelObject() = listOf( + arguments( + InstallmentModel( + textResId = Int.MAX_VALUE, + numberOfInstallments = null, + option = InstallmentOption.REGULAR, + amount = null, + shopperLocale = Locale.US, + showAmount = false + ) + ), + arguments( + InstallmentModel( + textResId = Int.MAX_VALUE, + 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-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..5ac2e0c075 --- /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.format(locale: Locale): String = NumberFormat.getInstance(locale).format(this) 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..22f7926b1b --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/CurrencyUtilsTest.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 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 java.util.Locale + +internal class CurrencyUtilsTest { + + @Test + fun `format amount with nl-NL locale`() { + val amount = Amount("EUR", 10050L) + val locale = Locale.forLanguageTag("nl-NL") + + val formattedAmount = CurrencyUtils.formatAmount(amount, locale) + + assertEquals("€ 100,50", 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", 10050L) + val locale = Locale.forLanguageTag("en-US") + + val formattedAmount = CurrencyUtils.formatAmount(amount, locale) + + assertEquals("$100.50", formattedAmount) + } + + @Test + fun `assert currency does nothing, if currency code is supported`() { + val currencyCode = "EUR" + + val formattedAmount = CurrencyUtils.assertCurrency(currencyCode) + + assertEquals(Unit, formattedAmount) + } + + @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..ff2c338431 --- /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 formats with en-US locale`() { + val locale = Locale.forLanguageTag("en-US") + + assertEquals("1", 1.format(locale)) + assertEquals("5", 5.format(locale)) + assertEquals("10", 10.format(locale)) + assertEquals("15", 15.format(locale)) + } + + @Test + fun `integer format formats with ar-LB locale`() { + val locale = Locale.forLanguageTag("ar-LB") + + assertEquals("١", 1.format(locale)) + assertEquals("٥", 5.format(locale)) + assertEquals("١٠", 10.format(locale)) + assertEquals("١٥", 15.format(locale)) + } +} 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..85ef40eff5 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 @@ -373,7 +373,7 @@ internal class GooglePayComponentParamsMapperTest { PaymentMethod(), sessionParams = SessionParams( enableStoreDetails = null, - installmentOptions = null, + installmentConfiguration = null, amount = sessionsValue, returnUrl = "", ) 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 = "", ) From 1d95c41e3ac544072bf27a0df97b433030fa91ba Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Tue, 21 Nov 2023 15:35:18 +0100 Subject: [PATCH 26/60] Do optimisations and improve tests for InstallmentUtils COAND-802 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 9518eb896..64ec510ab 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 @@ -16,7 +16,6 @@ import android.widget.BaseAdapter import android.widget.Filter import android.widget.Filterable import androidx.annotation.RestrictTo -import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import com.adyen.checkout.card.databinding.InstallmentViewBinding import com.adyen.checkout.card.internal.ui.model.InstallmentOption @@ -70,7 +69,6 @@ internal class InstallmentListAdapter( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class InstallmentModel( - @StringRes val textResId: Int, val numberOfInstallments: Int?, val option: InstallmentOption, val amount: Amount?, 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 b65d6feb4..1df2a34ac 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 @@ -75,7 +75,6 @@ internal object InstallmentUtils { if (installmentOptions == null) return emptyList() val installmentOptionsList = mutableListOf() val oneTimeOption = InstallmentModel( - textResId = R.string.checkout_card_installments_option_one_time, numberOfInstallments = null, option = InstallmentOption.ONE_TIME, amount = amount, @@ -86,7 +85,6 @@ internal object InstallmentUtils { if (installmentOptions.includeRevolving) { val revolvingOption = InstallmentModel( - textResId = R.string.checkout_card_installments_option_revolving, numberOfInstallments = REVOLVING_INSTALLMENT_VALUE, option = InstallmentOption.REVOLVING, amount = amount, @@ -96,14 +94,8 @@ internal object InstallmentUtils { installmentOptionsList.add(revolvingOption) } - val regularOptionTextResId = if (showAmount && amount != null) { - R.string.checkout_card_installments_option_regular_with_price - } else { - R.string.checkout_card_installments_option_regular - } val regularOptions = installmentOptions.values.map { numberOfInstallments -> InstallmentModel( - textResId = regularOptionTextResId, numberOfInstallments = numberOfInstallments, option = InstallmentOption.REGULAR, amount = amount, @@ -121,6 +113,8 @@ internal object InstallmentUtils { 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) @@ -128,13 +122,19 @@ internal object InstallmentUtils { if (showAmount && installmentAmount != null) { val formattedInstallmentAmount = CurrencyUtils.formatAmount(installmentAmount, shopperLocale) - context.getString(textResId, formattedNumberOfInstallments, formattedInstallmentAmount) + context.getString( + R.string.checkout_card_installments_option_regular_with_price, + formattedNumberOfInstallments, + formattedInstallmentAmount + ) } else { - context.getString(textResId, formattedNumberOfInstallments) + context.getString( + R.string.checkout_card_installments_option_regular, + formattedNumberOfInstallments + ) } } - InstallmentOption.REVOLVING, InstallmentOption.ONE_TIME -> context.getString(textResId) else -> "" } } 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 1887b9c74..3b941ed63 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 @@ -566,7 +566,6 @@ internal class DefaultCardDelegateTest( delegate.outputDataFlow.test { val installmentModel = InstallmentModel( - textResId = R.string.checkout_card_installments_option_revolving, numberOfInstallments = 1, option = InstallmentOption.REVOLVING, amount = null, @@ -809,7 +808,6 @@ internal class DefaultCardDelegateTest( val addressUIState = AddressFormUIState.FULL_ADDRESS val installmentModel = InstallmentModel( - textResId = R.string.checkout_card_installments_option_revolving, numberOfInstallments = 1, option = InstallmentOption.REVOLVING, amount = null, 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 index 31b519555..ccaa5157e 100644 --- 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 @@ -13,6 +13,7 @@ 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 @@ -161,9 +162,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is one time`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_one_time val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = null, option = InstallmentOption.ONE_TIME, amount = null, @@ -178,9 +178,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is revolving`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_revolving val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = null, option = InstallmentOption.REVOLVING, amount = null, @@ -195,9 +194,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is regular and amount is not shown`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_regular val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = 2, option = InstallmentOption.REGULAR, amount = Amount("USD", 100L), @@ -212,9 +210,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is regular and amount is null`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_regular val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = 2, option = InstallmentOption.REGULAR, amount = null, @@ -229,9 +226,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is regular and amount is shown`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_regular_with_price val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = 3, option = InstallmentOption.REGULAR, amount = Amount("USD", 10000L), @@ -359,7 +355,6 @@ internal class InstallmentUtilsTest { arguments(null), arguments( InstallmentModel( - textResId = Int.MAX_VALUE, numberOfInstallments = null, option = InstallmentOption.ONE_TIME, amount = null, @@ -373,7 +368,6 @@ internal class InstallmentUtilsTest { fun validInstallmentOptionForMakeInstallmentModelObject() = listOf( arguments( InstallmentModel( - textResId = Int.MAX_VALUE, numberOfInstallments = null, option = InstallmentOption.REGULAR, amount = null, @@ -383,7 +377,6 @@ internal class InstallmentUtilsTest { ), arguments( InstallmentModel( - textResId = Int.MAX_VALUE, numberOfInstallments = null, option = InstallmentOption.REVOLVING, amount = null, --- .../ui/view/InstallmentListAdapter.kt | 2 -- .../card/internal/util/InstallmentUtils.kt | 22 +++++++++---------- .../internal/ui/DefaultCardDelegateTest.kt | 2 -- .../internal/util/InstallmentUtilsTest.kt | 19 +++++----------- 4 files changed, 17 insertions(+), 28 deletions(-) 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 9518eb8964..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 @@ -16,7 +16,6 @@ import android.widget.BaseAdapter import android.widget.Filter import android.widget.Filterable import androidx.annotation.RestrictTo -import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import com.adyen.checkout.card.databinding.InstallmentViewBinding import com.adyen.checkout.card.internal.ui.model.InstallmentOption @@ -70,7 +69,6 @@ internal class InstallmentListAdapter( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class InstallmentModel( - @StringRes val textResId: Int, val numberOfInstallments: Int?, val option: InstallmentOption, val amount: Amount?, 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 b65d6feb47..1df2a34ac5 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 @@ -75,7 +75,6 @@ internal object InstallmentUtils { if (installmentOptions == null) return emptyList() val installmentOptionsList = mutableListOf() val oneTimeOption = InstallmentModel( - textResId = R.string.checkout_card_installments_option_one_time, numberOfInstallments = null, option = InstallmentOption.ONE_TIME, amount = amount, @@ -86,7 +85,6 @@ internal object InstallmentUtils { if (installmentOptions.includeRevolving) { val revolvingOption = InstallmentModel( - textResId = R.string.checkout_card_installments_option_revolving, numberOfInstallments = REVOLVING_INSTALLMENT_VALUE, option = InstallmentOption.REVOLVING, amount = amount, @@ -96,14 +94,8 @@ internal object InstallmentUtils { installmentOptionsList.add(revolvingOption) } - val regularOptionTextResId = if (showAmount && amount != null) { - R.string.checkout_card_installments_option_regular_with_price - } else { - R.string.checkout_card_installments_option_regular - } val regularOptions = installmentOptions.values.map { numberOfInstallments -> InstallmentModel( - textResId = regularOptionTextResId, numberOfInstallments = numberOfInstallments, option = InstallmentOption.REGULAR, amount = amount, @@ -121,6 +113,8 @@ internal object InstallmentUtils { 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) @@ -128,13 +122,19 @@ internal object InstallmentUtils { if (showAmount && installmentAmount != null) { val formattedInstallmentAmount = CurrencyUtils.formatAmount(installmentAmount, shopperLocale) - context.getString(textResId, formattedNumberOfInstallments, formattedInstallmentAmount) + context.getString( + R.string.checkout_card_installments_option_regular_with_price, + formattedNumberOfInstallments, + formattedInstallmentAmount + ) } else { - context.getString(textResId, formattedNumberOfInstallments) + context.getString( + R.string.checkout_card_installments_option_regular, + formattedNumberOfInstallments + ) } } - InstallmentOption.REVOLVING, InstallmentOption.ONE_TIME -> context.getString(textResId) else -> "" } } 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 1887b9c74e..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 @@ -566,7 +566,6 @@ internal class DefaultCardDelegateTest( delegate.outputDataFlow.test { val installmentModel = InstallmentModel( - textResId = R.string.checkout_card_installments_option_revolving, numberOfInstallments = 1, option = InstallmentOption.REVOLVING, amount = null, @@ -809,7 +808,6 @@ internal class DefaultCardDelegateTest( val addressUIState = AddressFormUIState.FULL_ADDRESS val installmentModel = InstallmentModel( - textResId = R.string.checkout_card_installments_option_revolving, numberOfInstallments = 1, option = InstallmentOption.REVOLVING, amount = null, 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 index 31b5195559..ccaa5157ea 100644 --- 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 @@ -13,6 +13,7 @@ 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 @@ -161,9 +162,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is one time`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_one_time val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = null, option = InstallmentOption.ONE_TIME, amount = null, @@ -178,9 +178,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is revolving`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_revolving val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = null, option = InstallmentOption.REVOLVING, amount = null, @@ -195,9 +194,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is regular and amount is not shown`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_regular val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = 2, option = InstallmentOption.REGULAR, amount = Amount("USD", 100L), @@ -212,9 +210,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is regular and amount is null`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_regular val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = 2, option = InstallmentOption.REGULAR, amount = null, @@ -229,9 +226,8 @@ internal class InstallmentUtilsTest { @Test fun `get text for installment option gets a string, if installment option is regular and amount is shown`() { - val textResourceId = Int.MAX_VALUE + val textResourceId = R.string.checkout_card_installments_option_regular_with_price val installmentModel = InstallmentModel( - textResId = textResourceId, numberOfInstallments = 3, option = InstallmentOption.REGULAR, amount = Amount("USD", 10000L), @@ -359,7 +355,6 @@ internal class InstallmentUtilsTest { arguments(null), arguments( InstallmentModel( - textResId = Int.MAX_VALUE, numberOfInstallments = null, option = InstallmentOption.ONE_TIME, amount = null, @@ -373,7 +368,6 @@ internal class InstallmentUtilsTest { fun validInstallmentOptionForMakeInstallmentModelObject() = listOf( arguments( InstallmentModel( - textResId = Int.MAX_VALUE, numberOfInstallments = null, option = InstallmentOption.REGULAR, amount = null, @@ -383,7 +377,6 @@ internal class InstallmentUtilsTest { ), arguments( InstallmentModel( - textResId = Int.MAX_VALUE, numberOfInstallments = null, option = InstallmentOption.REVOLVING, amount = null, From 3dd68a6312131b157afedb9c27d822cea97b8de3 Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Wed, 22 Nov 2023 10:49:34 +0100 Subject: [PATCH 27/60] Update release notes COAND-802 --- RELEASE_NOTES.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index eb455388be..4094463323 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -9,5 +9,4 @@ [//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object) ## New -- You can now override payment method names in drop in by using `DropInConfiguration.Builder.overridePaymentMethodName(type, name)` -- For stored cards, Drop-in will show the card name ("Visa", "Mastercard"...), instead of "Credit Card" +- Now it is possible to show installment amounts for card payments. From 6d44e09f5ec7aac1cc439c38e9edd660fb533ae8 Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Thu, 23 Nov 2023 10:31:52 +0100 Subject: [PATCH 28/60] Address CR comments COAND-802 --- .../card/internal/util/InstallmentUtils.kt | 4 +- .../internal/util/InstallmentUtilsTest.kt | 168 +++++++++++------- .../core/internal/util/NumberExtension.kt | 2 +- .../core/internal/util/CurrencyUtilsTest.kt | 15 +- .../core/internal/util/NumberExtensionTest.kt | 20 +-- 5 files changed, 125 insertions(+), 84 deletions(-) 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 1df2a34ac5..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 @@ -20,7 +20,7 @@ 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.format +import com.adyen.checkout.components.core.internal.util.formatToLocalizedString import java.util.Locale private const val REVOLVING_INSTALLMENT_VALUE = 1 @@ -118,7 +118,7 @@ internal object InstallmentUtils { InstallmentOption.REGULAR -> { val numberOfInstallments = numberOfInstallments ?: 1 val installmentAmount = amount?.copy(value = amount.value / numberOfInstallments) - val formattedNumberOfInstallments = numberOfInstallments.format(shopperLocale) + val formattedNumberOfInstallments = numberOfInstallments.formatToLocalizedString(shopperLocale) if (showAmount && installmentAmount != null) { val formattedInstallmentAmount = CurrencyUtils.formatAmount(installmentAmount, shopperLocale) 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 index ccaa5157ea..2e7f632728 100644 --- 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 @@ -9,6 +9,7 @@ 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 @@ -19,6 +20,7 @@ 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 @@ -160,84 +162,44 @@ internal class InstallmentUtilsTest { assertTrue(installmentOptionText.isEmpty()) } - @Test - fun `get text for installment option gets a string, if installment option is one time`() { - val textResourceId = R.string.checkout_card_installments_option_one_time - val installmentModel = InstallmentModel( - numberOfInstallments = null, - option = InstallmentOption.ONE_TIME, - amount = null, - shopperLocale = Locale.US, - showAmount = false - ) - - InstallmentUtils.getTextForInstallmentOption(context, installmentModel) - - verify(context).getString(textResourceId) - } - - @Test - fun `get text for installment option gets a string, if installment option is revolving`() { - val textResourceId = R.string.checkout_card_installments_option_revolving - val installmentModel = InstallmentModel( - numberOfInstallments = null, - option = InstallmentOption.REVOLVING, - amount = null, - shopperLocale = Locale.US, - showAmount = false - ) - + @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) } - @Test - fun `get text for installment option gets a string, if installment option is regular and amount is not shown`() { - val textResourceId = R.string.checkout_card_installments_option_regular - val installmentModel = InstallmentModel( - numberOfInstallments = 2, - option = InstallmentOption.REGULAR, - amount = Amount("USD", 100L), - shopperLocale = Locale.US, - showAmount = false - ) - - InstallmentUtils.getTextForInstallmentOption(context, installmentModel) - - verify(context).getString(textResourceId, "2") - } - - @Test - fun `get text for installment option gets a string, if installment option is regular and amount is null`() { + @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 installmentModel = InstallmentModel( - numberOfInstallments = 2, - option = InstallmentOption.REGULAR, - amount = null, - shopperLocale = Locale.US, - showAmount = false - ) + val formattedNumberOfInstallments = + installmentModel.numberOfInstallments?.formatToLocalizedString(installmentModel.shopperLocale) InstallmentUtils.getTextForInstallmentOption(context, installmentModel) - verify(context).getString(textResourceId, "2") + verify(context).getString(textResourceId, formattedNumberOfInstallments) } - @Test - fun `get text for installment option gets a string, if installment option is regular and amount is shown`() { + @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 installmentModel = InstallmentModel( - numberOfInstallments = 3, - option = InstallmentOption.REGULAR, - amount = Amount("USD", 10000L), - shopperLocale = Locale.US, - showAmount = true - ) + val formattedNumberOfInstallments = + installmentModel.numberOfInstallments?.formatToLocalizedString(installmentModel.shopperLocale) InstallmentUtils.getTextForInstallmentOption(context, installmentModel) - verify(context).getString(textResourceId, "3", "$33.33") + verify(context).getString(textResourceId, formattedNumberOfInstallments, installmentAmount) } @ParameterizedTest @@ -350,6 +312,86 @@ internal class InstallmentUtilsTest { 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), 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 index 5ac2e0c075..5b92a81319 100644 --- 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 @@ -19,4 +19,4 @@ import java.util.Locale * @return A formatted string displaying value. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -fun Int.format(locale: Locale): String = NumberFormat.getInstance(locale).format(this) +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/util/CurrencyUtilsTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/CurrencyUtilsTest.kt index 22f7926b1b..3e75e411cb 100644 --- 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 @@ -13,18 +13,19 @@ 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", 10050L) + val amount = Amount("EUR", 1075L) val locale = Locale.forLanguageTag("nl-NL") val formattedAmount = CurrencyUtils.formatAmount(amount, locale) - assertEquals("€ 100,50", formattedAmount) + assertEquals("€ 10,75", formattedAmount) } @Test @@ -39,21 +40,19 @@ internal class CurrencyUtilsTest { @Test fun `format amount with en-US locale`() { - val amount = Amount("USD", 10050L) + val amount = Amount("USD", 220000L) val locale = Locale.forLanguageTag("en-US") val formattedAmount = CurrencyUtils.formatAmount(amount, locale) - assertEquals("$100.50", formattedAmount) + assertEquals("$2,200.00", formattedAmount) } @Test - fun `assert currency does nothing, if currency code is supported`() { + fun `assert currency does not throw an exception, if currency code is supported`() { val currencyCode = "EUR" - val formattedAmount = CurrencyUtils.assertCurrency(currencyCode) - - assertEquals(Unit, formattedAmount) + assertDoesNotThrow { CurrencyUtils.assertCurrency(currencyCode) } } @Test 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 index ff2c338431..3fa2fa9180 100644 --- 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 @@ -15,22 +15,22 @@ import java.util.Locale internal class NumberExtensionTest { @Test - fun `integer format formats with en-US locale`() { + fun `integer format to localized string formats with en-US locale`() { val locale = Locale.forLanguageTag("en-US") - assertEquals("1", 1.format(locale)) - assertEquals("5", 5.format(locale)) - assertEquals("10", 10.format(locale)) - assertEquals("15", 15.format(locale)) + assertEquals("1", 1.formatToLocalizedString(locale)) + assertEquals("5", 5.formatToLocalizedString(locale)) + assertEquals("10", 10.formatToLocalizedString(locale)) + assertEquals("15", 15.formatToLocalizedString(locale)) } @Test - fun `integer format formats with ar-LB locale`() { + fun `integer format to localized string formats with ar-LB locale`() { val locale = Locale.forLanguageTag("ar-LB") - assertEquals("١", 1.format(locale)) - assertEquals("٥", 5.format(locale)) - assertEquals("١٠", 10.format(locale)) - assertEquals("١٥", 15.format(locale)) + assertEquals("١", 1.formatToLocalizedString(locale)) + assertEquals("٥", 5.formatToLocalizedString(locale)) + assertEquals("١٠", 10.formatToLocalizedString(locale)) + assertEquals("١٥", 15.formatToLocalizedString(locale)) } } From d5fe9cf8592635413dec534e8292f406c4c39d31 Mon Sep 17 00:00:00 2001 From: josephj Date: Thu, 16 Nov 2023 18:06:09 +0100 Subject: [PATCH 29/60] Update ktlint to version 1.0.1 --- config/gradle/ktlint.gradle | 2 +- dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/dependencies.gradle b/dependencies.gradle index 88f7c999e9..22c208a03c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -28,7 +28,7 @@ ext { // Code quality detekt_version = "1.23.1" - ktlint_version = '0.50.0' + ktlint_version = '1.0.1' // Android Dependencies annotation_version = "1.7.0" From 05a80b1aeb4c502f367b4f93121c19d2c155667f Mon Sep 17 00:00:00 2001 From: josephj Date: Thu, 16 Nov 2023 18:40:49 +0100 Subject: [PATCH 30/60] Update verification-metadata.xml --- gradle/verification-metadata.xml | 162 +++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6ced2953b6..a1a14f28cd 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5173,6 +5173,14 @@ + + + + + + + + @@ -5189,6 +5197,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -5205,6 +5285,14 @@ + + + + + + + + @@ -5237,6 +5325,14 @@ + + + + + + + + @@ -5381,6 +5477,14 @@ + + + + + + + + @@ -5397,6 +5501,14 @@ + + + + + + + + @@ -5429,6 +5541,14 @@ + + + + + + + + @@ -6072,6 +6192,19 @@ + + + + + + + + + + + + + @@ -6088,6 +6221,14 @@ + + + + + + + + @@ -6117,6 +6258,11 @@ + + + + + @@ -6125,6 +6271,14 @@ + + + + + + + + @@ -6141,6 +6295,14 @@ + + + + + + + + From 02afea929f51a598d87850c94e09d9b5992d5c0c Mon Sep 17 00:00:00 2001 From: josephj Date: Thu, 16 Nov 2023 18:41:14 +0100 Subject: [PATCH 31/60] Fix new ktlint rules with 1.0.1 --- .editorconfig | 7 +++++-- .../com/adyen/checkout/card/KCPAuthVisibility.kt | 3 ++- .../card/SocialSecurityNumberVisibility.kt | 3 ++- .../card/internal/ui/model/CVCVisibility.kt | 7 +++++-- .../card/internal/ui/model/InputFieldUIState.kt | 4 +++- .../core/internal/data/api/AnalyticsMapper.kt | 16 ++++++++-------- .../core/internal/ui/model/FieldState.kt | 2 +- .../checkout/cse/internal/ClientSideEncrypter.kt | 14 +++++++------- .../example/data/api/CheckoutApiService.kt | 4 ++-- .../checkout/example/ui/main/MainViewModel.kt | 16 ++++++++-------- .../qrcode/internal/ui/QrCodeViewProvider.kt | 4 +++- .../ui/core/internal/ui/AddressFormUIState.kt | 4 +++- .../voucher/internal/ui/VoucherViewProvider.kt | 3 ++- 13 files changed, 51 insertions(+), 36 deletions(-) diff --git a/.editorconfig b/.editorconfig index 5aa737404a..8c0e665baf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -472,10 +472,13 @@ 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_standard_discouraged-comment-location = disabled +ktlint_standard_type-parameter-list-spacing = 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 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/ui/model/CVCVisibility.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CVCVisibility.kt index 1a975b9fed..fea2fec269 100644 --- 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 @@ -12,10 +12,13 @@ import androidx.annotation.RestrictTo @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) enum class CVCVisibility { - ALWAYS_SHOW, HIDE_FIRST, ALWAYS_HIDE + ALWAYS_SHOW, + HIDE_FIRST, + ALWAYS_HIDE } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) enum class StoredCVCVisibility { - SHOW, HIDE + SHOW, + HIDE } 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 852a0bf8c6..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 @@ -12,5 +12,7 @@ import androidx.annotation.RestrictTo @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) enum class InputFieldUIState { - REQUIRED, OPTIONAL, HIDDEN + REQUIRED, + OPTIONAL, + HIDDEN } 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 70ce5befed..5a1bd2605a 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 = VERSION, + version = version, channel = ANDROID_CHANNEL, - platform = PLATFORM, + platform = platform, locale = locale.toString(), component = getComponentQueryParameter(source), flavor = getFlavorQueryParameter(source), @@ -75,22 +75,22 @@ class AnalyticsMapper { private const val DROP_IN_COMPONENT = "dropin" private const val ANDROID_CHANNEL = "android" - private var PLATFORM = AnalyticsPlatform.ANDROID.value - private var VERSION = BuildConfig.CHECKOUT_VERSION + private var platform = AnalyticsPlatform.ANDROID.value + private var version = BuildConfig.CHECKOUT_VERSION @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun overrideForCrossPlatform( platform: AnalyticsPlatform, version: String, ) { - PLATFORM = platform.value - VERSION = version + this.platform = platform.value + this.version = version } @VisibleForTesting internal fun resetToDefaults() { - PLATFORM = AnalyticsPlatform.ANDROID.value - VERSION = BuildConfig.CHECKOUT_VERSION + platform = AnalyticsPlatform.ANDROID.value + version = BuildConfig.CHECKOUT_VERSION } } } 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/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/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/ui/main/MainViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt index a0742596ea..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) } @@ -195,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() { @@ -208,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/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/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/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 } From c95e82ee346c518d4c8608aaac8268256e3459b4 Mon Sep 17 00:00:00 2001 From: josephj Date: Thu, 16 Nov 2023 19:08:47 +0100 Subject: [PATCH 32/60] Fix ktlint rules discouraged-comment-location and parameter-list-spacing except in some base classes with generics where they don't work well --- .editorconfig | 2 -- .../ActionHandlingPaymentMethodConfigurationBuilder.kt | 6 +++++- .../components/core/internal/BaseConfigurationBuilder.kt | 1 + .../components/core/internal/data/api/AnalyticsMapper.kt | 3 ++- .../econtext/internal/provider/EContextComponentProvider.kt | 1 + .../java/com/adyen/checkout/example/service/RequestUtils.kt | 3 ++- .../internal/provider/IssuerListComponentProvider.kt | 1 + .../internal/provider/OnlineBankingComponentProvider.kt | 1 + 8 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.editorconfig b/.editorconfig index 8c0e665baf..ce9feb1564 100644 --- a/.editorconfig +++ b/.editorconfig @@ -476,8 +476,6 @@ 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_standard_discouraged-comment-location = disabled -ktlint_standard_type-parameter-list-spacing = disabled ktlint_function_naming_ignore_when_annotated_with = Composable max_line_length = 120 ij_kotlin_align_in_columns_case_branch = false 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/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 5a1bd2605a..c49317842b 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 @@ -43,7 +43,8 @@ class AnalyticsMapper { screenWidth = screenWidth, paymentMethods = paymentMethods, amount = amount, - containerWidth = null, // unused for Android, + // unused for Android + containerWidth = null, sessionId = sessionId, ) } 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/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt b/example-app/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt index 53a40a0aed..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 @@ -85,7 +85,8 @@ 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, 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/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, From b97df6acbbc41f9f38ed70f31489bb7d3d0c5587 Mon Sep 17 00:00:00 2001 From: josephj Date: Wed, 22 Nov 2023 17:35:41 +0100 Subject: [PATCH 33/60] Rename version and platform in AnalyticsMapper to provide more clarity --- .../core/internal/data/api/AnalyticsMapper.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 c49317842b..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 = version, + version = actualVersion, channel = ANDROID_CHANNEL, - platform = platform, + platform = actualPlatform, locale = locale.toString(), component = getComponentQueryParameter(source), flavor = getFlavorQueryParameter(source), @@ -76,22 +76,24 @@ class AnalyticsMapper { private const val DROP_IN_COMPONENT = "dropin" private const val ANDROID_CHANNEL = "android" - private var platform = AnalyticsPlatform.ANDROID.value - private var version = BuildConfig.CHECKOUT_VERSION + // 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.platform = platform.value - this.version = version + this.actualPlatform = platform.value + this.actualVersion = version } @VisibleForTesting internal fun resetToDefaults() { - platform = AnalyticsPlatform.ANDROID.value - version = BuildConfig.CHECKOUT_VERSION + actualPlatform = AnalyticsPlatform.ANDROID.value + actualVersion = BuildConfig.CHECKOUT_VERSION } } } From c5334547c44182d0cc8dde1303070a5082b17ddd Mon Sep 17 00:00:00 2001 From: josephj Date: Fri, 24 Nov 2023 14:16:46 +0100 Subject: [PATCH 34/60] Add allow trailing comma rules The IDE will automatically add any missing trailing commas when you format a file --- .editorconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index ce9feb1564..95d358222f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -484,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 From 1ba2349933aae557f745cb72b8b755a836a80114 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 14:42:53 +0000 Subject: [PATCH 35/60] Update dependency androidx.compose:compose-bom to v2023.10.01 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 130 +++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 22c208a03c..e9514ae5a4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -43,7 +43,7 @@ ext { // Compose Dependencies compose_activity_version = '1.8.0' - compose_bom_version = '2023.09.01' + compose_bom_version = '2023.10.01' compose_viewmodel_version = '2.6.2' // Adyen Dependencies diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a1a14f28cd..93c9e177fc 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -459,6 +459,11 @@ + + + + + @@ -528,6 +533,14 @@ + + + + + + + + @@ -544,6 +557,14 @@ + + + + + + + + @@ -576,6 +597,14 @@ + + + + + + + + @@ -592,6 +621,14 @@ + + + + + + + + @@ -629,6 +666,14 @@ + + + + + + + + @@ -645,6 +690,14 @@ + + + + + + + + @@ -677,6 +730,14 @@ + + + + + + + + @@ -693,6 +754,14 @@ + + + + + + + + @@ -725,6 +794,14 @@ + + + + + + + + @@ -741,6 +818,14 @@ + + + + + + + + @@ -773,6 +858,14 @@ + + + + + + + + @@ -789,6 +882,14 @@ + + + + + + + + @@ -821,6 +922,14 @@ + + + + + + + + @@ -837,6 +946,14 @@ + + + + + + + + @@ -863,6 +980,11 @@ + + + + + @@ -879,6 +1001,14 @@ + + + + + + + + From 313d13dbe4f4e44cb5040c8d4954ba551b548d29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 00:13:39 +0000 Subject: [PATCH 36/60] Update detekt_version to v1.23.3 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 192 +++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index e9514ae5a4..dbe5f54ed5 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -27,7 +27,7 @@ ext { compose_compiler_version = '1.5.3' // Code quality - detekt_version = "1.23.1" + detekt_version = "1.23.3" ktlint_version = '1.0.1' // Android Dependencies diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 93c9e177fc..30dc81259c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -6457,6 +6457,14 @@ + + + + + + + + @@ -6481,6 +6489,14 @@ + + + + + + + + @@ -6505,6 +6521,14 @@ + + + + + + + + @@ -6529,6 +6553,14 @@ + + + + + + + + @@ -6577,6 +6609,14 @@ + + + + + + + + @@ -6601,6 +6641,14 @@ + + + + + + + + @@ -6625,6 +6673,14 @@ + + + + + + + + @@ -6649,6 +6705,14 @@ + + + + + + + + @@ -6673,6 +6737,14 @@ + + + + + + + + @@ -6697,6 +6769,14 @@ + + + + + + + + @@ -6721,6 +6801,14 @@ + + + + + + + + @@ -6745,6 +6833,14 @@ + + + + + + + + @@ -6769,6 +6865,14 @@ + + + + + + + + @@ -6793,6 +6897,14 @@ + + + + + + + + @@ -6817,6 +6929,14 @@ + + + + + + + + @@ -6841,6 +6961,14 @@ + + + + + + + + @@ -6865,6 +6993,14 @@ + + + + + + + + @@ -6889,6 +7025,14 @@ + + + + + + + + @@ -6913,6 +7057,14 @@ + + + + + + + + @@ -6937,6 +7089,14 @@ + + + + + + + + @@ -6961,6 +7121,14 @@ + + + + + + + + @@ -6985,6 +7153,14 @@ + + + + + + + + @@ -7009,6 +7185,14 @@ + + + + + + + + @@ -7033,6 +7217,14 @@ + + + + + + + + From 80f58805a8f54090dfd74b588694b567bc13fe76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 10:53:50 +0000 Subject: [PATCH 37/60] Update dependency androidx.recyclerview:recyclerview to v1.3.2 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index dbe5f54ed5..75841e6d62 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -38,7 +38,7 @@ ext { fragment_version = "1.6.1" lifecycle_version = "2.5.1" material_version = "1.10.0" - recyclerview_version = "1.3.1" + recyclerview_version = "1.3.2" constraintlayout_version = '2.1.4' // Compose Dependencies diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 30dc81259c..23640a6af5 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2025,6 +2025,14 @@ + + + + + + + + From 28e81b6f66b9744dc26b0add390a4d52ae1ca792 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:09:16 +0000 Subject: [PATCH 38/60] Update plugin io.gitlab.arturbosch.detekt to v1.23.3 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 75841e6d62..076fa11204 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -21,7 +21,7 @@ ext { // Build Script android_gradle_plugin_version = '8.1.2' kotlin_version = '1.9.10' - detekt_gradle_plugin_version = "1.23.1" + detekt_gradle_plugin_version = "1.23.3" dokka_version = "1.9.10" hilt_version = "2.48.1" compose_compiler_version = '1.5.3' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 23640a6af5..f316e69cb8 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -6593,6 +6593,14 @@ + + + + + + + + @@ -7248,6 +7256,11 @@ + + + + + From c1c36c102202918562466b14d3fd33a8e17d4f25 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Wed, 22 Nov 2023 15:08:07 +0100 Subject: [PATCH 39/60] Add pinRequired configuration for gift card component COAND-741 --- .../checkout/dropin/DropInConfiguration.kt | 9 +++ .../CheckoutConfigurationProvider.kt | 2 + .../giftcard/GiftCardConfiguration.kt | 16 +++++ .../provider/GiftCardComponentProvider.kt | 4 +- .../internal/ui/DefaultGiftCardDelegate.kt | 37 +++++++--- .../giftcard/internal/ui/GiftCardDelegate.kt | 2 + .../ui/model/GiftCardComponentParams.kt | 27 ++++++++ .../ui/model/GiftCardComponentParamsMapper.kt | 67 +++++++++++++++++++ .../internal/ui/model/GiftCardOutputData.kt | 12 ++-- .../giftcard/internal/ui/view/GiftCardView.kt | 49 ++++++++------ .../ui/DefaultGiftCardDelegateTest.kt | 38 +++++++++-- 11 files changed, 217 insertions(+), 46 deletions(-) create mode 100644 giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt create mode 100644 giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapper.kt 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 3bf9676563..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 @@ -30,6 +30,7 @@ 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 @@ -379,6 +380,14 @@ 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. 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 7ddf84f76f..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()) { 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/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt new file mode 100644 index 0000000000..4bfbaa3b0b --- /dev/null +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt @@ -0,0 +1,27 @@ +/* + * 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.Amount +import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams +import com.adyen.checkout.components.core.internal.ui.model.ButtonParams +import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.core.Environment +import java.util.Locale + +internal data class GiftCardComponentParams( + override val shopperLocale: Locale, + override val environment: Environment, + override val clientKey: String, + override val analyticsParams: AnalyticsParams, + override val isCreatedByDropIn: Boolean, + override val amount: Amount?, + override val isSubmitButtonVisible: 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") From 4694ac66cb63c603be04899dcbd08dce0b077839 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:29:58 +0000 Subject: [PATCH 40/60] Update dependency org.robolectric:robolectric to v4.11.1 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 194 +++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 076fa11204..65e9558789 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -70,7 +70,7 @@ ext { 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" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f316e69cb8..e3aa327222 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -4598,6 +4598,14 @@ + + + + + + + + @@ -4611,6 +4619,11 @@ + + + + + @@ -5008,6 +5021,14 @@ + + + + + + + + @@ -5086,6 +5107,11 @@ + + + + + @@ -5287,6 +5313,14 @@ + + + + + + + + @@ -7741,6 +7775,14 @@ + + + + + + + + @@ -10190,6 +10232,14 @@ + + + + + + + + @@ -10206,6 +10256,14 @@ + + + + + + + + @@ -10222,6 +10280,14 @@ + + + + + + + + @@ -10238,6 +10304,14 @@ + + + + + + + + @@ -10254,6 +10328,14 @@ + + + + + + + + @@ -10270,6 +10352,14 @@ + + + + + + + + @@ -10286,6 +10376,14 @@ + + + + + + + + @@ -10302,6 +10400,14 @@ + + + + + + + + @@ -10310,6 +10416,14 @@ + + + + + + + + @@ -10326,6 +10440,14 @@ + + + + + + + + @@ -10342,6 +10464,14 @@ + + + + + + + + @@ -10358,6 +10488,14 @@ + + + + + + + + @@ -10374,6 +10512,14 @@ + + + + + + + + @@ -10390,6 +10536,14 @@ + + + + + + + + @@ -10406,6 +10560,14 @@ + + + + + + + + @@ -10422,6 +10584,22 @@ + + + + + + + + + + + + + + + + @@ -10438,6 +10616,14 @@ + + + + + + + + @@ -10454,6 +10640,14 @@ + + + + + + + + From ac9bbc72d05f0bd73afeba9063c80e6a98c63187 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:41:33 +0000 Subject: [PATCH 41/60] Update okhttp monorepo to v4.12.0 --- dependencies.gradle | 4 ++-- gradle/verification-metadata.xml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 65e9558789..7c7687ad86 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -51,7 +51,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,7 +59,7 @@ 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' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e3aa327222..849dd9b7ba 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -6192,6 +6192,14 @@ + + + + + + + + @@ -6210,6 +6218,14 @@ + + + + + + + + @@ -6228,6 +6244,14 @@ + + + + + + + + @@ -6236,6 +6260,14 @@ + + + + + + + + From fb63e1cd5a90679afa3e82de2ac6a1d59bae3d6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:04:27 +0000 Subject: [PATCH 42/60] Update dependency androidx.fragment:fragment-ktx to v1.6.2 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 7c7687ad86..bf5df1a37e 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -35,7 +35,7 @@ ext { 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.10.0" recyclerview_version = "1.3.2" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 849dd9b7ba..c498f5817d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1431,6 +1431,14 @@ + + + + + + + + @@ -1463,6 +1471,14 @@ + + + + + + + + From 71ae855a6bba0dd06187ee9817639e1d309d74cf Mon Sep 17 00:00:00 2001 From: Ararat Mnatsakanyan Date: Fri, 1 Dec 2023 10:39:18 +0100 Subject: [PATCH 43/60] Reverted back the changes made in AdyenComponentView to add the component view only on next layout. Those changes would cause Compose to not re-draw the components when used in lazy lists. Instead, disabled the `windowEnterAnimation` for ActionComponentDialogFragment, which caused the dialog to jump before navigating to an external browser activity. COAND-825 --- RELEASE_NOTES.md | 4 ++-- .../internal/ui/ActionComponentDialogFragment.kt | 6 +++++- .../adyen/checkout/ui/core/AdyenComponentView.kt | 13 +++---------- ui-core/src/main/res/values/styles_bottom_sheet.xml | 5 +++++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4094463323..6259c9655c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,5 +8,5 @@ [//]: # (## Deprecated) [//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object) -## New -- Now it is possible to show installment amounts for card payments. +## Fixed +- Fixed the bug which would not show components in Compose lazy lists. 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/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/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 + + + From c057ca585340d10928daa6462a47346a96703bc2 Mon Sep 17 00:00:00 2001 From: josephj Date: Tue, 5 Dec 2023 17:31:53 +0100 Subject: [PATCH 44/60] Add tests for GooglePayUtils COAND-830 --- .../internal/util/GooglePayUtilsTest.kt | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 googlepay/src/test/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtilsTest.kt 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, + ), + ) + } +} From 6523e4e2e7c5840b39dfb05cb083bbf652ef54f5 Mon Sep 17 00:00:00 2001 From: josephj Date: Tue, 5 Dec 2023 17:36:28 +0100 Subject: [PATCH 45/60] Add getGooglePayButtonParameters to GooglePayComponent It's basically just a wrapper around allowedPaymentMethods in case something else is required for the button later on COAND-830 --- .../googlepay/GooglePayButtonParameters.kt | 16 ++++ .../checkout/googlepay/GooglePayComponent.kt | 8 ++ .../internal/ui/DefaultGooglePayDelegate.kt | 16 +++- .../internal/ui/GooglePayDelegate.kt | 3 + .../googlepay/internal/util/GooglePayUtils.kt | 90 +++++++++---------- 5 files changed, 86 insertions(+), 47 deletions(-) create mode 100644 googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayButtonParameters.kt 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/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() - 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 } } From 45f18d4335de3a5313e550cd81219c12acfacb48 Mon Sep 17 00:00:00 2001 From: josephj Date: Tue, 5 Dec 2023 17:48:33 +0100 Subject: [PATCH 46/60] Make AllowedAuthMethods and AllowedCardNetworks public COAND-830 --- .../checkout/googlepay/AllowedAuthMethods.kt | 20 ++++++ .../checkout/googlepay/AllowedCardNetworks.kt | 24 +++++++ .../googlepay/GooglePayConfiguration.kt | 4 +- .../model/GooglePayComponentParamsMapper.kt | 6 +- .../internal/util/AllowedAuthMethods.kt | 21 ------- .../internal/util/AllowedCardNetworks.kt | 26 -------- .../GooglePayComponentParamsMapperTest.kt | 62 +++++++++---------- 7 files changed, 80 insertions(+), 83 deletions(-) create mode 100644 googlepay/src/main/java/com/adyen/checkout/googlepay/AllowedAuthMethods.kt create mode 100644 googlepay/src/main/java/com/adyen/checkout/googlepay/AllowedCardNetworks.kt delete mode 100644 googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/AllowedAuthMethods.kt delete mode 100644 googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/AllowedCardNetworks.kt 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/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/model/GooglePayComponentParamsMapper.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapper.kt index 4bf0568af4..c97e413667 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapper.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapper.kt @@ -18,9 +18,9 @@ import com.adyen.checkout.core.Environment 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.googlepay.AllowedAuthMethods +import com.adyen.checkout.googlepay.AllowedCardNetworks import com.adyen.checkout.googlepay.GooglePayConfiguration -import com.adyen.checkout.googlepay.internal.util.AllowedAuthMethods -import com.adyen.checkout.googlepay.internal.util.AllowedCardNetworks import com.google.android.gms.wallet.WalletConstants internal class GooglePayComponentParamsMapper( @@ -79,7 +79,7 @@ internal class GooglePayComponentParamsMapper( ?: paymentMethod.configuration?.gatewayMerchantId ?: throw ComponentException( "GooglePay merchantAccount not found. Update your API version or pass it manually inside your " + - "GooglePayConfiguration" + "GooglePayConfiguration", ) } diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/AllowedAuthMethods.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/AllowedAuthMethods.kt deleted file mode 100644 index b4fc882b6e..0000000000 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/AllowedAuthMethods.kt +++ /dev/null @@ -1,21 +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 22/7/2019. - */ -package com.adyen.checkout.googlepay.internal.util - -internal object AllowedAuthMethods { - - private const val PAN_ONLY = "PAN_ONLY" - private const val CRYPTOGRAM_3DS = "CRYPTOGRAM_3DS" - - /** - * The the Google Pay authentication methods accepted by Adyen. - * - * @return A list of the allowed authentication methods. - */ - val allAllowedAuthMethods: List = 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/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 85ef40eff5..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( @@ -376,11 +376,11 @@ internal class GooglePayComponentParamsMapperTest { 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") From aec762be8587e46897ba5239e0863a2dea73b7cc Mon Sep 17 00:00:00 2001 From: josephj Date: Tue, 5 Dec 2023 18:07:03 +0100 Subject: [PATCH 47/60] Restore previous release notes --- RELEASE_NOTES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 6259c9655c..e7ca776c14 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,5 +8,11 @@ [//]: # (## Deprecated) [//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object) +## New +- You can now override payment method names in drop in by using `DropInConfiguration.Builder.overridePaymentMethodName(type, name)`. +- For stored cards, Drop-in will show the card name ("Visa", "Mastercard"...), instead of "Credit Card". +- Now it is possible to show installment amounts for card payments using `InstallmentConfiguration.showInstallmentAmount` in `CardConfiguration.Builder.setInstallmentConfigurations()`. +- For gift cards, you can now hide the PIN text field using `GiftCardConfiguration.Builder.setPinRequired()`. + ## Fixed - Fixed the bug which would not show components in Compose lazy lists. From 74882b674696cec76217e70a4315c52088230f5a Mon Sep 17 00:00:00 2001 From: josephj Date: Wed, 6 Dec 2023 10:59:33 +0100 Subject: [PATCH 48/60] Update release notes for Google Pay COAND-830 --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e7ca776c14..16a538c50c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -13,6 +13,8 @@ - For stored cards, Drop-in will show the card name ("Visa", "Mastercard"...), instead of "Credit Card". - Now it is possible to show installment amounts for card payments using `InstallmentConfiguration.showInstallmentAmount` in `CardConfiguration.Builder.setInstallmentConfigurations()`. - For gift cards, you can now hide the PIN text field using `GiftCardConfiguration.Builder.setPinRequired()`. +- 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()` to get the `allowedPaymentMethods` attribute. +- For Google Pay, you can now use `AllowedAuthMethods` and `AllowedCardNetworks` to easily access to the possible values for `GooglePayConfiguration.Builder.setAllowedAuthMethods()` and `GooglePayConfiguration.Builder.setAllowedCardNetworks()`. ## Fixed - Fixed the bug which would not show components in Compose lazy lists. From 1a7ff194102db56ebc39b7bcc2d564662737c888 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Thu, 30 Nov 2023 14:02:33 +0100 Subject: [PATCH 49/60] Add compose dependencies COAND-826 --- dependencies.gradle | 8 + example-app/build.gradle | 17 ++- gradle/verification-metadata.xml | 249 +++++++++++++++++++++++++++++++ 3 files changed, 273 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index bf5df1a37e..6c0d0f53fb 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -44,6 +44,7 @@ ext { // Compose Dependencies 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 @@ -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/example-app/build.gradle b/example-app/build.gradle index c5c8a69457..d45810f85f 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -55,13 +55,19 @@ android { } buildFeatures { + compose true viewBinding true } + + composeOptions { + kotlinCompilerExtensionVersion = compose_compiler_version + } } dependencies { // Checkout implementation project(':drop-in') + implementation project(':components-compose') // implementation "com.adyen.checkout:drop-in:5.0.1" // Dependencies @@ -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/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index c498f5817d..46ba1da5bd 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -464,6 +464,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -496,11 +528,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -634,6 +756,11 @@ + + + + + @@ -826,6 +953,14 @@ + + + + + + + + @@ -890,6 +1025,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1362,6 +1539,14 @@ + + + + + + + + @@ -1370,6 +1555,14 @@ + + + + + + + + @@ -1471,6 +1664,22 @@ + + + + + + + + + + + + + + + + @@ -1956,6 +2165,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 1afa81e0ddce839727003f061883600bd428a818 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Thu, 30 Nov 2023 14:03:36 +0100 Subject: [PATCH 50/60] Remove unnecessary compose compiler args The Kotlin and Compose compiler versions no longer mismatch, so this is no longer needed. COAND-826 --- components-compose/build.gradle | 7 ------- drop-in-compose/build.gradle | 7 ------- 2 files changed, 14 deletions(-) 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/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 } From 1ea76a0d485d566e10895cb7ddd66d65ddb54b2d Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Thu, 30 Nov 2023 14:05:19 +0100 Subject: [PATCH 51/60] Setup Compose theming COAND-826 --- .../example/CheckoutExampleApplication.kt | 12 ++- .../adyen/checkout/example/di/ThemeModule.kt | 4 +- .../ui/configuration/ConfigurationActivity.kt | 4 +- .../adyen/checkout/example/ui/theme/Color.kt | 67 ++++++++++++++ .../checkout/example/ui/theme/ExampleTheme.kt | 90 +++++++++++++++++++ .../ui/{ => theme}/NightThemeRepository.kt | 2 +- 6 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 example-app/src/main/java/com/adyen/checkout/example/ui/theme/Color.kt create mode 100644 example-app/src/main/java/com/adyen/checkout/example/ui/theme/ExampleTheme.kt rename example-app/src/main/java/com/adyen/checkout/example/ui/{ => theme}/NightThemeRepository.kt (97%) 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/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/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/theme/Color.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/Color.kt new file mode 100644 index 0000000000..220a133f3e --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/Color.kt @@ -0,0 +1,67 @@ +package com.adyen.checkout.example.ui.theme + +import androidx.compose.ui.graphics.Color + +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(0xFFBA1A1A) +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 used 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_shadow = Color(0xFF000000) +val md_theme_light_surfaceTint = Color(0xFF006E2C) +val md_theme_light_outlineVariant = Color(0xFFC1C9BE) +val md_theme_light_scrim = Color(0xFF000000) + +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(0xFFFFB4AB) +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 used 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_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFF47E270) +val md_theme_dark_outlineVariant = Color(0xFF424940) +val md_theme_dark_scrim = Color(0xFF000000) 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..d183f52149 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/ExampleTheme.kt @@ -0,0 +1,90 @@ +package com.adyen.checkout.example.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable + + +private 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, +) + + +private 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, +) + +@Composable +fun ExampleTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable() () -> Unit +) { + val colors = if (!useDarkTheme) { + LightColors + } else { + DarkColors + } + + MaterialTheme( + colorScheme = colors, + content = content + ) +} 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 From c0963907e4be650b47c9ddfacd36f37a5bc967b8 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Thu, 30 Nov 2023 14:07:24 +0100 Subject: [PATCH 52/60] Migrate Card Component with sessions example to compose COAND-826 --- example-app/src/main/AndroidManifest.xml | 2 +- .../example/ui/card/SessionsCardActivity.kt | 142 ------------------ .../example/ui/card/SessionsCardUiState.kt | 22 +++ .../example/ui/card/SessionsCardViewModel.kt | 73 +++++---- .../ui/card/compose/SessionsCardActivity.kt | 56 +++++++ .../ui/card/compose/SessionsCardScreen.kt | 135 +++++++++++++++++ .../checkout/example/ui/main/MainActivity.kt | 2 +- 7 files changed, 251 insertions(+), 181 deletions(-) delete mode 100644 example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardActivity.kt create mode 100644 example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.kt create mode 100644 example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardActivity.kt create mode 100644 example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardScreen.kt 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/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/SessionsCardUiState.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.kt new file mode 100644 index 0000000000..759c980abc --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.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 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 + +@Immutable +internal data class SessionsCardUiState( + val cardConfiguration: CardConfiguration, + val isLoading: Boolean = false, + val toastMessage: String? = null, + val componentData: SessionsCardComponentData? = null, + val action: Action? = 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 df35a851e2..fa8efe8a72 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 @@ -22,6 +22,7 @@ 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.configuration.CheckoutConfigurationProvider import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.CheckoutSessionProvider @@ -30,10 +31,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 +47,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? { @@ -102,8 +96,8 @@ internal class SessionsCardViewModel @Inject constructor( shopperEmail = keyValueStorage.getShopperEmail(), allowedPaymentMethods = listOf(paymentMethodType), installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), - showInstallmentAmount = keyValueStorage.isInstallmentAmountShown() - ) + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), + ), ) ?: return null return getCheckoutSession(sessionModel, cardConfiguration) @@ -120,30 +114,35 @@ 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) + onError(componentError.errorMessage) } - override fun onFinished(result: SessionPaymentResult) { - viewModelScope.launch { _events.emit(CardEvent.PaymentResult(result.resultCode.orEmpty())) } + private fun onError(message: String) { + updateUiState { it.copy(toastMessage = "Error: $message") } } - private fun onComponentError(error: ComponentError) { - viewModelScope.launch { _events.emit(CardEvent.PaymentResult("Failed: ${error.errorMessage}")) } + override fun onFinished(result: SessionPaymentResult) { + updateUiState { it.copy(toastMessage = "Finished: ${result.resultCode}") } } 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 toastMessageConsumed() { + updateUiState { it.copy(toastMessage = 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..a6a522e2f6 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardActivity.kt @@ -0,0 +1,56 @@ +/* + * 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 android.view.WindowManager +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.NightTheme +import com.adyen.checkout.example.ui.theme.NightThemeRepository +import com.adyen.checkout.redirect.RedirectComponent +import com.adyen.checkout.example.ui.theme.ExampleTheme +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..926a601984 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardScreen.kt @@ -0,0 +1,135 @@ +/* + * 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.SessionsCardViewModel + +@Composable +internal fun SessionsCardScreen( + onBackPressed: () -> Unit, +) { + 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 -> + MainContent(Modifier.padding(innerPadding)) + } +} + +@Composable +private fun MainContent( + modifier: Modifier = Modifier, + viewModel: SessionsCardViewModel = hiltViewModel(), +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + val uiState by viewModel.uiState.collectAsState() + + val (cardConfiguration, isLoading, toastMessage, componentData, action) = uiState + + if (isLoading) { + CircularProgressIndicator() + } + + if (toastMessage != null) { + Toast.makeText(LocalContext.current, toastMessage, Toast.LENGTH_SHORT).show() + viewModel.toastMessageConsumed() + } + + if (componentData != null) { + CardComponent( + configuration = cardConfiguration, + componentData = componentData, + action = action, + onActionConsumed = viewModel::actionConsumed, + 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/main/MainActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt index cc50957579..b2ddf6a396 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,7 +30,7 @@ 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.compose.SessionsCardActivity import com.adyen.checkout.example.ui.card.SessionsCardTakenOverActivity import com.adyen.checkout.example.ui.configuration.ConfigurationActivity import com.adyen.checkout.example.ui.giftcard.GiftCardActivity From 3e1d0f4017e0197c42525aaca3218b8e6d78c35a Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Thu, 30 Nov 2023 17:06:26 +0100 Subject: [PATCH 53/60] Add result screen when payment flow is finished COAND-826 --- .../example/ui/card/SessionsCardUiState.kt | 2 + .../example/ui/card/SessionsCardViewModel.kt | 16 ++++- .../ui/card/compose/SessionsCardScreen.kt | 7 ++- .../example/ui/compose/ResultContent.kt | 58 +++++++++++++++++++ .../example/ui/compose/ResultState.kt | 20 +++++++ .../adyen/checkout/example/ui/theme/Color.kt | 23 +++++--- .../checkout/example/ui/theme/ExampleTheme.kt | 58 ++++++++++++++----- .../main/res/drawable/ic_result_failure.xml | 24 ++++++++ .../main/res/drawable/ic_result_pending.xml | 44 ++++++++++++++ .../main/res/drawable/ic_result_success.xml | 20 +++++++ 10 files changed, 248 insertions(+), 24 deletions(-) create mode 100644 example-app/src/main/java/com/adyen/checkout/example/ui/compose/ResultContent.kt create mode 100644 example-app/src/main/java/com/adyen/checkout/example/ui/compose/ResultState.kt create mode 100644 example-app/src/main/res/drawable/ic_result_failure.xml create mode 100644 example-app/src/main/res/drawable/ic_result_pending.xml create mode 100644 example-app/src/main/res/drawable/ic_result_success.xml 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 index 759c980abc..9ddf36d529 100644 --- 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 @@ -11,6 +11,7 @@ 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( @@ -19,4 +20,5 @@ internal data class SessionsCardUiState( val toastMessage: 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 fa8efe8a72..0306a573b0 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 @@ -23,6 +23,7 @@ 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 @@ -126,7 +127,20 @@ internal class SessionsCardViewModel @Inject constructor( } override fun onFinished(result: SessionPaymentResult) { - updateUiState { it.copy(toastMessage = "Finished: ${result.resultCode}") } + updateUiState { + it.copy( + toastMessage = "Finished: ${result.resultCode}", + finalResult = getFinalResultState(result), + ) + } + } + + private fun getFinalResultState(result: SessionPaymentResult): ResultState = when (result.resultCode) { + "Authorised" -> ResultState.SUCCESS + "Pending", + "Received" -> ResultState.PENDING + + else -> ResultState.FAILURE } override fun onLoading(isLoading: Boolean) { 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 index 926a601984..1ff41422a4 100644 --- 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 @@ -45,6 +45,7 @@ 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.SessionsCardViewModel +import com.adyen.checkout.example.ui.compose.ResultContent @Composable internal fun SessionsCardScreen( @@ -78,7 +79,7 @@ private fun MainContent( ) { val uiState by viewModel.uiState.collectAsState() - val (cardConfiguration, isLoading, toastMessage, componentData, action) = uiState + val (cardConfiguration, isLoading, toastMessage, componentData, action, finalResult) = uiState if (isLoading) { CircularProgressIndicator() @@ -89,7 +90,9 @@ private fun MainContent( viewModel.toastMessageConsumed() } - if (componentData != null) { + if (finalResult != null) { + ResultContent(finalResult) + } else if (componentData != null) { CardComponent( configuration = cardConfiguration, componentData = componentData, 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..40a414cca6 --- /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.LocalCustomColorScheme + +@Composable +internal fun ResultContent( + resultState: ResultState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + val tint = when (resultState) { + ResultState.SUCCESS -> LocalCustomColorScheme.current.success + ResultState.PENDING -> LocalCustomColorScheme.current.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(16.dp)) + 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/theme/Color.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/Color.kt index 220a133f3e..18394f27d3 100644 --- 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 @@ -1,7 +1,7 @@ package com.adyen.checkout.example.ui.theme 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) @@ -14,7 +14,7 @@ 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(0xFFBA1A1A) +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) @@ -24,16 +24,21 @@ 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 used for now + +// 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_shadow = Color(0xFF000000) 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) @@ -46,7 +51,7 @@ 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(0xFFFFB4AB) +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) @@ -56,12 +61,16 @@ 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 used for now + +// 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_shadow = Color(0xFF000000) 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) 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 index d183f52149..f1d85245aa 100644 --- 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 @@ -2,10 +2,13 @@ package com.adyen.checkout.example.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.lightColorScheme import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable - +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color private val LightColors = lightColorScheme( primary = md_theme_light_primary, @@ -39,7 +42,6 @@ private val LightColors = lightColorScheme( scrim = md_theme_light_scrim, ) - private val DarkColors = darkColorScheme( primary = md_theme_dark_primary, onPrimary = md_theme_dark_onPrimary, @@ -72,19 +74,47 @@ private val DarkColors = darkColorScheme( scrim = md_theme_dark_scrim, ) +@Immutable +data class CustomColorScheme( + val success: Color = Color.Unspecified, + val warning: Color = Color.Unspecified, +) + +val LocalCustomColorScheme = staticCompositionLocalOf { CustomColorScheme() } + +private val CustomLightColors = CustomColorScheme( + success = md_theme_light_success, + warning = md_theme_light_warning, +) + +private val CustomDarkColors = CustomColorScheme( + success = md_theme_dark_success, + warning = md_theme_dark_warning, +) + @Composable fun ExampleTheme( - useDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable() () -> Unit + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable() () -> Unit ) { - val colors = if (!useDarkTheme) { - LightColors - } else { - DarkColors - } + val colors = if (!useDarkTheme) { + LightColors + } else { + DarkColors + } + + val customColors = if (!useDarkTheme) { + CustomLightColors + } else { + CustomDarkColors + } - MaterialTheme( - colorScheme = colors, - content = content - ) + CompositionLocalProvider( + LocalCustomColorScheme provides customColors, + ) { + MaterialTheme( + colorScheme = colors, + content = content, + ) + } } 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..23b59ed6ea --- /dev/null +++ b/example-app/src/main/res/drawable/ic_result_failure.xml @@ -0,0 +1,24 @@ + + + + + + + + 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..18abb04e34 --- /dev/null +++ b/example-app/src/main/res/drawable/ic_result_success.xml @@ -0,0 +1,20 @@ + + + + + From 28e4144cfbcd7b6625b07fa59c0154d27f152910 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Thu, 30 Nov 2023 17:13:37 +0100 Subject: [PATCH 54/60] Add copyright notice to files that missed it COAND-826 --- .../java/com/adyen/checkout/example/ui/theme/Color.kt | 8 ++++++++ .../com/adyen/checkout/example/ui/theme/ExampleTheme.kt | 8 ++++++++ example-app/src/main/res/drawable/ic_result_failure.xml | 8 ++++++++ example-app/src/main/res/drawable/ic_result_success.xml | 8 ++++++++ 4 files changed, 32 insertions(+) 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 index 18394f27d3..d04a67136a 100644 --- 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 @@ -1,3 +1,11 @@ +/* + * 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.ui.graphics.Color 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 index f1d85245aa..113994f0eb 100644 --- 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 @@ -1,3 +1,11 @@ +/* + * 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 diff --git a/example-app/src/main/res/drawable/ic_result_failure.xml b/example-app/src/main/res/drawable/ic_result_failure.xml index 23b59ed6ea..bf77201c10 100644 --- a/example-app/src/main/res/drawable/ic_result_failure.xml +++ b/example-app/src/main/res/drawable/ic_result_failure.xml @@ -1,3 +1,11 @@ + + + Date: Thu, 30 Nov 2023 17:22:27 +0100 Subject: [PATCH 55/60] Tweak Detekt rules for Compose COAND-826 --- config/detekt/detekt.yml | 8 ++++++++ .../example/ui/card/compose/SessionsCardActivity.kt | 3 +-- .../example/ui/card/compose/SessionsCardScreen.kt | 1 + .../com/adyen/checkout/example/ui/main/MainActivity.kt | 10 +++++----- .../adyen/checkout/example/ui/theme/ExampleTheme.kt | 2 +- 5 files changed, 16 insertions(+), 8 deletions(-) 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/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 index a6a522e2f6..64c7bcf984 100644 --- 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 @@ -10,15 +10,14 @@ package com.adyen.checkout.example.ui.card.compose import android.content.Intent import android.os.Bundle -import android.view.WindowManager 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 com.adyen.checkout.example.ui.theme.ExampleTheme import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject 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 index 1ff41422a4..2509041cd0 100644 --- 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 @@ -68,6 +68,7 @@ internal fun SessionsCardScreen( } } +@Suppress("DestructuringDeclarationWithTooManyEntries") @Composable private fun MainContent( modifier: Modifier = Modifier, 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 b2ddf6a396..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.compose.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/theme/ExampleTheme.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/ExampleTheme.kt index 113994f0eb..ba7081ce89 100644 --- 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 @@ -103,7 +103,7 @@ private val CustomDarkColors = CustomColorScheme( @Composable fun ExampleTheme( useDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable() () -> Unit + content: @Composable () -> Unit ) { val colors = if (!useDarkTheme) { LightColors From 6d44effdef04730e4c2c6c2728267b646f28432b Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Mon, 4 Dec 2023 12:11:54 +0100 Subject: [PATCH 56/60] Handle user cancellation COAND-826 --- .../example/ui/card/SessionsCardViewModel.kt | 12 +++++++++++- .../example/ui/card/compose/SessionsCardScreen.kt | 7 +++++-- 2 files changed, 16 insertions(+), 3 deletions(-) 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 0306a573b0..e5fbf48870 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,6 +17,7 @@ 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 @@ -119,7 +120,16 @@ internal class SessionsCardViewModel @Inject constructor( } override fun onError(componentError: ComponentError) { - onError(componentError.errorMessage) + if (componentError.exception is CancellationException) { + updateUiState { + it.copy( + toastMessage = "Payment in progress was cancelled", + finalResult = ResultState.FAILURE, + ) + } + } else { + onError(componentError.errorMessage) + } } private fun onError(message: String) { 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 index 2509041cd0..24d3a02a59 100644 --- 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 @@ -87,8 +87,11 @@ private fun MainContent( } if (toastMessage != null) { - Toast.makeText(LocalContext.current, toastMessage, Toast.LENGTH_SHORT).show() - viewModel.toastMessageConsumed() + val context = LocalContext.current + LaunchedEffect(toastMessage) { + Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show() + viewModel.toastMessageConsumed() + } } if (finalResult != null) { From c91f1b67d07c475036c7a5fcbe18bf5dc1016df8 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Tue, 5 Dec 2023 16:44:30 +0100 Subject: [PATCH 57/60] Improve composables COAND-826 --- .../example/ui/card/SessionsCardUiState.kt | 2 +- .../example/ui/card/SessionsCardViewModel.kt | 10 +++---- .../ui/card/compose/SessionsCardScreen.kt | 30 ++++++++++++------- .../example/ui/compose/ResultContent.kt | 2 +- 4 files changed, 26 insertions(+), 18 deletions(-) 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 index 9ddf36d529..26981c1eac 100644 --- 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 @@ -17,7 +17,7 @@ import com.adyen.checkout.example.ui.compose.ResultState internal data class SessionsCardUiState( val cardConfiguration: CardConfiguration, val isLoading: Boolean = false, - val toastMessage: String? = null, + 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 e5fbf48870..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 @@ -123,7 +123,7 @@ internal class SessionsCardViewModel @Inject constructor( if (componentError.exception is CancellationException) { updateUiState { it.copy( - toastMessage = "Payment in progress was cancelled", + oneTimeMessage = "Payment in progress was cancelled", finalResult = ResultState.FAILURE, ) } @@ -133,13 +133,13 @@ internal class SessionsCardViewModel @Inject constructor( } private fun onError(message: String) { - updateUiState { it.copy(toastMessage = "Error: $message") } + updateUiState { it.copy(oneTimeMessage = "Error: $message") } } override fun onFinished(result: SessionPaymentResult) { updateUiState { it.copy( - toastMessage = "Finished: ${result.resultCode}", + oneTimeMessage = "Finished: ${result.resultCode}", finalResult = getFinalResultState(result), ) } @@ -157,8 +157,8 @@ internal class SessionsCardViewModel @Inject constructor( updateUiState { it.copy(isLoading = isLoading) } } - fun toastMessageConsumed() { - updateUiState { it.copy(toastMessage = null) } + fun oneTimeMessageConsumed() { + updateUiState { it.copy(oneTimeMessage = null) } } fun actionConsumed() { 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 index 24d3a02a59..f2c82051f0 100644 --- 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 @@ -44,12 +44,14 @@ 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), @@ -57,40 +59,46 @@ internal fun SessionsCardScreen( TopAppBar( title = { Text(text = "Card component with sessions") }, navigationIcon = { - IconButton(onClick = { onBackPressed() }) { + IconButton(onClick = onBackPressed) { Icon(Icons.Filled.ArrowBack, contentDescription = "Back") } }, ) }, ) { innerPadding -> - MainContent(Modifier.padding(innerPadding)) + val uiState by viewModel.uiState.collectAsState() + MainContent( + uiState = uiState, + onOneTimeMessageConsumed = viewModel::oneTimeMessageConsumed, + onActionConsumed = viewModel::actionConsumed, + modifier = Modifier.padding(innerPadding), + ) } } @Suppress("DestructuringDeclarationWithTooManyEntries") @Composable private fun MainContent( + uiState: SessionsCardUiState, + onOneTimeMessageConsumed: () -> Unit, + onActionConsumed: () -> Unit, modifier: Modifier = Modifier, - viewModel: SessionsCardViewModel = hiltViewModel(), ) { Box( modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { - val uiState by viewModel.uiState.collectAsState() - - val (cardConfiguration, isLoading, toastMessage, componentData, action, finalResult) = uiState + val (cardConfiguration, isLoading, oneTimeMessage, componentData, action, finalResult) = uiState if (isLoading) { CircularProgressIndicator() } - if (toastMessage != null) { + if (oneTimeMessage != null) { val context = LocalContext.current - LaunchedEffect(toastMessage) { - Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show() - viewModel.toastMessageConsumed() + LaunchedEffect(oneTimeMessage) { + Toast.makeText(context, oneTimeMessage, Toast.LENGTH_SHORT).show() + onOneTimeMessageConsumed() } } @@ -101,7 +109,7 @@ private fun MainContent( configuration = cardConfiguration, componentData = componentData, action = action, - onActionConsumed = viewModel::actionConsumed, + onActionConsumed = onActionConsumed, modifier = Modifier.fillMaxSize(), ) } 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 index 40a414cca6..d31996a41c 100644 --- 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 @@ -44,7 +44,7 @@ internal fun ResultContent( painter = painterResource(id = resultState.drawable), contentDescription = null, tint = tint, - modifier = modifier.size(100.dp), + modifier = Modifier.size(100.dp), ) Spacer(modifier = Modifier.height(16.dp)) Text(text = resultState.text, style = MaterialTheme.typography.displaySmall) From 51de3ac833b50d4ae7c831fb4f0a11e895a3bd0c Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Wed, 6 Dec 2023 14:39:19 +0100 Subject: [PATCH 58/60] Add dimensions to compose theme COAND-826 --- .../ui/card/compose/SessionsCardScreen.kt | 4 +- .../example/ui/compose/ResultContent.kt | 10 +- .../adyen/checkout/example/ui/theme/Color.kt | 84 ++++++++++++++ .../checkout/example/ui/theme/Dimensions.kt | 38 +++++++ .../checkout/example/ui/theme/ExampleTheme.kt | 106 ++++-------------- 5 files changed, 149 insertions(+), 93 deletions(-) create mode 100644 example-app/src/main/java/com/adyen/checkout/example/ui/theme/Dimensions.kt 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 index f2c82051f0..1c433aea44 100644 --- 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 @@ -67,7 +67,7 @@ internal fun SessionsCardScreen( }, ) { innerPadding -> val uiState by viewModel.uiState.collectAsState() - MainContent( + SessionsCardContent( uiState = uiState, onOneTimeMessageConsumed = viewModel::oneTimeMessageConsumed, onActionConsumed = viewModel::actionConsumed, @@ -78,7 +78,7 @@ internal fun SessionsCardScreen( @Suppress("DestructuringDeclarationWithTooManyEntries") @Composable -private fun MainContent( +private fun SessionsCardContent( uiState: SessionsCardUiState, onOneTimeMessageConsumed: () -> Unit, onActionConsumed: () -> Unit, 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 index d31996a41c..c3676375b6 100644 --- 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 @@ -23,7 +23,7 @@ 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.LocalCustomColorScheme +import com.adyen.checkout.example.ui.theme.ExampleTheme @Composable internal fun ResultContent( @@ -31,13 +31,13 @@ internal fun ResultContent( modifier: Modifier = Modifier, ) { Column( - modifier = modifier.padding(16.dp), + modifier = modifier.padding(ExampleTheme.dimensions.grid_2), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { val tint = when (resultState) { - ResultState.SUCCESS -> LocalCustomColorScheme.current.success - ResultState.PENDING -> LocalCustomColorScheme.current.warning + ResultState.SUCCESS -> ExampleTheme.customColors.success + ResultState.PENDING -> ExampleTheme.customColors.warning ResultState.FAILURE -> MaterialTheme.colorScheme.error } Icon( @@ -46,7 +46,7 @@ internal fun ResultContent( tint = tint, modifier = Modifier.size(100.dp), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(ExampleTheme.dimensions.grid_2)) Text(text = resultState.text, style = MaterialTheme.typography.displaySmall) } } 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 index d04a67136a..cdb38197c3 100644 --- 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 @@ -8,7 +8,11 @@ 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) @@ -82,3 +86,83 @@ 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 index ba7081ce89..6a03451e1b 100644 --- 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 @@ -10,95 +10,10 @@ package com.adyen.checkout.example.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Color - -private 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, -) - -private 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, -) - -@Immutable -data class CustomColorScheme( - val success: Color = Color.Unspecified, - val warning: Color = Color.Unspecified, -) - -val LocalCustomColorScheme = staticCompositionLocalOf { CustomColorScheme() } - -private val CustomLightColors = CustomColorScheme( - success = md_theme_light_success, - warning = md_theme_light_warning, -) - -private val CustomDarkColors = CustomColorScheme( - success = md_theme_dark_success, - warning = md_theme_dark_warning, -) @Composable fun ExampleTheme( @@ -117,8 +32,11 @@ fun ExampleTheme( CustomDarkColors } + val dimensions = DefaultDimensions + CompositionLocalProvider( LocalCustomColorScheme provides customColors, + LocalDimensions provides dimensions, ) { MaterialTheme( colorScheme = colors, @@ -126,3 +44,19 @@ fun ExampleTheme( ) } } + +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() } From 185ecde84d890a0e733d4f45e01f52537c21861f Mon Sep 17 00:00:00 2001 From: josephj Date: Thu, 7 Dec 2023 14:58:15 +0100 Subject: [PATCH 59/60] Bump version to 5.1.0 --- README.md | 10 +++++----- .../core/internal/data/api/AnalyticsMapperTest.kt | 2 +- dependencies.gradle | 2 +- example-app/build.gradle | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) 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/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 d03a7c9669..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 @@ -119,7 +119,7 @@ internal class AnalyticsMapperTest { ) val expected = AnalyticsSetupRequest( - version = "5.0.1", + version = "5.1.0", channel = "android", platform = "android", locale = "en_US", diff --git a/dependencies.gradle b/dependencies.gradle index 6c0d0f53fb..108cb75f97 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -16,7 +16,7 @@ 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.2' diff --git a/example-app/build.gradle b/example-app/build.gradle index d45810f85f..61079a8e5e 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -68,7 +68,7 @@ dependencies { // Checkout implementation project(':drop-in') implementation project(':components-compose') -// implementation "com.adyen.checkout:drop-in:5.0.1" +// implementation "com.adyen.checkout:drop-in:5.1.0" // Dependencies implementation libraries.kotlinCoroutines From a9d9c8b0a43cf791ed9156d1d2a5eb642ee7adb2 Mon Sep 17 00:00:00 2001 From: josephj Date: Thu, 7 Dec 2023 15:11:41 +0100 Subject: [PATCH 60/60] Update release notes for 5.1.0 --- RELEASE_NOTES.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 16a538c50c..102bce94d9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -9,12 +9,27 @@ [//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object) ## New -- You can now override payment method names in drop in by using `DropInConfiguration.Builder.overridePaymentMethodName(type, name)`. -- For stored cards, Drop-in will show the card name ("Visa", "Mastercard"...), instead of "Credit Card". -- Now it is possible to show installment amounts for card payments using `InstallmentConfiguration.showInstallmentAmount` in `CardConfiguration.Builder.setInstallmentConfigurations()`. -- For gift cards, you can now hide the PIN text field using `GiftCardConfiguration.Builder.setPinRequired()`. -- 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()` to get the `allowedPaymentMethods` attribute. -- For Google Pay, you can now use `AllowedAuthMethods` and `AllowedCardNetworks` to easily access to the possible values for `GooglePayConfiguration.Builder.setAllowedAuthMethods()` and `GooglePayConfiguration.Builder.setAllowedCardNetworks()`. +- 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 -- Fixed the bug which would not show components in Compose lazy lists. +- Fixed a bug where components would not be displayed in Jetpack Compose lazy lists. + +## Changed +- Dependency versions: + | Name | Version | + |--------------------------------------------------------------------------------------------------------|-------------------------------| + | [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** |