Open DevonAllary opened 1 year ago
Hello @DevonAllary,
Thanks for writing in. I'm not yet sure how to fix the issue you've raised, but in the mean time -
I also don't want to generate a paymentIntent when the view loads because I don't know which item the client is purchasing. All the examples I've found through Stripe have the paymentIntent being generated when the view loads.
You can defer the creation of the PaymentIntent and PaymentSheet until the time the client taps your "Pay" button, as I think you're doing in the example code.
Thanks for getting back to me, @yuki-stripe. The code is generating the PaymentIntent and creating the PaymentSheet when the "Buy" button is pressed (I've confirmed this through the debugger and logs). The only issue is actually updating the view. For some reason, the payment sheet isn't rendering to the bottom sheet even though the isPresented binding is set to true and the payment sheet is non-nil. The viewModel.paymentSheet nil check isn't the problem because I can confirm that clause is being rendered after I construct the paymentSheet.
I inspected the PaymentSheet.PaymentButton and it looks like it's just a wrapper around the PaymentSheet that presents when the the button is tapped and the isPresented flag is set to true so I expected my implement to work similarly.
@DevonAllary were you able to resolve this while maintaining the approach ?
This issue persists for me on the latest versions of the Stripe SDK (23.9.0). The hack I'm using is to delay toggling the PaymentSheet's isPresented
binding by a fraction of a second as seen in this screenshot:
If I remove the delay, the payment sheet never appears and Xcode says:
PaymentSheet+SwiftUI.swift:242 Modifying state during view update, this will cause undefined behavior.
Screenshot:
It'd be great to get this sorted out since I worry that my hack may stop working for reasons I don't understand. Happy to provide any additional information that might help.
@JUSTINMKAUFMAN Thanks for this, I will give a try.
Any updates on this?
When I'm using EmptyView()
the sheet is not working. Looks like EmptyView()
does not support sheet modifier or so. Does not look like Stripe SDK bug. This worked for me without any delays
if let paymentSheet = shoppingCart.paymentSheet {
Spacer()
.frame(height: 0)
.paymentSheet(
isPresented: $shoppingCart.isCheckoutReady,
paymentSheet: paymentSheet,
onCompletion: shoppingCart.onCompletion(result:)
)
}
@yuki-stripe would be very handy if the sheet accept optional PaymentSheet?
Noticed same issue. Spacer() or other views did not work, delay did. I also wanted to get rid of the PaymentSheet.PaymentButton for the same reasons as the author (have a button that makes the post call via a ViewModel and returns back the result as an observable and I have this logic common as it is a KMM project). The way Android works is a different in the sense that you do not need to play with a @Published var showPaymentSheet = false and you can just call
paymentSheet.presentWithPaymentIntent(
paymentIntentClientSecret = ...,
configuration = PaymentSheet.Configuration()
)
)
and from my understanding it will start a new activity. Running on 23.16.0
I finally had some time to look into this more - very sorry for the delay here. I see .paymentSheet(..)
has some issues when it's first initialized with isPresented = true
and when it's called on an EmptyView()
.
If anyone's still having problems, could you try the below approach and report back? This lets you call PaymentSheet's present
API directly with a view controller rather than using our .paymentSheet(...)
view modifiers.
/// Adding this to a SwiftUI view causes its `viewController` to be added as a child view controller and able to present.
struct ViewControllerProvider: UIViewControllerRepresentable {
let viewController = UIViewController()
func makeUIViewController(context: Context) -> some UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
init() {}
}
Add an instance of ViewControllerProvider
somewhere in your SwiftUI checkout screen. All it does is get SwiftUI to add its viewController
property as a child view controller, allowing you to present on top of it later.
Present PaymentSheet directly using the ViewControllerProvider's viewController
.
Example:
struct MyCheckoutView: View {
let viewControllerProvider = ViewControllerProvider()
var body: some View {
Button(action: {
// ⭐️ Present PaymentSheet directly
paymentSheet.present(from: paymentSheetPresenter.viewController, completion: model.onCompletion)
}) {
Text("Pay")
}
// ⭐️ Include `viewControllerProvider` somewhere on your checkout screen.
viewControllerProvider
}
}
I have the same issue in present the sheet but they did not present if @all any one have the answer please suggest me what I can do :-
import SwiftUI import Combine import StripePaymentSheet
struct ContentView: View { @ObservedObject var model = MyBackendModel() @State private var selectedAmount: Int? @State private var showAlert = false @State private var isSheetPresented = false @State private var sheet : PaymentSheet?
var body: some View {
VStack {
HStack {
AmountSelectionBox(amount: 39, isSelected: selectedAmount == 39, onTap: { selectAmount(39) })
AmountSelectionBox(amount: 49, isSelected: selectedAmount == 49, onTap: { selectAmount(49) })
AmountSelectionBox(amount: 59, isSelected: selectedAmount == 59, onTap: { selectAmount(59) })
}.padding(.bottom , 100)
Button(action: {
if let selectedAmount = selectedAmount {
model.preparePaymentSheet(createuserdata: PaymentReq(currency: "USD", amount: "\(selectedAmount)00"))
isSheetPresented = true
} else {
showAlert = true
}
}) {
Text("Buy")
}.alert(isPresented: $showAlert) {
Alert(title: Text("Error"), message: Text("Please select the amount."), dismissButton: .default(Text("OK")))
}
if let paymentSheet = model.paymentSheet{
Spacer()
.frame(height: 0)
.paymentSheet(isPresented: $isSheetPresented, paymentSheet: paymentSheet, onCompletion: model.onPaymentCompletion)
}
if let result = model.paymentResult {
switch result {
case .completed:
Text("Payment complete")
case .failed(let error):
Text("Payment failed: \(error.localizedDescription)")
case .canceled:
Text("Payment canceled.")
}
}
}.onAppear {
model.preparePaymentSheet(createuserdata: PaymentReq(currency: "USD", amount: "5800"))
}
}
private func selectAmount(_ amount: Int) {
selectedAmount = amount
}
}
ContentView()
}
// // PaymentIntent.swift // iOSPaymentGatway // // Created by John on 15/11/23. //
import Foundation import SwiftUI import Combine import StripePaymentSheet
class MyBackendModel: ObservableObject {
@Published var paymentSheet : PaymentSheet?
@Published var paymentResult: PaymentSheetResult?
@Published var paymentintent: [String: Any] = [:]
@Published var appResponse: AppResponse = AppResponse()
private var apiService = APIService()
@Published var isLoading = false
var cancellables = Set<AnyCancellable>()
@Published var error: Error?
@Published var client_Secret: String?
@Published var isSelected = false
func preparePaymentSheet(createuserdata: PaymentReq) {
apiService.apiHandler(endpoint: "payment_intents", parameters: createuserdata, method: .post, objectType: createuser.self)
.sink(receiveCompletion: { [weak self] completion in
switch completion {
case .finished:
break
case .failure(let error):
self?.error = error
}
}, receiveValue: { [weak self] res in
if let clientSecret = res.client_secret {
print("Client Secret:", clientSecret)
self?.client_Secret = clientSecret
self?.isSelected = true
self?.makePayment()
} else {
// Handle the case where client_secret is not received
print("Client Secret not received.")
}
})
.store(in: &cancellables)
}
func makePayment() {
STPAPIClient.shared.publishableKey = "pk_test_51OA8V1JYZCpvm4VGYouV8l4KiQFAs7s5*****************************"
var configuration = PaymentSheet.Configuration()
configuration.merchantDisplayName = "Example, Inc."
configuration.allowsDelayedPaymentMethods = true
configuration.applePay = .init(merchantId: "merchant.com.iOSPaymentGatway", merchantCountryCode: "US")
DispatchQueue.main.async {
if let clientttSecret = self.client_Secret {
self.paymentSheet = PaymentSheet(paymentIntentClientSecret: clientttSecret, configuration: configuration)
}
}
}
func onPaymentCompletion(result: PaymentSheetResult) {
self.paymentResult = result
}
}
Any update on this?
Weirdly enough, I got it working when I don't call the payment sheet inside of a navigation stack. If I call it inside of my navigation stack, it freaks out.
Stripe 23.27.2 Xcode 15.4 iOS 17.2
Works for me with code:
`import SwiftUI import StripePaymentSheet struct ContentView: View { @ObservedObject var model = StripeHandler()
@State private var enteredNumber = ""
var enteredNumberFormatted: Double {
return (Double(enteredNumber) ?? 0) / 100
}
var body: some View {
VStack {
Text("Enter the amount")
ZStack(alignment: .center) {
Text("$\(enteredNumberFormatted, specifier: "%.2f")").font(Font.system(size: 30))
TextField("", text: $enteredNumber)
.keyboardType(.numberPad)
.foregroundColor(.clear)
.disableAutocorrection(true)
.accentColor(.clear)
}
Spacer()
if let paymentSheet = model.paymentSheet {
PaymentSheet.PaymentButton(
paymentSheet: paymentSheet,
onCompletion: model.onPaymentCompletion
) {
Text("Buy")
}
}
}
.onAppear {
model.prepareWebhook()
model.preparePaymentSheet()
}
.padding(.horizontal)
.padding(.top, 50)
.padding(.bottom)
}
}`
the problem was gone when I updated to the lateset version 23.29.2
I was getting the same error until I updated to the newest version (23.31.0). Seems to be resolved now. Thank you. 🙏
Summary
I have a list view in SwiftUI with many line items and I want to show a "Pay" button for each row that, when clicked, would generate and present a PaymentSheet bottom view. However, when I create the payment sheet and set it to isPresented, the modal doesn't appear.
I also don't want to generate a paymentIntent when the view loads because I don't know which item the client is purchasing. All the examples I've found through Stripe have the paymentIntent being generated when the view loads.
I've created an example with the basic functionality I'm looking for - the use case in my actual app is slightly different. I have a button that generates a paymentIntent from my server. However, the PaymentSheet isn't being presented.
Code to reproduce
iOS version
16.1
Installation method
SPM
SDK version
22.8.4