From a604277eb58308474b982fcec973561205e296f4 Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Fri, 27 Aug 2021 10:44:56 +0200 Subject: [PATCH 01/41] Fix crash when there are stored payments but none of them is Ecommerce. --- .../main/java/com/adyen/checkout/dropin/ui/DropInViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/DropInViewModel.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/DropInViewModel.kt index 73149bd682..5ea5405a74 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/DropInViewModel.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/DropInViewModel.kt @@ -22,7 +22,7 @@ class DropInViewModel( val resultHandlerIntent: Intent? ) : ViewModel() { - val showPreselectedStored = (paymentMethodsApiResponse.storedPaymentMethods?.isNotEmpty() ?: false) && + val showPreselectedStored = paymentMethodsApiResponse.storedPaymentMethods?.any { it.isEcommerce } == true && dropInConfiguration.showPreselectedStoredPaymentMethod val preselectedStoredPayment = paymentMethodsApiResponse.storedPaymentMethods?.firstOrNull { it.isEcommerce && PaymentMethodTypes.SUPPORTED_PAYMENT_METHODS.contains(it.type) From 86102049c16ae5f1e78c6e2928aed8f8c645744d Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Tue, 17 Aug 2021 11:36:47 +0200 Subject: [PATCH 02/41] Add secondary image view for dual branded cards --- .../java/com/adyen/checkout/card/CardView.kt | 12 +++---- card/src/main/res/layout/card_view.xml | 31 +++++++++++++------ card/src/main/res/values/styles.xml | 5 ++- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/card/src/main/java/com/adyen/checkout/card/CardView.kt b/card/src/main/java/com/adyen/checkout/card/CardView.kt index 980f5647c1..0c9b8e8348 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardView.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardView.kt @@ -201,12 +201,12 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? private fun onCardNumberValidated(detectedCardTypes: List) { if (detectedCardTypes.isEmpty()) { - binding.cardBrandLogoImageView.setStrokeWidth(0f) - binding.cardBrandLogoImageView.setImageResource(R.drawable.ic_card) + binding.cardBrandLogoImageViewPrimary.setStrokeWidth(0f) + binding.cardBrandLogoImageViewPrimary.setImageResource(R.drawable.ic_card) binding.editTextCardNumber.setAmexCardFormat(false) } else { - binding.cardBrandLogoImageView.setStrokeWidth(RoundCornerImageView.DEFAULT_STROKE_WIDTH) - mImageLoader?.load(detectedCardTypes[0].cardType.txVariant, binding.cardBrandLogoImageView, 0, R.drawable.ic_card) + binding.cardBrandLogoImageViewPrimary.setStrokeWidth(RoundCornerImageView.DEFAULT_STROKE_WIDTH) + mImageLoader?.load(detectedCardTypes[0].cardType.txVariant, binding.cardBrandLogoImageViewPrimary, 0, R.drawable.ic_card) // TODO: 29/01/2021 get this logic from OutputData var isAmex = false for ((cardType) in detectedCardTypes) { @@ -251,10 +251,10 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? private fun setCardNumberError(@StringRes stringResId: Int?) { if (stringResId == null) { binding.textInputLayoutCardNumber.error = null - binding.cardBrandLogoImageView.isVisible = true + binding.cardBrandLogoImageViewPrimary.isVisible = true } else { binding.textInputLayoutCardNumber.error = mLocalizedContext.getString(stringResId) - binding.cardBrandLogoImageView.isVisible = false + binding.cardBrandLogoImageViewPrimary.isVisible = false } } diff --git a/card/src/main/res/layout/card_view.xml b/card/src/main/res/layout/card_view.xml index c2ecf1b4bf..3c2e4c2914 100644 --- a/card/src/main/res/layout/card_view.xml +++ b/card/src/main/res/layout/card_view.xml @@ -33,15 +33,28 @@ - - - + + + + + + + @dimen/standard_quarter_margin - + + From 10f07cf63907203dd28ed655334f2d3d1dcaf948 Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Tue, 17 Aug 2021 11:37:33 +0200 Subject: [PATCH 03/41] Show secondary brand logo if it exists --- card/src/main/java/com/adyen/checkout/card/CardView.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/card/src/main/java/com/adyen/checkout/card/CardView.kt b/card/src/main/java/com/adyen/checkout/card/CardView.kt index 0c9b8e8348..6d8725f9a6 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardView.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardView.kt @@ -203,10 +203,17 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? if (detectedCardTypes.isEmpty()) { binding.cardBrandLogoImageViewPrimary.setStrokeWidth(0f) binding.cardBrandLogoImageViewPrimary.setImageResource(R.drawable.ic_card) + binding.cardBrandLogoImageViewSecondary.isVisible = false binding.editTextCardNumber.setAmexCardFormat(false) } else { binding.cardBrandLogoImageViewPrimary.setStrokeWidth(RoundCornerImageView.DEFAULT_STROKE_WIDTH) mImageLoader?.load(detectedCardTypes[0].cardType.txVariant, binding.cardBrandLogoImageViewPrimary, 0, R.drawable.ic_card) + + detectedCardTypes.getOrNull(1)?.takeIf { it.isReliable }?.let { + binding.cardBrandLogoImageViewSecondary.isVisible = true + binding.cardBrandLogoImageViewSecondary.setStrokeWidth(RoundCornerImageView.DEFAULT_STROKE_WIDTH) + mImageLoader?.load(it.cardType.txVariant, binding.cardBrandLogoImageViewSecondary, 0, R.drawable.ic_card) + } ?: run { binding.cardBrandLogoImageViewSecondary.isVisible = false } // TODO: 29/01/2021 get this logic from OutputData var isAmex = false for ((cardType) in detectedCardTypes) { From 1315da1f72febed303638155abe35f5591202208 Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Wed, 18 Aug 2021 13:19:11 +0200 Subject: [PATCH 04/41] Support dual branded cards from bin lookup response --- .../com/adyen/checkout/card/CardComponent.kt | 23 ++++++--- .../com/adyen/checkout/card/CardInputData.kt | 3 +- .../java/com/adyen/checkout/card/CardView.kt | 49 ++++++++++++++++++- .../checkout/card/data/DetectedCardType.kt | 3 +- 4 files changed, 67 insertions(+), 11 deletions(-) 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 918bcec651..0d4ce153cf 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -75,7 +75,7 @@ class CardComponent private constructor( kcpCardPassword = kcpCardPasswordState.value, postalCode = postalCodeState.value, isStorePaymentSelected = isStoredPaymentMethodEnable, - detectedCardTypes = it + detectedCardTypes = it.map { item -> if (item == it.first()) item.copy(isSelected = true) else item } ) notifyStateChanged(newOutputData) } @@ -112,7 +112,13 @@ class CardComponent private constructor( override fun onInputDataChanged(inputData: CardInputData): CardOutputData { Logger.v(TAG, "onInputDataChanged") - val detectedCardTypes = cardDelegate.detectCardType(inputData.cardNumber, publicKey, viewModelScope) + val detectedCardTypes = cardDelegate.detectCardType(inputData.cardNumber, publicKey, viewModelScope).mapIndexed { index, detectedCardType -> + if (index == inputData.selectedCardIndex) { + detectedCardType.copy(isSelected = true) + } else { + detectedCardType.copy(isSelected = false) + } + } return makeOutputData( cardNumber = inputData.cardNumber, @@ -141,19 +147,20 @@ class CardComponent private constructor( postalCode: String, detectedCardTypes: List ): CardOutputData { - val firstDetectedType = detectedCardTypes.firstOrNull() + val detectedType = detectedCardTypes.firstOrNull { it.isSelected } ?: detectedCardTypes.firstOrNull() + return CardOutputData( - cardDelegate.validateCardNumber(cardNumber, firstDetectedType?.enableLuhnCheck), - cardDelegate.validateExpiryDate(expiryDate, firstDetectedType?.expiryDatePolicy), - cardDelegate.validateSecurityCode(securityCode, firstDetectedType), + cardDelegate.validateCardNumber(cardNumber, detectedType?.enableLuhnCheck), + cardDelegate.validateExpiryDate(expiryDate, detectedType?.expiryDatePolicy), + cardDelegate.validateSecurityCode(securityCode, detectedType), cardDelegate.validateHolderName(holderName), cardDelegate.validateSocialSecurityNumber(socialSecurityNumber), cardDelegate.validateKcpBirthDateOrTaxNumber(kcpBirthDateOrTaxNumber), cardDelegate.validateKcpCardPassword(kcpCardPassword), cardDelegate.validatePostalCode(postalCode), isStorePaymentSelected, - makeCvcUIState(firstDetectedType?.cvcPolicy), - makeExpiryDateUIState(firstDetectedType?.expiryDatePolicy), + makeCvcUIState(detectedType?.cvcPolicy), + makeExpiryDateUIState(detectedType?.expiryDatePolicy), detectedCardTypes, cardDelegate.isSocialSecurityNumberRequired(), cardDelegate.isKCPAuthRequired(), diff --git a/card/src/main/java/com/adyen/checkout/card/CardInputData.kt b/card/src/main/java/com/adyen/checkout/card/CardInputData.kt index 01728dd448..6abad1ffe7 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardInputData.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardInputData.kt @@ -19,5 +19,6 @@ data class CardInputData( var kcpBirthDateOrTaxNumber: String = "", var kcpCardPassword: String = "", var postalCode: String = "", - var isStorePaymentSelected: Boolean = false + var isStorePaymentSelected: Boolean = false, + var selectedCardIndex: Int? = null ) : InputData diff --git a/card/src/main/java/com/adyen/checkout/card/CardView.kt b/card/src/main/java/com/adyen/checkout/card/CardView.kt index 6d8725f9a6..d870b145ec 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardView.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardView.kt @@ -32,6 +32,7 @@ import com.adyen.checkout.components.ui.Validation import com.adyen.checkout.components.ui.view.AdyenLinearLayout import com.adyen.checkout.components.ui.view.AdyenTextInputEditText import com.adyen.checkout.components.ui.view.RoundCornerImageView +import com.adyen.checkout.core.exception.CheckoutException /** * CardView for [CardComponent]. @@ -203,8 +204,10 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? if (detectedCardTypes.isEmpty()) { binding.cardBrandLogoImageViewPrimary.setStrokeWidth(0f) binding.cardBrandLogoImageViewPrimary.setImageResource(R.drawable.ic_card) + binding.cardBrandLogoImageViewPrimary.alpha = 1f binding.cardBrandLogoImageViewSecondary.isVisible = false binding.editTextCardNumber.setAmexCardFormat(false) + resetBrandSelectionInput() } else { binding.cardBrandLogoImageViewPrimary.setStrokeWidth(RoundCornerImageView.DEFAULT_STROKE_WIDTH) mImageLoader?.load(detectedCardTypes[0].cardType.txVariant, binding.cardBrandLogoImageViewPrimary, 0, R.drawable.ic_card) @@ -213,7 +216,14 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? binding.cardBrandLogoImageViewSecondary.isVisible = true binding.cardBrandLogoImageViewSecondary.setStrokeWidth(RoundCornerImageView.DEFAULT_STROKE_WIDTH) mImageLoader?.load(it.cardType.txVariant, binding.cardBrandLogoImageViewSecondary, 0, R.drawable.ic_card) - } ?: run { binding.cardBrandLogoImageViewSecondary.isVisible = false } + initCardBrandLogoViews(detectedCardTypes.indexOfFirst { it.isSelected }) + initBrandSelectionListeners() + } ?: run { + binding.cardBrandLogoImageViewPrimary.alpha = 1f + binding.cardBrandLogoImageViewSecondary.isVisible = false + resetBrandSelectionInput() + } + // TODO: 29/01/2021 get this logic from OutputData var isAmex = false for ((cardType) in detectedCardTypes) { @@ -265,6 +275,43 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } } + private fun initCardBrandLogoViews(selectedIndex: Int) { + when (selectedIndex) { + 0 -> selectPrimaryBrand() + 1 -> selectSecondaryBrand() + else -> throw CheckoutException("") + } + } + + private fun initBrandSelectionListeners() { + binding.cardBrandLogoImageViewPrimary.setOnClickListener { + mCardInputData.selectedCardIndex = 0 + notifyInputDataChanged() + selectPrimaryBrand() + } + + binding.cardBrandLogoImageViewSecondary.setOnClickListener { + mCardInputData.selectedCardIndex = 1 + notifyInputDataChanged() + selectSecondaryBrand() + } + } + + private fun resetBrandSelectionInput() { + binding.cardBrandLogoImageViewPrimary.setOnClickListener(null) + binding.cardBrandLogoImageViewSecondary.setOnClickListener(null) + } + + private fun selectPrimaryBrand() { + binding.cardBrandLogoImageViewPrimary.alpha = 1f + binding.cardBrandLogoImageViewSecondary.alpha = 0.2f + } + + private fun selectSecondaryBrand() { + binding.cardBrandLogoImageViewPrimary.alpha = 0.2f + binding.cardBrandLogoImageViewSecondary.alpha = 1f + } + private fun initExpiryDateInput() { binding.editTextExpiryDate.setOnChangeListener { val date = binding.editTextExpiryDate.date diff --git a/card/src/main/java/com/adyen/checkout/card/data/DetectedCardType.kt b/card/src/main/java/com/adyen/checkout/card/data/DetectedCardType.kt index 84802160c8..5484236ef6 100644 --- a/card/src/main/java/com/adyen/checkout/card/data/DetectedCardType.kt +++ b/card/src/main/java/com/adyen/checkout/card/data/DetectedCardType.kt @@ -15,5 +15,6 @@ data class DetectedCardType( val isReliable: Boolean, val enableLuhnCheck: Boolean, val cvcPolicy: Brand.FieldPolicy, - val expiryDatePolicy: Brand.FieldPolicy + val expiryDatePolicy: Brand.FieldPolicy, + val isSelected: Boolean = false ) From d66e866d4456203be072d7a50bed93d8e54087e5 Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Wed, 18 Aug 2021 13:59:46 +0200 Subject: [PATCH 05/41] Extract marking cards selected to a private method --- .../com/adyen/checkout/card/CardComponent.kt | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) 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 0d4ce153cf..f04815490f 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -38,6 +38,8 @@ private val TAG = LogUtil.getTag() private val PAYMENT_METHOD_TYPES = arrayOf(PaymentMethodTypes.SCHEME) private const val BIN_VALUE_LENGTH = 6 private const val LAST_FOUR_LENGTH = 4 +private const val FIRST_CARD_INDEX = 0 +private const val SINGLE_CARD_LIST_SIZE = 1 @Suppress("TooManyFunctions") class CardComponent private constructor( @@ -75,7 +77,7 @@ class CardComponent private constructor( kcpCardPassword = kcpCardPasswordState.value, postalCode = postalCodeState.value, isStorePaymentSelected = isStoredPaymentMethodEnable, - detectedCardTypes = it.map { item -> if (item == it.first()) item.copy(isSelected = true) else item } + detectedCardTypes = markSelectedCard(it, 0) ) notifyStateChanged(newOutputData) } @@ -112,13 +114,10 @@ class CardComponent private constructor( override fun onInputDataChanged(inputData: CardInputData): CardOutputData { Logger.v(TAG, "onInputDataChanged") - val detectedCardTypes = cardDelegate.detectCardType(inputData.cardNumber, publicKey, viewModelScope).mapIndexed { index, detectedCardType -> - if (index == inputData.selectedCardIndex) { - detectedCardType.copy(isSelected = true) - } else { - detectedCardType.copy(isSelected = false) - } - } + val detectedCardTypes = markSelectedCard( + cardDelegate.detectCardType(inputData.cardNumber, publicKey, viewModelScope), + inputData.selectedCardIndex + ) return makeOutputData( cardNumber = inputData.cardNumber, @@ -185,6 +184,20 @@ class CardComponent private constructor( } } + private fun markSelectedCard(cards: List, selectedIndex: Int?): List { + return if (cards.size > SINGLE_CARD_LIST_SIZE) { + cards.mapIndexed { index, card -> + if (index == selectedIndex) { + card.copy(isSelected = true) + } else { + card + } + } + } else { + cards + } + } + @Suppress("ReturnCount") override fun createComponentState(): CardComponentState { Logger.v(TAG, "createComponentState") From 5007cefc58e22a16c62abeb1cdcf9531223da214 Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Wed, 25 Aug 2021 13:08:37 +0200 Subject: [PATCH 06/41] Add DualBrandedCardUtils --- .../com/adyen/checkout/card/CardComponent.kt | 7 +- .../checkout/card/DualBrandedCardUtils.kt | 29 +++ .../checkout/card/DualBrandedCardUtilsTest.kt | 186 ++++++++++++++++++ 3 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 card/src/main/java/com/adyen/checkout/card/DualBrandedCardUtils.kt create mode 100644 card/src/test/java/com/adyen/checkout/card/DualBrandedCardUtilsTest.kt 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 f04815490f..e26c605b8c 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -63,6 +63,7 @@ class CardComponent private constructor( if (cardDelegate is NewCardDelegate) { cardDelegate.binLookupFlow .onEach { + val sortedCardList = DualBrandedCardUtils.sortBrands(it) Logger.d(TAG, "New binLookupFlow emitted") Logger.d(TAG, "Brands: $it") with(outputData) { @@ -77,7 +78,7 @@ class CardComponent private constructor( kcpCardPassword = kcpCardPasswordState.value, postalCode = postalCodeState.value, isStorePaymentSelected = isStoredPaymentMethodEnable, - detectedCardTypes = markSelectedCard(it, 0) + detectedCardTypes = markSelectedCard(sortedCardList, 0) ) notifyStateChanged(newOutputData) } @@ -114,8 +115,10 @@ class CardComponent private constructor( override fun onInputDataChanged(inputData: CardInputData): CardOutputData { Logger.v(TAG, "onInputDataChanged") + val sortedCardList = DualBrandedCardUtils.sortBrands(cardDelegate.detectCardType(inputData.cardNumber, publicKey, viewModelScope)) + val detectedCardTypes = markSelectedCard( - cardDelegate.detectCardType(inputData.cardNumber, publicKey, viewModelScope), + sortedCardList, inputData.selectedCardIndex ) diff --git a/card/src/main/java/com/adyen/checkout/card/DualBrandedCardUtils.kt b/card/src/main/java/com/adyen/checkout/card/DualBrandedCardUtils.kt new file mode 100644 index 0000000000..c2af00f335 --- /dev/null +++ b/card/src/main/java/com/adyen/checkout/card/DualBrandedCardUtils.kt @@ -0,0 +1,29 @@ +package com.adyen.checkout.card + +import com.adyen.checkout.card.data.CardType +import com.adyen.checkout.card.data.DetectedCardType + +object DualBrandedCardUtils { + + fun sortBrands(cards: List): List { + return when { + cards.size <= 1 -> cards + else -> { + val hasCarteBancaire = cards.any { it.cardType == CardType.CARTEBANCAIRE } + val hasVisa = cards.any { it.cardType == CardType.VISA } + val hasPlcc = cards.any { + it.cardType == CardType.UNKNOWN && + (it.cardType.txVariant.contains("plcc") || it.cardType.txVariant.contains("cbcc")) + } + + if (hasCarteBancaire && hasVisa) { + cards.sortedByDescending { it.cardType == CardType.VISA } + } else if (hasPlcc) { + cards.sortedByDescending { it.cardType.txVariant.contains("plcc") || it.cardType.txVariant.contains("cbcc") } + } else { + cards + } + } + } + } +} diff --git a/card/src/test/java/com/adyen/checkout/card/DualBrandedCardUtilsTest.kt b/card/src/test/java/com/adyen/checkout/card/DualBrandedCardUtilsTest.kt new file mode 100644 index 0000000000..86a9b9ffae --- /dev/null +++ b/card/src/test/java/com/adyen/checkout/card/DualBrandedCardUtilsTest.kt @@ -0,0 +1,186 @@ +package com.adyen.checkout.card + +import com.adyen.checkout.card.api.model.Brand +import com.adyen.checkout.card.data.CardType +import com.adyen.checkout.card.data.DetectedCardType +import org.junit.Assert.assertEquals +import org.junit.Test + +class DualBrandedCardUtilsTest { + + @Test + fun testDualBrandSortingEmptyList() { + val list = emptyList() + assertEquals(emptyList(), DualBrandedCardUtils.sortBrands(list)) + } + + @Test + fun testDualBrandSortingSingleItemList() { + val detectedCards = listOf( + DetectedCardType( + cardType = CardType.CARTEBANCAIRE, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ) + ) + assertEquals(detectedCards, DualBrandedCardUtils.sortBrands(detectedCards)) + } + + @Test + fun testDualBrandVisaAndCarteBancaire() { + val detectedCards = listOf( + DetectedCardType( + cardType = CardType.CARTEBANCAIRE, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ), + DetectedCardType( + cardType = CardType.VISA, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ) + ) + + val sortedCards = listOf( + DetectedCardType( + cardType = CardType.VISA, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ), + DetectedCardType( + cardType = CardType.CARTEBANCAIRE, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ) + ) + + assertEquals(sortedCards, DualBrandedCardUtils.sortBrands(detectedCards)) + } + + @Test + fun testDualBrandVisaAndCarteBancaireAlreadySorted() { + val detectedCards = listOf( + DetectedCardType( + cardType = CardType.VISA, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ), + DetectedCardType( + cardType = CardType.CARTEBANCAIRE, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ) + ) + + val sortedCards = listOf( + DetectedCardType( + cardType = CardType.VISA, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ), + DetectedCardType( + cardType = CardType.CARTEBANCAIRE, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ) + ) + + assertEquals(sortedCards, DualBrandedCardUtils.sortBrands(detectedCards)) + } + + @Test + fun testDualBrandPlccAndMasterCard() { + val detectedCards = listOf( + DetectedCardType( + cardType = CardType.MASTERCARD, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ), + DetectedCardType( + cardType = CardType.UNKNOWN.apply { txVariant = "plcc_mastercard" }, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ) + ) + + val sortedCards = listOf( + DetectedCardType( + cardType = CardType.UNKNOWN.apply { txVariant = "plcc_mastercard" }, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ), + DetectedCardType( + cardType = CardType.MASTERCARD, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ) + ) + + assertEquals(sortedCards, DualBrandedCardUtils.sortBrands(detectedCards)) + } + + @Test + fun testDualBrandPlccAndMasterCardAlreadySorted() { + val detectedCards = listOf( + DetectedCardType( + cardType = CardType.UNKNOWN.apply { txVariant = "plcc_mastercard" }, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ), + DetectedCardType( + cardType = CardType.MASTERCARD, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ) + ) + + val sortedCards = listOf( + DetectedCardType( + cardType = CardType.UNKNOWN.apply { txVariant = "plcc_mastercard" }, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ), + DetectedCardType( + cardType = CardType.MASTERCARD, + isReliable = true, + enableLuhnCheck = false, + cvcPolicy = Brand.FieldPolicy.REQUIRED, + expiryDatePolicy = Brand.FieldPolicy.REQUIRED + ) + ) + + assertEquals(sortedCards, DualBrandedCardUtils.sortBrands(detectedCards)) + } +} From 2e8bb4fb1cbfacbd14af05922c2fce4ff8b2fd31 Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Thu, 26 Aug 2021 10:45:27 +0200 Subject: [PATCH 07/41] Increase touch area for card brand logos --- .../java/com/adyen/checkout/card/CardView.kt | 18 ++++---- card/src/main/res/layout/card_view.xml | 41 ++++++++++++------- card/src/main/res/values/dimens.xml | 2 +- card/src/main/res/values/styles.xml | 13 ++++-- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/card/src/main/java/com/adyen/checkout/card/CardView.kt b/card/src/main/java/com/adyen/checkout/card/CardView.kt index d870b145ec..b9f3863e84 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardView.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardView.kt @@ -205,7 +205,7 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? binding.cardBrandLogoImageViewPrimary.setStrokeWidth(0f) binding.cardBrandLogoImageViewPrimary.setImageResource(R.drawable.ic_card) binding.cardBrandLogoImageViewPrimary.alpha = 1f - binding.cardBrandLogoImageViewSecondary.isVisible = false + binding.cardBrandLogoContainerSecondary.isVisible = false binding.editTextCardNumber.setAmexCardFormat(false) resetBrandSelectionInput() } else { @@ -213,14 +213,14 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? mImageLoader?.load(detectedCardTypes[0].cardType.txVariant, binding.cardBrandLogoImageViewPrimary, 0, R.drawable.ic_card) detectedCardTypes.getOrNull(1)?.takeIf { it.isReliable }?.let { - binding.cardBrandLogoImageViewSecondary.isVisible = true + binding.cardBrandLogoContainerSecondary.isVisible = true binding.cardBrandLogoImageViewSecondary.setStrokeWidth(RoundCornerImageView.DEFAULT_STROKE_WIDTH) mImageLoader?.load(it.cardType.txVariant, binding.cardBrandLogoImageViewSecondary, 0, R.drawable.ic_card) initCardBrandLogoViews(detectedCardTypes.indexOfFirst { it.isSelected }) initBrandSelectionListeners() } ?: run { binding.cardBrandLogoImageViewPrimary.alpha = 1f - binding.cardBrandLogoImageViewSecondary.isVisible = false + binding.cardBrandLogoContainerSecondary.isVisible = false resetBrandSelectionInput() } @@ -268,10 +268,10 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? private fun setCardNumberError(@StringRes stringResId: Int?) { if (stringResId == null) { binding.textInputLayoutCardNumber.error = null - binding.cardBrandLogoImageViewPrimary.isVisible = true + binding.cardBrandLogoContainerPrimary.isVisible = true } else { binding.textInputLayoutCardNumber.error = mLocalizedContext.getString(stringResId) - binding.cardBrandLogoImageViewPrimary.isVisible = false + binding.cardBrandLogoContainerPrimary.isVisible = false } } @@ -284,13 +284,13 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } private fun initBrandSelectionListeners() { - binding.cardBrandLogoImageViewPrimary.setOnClickListener { + binding.cardBrandLogoContainerPrimary.setOnClickListener { mCardInputData.selectedCardIndex = 0 notifyInputDataChanged() selectPrimaryBrand() } - binding.cardBrandLogoImageViewSecondary.setOnClickListener { + binding.cardBrandLogoContainerSecondary.setOnClickListener { mCardInputData.selectedCardIndex = 1 notifyInputDataChanged() selectSecondaryBrand() @@ -298,8 +298,8 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } private fun resetBrandSelectionInput() { - binding.cardBrandLogoImageViewPrimary.setOnClickListener(null) - binding.cardBrandLogoImageViewSecondary.setOnClickListener(null) + binding.cardBrandLogoContainerPrimary.setOnClickListener(null) + binding.cardBrandLogoContainerSecondary.setOnClickListener(null) } private fun selectPrimaryBrand() { diff --git a/card/src/main/res/layout/card_view.xml b/card/src/main/res/layout/card_view.xml index 3c2e4c2914..7ff80b5264 100644 --- a/card/src/main/res/layout/card_view.xml +++ b/card/src/main/res/layout/card_view.xml @@ -35,25 +35,38 @@ - - - + + + + + - + tools:visibility="visible"> + + + diff --git a/card/src/main/res/values/dimens.xml b/card/src/main/res/values/dimens.xml index adbb258567..6539c324e4 100644 --- a/card/src/main/res/values/dimens.xml +++ b/card/src/main/res/values/dimens.xml @@ -8,7 +8,7 @@ --> - 24dp + 16dp 24dp 16dp diff --git a/card/src/main/res/values/styles.xml b/card/src/main/res/values/styles.xml index ce4677d7f7..413c0c16cc 100644 --- a/card/src/main/res/values/styles.xml +++ b/card/src/main/res/values/styles.xml @@ -20,19 +20,26 @@ @dimen/standard_quarter_margin - + + From 88c96e78b2c6c6391296607d850f83fe77709e41 Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Fri, 27 Aug 2021 15:29:13 +0200 Subject: [PATCH 08/41] Add API field for brand in CardPaymentMethod --- .../com/adyen/checkout/card/CardComponent.kt | 26 ++++++++++++------- .../payments/request/CardPaymentMethod.java | 13 ++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) 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 e26c605b8c..ced63f5207 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -187,7 +187,7 @@ class CardComponent private constructor( } } - private fun markSelectedCard(cards: List, selectedIndex: Int?): List { + private fun markSelectedCard(cards: List, selectedIndex: Int): List { return if (cards.size > SINGLE_CARD_LIST_SIZE) { cards.mapIndexed { index, card -> if (index == selectedIndex) { @@ -304,6 +304,10 @@ class CardComponent private constructor( cardPaymentMethod.taxNumber = stateOutputData.kcpBirthDateOrTaxNumberState.value } + if (isDualBrandedFlow(stateOutputData)) { + cardPaymentMethod.brand = stateOutputData.detectedCardTypes.first { it.isSelected }.cardType.txVariant + } + val paymentComponentData = PaymentComponentData().apply { paymentMethod = cardPaymentMethod setStorePaymentMethod(stateOutputData.isStoredPaymentMethodEnable) @@ -346,7 +350,18 @@ class CardComponent private constructor( return configuration.isShowStorePaymentFieldEnable } - fun makeAddressData(outputData: CardOutputData): Address { + @StringRes fun getKcpBirthDateOrTaxNumberHint(input: String): Int { + return when { + input.length > KcpValidationUtils.KCP_BIRTH_DATE_LENGTH -> R.string.checkout_kcp_tax_number_hint + else -> R.string.checkout_kcp_birth_date_or_tax_number_hint + } + } + + private fun isDualBrandedFlow(cardOutputData: CardOutputData): Boolean { + return cardOutputData.detectedCardTypes.size > 1 && cardOutputData.detectedCardTypes.any { it.isSelected } + } + + private fun makeAddressData(outputData: CardOutputData): Address { return Address().apply { postalCode = outputData.postalCodeState.value street = Address.ADDRESS_NULL_PLACEHOLDER @@ -357,13 +372,6 @@ class CardComponent private constructor( } } - @StringRes fun getKcpBirthDateOrTaxNumberHint(input: String): Int { - return when { - input.length > KcpValidationUtils.KCP_BIRTH_DATE_LENGTH -> R.string.checkout_kcp_tax_number_hint - else -> R.string.checkout_kcp_birth_date_or_tax_number_hint - } - } - companion object { @JvmStatic val PROVIDER: StoredPaymentComponentProvider = CardComponentProvider() diff --git a/components-core/src/main/java/com/adyen/checkout/components/model/payments/request/CardPaymentMethod.java b/components-core/src/main/java/com/adyen/checkout/components/model/payments/request/CardPaymentMethod.java index 970cb0ff5e..1b0a93d019 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/model/payments/request/CardPaymentMethod.java +++ b/components-core/src/main/java/com/adyen/checkout/components/model/payments/request/CardPaymentMethod.java @@ -36,6 +36,7 @@ public final class CardPaymentMethod extends PaymentMethodDetails { private static final String STORED_PAYMENT_METHOD_ID = "storedPaymentMethodId"; private static final String ENCRYPTED_PASSWORD = "encryptedPassword"; private static final String TAX_NUMBER = "taxNumber"; + private static final String BRAND = "brand"; @NonNull public static final Serializer SERIALIZER = new Serializer() { @@ -56,6 +57,7 @@ public JSONObject serialize(@NonNull CardPaymentMethod modelObject) { jsonObject.putOpt(HOLDER_NAME, modelObject.getHolderName()); jsonObject.putOpt(ENCRYPTED_PASSWORD, modelObject.getEncryptedPassword()); jsonObject.putOpt(TAX_NUMBER, modelObject.getTaxNumber()); + jsonObject.putOpt(BRAND, modelObject.getBrand()); } catch (JSONException e) { throw new ModelSerializationException(IdealPaymentMethod.class, e); } @@ -78,6 +80,7 @@ public CardPaymentMethod deserialize(@NonNull JSONObject jsonObject) { cardPaymentMethod.setHolderName(jsonObject.optString(HOLDER_NAME, null)); cardPaymentMethod.setEncryptedPassword(jsonObject.optString(ENCRYPTED_PASSWORD, null)); cardPaymentMethod.setTaxNumber(jsonObject.optString(TAX_NUMBER)); + cardPaymentMethod.setBrand(jsonObject.optString(BRAND)); return cardPaymentMethod; } @@ -91,6 +94,7 @@ public CardPaymentMethod deserialize(@NonNull JSONObject jsonObject) { private String holderName; private String storedPaymentMethodId; private String taxNumber; + private String brand; @Override public void writeToParcel(@NonNull Parcel dest, int flags) { @@ -160,6 +164,15 @@ public void setHolderName(@Nullable String holderName) { this.holderName = holderName; } + @Nullable + public String getBrand() { + return brand; + } + + public void setBrand(@Nullable String brand) { + this.brand = brand; + } + public void setStoredPaymentMethodId(@Nullable String storedPaymentMethodId) { this.storedPaymentMethodId = storedPaymentMethodId; } From ef9aee511b56cb1034ad51bddbc8710befa34ab8 Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Fri, 27 Aug 2021 15:37:22 +0200 Subject: [PATCH 09/41] Mark first card brand selected by default --- card/src/main/java/com/adyen/checkout/card/CardInputData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/card/src/main/java/com/adyen/checkout/card/CardInputData.kt b/card/src/main/java/com/adyen/checkout/card/CardInputData.kt index 6abad1ffe7..1478ecbfc9 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardInputData.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardInputData.kt @@ -20,5 +20,5 @@ data class CardInputData( var kcpCardPassword: String = "", var postalCode: String = "", var isStorePaymentSelected: Boolean = false, - var selectedCardIndex: Int? = null + var selectedCardIndex: Int = 0 ) : InputData From ba958662443435307415b94d475bc64bf1fcaffc Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Fri, 27 Aug 2021 15:44:13 +0200 Subject: [PATCH 10/41] Remove magic number usages in CardView --- .../java/com/adyen/checkout/card/CardView.kt | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/card/src/main/java/com/adyen/checkout/card/CardView.kt b/card/src/main/java/com/adyen/checkout/card/CardView.kt index b9f3863e84..d7a3196025 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardView.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardView.kt @@ -42,6 +42,13 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? AdyenLinearLayout(context, attrs, defStyleAttr), Observer { + companion object { + private const val UNSELECTED_BRAND_LOGO_ALPHA = 0.2f + private const val SELECTED_BRAND_LOGO_ALPHA = 1f + private const val PRIMARY_BRAND_INDEX = 0 + private const val SECONDARY_BRAND_INDEX = 1 + } + private val binding: CardViewBinding = CardViewBinding.inflate(LayoutInflater.from(context), this) private val mCardInputData = CardInputData() @@ -277,21 +284,21 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? private fun initCardBrandLogoViews(selectedIndex: Int) { when (selectedIndex) { - 0 -> selectPrimaryBrand() - 1 -> selectSecondaryBrand() - else -> throw CheckoutException("") + PRIMARY_BRAND_INDEX -> selectPrimaryBrand() + SECONDARY_BRAND_INDEX -> selectSecondaryBrand() + else -> throw CheckoutException("Illegal brand index selected. Selected index must be either 0 or 1.") } } private fun initBrandSelectionListeners() { binding.cardBrandLogoContainerPrimary.setOnClickListener { - mCardInputData.selectedCardIndex = 0 + mCardInputData.selectedCardIndex = PRIMARY_BRAND_INDEX notifyInputDataChanged() selectPrimaryBrand() } binding.cardBrandLogoContainerSecondary.setOnClickListener { - mCardInputData.selectedCardIndex = 1 + mCardInputData.selectedCardIndex = SECONDARY_BRAND_INDEX notifyInputDataChanged() selectSecondaryBrand() } @@ -303,13 +310,13 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } private fun selectPrimaryBrand() { - binding.cardBrandLogoImageViewPrimary.alpha = 1f - binding.cardBrandLogoImageViewSecondary.alpha = 0.2f + binding.cardBrandLogoImageViewPrimary.alpha = SELECTED_BRAND_LOGO_ALPHA + binding.cardBrandLogoImageViewSecondary.alpha = UNSELECTED_BRAND_LOGO_ALPHA } private fun selectSecondaryBrand() { - binding.cardBrandLogoImageViewPrimary.alpha = 0.2f - binding.cardBrandLogoImageViewSecondary.alpha = 1f + binding.cardBrandLogoImageViewPrimary.alpha = UNSELECTED_BRAND_LOGO_ALPHA + binding.cardBrandLogoImageViewSecondary.alpha = SELECTED_BRAND_LOGO_ALPHA } private fun initExpiryDateInput() { From 4fe8cb1cc50eecb9b29b225d2bf5774a5c5a725c Mon Sep 17 00:00:00 2001 From: jreij Date: Tue, 3 Aug 2021 13:39:32 +0200 Subject: [PATCH 11/41] Use activity result api in DropIn.startPayment --- .../java/com/adyen/checkout/dropin/DropIn.kt | 50 +++++++++++++++++++ .../adyen/checkout/dropin/DropInCallback.kt | 13 +++++ .../checkout/dropin/DropInResultContract.kt | 23 +++++++++ .../checkout/example/ui/main/MainActivity.kt | 13 ++--- 4 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 drop-in/src/main/java/com/adyen/checkout/dropin/DropInCallback.kt create mode 100644 drop-in/src/main/java/com/adyen/checkout/dropin/DropInResultContract.kt diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt index 8940b41e10..929f32b96c 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt @@ -11,6 +11,8 @@ package com.adyen.checkout.dropin import android.app.Activity import android.content.Context import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher import androidx.fragment.app.Fragment import com.adyen.checkout.components.model.PaymentMethodsApiResponse import com.adyen.checkout.core.log.LogUtil @@ -40,6 +42,35 @@ object DropIn { internal const val DROP_IN_PREFS = "drop-in-shared-prefs" internal const val LOCALE_PREF = "drop-in-locale" + @JvmStatic + fun registerForDropInResult(activity: ComponentActivity, callback: DropInCallback): ActivityResultLauncher { + return activity.registerForActivityResult(DropInResultContract(), callback::onDropInResult) + } + + @JvmStatic + fun registerForDropInResult(fragment: Fragment, callback: DropInCallback): ActivityResultLauncher { + return fragment.registerForActivityResult(DropInResultContract(), callback::onDropInResult) + } + + @JvmStatic + fun startPayment( + activity: ComponentActivity, + paymentMethodsApiResponse: PaymentMethodsApiResponse, + dropInConfiguration: DropInConfiguration, + dropInLauncher: ActivityResultLauncher, + resultHandlerIntent: Intent? = null + ) { + Logger.d(TAG, "startPayment from Activity") + + val intent = preparePayment( + activity, + paymentMethodsApiResponse, + dropInConfiguration, + resultHandlerIntent + ) + dropInLauncher.launch(intent) + } + /** * Starts the checkout flow to be handled by the Drop-in solution. * Make sure you have [DropInService] set up before calling this. @@ -99,6 +130,25 @@ object DropIn { activity.startActivityForResult(intent, DROP_IN_REQUEST_CODE) } + @JvmStatic + fun startPayment( + fragment: Fragment, + paymentMethodsApiResponse: PaymentMethodsApiResponse, + dropInConfiguration: DropInConfiguration, + dropInLauncher: ActivityResultLauncher, + resultHandlerIntent: Intent? = null + ) { + Logger.d(TAG, "startPayment from Fragment") + + val intent = preparePayment( + fragment.requireContext(), + paymentMethodsApiResponse, + dropInConfiguration, + resultHandlerIntent + ) + dropInLauncher.launch(intent) + } + /** * Starts the checkout flow to be handled by the Drop-in solution. * Make sure you have [DropInService] set up before calling this. diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInCallback.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInCallback.kt new file mode 100644 index 0000000000..073e884eb6 --- /dev/null +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInCallback.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2021 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 2/8/2021. + */ + +package com.adyen.checkout.dropin + +interface DropInCallback { + fun onDropInResult(dropInResult: DropInResult?) +} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResultContract.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResultContract.kt new file mode 100644 index 0000000000..e9a4ee59d8 --- /dev/null +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResultContract.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 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 2/8/2021. + */ + +package com.adyen.checkout.dropin + +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract + +internal class DropInResultContract : ActivityResultContract() { + override fun createIntent(context: Context, input: Intent): Intent { + return input + } + + override fun parseResult(resultCode: Int, intent: Intent?): DropInResult? { + return DropIn.handleActivityResult(DropIn.DROP_IN_REQUEST_CODE, resultCode, intent) + } +} 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 176ed09546..4dd8fc497d 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 @@ -26,6 +26,7 @@ import com.adyen.checkout.core.log.LogUtil import com.adyen.checkout.core.log.Logger import com.adyen.checkout.core.util.LocaleUtil import com.adyen.checkout.dropin.DropIn +import com.adyen.checkout.dropin.DropInCallback import com.adyen.checkout.dropin.DropInConfiguration import com.adyen.checkout.dropin.DropInResult import com.adyen.checkout.example.BuildConfig @@ -39,7 +40,7 @@ import com.adyen.checkout.googlepay.GooglePayConfiguration import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), DropInCallback { companion object { private val TAG: String = LogUtil.getTag() @@ -49,6 +50,8 @@ class MainActivity : AppCompatActivity() { private val paymentMethodsViewModel: PaymentMethodsViewModel by viewModel() private val keyValueStorage: KeyValueStorage by inject() + private val dropInLauncher = DropIn.registerForDropInResult(this, this) + private var isWaitingPaymentMethods = false override fun onCreate(savedInstanceState: Bundle?) { @@ -126,10 +129,8 @@ class MainActivity : AppCompatActivity() { Toast.makeText(this, result, Toast.LENGTH_SHORT).show() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - Logger.d(TAG, "onActivityResult") - val dropInResult = DropIn.handleActivityResult(requestCode, resultCode, data) ?: return + override fun onDropInResult(dropInResult: DropInResult?) { + if (dropInResult == null) return when (dropInResult) { is DropInResult.CancelledByUser -> Toast.makeText(this, "Canceled by user", Toast.LENGTH_SHORT).show() is DropInResult.Error -> Toast.makeText(this, dropInResult.reason, Toast.LENGTH_SHORT).show() @@ -182,7 +183,7 @@ class MainActivity : AppCompatActivity() { val resultIntent = Intent(this, MainActivity::class.java) resultIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP - DropIn.startPayment(this, paymentMethodsApiResponse, dropInConfigurationBuilder.build(), resultIntent) + DropIn.startPayment(this, paymentMethodsApiResponse, dropInConfigurationBuilder.build(), dropInLauncher, resultIntent) } private fun setLoading(isLoading: Boolean) { From cf85160ca564033f4f0697bd4a48eaca7e2d21e1 Mon Sep 17 00:00:00 2001 From: jreij Date: Tue, 3 Aug 2021 13:52:04 +0200 Subject: [PATCH 12/41] Add TODO for google pay and onActivityResult --- .../java/com/adyen/checkout/googlepay/GooglePayComponent.java | 1 + 1 file changed, 1 insertion(+) diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.java b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.java index afd8c1492b..e8955f1f14 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.java +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.java @@ -83,6 +83,7 @@ public void startGooglePayScreen(@NonNull Activity activity, int requestCode) { final GooglePayParams googlePayParams = getGooglePayParams(); final PaymentsClient paymentsClient = Wallet.getPaymentsClient(activity, GooglePayUtils.createWalletOptions(googlePayParams)); final PaymentDataRequest paymentDataRequest = GooglePayUtils.createPaymentDataRequest(googlePayParams); + // TODO this forces us to use the deprecated onActivityResult. Look into alternatives when/if Google provides any later. AutoResolveHelper.resolveTask(paymentsClient.loadPaymentData(paymentDataRequest), activity, requestCode); } From 49f2efb26514f9818137ff21759a98bca8e2b78c Mon Sep 17 00:00:00 2001 From: jreij Date: Thu, 5 Aug 2021 10:08:37 +0200 Subject: [PATCH 13/41] Change activity result method signatures --- .../main/java/com/adyen/checkout/dropin/DropIn.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt index 929f32b96c..5a1206dba2 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt @@ -11,7 +11,7 @@ package com.adyen.checkout.dropin import android.app.Activity import android.content.Context import android.content.Intent -import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher import androidx.fragment.app.Fragment import com.adyen.checkout.components.model.PaymentMethodsApiResponse @@ -43,18 +43,13 @@ object DropIn { internal const val LOCALE_PREF = "drop-in-locale" @JvmStatic - fun registerForDropInResult(activity: ComponentActivity, callback: DropInCallback): ActivityResultLauncher { - return activity.registerForActivityResult(DropInResultContract(), callback::onDropInResult) - } - - @JvmStatic - fun registerForDropInResult(fragment: Fragment, callback: DropInCallback): ActivityResultLauncher { - return fragment.registerForActivityResult(DropInResultContract(), callback::onDropInResult) + fun registerForDropInResult(caller: ActivityResultCaller, callback: DropInCallback): ActivityResultLauncher { + return caller.registerForActivityResult(DropInResultContract(), callback::onDropInResult) } @JvmStatic fun startPayment( - activity: ComponentActivity, + activity: Activity, paymentMethodsApiResponse: PaymentMethodsApiResponse, dropInConfiguration: DropInConfiguration, dropInLauncher: ActivityResultLauncher, From 7cc014cccc1d0768bccbe38b66a93a49c486e69e Mon Sep 17 00:00:00 2001 From: jreij Date: Fri, 6 Aug 2021 10:16:55 +0200 Subject: [PATCH 14/41] Update documentation --- .../java/com/adyen/checkout/dropin/DropIn.kt | 123 +++++++++++++++--- .../adyen/checkout/dropin/DropInCallback.kt | 10 ++ .../com/adyen/checkout/dropin/DropInResult.kt | 28 ++++ .../checkout/example/ui/main/MainActivity.kt | 2 +- 4 files changed, 142 insertions(+), 21 deletions(-) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt index 5a1206dba2..e9964547f0 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt @@ -42,17 +42,64 @@ object DropIn { internal const val DROP_IN_PREFS = "drop-in-shared-prefs" internal const val LOCALE_PREF = "drop-in-locale" + /** + * Register your Activity or Fragment with the Activity Result API and receive the final + * Drop-in result using the [DropInCallback]. + * + * This *must* be called unconditionally, as part of initialization path, typically as a field + * initializer of an Activity or Fragment. + * + * @param caller The class that needs to launch Drop-in and receive its callback. + * @param callback Callback for the Drop-in result. + * + * @return The launcher that can be used to start Drop-in. + */ @JvmStatic fun registerForDropInResult(caller: ActivityResultCaller, callback: DropInCallback): ActivityResultLauncher { return caller.registerForActivityResult(DropInResultContract(), callback::onDropInResult) } + /** + * Starts the checkout flow to be handled by the Drop-in solution. + * Make sure you have [DropInService] set up before calling this. + * Call [registerForDropInResult] to create a launcher when initializing your Activity. + * You can pass a [resultHandlerIntent] that will be launched after the Drop-in has completed + * without any errors. + * We suggest that you set up the [resultHandlerIntent] with the appropriate flags to clear + * the stack of the checkout activities. + * + * You will receive the Drop-in result in the [DropInCallback] parameter specified when + * calling [registerForDropInResult]. + * + * 3 states can occur from this operation: + * - Cancelled by user: the user dismissed the Drop-in before it has completed. + * - Error: a [DropInServiceResult.Error] was returned in the [DropInService], or an error + * has occurred. + * - Finished: a [DropInServiceResult.Finished] was returned in the [DropInService]. + * + * You should always handle the cases of cancellation and error in [DropInCallback.onDropInResult]. + * + * As for the Drop-in finished case, if you did not specify a [resultHandlerIntent], you will + * also receive the result in [DropInCallback.onDropInResult]. + * However, if you do specify a [resultHandlerIntent], [DropInCallback.onDropInResult] will not + * receive the result. Instead, that [resultHandlerIntent] will be launched when the + * payment is finished and will contain the result. You can use the + * [getDropInResultFromIntent] helper method to get it or you can find it in the intent + * extras with key [RESULT_KEY]. + * + * @param activity An activity to start the Checkout flow. + * @param dropInLauncher A launcher to start Drop-in, obtained with [registerForDropInResult]. + * @param paymentMethodsApiResponse The result from the paymentMethods/ endpoint. + * @param dropInConfiguration Additional required configuration data. + * @param resultHandlerIntent Intent to be called after Drop-in has finished. + * + */ @JvmStatic fun startPayment( activity: Activity, + dropInLauncher: ActivityResultLauncher, paymentMethodsApiResponse: PaymentMethodsApiResponse, dropInConfiguration: DropInConfiguration, - dropInLauncher: ActivityResultLauncher, resultHandlerIntent: Intent? = null ) { Logger.d(TAG, "startPayment from Activity") @@ -66,6 +113,61 @@ object DropIn { dropInLauncher.launch(intent) } + + /** + * Starts the checkout flow to be handled by the Drop-in solution. + * Make sure you have [DropInService] set up before calling this. + * Call [registerForDropInResult] to create a launcher when initializing your Fragment. + * You can pass a [resultHandlerIntent] that will be launched after the Drop-in has completed + * without any errors. + * We suggest that you set up the [resultHandlerIntent] with the appropriate flags to clear + * the stack of the checkout activities. + * + * You will receive the Drop-in result in the [DropInCallback] parameter specified when + * calling [registerForDropInResult]. + * + * 3 states can occur from this operation: + * - Cancelled by user: the user dismissed the Drop-in before it has completed. + * - Error: a [DropInServiceResult.Error] was returned in the [DropInService], or an error + * has occurred. + * - Finished: a [DropInServiceResult.Finished] was returned in the [DropInService]. + * + * You should always handle the cases of cancellation and error in [DropInCallback.onDropInResult]. + * + * As for the Drop-in finished case, if you did not specify a [resultHandlerIntent], you will + * also receive the result in [DropInCallback.onDropInResult]. + * However, if you do specify a [resultHandlerIntent], [DropInCallback.onDropInResult] will not + * receive the result. Instead, that [resultHandlerIntent] will be launched when the + * payment is finished and will contain the result. You can use the + * [getDropInResultFromIntent] helper method to get it or you can find it in the intent + * extras with key [RESULT_KEY]. + * + * @param fragment A fragment to start the Checkout flow. + * @param dropInLauncher A launcher to start Drop-in, obtained with [registerForDropInResult]. + * @param paymentMethodsApiResponse The result from the paymentMethods/ endpoint. + * @param dropInConfiguration Additional required configuration data. + * @param resultHandlerIntent Intent to be called after Drop-in has finished. + * + */ + @JvmStatic + fun startPayment( + fragment: Fragment, + dropInLauncher: ActivityResultLauncher, + paymentMethodsApiResponse: PaymentMethodsApiResponse, + dropInConfiguration: DropInConfiguration, + resultHandlerIntent: Intent? = null + ) { + Logger.d(TAG, "startPayment from Fragment") + + val intent = preparePayment( + fragment.requireContext(), + paymentMethodsApiResponse, + dropInConfiguration, + resultHandlerIntent + ) + dropInLauncher.launch(intent) + } + /** * Starts the checkout flow to be handled by the Drop-in solution. * Make sure you have [DropInService] set up before calling this. @@ -125,25 +227,6 @@ object DropIn { activity.startActivityForResult(intent, DROP_IN_REQUEST_CODE) } - @JvmStatic - fun startPayment( - fragment: Fragment, - paymentMethodsApiResponse: PaymentMethodsApiResponse, - dropInConfiguration: DropInConfiguration, - dropInLauncher: ActivityResultLauncher, - resultHandlerIntent: Intent? = null - ) { - Logger.d(TAG, "startPayment from Fragment") - - val intent = preparePayment( - fragment.requireContext(), - paymentMethodsApiResponse, - dropInConfiguration, - resultHandlerIntent - ) - dropInLauncher.launch(intent) - } - /** * Starts the checkout flow to be handled by the Drop-in solution. * Make sure you have [DropInService] set up before calling this. diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInCallback.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInCallback.kt index 073e884eb6..ce2db434b6 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInCallback.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInCallback.kt @@ -8,6 +8,16 @@ package com.adyen.checkout.dropin +/** + * A class that defines the callbacks from Drop-in to the component that launched it. + */ interface DropInCallback { + + /** + * Returns the final result of Drop-in. + * Use this method together with [DropIn.registerForDropInResult]. + * + * @param dropInResult The final result of Drop-in. + */ fun onDropInResult(dropInResult: DropInResult?) } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResult.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResult.kt index e771f8482c..182daaf708 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResult.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResult.kt @@ -8,8 +8,36 @@ package com.adyen.checkout.dropin +import com.adyen.checkout.dropin.service.DropInService +import com.adyen.checkout.dropin.service.DropInServiceResult + +/** + * A class that contains the final result of Drop-in. + */ sealed class DropInResult { + + /** + * Drop-in was dismissed by the user before it has completed. + */ class CancelledByUser : DropInResult() + + /** + * Drop-in has encountered an error. + * + * Two scenarios could trigger this result: + * - An exception occurred during Drop-in. + * - [DropInServiceResult.Error] was returned in [DropInService]. In this case, the [reason] + * parameter will have the same value as [DropInServiceResult.Error.reason]. + * + * @param reason The reason of the error. + */ class Error(val reason: String?) : DropInResult() + + /** + * Drop-in has completed. + * This occurs after returning [DropInServiceResult.Finished] in the [DropInService]. + * + * @param result The result of Drop-in, mirrors the value of [DropInServiceResult.Finished.result]. + */ class Finished(val result: String) : DropInResult() } 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 4dd8fc497d..bbcb87c212 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 @@ -183,7 +183,7 @@ class MainActivity : AppCompatActivity(), DropInCallback { val resultIntent = Intent(this, MainActivity::class.java) resultIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP - DropIn.startPayment(this, paymentMethodsApiResponse, dropInConfigurationBuilder.build(), dropInLauncher, resultIntent) + DropIn.startPayment(this, dropInLauncher, paymentMethodsApiResponse, dropInConfigurationBuilder.build(), resultIntent) } private fun setLoading(isLoading: Boolean) { From b94a65f7e28bede598359d743c1c036455a71dbd Mon Sep 17 00:00:00 2001 From: jreij Date: Tue, 31 Aug 2021 14:04:46 +0200 Subject: [PATCH 15/41] Code checks --- drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt index e9964547f0..8943b9ef4a 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt @@ -113,7 +113,6 @@ object DropIn { dropInLauncher.launch(intent) } - /** * Starts the checkout flow to be handled by the Drop-in solution. * Make sure you have [DropInService] set up before calling this. From 4fe913d254a9a906954f4f48d6c49c96499b5383 Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Thu, 2 Sep 2021 13:12:02 +0200 Subject: [PATCH 16/41] Fix error logo overlapping secondary brand logo in card component --- .../com/adyen/checkout/card/CardComponent.kt | 20 ++++++------- .../java/com/adyen/checkout/card/CardView.kt | 10 +++++-- .../checkout/card/DualBrandedCardUtils.kt | 29 +++++++++---------- 3 files changed, 29 insertions(+), 30 deletions(-) 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 ced63f5207..16c170a074 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -188,16 +188,13 @@ class CardComponent private constructor( } private fun markSelectedCard(cards: List, selectedIndex: Int): List { - return if (cards.size > SINGLE_CARD_LIST_SIZE) { - cards.mapIndexed { index, card -> - if (index == selectedIndex) { - card.copy(isSelected = true) - } else { - card - } + if (cards.size <= SINGLE_CARD_LIST_SIZE) cards + return cards.mapIndexed { index, card -> + if (index == selectedIndex) { + card.copy(isSelected = true) + } else { + card } - } else { - cards } } @@ -357,8 +354,9 @@ class CardComponent private constructor( } } - private fun isDualBrandedFlow(cardOutputData: CardOutputData): Boolean { - return cardOutputData.detectedCardTypes.size > 1 && cardOutputData.detectedCardTypes.any { it.isSelected } + fun isDualBrandedFlow(cardOutputData: CardOutputData): Boolean { + return cardOutputData.detectedCardTypes.filter { it.isReliable }.size > 1 && + cardOutputData.detectedCardTypes.filter { it.isReliable }.any { it.isSelected } } private fun makeAddressData(outputData: CardOutputData): Address { diff --git a/card/src/main/java/com/adyen/checkout/card/CardView.kt b/card/src/main/java/com/adyen/checkout/card/CardView.kt index d7a3196025..e65f1fe223 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardView.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardView.kt @@ -258,13 +258,15 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? binding.editTextCardNumber.setOnChangeListener { mCardInputData.cardNumber = binding.editTextCardNumber.rawValue notifyInputDataChanged() - setCardNumberError(null) + val shouldShowSecondaryLogo = component.outputData?.let { component.isDualBrandedFlow(it) } ?: false + setCardNumberError(null, shouldShowSecondaryLogo) } binding.editTextCardNumber.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean -> if (!component.isStoredPaymentMethod()) { val cardNumberValidation = component.outputData?.cardNumberState?.validation if (hasFocus) { - setCardNumberError(null) + val shouldShowSecondaryLogo = component.outputData?.let { component.isDualBrandedFlow(it) } ?: false + setCardNumberError(null, shouldShowSecondaryLogo) } else if (cardNumberValidation != null && cardNumberValidation is Validation.Invalid) { setCardNumberError(cardNumberValidation.reason) } @@ -272,13 +274,15 @@ class CardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } } - private fun setCardNumberError(@StringRes stringResId: Int?) { + private fun setCardNumberError(@StringRes stringResId: Int?, shouldShowSecondaryLogo: Boolean = false) { if (stringResId == null) { binding.textInputLayoutCardNumber.error = null binding.cardBrandLogoContainerPrimary.isVisible = true + binding.cardBrandLogoContainerSecondary.isVisible = shouldShowSecondaryLogo } else { binding.textInputLayoutCardNumber.error = mLocalizedContext.getString(stringResId) binding.cardBrandLogoContainerPrimary.isVisible = false + binding.cardBrandLogoContainerSecondary.isVisible = false } } diff --git a/card/src/main/java/com/adyen/checkout/card/DualBrandedCardUtils.kt b/card/src/main/java/com/adyen/checkout/card/DualBrandedCardUtils.kt index c2af00f335..e3630ee23b 100644 --- a/card/src/main/java/com/adyen/checkout/card/DualBrandedCardUtils.kt +++ b/card/src/main/java/com/adyen/checkout/card/DualBrandedCardUtils.kt @@ -6,23 +6,20 @@ import com.adyen.checkout.card.data.DetectedCardType object DualBrandedCardUtils { fun sortBrands(cards: List): List { - return when { - cards.size <= 1 -> cards - else -> { - val hasCarteBancaire = cards.any { it.cardType == CardType.CARTEBANCAIRE } - val hasVisa = cards.any { it.cardType == CardType.VISA } - val hasPlcc = cards.any { - it.cardType == CardType.UNKNOWN && - (it.cardType.txVariant.contains("plcc") || it.cardType.txVariant.contains("cbcc")) - } + return if (cards.size <= 1) { + cards + } else { + val hasCarteBancaire = cards.any { it.cardType == CardType.CARTEBANCAIRE } + val hasVisa = cards.any { it.cardType == CardType.VISA } + val hasPlcc = cards.any { + it.cardType == CardType.UNKNOWN && + (it.cardType.txVariant.contains("plcc") || it.cardType.txVariant.contains("cbcc")) + } - if (hasCarteBancaire && hasVisa) { - cards.sortedByDescending { it.cardType == CardType.VISA } - } else if (hasPlcc) { - cards.sortedByDescending { it.cardType.txVariant.contains("plcc") || it.cardType.txVariant.contains("cbcc") } - } else { - cards - } + when { + hasCarteBancaire && hasVisa -> cards.sortedByDescending { it.cardType == CardType.VISA } + hasPlcc -> cards.sortedByDescending { it.cardType.txVariant.contains("plcc") || it.cardType.txVariant.contains("cbcc") } + else -> cards } } } From bc7c08a583e38915729cd397369c12d5b9029d82 Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Fri, 3 Sep 2021 11:14:49 +0200 Subject: [PATCH 17/41] Extract filtered reliable detected card list to prevent filtering twice --- card/src/main/java/com/adyen/checkout/card/CardComponent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 16c170a074..f19fb5d869 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -355,8 +355,8 @@ class CardComponent private constructor( } fun isDualBrandedFlow(cardOutputData: CardOutputData): Boolean { - return cardOutputData.detectedCardTypes.filter { it.isReliable }.size > 1 && - cardOutputData.detectedCardTypes.filter { it.isReliable }.any { it.isSelected } + val reliableDetectedCards = cardOutputData.detectedCardTypes.filter { it.isReliable } + return reliableDetectedCards.size > 1 && reliableDetectedCards.any { it.isSelected } } private fun makeAddressData(outputData: CardOutputData): Address { From 893d45695b05a36516c56f402526d08842b3f93b Mon Sep 17 00:00:00 2001 From: Alvin Choo Date: Wed, 8 Sep 2021 14:10:43 +0800 Subject: [PATCH 18/41] feat: Add NOT_CURRENTLY_KNOWN flag --- .../com/adyen/checkout/googlepay/util/GooglePayUtils.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/util/GooglePayUtils.java b/googlepay/src/main/java/com/adyen/checkout/googlepay/util/GooglePayUtils.java index eb4166461e..07a2f5d90f 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/util/GooglePayUtils.java +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/util/GooglePayUtils.java @@ -69,6 +69,8 @@ public final class GooglePayUtils { private static final String TOKENIZATION_DATA = "tokenizationData"; private static final String TOKEN = "token"; + private static final String NOT_CURRENTLY_KNOWN = "NOT_CURRENTLY_KNOWN"; + /** * Create a {@link com.google.android.gms.wallet.Wallet.WalletOptions} based on the component configuration. * @@ -235,7 +237,9 @@ private static TransactionInfoModel createTransactionInfo(@NonNull GooglePayPara final String displayAmount = GOOGLE_PAY_DECIMAL_FORMAT.format(bigDecimal); final TransactionInfoModel transactionInfoModel = new TransactionInfoModel(); - transactionInfoModel.setTotalPrice(displayAmount); + if (!params.getTotalPriceStatus().equals(NOT_CURRENTLY_KNOWN)) { + transactionInfoModel.setTotalPrice(displayAmount); + } transactionInfoModel.setCountryCode(params.getCountryCode()); transactionInfoModel.setTotalPriceStatus(params.getTotalPriceStatus()); transactionInfoModel.setCurrencyCode(params.getAmount().getCurrency()); From d063c8abfb2fb14e47ef066008a1018a9f124939 Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Wed, 8 Sep 2021 11:35:02 +0200 Subject: [PATCH 19/41] Add ProGuard/R8 consumer rules to the release artifacts. This way merchants don't have to add the ProGuard rules themselves anymore. --- checkout-core/consumer-rules.pro | 2 ++ components-core/consumer-rules.pro | 10 ++++++++++ example-app/build.gradle | 3 ++- example-app/proguard-rules.pro | 5 +---- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/checkout-core/consumer-rules.pro b/checkout-core/consumer-rules.pro index e69de29bb2..c5261a74ff 100644 --- a/checkout-core/consumer-rules.pro +++ b/checkout-core/consumer-rules.pro @@ -0,0 +1,2 @@ +# Keep the model classes for JSON parsing. +-keep class com.adyen.checkout.core.model.** { * ;} diff --git a/components-core/consumer-rules.pro b/components-core/consumer-rules.pro index e69de29bb2..d3eca29fb8 100644 --- a/components-core/consumer-rules.pro +++ b/components-core/consumer-rules.pro @@ -0,0 +1,10 @@ +# Keep the model classes for JSON parsing. +-keep class com.adyen.checkout.components.model.** { *; } + +# Keep the Component constructors for reflection initialization in the Factory. +-keepclassmembers public class * implements com.adyen.checkout.components.PaymentComponent { + public (...); +} +-keepclassmembers public class * implements com.adyen.checkout.components.ActionComponent { + public (...); +} diff --git a/example-app/build.gradle b/example-app/build.gradle index 3440d00389..67f4cebed1 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -57,7 +57,7 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -71,6 +71,7 @@ android { dependencies { // Checkout implementation project(':drop-in') +// implementation "com.adyen.checkout:drop-in:4.1.1" // Dependencies implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_version" diff --git a/example-app/proguard-rules.pro b/example-app/proguard-rules.pro index e5e4d8d945..2080800761 100644 --- a/example-app/proguard-rules.pro +++ b/example-app/proguard-rules.pro @@ -20,7 +20,4 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile --keep class com.adyen.checkout.components.model.** { *; } --keepclassmembers public class * implements com.adyen.checkout.components.PaymentComponent { - public (...); -} \ No newline at end of file +-keep class com.adyen.checkout.example.data.api.model.paymentsRequest.** { *; } From 9c3b892ebac35e512b383e631fbd6c2f1b67738b Mon Sep 17 00:00:00 2001 From: Alvin Choo Date: Thu, 9 Sep 2021 16:16:55 +0800 Subject: [PATCH 20/41] chore: Added comment --- .../java/com/adyen/checkout/googlepay/util/GooglePayUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/util/GooglePayUtils.java b/googlepay/src/main/java/com/adyen/checkout/googlepay/util/GooglePayUtils.java index 07a2f5d90f..85b9191f0b 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/util/GooglePayUtils.java +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/util/GooglePayUtils.java @@ -237,6 +237,7 @@ private static TransactionInfoModel createTransactionInfo(@NonNull GooglePayPara final String displayAmount = GOOGLE_PAY_DECIMAL_FORMAT.format(bigDecimal); final TransactionInfoModel transactionInfoModel = new TransactionInfoModel(); + // Google requires to not pass the price when the price status is NOT_CURRENTLY_KNOWN if (!params.getTotalPriceStatus().equals(NOT_CURRENTLY_KNOWN)) { transactionInfoModel.setTotalPrice(displayAmount); } From d07badd886f572e42a3b7ff08573c03c972f7c5e Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Fri, 10 Sep 2021 10:15:30 +0200 Subject: [PATCH 21/41] Add threeDS2SdkVersion to CardPaymentMethod model. --- .../model/payments/request/CardPaymentMethod.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/components-core/src/main/java/com/adyen/checkout/components/model/payments/request/CardPaymentMethod.java b/components-core/src/main/java/com/adyen/checkout/components/model/payments/request/CardPaymentMethod.java index 1b0a93d019..141a405b6c 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/model/payments/request/CardPaymentMethod.java +++ b/components-core/src/main/java/com/adyen/checkout/components/model/payments/request/CardPaymentMethod.java @@ -37,6 +37,7 @@ public final class CardPaymentMethod extends PaymentMethodDetails { private static final String ENCRYPTED_PASSWORD = "encryptedPassword"; private static final String TAX_NUMBER = "taxNumber"; private static final String BRAND = "brand"; + private static final String THREEDS2_SDK_VERSION = "threeDS2SdkVersion"; @NonNull public static final Serializer SERIALIZER = new Serializer() { @@ -58,6 +59,7 @@ public JSONObject serialize(@NonNull CardPaymentMethod modelObject) { jsonObject.putOpt(ENCRYPTED_PASSWORD, modelObject.getEncryptedPassword()); jsonObject.putOpt(TAX_NUMBER, modelObject.getTaxNumber()); jsonObject.putOpt(BRAND, modelObject.getBrand()); + jsonObject.putOpt(THREEDS2_SDK_VERSION, modelObject.getThreeDS2SdkVersion()); } catch (JSONException e) { throw new ModelSerializationException(IdealPaymentMethod.class, e); } @@ -81,6 +83,7 @@ public CardPaymentMethod deserialize(@NonNull JSONObject jsonObject) { cardPaymentMethod.setEncryptedPassword(jsonObject.optString(ENCRYPTED_PASSWORD, null)); cardPaymentMethod.setTaxNumber(jsonObject.optString(TAX_NUMBER)); cardPaymentMethod.setBrand(jsonObject.optString(BRAND)); + cardPaymentMethod.setThreeDS2SdkVersion(jsonObject.optString(THREEDS2_SDK_VERSION, null)); return cardPaymentMethod; } @@ -95,6 +98,7 @@ public CardPaymentMethod deserialize(@NonNull JSONObject jsonObject) { private String storedPaymentMethodId; private String taxNumber; private String brand; + private String threeDS2SdkVersion; @Override public void writeToParcel(@NonNull Parcel dest, int flags) { @@ -179,6 +183,15 @@ public void setStoredPaymentMethodId(@Nullable String storedPaymentMethodId) { @Nullable public String getStoredPaymentMethodId() { - return this.storedPaymentMethodId; + return storedPaymentMethodId; + } + + @Nullable + public String getThreeDS2SdkVersion() { + return threeDS2SdkVersion; + } + + public void setThreeDS2SdkVersion(@Nullable String threeDS2SdkVersion) { + this.threeDS2SdkVersion = threeDS2SdkVersion; } } From 62ef9155ed7688d5c43e4cb4232946b7542fc056 Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Fri, 10 Sep 2021 13:47:30 +0200 Subject: [PATCH 22/41] Pass 3DS2 SDK version to CardPaymentMethod If the 3DS2 SDK is available in the code. --- card/build.gradle | 3 +++ .../src/main/java/com/adyen/checkout/card/CardComponent.kt | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/card/build.gradle b/card/build.gradle index dd050ea849..2fd6e85c31 100644 --- a/card/build.gradle +++ b/card/build.gradle @@ -51,6 +51,9 @@ dependencies { api project(':ui-core') api project(':cse') + // If 3DS2 SDK is present. + compileOnly "com.adyen.threeds:adyen-3ds2:$adyen3ds2_version" + // Dependencies implementation "com.google.android.material:material:$material_version" 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 f19fb5d869..dc62a635ef 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -29,6 +29,7 @@ import com.adyen.checkout.cse.EncryptedCard import com.adyen.checkout.cse.GenericEncrypter import com.adyen.checkout.cse.UnencryptedCard import com.adyen.checkout.cse.exception.EncryptionException +import com.adyen.threeds2.ThreeDS2Service import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -305,6 +306,12 @@ class CardComponent private constructor( cardPaymentMethod.brand = stateOutputData.detectedCardTypes.first { it.isSelected }.cardType.txVariant } + try { + cardPaymentMethod.threeDS2SdkVersion = ThreeDS2Service.INSTANCE.sdkVersion + } catch (e: ClassNotFoundException) { + Logger.e(TAG, "threeDS2SdkVersion not set because 3DS2 SDK is not present in project.") + } + val paymentComponentData = PaymentComponentData().apply { paymentMethod = cardPaymentMethod setStorePaymentMethod(stateOutputData.isStoredPaymentMethodEnable) From 5c0bca4b690f3c972af88ca41539d53f93395c17 Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Fri, 10 Sep 2021 14:01:34 +0200 Subject: [PATCH 23/41] Also pass the 3DS2 SDK version in BCMC component. --- bcmc/build.gradle | 3 +++ bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/bcmc/build.gradle b/bcmc/build.gradle index 72d7721dcc..c1eaea1cbc 100644 --- a/bcmc/build.gradle +++ b/bcmc/build.gradle @@ -45,6 +45,9 @@ dependencies { // Checkout api project(":card") + // If 3DS2 SDK is present. + compileOnly "com.adyen.threeds:adyen-3ds2:$adyen3ds2_version" + // Dependencies implementation "com.google.android.material:material:$material_version" 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 bb97c6484d..3fd117f263 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt @@ -27,6 +27,7 @@ import com.adyen.checkout.core.log.Logger import com.adyen.checkout.cse.CardEncrypter import com.adyen.checkout.cse.UnencryptedCard import com.adyen.checkout.cse.exception.EncryptionException +import com.adyen.threeds2.ThreeDS2Service import kotlinx.coroutines.launch private val TAG = LogUtil.getTag() @@ -121,6 +122,11 @@ class BcmcComponent( encryptedCardNumber = encryptedCard.encryptedCardNumber encryptedExpiryMonth = encryptedCard.encryptedExpiryMonth encryptedExpiryYear = encryptedCard.encryptedExpiryYear + try { + threeDS2SdkVersion = ThreeDS2Service.INSTANCE.sdkVersion + } catch (e: ClassNotFoundException) { + Logger.e(TAG, "threeDS2SdkVersion not set because 3DS2 SDK is not present in project.") + } } paymentComponentData.paymentMethod = cardPaymentMethod return GenericComponentState(paymentComponentData, true, true) From 8f0a0bb33b785c7bd9b17868ef693541569be0b4 Mon Sep 17 00:00:00 2001 From: jreij Date: Tue, 7 Sep 2021 18:39:12 +0200 Subject: [PATCH 24/41] Allow removing observers from components --- .../com/adyen/checkout/components/Component.java | 16 +++++++++++++++- .../components/base/BaseActionComponent.java | 10 ++++++++++ .../components/base/BasePaymentComponent.java | 10 ++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/components-core/src/main/java/com/adyen/checkout/components/Component.java b/components-core/src/main/java/com/adyen/checkout/components/Component.java index 2cba00cab5..d1cd96d454 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/Component.java +++ b/components-core/src/main/java/com/adyen/checkout/components/Component.java @@ -24,13 +24,20 @@ public interface Component observer); + /** + * Remove all observers attached to this component using {@link #observe(LifecycleOwner, Observer)}. + * + * @param lifecycleOwner The lifecycle for which the observer is active. + */ + void removeObservers(@NonNull LifecycleOwner lifecycleOwner); + /** * Observe if an unexpected error happens during the processing of the Component. * Error handling might need to fail the payment process, retry or show UI feedback. @@ -40,6 +47,13 @@ public interface Component observer); + /** + * Remove all error observers attached to this component using {@link #observeErrors(LifecycleOwner, Observer)}. + * + * @param lifecycleOwner The lifecycle for which the observer is active. + */ + void removeErrorObservers(@NonNull LifecycleOwner lifecycleOwner); + /** * @return The {@link Configuration} object used to initialize this Component. */ diff --git a/components-core/src/main/java/com/adyen/checkout/components/base/BaseActionComponent.java b/components-core/src/main/java/com/adyen/checkout/components/base/BaseActionComponent.java index 8f20a2be7e..9141845982 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/base/BaseActionComponent.java +++ b/components-core/src/main/java/com/adyen/checkout/components/base/BaseActionComponent.java @@ -65,11 +65,21 @@ public void observe(@NonNull LifecycleOwner lifecycleOwner, @NonNull Observer observer) { mErrorMutableLiveData.observe(lifecycleOwner, observer); } + @Override + public void removeErrorObservers(@NonNull LifecycleOwner lifecycleOwner) { + mErrorMutableLiveData.removeObservers(lifecycleOwner); + } + /** * Call this method to save the current data of the Component to the Bundle from {@link Activity#onSaveInstanceState(Bundle)}. * diff --git a/components-core/src/main/java/com/adyen/checkout/components/base/BasePaymentComponent.java b/components-core/src/main/java/com/adyen/checkout/components/base/BasePaymentComponent.java index 7c1a44af53..3271fd1bf0 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/base/BasePaymentComponent.java +++ b/components-core/src/main/java/com/adyen/checkout/components/base/BasePaymentComponent.java @@ -75,11 +75,21 @@ public void observe(@NonNull LifecycleOwner lifecycleOwner, @NonNull Observer observer) { mComponentErrorLiveData.observe(lifecycleOwner, observer); } + @Override + public void removeErrorObservers(@NonNull LifecycleOwner lifecycleOwner) { + mComponentErrorLiveData.removeObservers(lifecycleOwner); + } + @Override @Nullable public PaymentComponentState getState() { From 5d4d44f45fa7b5267f913318cf7019e3acb65700 Mon Sep 17 00:00:00 2001 From: jreij Date: Mon, 13 Sep 2021 09:54:28 +0200 Subject: [PATCH 25/41] Allow removing single observer --- .../com/adyen/checkout/components/Component.java | 14 ++++++++++++++ .../components/base/BaseActionComponent.java | 10 ++++++++++ .../components/base/BasePaymentComponent.java | 10 ++++++++++ 3 files changed, 34 insertions(+) diff --git a/components-core/src/main/java/com/adyen/checkout/components/Component.java b/components-core/src/main/java/com/adyen/checkout/components/Component.java index d1cd96d454..eb40a07c97 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/Component.java +++ b/components-core/src/main/java/com/adyen/checkout/components/Component.java @@ -38,6 +38,13 @@ public interface Component observer); + /** * Observe if an unexpected error happens during the processing of the Component. * Error handling might need to fail the payment process, retry or show UI feedback. @@ -54,6 +61,13 @@ public interface Component observer); + /** * @return The {@link Configuration} object used to initialize this Component. */ diff --git a/components-core/src/main/java/com/adyen/checkout/components/base/BaseActionComponent.java b/components-core/src/main/java/com/adyen/checkout/components/base/BaseActionComponent.java index 9141845982..b233d79fc0 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/base/BaseActionComponent.java +++ b/components-core/src/main/java/com/adyen/checkout/components/base/BaseActionComponent.java @@ -70,6 +70,11 @@ public void removeObservers(@NonNull LifecycleOwner lifecycleOwner) { mResultLiveData.removeObservers(lifecycleOwner); } + @Override + public void removeObserver(@NonNull final Observer observer) { + mResultLiveData.removeObserver(observer); + } + @Override public void observeErrors(@NonNull LifecycleOwner lifecycleOwner, @NonNull Observer observer) { mErrorMutableLiveData.observe(lifecycleOwner, observer); @@ -80,6 +85,11 @@ public void removeErrorObservers(@NonNull LifecycleOwner lifecycleOwner) { mErrorMutableLiveData.removeObservers(lifecycleOwner); } + @Override + public void removeErrorObserver(@NonNull final Observer observer) { + mErrorMutableLiveData.removeObserver(observer); + } + /** * Call this method to save the current data of the Component to the Bundle from {@link Activity#onSaveInstanceState(Bundle)}. * diff --git a/components-core/src/main/java/com/adyen/checkout/components/base/BasePaymentComponent.java b/components-core/src/main/java/com/adyen/checkout/components/base/BasePaymentComponent.java index 3271fd1bf0..53744eec5d 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/base/BasePaymentComponent.java +++ b/components-core/src/main/java/com/adyen/checkout/components/base/BasePaymentComponent.java @@ -80,6 +80,11 @@ public void removeObservers(@NonNull LifecycleOwner lifecycleOwner) { mPaymentComponentStateLiveData.removeObservers(lifecycleOwner); } + @Override + public void removeObserver(@NonNull final Observer observer) { + mPaymentComponentStateLiveData.removeObserver(observer); + } + @Override public void observeErrors(@NonNull LifecycleOwner lifecycleOwner, @NonNull Observer observer) { mComponentErrorLiveData.observe(lifecycleOwner, observer); @@ -90,6 +95,11 @@ public void removeErrorObservers(@NonNull LifecycleOwner lifecycleOwner) { mComponentErrorLiveData.removeObservers(lifecycleOwner); } + @Override + public void removeErrorObserver(@NonNull final Observer observer) { + mComponentErrorLiveData.removeObserver(observer); + } + @Override @Nullable public PaymentComponentState getState() { From fa99b6bb35e6894d11714307bdec0ff27610213b Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Thu, 9 Sep 2021 10:56:48 +0200 Subject: [PATCH 26/41] Add GooglePayParamUtils and tests --- .../googlepay/model/GooglePayParamUtils.kt | 15 ++++++++++ .../googlepay/GooglePayParamUtilsTest.kt | 28 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParamUtils.kt create mode 100644 googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayParamUtilsTest.kt diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParamUtils.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParamUtils.kt new file mode 100644 index 0000000000..04d04dcd16 --- /dev/null +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParamUtils.kt @@ -0,0 +1,15 @@ +package com.adyen.checkout.googlepay.model + +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.googlepay.util.AllowedCardNetworks + +internal object GooglePayParamUtils { + + fun mapTxVariantToGooglePayCode(txVariant: String): String { + return when { + txVariant == "mc" -> AllowedCardNetworks.MASTERCARD + AllowedCardNetworks.getAllAllowedCardNetworks().contains(txVariant.uppercase()) -> txVariant.uppercase() + else -> throw CheckoutException("txVariant $txVariant is not an allowed card network.") + } + } +} diff --git a/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayParamUtilsTest.kt b/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayParamUtilsTest.kt new file mode 100644 index 0000000000..7ecef06090 --- /dev/null +++ b/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayParamUtilsTest.kt @@ -0,0 +1,28 @@ +package com.adyen.checkout.googlepay + +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.googlepay.model.GooglePayParamUtils +import com.adyen.checkout.googlepay.util.AllowedCardNetworks +import org.junit.Assert.assertEquals +import org.junit.Test + +class GooglePayParamUtilsTest { + + @Test + fun testMasterCardTxVariantToGooglePayCodeMapping() { + val mc = "mc" + assertEquals(AllowedCardNetworks.MASTERCARD, GooglePayParamUtils.mapTxVariantToGooglePayCode(mc)) + } + + @Test + fun testOtherTxVariantToGooglePayCodeMapping() { + val amex = "amex" + assertEquals(AllowedCardNetworks.AMEX, GooglePayParamUtils.mapTxVariantToGooglePayCode(amex)) + } + + @Test(expected = CheckoutException::class) + fun testUnsupportedTxVariantToGooglePayCodeMapping() { + val maestro = "maestro" + GooglePayParamUtils.mapTxVariantToGooglePayCode(maestro) + } +} From 78cceddc45d4dbfecadf18ba63bb01212f3d213f Mon Sep 17 00:00:00 2001 From: ozgur <6615094+ozgur00@users.noreply.github.com> Date: Thu, 9 Sep 2021 11:11:26 +0200 Subject: [PATCH 27/41] Use brands from API response for allowed card networks in Google Pay --- .../checkout/googlepay/GooglePayComponent.java | 2 +- .../checkout/googlepay/GooglePayConfiguration.java | 3 +-- .../adyen/checkout/googlepay/GooglePayProvider.kt | 2 +- .../checkout/googlepay/model/GooglePayParams.kt | 14 ++++++++++++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.java b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.java index e8955f1f14..05b9ed07bf 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.java +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.java @@ -90,7 +90,7 @@ public void startGooglePayScreen(@NonNull Activity activity, int requestCode) { private GooglePayParams getGooglePayParams() { final Configuration configuration = getPaymentMethod().getConfiguration(); final String serverGatewayMerchantId = (configuration != null) ? configuration.getGatewayMerchantId() : null; - return new GooglePayParams(getConfiguration(), serverGatewayMerchantId); + return new GooglePayParams(getConfiguration(), serverGatewayMerchantId, getPaymentMethod().getBrands()); } private PaymentMethod getPaymentMethod() { diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.java b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.java index 96df16b880..949adbcb92 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.java +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.java @@ -26,7 +26,6 @@ import com.adyen.checkout.googlepay.model.MerchantInfo; import com.adyen.checkout.googlepay.model.ShippingAddressParameters; import com.adyen.checkout.googlepay.util.AllowedAuthMethods; -import com.adyen.checkout.googlepay.util.AllowedCardNetworks; import com.google.android.gms.wallet.WalletConstants; import java.util.List; @@ -200,7 +199,7 @@ public static final class Builder extends BaseConfigurationBuilder mBuilderAllowedAuthMethods = AllowedAuthMethods.getAllAllowedAuthMethods(); - private List mBuilderAllowedCardNetworks = AllowedCardNetworks.getAllAllowedCardNetworks(); + private List mBuilderAllowedCardNetworks = null; private boolean mBuilderAllowPrepaidCards = false; private boolean mBuilderEmailRequired; private boolean mBuilderExistingPaymentMethodRequired; diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayProvider.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayProvider.kt index aae7e050f7..5659f1cccd 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayProvider.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayProvider.kt @@ -55,7 +55,7 @@ class GooglePayProvider : val callbackWeakReference: WeakReference> = WeakReference>(callback) val serverGatewayMerchantId = paymentMethod.configuration?.gatewayMerchantId - val params = GooglePayParams(configuration, serverGatewayMerchantId) + val params = GooglePayParams(configuration, serverGatewayMerchantId, paymentMethod.brands) val paymentsClient: PaymentsClient = Wallet.getPaymentsClient(applicationContext, GooglePayUtils.createWalletOptions(params)) val readyToPayRequest: IsReadyToPayRequest = GooglePayUtils.createIsReadyToPayRequest(params) val readyToPayTask: Task = paymentsClient.isReadyToPay(readyToPayRequest) diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParams.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParams.kt index 7a34293e13..0a5129f2a7 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParams.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParams.kt @@ -11,13 +11,15 @@ package com.adyen.checkout.googlepay.model import com.adyen.checkout.components.model.payments.Amount import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.googlepay.GooglePayConfiguration +import com.adyen.checkout.googlepay.util.AllowedCardNetworks /** * Model class holding the parameters required to build requests for GooglePay */ data class GooglePayParams( private val googlePayConfiguration: GooglePayConfiguration, - private val serverGatewayMerchantId: String? + private val serverGatewayMerchantId: String?, + private val availableCardNetworksFromApi: List? ) { val gatewayMerchantId: String = getPreferredGatewayMerchantId() val googlePayEnvironment: Int = googlePayConfiguration.googlePayEnvironment @@ -26,7 +28,7 @@ data class GooglePayParams( val countryCode: String? = googlePayConfiguration.countryCode val merchantInfo: MerchantInfo? = googlePayConfiguration.merchantInfo val allowedAuthMethods: List? = googlePayConfiguration.allowedAuthMethods - val allowedCardNetworks: List? = googlePayConfiguration.allowedCardNetworks + val allowedCardNetworks: List = getAvailableCardNetworks() val isAllowPrepaidCards: Boolean = googlePayConfiguration.isAllowPrepaidCards val isEmailRequired: Boolean = googlePayConfiguration.isEmailRequired val isExistingPaymentMethodRequired: Boolean = googlePayConfiguration.isExistingPaymentMethodRequired @@ -45,4 +47,12 @@ data class GooglePayParams( "GooglePay merchantAccount not found. Update your API version or pass it manually inside your GooglePayConfiguration" ) } + + private fun getAvailableCardNetworks(): List { + return googlePayConfiguration.allowedCardNetworks + ?: availableCardNetworksFromApi + ?.map { GooglePayParamUtils.mapTxVariantToGooglePayCode(it) } + ?.filter { code -> AllowedCardNetworks.getAllAllowedCardNetworks().any { it == code } } + ?: AllowedCardNetworks.getAllAllowedCardNetworks() + } } From a1dd8de055303cfc333756bb5cca548e9e6a8b66 Mon Sep 17 00:00:00 2001 From: jreij Date: Mon, 13 Sep 2021 16:02:26 +0200 Subject: [PATCH 28/41] Minor refactoring of GooglePayParamUtils --- .../googlepay/model/GooglePayParamUtils.kt | 9 ++++----- .../checkout/googlepay/model/GooglePayParams.kt | 17 ++++++++++++++--- .../googlepay/GooglePayParamUtilsTest.kt | 16 ++++++++-------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParamUtils.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParamUtils.kt index 04d04dcd16..13eb1be7ec 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParamUtils.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParamUtils.kt @@ -1,15 +1,14 @@ package com.adyen.checkout.googlepay.model -import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.googlepay.util.AllowedCardNetworks internal object GooglePayParamUtils { - fun mapTxVariantToGooglePayCode(txVariant: String): String { + fun mapBrandToGooglePayNetwork(brand: String): String? { return when { - txVariant == "mc" -> AllowedCardNetworks.MASTERCARD - AllowedCardNetworks.getAllAllowedCardNetworks().contains(txVariant.uppercase()) -> txVariant.uppercase() - else -> throw CheckoutException("txVariant $txVariant is not an allowed card network.") + brand == "mc" -> AllowedCardNetworks.MASTERCARD + AllowedCardNetworks.getAllAllowedCardNetworks().contains(brand.uppercase()) -> brand.uppercase() + else -> null } } } diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParams.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParams.kt index 0a5129f2a7..56bd87db20 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParams.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/model/GooglePayParams.kt @@ -10,9 +10,13 @@ package com.adyen.checkout.googlepay.model import com.adyen.checkout.components.model.payments.Amount import com.adyen.checkout.core.exception.ComponentException +import com.adyen.checkout.core.log.LogUtil +import com.adyen.checkout.core.log.Logger import com.adyen.checkout.googlepay.GooglePayConfiguration import com.adyen.checkout.googlepay.util.AllowedCardNetworks +private val TAG = LogUtil.getTag() + /** * Model class holding the parameters required to build requests for GooglePay */ @@ -50,9 +54,16 @@ data class GooglePayParams( private fun getAvailableCardNetworks(): List { return googlePayConfiguration.allowedCardNetworks - ?: availableCardNetworksFromApi - ?.map { GooglePayParamUtils.mapTxVariantToGooglePayCode(it) } - ?.filter { code -> AllowedCardNetworks.getAllAllowedCardNetworks().any { it == code } } + ?: getAvailableCardNetworksFromApi() ?: AllowedCardNetworks.getAllAllowedCardNetworks() } + + private fun getAvailableCardNetworksFromApi(): List? { + if (availableCardNetworksFromApi == null) return null + return availableCardNetworksFromApi.mapNotNull { brand -> + val network = GooglePayParamUtils.mapBrandToGooglePayNetwork(brand) + if (network == null) Logger.e(TAG, "skipping brand $brand, as it is not an allowed card network.") + return@mapNotNull network + } + } } diff --git a/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayParamUtilsTest.kt b/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayParamUtilsTest.kt index 7ecef06090..7dbfbb4cd4 100644 --- a/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayParamUtilsTest.kt +++ b/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayParamUtilsTest.kt @@ -1,28 +1,28 @@ package com.adyen.checkout.googlepay -import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.googlepay.model.GooglePayParamUtils import com.adyen.checkout.googlepay.util.AllowedCardNetworks import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Test class GooglePayParamUtilsTest { @Test - fun testMasterCardTxVariantToGooglePayCodeMapping() { + fun testMasterCardToGooglePayNetworkMapping() { val mc = "mc" - assertEquals(AllowedCardNetworks.MASTERCARD, GooglePayParamUtils.mapTxVariantToGooglePayCode(mc)) + assertEquals(AllowedCardNetworks.MASTERCARD, GooglePayParamUtils.mapBrandToGooglePayNetwork(mc)) } @Test - fun testOtherTxVariantToGooglePayCodeMapping() { + fun testOtherBrandToGooglePayNetworkMapping() { val amex = "amex" - assertEquals(AllowedCardNetworks.AMEX, GooglePayParamUtils.mapTxVariantToGooglePayCode(amex)) + assertEquals(AllowedCardNetworks.AMEX, GooglePayParamUtils.mapBrandToGooglePayNetwork(amex)) } - @Test(expected = CheckoutException::class) - fun testUnsupportedTxVariantToGooglePayCodeMapping() { + @Test + fun testUnsupportedBrandToGooglePayNetworkMapping() { val maestro = "maestro" - GooglePayParamUtils.mapTxVariantToGooglePayCode(maestro) + assertNull(GooglePayParamUtils.mapBrandToGooglePayNetwork(maestro)) } } From cd4da06957714de9fa16e2410a65892c0feefe8d Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Tue, 14 Sep 2021 14:20:37 +0200 Subject: [PATCH 29/41] Add shopper reference and show store payment options to BcmcConfiguration. --- .../checkout/bcmc/BcmcConfiguration.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java index 5c08b9f2fd..2b738c85c5 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java @@ -13,10 +13,12 @@ import android.os.Parcelable; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.adyen.checkout.components.base.BaseConfigurationBuilder; import com.adyen.checkout.components.base.Configuration; import com.adyen.checkout.core.api.Environment; +import com.adyen.checkout.core.util.ParcelUtils; import java.util.Locale; @@ -25,6 +27,9 @@ */ public class BcmcConfiguration extends Configuration { + private final String mShopperReference; + private final boolean mShowStorePaymentField; + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public BcmcConfiguration createFromParcel(@NonNull Parcel in) { return new BcmcConfiguration(in); @@ -37,15 +42,31 @@ public BcmcConfiguration[] newArray(int size) { BcmcConfiguration(@NonNull Builder builder) { super(builder.getBuilderShopperLocale(), builder.getBuilderEnvironment(), builder.getBuilderClientKey()); + + mShopperReference = builder.mShopperReference; + mShowStorePaymentField = builder.mBuilderShowStorePaymentField; } BcmcConfiguration(@NonNull Parcel in) { super(in); + mShopperReference = in.readString(); + mShowStorePaymentField = ParcelUtils.readBoolean(in); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); + dest.writeString(mShopperReference); + ParcelUtils.writeBoolean(dest, mShowStorePaymentField); + } + + @Nullable + public String getShopperReference() { + return mShopperReference; + } + + public boolean isShowStorePaymentFieldEnable() { + return mShowStorePaymentField; } /** @@ -53,6 +74,9 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { */ public static final class Builder extends BaseConfigurationBuilder { + private boolean mBuilderShowStorePaymentField = false; + private String mShopperReference; + /** * Constructor of Card Configuration Builder with default values. * @@ -90,6 +114,32 @@ public Builder setEnvironment(@NonNull Environment builderEnvironment) { return (Builder) super.setEnvironment(builderEnvironment); } + /** + * Set if the option to store the card for future payments should be shown as an input field. + * + * @param showStorePaymentField {@link Boolean} + * @return {@link BcmcConfiguration.Builder} + */ + @NonNull + public BcmcConfiguration.Builder setShowStorePaymentField(boolean showStorePaymentField) { + mBuilderShowStorePaymentField = showStorePaymentField; + return this; + } + + /** + * Set the unique reference for the shopper doing this transaction. + * This value will simply be passed back to you in the {@link com.adyen.checkout.components.model.payments.request.PaymentComponentData} + * for convenience. + * + * @param shopperReference The unique shopper reference + * @return {@link BcmcConfiguration.Builder} + */ + @NonNull + public BcmcConfiguration.Builder setShopperReference(@NonNull String shopperReference) { + mShopperReference = shopperReference; + return this; + } + /** * Build {@link BcmcConfiguration} object from {@link BcmcConfiguration.Builder} inputs. * From 45c87d9803d6480143a138bc32cdbbccbdb195eb Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Tue, 14 Sep 2021 14:44:00 +0200 Subject: [PATCH 30/41] Add Switch to store payment method to BcmcView. --- .../checkout/bcmc/BcmcConfiguration.java | 2 +- .../adyen/checkout/bcmc/BcmcInputData.java | 9 +++++++ .../com/adyen/checkout/bcmc/BcmcView.java | 24 +++++++++++++++++-- bcmc/src/main/res/layout/bcmc_view.xml | 6 +++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java index 2b738c85c5..887d318a38 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java @@ -65,7 +65,7 @@ public String getShopperReference() { return mShopperReference; } - public boolean isShowStorePaymentFieldEnable() { + public boolean isShowStorePaymentField() { return mShowStorePaymentField; } diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcInputData.java b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcInputData.java index 33b0e91723..71575564c0 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcInputData.java +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcInputData.java @@ -16,6 +16,7 @@ public final class BcmcInputData implements InputData { private String mCardNumber = ""; private ExpiryDate mExpiryDate = ExpiryDate.EMPTY_DATE; + private boolean mIsStorePaymentSelected = false; @NonNull public String getCardNumber() { @@ -34,4 +35,12 @@ public ExpiryDate getExpiryDate() { public void setExpiryDate(@NonNull ExpiryDate expiryDate) { mExpiryDate = expiryDate; } + + public boolean isStorePaymentSelected() { + return mIsStorePaymentSelected; + } + + public void setStorePaymentSelected( boolean storePaymentSelected) { + mIsStorePaymentSelected = storePaymentSelected; + } } diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcView.java b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcView.java index 127860ffdd..a0293812ad 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcView.java +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcView.java @@ -18,6 +18,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.appcompat.widget.SwitchCompat; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.Observer; @@ -26,10 +27,10 @@ import com.adyen.checkout.components.GenericComponentState; import com.adyen.checkout.components.api.ImageLoader; import com.adyen.checkout.components.model.payments.request.CardPaymentMethod; +import com.adyen.checkout.components.ui.FieldState; import com.adyen.checkout.components.ui.Validation; import com.adyen.checkout.components.ui.view.AdyenLinearLayout; import com.adyen.checkout.components.ui.view.RoundCornerImageView; -import com.adyen.checkout.components.ui.FieldState; import com.google.android.material.textfield.TextInputLayout; /** @@ -47,6 +48,8 @@ public final class BcmcView private TextInputLayout mExpiryDateInput; private TextInputLayout mCardNumberInput; + private SwitchCompat mSwitchStorePaymentMethod; + private final BcmcInputData mCardInputData = new BcmcInputData(); private ImageLoader mImageLoader; @@ -73,7 +76,7 @@ public BcmcView(@NonNull Context context, @Nullable AttributeSet attrs, int defS @Override protected void initLocalizedStrings(@NonNull Context localizedContext) { - final int[] myAttrs = {android.R.attr.hint}; + int[] myAttrs = {android.R.attr.hint}; TypedArray typedArray; // Card Number @@ -85,6 +88,12 @@ protected void initLocalizedStrings(@NonNull Context localizedContext) { typedArray = localizedContext.obtainStyledAttributes(R.style.AdyenCheckout_Card_ExpiryDateInput, myAttrs); mExpiryDateInput.setHint(typedArray.getString(0)); typedArray.recycle(); + + // Store Switch + myAttrs = new int[] {android.R.attr.text}; + typedArray = localizedContext.obtainStyledAttributes(R.style.AdyenCheckout_Card_StorePaymentSwitch, myAttrs); + mSwitchStorePaymentMethod.setText(typedArray.getString(0)); + typedArray.recycle(); } @Override @@ -93,6 +102,7 @@ public void initView() { initCardNumberInput(); initExpiryDateInput(); + initStorePaymentMethodSwitch(); } @Override @@ -215,4 +225,14 @@ private void initExpiryDateInput() { } }); } + + private void initStorePaymentMethodSwitch() { + mSwitchStorePaymentMethod = findViewById(R.id.switch_storePaymentMethod); + + mSwitchStorePaymentMethod.setVisibility(getComponent().getConfiguration().isShowStorePaymentField() ? VISIBLE : GONE); + mSwitchStorePaymentMethod.setOnCheckedChangeListener((buttonView, isChecked) -> { + mCardInputData.setStorePaymentSelected(isChecked); + notifyInputDataChanged(); + }); + } } diff --git a/bcmc/src/main/res/layout/bcmc_view.xml b/bcmc/src/main/res/layout/bcmc_view.xml index d5e1df99d0..6a413f0904 100644 --- a/bcmc/src/main/res/layout/bcmc_view.xml +++ b/bcmc/src/main/res/layout/bcmc_view.xml @@ -59,4 +59,10 @@ tools:ignore="RequiredSize" /> + + \ No newline at end of file From 5bd745ecad963891e3df413bf8d7cfe4c6a132a7 Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Tue, 14 Sep 2021 15:00:41 +0200 Subject: [PATCH 31/41] Rename isShowStorePaymentFieldEnable to isStorePaymentFieldVisible because it had a typo. --- .../com/adyen/checkout/bcmc/BcmcConfiguration.java | 2 +- .../main/java/com/adyen/checkout/bcmc/BcmcView.java | 2 +- .../main/java/com/adyen/checkout/card/CardComponent.kt | 2 +- .../com/adyen/checkout/card/CardConfiguration.java | 10 +++++++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java index 887d318a38..612f797fda 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.java @@ -65,7 +65,7 @@ public String getShopperReference() { return mShopperReference; } - public boolean isShowStorePaymentField() { + public boolean isStorePaymentFieldVisible() { return mShowStorePaymentField; } diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcView.java b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcView.java index a0293812ad..bbe1649f24 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcView.java +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcView.java @@ -229,7 +229,7 @@ private void initExpiryDateInput() { private void initStorePaymentMethodSwitch() { mSwitchStorePaymentMethod = findViewById(R.id.switch_storePaymentMethod); - mSwitchStorePaymentMethod.setVisibility(getComponent().getConfiguration().isShowStorePaymentField() ? VISIBLE : GONE); + mSwitchStorePaymentMethod.setVisibility(getComponent().getConfiguration().isStorePaymentFieldVisible() ? VISIBLE : GONE); mSwitchStorePaymentMethod.setOnCheckedChangeListener((buttonView, isChecked) -> { mCardInputData.setStorePaymentSelected(isChecked); notifyInputDataChanged(); 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 dc62a635ef..119c17d80e 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -351,7 +351,7 @@ class CardComponent private constructor( } fun showStorePaymentField(): Boolean { - return configuration.isShowStorePaymentFieldEnable + return configuration.isStorePaymentFieldVisible } @StringRes fun getKcpBirthDateOrTaxNumberHint(input: String): Int { diff --git a/card/src/main/java/com/adyen/checkout/card/CardConfiguration.java b/card/src/main/java/com/adyen/checkout/card/CardConfiguration.java index d7c8fb697b..c61cd28893 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardConfiguration.java +++ b/card/src/main/java/com/adyen/checkout/card/CardConfiguration.java @@ -130,10 +130,18 @@ public String getShopperReference() { return mShopperReference; } + /** + * @deprecated in favor of isStorePaymentFieldVisible because it had a typo. + */ + @Deprecated public boolean isShowStorePaymentFieldEnable() { return mShowStorePaymentField; } + public boolean isStorePaymentFieldVisible() { + return mShowStorePaymentField; + } + @NonNull public Builder newBuilder() { return new Builder(this); @@ -184,7 +192,7 @@ public Builder(@NonNull CardConfiguration cardConfiguration) { super(cardConfiguration.getShopperLocale(), cardConfiguration.getEnvironment(), cardConfiguration.getClientKey()); mBuilderSupportedCardTypes = cardConfiguration.getSupportedCardTypes(); mBuilderHolderNameRequired = cardConfiguration.isHolderNameRequired(); - mBuilderShowStorePaymentField = cardConfiguration.isShowStorePaymentFieldEnable(); + mBuilderShowStorePaymentField = cardConfiguration.isStorePaymentFieldVisible(); mShopperReference = cardConfiguration.getShopperReference(); mBuilderHideCvc = cardConfiguration.isHideCvc(); mBuilderHideCvcStoredCard = cardConfiguration.isHideCvcStoredCard(); From c1d7a67cec7428e588d81638e86029f816896ca8 Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Tue, 14 Sep 2021 15:13:37 +0200 Subject: [PATCH 32/41] Add isStorePaymentSelected result to the paymentComponentData. --- .../main/java/com/adyen/checkout/bcmc/BcmcComponent.kt | 5 ++++- .../java/com/adyen/checkout/bcmc/BcmcOutputData.java | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) 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 3fd117f263..8fad2302c1 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt @@ -79,7 +79,8 @@ class BcmcComponent( Logger.v(TAG, "onInputDataChanged") return BcmcOutputData( validateCardNumber(inputData.cardNumber), - validateExpiryDate(inputData.expiryDate) + validateExpiryDate(inputData.expiryDate), + inputData.isStorePaymentSelected ) } @@ -129,6 +130,8 @@ class BcmcComponent( } } paymentComponentData.paymentMethod = cardPaymentMethod + paymentComponentData.setStorePaymentMethod(outputData.isStoredPaymentMethodEnabled) + paymentComponentData.shopperReference = configuration.shopperReference return GenericComponentState(paymentComponentData, true, true) } diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcOutputData.java b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcOutputData.java index dee4c680c7..1c4fa2b445 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcOutputData.java +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcOutputData.java @@ -18,13 +18,16 @@ public final class BcmcOutputData implements OutputData { private final FieldState mCardNumberField; private final FieldState mExpiryDateField; + private final boolean mIsStoredPaymentMethodEnabled; BcmcOutputData( @NonNull FieldState cardNumberField, - @NonNull FieldState expiryDateField + @NonNull FieldState expiryDateField, + boolean isStoredPaymentMethodEnabled ) { mCardNumberField = cardNumberField; mExpiryDateField = expiryDateField; + mIsStoredPaymentMethodEnabled = isStoredPaymentMethodEnabled; } @NonNull @@ -37,6 +40,10 @@ public FieldState getExpiryDateField() { return mExpiryDateField; } + public boolean isStoredPaymentMethodEnabled() { + return mIsStoredPaymentMethodEnabled; + } + @Override public boolean isValid() { return mCardNumberField.getValidation().isValid() From 9d6ca998b4221cf4403d21c14648665aab5c5aed Mon Sep 17 00:00:00 2001 From: Caio Faustino Date: Tue, 14 Sep 2021 15:26:27 +0200 Subject: [PATCH 33/41] Fix logo image on BCMC component with same structure as CardView. --- bcmc/src/main/res/layout/bcmc_view.xml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/bcmc/src/main/res/layout/bcmc_view.xml b/bcmc/src/main/res/layout/bcmc_view.xml index 6a413f0904..e215a930bf 100644 --- a/bcmc/src/main/res/layout/bcmc_view.xml +++ b/bcmc/src/main/res/layout/bcmc_view.xml @@ -32,16 +32,21 @@ tools:ignore="RequiredSize" /> + - + + - Date: Tue, 14 Sep 2021 15:26:51 +0200 Subject: [PATCH 34/41] Add stored toggle to BCMC config on example app. --- bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcInputData.java | 2 +- .../java/com/adyen/checkout/example/ui/main/MainActivity.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcInputData.java b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcInputData.java index 71575564c0..9b5ff15eaa 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcInputData.java +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcInputData.java @@ -40,7 +40,7 @@ public boolean isStorePaymentSelected() { return mIsStorePaymentSelected; } - public void setStorePaymentSelected( boolean storePaymentSelected) { + public void setStorePaymentSelected(boolean storePaymentSelected) { mIsStorePaymentSelected = storePaymentSelected; } } 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 bbcb87c212..bc1ca247e3 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 @@ -157,6 +157,8 @@ class MainActivity : AppCompatActivity(), DropInCallback { .build() val bcmcConfiguration = BcmcConfiguration.Builder(shopperLocale, Environment.TEST, BuildConfig.CLIENT_KEY) + .setShopperReference(keyValueStorage.getShopperReference()) + .setShowStorePaymentField(true) .build() val adyen3DS2Configuration = Adyen3DS2Configuration.Builder(shopperLocale, Environment.TEST, BuildConfig.CLIENT_KEY) From f1285b97836b51ebcb82fe22b38e71eb575bbe32 Mon Sep 17 00:00:00 2001 From: jreij Date: Wed, 15 Sep 2021 09:30:52 +0200 Subject: [PATCH 35/41] Index paymentMethods instead of relying on their type when one is selected --- .../adyen/checkout/dropin/ui/DropInViewModel.kt | 5 ----- .../PaymentMethodListDialogFragment.kt | 4 ++-- .../ui/paymentmethods/PaymentMethodModel.kt | 1 + .../paymentmethods/PaymentMethodsListViewModel.kt | 15 ++++++++++----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/DropInViewModel.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/DropInViewModel.kt index 5ea5405a74..5a77f3c179 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/DropInViewModel.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/DropInViewModel.kt @@ -11,7 +11,6 @@ package com.adyen.checkout.dropin.ui import android.content.Intent import androidx.lifecycle.ViewModel import com.adyen.checkout.components.model.PaymentMethodsApiResponse -import com.adyen.checkout.components.model.paymentmethods.PaymentMethod import com.adyen.checkout.components.model.paymentmethods.StoredPaymentMethod import com.adyen.checkout.components.util.PaymentMethodTypes import com.adyen.checkout.dropin.DropInConfiguration @@ -31,8 +30,4 @@ class DropInViewModel( fun getStoredPaymentMethod(id: String): StoredPaymentMethod { return paymentMethodsApiResponse.storedPaymentMethods?.firstOrNull { it.id == id } ?: StoredPaymentMethod() } - - fun getPaymentMethod(type: String): PaymentMethod { - return paymentMethodsApiResponse.paymentMethods?.firstOrNull { it.type == type } ?: PaymentMethod() - } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodListDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodListDialogFragment.kt index 5593daf010..9aef103fb8 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodListDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodListDialogFragment.kt @@ -116,7 +116,7 @@ class PaymentMethodListDialogFragment : DropInBottomSheetDialogFragment(), Payme GooglePayComponent.PAYMENT_METHOD_TYPES.contains(paymentMethod.type) -> { Logger.d(TAG, "onPaymentMethodSelected: starting Google Pay") protocol.startGooglePay( - dropInViewModel.getPaymentMethod(paymentMethod.type), + paymentMethodsListViewModel.getPaymentMethod(paymentMethod), dropInViewModel.dropInConfiguration.getConfigurationForPaymentMethod(paymentMethod.type, requireContext()) ) } @@ -126,7 +126,7 @@ class PaymentMethodListDialogFragment : DropInBottomSheetDialogFragment(), Payme } PaymentMethodTypes.SUPPORTED_PAYMENT_METHODS.contains(paymentMethod.type) -> { Logger.d(TAG, "onPaymentMethodSelected: payment method is supported") - protocol.showComponentDialog(dropInViewModel.getPaymentMethod(paymentMethod.type)) + protocol.showComponentDialog(paymentMethodsListViewModel.getPaymentMethod(paymentMethod)) } else -> { Logger.d(TAG, "onPaymentMethodSelected: unidentified payment method, sending payment in case of redirect") diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodModel.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodModel.kt index e329acf30a..2064322820 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodModel.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodModel.kt @@ -9,6 +9,7 @@ package com.adyen.checkout.dropin.ui.paymentmethods data class PaymentMethodModel( + val index: Int, val type: String, val name: String, val icon: String, diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodsListViewModel.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodsListViewModel.kt index 7f506e9894..7d1b779f89 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodsListViewModel.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/ui/paymentmethods/PaymentMethodsListViewModel.kt @@ -26,7 +26,7 @@ import com.adyen.checkout.dropin.ui.stored.makeStoredModel class PaymentMethodsListViewModel( application: Application, - paymentMethods: List, + private val paymentMethods: List, storedPaymentMethods: List, val dropInConfiguration: DropInConfiguration ) : AndroidViewModel(application), ComponentAvailableCallback { @@ -47,6 +47,10 @@ class PaymentMethodsListViewModel( setupPaymentMethods(paymentMethods) } + fun getPaymentMethod(model: PaymentMethodModel): PaymentMethod { + return paymentMethods[model.index] + } + private fun setupStoredPaymentMethods(storedPaymentMethods: List) { storedPaymentMethodsList.clear() for (storedPaymentMethod in storedPaymentMethods) { @@ -78,7 +82,7 @@ class PaymentMethodsListViewModel( availabilityChecksum = paymentMethods.size paymentMethodsList.clear() - for (paymentMethod in paymentMethods) { + paymentMethods.forEachIndexed { index, paymentMethod -> val type = paymentMethod.type when { type == null -> { @@ -88,7 +92,7 @@ class PaymentMethodsListViewModel( Logger.v(TAG, "Supported payment method: $type") // We assume payment method is available and remove it later when the callback comes // this is the overwhelming majority of cases, and we keep the list ordered this way. - paymentMethodsList.add(paymentMethod.mapToModel()) + paymentMethodsList.add(paymentMethod.mapToModel(index)) checkPaymentMethodAvailability(getApplication(), paymentMethod, dropInConfiguration, this) } else -> { @@ -97,7 +101,7 @@ class PaymentMethodsListViewModel( Logger.e(TAG, "PaymentMethod not yet supported - $type") } else { Logger.d(TAG, "No details required - $type") - paymentMethodsList.add(paymentMethod.mapToModel()) + paymentMethodsList.add(paymentMethod.mapToModel(index)) } // If last payment method is redirect list might be ready now checkIfListIsReady() @@ -106,7 +110,7 @@ class PaymentMethodsListViewModel( } } - private fun PaymentMethod.mapToModel(): PaymentMethodModel { + private fun PaymentMethod.mapToModel(index: Int): PaymentMethodModel { val icon = when (type) { PaymentMethodTypes.SCHEME -> CARD_LOGO_TYPE PaymentMethodTypes.GOOGLE_PAY_LEGACY -> GOOGLE_PAY_LOGO_TYPE @@ -115,6 +119,7 @@ class PaymentMethodsListViewModel( } val drawIconBorder = icon != GOOGLE_PAY_LOGO_TYPE return PaymentMethodModel( + index = index, type = type.orEmpty(), name = name.orEmpty(), icon = icon.orEmpty(), From 29328a80e29c0f1353cf7cf6b45d16f0f895092a Mon Sep 17 00:00:00 2001 From: jreij Date: Mon, 20 Sep 2021 11:43:30 +0200 Subject: [PATCH 36/41] Add default BCMC configuration for drop-in --- .../java/com/adyen/checkout/dropin/ComponentParsingProvider.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/ComponentParsingProvider.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/ComponentParsingProvider.kt index e10b0f9ab5..9b55da4db6 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/ComponentParsingProvider.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/ComponentParsingProvider.kt @@ -99,6 +99,7 @@ internal fun getDefaultConfigForPaymentMethod( // get default builder for Configuration type val builder: BaseConfigurationBuilder = when (paymentMethod) { + PaymentMethodTypes.BCMC -> BcmcConfiguration.Builder(shopperLocale, environment, clientKey) PaymentMethodTypes.BLIK -> BlikConfiguration.Builder(shopperLocale, environment, clientKey) PaymentMethodTypes.DOTPAY -> DotpayConfiguration.Builder(shopperLocale, environment, clientKey) PaymentMethodTypes.ENTERCASH -> EntercashConfiguration.Builder(shopperLocale, environment, clientKey) From bc77f4f437f6637b6d67f50369fd3b034629c1e5 Mon Sep 17 00:00:00 2001 From: jreij Date: Mon, 20 Sep 2021 11:50:23 +0200 Subject: [PATCH 37/41] Fix visual bug in BCMC component card icon --- bcmc/src/main/res/layout/bcmc_view.xml | 29 ++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/bcmc/src/main/res/layout/bcmc_view.xml b/bcmc/src/main/res/layout/bcmc_view.xml index e215a930bf..5f2d28d777 100644 --- a/bcmc/src/main/res/layout/bcmc_view.xml +++ b/bcmc/src/main/res/layout/bcmc_view.xml @@ -32,21 +32,28 @@ tools:ignore="RequiredSize" /> - + android:layout_height="wrap_content" + android:orientation="horizontal"> - - + + + + Date: Tue, 21 Sep 2021 14:26:21 +0200 Subject: [PATCH 38/41] Change version name to 4.2.0 --- README.md | 4 ++-- build.gradle | 2 +- example-app/build.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3282733bda..bf4b2faf32 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,11 @@ If you are upgrading from 3.x.x to a current release, check out our [migration g Import the Component module for the Payment Method you want to use by adding it to your `build.gradle` file. For example, for the Drop-in solution you should add: ```groovy -implementation "com.adyen.checkout:drop-in:4.1.1" +implementation "com.adyen.checkout:drop-in:4.2.0" ``` For a Credit Card component you should add: ```groovy -implementation "com.adyen.checkout:card:4.1.1" +implementation "com.adyen.checkout:card:4.2.0" ``` ### Client Key diff --git a/build.gradle b/build.gradle index d8417533af..65298cc567 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ allprojects { // just for example app, don't need to increment ext.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). - ext.version_name = "4.1.1" + ext.version_name = "4.2.0" // Code quality ext.ktlint_version = '0.40.0' diff --git a/example-app/build.gradle b/example-app/build.gradle index 67f4cebed1..30a68a6405 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -71,7 +71,7 @@ android { dependencies { // Checkout implementation project(':drop-in') -// implementation "com.adyen.checkout:drop-in:4.1.1" +// implementation "com.adyen.checkout:drop-in:4.2.0" // Dependencies implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_version" From c5bd64490a5062aef8c18f91e9df201c610550e0 Mon Sep 17 00:00:00 2001 From: jreij Date: Tue, 21 Sep 2021 14:32:17 +0200 Subject: [PATCH 39/41] Remove proguard rules from README.md --- README.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/README.md b/README.md index bf4b2faf32..780a4ff1e0 100644 --- a/README.md +++ b/README.md @@ -183,21 +183,9 @@ cardComponent.observe(this) { paymentComponentState -> ## ProGuard -If you use ProGuard or R8, the following rules should be enough to maintain all expected functionality. +If you use ProGuard or R8, you do not need to manually add any rules, as they are automatically embedded in the artifacts. Please let us know if you find any issues. -``` --keep class com.adyen.checkout.core.model.** { * ;} --keep class com.adyen.checkout.components.model.** { *; } --keep class com.adyen.threeds2.** { *; } --keepclassmembers public class * implements com.adyen.checkout.components.PaymentComponent { - public (...); -} --keepclassmembers public class * implements com.adyen.checkout.components.ActionComponent { - public (...); -} -``` - ## See also * [Android Documentation][docs.android] From bfe15a1f59757c3713b012f69f6df81ad08aea4b Mon Sep 17 00:00:00 2001 From: jreij Date: Tue, 21 Sep 2021 14:51:38 +0200 Subject: [PATCH 40/41] Update release notes --- RELEASE_NOTES.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 22ada953ad..4158669bd6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,5 +8,15 @@ [//]: # ( # Deprecated) [//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object) +## Added +- Dual-branded card flow: shoppers can now select their card's brand when two brands are detected. +- Ability to store BCMC cards. +- Ability to stop observing components. +- 3DS2 SDK version in `CardComponent`'s output. +- Read list of supported card networks for Google Pay from the payment methods API response. + ## Fixed -- Address Visibility in Card Component configuration not being reflected on UI. \ No newline at end of file +- Crash caused by having stored payment methods none of which is Ecommerce. +- Google Pay Component will not include `TotalPrice` in its output, if `TotalPriceStatus` is set to `NOT_CURRENTLY_KNOWN`. +- Issue in Drop-in when multiple payment methods have the same type. +- Missing default BCMC configuration in Drop-in. From d43970d7ef33b6739db2d8f034f0d0bd98247a32 Mon Sep 17 00:00:00 2001 From: jreij Date: Tue, 21 Sep 2021 14:56:14 +0200 Subject: [PATCH 41/41] Update release notes --- RELEASE_NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4158669bd6..3b7c52f9f5 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -11,6 +11,7 @@ ## Added - Dual-branded card flow: shoppers can now select their card's brand when two brands are detected. - Ability to store BCMC cards. +- Support for new Activity Result API. - Ability to stop observing components. - 3DS2 SDK version in `CardComponent`'s output. - Read list of supported card networks for Google Pay from the payment methods API response.