Skip to content

Commit

Permalink
Merge pull request #1585 from Adyen/update-3ds2-sdk
Browse files Browse the repository at this point in the history
Update 3ds2 sdk
  • Loading branch information
OscarSpruit committed Apr 29, 2024
2 parents 90a4180 + dddc165 commit 6aef6f7
Show file tree
Hide file tree
Showing 13 changed files with 429 additions and 353 deletions.
Expand Up @@ -13,11 +13,6 @@ import org.json.JSONException
import org.json.JSONObject

internal class Adyen3DS2Serializer {
companion object {
private const val FINGERPRINT_DETAILS_KEY = "threeds2.fingerprint"
private const val CHALLENGE_DETAILS_KEY = "threeds2.challengeResult"
private const val THREEDS_RESULT_KEY = "threeDSResult"
}

@Throws(ComponentException::class)
fun createFingerprintDetails(encodedFingerprint: String): JSONObject {
Expand Down Expand Up @@ -57,4 +52,10 @@ internal class Adyen3DS2Serializer {
}
return threeDsDetails
}

companion object {
private const val FINGERPRINT_DETAILS_KEY = "threeds2.fingerprint"
private const val CHALLENGE_DETAILS_KEY = "threeds2.challengeResult"
private const val THREEDS_RESULT_KEY = "threeDSResult"
}
}
Expand Up @@ -7,9 +7,10 @@
*/
package com.adyen.checkout.adyen3ds2.internal.data.model

import com.adyen.checkout.components.core.internal.util.AndroidBase64Encoder
import org.json.JSONException
import org.json.JSONObject
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

internal class ChallengeResult private constructor(val isAuthenticated: Boolean, val payload: String) {

Expand All @@ -28,6 +29,7 @@ internal class ChallengeResult private constructor(val isAuthenticated: Boolean,
* @return The filled object with the content needed for the details response.
* @throws JSONException In case parsing fails.
*/
@OptIn(ExperimentalEncodingApi::class)
fun from(
transactionStatus: String,
errorDetails: String? = null,
Expand All @@ -38,7 +40,7 @@ internal class ChallengeResult private constructor(val isAuthenticated: Boolean,
jsonObject.put(KEY_TRANSACTION_STATUS, transactionStatus)
jsonObject.putOpt(KEY_AUTHORISATION_TOKEN, authorisationToken)
jsonObject.putOpt(KEY_SDK_ERROR, errorDetails)
val payload = AndroidBase64Encoder().encode(jsonObject.toString())
val payload = Base64.encode(jsonObject.toString().toByteArray())
return ChallengeResult(isAuthenticated, payload)
}
}
Expand Down
Expand Up @@ -36,7 +36,6 @@ import com.adyen.checkout.components.core.internal.PaymentDataRepository
import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider
import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper
import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams
import com.adyen.checkout.components.core.internal.util.AndroidBase64Encoder
import com.adyen.checkout.components.core.internal.util.get
import com.adyen.checkout.components.core.internal.util.viewModelFactory
import com.adyen.checkout.core.internal.data.api.HttpClientFactory
Expand Down Expand Up @@ -105,7 +104,6 @@ constructor(
redirectHandler = redirectHandler,
threeDS2Service = ThreeDS2Service.INSTANCE,
coroutineDispatcher = Dispatchers.Default,
base64Encoder = AndroidBase64Encoder(),
application = application,
)
}
Expand Down
Expand Up @@ -34,7 +34,6 @@ import com.adyen.checkout.components.core.internal.ActionObserverRepository
import com.adyen.checkout.components.core.internal.PaymentDataRepository
import com.adyen.checkout.components.core.internal.SavedStateHandleContainer
import com.adyen.checkout.components.core.internal.SavedStateHandleProperty
import com.adyen.checkout.components.core.internal.util.Base64Encoder
import com.adyen.checkout.components.core.internal.util.bufferedChannel
import com.adyen.checkout.core.AdyenLogLevel
import com.adyen.checkout.core.exception.CheckoutException
Expand All @@ -46,8 +45,10 @@ import com.adyen.checkout.ui.core.internal.ui.ComponentViewType
import com.adyen.threeds2.AuthenticationRequestParameters
import com.adyen.threeds2.ChallengeResult
import com.adyen.threeds2.ChallengeStatusHandler
import com.adyen.threeds2.InitializeResult
import com.adyen.threeds2.ThreeDS2Service
import com.adyen.threeds2.Transaction
import com.adyen.threeds2.TransactionResult
import com.adyen.threeds2.exception.InvalidInputException
import com.adyen.threeds2.exception.SDKNotInitializedException
import com.adyen.threeds2.exception.SDKRuntimeException
Expand All @@ -64,6 +65,8 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import org.json.JSONException
import org.json.JSONObject
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

@Suppress("TooManyFunctions", "LongParameterList")
internal class DefaultAdyen3DS2Delegate(
Expand All @@ -76,7 +79,6 @@ internal class DefaultAdyen3DS2Delegate(
private val redirectHandler: RedirectHandler,
private val threeDS2Service: ThreeDS2Service,
private val coroutineDispatcher: CoroutineDispatcher,
private val base64Encoder: Base64Encoder,
private val application: Application,
) : Adyen3DS2Delegate, ChallengeStatusHandler, SavedStateHandleContainer {

Expand Down Expand Up @@ -210,7 +212,10 @@ internal class DefaultAdyen3DS2Delegate(
return
}

val configParameters = createAdyenConfigParameters(fingerprintToken)
val configParameters = createAdyenConfigParameters(fingerprintToken) ?: run {
exceptionChannel.trySend(ComponentException("Failed to create ConfigParameters."))
return
}

val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
adyenLog(AdyenLogLevel.ERROR, throwable) { "Unexpected uncaught 3DS2 Exception" }
Expand All @@ -221,11 +226,13 @@ internal class DefaultAdyen3DS2Delegate(
// This makes sure the 3DS2 SDK doesn't re-use any state from previous transactions
closeTransaction()

try {
adyenLog(AdyenLogLevel.DEBUG) { "initialize 3DS2 SDK" }
adyenLog(AdyenLogLevel.DEBUG) { "initialize 3DS2 SDK" }
val initializeResult =
threeDS2Service.initialize(activity, configParameters, null, componentParams.uiCustomization)
} catch (e: SDKRuntimeException) {
exceptionChannel.trySend(ComponentException("Failed to initialize 3DS2 SDK", e))

if (initializeResult is InitializeResult.Failure) {
val details = makeDetails(initializeResult.transactionStatus, initializeResult.additionalDetails)
emitDetails(details)
return@launch
}

Expand All @@ -246,9 +253,10 @@ internal class DefaultAdyen3DS2Delegate(
}
}

@OptIn(ExperimentalEncodingApi::class)
@Throws(ComponentException::class, ModelSerializationException::class)
private fun decodeFingerprintToken(encoded: String): FingerprintToken {
val decodedFingerprintToken = base64Encoder.decode(encoded)
val decodedFingerprintToken = Base64.decode(encoded).toString(Charsets.UTF_8)

val fingerprintJson: JSONObject = try {
JSONObject(decodedFingerprintToken)
Expand All @@ -259,32 +267,46 @@ internal class DefaultAdyen3DS2Delegate(
return FingerprintToken.SERIALIZER.deserialize(fingerprintJson)
}

@Suppress("DestructuringDeclarationWithTooManyEntries")
private fun createAdyenConfigParameters(
fingerprintToken: FingerprintToken
): ConfigParameters = AdyenConfigParameters.Builder(
// directoryServerId
fingerprintToken.directoryServerId,
// directoryServerPublicKey
fingerprintToken.directoryServerPublicKey,
// directoryServerRootCertificates
fingerprintToken.directoryServerRootCertificates,
)
.deviceParameterBlockList(componentParams.deviceParameterBlockList)
.build()
): ConfigParameters? {
val (directoryServerId, directoryServerPublicKey, directoryServerRootCertificates, _, _) = fingerprintToken

if (directoryServerId == null || directoryServerPublicKey == null || directoryServerRootCertificates == null) {
adyenLog(AdyenLogLevel.DEBUG) {
"directoryServerId, directoryServerPublicKey or directoryServerRootCertificates is null."
}
return null
}

return AdyenConfigParameters.Builder(
directoryServerId,
directoryServerPublicKey,
directoryServerRootCertificates,
)
.deviceParameterBlockList(componentParams.deviceParameterBlockList)
.build()
}

private fun createTransaction(fingerprintToken: FingerprintToken): Transaction? {
if (fingerprintToken.threeDSMessageVersion == null) {
exceptionChannel.trySend(
ComponentException(
"Failed to create 3DS2 Transaction. Missing threeDSMessageVersion inside fingerprintToken.",
),
)
val error = "Failed to create 3DS2 Transaction. Missing threeDSMessageVersion inside fingerprintToken."
exceptionChannel.trySend(ComponentException(error))
return null
}

return try {
adyenLog(AdyenLogLevel.DEBUG) { "create transaction" }
threeDS2Service.createTransaction(null, fingerprintToken.threeDSMessageVersion)
when (val result = threeDS2Service.createTransaction(null, fingerprintToken.threeDSMessageVersion)) {
is TransactionResult.Failure -> {
val details = makeDetails(result.transactionStatus, result.additionalDetails)
emitDetails(details)
null
}

is TransactionResult.Success -> result.transaction
}
} catch (e: SDKNotInitializedException) {
exceptionChannel.trySend(ComponentException("Failed to create 3DS2 Transaction", e))
null
Expand All @@ -294,6 +316,7 @@ internal class DefaultAdyen3DS2Delegate(
}
}

@OptIn(ExperimentalEncodingApi::class)
@Throws(ComponentException::class)
private fun createEncodedFingerprint(authenticationRequestParameters: AuthenticationRequestParameters): String {
return try {
Expand All @@ -308,7 +331,7 @@ internal class DefaultAdyen3DS2Delegate(
}
}

base64Encoder.encode(fingerprintJson.toString())
Base64.encode(fingerprintJson.toString().toByteArray())
} catch (e: JSONException) {
throw ComponentException("Failed to create encoded fingerprint", e)
}
Expand Down Expand Up @@ -368,6 +391,7 @@ internal class DefaultAdyen3DS2Delegate(
}
}

@OptIn(ExperimentalEncodingApi::class)
@VisibleForTesting
internal fun challengeShopper(activity: Activity, encodedChallengeToken: String) {
adyenLog(AdyenLogLevel.DEBUG) { "challengeShopper" }
Expand All @@ -379,7 +403,7 @@ internal class DefaultAdyen3DS2Delegate(
return
}

val decodedChallengeToken = base64Encoder.decode(encodedChallengeToken)
val decodedChallengeToken = Base64.decode(encodedChallengeToken).toString(Charsets.UTF_8)
val challengeTokenJson: JSONObject = try {
JSONObject(decodedChallengeToken)
} catch (e: JSONException) {
Expand Down Expand Up @@ -479,7 +503,7 @@ internal class DefaultAdyen3DS2Delegate(
private fun cleanUp3DS2() {
@Suppress("SwallowedException")
try {
ThreeDS2Service.INSTANCE.cleanup(application)
threeDS2Service.cleanup(application)
} catch (e: SDKNotInitializedException) {
// Safe to ignore
}
Expand Down
Expand Up @@ -12,7 +12,6 @@ import android.app.Activity
import android.content.Intent
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
import com.adyen.checkout.adyen3ds2.internal.ui.Adyen3DS2ComponentViewType
import com.adyen.checkout.adyen3ds2.internal.ui.Adyen3DS2Delegate
import com.adyen.checkout.components.core.action.Threeds2Action
Expand All @@ -21,6 +20,7 @@ import com.adyen.checkout.components.core.internal.ActionComponentEventHandler
import com.adyen.checkout.test.LoggingExtension
import com.adyen.checkout.test.TestDispatcherExtension
import com.adyen.checkout.test.extensions.invokeOnCleared
import com.adyen.checkout.test.extensions.test
import com.adyen.checkout.ui.core.internal.test.TestComponentViewType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
Expand Down Expand Up @@ -80,26 +80,22 @@ internal class Adyen3DS2ComponentTest(

@Test
fun `when component is initialized then view flow should match delegate view flow`() = runTest {
component.viewFlow.test {
assertEquals(Adyen3DS2ComponentViewType, awaitItem())
expectNoEvents()
}
val viewFlow = component.viewFlow.test(testScheduler)

assertEquals(Adyen3DS2ComponentViewType, viewFlow.latestValue)
}

@Test
fun `when delegate view flow emits a value then component view flow should match that value`() = runTest {
val delegateViewFlow = MutableStateFlow(TestComponentViewType.VIEW_TYPE_1)
whenever(adyen3DS2Delegate.viewFlow) doReturn delegateViewFlow
component = Adyen3DS2Component(adyen3DS2Delegate, actionComponentEventHandler)
val viewFlow = component.viewFlow.test(testScheduler)

component.viewFlow.test {
assertEquals(TestComponentViewType.VIEW_TYPE_1, awaitItem())

delegateViewFlow.emit(TestComponentViewType.VIEW_TYPE_2)
assertEquals(TestComponentViewType.VIEW_TYPE_2, awaitItem())
assertEquals(TestComponentViewType.VIEW_TYPE_1, viewFlow.values[0])

expectNoEvents()
}
delegateViewFlow.emit(TestComponentViewType.VIEW_TYPE_2)
assertEquals(TestComponentViewType.VIEW_TYPE_2, viewFlow.values[1])
}

@Test
Expand Down

0 comments on commit 6aef6f7

Please sign in to comment.