Skip to content

Commit

Permalink
Merge pull request #1632 from Adyen/fix/restore-3ds2-state
Browse files Browse the repository at this point in the history
Restore 3DS2 state after process death
  • Loading branch information
OscarSpruit authored May 22, 2024
2 parents 6edff78 + 7442464 commit 5c7bdaf
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ internal class DefaultAdyen3DS2Delegate(

private var currentTransaction: Transaction? = null

private var authorizationToken: String? by SavedStateHandleProperty(AUTHORIZATION_TOKEN_KEY)
private var action: BaseThreeds2Action? by SavedStateHandleProperty(ACTION_KEY)

override fun initialize(coroutineScope: CoroutineScope) {
_coroutineScope = coroutineScope
SharedChallengeStatusHandler.onCompletionListener = this
}

override fun observe(
Expand All @@ -122,10 +123,12 @@ internal class DefaultAdyen3DS2Delegate(

override fun handleAction(action: Action, activity: Activity) {
if (action !is BaseThreeds2Action) {
exceptionChannel.trySend(ComponentException("Unsupported action"))
emitError(ComponentException("Unsupported action"))
return
}

this.action = action

val paymentData = action.paymentData
paymentDataRepository.paymentData = paymentData
when (action) {
Expand All @@ -140,7 +143,7 @@ internal class DefaultAdyen3DS2Delegate(
activity: Activity,
) {
if (action.token.isNullOrEmpty()) {
exceptionChannel.trySend(ComponentException("Fingerprint token not found."))
emitError(ComponentException("Fingerprint token not found."))
return
}
identifyShopper(
Expand All @@ -155,7 +158,7 @@ internal class DefaultAdyen3DS2Delegate(
activity: Activity,
) {
if (action.token.isNullOrEmpty()) {
exceptionChannel.trySend(ComponentException("Challenge token not found."))
emitError(ComponentException("Challenge token not found."))
return
}
challengeShopper(activity, action.token.orEmpty())
Expand All @@ -166,16 +169,14 @@ internal class DefaultAdyen3DS2Delegate(
activity: Activity,
) {
if (action.token.isNullOrEmpty()) {
exceptionChannel.trySend(ComponentException("3DS2 token not found."))
emitError(ComponentException("3DS2 token not found."))
return
}
if (action.subtype == null) {
exceptionChannel.trySend(ComponentException("3DS2 Action subtype not found."))
emitError(ComponentException("3DS2 Action subtype not found."))
return
}
val subtype = Threeds2Action.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())
}

Expand Down Expand Up @@ -208,18 +209,18 @@ internal class DefaultAdyen3DS2Delegate(
val fingerprintToken = try {
decodeFingerprintToken(encodedFingerprintToken)
} catch (e: CheckoutException) {
exceptionChannel.trySend(ComponentException("Failed to decode fingerprint token", e))
emitError(ComponentException("Failed to decode fingerprint token", e))
return
}

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

val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
adyenLog(AdyenLogLevel.ERROR, throwable) { "Unexpected uncaught 3DS2 Exception" }
exceptionChannel.trySend(CheckoutException("Unexpected 3DS2 exception.", throwable))
emitError(CheckoutException("Unexpected 3DS2 exception.", throwable))
}

coroutineScope.launch(coroutineDispatcher + coroutineExceptionHandler) {
Expand All @@ -240,7 +241,7 @@ internal class DefaultAdyen3DS2Delegate(

val authenticationRequestParameters = currentTransaction?.authenticationRequestParameters
if (authenticationRequestParameters == null) {
exceptionChannel.trySend(ComponentException("Failed to retrieve 3DS2 authentication parameters"))
emitError(ComponentException("Failed to retrieve 3DS2 authentication parameters"))
return@launch
}
val encodedFingerprint = createEncodedFingerprint(authenticationRequestParameters)
Expand Down Expand Up @@ -292,7 +293,7 @@ internal class DefaultAdyen3DS2Delegate(
private fun createTransaction(fingerprintToken: FingerprintToken): Transaction? {
if (fingerprintToken.threeDSMessageVersion == null) {
val error = "Failed to create 3DS2 Transaction. Missing threeDSMessageVersion inside fingerprintToken."
exceptionChannel.trySend(ComponentException(error))
emitError(ComponentException(error))
return null
}

Expand All @@ -308,10 +309,10 @@ internal class DefaultAdyen3DS2Delegate(
is TransactionResult.Success -> result.transaction
}
} catch (e: SDKNotInitializedException) {
exceptionChannel.trySend(ComponentException("Failed to create 3DS2 Transaction", e))
emitError(ComponentException("Failed to create 3DS2 Transaction", e))
null
} catch (e: SDKRuntimeException) {
exceptionChannel.trySend(ComponentException("Failed to create 3DS2 Transaction", e))
emitError(ComponentException("Failed to create 3DS2 Transaction", e))
null
}
}
Expand Down Expand Up @@ -348,7 +349,7 @@ internal class DefaultAdyen3DS2Delegate(
)
.fold(
onSuccess = { result -> onSubmitFingerprintResult(result, activity) },
onFailure = { e -> exceptionChannel.trySend(ComponentException("Unable to submit fingerprint", e)) },
onFailure = { e -> emitError(ComponentException("Unable to submit fingerprint", e)) },
)
}

Expand All @@ -373,21 +374,13 @@ internal class DefaultAdyen3DS2Delegate(
}
}

private fun emitDetails(details: JSONObject) {
val actionComponentData = ActionComponentData(
details = details,
paymentData = paymentDataRepository.paymentData,
)
detailsChannel.trySend(actionComponentData)
}

private fun makeRedirect(activity: Activity, action: RedirectAction) {
val url = action.url
try {
adyenLog(AdyenLogLevel.DEBUG) { "makeRedirect - $url" }
redirectHandler.launchUriRedirect(activity, url)
} catch (e: CheckoutException) {
exceptionChannel.trySend(e)
emitError(e)
}
}

Expand All @@ -397,7 +390,7 @@ internal class DefaultAdyen3DS2Delegate(
adyenLog(AdyenLogLevel.DEBUG) { "challengeShopper" }

if (currentTransaction == null) {
exceptionChannel.trySend(
emitError(
Authentication3DS2Exception("Failed to make challenge, missing reference to initial transaction."),
)
return
Expand All @@ -407,16 +400,21 @@ internal class DefaultAdyen3DS2Delegate(
val challengeTokenJson: JSONObject = try {
JSONObject(decodedChallengeToken)
} catch (e: JSONException) {
exceptionChannel.trySend(ComponentException("JSON parsing of FingerprintToken failed", e))
emitError(ComponentException("JSON parsing of FingerprintToken failed", e))
return
}

val challengeToken = ChallengeToken.SERIALIZER.deserialize(challengeTokenJson)
val challengeParameters = createChallengeParameters(challengeToken)
try {
currentTransaction?.doChallenge(activity, challengeParameters, this, DEFAULT_CHALLENGE_TIME_OUT)
currentTransaction?.doChallenge(
activity,
challengeParameters,
SharedChallengeStatusHandler,
DEFAULT_CHALLENGE_TIME_OUT,
)
} catch (e: InvalidInputException) {
exceptionChannel.trySend(CheckoutException("Error starting challenge", e))
emitError(CheckoutException("Error starting challenge", e))
}
}

Expand All @@ -439,7 +437,7 @@ internal class DefaultAdyen3DS2Delegate(
val parsedResult = redirectHandler.parseRedirectResult(intent.data)
emitDetails(parsedResult)
} catch (e: CheckoutException) {
exceptionChannel.trySend(e)
emitError(e)
}
}

Expand All @@ -449,15 +447,15 @@ internal class DefaultAdyen3DS2Delegate(
val details = makeDetails(transactionStatus)
emitDetails(details)
} catch (e: CheckoutException) {
exceptionChannel.trySend(e)
emitError(e)
} finally {
closeTransaction()
}
}

private fun onCancelled() {
adyenLog(AdyenLogLevel.DEBUG) { "challenge cancelled" }
exceptionChannel.trySend(Cancelled3DS2Exception("Challenge canceled."))
emitError(Cancelled3DS2Exception("Challenge canceled."))
closeTransaction()
}

Expand All @@ -467,7 +465,7 @@ internal class DefaultAdyen3DS2Delegate(
val details = makeDetails(result.transactionStatus, result.additionalDetails)
emitDetails(details)
} catch (e: CheckoutException) {
exceptionChannel.trySend(e)
emitError(e)
} finally {
closeTransaction()
}
Expand All @@ -479,7 +477,7 @@ internal class DefaultAdyen3DS2Delegate(
val details = makeDetails(result.transactionStatus, result.additionalDetails)
emitDetails(details)
} catch (e: CheckoutException) {
exceptionChannel.trySend(e)
emitError(e)
} finally {
closeTransaction()
}
Expand Down Expand Up @@ -510,22 +508,30 @@ internal class DefaultAdyen3DS2Delegate(
}

override fun onError(e: CheckoutException) {
exceptionChannel.trySend(e)
emitError(e)
}

override fun setOnRedirectListener(listener: () -> Unit) {
redirectHandler.setOnRedirectListener(listener)
}

override fun onCleared() {
removeObserver()
_coroutineScope = null
redirectHandler.removeOnRedirectListener()
private fun emitError(e: CheckoutException) {
exceptionChannel.trySend(e)
clearState()
}

private fun emitDetails(details: JSONObject) {
val actionComponentData = ActionComponentData(
details = details,
paymentData = paymentDataRepository.paymentData,
)
detailsChannel.trySend(actionComponentData)
clearState()
}

private fun makeDetails(transactionStatus: String, errorDetails: String? = null): JSONObject {
// Check whether authorizationToken was set and create the corresponding details object
val token = authorizationToken
val token = (action as? Threeds2Action)?.authorisationToken
return if (token == null) {
adyen3DS2Serializer.createChallengeDetails(
transactionStatus = transactionStatus,
Expand All @@ -540,9 +546,22 @@ internal class DefaultAdyen3DS2Delegate(
}
}

private fun clearState() {
action = null
}

override fun onCleared() {
removeObserver()
SharedChallengeStatusHandler.onCompletionListener = null
_coroutineScope = null
redirectHandler.removeOnRedirectListener()
}

companion object {
private const val AUTHORIZATION_TOKEN_KEY = "authorization_token"
private const val DEFAULT_CHALLENGE_TIME_OUT = 10
private const val PROTOCOL_VERSION_2_1_0 = "2.1.0"

@VisibleForTesting
internal const val ACTION_KEY = "ACTION_KEY"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2024 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by oscars on 21/5/2024.
*/

package com.adyen.checkout.adyen3ds2.internal.ui

import androidx.annotation.VisibleForTesting
import com.adyen.threeds2.ChallengeResult
import com.adyen.threeds2.ChallengeStatusHandler

internal object SharedChallengeStatusHandler : ChallengeStatusHandler {

var onCompletionListener: ChallengeStatusHandler? = null
set(value) {
field = value
queuedResult?.let { onCompletion(it) }
}

private var queuedResult: ChallengeResult? = null

override fun onCompletion(result: ChallengeResult) {
onCompletionListener
?.onCompletion(result)
?.also {
queuedResult = null
} ?: run {
queuedResult = result
}
}

@VisibleForTesting
internal fun reset() {
onCompletionListener = null
queuedResult = null
}
}
Loading

0 comments on commit 5c7bdaf

Please sign in to comment.