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?