Skip to content

Commit

Permalink
Merge pull request #427 from Adyen/develop
Browse files Browse the repository at this point in the history
Release 4.0.0
  • Loading branch information
jreij authored Jul 8, 2021
2 parents 754f49e + 1428c9d commit eba9c22
Show file tree
Hide file tree
Showing 91 changed files with 1,485 additions and 809 deletions.
1 change: 1 addition & 0 deletions 3ds2/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {

// Checkout
api project(':components-core')
api project(':redirect')

// Dependencies
api "com.adyen.threeds:adyen-3ds2:$adyen3ds2_version"
Expand Down
162 changes: 104 additions & 58 deletions 3ds2/src/main/java/com/adyen/checkout/adyen3ds2/Adyen3DS2Component.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ package com.adyen.checkout.adyen3ds2
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import com.adyen.checkout.adyen3ds2.exception.Authentication3DS2Exception
import com.adyen.checkout.adyen3ds2.exception.Cancelled3DS2Exception
import com.adyen.checkout.adyen3ds2.model.ChallengeResult
import com.adyen.checkout.adyen3ds2.model.ChallengeToken
import com.adyen.checkout.adyen3ds2.model.FingerprintToken
import com.adyen.checkout.adyen3ds2.repository.SubmitFingerprintRepository
import com.adyen.checkout.adyen3ds2.repository.SubmitFingerprintResult
import com.adyen.checkout.components.ActionComponentData
import com.adyen.checkout.components.ActionComponentProvider
import com.adyen.checkout.components.base.ActionComponentProviderImpl
import com.adyen.checkout.components.base.BaseActionComponent
import com.adyen.checkout.components.base.IntentHandlingComponent
import com.adyen.checkout.components.encoding.Base64Encoder
import com.adyen.checkout.components.model.payments.response.Action
import com.adyen.checkout.components.model.payments.response.Threeds2Action
Expand All @@ -32,6 +35,7 @@ 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
import com.adyen.checkout.redirect.RedirectDelegate
import com.adyen.threeds2.AuthenticationRequestParameters
import com.adyen.threeds2.ChallengeStatusReceiver
import com.adyen.threeds2.CompletionEvent
Expand All @@ -51,14 +55,19 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONException
import org.json.JSONObject
import java.util.Collections

@Suppress("TooManyFunctions")
class Adyen3DS2Component(application: Application, configuration: Adyen3DS2Configuration) :
BaseActionComponent<Adyen3DS2Configuration>(application, configuration), ChallengeStatusReceiver {
class Adyen3DS2Component(
application: Application,
configuration: Adyen3DS2Configuration,
private val submitFingerprintRepository: SubmitFingerprintRepository,
private val adyen3DS2Serializer: Adyen3DS2Serializer,
private val redirectDelegate: RedirectDelegate
) : BaseActionComponent<Adyen3DS2Configuration>(application, configuration), ChallengeStatusReceiver, IntentHandlingComponent {

private var mTransaction: Transaction? = null
private var mUiCustomization: UiCustomization? = null
private var authorizationToken: String? = null

override fun onCleared() {
super.onCleared()
Expand Down Expand Up @@ -88,56 +97,73 @@ class Adyen3DS2Component(application: Application, configuration: Adyen3DS2Confi
mUiCustomization = uiCustomization
}

override fun getSupportedActionTypes(): List<String> {
return Collections.unmodifiableList(
listOf(Threeds2FingerprintAction.ACTION_TYPE, Threeds2ChallengeAction.ACTION_TYPE, Threeds2Action.ACTION_TYPE)
)
override fun canHandleAction(action: Action): Boolean {
return PROVIDER.canHandleAction(action)
}

override fun getSupportedPaymentMethodTypes(): List<String>? = null
override fun saveState(bundle: Bundle?) {
if (bundle != null && authorizationToken != null) {
if (bundle.containsKey(AUTHORIZATION_TOKEN_KEY)) {
Logger.d(TAG, "bundle already has authorizationToken, overriding")
}
bundle.putString(AUTHORIZATION_TOKEN_KEY, authorizationToken)
}
super.saveState(bundle)
}

override fun restoreState(bundle: Bundle?) {
if (bundle != null && bundle.containsKey(AUTHORIZATION_TOKEN_KEY) && authorizationToken == null) {
authorizationToken = bundle.getString(AUTHORIZATION_TOKEN_KEY)
}
super.restoreState(bundle)
}

@Throws(ComponentException::class)
override fun handleActionInternal(activity: Activity, action: Action) {
when (action.type) {
Threeds2FingerprintAction.ACTION_TYPE -> {
val fingerprintAction = action as Threeds2FingerprintAction
if (fingerprintAction.token.isNullOrEmpty()) {
when (action) {
is Threeds2FingerprintAction -> {
if (action.token.isNullOrEmpty()) {
throw ComponentException("Fingerprint token not found.")
}
identifyShopper(activity, fingerprintAction.token.orEmpty())
identifyShopper(activity, action.token.orEmpty(), submitFingerprintAutomatically = false)
}
Threeds2ChallengeAction.ACTION_TYPE -> {
val challengeAction = action as Threeds2ChallengeAction
if (challengeAction.token.isNullOrEmpty()) {
is Threeds2ChallengeAction -> {
if (action.token.isNullOrEmpty()) {
throw ComponentException("Challenge token not found.")
}
challengeShopper(activity, challengeAction.token.orEmpty())
challengeShopper(activity, action.token.orEmpty())
}
Threeds2Action.ACTION_TYPE -> {
val threeds2Action = action as Threeds2Action
if (threeds2Action.token.isNullOrEmpty()) {
is Threeds2Action -> {
if (action.token.isNullOrEmpty()) {
throw ComponentException("3DS2 token not found.")
}
if (threeds2Action.subtype == null) {
if (action.subtype == null) {
throw ComponentException("3DS2 Action subtype not found.")
}
val subtype = SubType.parse(threeds2Action.subtype.orEmpty())
handleActionSubtype(activity, subtype, threeds2Action.token.orEmpty())
val subtype = SubType.parse(action.subtype.orEmpty())
// We need to keep authorizationToken in memory to access it later when the 3DS2 challenge is done
authorizationToken = action.authorisationToken
handleActionSubtype(activity, subtype, action.token.orEmpty())
}
}
}

private fun handleActionSubtype(activity: Activity, subtype: SubType, token: String) {
when (subtype) {
SubType.FINGERPRINT -> identifyShopper(activity, token)
SubType.FINGERPRINT -> identifyShopper(activity, token, submitFingerprintAutomatically = true)
SubType.CHALLENGE -> challengeShopper(activity, token)
}
}

override fun completed(completionEvent: CompletionEvent) {
Logger.d(TAG, "challenge completed")
try {
notifyDetails(createChallengeDetails(completionEvent))
// Check whether authorizationToken was set and create the corresponding details object
val token = authorizationToken
val details =
if (token == null) adyen3DS2Serializer.createChallengeDetails(completionEvent)
else adyen3DS2Serializer.createThreeDsResultDetails(completionEvent, token)
notifyDetails(details)
} catch (e: CheckoutException) {
notifyException(e)
} finally {
Expand Down Expand Up @@ -172,8 +198,8 @@ class Adyen3DS2Component(application: Application, configuration: Adyen3DS2Confi
}

@Throws(ComponentException::class)
private fun identifyShopper(context: Context, encodedFingerprintToken: String) {
Logger.d(TAG, "identifyShopper")
private fun identifyShopper(activity: Activity, encodedFingerprintToken: String, submitFingerprintAutomatically: Boolean) {
Logger.d(TAG, "identifyShopper - submitFingerprintAutomatically: $submitFingerprintAutomatically")
val decodedFingerprintToken = Base64Encoder.decode(encodedFingerprintToken)

val fingerprintJson: JSONObject = try {
Expand All @@ -195,7 +221,7 @@ class Adyen3DS2Component(application: Application, configuration: Adyen3DS2Confi
viewModelScope.launch(Dispatchers.Default + coroutineExceptionHandler) {
try {
Logger.d(TAG, "initialize 3DS2 SDK")
ThreeDS2Service.INSTANCE.initialize(context, configParameters, null, mUiCustomization)
ThreeDS2Service.INSTANCE.initialize(activity, configParameters, null, mUiCustomization)
} catch (e: SDKRuntimeException) {
notifyException(ComponentException("Failed to initialize 3DS2 SDK", e))
return@launch
Expand All @@ -216,15 +242,45 @@ class Adyen3DS2Component(application: Application, configuration: Adyen3DS2Confi
}

val authenticationRequestParameters = mTransaction?.authenticationRequestParameters
if (authenticationRequestParameters != null) {
val encodedFingerprint = createEncodedFingerprint(authenticationRequestParameters)
if (authenticationRequestParameters == null) {
notifyException(ComponentException("Failed to retrieve 3DS2 authentication parameters"))
return@launch
}
val encodedFingerprint = createEncodedFingerprint(authenticationRequestParameters)
if (submitFingerprintAutomatically) {
submitFingerprintAutomatically(activity, encodedFingerprint)
} else {
launch(Dispatchers.Main) {
notifyDetails(createFingerprintDetails(encodedFingerprint))
notifyDetails(adyen3DS2Serializer.createFingerprintDetails(encodedFingerprint))
}
}
}
}

private suspend fun submitFingerprintAutomatically(activity: Activity, encodedFingerprint: String) {
try {
val result = submitFingerprintRepository.submitFingerprint(encodedFingerprint, configuration, paymentData)
// This flow (calling the internal submitFingerprint endpoint) requires that we do not send paymentData back to the merchant.
// Setting it to null ensures that when the flow ends and notifyDetails is called, paymentData will not be included in the response.
paymentData = null
when (result) {
is SubmitFingerprintResult.Completed -> {
viewModelScope.launch(Dispatchers.Main) {
notifyDetails(result.details)
}
}
is SubmitFingerprintResult.Redirect -> {
redirectDelegate.makeRedirect(activity, result.action)
}
is SubmitFingerprintResult.Threeds2 -> {
handleAction(activity, result.action)
}
}
} catch (e: ComponentException) {
notifyException(e)
}
}

@Throws(ComponentException::class)
private fun challengeShopper(activity: Activity, encodedChallengeToken: String) {
Logger.d(TAG, "challengeShopper")
Expand Down Expand Up @@ -288,39 +344,29 @@ class Adyen3DS2Component(application: Application, configuration: Adyen3DS2Confi
}
}

@Throws(ComponentException::class)
fun createFingerprintDetails(encodedFingerprint: String?): JSONObject {
val fingerprintDetails = JSONObject()
try {
fingerprintDetails.put(FINGERPRINT_DETAILS_KEY, encodedFingerprint)
} catch (e: JSONException) {
throw ComponentException("Failed to create fingerprint details", e)
}
return fingerprintDetails
}

@Throws(ComponentException::class)
private fun createChallengeDetails(completionEvent: CompletionEvent): JSONObject {
val challengeDetails = JSONObject()
/**
* Call this method when receiving the return URL from the 3DS redirect with the result data.
* This result will be in the [Intent.getData] and begins with the returnUrl you specified on the payments/ call.
*
* @param intent The received [Intent].
*/
override fun handleIntent(intent: Intent) {
try {
val challengeResult = ChallengeResult.from(completionEvent)
challengeDetails.put(CHALLENGE_DETAILS_KEY, challengeResult.payload)
} catch (e: JSONException) {
throw ComponentException("Failed to create challenge details", e)
val parsedResult = redirectDelegate.handleRedirectResponse(intent.data)
notifyDetails(parsedResult)
} catch (e: CheckoutException) {
notifyException(e)
}
return challengeDetails
}

companion object {
val TAG = LogUtil.getTag()
private val TAG = LogUtil.getTag()

private const val AUTHORIZATION_TOKEN_KEY = "authorization_token"

@JvmField
val PROVIDER: ActionComponentProvider<Adyen3DS2Component, Adyen3DS2Configuration> = ActionComponentProviderImpl(
Adyen3DS2Component::class.java, Adyen3DS2Configuration::class.java
)
val PROVIDER: ActionComponentProvider<Adyen3DS2Component, Adyen3DS2Configuration> = Adyen3DS2ComponentProvider()

private const val FINGERPRINT_DETAILS_KEY = "threeds2.fingerprint"
private const val CHALLENGE_DETAILS_KEY = "threeds2.challengeResult"
private const val DEFAULT_CHALLENGE_TIME_OUT = 10
private const val PROTOCOL_VERSION_2_1_0 = "2.1.0"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 21/5/2021.
*/

package com.adyen.checkout.adyen3ds2

import android.app.Application
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import com.adyen.checkout.adyen3ds2.repository.SubmitFingerprintRepository
import com.adyen.checkout.components.ActionComponentProvider
import com.adyen.checkout.components.base.lifecycle.viewModelFactory
import com.adyen.checkout.components.model.payments.response.Action
import com.adyen.checkout.components.model.payments.response.Threeds2Action
import com.adyen.checkout.components.model.payments.response.Threeds2ChallengeAction
import com.adyen.checkout.components.model.payments.response.Threeds2FingerprintAction
import com.adyen.checkout.redirect.RedirectDelegate

class Adyen3DS2ComponentProvider : ActionComponentProvider<Adyen3DS2Component, Adyen3DS2Configuration> {
override fun get(
viewModelStoreOwner: ViewModelStoreOwner,
application: Application,
configuration: Adyen3DS2Configuration
): Adyen3DS2Component {
val submitFingerprintRepository = SubmitFingerprintRepository()
val adyen3DS2DetailsParser = Adyen3DS2Serializer()
val redirectDelegate = RedirectDelegate()
val threeDS2Factory = viewModelFactory {
Adyen3DS2Component(
application,
configuration,
submitFingerprintRepository,
adyen3DS2DetailsParser,
redirectDelegate
)
}
return ViewModelProvider(viewModelStoreOwner, threeDS2Factory).get(Adyen3DS2Component::class.java)
}

override fun requiresConfiguration(): Boolean = false

override fun requiresView(action: Action): Boolean = false

override fun getSupportedActionTypes(): List<String> {
return listOf(Threeds2FingerprintAction.ACTION_TYPE, Threeds2ChallengeAction.ACTION_TYPE, Threeds2Action.ACTION_TYPE)
}

override fun canHandleAction(action: Action): Boolean {
return supportedActionTypes.contains(action.type)
}
}
Loading

0 comments on commit eba9c22

Please sign in to comment.