Stripe Error: "Should not handle multiple payments at once."

Viewed 179

I am trying to confirm a setupintent generated by my backend api but i think it tries to submit the setup intent more than once. The method below is used to add a new card to the User by using the Stripe iOS SDK(Which is unfortunately made for uikit). I adapted the example methods and wrapped them in some uiviewrepresentables so I can have the 3DS popups if neccesary. The problem is that it gets verified in Stripe. The SetupIntent appears in logs but the app crashes. I will share my code below for other people that may be struggling with implementing Stripe SDK on Swiftui.

This is the piece of code where i retrieve and confirm the setupintent with the STPPaymentMethod params:

struct FakeStripeView: UIViewControllerRepresentable {
    @EnvironmentObject var navView : NavView
    @Binding var cardParams : STPPaymentMethodCardParams
    @Binding var isActive : Bool
    @Binding var billing : STPPaymentMethodBillingDetails
    
    public typealias UIViewControllerType = FakeStripeViewController
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<FakeStripeView>) -> FakeStripeViewController {
        let viewController = FakeStripeViewController(navView: navView)
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: FakeStripeViewController, context _: UIViewControllerRepresentableContext<FakeStripeView>) {
        if navView.isActive{
            uiViewController.createSetupIntent(cardParams: cardParams, billing: billing, completion: { state in
                if state != false{
                    self.navView.isActive = false
                    self.navView.addNewCard = false
                }
            })
        }
    }
}

class FakeStripeViewController: UIViewController {
    
    var navView: NavView?
        
        convenience init() {
            self.init(navView: nil)
        }
        
        init(navView: NavView?) {
            self.navView = navView
            super.init(nibName: nil, bundle: nil)
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    }
    
    func createSetupIntent(cardParams: STPPaymentMethodCardParams, billing: STPPaymentMethodBillingDetails, completion: @escaping (Bool) -> ()) {
        StripeAPIClient.shared.createSetupIntent(completion: { (setupIntent) in
            if(setupIntent.status != "not completed"){
                if(setupIntent.status.contains("error:")){
                    print(setupIntent.status)
                }
                else{
                    let paymentMethodParams = STPPaymentMethodParams(card: cardParams, billingDetails: billing, metadata: nil)
                    let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: setupIntent.client_secret)
                    setupIntentParams.paymentMethodParams = paymentMethodParams
                    
                    let paymentHandler = STPPaymentHandler.shared()
                    paymentHandler.confirmSetupIntent(setupIntentParams, with: self) { status, setupIntent, error in
                                switch (status) {
                                case .failed:
                                    // Setup failed
                                    break
                                case .canceled:
                                    // Setup canceled
                                    break
                                case .succeeded:
                                    print("successful")
                                    StripeAPIClient.shared.retrievePaymentMethods()
                                    completion(true)
                                    self.navView?.isActive = false
                                    self.navView?.addNewCard = false
                                    break
                                @unknown default:
                                    fatalError()
                                    break
                                }
                            }
                        }
                    }
                })
            }
        }

extension FakeStripeViewController: STPAuthenticationContext {
    func authenticationPresentingViewController() -> UIViewController {
        self
    }
}

Here is a wrapper for the card field:

struct StripePaymentCardTextField: UIViewRepresentable {
    
    @Binding var cardParams: STPPaymentMethodCardParams
    @Binding var isValid: Bool
    @Binding var billing : STPPaymentMethodBillingDetails
    
    func makeUIView(context: Context) -> STPPaymentCardTextField {
        let input = STPPaymentCardTextField()
        input.borderColor = getColorFromHex(hex: 0x7653DB, alpha: 1.0)
        input.borderWidth = 5
        input.cornerRadius = 15
        input.delegate = context.coordinator
        return input
    }
    
    func makeCoordinator() -> StripePaymentCardTextField.Coordinator { Coordinator(self) }

    func updateUIView(_ view: STPPaymentCardTextField, context: Context) { }
    
    class Coordinator: NSObject, STPPaymentCardTextFieldDelegate {

        var parent: StripePaymentCardTextField
        
        init(_ textField: StripePaymentCardTextField) {
            parent = textField
            parent.billing.email = UserData.shared.user?.email
            parent.billing.name = UserData.shared.user?.name
            parent.billing.phone = UserData.shared.user?.phone
        }
        
        func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) {
            parent.cardParams = textField.cardParams
            parent.isValid = textField.isValid
            parent.billing.address?.postalCode = textField.postalCode
        }
    }
}

This is the view for adding the payment method. In this view the fakestripeview is implemented so we can have the 3ds pop-up if needed.

struct AddPaymentMethod: View {
    @State var cardParams : STPPaymentMethodCardParams = STPPaymentMethodCardParams()
    @State var billingParams : STPPaymentMethodBillingDetails = STPPaymentMethodBillingDetails()
    @State var isValid = false
    @EnvironmentObject var navView : NavView
    
    @State var showAlert = false
    var body: some View {
        VStack(alignment: .center){
            CCView(cardParams: $cardParams)
                .padding(.leading,10)
                .frame(maxWidth: .infinity, alignment: .center)
                .frame(height: UIScreen.main.bounds.width/2)
                .padding(.bottom)
            
            
                
            
            StripePaymentCardTextField(cardParams: $cardParams, isValid: $isValid, billing: $billingParams)
                .frame(height:70)
                .padding()

            Button(action: {
                if(isValid){
                    navView.isActive = true
                }
                else{
                    self.showAlert = true
                }
                
            }, label: {
                Text("Submit")
                    .foregroundColor(.white)
                    .frame(height: 50)
                    .frame(maxWidth: .infinity)
                    .background(beatactViolet)
                    .modifier(EventCardModifier())
                    .padding()
                    
            })
            Spacer()
            FakeStripeView(cardParams: $cardParams, isActive: $navView.isActive, billing: $billingParams)
                .frame(width: 0, height: 0).environmentObject(NavView.shared)
        }.alert(isPresented: $showAlert, content: {
            Alert(title: Text("Card/Adress Invalid"), message: Text("Please try again."), dismissButton: .default(Text("OK!")))
        }).navigationTitle("Add card")
    }
}

I think the main problem of this approach is the interfacing between the methods of the fakestripeview and the AddPaymentMethod View. Please feel free to use any of this code if it helps you in any way. I know it took me a while to wrap my head around a viable way to do this. This approach works but after the setupintent is confirm it crashes the app(but at least it shows up in stripe). Any clues about how i might be able to improve this and make it working?

2 Answers

For anyone interested in this:

struct FakeStripeView: UIViewControllerRepresentable {
    @EnvironmentObject var navView : NavView
    @Binding var cardParams : STPPaymentMethodCardParams
    @Binding var isActive : Bool
    @Binding var billing : STPPaymentMethodBillingDetails
    
    public typealias UIViewControllerType = FakeStripeViewController
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<FakeStripeView>) -> FakeStripeViewController {
        let viewController = FakeStripeViewController.shared
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: FakeStripeViewController, context _: UIViewControllerRepresentableContext<FakeStripeView>) {}
}

class FakeStripeViewController: UIViewController {
    
    static let shared = FakeStripeViewController()
    var navView: NavView?
        
        convenience init() {
            self.init(navView: nil)
        }
        
        init(navView: NavView?) {
            self.navView = navView
            super.init(nibName: nil, bundle: nil)
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    }
    
    func createSetupIntent(cardParams: STPPaymentMethodCardParams, billing: STPPaymentMethodBillingDetails, completion: @escaping (Bool) -> ()) {
        StripeAPIClient.shared.createSetupIntent(completion: { (setupIntent) in
            if(setupIntent.status != "not completed"){
                if(setupIntent.status.contains("error:")){
                    print(setupIntent.status)
                }
                else{
                    let paymentMethodParams = STPPaymentMethodParams(card: cardParams, billingDetails: billing, metadata: nil)
                    let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: setupIntent.client_secret)
                    setupIntentParams.paymentMethodParams = paymentMethodParams
                    
                    let paymentHandler = STPPaymentHandler.shared()
                    paymentHandler.confirmSetupIntent(setupIntentParams, with: self) { status, setupIntent, error in
                                switch (status) {
                                case .failed:
                                    // Setup failed
                                    break
                                case .canceled:
                                    // Setup canceled
                                    break
                                case .succeeded:
                                    print("successful")
                                    StripeAPIClient.shared.retrievePaymentMethods()
                                    completion(true)
                                    self.navView?.isActive = false
                                    self.navView?.addNewCard = false
                                    break
                                @unknown default:
                                    fatalError()
                                    break
                                }
                            }
                        }
                    }
                })
            }
        }

extension FakeStripeViewController: STPAuthenticationContext {
    func authenticationPresentingViewController() -> UIViewController {
        self
    }
}

I added a shared instance of the class that I can call within swiftui views. Such as this:


struct AddPaymentMethod: View {
    @State var cardParams : STPPaymentMethodCardParams = STPPaymentMethodCardParams()
    @State var billingParams : STPPaymentMethodBillingDetails = STPPaymentMethodBillingDetails()
    @State var isValid = false
    @EnvironmentObject var navView : NavView
    
    
    @State var showAlert = false
    var body: some View {
        VStack(alignment: .center){
            CCView(cardParams: $cardParams)
                .padding(.leading,10)
                .frame(maxWidth: .infinity, alignment: .center)
                .frame(height: UIScreen.main.bounds.width/2)
                .padding(.bottom)
            
            
                
            
            StripePaymentCardTextField(cardParams: $cardParams, isValid: $isValid, billing: $billingParams)
                .frame(height:70)
                .padding()

            Button(action: {
                if(isValid){
                    FakeStripeViewController.shared.createSetupIntent(cardParams: cardParams, billing: billingParams, completion: { state in
                        if(state){
                            navView.addNewCard = false
                        }
                    })
                }
                else{
                    self.showAlert = true
                }
                
            }, label: {
                Text("Submit")
                    .foregroundColor(.white)
                    .frame(height: 50)
                    .frame(maxWidth: .infinity)
                    .background(beatactViolet)
                    .modifier(EventCardModifier())
                    .padding()
                    
            })
            Spacer()
            FakeStripeView(cardParams: $cardParams, isActive: $navView.isActive, billing: $billingParams)
                .frame(width: 0, height: 0).environmentObject(NavView.shared)
        }.alert(isPresented: $showAlert, content: {
            Alert(title: Text("Card/Adress Invalid"), message: Text("Please try again."), dismissButton: .default(Text("OK!")))
        }).navigationTitle("Add card")
    }
}

Related