stripe / stripe-terminal-ios

Stripe Terminal iOS SDK
https://stripe.com/docs/terminal/sdk/ios
Other
100 stars 61 forks source link

Payment intent was created online, but sdk confirmed intent offline, with wrong confirmed status #294

Closed jonhcbs closed 3 months ago

jonhcbs commented 6 months ago

Summary

When using in online mode, we found there is an issue some times, not very often but happens several times this month.

Steps:

1 start a payment, create a payment intent when online.
2 collectPaymentMethod // success 3 confirmPaymentIntent

but at this point sdk will change to offline state for some reason network not stable issue etc.

extension StripeReaderSession: OfflineDelegate {
    func terminal(_ terminal: Terminal, didChange offlineStatus: OfflineStatus) {
         print( offlineStatus.sdk.networkStatus)
    }
}

we add the log and noticed that offlineStatus.sdk.networkStatus value is 1 , that is offline status.

sdk Confirm Payment Intent returns success, but it is not correct state. it is an offline confirm. some times sdk will later return Your card was declined. or Your card has insufficient funds. etc error.

since sdk returns capture success, we think that this payment is success, and the order is finished and customer left. but actually payment Intent capture fail, this is bad.

The problem is we don't want to sdk change to offline by itself which is out of control, if confirmPaymentIntent when online but sdk changed to offline suddenly, just return error or retry ? Since the payment Intent is an online PI withpi_ prefix, should not confirm success offline ? not sure it is a bug or not. We are mostly using sdk 3.2.1 version now.

Below is some payment intent which have this issue.

pi_3OsxcLAPQoqa93xj2JnQu8kA
pi_3Ot1SEAPQoqa93xj160LezIg
pi_3OsxcJAPQoqa93xj0nmWGzIf

Code to reproduce

Actually we can not reproduce this, we find this issue from the logs.

Terminal.shared.collectPaymentMethod(intent) { (intentWithPaymentMethod, attachError) in
if let error = attachError {
                if (error as NSError).code == ErrorCode.canceled.rawValue {
                    ///
                } else {
                    confirmPaymentIntent
                }
          }
 }

iOS version

16.x 17.x etc but not related to ios version

Installation method

cocoapods

SDK version

3.2.1

Other information

firmwareVersion: 2.01.00.20-SZZZ_Prod_US_v10-480001 readerSN: STRM26214141499

dver-stripe commented 6 months ago

Hey John,

You're correct that what's happening is that the SDK is transitioning to offline operation between /create and /confirm. Like you mention, this can happen due to a flaky or intermittent network connection; in other cases, like for pi_3OsxcLAPQoqa93xj2JnQu8kA, the SDK retried a payment offline because Stripe's backend returned a transient 500 error. When the SDK comes back online, some of these payments will decline due to insufficent funds, etc.

This auto-transition is intentional behavior and not a bug -- the SDK automatically retries failed confirmations offline even if the PI is created online, so you can complete the transaction even if you lost connectivity immediately after a /create call. If you don't want this to happen, one option is to use the per-transaction offlineBehavior param on collectPaymentMethod to force the SDK to operate fully online/offline. This will prevent transitions mid-transaction, but requires you to manage your own network transitions -- you'll have to write extra code to consume the networkStatus from the SDK and set the param accordingly.

Can you say more about how you would expect network transitions to work? Are you looking for the SDK to retry more often before transitioning to offline mode/never to transition in the middle of a transaction/something else?

jonhcbs commented 6 months ago

yes, we don't want the sdk transit to offline state in this case, because our app is still in online. we expected that if payment intent is created online, then should confirm payment intent online. since even the network is weak or intermittent, still have the possibility to confirm intent online.

In other words, we want the sdk's offline behavior to be in control. for example, we want this payment or payment intent to be created/confirm online, sdk should not transit to offline in the mid of the process automatically. Or even if network is totally down, but user do not want to use offline, sdk should also not go to offline automatically.

we didn't see offlineBehavior param in collectPaymentMethod. can you confirm or share some sample code?

We did see createPaymentIntent have the param, but the problem is we create payment Intent in backend when online, but not via sdk. any way to set offlineBehavior in our case?

- (nullable SCPCancelable *)collectPaymentMethod:(SCPPaymentIntent *)paymentIntent completion:(SCPPaymentIntentCompletionBlock)completion NS_SWIFT_NAME(collectPaymentMethod(_:completion:));

dver-stripe commented 6 months ago

Got it, thanks. Re: offlineBehavior, apologies, that was a typo -- you're correct that offlineBehavior is on the SDK createPaymentIntent method. There isn't an equivalent on collect or confirm.

Unfortunately, we currently don't have an easy workaround for this scenario; we recommend that offline mode users create their PaymentIntents client-side while online and offline. We'll take this as feedback.

Just so I can understand the use case better -- is there a particular reason you prefer to create PIs server-side while online? How does your integration determine whether to create the PaymentIntent through your backend vs the SDK -- do you use the SDK's networkStatus?

jonhcbs commented 6 months ago

that's a historical reason that we create paymentIntent in server-side while online, maybe there is other logic in server-side when create paymentIntent, for example save paymentIntent to server db or some statistics reason etc.

Because our app also have an offline state, if app is offline then create payment intent via sdk, otherwise create online in backend. Not the SDK's networkStatus.

using the SDK's networkStatus is not correct, since maybe it is online when create payment intent, but when confirm it is offline.

dver-stripe commented 6 months ago

Thanks for the insight into your payment state machine and network tracking. We'll evaluate what it would take to add offlineBehavior to collectPaymentMethod or confirmPaymentIntent, but we'll need to assess broader usage patterns to determine the optimal solution shape. Most offline users today create their PaymentIntents client-side, but it's possible we'll see additional signal as our beta progresses.

As a side note -- the possibility of switching to offline operation mid-transaction is true regardless of SDK 3.2.1 vs. 3.3 usage. We recommend you upgrade to prevent the bug we mentioned in our previous communication, where payment forwarding fails with error INTEGRATION_ERROR.SCPErrorForwardingTestModePaymentInLiveMode.

Please let us know if you experience any other rough edges, and apologies we don't have a nearer-term solution for you.

jonhcbs commented 5 months ago

There may be some risk for us to change create paymentIntent to client when online, since there is some logic in backend. Is it possible that when create PI online, we can pass a parameter to stripe such as "offline_behavior": "require_online", so that backend will create a require online PI, and sdk will not confirm this kind of PI offline?

Seems this is another workaround to fix this, but don't know if stripe api or sdk support this?
I didn't see this param in docs. https://docs.stripe.com/api/payment_intents/create, can you help a look? thanks.

dver-stripe commented 5 months ago

Yeah, offlineBehavior is an SDK-only configuration option, unfortunately. It doesn't currently exist on the backend PaymentIntent object, and it can't be specified on the public /create API.

jonhcbs commented 5 months ago

ok, If there is any plan to support this kind of case please tell us. Thanks

jonhcbs commented 5 months ago

Since our user is still complained about this. So just want to confirm what's the default OfflineBehavior for sdk? Maybe the default behavior is SCPOfflineBehaviorPreferOnline from the source code? so the sdk will transit to offline by itself, but not in our control.

Could we set the default OfflineBehavior to SCPOfflineBehaviorRequireOnline? so that sdk will not transit to offline by itself in this case, even the payment intent is created in backend.

Could you help check if could add this config? I think it's not a big change if works.


typedef NS_ENUM(NSInteger, SCPOfflineBehavior) {
    /**
      When network connectivity is available, this PaymentIntent will be transacted online. When network connectivity is unavailable, the PaymentIntent will be transacted offline.
     */
    SCPOfflineBehaviorPreferOnline,
    /**
      The PaymentIntent will only be transacted when network connectivity is available.
     */
    SCPOfflineBehaviorRequireOnline,
    /**
      The PaymentIntent will only be transacted offline, regardless of network connectivity.
     */
    SCPOfflineBehaviorForceOffline
} NS_SWIFT_NAME(OfflineBehavior);

@dver-stripe

dver-stripe commented 5 months ago

Unfortunately, that change would be disruptive to other workflows. You're correct that the current default is PreferOnline, which allows the SDK to automatically switch to offline operation. Changing it to RequireOnline would mean that users who don't explicitly set offlineBehavior (the majority of users) would suddenly have offline mode disabled by default.

Apologies that this continues to be a pain point for you. We'll keep looking into solutions on our side.