Skip to content

Commit

Permalink
Merge pull request #365 from Adyen/develop
Browse files Browse the repository at this point in the history
Release 4.0.0-beta02
  • Loading branch information
caiofaustino authored Mar 24, 2021
2 parents e6981df + 0f12c4b commit ada45bd
Show file tree
Hide file tree
Showing 38 changed files with 594 additions and 157 deletions.
39 changes: 5 additions & 34 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

[//]: # (This file will be used for the release notes on GitHub when publishing.)
[//]: # (Types of changes: `Added` `Changed` `Deprecated` `Removed` `Fixed` `Security`)
[//]: # (Example:)
Expand All @@ -9,42 +8,14 @@
[//]: # ( ### Deprecated)
[//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object)

## WARNINGS
- This version has changes in dependency names and package names. Make sure you are using the correct dependency (for components) and re-import classes that were in the `base` package.
- This version now targets Checkout API version [v67](https://docs.adyen.com/online-payments/release-notes#checkout-api-v67).

## Added
- `CardComponent` and `BcmcComponent` will now fetch the public key for card encryption using the `clientKey`
- `CardComponent` will now use Bin Lookup endpoint to more reliably verify the card brand.
- `CardComponent` will now return the Bin and last 4 digits of the card number in the `CardComponentState`
- `DropInService` now has `onPaymentsCallRequested` and `onDetailsCallRequested` that can be used to intercept a request.
- This allows access to the whole `PaymentComponentState` and to handle asynchronous handling of the API call if necessary.
- `DropIn` result can now be fully handled in `onActivityResult`.
- New code [Documentation page](https://adyen.github.io/adyen-android/)

## Changed
Features:
- MBWay Component removed email field and updated UI for phone number input.

Code:
- Refactored module structure and artifact IDs. Now each payment method has only 1 module with a simplified name.
- Renamed package `com.adyen.checkout.base` to `com.adyen.checkout.components`
- Renamed package `com.adyen.checkout.base.component` to `com.adyen.checkout.components.base`
- Refactored `CallResult` to `DropInServiceResult` as sealed classes.
- CSE module now has all the encryption code and has a simplified API.
- `CardEncrypter.encryptFields(unencryptedCardBuilder.build(), publicKey)`
- `DropInService` is now a regular bound `Service` instead of a `JobIntentService`
- `ResultHandlerIntent` is now an optional parameter in `startPayment` instead of `DropInConfiguration`
- `clientKey` is now a required parameter for all `Configuration` objects
- `publicKey` cannot be passed directly to the `Configuration` of Card and BCMC anymore, use `clientKey` instead.
- A bunch of internal refactorings and moving classes to Kotlin :rocket:

## Removed
- `SimplifiedDropInService` to not encourage bad practice of passing whole API response to the App.
- AfterPay Component
- Validation of PublicKey in CSE module if it's used standalone.
- 'isReady' flag to ComponentState because some components might require some initialization time even if all the inputs are valid.
- 'isValid' now checks both 'isInputValid' and 'isReady' to be true.

## Fixed
- Issue where card logo and validation logos would overlap.
- Handle Intent results if DropInActivity got destroyed.
- Queue API request if DropInService is not yet bound to DropInActivity



Expand Down
23 changes: 15 additions & 8 deletions bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.adyen.checkout.components.model.payments.request.CardPaymentMethod
import com.adyen.checkout.components.model.payments.request.PaymentComponentData
import com.adyen.checkout.components.ui.FieldState
import com.adyen.checkout.components.util.PaymentMethodTypes
import com.adyen.checkout.core.exception.CheckoutException
import com.adyen.checkout.core.exception.ComponentException
import com.adyen.checkout.core.log.LogUtil
import com.adyen.checkout.core.log.Logger
Expand Down Expand Up @@ -53,13 +54,15 @@ class BcmcComponent(
val SUPPORTED_CARD_TYPE = CardType.BCMC
}

private var publicKey = ""
private var publicKey: String? = null

init {
viewModelScope.launch {
publicKey = fetchPublicKey()
if (publicKey.isEmpty()) {
notifyException(ComponentException("Unable to fetch publicKey."))
try {
publicKey = fetchPublicKey()
notifyStateChanged()
} catch (e: CheckoutException) {
notifyException(ComponentException("Unable to fetch publicKey.", e))
}
}
}
Expand Down Expand Up @@ -91,9 +94,13 @@ class BcmcComponent(
val outputData = outputData
val paymentComponentData = PaymentComponentData<CardPaymentMethod>()

val publicKey = publicKey

// If data is not valid we just return empty object, encryption would fail and we don't pass unencrypted data.
if (outputData == null || !outputData.isValid) {
return GenericComponentState(paymentComponentData, false)
if (outputData?.isValid != true || publicKey == null) {
val isInputValid = outputData?.isValid ?: false
val isReady = publicKey != null
return GenericComponentState(paymentComponentData, isInputValid, isReady)
}
val encryptedCard = try {
unencryptedCardBuilder.setNumber(outputData.cardNumberField.value)
Expand All @@ -105,7 +112,7 @@ class BcmcComponent(
CardEncrypter.encryptFields(unencryptedCardBuilder.build(), publicKey)
} catch (e: EncryptionException) {
notifyException(e)
return GenericComponentState(paymentComponentData, false)
return GenericComponentState(paymentComponentData, false, true)
}

// BCMC payment method is scheme type.
Expand All @@ -116,7 +123,7 @@ class BcmcComponent(
encryptedExpiryYear = encryptedCard.encryptedExpiryYear
}
paymentComponentData.paymentMethod = cardPaymentMethod
return GenericComponentState(paymentComponentData, outputData.isValid)
return GenericComponentState(paymentComponentData, true, true)
}

fun isCardNumberSupported(cardNumber: String?): Boolean {
Expand Down
4 changes: 2 additions & 2 deletions blik/src/main/java/com/adyen/checkout/blik/BlikComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ protected GenericComponentState<BlikPaymentMethod> createComponentState() {

paymentComponentData.setPaymentMethod(paymentMethod);

final boolean isValid = mPaymentMethodDelegate instanceof GenericStoredPaymentDelegate
final boolean isInputValid = mPaymentMethodDelegate instanceof GenericStoredPaymentDelegate
|| blikOutputData != null
&& blikOutputData.isValid();

return new GenericComponentState<>(paymentComponentData, isValid);
return new GenericComponentState<>(paymentComponentData, isInputValid, true);
}

@NonNull
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,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.0.0-beta01"
ext.version_name = "4.0.0-beta02"

// Code quality
ext.ktlint_version = '0.40.0'
Expand Down
84 changes: 61 additions & 23 deletions card/src/main/java/com/adyen/checkout/card/CardComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package com.adyen.checkout.card

import androidx.lifecycle.viewModelScope
import com.adyen.checkout.card.data.CardType
import com.adyen.checkout.card.data.ExpiryDate
import com.adyen.checkout.components.StoredPaymentComponentProvider
import com.adyen.checkout.components.base.BasePaymentComponent
Expand Down Expand Up @@ -40,13 +41,15 @@ class CardComponent private constructor(
) : BasePaymentComponent<CardConfiguration, CardInputData, CardOutputData, CardComponentState>(cardDelegate, cardConfiguration) {

private var storedPaymentInputData: CardInputData? = null
private var publicKey = ""
private var publicKey: String? = null

init {
viewModelScope.launch {
publicKey = cardDelegate.fetchPublicKey()
if (publicKey.isEmpty()) {
notifyException(ComponentException("Unable to fetch publicKey."))
try {
publicKey = cardDelegate.fetchPublicKey()
notifyStateChanged()
} catch (e: CheckoutException) {
notifyException(ComponentException("Unable to fetch publicKey.", e))
}
}

Expand Down Expand Up @@ -121,24 +124,28 @@ class CardComponent private constructor(
// TODO: 29/01/2021 pass outputData as non null parameter
val stateOutputData = outputData ?: throw CheckoutException("Cannot create state with null outputData")

val cardPaymentMethod = CardPaymentMethod()
cardPaymentMethod.type = CardPaymentMethod.PAYMENT_METHOD_TYPE

val unencryptedCardBuilder = UnencryptedCard.Builder()

val paymentComponentData = PaymentComponentData<CardPaymentMethod>()

val cardNumber = stateOutputData.cardNumberState.value

val firstCardType = stateOutputData.detectedCardTypes.firstOrNull()?.cardType

val binValue: String = getBinValueFromCardNumber(cardNumber)
val binValue = cardNumber.take(BIN_VALUE_LENGTH)

val publicKey = publicKey

// If data is not valid we just return empty object, encryption would fail and we don't pass unencrypted data.
if (!stateOutputData.isValid) {
return CardComponentState(paymentComponentData, false, firstCardType, binValue, null)
if (!stateOutputData.isValid || publicKey == null) {
return CardComponentState(
paymentComponentData = PaymentComponentData<CardPaymentMethod>(),
isInputValid = stateOutputData.isValid,
isReady = publicKey != null,
cardType = firstCardType,
binValue = binValue,
lastFourDigits = null
)
}

val unencryptedCardBuilder = UnencryptedCard.Builder()

val encryptedCard: EncryptedCard = try {
if (!isStoredPaymentMethod()) {
unencryptedCardBuilder.setNumber(stateOutputData.cardNumberState.value)
Expand All @@ -155,9 +162,35 @@ class CardComponent private constructor(
CardEncrypter.encryptFields(unencryptedCardBuilder.build(), publicKey)
} catch (e: EncryptionException) {
notifyException(e)
return CardComponentState(paymentComponentData, false, firstCardType, binValue, null)
return CardComponentState(
paymentComponentData = PaymentComponentData<CardPaymentMethod>(),
isInputValid = false,
isReady = true,
cardType = firstCardType,
binValue = binValue,
lastFourDigits = null
)
}

return mapComponentState(
encryptedCard,
stateOutputData,
cardNumber,
firstCardType,
binValue
)
}

private fun mapComponentState(
encryptedCard: EncryptedCard,
stateOutputData: CardOutputData,
cardNumber: String,
firstCardType: CardType?,
binValue: String
): CardComponentState {
val cardPaymentMethod = CardPaymentMethod()
cardPaymentMethod.type = CardPaymentMethod.PAYMENT_METHOD_TYPE

if (!isStoredPaymentMethod()) {
cardPaymentMethod.encryptedCardNumber = encryptedCard.encryptedCardNumber
cardPaymentMethod.encryptedExpiryMonth = encryptedCard.encryptedExpiryMonth
Expand All @@ -174,13 +207,22 @@ class CardComponent private constructor(
cardPaymentMethod.holderName = stateOutputData.holderNameState.value
}

paymentComponentData.paymentMethod = cardPaymentMethod
paymentComponentData.setStorePaymentMethod(stateOutputData.isStoredPaymentMethodEnable)
paymentComponentData.shopperReference = configuration.shopperReference
val paymentComponentData = PaymentComponentData<CardPaymentMethod>().apply {
paymentMethod = cardPaymentMethod
setStorePaymentMethod(stateOutputData.isStoredPaymentMethodEnable)
shopperReference = configuration.shopperReference
}

val lastFour = cardNumber.takeLast(LAST_FOUR_LENGTH)

return CardComponentState(paymentComponentData, stateOutputData.isValid, firstCardType, binValue, lastFour)
return CardComponentState(
paymentComponentData = paymentComponentData,
isInputValid = true,
isReady = true,
cardType = firstCardType,
binValue = binValue,
lastFourDigits = lastFour
)
}

fun isStoredPaymentMethod(): Boolean {
Expand All @@ -199,10 +241,6 @@ class CardComponent private constructor(
return configuration.isShowStorePaymentFieldEnable
}

private fun getBinValueFromCardNumber(cardNumber: String): String {
return if (cardNumber.length < BIN_VALUE_LENGTH) cardNumber else cardNumber.substring(0..BIN_VALUE_LENGTH)
}

companion object {
@JvmStatic
val PROVIDER: StoredPaymentComponentProvider<CardComponent, CardConfiguration> = CardComponentProvider()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import com.adyen.checkout.components.model.payments.request.PaymentComponentData
*/
class CardComponentState(
paymentComponentData: PaymentComponentData<CardPaymentMethod>,
isValid: Boolean,
isInputValid: Boolean,
isReady: Boolean,
val cardType: CardType?,
val binValue: String,
val lastFourDigits: String?
) : PaymentComponentState<CardPaymentMethod>(paymentComponentData, isValid)
) : PaymentComponentState<CardPaymentMethod>(paymentComponentData, isInputValid, isReady)
2 changes: 1 addition & 1 deletion card/src/main/java/com/adyen/checkout/card/CardDelegate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ abstract class CardDelegate(
abstract fun isCvcHidden(): Boolean
abstract fun requiresInput(): Boolean
abstract fun isHolderNameRequired(): Boolean
abstract fun detectCardType(cardNumber: String, publicKey: String, coroutineScope: CoroutineScope): List<DetectedCardType>
abstract fun detectCardType(cardNumber: String, publicKey: String?, coroutineScope: CoroutineScope): List<DetectedCardType>

suspend fun fetchPublicKey(): String {
return publicKeyRepository.fetchPublicKey(
Expand Down
4 changes: 2 additions & 2 deletions card/src/main/java/com/adyen/checkout/card/NewCardDelegate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class NewCardDelegate(

override fun detectCardType(
cardNumber: String,
publicKey: String,
publicKey: String?,
coroutineScope: CoroutineScope
): List<DetectedCardType> {
Logger.d(TAG, "detectCardType")
Expand All @@ -91,7 +91,7 @@ class NewCardDelegate(
}

// if length is exactly the size, we call bin lookup API
if (cardNumber.length == BinLookupConnection.REQUIRED_BIN_SIZE) {
if (cardNumber.length == BinLookupConnection.REQUIRED_BIN_SIZE && publicKey != null) {
Logger.d(TAG, "Launching Bin Lookup")
coroutineScope.launch {
val detectedCardTypes = binLookupRepository.fetch(cardNumber, publicKey, cardConfiguration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class StoredCardDelegate(

override fun detectCardType(
cardNumber: String,
publicKey: String,
publicKey: String?,
coroutineScope: CoroutineScope
): List<DetectedCardType> {
return storedDetectedCardTypes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package com.adyen.checkout.card.repository
import com.adyen.checkout.card.api.PublicKeyConnection
import com.adyen.checkout.components.api.suspendedCall
import com.adyen.checkout.core.api.Environment
import com.adyen.checkout.core.exception.CheckoutException
import com.adyen.checkout.core.log.LogUtil
import com.adyen.checkout.core.log.Logger
import org.json.JSONException
Expand All @@ -31,6 +32,6 @@ class PublicKeyRepository {
Logger.e(TAG, "PublicKeyConnection unexpected result", e)
}
}
return ""
throw CheckoutException("Unable to fetch public key")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public class GenericComponentState<PaymentMethodDetailsT extends PaymentMethodDe
*/
public GenericComponentState(
@NonNull PaymentComponentData<PaymentMethodDetailsT> paymentComponentData,
boolean isValid) {
super(paymentComponentData, isValid);
boolean isInputValid,
boolean isReady
) {
super(paymentComponentData, isInputValid, isReady);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
public abstract class PaymentComponentState<PaymentMethodDetailsT extends PaymentMethodDetails> {

private final PaymentComponentData<PaymentMethodDetailsT> mPaymentComponentData;
private final boolean mIsValid;
private final boolean mIsInputValid;
private final boolean mIsReady;

public PaymentComponentState(@NonNull PaymentComponentData<PaymentMethodDetailsT> paymentComponentData, boolean isValid) {
public PaymentComponentState(@NonNull PaymentComponentData<PaymentMethodDetailsT> paymentComponentData, boolean isInputValid, boolean isReady) {
mPaymentComponentData = paymentComponentData;
mIsValid = isValid;
mIsInputValid = isInputValid;
mIsReady = isReady;
}

/**
Expand All @@ -39,6 +41,20 @@ public PaymentComponentData<PaymentMethodDetailsT> getData() {
* @return If the collected data is valid to be sent to the backend.
*/
public boolean isValid() {
return mIsValid;
return mIsInputValid && mIsReady;
}

/**
* @return If the component UI data is valid.
*/
public boolean isInputValid() {
return mIsInputValid;
}

/**
* @return If the component initialisation is done and data can be sent to the backend when valid.
*/
public boolean isReady() {
return mIsReady;
}
}
Loading

0 comments on commit ada45bd

Please sign in to comment.