Confirm Stripe payment in SwiftUI (iOS 16) - ios

I am using Stripe API to accept card payment in SwiftUI.
I managed to make successful payment using STPPaymentCardTextField wrapped in UIViewRepresentable and confirm it using the provided sheet modifier .paymentConfirmationSheet(....).
The problem appeared when I adopted the new NavigationStack in iOS 16. It appears that the .paymentConfirmationSheet(....) doesn't work properly if it is presented inside the new NavigationStack.
Is there any other way I can confirm card payment in SwiftUI? How can I fix this?
If I switch back to NavigationView it works as expected but I would want to use the new features of NavigationStack.
My demo checkout view and its viewmodel
struct TestCheckout: View {
#ObservedObject var model = TestCheckoutViewModel()
var body: some View {
VStack {
CardField(paymentMethodParams: $model.paymentMethodParams)
Button("Pay") {
model.pay()
}
.buttonStyle(LargeButtonStyle())
.paymentConfirmationSheet(isConfirmingPayment: $model.confirmPayment,
paymentIntentParams: model.paymentIntentParams,
onCompletion: model.onPaymentComplete)
}
}
}
class TestCheckoutViewModel: ObservableObject {
#Published var paymentMethodParams: STPPaymentMethodParams?
#Published var confirmPayment = false
#Published var paymentIntentParams = STPPaymentIntentParams(clientSecret: "")
func pay() {
Task {
do {
// Create dummy payment intent
let paymentIntent = try await StripeManager.shared.getPaymentIntent(orderId: "", amount: 1000)
// Collect card details
let paymentIntentParams = STPPaymentIntentParams(clientSecret: paymentIntent.secret)
paymentIntentParams.paymentMethodParams = paymentMethodParams
// Submit the payment
DispatchQueue.main.async {
self.paymentIntentParams = paymentIntentParams
self.confirmPayment = true
}
} catch {
print(error)
}
}
}
func onPaymentComplete(status: STPPaymentHandlerActionStatus, paymentIntent: STPPaymentIntent?, error: Error?) {
print("Payment completed: \(error)")
}
}
Now this doesn't work
struct TestView: View {
var body: some View {
NavigationStack {
NavigationLink("Checkout", value: "checkout")
.navigationDestination(for: String.self) { string in
if string == "checkout" {
TestCheckout()
}
}
}
}
}
But this does
struct TestView: View {
var body: some View {
TestCheckout()
}
}
The error I get is:
Error Domain=com.stripe.lib Code=50 "There was an unexpected error -- try again in a few seconds" UserInfo={NSLocalizedDescription=There was an unexpected error -- try again in a few seconds, com.stripe.lib:StripeErrorTypeKey=invalid_request_error, com.stripe.lib:StripeErrorCodeKey=payment_intent_unexpected_state, com.stripe.lib:ErrorMessageKey=Nemůžete potvrdit tento Platební záměr, protože v něm chybí platební metoda. Můžete buď aktualizovat Platební záměr s platební metodou a pak jej znovu potvrdit, nebo jej znovu potvrďte přímo s platební metodou.}

Finally I found the problem.
The problem is the #ObservedObject var model = TestCheckoutViewModel().
For some reason it doesn't work with #ObservedObject anymore and it has to be #StateObject.

Related

Changing the property of observableObject from another observableObject

There is a problem of the following nature: it is necessary to create an authorization window for the application, the most logical solution I found the following implementation (I had to do this because the mainView has a tabView which behaves incorrectly if it is in a navigationView)
struct ContentView: View {
#EnvironmentObject var vm: AppSettings
var body: some View {
if vm.isLogin {
MainView()
} else {
LoginView()
}
}
AppSettings looks like this:
struct MyApp: App {
#StateObject var appSetting = AppSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appSetting)
}
}
}
class AppSettings: ObservableObject {
#Published var isLogin = false
}
By default, the user will be presented with an authorization window that looks like this:
struct LoginView: View {
#StateObject var vm = LoginViewModel()
var body: some View {
NavigationView {
VStack {
TextField("Email", text: $vm.login)
TextField("Password", text: $vm.password)
Button {
vm.auth()
} label: {
Text("SignIn")
}
}
}
}
}
And finally the loginViewModel looks like this:
class LoginViewModel: ObservableObject {
#Published var login = ""
#Published var password = ""
//#Published var appSettings = AppSettings() -- error on the first screenshot
//or
//#EnvironmentObject var appSettings: AppSettings -- error on the second screenshot
func auth() {
UserAPI().Auth(req: LoginRequest(email: login, password: password)) { response, error in
if let err = error {
// Error Processing
} else if let response = response {
Defaults.accessToken = response.tokens.accessToken
Defaults.refreshToken = response.tokens.refreshToken
self.appSettings.isLogin = true
}
}
}
}
1 error - Accessing StateObject's object without being installed on a View. This will create a new instance each time
2 error - No ObservableObject of type AppSettings found. A View.environmentObject(_:) for AppSettings may be missing as an ancestor of this view
I ask for help, I just can not find a way for the interaction of two observableObject. I had to insert all the logic into the action of the button to implement such functionality
In addition to this functionality, it is planned to implement an exit from the account by changing the isLogin variable to false in various cases or use other environment variables to easily implement other functions
The example is deliberately simplified for an easy explanation of the situation
I would think using only LoginViewModel at top level would be the easiest way to solve this. But if you want to keep both, you can synchronise them with an .onChanged modifier.
class LoginViewModel: ObservableObject {
#Published var login = ""
#Published var password = ""
#Published var isLogin = false
func auth() {
UserAPI().Auth(req: LoginRequest(email: login, password: password)) { response, error in
if let err = error {
// Error Processing
} else if let response = response {
Defaults.accessToken = response.tokens.accessToken
Defaults.refreshToken = response.tokens.refreshToken
isLogin = true
}
}
}
}
struct LoginView: View {
#StateObject var vm = LoginViewModel()
// grab the AppSettings from the environment
#EnvironmentObject var appSetting: AppSettings
var body: some View {
NavigationView {
VStack {
TextField("Email", text: $vm.login)
TextField("Password", text: $vm.password)
Button {
vm.auth()
} label: {
Text("SignIn")
}
// synchronise the viewmodels here
.onChange(of: vm.isLogin) { newValue in
appSetting.isLogin = newValue
}
}
}
}
}

Failing to send email

I have a button that allows you to send an email with all the content on your app.
I'm iterating thru all the data stored in a core data container, and creating a string that I then pass to the sheet presenting the ability to send email.
When I test it, the string always seems to be empty, and I can see an error: [PPT] Error creating the CFMessagePort needed to communicate with PPT.
I'm using the same mechanism I use to email each item on the list, which works like a charm.
Anyway, I've see a lot of posts about the error, but nothing that points to a solution.
Here's the code, maybe it's related to how I call the .sheet? What am I missing? What's that error even try to tell me?
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: Jot.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Jot.date, ascending: false)])
var jots: FetchedResults<Jot>
#State private var sheetbackupJotMail = false
//for sending mail
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
#State private var emailText: String = ""
var body: some View {
NavigationView {
List (jots) { jot in
Text(jot.text!)
}
}
.toolbar {
// toolbar button that send the message with all content
ToolbarItem(placement: .navigation) {
if MFMailComposeViewController.canSendMail() {
Button(action: {
sheetbackupJotMail.toggle()
}) {
Label("Back up all jots", systemImage: "arrow.up.square").foregroundColor(.secondary)
}
// sheet for backing up email
.sheet(isPresented: $sheetbackupJotMail) {
MailView(result: $result) { composer in
emailText = ""
for jot in jots {
emailText = emailText + jot.dateText! + "\n" + jot.text! + "\n\n"
}
print(">>>: " + emailText) //<-- this is always empty, however if I move to the button before the toggle(), I get the right text
// emailing all
composer.setSubject("Jot Backup")
composer.setMessageBody(emailText, isHTML: false)
}
}
}
}
}
}
}
// mail view
import SwiftUI
import UIKit
import MessageUI
public struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
public var configure: ((MFMailComposeViewController) -> Void)?
public class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
public func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
public func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
configure?(vc)
return vc
}
public func updateUIViewController(
_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
Also moving the creation of the text here caused the text I need to mail to be ok, but the error continues to be: PPT] Error creating the CFMessagePort needed to communicate with PPT.
Button(action: {
emailText = ""
for jot in jots {
emailText = emailText + jot.dateText! + "\n" + jot.text! + "\n\n"
}
print(">>>: " + emailText)
sheetbackupJotMail.toggle()
}) {
Label("Back up all jots", systemImage: "arrow.up.square").foregroundColor(.secondary)
}
The sheet content is computed only once on creation time, so all later changes in dependencies are not re-injected, so you have to put everything dependent by binding inside MailView and do composing there.
I.e. your sheet should look like (sketch)
.sheet(isPresented: $sheetbackupJotMail) {
MailView(result: $result, emailText: $emailText, jots: $jots)
}
*Many dependencies are missing, so it is possible to provide testable solution, but the idea should be clear. See also https://stackoverflow.com/a/64554083/12299030 - it demos solution for similar issue.

What is wrong with this simple SwiftUI example? [duplicate]

I am using .sheet view in SwiftUI and I am observing a strange behavior in the execution of the code.
I am having a view SignInView2:
struct SignInView2: View {
#Environment(\.presentationMode) var presentationMode
#State var invitationUrl = URL(string: "www")
#State private var showingSheet = false
var body: some View {
VStack {
Text("Share Screen")
Button(action: {
print("link: \(invitationUrl)") // Here I see the new value assigned from createLink()
self.showingSheet = true
}) {
Text("Share")
}
.sheet(isPresented: $showingSheet) {
let invitationLink = invitationUrl?.absoluteString // Paasing the old value (www)
ActivityView(activityItems: [NSURL(string: invitationLink!)] as [Any], applicationActivities: nil)
}
}
.onAppear() {
createLink()
}
}
}
which calls create a link method when it appears:
extension SignInView2 {
func createLink() {
guard let uid = Auth.auth().currentUser?.uid else {
print("tuk0")
return }
let link = URL(string: "https://www.example.com/?invitedby=\(uid)")
print("tuk1:\(String(describing: link))")
let referralLink = DynamicLinkComponents(link: link!, domainURIPrefix: "https://makeitso.page.link")
print("tuk2:\(String(describing: referralLink))")
referralLink?.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.IVANDOS.ToDoFirebase")
referralLink?.iOSParameters?.minimumAppVersion = "1.0"
referralLink?.iOSParameters?.appStoreID = "13129650"
referralLink?.shorten { (shortURL, warnings, error) in
if let error = error {
print(error.localizedDescription)
return
}
print("tuk4: \(shortURL)")
self.invitationUrl = shortURL!
}
}
}
That method assigns a value to the invitationUrl variable, which is passed to the sheet. Unfortunatelly, when the sheet appears, I don't see the newly assigned variable but I see only "www". Can you explain me how to pass the new value generated from createLink()?
This is known behaviour of sheet since SwiftUI 2.0. Content is created in time of sheet created not in time of showing. So the solution can be either to use .sheet(item:... modifier or passing binding in sheet content view (which is kind of reference to state storage and don't need to be updated).
Here is a demo of possible approach. Prepared with Xcode 12.4.
struct SignInView2: View {
#Environment(\.presentationMode) var presentationMode
#State private var invitationUrl: URL? // by default is absent
var body: some View {
VStack {
Text("Share Screen")
Button(action: {
print("link: \(invitationUrl)")
self.invitationUrl = createLink() // assignment activates sheet
}) {
Text("Share")
}
.sheet(item: $invitationUrl) {
ActivityView(activityItems: [$0] as [Any], applicationActivities: nil)
}
}
}
}
// Needed to be used as sheet item
extension URL: Identifiable {
public var id: String { self.absoluteString }
}

.sheet in SwiftUI strange behaviour when passing a variable

I am using .sheet view in SwiftUI and I am observing a strange behavior in the execution of the code.
I am having a view SignInView2:
struct SignInView2: View {
#Environment(\.presentationMode) var presentationMode
#State var invitationUrl = URL(string: "www")
#State private var showingSheet = false
var body: some View {
VStack {
Text("Share Screen")
Button(action: {
print("link: \(invitationUrl)") // Here I see the new value assigned from createLink()
self.showingSheet = true
}) {
Text("Share")
}
.sheet(isPresented: $showingSheet) {
let invitationLink = invitationUrl?.absoluteString // Paasing the old value (www)
ActivityView(activityItems: [NSURL(string: invitationLink!)] as [Any], applicationActivities: nil)
}
}
.onAppear() {
createLink()
}
}
}
which calls create a link method when it appears:
extension SignInView2 {
func createLink() {
guard let uid = Auth.auth().currentUser?.uid else {
print("tuk0")
return }
let link = URL(string: "https://www.example.com/?invitedby=\(uid)")
print("tuk1:\(String(describing: link))")
let referralLink = DynamicLinkComponents(link: link!, domainURIPrefix: "https://makeitso.page.link")
print("tuk2:\(String(describing: referralLink))")
referralLink?.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.IVANDOS.ToDoFirebase")
referralLink?.iOSParameters?.minimumAppVersion = "1.0"
referralLink?.iOSParameters?.appStoreID = "13129650"
referralLink?.shorten { (shortURL, warnings, error) in
if let error = error {
print(error.localizedDescription)
return
}
print("tuk4: \(shortURL)")
self.invitationUrl = shortURL!
}
}
}
That method assigns a value to the invitationUrl variable, which is passed to the sheet. Unfortunatelly, when the sheet appears, I don't see the newly assigned variable but I see only "www". Can you explain me how to pass the new value generated from createLink()?
This is known behaviour of sheet since SwiftUI 2.0. Content is created in time of sheet created not in time of showing. So the solution can be either to use .sheet(item:... modifier or passing binding in sheet content view (which is kind of reference to state storage and don't need to be updated).
Here is a demo of possible approach. Prepared with Xcode 12.4.
struct SignInView2: View {
#Environment(\.presentationMode) var presentationMode
#State private var invitationUrl: URL? // by default is absent
var body: some View {
VStack {
Text("Share Screen")
Button(action: {
print("link: \(invitationUrl)")
self.invitationUrl = createLink() // assignment activates sheet
}) {
Text("Share")
}
.sheet(item: $invitationUrl) {
ActivityView(activityItems: [$0] as [Any], applicationActivities: nil)
}
}
}
}
// Needed to be used as sheet item
extension URL: Identifiable {
public var id: String { self.absoluteString }
}

SwiftUI #State not triggering update when value changed by separate method

I have this (simpilied) section of code for a SwiftUI display:
struct ContentView: View {
private var errorMessage: String?
#State private var showErrors: Bool = false
var errorAlert: Alert {
Alert(title: Text("Error!"),
message: Text(errorMessage ?? "oops!"),
dismissButton: .default(Text("Ok")))
}
init() {}
var body: some View {
VStack {
Text("Hello, World!")
Button(action: {
self.showErrors.toggle()
}) {
Text("Do it!")
}
}
.alert(isPresented: $showErrors) { errorAlert }
}
mutating func display(errors: [String]) {
errorMessage = errors.joined(separator: "\n")
showErrors.toggle()
}
}
When the view is displayed and I tape the "Do it!" button then the alert is displayed as expected.
However if I call the display(errors:...) function the error message is set, but the display does not put up an alert.
I'm guessing this is something to do with the button being inside the view and the function being outside, but I'm at a loss as to how to fix it. It should be easy considering the amount of functionality that any app would have that needs to update a display like this.
Ok, some more reading and a refactor switched to using an observable view model like this:
class ContentViewModel: ObservableObject {
var message: String? = nil {
didSet {
displayMessage = message != nil
}
}
#Published var displayMessage: Bool = false
}
struct ContentView: View {
#ObservedObject private var viewModel: ContentViewModel
var errorAlert: Alert {
Alert(title: Text("Error!"), message: Text(viewModel.message ?? "oops!"), dismissButton: .default(Text("Ok")))
}
init(viewModel: ContentViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
Button(action: {
self.viewModel.displayMessage.toggle()
}) {
Text("Do it!")
}
}
.alert(isPresented: $viewModel.displayMessage) { errorAlert }
}
}
Which is now working as expected. So the takeaway from this is that using observable view models is more useful even in simpler code like this.

Resources