Open Cattari opened 1 year ago
Hey @Cattari
Thanks for reaching out!
We are aware of this unfortunate behavior. None of our tests and attempts to resolve it were fruitful. This is not a problem for native Android but only affects React Native. The only reference to the problem we were able to find is not fruitful either.
For now, the only advice we have - is always to be prepared for the worst. In LIVE transactions the "happy flow" with successful redirect may not occur due to numerous factors on both iOS and Android (even on web). Always treat your transactions as "pending" until webhook with the actual status received.
For example, you can keep track of a payment state: store the flag when 'onSubmit' is called and when your App leaves the foreground, so when the next time the App is open, no redirect is detected and the flag is set - there is a high chance a payment was made and your backend is the only source of truth now.
Indeed this is another blocker.
@Cattari can you please try poc
We need to check if there are some unknown unwanted behaviors e.g. google pay, google login etc.
hi @pinpong yeah, we actually tried that before a long time ago it solves the issue with the payment flow, but it brings a new issue
if universal/deep links are used, they start a new instance (a copy) of the app, and we didn't manage to avoid that. unfortunately, for us it's an unacceptable behaviour :(
We are facing the same issue. Do you have a proposed workaround?
Thanks
We are aware of this unfortunate behavior. None of our tests and attempts to resolve it were fruitful. This is not a problem for native Android but only affects React Native. The only reference to the problem we were able to find is not fruitful either.
How about if react-native-adyen
tried to serialize it's internal state before handing of, and then when resuming restoring from the serialized state or offering the implementing app to try and resume the checkout session?
For now, the only advice we have - is always to be prepared for the worst. In LIVE transactions the "happy flow" with successful redirect may not occur due to numerous factors on both iOS and Android (even on web). Always treat your transactions as "pending" until webhook with the actual status received.
For example, you can keep track of a payment state: store the flag when 'onSubmit' is called and when your App leaves the foreground, so when the next time the App is open, no redirect is detected and the flag is set - there is a high chance a payment was made and your backend is the only source of truth now.
I don't see how Webhooks can solve the fact that for a 3DS OOB challenge, additionalDetails
cannot be submitted so our offers are "stuck" in an "open" state.
@StefanWallin
You have posted previously:
A potential workaround would be for us to store data from our api-request in the onSubmit-callback in local storage, then on component rerender upon activity restart, check for that data and somehow be able to call nativeComponent.handle(data), but we don't have access to nativeComponent in that scope.
To be able to do that we would need to be able to check if the adyen drop-in component is active so as to not trigger multiple nativeComponent.handle(data).
in v2 BETA it is possible to call AdyenAction.handle(data)
independently from the payment flow. Maybe this will be a workaround 🤔
@descorp Would you consider v2 BETA production ready yet?
And also, my suggestion above is based on the assumption that the adyen-library can resume the session and call the onAdditionalDetails-callback even though it did not receive any additionalDetails from redirectUri, or display the challenge page again (which might have a button from issuer that triggers additionalDetails)
@StefanWallin
We expect it to be stable enough for existing merchant, but can't officially call it one, until we have several brave pilot merchants to go LIVE with it.
It will be published to NPM this week, so you can give it a go.
So after two long days (involving RN 0.73-upgrade with kotlin-migration repercussions) I'm finally at a stage to try this out.
I have managed to narrow down this to happen in the correct moment on the right platform, might have som false positives still, but when I do the AdyenAction.handle(paymentAction)
fails and crashes the android app. I might be missing something here.
const onAppStateChange = useCallback(
(appState: AppStateStatus) => {
const storedpaymentData = Storage.loadString(TEMP_PAYMENT_DATA_KEY)
const active = appState === 'active'
const midProcess = status === 'awaiting3DSecure' && storedpaymentData
const android = Platform.OS === 'android'
if (active && midProcess && android) {
const paymentAction: PaymentAction = JSON.parse(storedpaymentData)
AdyenAction.handle(paymentAction, adyenConfiguration)
}
},
[status]
)
The contents of paymentAction
is the same that I in my onSubmit-handler in the normal case use to call nativeComponent.handle(paymentAction)
with. It's contents look like this:
{
"paymentData": "Ab0...",
"paymentMethodType": "scheme",
"authorisationToken": "Ab0...",
"subtype": "fingerprint",
"token": "eyJ...",
"type": "threeDS2"
}
Am I missing some detail on how you think this could end up working for resuming a 3DS challenge session?
Missed the StackTrace:
E FATAL EXCEPTION: main
Process: se.myapp.app, PID: 4266
android.view.InflateException: Binary XML file line #53 in se.myapp.app:layout/view_payment_in_progress: Binary XML file line #53 in se.myapp.app:layout/view_payment_in_progress: Error inflating class <unknown>
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@18f1660, Dispatchers.Main.immediate]
Caused by: android.view.InflateException: Binary XML file line #53 in se.myapp.app:layout/view_payment_in_progress: Error inflating class <unknown>
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Constructor.newInstance0(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
at android.view.LayoutInflater.createView(LayoutInflater.java:852)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1004)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:959)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:1121)
at android.view.LayoutInflater.inflate(LayoutInflater.java:654)
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at android.view.LayoutInflater.inflate(LayoutInflater.java:479)
at com.adyen.checkout.ui.core.databinding.ViewPaymentInProgressBinding.inflate(ViewPaymentInProgressBinding.java:58)
at com.adyen.checkout.ui.core.internal.ui.view.PaymentInProgressView.<init>(PaymentInProgressView.kt:39)
at com.adyen.checkout.ui.core.internal.ui.view.PaymentInProgressView.<init>(PaymentInProgressView.kt:27)
at com.adyen.checkout.adyen3ds2.internal.ui.Adyen3DS2ViewProvider.getView(Adyen3DS2ViewProvider.kt:23)
at com.adyen.checkout.ui.core.AdyenComponentView.loadView(AdyenComponentView.kt:121)
at com.adyen.checkout.ui.core.AdyenComponentView.access$loadView(AdyenComponentView.kt:48)
at com.adyen.checkout.ui.core.AdyenComponentView$attach$1.invokeSuspend(AdyenComponentView.kt:104)
at com.adyen.checkout.ui.core.AdyenComponentView$attach$1.invoke(Unknown Source:8)
at com.adyen.checkout.ui.core.AdyenComponentView$attach$1.invoke(Unknown Source:4)
at kotlinx.coroutines.flow.FlowKt__TransformKt$onEach$$inlined$unsafeTransform$1$2.emit(Emitters.kt:223)
at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:396)
at kotlinx.coroutines.flow.StateFlowImpl$collect$1.invokeSuspend(Unknown Source:15)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:68)
at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:373)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:25)
at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:110)
at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:126)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)
at kotlinx.coroutines.BuildersKt.launch(Unknown Source:1)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:1)
at kotlinx.coroutines.flow.FlowKt__CollectKt.launchIn(Collect.kt:49)
at kotlinx.coroutines.flow.FlowKt.launchIn(Unknown Source:1)
at com.adyen.checkout.action.core.internal.ui.DefaultGenericActionDelegate.observeViewFlow(DefaultGenericActionDelegate.kt:152)
at com.adyen.checkout.action.core.internal.ui.DefaultGenericActionDelegate.handleAction(DefaultGenericActionDelegate.kt:122)
at com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent.handleAction(DefaultActionHandlingComponent.kt:35)
at com.adyen.checkout.action.core.GenericActionComponent.handleAction(Unknown Source:12)
E at com.adyenreactnativesdk.cse.ActionFragment.handle(ActionFragment.kt:64)
at com.adyenreactnativesdk.cse.ActionFragment.setupComponent(ActionFragment.kt:58)
at com.adyenreactnativesdk.cse.ActionFragment.onStart(ActionFragment.kt:40)
at androidx.fragment.app.Fragment.performStart(Fragment.java:3187)
at androidx.fragment.app.FragmentStateManager.start(FragmentStateManager.java:628)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:290)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1943)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1845)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1782)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:565)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.MaterialComponents (or a descendant).
at com.google.android.material.internal.ThemeEnforcement.checkTheme(ThemeEnforcement.java:247)
at com.google.android.material.internal.ThemeEnforcement.checkMaterialTheme(ThemeEnforcement.java:216)
at com.google.android.material.internal.ThemeEnforcement.checkCompatibleTheme(ThemeEnforcement.java:144)
at com.google.android.material.internal.ThemeEnforcement.obtainStyledAttributes(ThemeEnforcement.java:76)
at com.google.android.material.button.MaterialButton.<init>(MaterialButton.java:234)
at com.google.android.material.button.MaterialButton.<init>(MaterialButton.java:225)
... 56 more
Okay, I solved this style issue with changing the parent of the style my android-app uses, like this:
<resources>
<!-- Base application theme. -->
- <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
+ <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="android:windowIsTranslucent">true</item>
</style>
</resources>
Now, when I try again, I've changed the code to look like this..
const onAppStateChange = useCallback(
(appState: AppStateStatus) => {
const storedpaymentData = Storage.loadString(TEMP_PAYMENT_DATA_KEY)
const active = appState === 'active'
const midProcess = status === 'awaiting3DSecure' && storedpaymentData
const android = Platform.OS === 'android'
if (active && midProcess && android) {
const paymentAction: PaymentAction = JSON.parse(storedpaymentData)
try {
const data = await AdyenAction.handle(
paymentAction,
adyenConfiguration
)
console.log('DATA', data)
} catch (error) {
console.log('ERROR', JSON.stringify(error, null, 2))
AdyenAction.hide(false)
}
}
},
[status]
)
So this enters the catch-clause which spits the following into the terminal:
ERROR {
"nativeStackAndroid": [
{
"lineNumber": 298,
"file": "DefaultAdyen3DS2Delegate.kt",
"methodName": "submitFingerprintAutomatically",
"class": "com.adyen.checkout.adyen3ds2.internal.ui.DefaultAdyen3DS2Delegate"
},
{
"lineNumber": 67,
"file": "DefaultAdyen3DS2Delegate.kt",
"methodName": "access$submitFingerprintAutomatically",
"class": "com.adyen.checkout.adyen3ds2.internal.ui.DefaultAdyen3DS2Delegate"
},
{
"lineNumber": 15,
"file": null,
"methodName": "invokeSuspend",
"class": "com.adyen.checkout.adyen3ds2.internal.ui.DefaultAdyen3DS2Delegate$submitFingerprintAutomatically$1"
},
{
"lineNumber": 33,
"file": "ContinuationImpl.kt",
"methodName": "resumeWith",
"class": "kotlin.coroutines.jvm.internal.BaseContinuationImpl"
},
{
"lineNumber": 104,
"file": "DispatchedTask.kt",
"methodName": "run",
"class": "kotlinx.coroutines.DispatchedTask"
},
{
"lineNumber": 584,
"file": "CoroutineScheduler.kt",
"methodName": "runSafely",
"class": "kotlinx.coroutines.scheduling.CoroutineScheduler"
},
{
"lineNumber": 793,
"file": "CoroutineScheduler.kt",
"methodName": "executeTask",
"class": "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker"
},
{
"lineNumber": 697,
"file": "CoroutineScheduler.kt",
"methodName": "runWorker",
"class": "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker"
},
{
"lineNumber": 684,
"file": "CoroutineScheduler.kt",
"methodName": "run",
"class": "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker"
}
],
"userInfo": null,
"message": "Unable to submit fingerprint",
"code": "actionError"
}
https://github.com/Adyen/adyen-react-native/assets/457653/5b361179-6367-455d-b33c-9d0f50487200
AdyenAction.hide(false)
. One effect of this is that you have to know to hit the blue key on the keyboard to go to the next field instead of clicking the next field.
@descorp If it wasn't clear I'm at the end of my understanding here. Is it supposed to be possible to resume a 3DS-challenge in this way?
Hey @StefanWallin
Am I missing some detail on how you think this could end up working for resuming a 3DS challenge session?
Sorry you have to go thought all of this research all by yourself 💚
This approach would perfectly works with redirect
action, but for Native 3DS - it is not supported out of the box.
Each 3DS transaction is unique and when you use the previous action
- our backend returns an error cause this request was already made.
There was potential workaround on API v66, I am verifying if it is possible to do on modern v69+ versions..
I confirmed that "old way" is possible on any modern API version. I'll do some tests locally and will get back to you if it turns out to be successful.
How did your testing go?
Hey @StefanWallin
Unfortunately it is not possible. 3DS flow is "one-time" thing by design. The only 100% solution here is to rely on web-hooks.
@descorp, okay. that's unfortunate but understandable. I guess we might have to keep poking at this from the activity-perspective.
adyen-react-native
to pass along the configuration parameter configuration.actionComponent.threeDS.requestorAppURL
which needs to be a universal link if I understand it correctly. Will track this separately in issue #374 Hey @StefanWallin
1) Web-hook is an ultimate source of truth about transaction state. Rule of thumb here - always consider the transaction as "pending" until it is concluded by the shopper (happy flow, canceled, component error) or webhook(broken redirect, delays, "icon click" or any other unexpected behavior). This means that your UI should be transparent with shoppers about the current status. It could be helpful to check with the shopper about their experience - whether they completed the payment or not.
2) requestorAppURL
is just an alternative for "Custom URL Scheme" to return to the app - in latest 3DS2 specification only "https" protocol is allowed. It have the same problems, since shopper still can abandon payment flow or redirect can be broken.
The good news is that now we have the ability to setOnRedirectListener on the Native side to notify about shopper being redirected to an external app/browser.
This callback for ReactNative code is in our backlog.
Is there any workaround for this? Does this mean that for Android there is no way to successfully make a payment with 3DS2.2 App<>App redirection? So you get redirected from our apps 3DS screen to the authenticator app (banking app) then when they are redirected back to our app with a deeplink, they will be unable to finish the payment?
@stuartmcvean
On v2.0.0-rc1
we have provided threeDS2.requestorAppUrl configuration that should help with this.
Keep in mind that this feature depend on issuer bank and platform.
We are interested in any feedback you may have on this to improve the flow.
Hello, guys!
We are facing a problem with our custom bridge solution for RN + adyen-android module (the same version you use here). That RN uses
singleTask
launch mode for the MainActivity. In that case, if a user is in the middle of the payment and goes back to the app from the launcher - it kills the DropInActivity and user doesn't finish the payment. It became very painful after Adyen Checkout API v68 when a user is charged right after they finish a transaction on the banking app.Several issues are also reported in adyen drop-in and adyen 3ds2 component repositories, but they are closed. Here are they:
https://github.com/Adyen/adyen-android/issues/711#issuecomment-1142319516 https://github.com/Adyen/adyen-3ds2-android/issues/17
To Reproduce
Reproduces only for Android.
Steps to reproduce the behavior:
Expected behavior
Actual behavior
Smartphone (please complete the following information):
Additional context We want to know if you handled the mentioned issue in this solution for RN. Because it's directly connected to launchMode=singleTask which is used in RN.