stripe / stripe-ios

Stripe iOS SDK
https://stripe.com
MIT License
2.08k stars 976 forks source link

[BUG] iDEAL bank selection scrolls away when trying to select a bank #3607

Closed maxmijn closed 3 months ago

maxmijn commented 3 months ago

https://drive.google.com/file/d/1egP1z9S4fBfmQ4bwqtdGAE_2cLk7qSVD/view?usp=drive_link https://github.com/stripe/stripe-ios/assets/16684728/4d0a4310-7626-4200-bdfe-e7e7e00bde8d

Summary

When trying to select a bank in the iDEAL selection, the selection sheets swipes away as you scroll, resulting in a terrible user experience. EDIT: also happens on the country selection when selecting credit card country.

I included a video of the behaviour.

Code to reproduce

// Config
  var configuration = PaymentSheet.Configuration()
  configuration.apiClient.publishableKey = t.publishableKey
  configuration.merchantDisplayName = "myApp"
  configuration.customer = .init(id: t.customer ?? "", ephemeralKeySecret: t.ephemeralKey ?? "")
  configuration.apiClient.stripeAccount = t.connectedAccountId
  //                STPAPIClient.shared.stripeAccount = t.connectedAccountId
  // Set `allowsDelayedPaymentMethods` to true if your business can handle payment methods
  // that complete payment after a delay, like SEPA Debit and Sofort.
  configuration.allowsDelayedPaymentMethods = false
  configuration.defaultBillingDetails.name = self.me?.name
  configuration.defaultBillingDetails.email = self.me?.emailAddress
  configuration.defaultBillingDetails.phone = self.me?.phoneNumber
  configuration.returnURL = Config.url_scheme + "stripe-redirect"
  configuration.applePay = .init(
    merchantId: "merchant.com.myApp",
    merchantCountryCode: "NL"
  )

  DispatchQueue.main.async {
      self.paymentSheet = PaymentSheet(paymentIntentClientSecret: t.paymentIntent ?? "", configuration: configuration)
  }

// Stripe button
PaymentSheet.PaymentButton(
  paymentSheet: paymentSheet,
  onCompletion: { _ in
      model.loadingStripeButton = true
      DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
          loadTicketOrder()
      })
  }
  ) {
  HStack {
      Spacer()
      Text("proceed_to_payment".localized)
          .button()
          .fixedSize(horizontal: false, vertical: true)
      Spacer()
  }
  .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
  .frame(height: 40)
  .background(Color.primaryDefault)
  .foregroundColor(Color.white)
  .opacity(1)
  .contentShape(Capsule())
  .clipShape(Capsule())
  }

iOS version

Seen on iOS 16 and 17

Installation method

Swift Package Manager

SDK version

23.27.2

yuki-stripe commented 3 months ago

Hello @maxmijn, is there any other part of your code that could be causing this? This looks like interactive dismissal of the keyboard. Perhaps you're using .scrollDismissesKeyboard(.interactively) somewhere?

maxmijn commented 3 months ago

Hi @yuki-stripe. We are not using that anywhere. I have confirmed that when running the SwiftUI payment sheet example, this does not occur, however, when I copy paste the example into my own SwiftUI app it does. This makes me think that it has something to do with the way SwiftUI handles views/scenes. There is no full SwiftUI example in your repository that I can test this with.

Also notice how I have to set configuration.apiClient.publishableKey = publishableKey for it to work. Another thing I noticed is that handleURLCallback always returns false when I use a full SwiftUI implementation.


struct MainView: App {
    @UIApplicationDelegateAdaptor(AppState.self) var appState

    var body: some Scene {
        WindowGroup {
            ExampleSwiftUIPaymentSheet()
             .onOpenURL { url in
                    let stripeHandled = StripeAPI.handleURLCallback(with: url)
                    print(stripeHandled)
                    if (stripeHandled) {
                        return
                    }
                }
        }
    }
}

struct ExampleSwiftUIPaymentSheet: View {
    @ObservedObject var model = MyBackendModel()

    var body: some View {
        VStack {
            if let paymentSheet = model.paymentSheet {
                PaymentSheet.PaymentButton(
                    paymentSheet: paymentSheet,
                    onCompletion: model.onCompletion
                ) {
                    Text("click")
                }
            } else {
                Text("loading")
            }
            if let result = model.paymentResult {
                Text("res")
            }
        }.onAppear { model.preparePaymentSheet() }
    }

}

class MyBackendModel: ObservableObject {
    let backendCheckoutUrl = URL(string: "https://stripe-mobile-payment-sheet.glitch.me/checkout")!  // An example backend endpoint
    @Published var paymentSheet: PaymentSheet?
    @Published var paymentResult: PaymentSheetResult?

    func preparePaymentSheet() {
        // MARK: Fetch the PaymentIntent and Customer information from the backend
        var request = URLRequest(url: backendCheckoutUrl)
        request.httpMethod = "POST"
        let task = URLSession.shared.dataTask(
            with: request,
            completionHandler: { (data, e, a) in
                guard let data = data,
                    let json = try? JSONSerialization.jsonObject(with: data, options: [])
                        as? [String: Any],
                    let customerId = json["customer"] as? String,
                    let customerEphemeralKeySecret = json["ephemeralKey"] as? String,
                    let paymentIntentClientSecret = json["paymentIntent"] as? String,
                    let publishableKey = json["publishableKey"] as? String
                else {
                    // Handle error
                    return
                }
                // MARK: Set your Stripe publishable key - this allows the SDK to make requests to Stripe for your account
                STPAPIClient.shared.publishableKey = publishableKey

                // MARK: Create a PaymentSheet instance
                var configuration = PaymentSheet.Configuration()
                configuration.apiClient.publishableKey = publishableKey
                configuration.merchantDisplayName = "Example, Inc."
                configuration.applePay = .init(
                    merchantId: "merchant.com.stripe.umbrella.test", // Be sure to use your own merchant ID here!
                    merchantCountryCode: "US"
                )
                configuration.customer = .init(
                    id: customerId, ephemeralKeySecret: customerEphemeralKeySecret)
                configuration.returnURL = Config.url_scheme + "stripe-redirect"
                // Set allowsDelayedPaymentMethods to true if your business can handle payment methods that complete payment after a delay, like SEPA Debit and Sofort.
                configuration.allowsDelayedPaymentMethods = true
                DispatchQueue.main.async {
                    self.paymentSheet = PaymentSheet(
                        paymentIntentClientSecret: paymentIntentClientSecret,
                        configuration: configuration)
                }
            })
        task.resume()
    }

    func onCompletion(result: PaymentSheetResult) {
        self.paymentResult = result

        // MARK: Demo cleanup
        if case .completed = result {
            // A PaymentIntent can't be reused after a successful payment. Prepare a new one for the demo.
            self.paymentSheet = nil
            preparePaymentSheet()
        }
    }
}
maxmijn commented 3 months ago

Ah it was indeed a UIScrollView.appearance().keyboardDismissMode = .interactive setting somewhere @yuki-stripe. Still looking into the returnUrl and configuration.apiClient.publishableKey = publishableKey. Think it has something to do with the .shared modules not being shared correctly.

yuki-stripe commented 3 months ago

Ah great, glad to hear that issue is fixed!

On the publishable key issue - The default value of configuration.apiClient is STPAPIClient.shared (src), so both these things should do the same thing:

  1. configuration.apiClient.publishableKey = publishableKey
  2. STPAPIClient.shared.publishableKey = publishableKey

Regarding the returnURL issue - to confirm my understanding, the . onOpenURL closure is being called but StripeAPI.handleURLCallback(with: url) is returning false, is that right? Could you provide the value of the URL?

maxmijn commented 3 months ago

Yes exactly. This is the URL that is returned "bashappbeta://stripe-redirect?payment_intent=pi_3PLNhhI6FPXZGnXb1U3Az0hd&payment_intent_client_secret=pi_3PLNhhI6FPXZGnXb1U3Az0hd_secret_•••xNCM&redirect_status=succeeded"

maxmijn commented 3 months ago

Since calling STPAPIClient.shared.publishableKey = publishableKey is not enough in my application (stripe returns and authentication error) I think the two issues might be related and that somehow the 'shared' environment is not picking up the returnUrl callback as well.

maxmijn commented 3 months ago

I deleted the Stripe package, re-added it and now it works!