Search code examples
iosswiftcapacitorcapacitor-plugin

Capacitor iOS how to get return value from native code to JS


I am working on passkey authentication and trying to get a return values from my ViewController native code to my JS code using Capacitor. How can I get the value from ViewController and return it to my JS after dispatch bridge?

SamplePlugin.swift - to be called from JS

import Foundation
import Capacitor

@objc(SamplePlugin)
public class SamplePlugin: CAPPlugin {
   
    @objc func someFunction(_ call: CAPPluginCall) {
        let value = call.getString("value") ?? ""
        let value2 = call.getString("value2") ?? ""
        
        let challenge: Data? = call.getString("challenge")!.data(using: .utf8)
        
        DispatchQueue.main.async {
            let signInViewController = SignInViewController(challenge: challenge!)
            self.bridge?.viewController?.present(signInViewController, animated: true, completion: nil)
        }
         
        // I want to return some value from SignInViewController to JS Code   
        call.resolve(["value": value, "testval": value2, "platform": "ios"])
    }
}

SignInViewController - Open modal pop up for passkey authorization

class SignInViewController: UIViewController {
    var challenge: Data!

    private var signInObserver: NSObjectProtocol?
    private var signInErrorObserver: NSObjectProtocol?
    
    init(challenge: Data) {
        self.challenge = challenge
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        signInObserver = NotificationCenter.default.addObserver(forName: .UserSignedIn, object: nil, queue: nil) {_ in
            self.didFinishSignIn()
        }

        signInErrorObserver = NotificationCenter.default.addObserver(forName: .ModalSignInSheetCanceled, object: nil, queue: nil) { _ in
            self.showSignInForm()
        }

        guard let window = self.view.window else { fatalError("The view was not in the app's view hierarchy!") }
        
        // Open modal for passkey authorization and authenticate user
        let userName = "Name"
        (UIApplication.shared.delegate as? AppDelegate)?.accountManager.signUpWith(userName: userName, challenge: self.challenge, anchor: window)
    }

    func didFinishSignIn() {
        // I want to return data to JS after authorization success
    }
}

AccountManager.swift - Controller to authorize user

import AuthenticationServices
import Foundation
import os

extension NSNotification.Name {
    static let UserSignedIn = Notification.Name("UserSignedInNotification")
    static let ModalSignInSheetCanceled = Notification.Name("ModalSignInSheetCanceledNotification")
}


class AccountManager: NSObject, ASAuthorizationControllerPresentationContextProviding, ASAuthorizationControllerDelegate {
    let domain = "com.domain.app"
    var authenticationAnchor: ASPresentationAnchor?
    var isPerformingModalReqest = false
    
    func signUpWith(userName: String, challenge: Data, anchor: ASPresentationAnchor) {
        self.authenticationAnchor = anchor
        
        if #available(iOS 15.0, *) {
            let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: domain)
            
            let userID = "d0a4bc91-2def-4567-8983-9188a4ca2048".data(using: .utf8)!

            let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest(challenge: challenge,
                                                                                                      name: "User Fullname", userID: userID)

            let authController = ASAuthorizationController(authorizationRequests: [ registrationRequest ] )
            authController.delegate = self
            authController.presentationContextProvider = self
            authController.performRequests()
            isPerformingModalReqest = true
        } else {
            // Fallback on earlier versions
        }
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        // Authorization complete 
        if #available(iOS 14.0, *) {
            let logger = Logger()
            
            if #available(iOS 15.0, *) {
                switch authorization.credential {
                case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration:
                    logger.log("A new passkey was registered: \(credentialRegistration)")
                    // I have managed to get the credential key success registration but couldn't get to pass it back to JS
                    didFinishSignIn()
                case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
                    logger.log("A passkey was used to sign in: \(credentialAssertion)")
                    didFinishSignIn()
                case let passwordCredential as ASPasswordCredential:
                    logger.log("A password was provided: \(passwordCredential)")
                    didFinishSignIn()
                default:
                    fatalError("Received unknown authorization type.")
                }
            } else {
                // Fallback on earlier versions
            }
        } else {
            // Fallback on earlier versions
        }

        isPerformingModalReqest = false
    }

    func didFinishSignIn() {
        print("didFinish")
        NotificationCenter.default.post(name: .UserSignedIn, object: nil)
    }
}

I have managed to get the credential key success registration but couldn't get to pass it back to JS, how can I return the value back? Thank you in advance.


Solution

  • In Swift you can use a Completion Handler.

    Here you can find an example in a Capacitor plugin: https://github.com/capawesome-team/capacitor-plugins/blob/main/packages/badge/ios/Plugin/BadgePlugin.swift#L27:L33

    BadgePlugin.swift:

        @objc override public func checkPermissions(_ call: CAPPluginCall) {
            implementation?.checkPermissions(completion: { permission in
                call.resolve([
                    "display": permission
                ])
            })
        }
    

    Badge.swift:

        @objc public func checkPermissions(completion: @escaping (_ status: String) -> Void) {
            UNUserNotificationCenter.current().getNotificationSettings { settings in
                let permission: String
    
                switch settings.authorizationStatus {
                case .authorized, .ephemeral, .provisional:
                    permission = "granted"
                case .denied:
                    permission = "denied"
                case .notDetermined:
                    permission = "prompt"
                @unknown default:
                    permission = "prompt"
                }
    
                completion(permission)
            }
        }