MaxAst / expo-share-extension

Expo config plugin for creating iOS share extensions with a custom view.
MIT License
224 stars 6 forks source link

[BUG] openHostApp doesn't work on iOS 18 #48

Closed dongsuo closed 2 weeks ago

dongsuo commented 1 month ago

I just found this code doesn't work on iOS 18, but works on iOS 17.5. It even doesn't call console.log Any idea?

<TouchableOpacity onPress={() => { console.log(111); openHostApp(/share?url=${url}) }} >

dongsuo commented 1 month ago

Found an article related to this bug, hope useful https://keyboardkit.com/blog/2024/09/11/ios18-breaks-selector-based-url-opening

MaxAst commented 1 month ago

damn, that's annoying. Thanks for reporting and for sharing the article. Will treat this issue as top priority

coryetzkorn commented 1 month ago

I looked into this a bit. Tried modifying the openHostApp method locally, but no luck.

It looks like Apple has intentionally removed the ability to open URLs from app extensions. The deprecated method used previously won't be coming back :(

App extensions are not allowed to open URLs directly. This isn’t accidental, but a deliberate design choice on Apple’s part. Don’t try to bypass such restrictions using Silly Runtime Hacks™. That just opens yourself up to compatibility problems down the pike.

If your app extension needs to get the user’s attention, do that by posting a local notification.

https://developer.apple.com/forums/thread/764570

AkbarKhamid commented 3 weeks ago

This is working on ios 18.1 (not thoroughly tested).

 // in ShareExtensionViewController.swift

  private func openHostApp(path: String?) {
    guard let scheme = Bundle.main.object(forInfoDictionaryKey: "HostAppScheme") as? String else { return }
    var urlComponents = URLComponents()
    urlComponents.scheme = scheme
    urlComponents.host = ""

    if let path = path {
      let pathComponents = path.split(separator: "?", maxSplits: 1)
      let pathWithoutQuery = String(pathComponents[0])
      let queryString = pathComponents.count > 1 ? String(pathComponents[1]) : nil

      // Parse and set query items
      if let queryString = queryString {
        let queryItems = queryString.split(separator: "&").map { queryParam -> URLQueryItem in
          let paramComponents = queryParam.split(separator: "=", maxSplits: 1)
          let name = String(paramComponents[0])
          let value = paramComponents.count > 1 ? String(paramComponents[1]) : nil
          return URLQueryItem(name: name, value: value)
        }
        urlComponents.queryItems = queryItems
      }

      let pathWithSlashEnsured = pathWithoutQuery.hasPrefix("/") ? pathWithoutQuery : "/\(pathWithoutQuery)"
      urlComponents.path = pathWithSlashEnsured
    }

    guard let url = urlComponents.url else { return }
    openURL(url)
    self.close()
  }

  @objc @discardableResult private func openURL(_ url: URL) -> Bool {
    var responder: UIResponder? = self
    while responder != nil {
      if let application = responder as? UIApplication {
        if #available(iOS 18.0, *) {
          application.open(url, options: [:], completionHandler: nil)
          return true
        } else {
          return application.perform(#selector(UIApplication.open(_:options:completionHandler:)), with: url, with: [:]) != nil
        }
      }
      responder = responder?.next
    }
    return false
  }
JimmyLv commented 3 weeks ago

yeah, we need this! ❤️

MaxAst commented 3 weeks ago

Thank you @AkbarKhamid! Will give this a try and release a patch this week

MaxAst commented 2 weeks ago

just released v1.10.5, which includes @AkbarKhamid's fix! Thanks a lot! Sorry it took so long, I've been swamped with work from my day job.