I am trying to integrate Razorpay SDK in my iOS SwiftUI project.
I am opening a CheckoutView in showSheet function with isPresented boolean that determines whether to present the sheet or not.
When i try to open razorpay checkout view in SwiftUI with the help of Coordinator and UIViewControllerRepresentable, it only shows me blank white screen.
Here is my CheckoutView
struct CheckoutView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> CheckoutViewController {
.init()
}
func updateUIViewController(_ uiViewController: CheckoutViewController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, RazorpayPaymentCompletionProtocol, RazorpayPaymentCompletionProtocolWithData {
let parent: CheckoutView
typealias Razorpay = RazorpayCheckout
var razorpay: RazorpayCheckout? = nil
init(_ parent: CheckoutView) {
self.parent = parent
super.init()
print("init called")
razorpay = RazorpayCheckout.initWithKey("<mykey>", andDelegate: self)
let options: [AnyHashable:Any] = [
"key": "<mykey>",
"prefill": [
"contact": "1234567890",
"email": "a#a.com"
],
// "image": merchantDetails.logo,
"amount" : 100,
"currency": "INR",
"name": "merchantDetails.name",
"theme": [
// "color": merchantDetails.color.toHexString()
]
]
print("options \(options)")
if let rzp = self.razorpay {
rzp.open(options)
} else {
print("Unable to initialize")
}
}
func onPaymentError(_ code: Int32, description str: String) {
print("error: ", code, str)
}
func onPaymentSuccess(_ payment_id: String) {
print("success: ", payment_id)
}
func onPaymentError(_ code: Int32, description str: String, andData response: [AnyHashable : Any]?) {
print("error: ", code)
}
func onPaymentSuccess(_ payment_id: String, andData response: [AnyHashable : Any]?) {
print("success: ", payment_id)
}
}
}
class CheckoutViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// self.showPaymentForm()
}
}
Here is the log that i am getting.
init called
options [AnyHashable("prefill"): ["email": "a#a.com", "contact": "1234567890"], AnyHashable("name"): "merchantDetails.name", AnyHashable("currency"): "INR", AnyHashable("theme"): [], AnyHashable("amount"): 100]
2023-01-04 11:30:31.510877+0530 kidsclutt[78228:1171954] [boringssl] boringssl_metrics_log_metric_block_invoke(153) Failed to log metrics
RAZORPAY_SDK_CHECKS
##### Check 0:
Version Upgrade Check
1.3.0
##### Check 1:
Minimum Deployment Version Check
Project deployment target > iOS 10
2023-01-04 11:30:32.837037+0530 kidsclutt[78228:1171437] [Presentation] Attempt to present <Razorpay.OpinionatedAlertVC: 0x7f8a1d1d5700> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentGS1_V9kidsclutt11ContentViewGVS_30_EnvironmentKeyWritingModifierGSqCS2_13AuthViewModel___GVS_26_PreferenceWritingModifierVS_23PreferredColorSchemeKey___: 0x7f8a1d10bfd0> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentGS1_V9kidsclutt11ContentViewGVS_30_EnvironmentKeyWritingModifierGSqCS2_13AuthViewModel___GVS_26_PreferenceWritingModifierVS_23PreferredColorSchemeKey___: 0x7f8a1d10bfd0>) which is already presenting <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x7f8a1d1cba30>.
2023-01-04 11:30:36.009980+0530 kidsclutt[78228:1171958] [boringssl] boringssl_metrics_log_metric_block_invoke(153) Failed to log metrics
Related
I'm trying it integrate the Person SDK v2 in a SwiftUI view. It's setup for UIKit to present from a specific UIViewController. Here is my code.
https://docs.withpersona.com/docs/ios-sdk-v2-integration-guide
I'm not sure how to call my present function from SwiftUI. The SDK is setup so when you create that Inquiry object it triggers it's nav to present on the view controller.
struct PersonaInquiry: UIViewControllerRepresentable {
private var viewController = UIViewController()
private var coordinator = Coordinator()
class Coordinator: NSObject, InquiryDelegate {
func inquiryComplete(inquiryId: String, status: String, fields: [String : Persona2.InquiryField]) {
}
func inquiryCanceled(inquiryId: String?, sessionToken: String?) {
}
func inquiryError(_ error: Error) {
}
}
func makeUIViewController(context: Context) -> UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
func present(templateId: String) {
let config = InquiryConfiguration(templateId: templateId)
// Create the inquiry with the view controller
// as the delegate and presenter.
Inquiry(config: config, delegate: coordinator).start(from: viewController)
}
func makeCoordinator() -> Coordinator {
return coordinator
}
}
struct PersonaInquiry_Previews: PreviewProvider {
static var previews: some View {
PersonaInquiry()
}
}
Here's an example
ContentView:
struct ContentView: View {
#State private var isPresentingSDK = false
#State private var message = ""
var body: some View {
VStack {
Button(
action: {
isPresentingSDK.toggle()
},
label: {
Text("Launch Inquiry from SwiftUI 🚀")
.foregroundColor(Color.white)
.padding()
}
)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.capsule)
.fullScreenCover(
isPresented: $isPresentingSDK,
onDismiss: {
// Do nothing
},
content: {
InquirySDKWrapper(
inquiryComplete: { inquiryId, status, fields in
self.message = """
Inquiry Complete
Inquiry ID: \(inquiryId)
Status: \(String(describing: status))
"""
},
inquiryCanceled: { inquiryId, sessionToken in
self.message = "🤷♀️ Inquiry Cancelled"
},
inquiryErrored: { error in
self.message = """
💀 Inquiry Error
\(error.localizedDescription)
"""
}
)
}
)
Text(message)
.multilineTextAlignment(.center)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
InquirySDKWrapper:
import Persona2
import SwiftUI
import UIKit
struct InquirySDKWrapper: UIViewControllerRepresentable {
/// The wrapper VC presents the SDK and acts as its delegate.
/// The delegate methods in turn call the callbacks in the WrapperDelegate
private let wrapperVC: WrapperViewController
/// Pass in the callbacks for each delegate method
init(
inquiryComplete: #escaping (String, String, [String: InquiryField]) -> Void,
inquiryCanceled: #escaping (_ inquiryId: String?, _ sessionToken: String?) -> Void,
inquiryErrored: #escaping (_ error: Error) -> Void
) {
wrapperVC = WrapperViewController()
wrapperVC.inquiryComplete = inquiryComplete
wrapperVC.inquiryCanceled = inquiryCanceled
wrapperVC.inquiryErrored = inquiryErrored
}
func makeUIViewController(context: Context) -> some UIViewController {
return wrapperVC
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
// Do nothing
}
}
final class WrapperViewController: UIViewController {
private var isPresenting = false
// The callbacks
var inquiryComplete: ((_ inquiryId: String, _ status: String, _ fields: [String: InquiryField]) -> Void)!
var inquiryCanceled: ((_ inquiryId: String?, _ sessionToken: String?) -> Void)!
var inquiryErrored: ((_ error: Error) -> Void)!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Otherwise this would trigger once the SDK exits too
guard !isPresenting else { return }
let inquiry = Inquiry(
config: InquiryConfiguration(
templateId: "YOUR TEMPLATE ID HERE"
),
delegate: self
)
inquiry.start(from: self)
isPresenting = true
}
}
extension WrapperViewController: InquiryDelegate {
func inquiryComplete(inquiryId: String, status: String, fields: [String: InquiryField]) {
inquiryComplete(inquiryId, status, fields)
dismiss(animated: true, completion: nil)
}
func inquiryCanceled(inquiryId: String?, sessionToken: String?) {
inquiryCanceled(inquiryId, sessionToken)
dismiss(animated: true, completion: nil)
}
func inquiryError(_ error: Error) {
inquiryErrored(error)
dismiss(animated: true, completion: nil)
}
}
I was looking at integrating the Razorpay checkout feature with iOS in Xcode and found the official documentation at https://razorpay.com/docs/payment-gateway/ios-integration/standard/. The documentation helps with integrating the Razorpay with UIViewController. The iOS app I am building does not make use of the storyboard and is strictly SwiftUI. I have looked at multiple ways of incorporating the UIViewController in SwiftUI which is totally possible with UIViewRepresentable but the code structure uses
struct ComponentName: UIViewRepresentable{}
But Razorpay SDK for iOS wants to implement RazorpayPaymentCompletionProtocol to a class and not struct. How do I go about in using this in a strictly SwiftUI application?
You can use coordinators to manage the view controllers, and that coordinator will RazorpayPaymentCompletionProtocol.
Example:
struct ComponentName: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> CheckoutViewController {
.init()
}
func updateUIViewController(_ uiViewController: CheckoutViewController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, RazorpayPaymentCompletionProtocol {
let parent: ComponentName
typealias Razorpay = RazorpayCheckout
var razorpay: RazorpayCheckout!
init(_ parent: ComponentName) {
self.parent = parent
RazorpayCheckout.initWithKey(razorpayTestKey, andDelegate: self)
}
func onPaymentError(_ code: Int32, description str: String) {
print("error: ", code, str)
// self.presentAlert(withTitle: "Alert", message: str)
// parent.alert with message
}
func onPaymentSuccess(_ payment_id: String) {
print("success: ", payment_id)
// self.presentAlert(withTitle: "Success", message: "Payment Succeeded")
}
}
}
class CheckoutViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// self.showPaymentForm()
}
}
The problem seems simple, didCreatePaymentResult never gets called.
BUT, in my old sample project, taken from your iOS example for payment intent, that didCreatePaymentResult gets called every single time I create or select a card, here's the repo of the working project: https://github.com/glennposadas/stripe-example-ios-nodejs
BUT again, my main concern is my current project.
I use v19.2.0 in both of these projects, I even tried the v19.3.0.
I wanted to use Stripe Charge really, but I believe Stripe does not support Apple pay for that. So I have no choice but to use Stripe Payment Intent.
CoreService.swift (conforms to STPCustomerEphemeralKeyProvider)
extension CoreService: STPCustomerEphemeralKeyProvider {
func createCustomerKey(withAPIVersion apiVersion: String, completion: #escaping STPJSONResponseCompletionBlock) {
orderServiceProvider.request(.requestEphemeralKey(stripeAPIVersion: apiVersion)) { (result) in
switch result {
case let .success(response):
guard let json = ((try? JSONSerialization.jsonObject(with: response.data, options: []) as? [String : Any]) as [String : Any]??) else {
completion(nil, NSError(domain: "Error parsing stripe data", code: 300, userInfo: nil))
return
}
completion(json, nil)
default:
UIViewController.current()?.alert(title: "Error stripe", okayButtonTitle: "OK", withBlock: nil)
}
}
}
}
PaymentController.swift
class PaymentViewController: BaseViewController {
// MARK: - Properties
private var paymentContext: STPPaymentContext!
private let paymentConstantValue: Int = 3000
// MARK: - Functions
// MARK: Overrides
override func viewDidLoad() {
super.viewDidLoad()
self.setupStripe()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.hideNavBar(animated: true)
}
#IBAction func creditCardButtonTapped(_ sender: Any) {
self.paymentContext.presentPaymentOptionsViewController()
}
private func setupStripe() {
let config = STPPaymentConfiguration.shared()
config.appleMerchantIdentifier = "merchant.com.gsample.app"
config.companyName = "Scoutd LLC"
config.requiredBillingAddressFields = .none
config.requiredShippingAddressFields = .none
config.additionalPaymentOptions = .applePay
let customerContext = STPCustomerContext(keyProvider: CoreService())
let paymentContext = STPPaymentContext(
customerContext: customerContext,
configuration: config,
theme: STPTheme.default()
)
let userInformation = STPUserInformation()
paymentContext.prefilledInformation = userInformation
paymentContext.paymentAmount = self.paymentConstantValue
paymentContext.paymentCurrency = "usd"
self.paymentContext = paymentContext
self.paymentContext.delegate = self
self.paymentContext.hostViewController = self
}
}
// MARK: - STPPaymentContextDelegate
extension PaymentViewController: STPPaymentContextDelegate {
func paymentContextDidChange(_ paymentContext: STPPaymentContext) {
print("paymentContextDidChange")
}
func paymentContext(_ paymentContext: STPPaymentContext, didFailToLoadWithError error: Error) {
// error alert....
}
func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: #escaping STPPaymentStatusBlock) {
print("didCreatePaymentResult ✅")
}
func paymentContext(_ paymentContext: STPPaymentContext, didFinishWith status: STPPaymentStatus, error: Error?) {
switch status {
case .success:
// success
case .error:
// error alert....
default:
break
}
}
}
SOLVED! This should help engineers struggling with Stripe implementation in the future.
So in my case, I have two buttons:
Apple Pay
Credit card.
The absolute solution for me is handle the selectedPaymentOption of the paymentContext.
Scenarios:
If the apple pay button is tapped, present apple pay sheet and don't present add/select card UI of Stripe.
If the credit card button is tapped, don't present apple pay sheet and instead present select card.
Related to #2, call requestPayment() if there's a selected option.
Voila! The didCreatePaymentResult now gets invoked!
// MARK: IBActions
#IBAction func applePayButtonTapped(_ sender: Any) {
if self.paymentContext.selectedPaymentOption is STPApplePayPaymentOption {
self.paymentContext.requestPayment()
}
}
#IBAction func creditCardButtonTapped(_ sender: Any) {
if let selectedPaymentOption = self.paymentContext.selectedPaymentOption,
!(selectedPaymentOption is STPApplePayPaymentOption) {
self.paymentContext.requestPayment()
return
}
self.paymentContext.presentPaymentOptionsViewController()
}
I´m trying to implement the firebaseUI login in an iOS app using swiftUI.
I´m able to use the main login view, but i can not control the flow after the user was loged on.
This is the code of my "login controller"
import SwiftUI
import FirebaseUI
struct CustomLogin: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let authUI = FUIAuth.defaultAuthUI()
let providers: [FUIAuthProvider] = [
FUIEmailAuth(),
FUIFacebookAuth()
// FUIPhoneAuth(authUI:FUIAuth.defaultAuthUI()),
]
authUI?.providers = providers
let authViewController = authUI?.authViewController()
// let controller = UIViewController()
// authUI!.delegate = self
return authViewController!
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
struct CustomLogin_Previews: PreviewProvider {
static var previews: some View {
CustomLogin()
}
}
I can show the login flow by add CustomLogin())to my content view.
how can I manage the call backs after the user has logged on?
The user was created in firebase, but the view don´t update.
if someone has implemented a firebaseUI login with SwiftUI I appreciated it.
Thanks
I was able to implement something that worked based off of your code and some Apple tutorials. Basically, you need to add a coordinator to your UIViewControllerRepresentable. You can add your delegate to the coordinator.
Here's my scratch code that works:
import SwiftUI
import FirebaseUI
import Firebase
public var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
public var screenHeight: CGFloat {
return UIScreen.main.bounds.height
}
struct LoginView : View {
#State private var viewState = CGSize(width: 0, height: screenHeight)
#State private var MainviewState = CGSize.zero
var body : some View {
ZStack {
CustomLoginViewController { (error) in
if error == nil {
self.status()
}
}.offset(y: self.MainviewState.height).animation(.spring())
MainView().environmentObject(DataStore()).offset(y: self.viewState.height).animation(.spring())
}
}
func status() {
self.viewState = CGSize(width: 0, height: 0)
self.MainviewState = CGSize(width: 0, height: screenHeight)
}
}
struct LoginView_Previews : PreviewProvider {
static var previews : some View {
LoginView()
}
}
struct CustomLoginViewController : UIViewControllerRepresentable {
var dismiss : (_ error : Error? ) -> Void
func makeCoordinator() -> CustomLoginViewController.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIViewController
{
let authUI = FUIAuth.defaultAuthUI()
let providers : [FUIAuthProvider] = [
FUIEmailAuth(),
FUIGoogleAuth(),
FUIOAuth.appleAuthProvider()
]
authUI?.providers = providers
authUI?.delegate = context.coordinator
let authViewController = authUI?.authViewController()
return authViewController!
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<CustomLoginViewController>)
{
}
//coordinator
class Coordinator : NSObject, FUIAuthDelegate {
var parent : CustomLoginViewController
init(_ customLoginViewController : CustomLoginViewController) {
self.parent = customLoginViewController
}
// MARK: FUIAuthDelegate
func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?)
{
if let error = error {
parent.dismiss(error)
}
else {
parent.dismiss(nil)
}
}
func authUI(_ authUI: FUIAuth, didFinish operation: FUIAccountSettingsOperationType, error: Error?)
{
}
}
}
I am trying to add google sign-in with swiftUI whith UIKitViewController, and for some reason I have difficulties showing the button does not appear in style
The signIn and the button works perfect but at some point the style of the button stopped appearing
I'm adding the button in a uikit viewcontroller, because I couldn't think of another way to handle the Google delegate
Here's the preview https://ibb.co/tYhx62b
//
// GoogleSignInButtonView.swift
//
// Created by Ivan Schaab on 11/09/2019.
// Copyright © 2019 Ivan Schaab. All rights reserved.
//
import GoogleSignIn
import SwiftUI
struct GoogleSignInButtonView: View {
#EnvironmentObject var lvm: LoginViewModel
var body: some View {
HStack {
Spacer()
GoogleButtonViewControllerRepresentable { (token, user) in
// Google Login Success
// Now do Backend Validations
self.lvm.loginOauth(token: token, user: user)
}
Spacer()
}.frame(alignment: .center)
}
}
class GoogleButtonUIKitViewController: UIViewController {
var signInButton = GIDSignInButton()
override func viewDidLoad() {
super.viewDidLoad()
GIDSignIn.sharedInstance().clientID = Constants.GOOGLE_CLIENT_ID
self.view.addSubview(signInButton)
GIDSignIn.sharedInstance()?.presentingViewController = self
// Automatically sign in the user.
GIDSignIn.sharedInstance()?.restorePreviousSignIn()
}
}
struct GoogleButtonViewControllerRepresentable: UIViewControllerRepresentable
{
let vc = GoogleButtonUIKitViewController()
var googleResponse: (String, User) -> Void
func makeUIViewController(context: Context) -> GoogleButtonUIKitViewController {
return vc
}
func updateUIViewController(_ uiViewController: GoogleButtonUIKitViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(vc: vc, googleResponse: googleResponse)
}
static func dismantleUIViewController(_ uiViewController: GoogleButtonUIKitViewController, coordinator: GoogleButtonViewControllerRepresentable.Coordinator) {
print("DISMANTLE")
}
class Coordinator: NSObject, GIDSignInDelegate {
var foo: (String, User) -> Void
init(vc: GoogleButtonUIKitViewController, googleResponse: #escaping (String, User) -> Void) {
self.foo = googleResponse
super.init()
GIDSignIn.sharedInstance()?.delegate = self
}
func sign(_ signIn: GIDSignIn!, didSignInFor googleUser: GIDGoogleUser!, withError error: Error!) {
if let error = error {
if (error as NSError).code == GIDSignInErrorCode.hasNoAuthInKeychain.rawValue {
print("The user has not signed in before or they have since signed out.")
} else {
print("\(error.localizedDescription)")
}
return
}
// let userId = googleUser.userID // For client-side use only!
let idToken = googleUser.authentication.idToken // Safe to send to the server
let email = googleUser.profile.email
if googleUser.profile.hasImage{
let imageUrl = googleUser.profile.imageURL(withDimension: 120)
print(" image url: ", imageUrl?.absoluteString ?? "NO URL")
}
let user : User = User(id: 1, name: googleUser.profile.givenName, surname: googleUser.profile.familyName, imgName: "" , email: googleUser.profile.email)
print("email: ",email ?? "NO EMAIL")
foo(idToken! , user)
}
}
}
#if DEBUG
struct SomeRepView_Previews: PreviewProvider {
static var previews: some View {
GoogleSignInButtonView().environmentObject(LoginViewModel())
}
}
#endif
This is a part of my code where I implement Facebook and Google login, using any button I hope it helps you
import SwiftUI
import GoogleSignIn
import FBSDKLoginKit
var body: some View {
LoadingView(isShowing: .constant(loading)) {
NavigationView {
ScrollView {
VStack {
Text("Or Login with").font(.footnote)
HStack {
Button(action: self.logginFb, label: {
Image("ic_facebook").foregroundColor(Color.white).frame(width: 20, height: 20)
})
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color("facebook"))
.cornerRadius(8.0)
Button(action: self.socialLogin.attemptLoginGoogle, label: {
Image("ic_google").frame(width: 20, height: 20)
})
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color.white)
.cornerRadius(8.0)
.shadow(radius: 4.0)
}.padding()
}.padding(.all, 32)
}.navigationBarTitle(Text("Login"))
}
}
}
func logginFb() {
socialLogin.attemptLoginFb(completion: { result, error in
})
}
struct SocialLogin: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<SocialLogin>) -> UIView {
return UIView()
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<SocialLogin>) {
}
func attemptLoginGoogle() {
GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
GIDSignIn.sharedInstance()?.signIn()
}
func attemptLoginFb(completion: #escaping (_ result: FBSDKLoginManagerLoginResult?, _ error: Error?) -> Void) {
let fbLoginManager: FBSDKLoginManager = FBSDKLoginManager()
fbLoginManager.logOut()
fbLoginManager.logIn(withReadPermissions: ["email"], from: UIApplication.shared.windows.last?.rootViewController) { (result, error) -> Void in
completion(result, error)
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
The configuration of google and facebook in AppDelegate is the same as its documentation.
Now with swiftUI it is not necessary to use the default buttons that you give us.
Google and Facebook custom buttons
Note for SwiftUI 2.0 lifecycle: there is no standard AppDelegate so you also need to add an adapter in main App file:
#main
struct ExampleApp: App {
#UIApplicationDelegateAdaptor(ExampleAppDelegate.self) var appDelegate
and prepare the Delegate in a separate file:
//No #UIApplicationMain
class ExampleAppDelegate: NSObject, UIApplicationDelegate, GIDSignInDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GIDSignIn.sharedInstance().clientID = "YOUR CLINET ID"
GIDSignIn.sharedInstance().delegate = self
return true
}
...
With the latest SwiftUI, you can setup the GIDSignInDelegate within your AppDelegate and ensure that it conforms to all methods. Then, within your SceneDelegate, when setting up the window, you can drop this line in to setup the presenting view controller:
GIDSignIn.sharedInstance()?.presentingViewController = window.rootViewController.
Lastly, when creating your button, set the action or tap gesture to call GIDSignIn.sharedInstance().signIn() and you should be good to go!
GoogleSignIn 6.2.4 has the signIn function that requires a presenting UIViewController.
To solve this, add a sceneDelegate to the environment so it's accessible to SwiftUI.
Set up an AppDelegate so we can hook up our SceneDelegate
class AppDelegate: NSObject, UIApplicationDelegate {
// Hook up our SceneDelegate
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
if connectingSceneSession.role == .windowApplication {
configuration.delegateClass = SceneDelegate.self
}
return configuration
}
}
Set up a SceneDelegate as an ObservableObject to keep track of the window
// Set this up so we can access the window in the environment
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
self.window = windowScene.keyWindow
}
}
Be sure to enable the AppDelegate in the App
#main
struct AdrichApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Finally, use it in a SwiftUI View to show your Continue with Google button
struct SignInWithGoogle: View {
#EnvironmentObject var sceneDelegate: SceneDelegate
var body: some View {
if let vc = sceneDelegate.window?.rootViewController {
continueWithGoogle(config: GIDConfiguration(clientID: "YOUR CLINET ID"), presenter: vc)
} else {
emptyView
}
}
private var emptyView: some View {
print("Unable to access the root view controller")
return EmptyView()
}
private func continueWithGoogle(config: GIDConfiguration, presenter: UIViewController) -> some View {
Button {
GIDSignIn.sharedInstance.signIn(with: config, presenting: presenter) { user, error in
if let error = error {
print(error)
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: authentication.accessToken)
// they are now signed in with Google!
}
} label: {
Text("Continue with Google")
}
}
}