I am trying to implement Stripe Add Payment Method in SwiftUI. So a user can add a payment method or select from listed. After many days of searching I was able to implement a working PaymentOptionsView. However, when add new card is clicked it does not display the STPAddCardViewController to enter new payment methods
Here is the code that display the payment option
import Foundation
import SwiftUI
import Stripe
struct PaymentOptionsView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, STPPaymentOptionsViewControllerDelegate {
var control: PaymentOptionsView
init(_ control: PaymentOptionsView) {
self.control = control
}
// Implement required delegate methods here:
func paymentOptionsViewControllerDidCancel(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewControllerDidFinish(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didFailToLoadWithError error: Error) {
}
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PaymentOptionsView>) -> STPPaymentOptionsViewController {
let config = STPPaymentConfiguration()
// config.requiredBillingAddressFields = .none
config.appleMerchantIdentifier = "dummy-merchant-id"
return STPPaymentOptionsViewController(configuration: config, theme: STPTheme(), apiAdapter: STPCustomerContext(keyProvider: MyAPIClient()), delegate: context.coordinator)
}
func updateUIViewController(_ uiViewController: STPPaymentOptionsViewController, context: UIViewControllerRepresentableContext<PaymentOptionsView>) { }
}
and here is APIClient
class MyAPIClient: NSObject, STPCustomerEphemeralKeyProvider {
func createCustomerKey(withAPIVersion apiVersion: String, completion: #escaping STPJSONResponseCompletionBlock) {
let customerId = "cus_LoK4MNElrbzeVg"
let url = "https://us-central1-test-mmmm.cloudfunctions.net/app/createEphemeralKey"
AF.request(url, method: .post, parameters: [
"api_version": "2020-08-27",
"customerId": customerId,
])
.validate(statusCode: 200..<300)
.responseJSON { responseJSON in
switch responseJSON.result {
case .success(let json):
completion(json as? [String: AnyObject], nil)
case .failure(let error):
print("Error creating customer Key (retrieving ephemeral key) with Alamofire. See: MyAPIClient - func createCustomerKey")
completion(nil, error)
}
}
}
}
What am I doing wrong and How do I implement the add new card method?
This code actually works fine. I was initially presenting PaymentOptionsView through .sheet(isPresented: so it does not allowing transition to a new view when the add card button is pressed. When the PaymentOptionsView is presented as
.background( // add a hidden `NavigationLink` in the background
NavigationLink(destination: PaymentOptionsView(), isActive: $showPaymentSheet) {EmptyView()}
.hidden()
)
everythings works. when the add new card button is pressed, the view transition to STPAddCardViewController perfectly.
Related
I have been having a hard time trying to figure out how to authenticate with the Box API using SwiftUI.
As far as I understand, SwiftUI does not currently have the ability to satisfy the ASWebAuthenticationPresentationContextProviding protocol required to show the Safari OAuth2 login sheet. I know that I can make a UIViewControllerRepresentable to use UIKit within SwiftUI, but I can't get this to work.
I have figured out how to get the OAuth2 login sheet for Dropbox to appear and authenticate the client using SwiftUI.
The trick is to use a Coordinator to make the UIViewControllerRepresentable satisfy a protocol.
import SwiftUI
import BoxSDK
import AuthenticationServices
var boxSDK = BoxSDK(clientId: "<Client ID>", clientSecret: "<Client Secret>")
var boxClient: BoxClient
struct BoxLoginView: View {
#State var showLogin = false
var body: some View {
VStack {
Button {
showLogin = true
} label: {
Text("Login")
}
BoxView(isShown: $showLogin)
// Arbitrary frame size so that this view does not take up the whole screen
.frame(width: 40, height: 40)
}
}
}
/// A UIViewController that will present the OAuth2 Safari login screen when the isShown is true.
struct BoxView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
let letsView = UIViewController()
#Binding var isShown : Bool
// Show the login Safari window when isShown
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if(isShown) {
getOAuthClient()
}
}
func makeUIViewController(context _: Self.Context) -> UIViewController {
return self.letsView
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
func getOAuthClient() {
boxSDK.getOAuth2Client(tokenStore: KeychainTokenStore(), context:self.makeCoordinator()) { result in
switch result {
case let .success(client):
boxClient = client
case let .failure(error):
print("error in getOAuth2Client: \(error)")
}
}
}
class Coordinator: NSObject, ASWebAuthenticationPresentationContextProviding {
var parent: BoxView
init(parent: BoxView) {
self.parent = parent
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return parent.letsView.view.window ?? ASPresentationAnchor()
}
}
}
I am going through the AppKit tutorial Supporting Drag and Drop Through File Promises. I downloaded the demo app.
I tried to extract the NSFilePromiseProviderDelegate functionality from the ImageCanvasController class (which is an NSViewController) into a separate class. (Please see before & after code snippets below.)
Before my changes, dragging an image from the app canvas out into Finder and Apple Notes worked fine. But after my changes, nothing happens when I drag into Notes, and I get this error when I drag into Finder:
2022-02-26 23:16:52.713742+0100 MemeGenerator[31536:1975798] *** CFMessagePort: dropping corrupt reply Mach message (0b000100)
Is there any undocumented protocol conformance that I need to add to the new class? Or is there some underlying logic for which NSFilePromiseProviderDelegate only works if it's also an NSView or an NSViewController? In all the guides I found online, it is always tied to a view, but I didn't find any warning that it has to be.
Note:
The reason I want to separate the promise-provider functionality from views is, this way I could provide multiple NSDraggingItem objects to beginDraggingSession. For example, when multiple items are selected, and there is a mouseDragged event on one of them, I could start a dragging session including all the selected items.
Code before
class ImageCanvasController: NSViewController, NSFilePromiseProviderDelegate, ImageCanvasDelegate, NSToolbarDelegate {
...
/// Queue used for reading and writing file promises.
private lazy var workQueue: OperationQueue = {
let providerQueue = OperationQueue()
providerQueue.qualityOfService = .userInitiated
return providerQueue
}()
...
func pasteboardWriter(forImageCanvas imageCanvas: ImageCanvas) -> NSPasteboardWriting {
let provider = NSFilePromiseProvider(fileType: kUTTypeJPEG as String, delegate: self)
provider.userInfo = imageCanvas.snapshotItem
return provider
}
// MARK: - NSFilePromiseProviderDelegate
/// - Tag: ProvideFileName
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
let droppedFileName = NSLocalizedString("DropFileTitle", comment: "")
return droppedFileName + ".jpg"
}
/// - Tag: ProvideOperationQueue
func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
return workQueue
}
/// - Tag: PerformFileWriting
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
do {
if let snapshot = filePromiseProvider.userInfo as? ImageCanvas.SnapshotItem {
try snapshot.jpegRepresentation?.write(to: url)
} else {
throw RuntimeError.unavailableSnapshot
}
completionHandler(nil)
} catch let error {
completionHandler(error)
}
}
}
Code after
class CustomFilePromiseProviderDelegate: NSObject, NSFilePromiseProviderDelegate {
/// Queue used for reading and writing file promises.
private lazy var workQueue: OperationQueue = {
let providerQueue = OperationQueue()
providerQueue.qualityOfService = .userInitiated
return providerQueue
}()
/// - Tag: ProvideFileName
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
let droppedFileName = NSLocalizedString("DropFileTitle", comment: "")
return droppedFileName + ".jpg"
}
/// - Tag: ProvideOperationQueue
func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
return workQueue
}
/// - Tag: PerformFileWriting
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
do {
if let snapshot = filePromiseProvider.userInfo as? ImageCanvas.SnapshotItem {
try snapshot.jpegRepresentation?.write(to: url)
} else {
throw RuntimeError.unavailableSnapshot
}
completionHandler(nil)
} catch let error {
completionHandler(error)
}
}
}
class ImageCanvasController: NSViewController, ImageCanvasDelegate, NSToolbarDelegate {
...
func pasteboardWriter(forImageCanvas imageCanvas: ImageCanvas) -> NSPasteboardWriting {
let delegate = CustomFilePromiseProviderDelegate()
let provider = NSFilePromiseProvider(fileType: kUTTypeJPEG as String, delegate: delegate)
provider.userInfo = imageCanvas.snapshotItem
return provider
}
}
Does an NSFilePromiseProviderDelegate need to be also an NSView or NSViewController?
No.
delegate is a local variable and is released at the end of pasteboardWriter(forImageCanvas:). Without a delegate the file promise provider doesn't work. Solution: keep a strong reference to the delegate.
class ImageCanvasController: NSViewController, ImageCanvasDelegate, NSToolbarDelegate {
let delegate = CustomFilePromiseProviderDelegate()
...
func pasteboardWriter(forImageCanvas imageCanvas: ImageCanvas) -> NSPasteboardWriting {
let provider = NSFilePromiseProvider(fileType: kUTTypeJPEG as String, delegate: delegate)
provider.userInfo = imageCanvas.snapshotItem
return provider
}
}
I want to complete the addition of new payment method that I implemented in this SO question. This save and reuse stripe tutorial shows that cardParams and billingDetails need to be passed to STPPaymentMethodParams. How to I get the cardParams and billingDetails from STPPaymentOptionsViewController?
This is the code that produce the STPPaymentOptionsViewController and then display the STPAddCardViewController when add card button is pressed.
import Foundation
import SwiftUI
import Stripe
struct PaymentOptionsView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, STPPaymentOptionsViewControllerDelegate {
var control: PaymentOptionsView
init(_ control: PaymentOptionsView) {
self.control = control
}
// Implement required delegate methods here:
func paymentOptionsViewControllerDidCancel(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewControllerDidFinish(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didFailToLoadWithError error: Error) {
}
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PaymentOptionsView>) -> STPPaymentOptionsViewController {
let config = STPPaymentConfiguration()
// config.requiredBillingAddressFields = .none
config.appleMerchantIdentifier = "dummy-merchant-id"
return STPPaymentOptionsViewController(configuration: config, theme: STPTheme(), apiAdapter: STPCustomerContext(keyProvider: MyAPIClient()), delegate: context.coordinator)
}
func updateUIViewController(_ uiViewController: STPPaymentOptionsViewController, context: UIViewControllerRepresentableContext<PaymentOptionsView>) { }
}
STPPaymentOptionsViewController by design only returns you a PaymentMethod and not the raw card details.
Since the STPPaymentOptionsViewControllerDelegate returns you a STPPaymentOption, you can cast it to a STPPaymentMethod and then get the billingDetails for that PaymentMethod.
In your didSelect() delegate method, you can cast it like:
if let paymentMethod = paymentOption as? STPPaymentMethod {
print(paymentMethod.stripeId)
}
https://stripe.dev/stripe-ios/docs/Classes/STPPaymentMethod.html#/c:#M#Stripe#objc(cs)STPPaymentMethod(py)billingDetails
I'm using Coordinator (conforms to LoginButtonDelegate) object to receive user data (profile, name) when user completes authorisation via FB Login Button.
Coordinator().userId property is updated with user ID but i need to pass it 1 level up to the LoginView and to update EnvironmentObject called thisSession (make thisSession.userId = Coordinator().userId somehow).
Is there any way to do that? I tried playing with ObservableObject/Published properties but i can't update parent object's properties from Coordinator.
Another idea is to subscribe to Auth.auth() changes but it seems too complicated and a bit "old school" solution. There show be some easy way i am missing.
Any hints/ideas?
import SwiftUI
import FirebaseCore
import FirebaseAuth
import FBSDKLoginKit
import FBSDKCoreKit
struct LoginView: View {
#EnvironmentObject var thisSession: CurrentSession
#ObservedObject var mainData = MainViewModel()
var facebookView = facebook()
var body: some View {
VStack {
facebookView.frame(width: 240, height: 50)
Text("\(self.thisSession.userId ?? "none")")
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView().environmentObject(CurrentSession())
}
}
struct facebook: UIViewRepresentable {
#EnvironmentObject var thisSession: CurrentSession
#ObservedObject var coordinator = Coordinator()
func makeCoordinator() -> facebook.Coordinator {
return self.coordinator
//facebook.Coordinator()
}
func makeUIView(context: UIViewRepresentableContext<facebook>) -> FBLoginButton {
let button = FBLoginButton()
button.delegate = self.coordinator
print("UPDATED")
return button
}
func updateUIView(_ uiView: FBLoginButton, context: UIViewRepresentableContext<facebook>) {
}
class Coordinator: NSObject, LoginButtonDelegate, ObservableObject {
#Published var userId: String?
func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?) {
if error != nil{
print((error?.localizedDescription)!)
return
}
if AccessToken.current != nil{
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (res,er) in
if er != nil{
print((er?.localizedDescription)!)
return
}
print("email: \(String(describing: res?.user.email))")
print("name: \(String(describing: res?.user.displayName))")
self.userId = String(describing: res?.user.displayName)
}
}
}
func loginButtonDidLogOut(_ loginButton: FBLoginButton) {
print("logged out")
try! Auth.auth().signOut()
}
}
}
Try the following, as coordinator is created during update the environment object should be already injected and present, so it should work
struct facebook: UIViewRepresentable {
#EnvironmentObject var thisSession: CurrentSession
func makeCoordinator() -> facebook.Coordinator {
Coordinator(session: thisSession) // << here !!
}
func makeUIView(context: UIViewRepresentableContext<facebook>) -> FBLoginButton {
let button = FBLoginButton()
button.delegate = self.coordinator
print("UPDATED")
return button
}
func updateUIView(_ uiView: FBLoginButton, context: UIViewRepresentableContext<facebook>) {
}
class Coordinator: NSObject, LoginButtonDelegate, ObservableObject {
let session: CurrentSession
init(session: CurrentSession) {
self.session = session
}
func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?) {
if error != nil{
print((error?.localizedDescription)!)
return
}
if AccessToken.current != nil{
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (res,er) in
if er != nil{
print((er?.localizedDescription)!)
return
}
print("email: \(String(describing: res?.user.email))")
print("name: \(String(describing: res?.user.displayName))")
DispatchQueue.main.async { // << here
self.session.userId = String(describing: res?.user.displayName)
}
}
}
}
func loginButtonDidLogOut(_ loginButton: FBLoginButton) {
print("logged out")
try! Auth.auth().signOut()
}
}
}
.sheet(isPresented: $showSheet) {
STPPaymentOptionsViewController()
}
I run this code hoping to present the Stripe Payment Options View Controller in my content view and I get this error:
Instance method sheet(isPresented:onDismiss:content:) requires that STPAddCardViewController conform to View
I also tried to wrap the view into a UIViewRepresentable like so:
struct PaymentOptionsView: UIViewRepresentable {
func makeUIView(context: Context) -> STPPaymentOptionsViewController {
let config = STPPaymentConfiguration()
config.additionalPaymentOptions = .default
config.requiredBillingAddressFields = .none
config.appleMerchantIdentifier = "dummy-merchant-id"
return STPPaymentOptionsViewController(configuration: config, e: STPTheme(), customerContext: STPCustomerContext(), delegate: self as! STPPaymentOptionsViewControllerDelegate)
}
}
Then I get the error:
Type CheckOut.PaymentOptionsView does not conform to protocol UIViewRepresentable.
Considering that STPPaymentOptionsViewController inherits from ViewController you need to use UIViewControllerRepresentable instead.
You also need to implement the required delegate methods for the STPPaymentOptionsViewControllerDelegate.
struct PaymentOptionsView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, STPPaymentOptionsViewControllerDelegate {
var control: PaymentOptionsView
init(_ control: PaymentOptionsView) {
self.control = control
}
// Implement required delegate methods here:
func paymentOptionsViewControllerDidCancel(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewControllerDidFinish(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didFailToLoadWithError error: Error) {
}
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PaymentOptionsView>) -> STPPaymentOptionsViewController {
let config = STPPaymentConfiguration()
config.additionalPaymentOptions = .default
config.requiredBillingAddressFields = .none
config.appleMerchantIdentifier = "dummy-merchant-id"
return STPPaymentOptionsViewController(configuration: config, theme: STPTheme(), apiAdapter: STPCustomerContext(), delegate: context.coordinator)
}
func updateUIViewController(_ uiViewController: STPPaymentOptionsViewController, context: UIViewControllerRepresentableContext<PaymentOptionsView>) { }
}
Keep in mind you're also setting the delegate in the STPPaymentOptionsViewController incorrectly. You need to use context.coordinator rather than self as! STPPaymentOptionsViewControllerDelegate.