stripe / stripe-ios

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

'STPAuthenticaionContext' problem in SwiftUI still unsolved #1829

Closed Pic2490 closed 3 years ago

Pic2490 commented 3 years ago
Thanks for the feedback! We'll see if we can make this less messy.

_Originally posted by @davidme-stripe in https://github.com/stripe/stripe-ios/issues/1204#issuecomment-531884071_

I noticed your team has implemented solutions for when SwiftUI users are integrating your PrebuiltUIs here:

https://stripe.com/docs/payments/accept-a-payment#ios

However, the original poster needed a solution to saving a card with a custom view: https://stripe.com/docs/payments/accept-a-payment?platform=ios&ui=custom

of which I unfortunately can not find any. Out of the 3 tabs, 'Card Element Only' doesn't have a SwiftUI example

I get to the point where:

let paymentHandler = STPPaymentHandler.shared()
//Problems here 
 paymentHandler.confirmSetupIntent(setupIntentParams, with: self){ status, setupIntent, error in
                                    switch (status) {
                                            case .failed:
                                                // Setup failed
                                                print("Setup failed status?")

                                                break
                                            case .canceled:
                                                // Setup canceled
                                                print("Setup canceled status?")

                                                break
                                            case .succeeded:
                                                // Setup succeeded
                                                print("setup sucessex!")
                                                break
                                            @unknown default:
                                                fatalError()
                                                break
                                            }
                                }
                            }

I have this code inside of a

Button(action:{

//Problems here

}){
Text("Testing")
}

I was wondering if you can point me towards the right direction or restore the code that was deleted? Thanks!

davidme-stripe commented 3 years ago

Hello, thanks for filing this! We're still working on some better documentation for our SwiftUI support. For now, here's an example app showing how to build a custom integration with SwiftUI: https://github.com/stripe-samples/accept-a-payment/tree/main/custom-payment-flow/client/ios-swiftui

We don't have an example for SetupIntents at the moment, but the flow is similar to the PaymentIntent example. As shown in https://github.com/stripe-samples/accept-a-payment/blob/main/custom-payment-flow/client/ios-swiftui/AcceptAPayment/Views/Card.swift, you'll want to use a STPPaymentCardTextField.Representable(paymentMethodParams: $paymentMethodParams) SwiftUI view, with a binding to your STPPaymentMethodParams and an isConfirmingSetupIntent Bool. You'll then want to use the .setupIntentConfirmationSheet() ViewModifier to confirm the SetupIntent:

.setupIntentConfirmationSheet(isConfirmingSetupIntent: $isConfirmingSetupIntent,
  setupIntentParams: setupIntent,
  onCompletion: model.onCompletion)

Your Button action can then set isConfirmingSetupIntent to true, which will trigger the SetupIntent confirmation.

Let me know if you'd like additional clarification on any of this!

Pic2490 commented 3 years ago

Hi, I've tried your suggestions but I now have an error with STPPaymentHandlerActionStatus failing

SwiftView (Note: assume card params and client secret are valid)

     Button(action:{

             //Make sure card is valid before saves
              print("isValid: \(self.isValid)")

               if(self.isValid){

                    //Set Stripe Values
                    self.cardField.number = self.cardNumber
                    self.cardField.expMonth = NumberFormatter()
                              .number(from: self.cardMonth )
                     self.cardField.expYear = NumberFormatter()
                                .number(from: self.cardYear )
                      self.cardField.cvc = self.cardCvv

                       print("ccNumber: \(self.cardField.number ?? "Error")")

                       let billingDetails = STPPaymentMethodBillingDetails()
                       let paymentMethodParams =
                            STPPaymentMethodParams(card: self.cardField,
                                        billingDetails: billingDetails,
                                        metadata: nil)

                      print("Testing: \(self.clientSecret)")

                     self.setupIntentParams = STPSetupIntentConfirmParams(clientSecret: self.clientSecret)

                      self.setupIntentParams.paymentMethodParams = paymentMethodParams

                       self.showSetupIntent = true
                       print("paymentStatus: \(self.model.paymentStatus ?? .failed)")

                     }
                    }){
                       ConfirmButton(text: "Save")
                   }
                        .setupIntentConfirmationSheet(isConfirmingSetupIntent: self.$showSetupIntent,
                                                                              setupIntentParams: self.setupIntentParams, 
                                                                              onCompletion: model.onCompletion)

BackendModel

class BackendModel : ObservableObject {

    @Published var paymentStatus: STPPaymentHandlerActionStatus?
    @Published var paymentIntentParams: STPPaymentIntentParams?
    @Published var lastPaymentError: NSError?
    var paymentMethodType: String?
    var currency: String?

    func onCompletion(status: STPPaymentHandlerActionStatus, pi: STPSetupIntent?, error: NSError?) {
        self.paymentStatus = status
        self.lastPaymentError = error

        // MARK: Demo cleanup
        if status == .succeeded {

            print("Status: Success")
            // A PaymentIntent can't be reused after a successful payment. Prepare a new one for the demo.
            self.paymentIntentParams = nil
        }

        if status == .failed{
            print("Status: Failed")
        }

    }
}

When I hit save on my swift view, BackendModel prints("Status: Failed") What should I do from here? Thanks

Pic2490 commented 3 years ago

As of this writing this solution worked for me:

import SwiftUI
import Stripe
import UIKit

struct StripeBuyView: View {
    @EnvironmentObject var viewModel: ViewModel
    @State var isPressed = false
    var body: some View {
        VStack {
            Button(action: {
                isPressed = !isPressed
            }, label: {
                Text("Buy")
            })

            FakeStripeView(isPressed: $isPressed)
                .frame(width: 0, height: 0)
        }
    }
}

struct FakeStripeView: UIViewControllerRepresentable {
    @EnvironmentObject var viewModel: ViewModel
    @Binding var isPressed: Bool

    public typealias UIViewControllerType = FakeStripeViewController

    func makeUIViewController(context: UIViewControllerRepresentableContext<FakeStripeView>) -> FakeStripeViewController {
        let viewController = FakeStripeViewController(viewModel: viewModel)
        return viewController
    }

    func updateUIViewController(_ uiViewController: FakeStripeViewController, context _: UIViewControllerRepresentableContext<FakeStripeView>) {
        if isPressed {
            uiViewController.sendBuyOrder()
        }
    }
}

class FakeStripeViewController: UIViewController {

    var viewModel: ViewModel?

    convenience init() {
        self.init(viewModel: nil)
    }

    init(viewModel: ViewModel?) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    func sendBuyOrder() {
        // Perform Stripe calls with ViewModel data
    }
}

extension FakeStripeViewController: STPAuthenticationContext {
    func authenticationPresentingViewController() -> UIViewController {
        self
    }
}
otymartin commented 2 years ago

@Pic2490 Is this still the solution you are using?

The stripe setup future payment method tutorials don't really address the simple connect use-case of saving a customer's card using SwiftUI custom views and especially handling the STPAuthenticationContext. They are more geared toward store fronts using prebuilt payment flows which is not a connect use case. We build our own payment flow, this is just about adding a card thats it 😞

Pic2490 commented 2 years ago

@Pic2490 Is this still the solution you are using?

The stripe setup future payment method tutorials don't really address the simple connect use-case of saving a customer's card using SwiftUI custom views and especially handling the STPAuthenticationContext. They are more geared toward store fronts using prebuilt payment flows which is not a connect use case. We build our own payment flow, this is just about adding a card thats it 😞

I haven't changed anything since, and it's working fine. If you come across any issues let me know

rromanchuk commented 1 year ago

Is there an example using a swiftui, for the most very base case that is less anti-pattern? Instead of wrapped around delegation methods, is there some sort of singleton i can use to pass stripe whatever it needs from the callback of https://developer.apple.com/documentation/passkit/paywithapplepaybutton

try await StripeApplePay.shared.finished(paymentIntentSecret, whateverYouNeed)

@MainActor
class ViewModel: NSObject, ObservableObject {

     var paymentRequest: PKPaymentRequest = {
         // truncated
         return StripeAPI.paymentRequest(withMerchantIdentifier: config.merchantId, country: "US", currency: "USD")
      }()
 }

struct MyView: View {

    @ObservedObject var model = ViewModel()

    var body: some View {
        VStack {
            PayWithApplePayButton(request: model.paymentRequest) { phase in
                // Give stripe whatever it needs
            } fallback: {

            }
        }
    }
}