8

I tried looking for a solution in posts such as this and this where people had a very similar problem: How to send a message from iOS App to a Safari Extension?

I even read this article where the author was explaining how to use SafariExtensionHandler to send a message from the browser to the app and back to the browser after selecting the context menu, but it's not quite what I was looking for.

Sending a Token from iOS App to Safari Extension

In the app, the user has to enter an email and password to log into their account. Once they log in, I save their information in UserDefaults like this:

class AuthDataService {
{...}
        URLSession.shared.dataTaskPublisher(for: urlRequest)
            .tryMap { data, response -> Data in
                guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200,
                      let accessToken = httpResponse.value(forHTTPHeaderField: "Access-Token"),
                      let clientId = httpResponse.value(forHTTPHeaderField: "Client"),
                      let uid = httpResponse.value(forHTTPHeaderField: "Uid")
                else {
                    throw CustomError.cannotExecuteRequest
                }
                
                let sharedDefaults = UserDefaults(suiteName: "group.com.MyCompany.MyProject")
                sharedDefaults?.set(accessToken, forKey: "Access-Token")
                sharedDefaults?.set(clientId, forKey: "Client")
                sharedDefaults?.set(uid, forKey: "Uid")

                return data
            }
{...}
}

App-Group

From my understanding of this article, I need to create an App Group, in order to share the data between the iOS App and the Safari Extension. I named the group: "group.com.MyCompany.MyProject" (just like the suiteName in UserDefaults).

Home View

The screen that the user sees when they log in, is a SwiftUI View that has a Link which takes the user to Safari so they can open the extension themselves:

struct HomeView: View {
    @EnvironmentObject var viewModel: AuthViewModel
    var body: some View {
        Link(destination: URL(string: "https://www.apple.com/")!) {
            Text("Take me to Safari")
        }
    }
}

SafariWebExtensionHandler

Now, all the articles that I read were talking about how to send data from the Safari Extension to the iOS app through SafariWebExtensionHandler's beginRequest(with:). However, I'm trying to send the Tokens in UserDefaults either whenever the user logs in the app, or when they open the Safari Extension.

I tried retrieving the data from UserDefaults to see if I could at least read it in the terminal, but the debugger never gets to the print statements:

import SafariServices
import os.log

let SFExtensionMessageKey = "message"

class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {

    func readData() {
        let sharedDefaults = UserDefaults(suiteName: "group.com.lever.clientTokens")
        print(sharedDefaults?.object(forKey: "Access-Token")) //<-- This line never gets executed
    }
    
    func beginRequest(with context: NSExtensionContext) {
        let item = context.inputItems[0] as! NSExtensionItem
        let message = item.userInfo?[SFExtensionMessageKey]
        os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)

        let response = NSExtensionItem()
        response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]

        readData()
        
        context.completeRequest(returningItems: [response], completionHandler: nil)
    }

}

Question

macOS vs iOS

This documentation from Apple has a section called Send messages from the app to JavaScript which is pretty much what I want to do. The documentation even mentions SFSafariApplication.dispatchMessage(withName:toExtensionWithIdentifier:userInfo:completionHandler:) which in theory sends a message to the JavaScript script, but it says it only works in macOS:

You can’t send messages from a containing iOS app to your web extension’s JavaScript scripts.

This excellent Medium article talks about sending an APIKey from the app to the Safari Extension using an API from openai.com. It seems that it also uses SFSafariApplication to communicate with SafariWebExtensionHandler, but again it looks like it only works for macOS.

Safari Extension to webPage

I also read this other Apple documentation thinking it would help, but it only talks about passing messages from the Safari Extension's popup to the webpage.

Conclusion

So my question is:

Is writing code in SafariWebExtensionHandler the right way to send data from the iOS App to my Safari Extension? Can this be done in iOS? Or is it only available for macOS?

I read some other articles that were talking about using the JavaScript files in the Resources folder in order to "listen" to changes. But I'm a little confused as to how I can send those changes from my App in order for the Safari Extension to listen to them.

What I am trying to achieve is for the user to be already logged-in in the Safari Extension after they are redirected from the HomeView in the iOS App, instead of having to sign in another time.

Thank you for your time and help!

Nico Cobelo
  • 557
  • 7
  • 18
  • 1
    You should never store an access token in the UserDefaults. Information in the UserDefaults is relatively easy to access. This kind of information must be stored in the KeyChain. – DatBlaueHus Nov 25 '22 at 09:37

1 Answers1

0

While it is not possible for app to initiate connection with extension, the other direction is supported. And when extension sends data to app, you can attach a response and send that back to the extension.

I'm not sure why your example of using SafariWebExtensionHandler did not work. It looks fine. Perhaps the problem is with JS side.

App

import SafariServices
import os.log

extension String: Error {}

let SFExtensionMessageKey = "message"

class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
    func beginRequest(with context: NSExtensionContext) {
        let item = context.inputItems[0] as! NSExtensionItem
        let data = item.userInfo?[SFExtensionMessageKey] as AnyObject?
        guard let key = data?["key"] as? String else {
            return
        }
        let response: [String: Any]
        do {
            switch(key) {
            case "getToken":
                os_log(.default, "handling getToken")
                guard let sharedDefault = UserDefaults(suiteName: "group.com.lever.clientTokens") else {
                    throw "UserDefaults not found"
                }
                guard let tokenData = sharedDefault.data(forKey: "Access-Token") else {
                    throw "Access-Token not found"
                }
                guard let tokenString = String(data: tokenData, encoding: .utf8) else {
                    throw "tokenData could not be converted into string";
                }
                response = ["success": true, "resp": tokenString]
            default:
                throw "Invalid key"
            }
        } catch {
            os_log(.error, "Error: %s", String(reflecting: error))
            response = ["success": false, "resp": ["message": error.localizedDescription]]
        }
        let resp = NSExtensionItem()
        resp.userInfo = [SFExtensionMessageKey: response]
        context.completeRequest(returningItems: [resp], completionHandler: nil)
    }
}

Extension

Add "nativeMessaging" to "permissions" in 'manifest.json' first.

async function getToken() {
  const resp = await browser.runtime.sendNativeMessage("_", { 
    key: "getToken"
  });
  if (resp.success) {
    return resp.resp
  } else {
    throw new Error(resp.message)
  }
}