about summary refs log tree commit diff
path: root/plugins
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-02-27 15:22:03 -0800
committerGitHub <noreply@github.com>2024-02-27 15:22:03 -0800
commitd451f82f54974b7b3da1477a7e1f221628860f62 (patch)
tree631d60b9d9ef47129529068753b10029fb99c34f /plugins
parentac726497a475f7492ee0269851979817b17d98c2 (diff)
downloadvoidsky-d451f82f54974b7b3da1477a7e1f221628860f62.tar.zst
Share Extension/Intents (#2587)
* add native ios code outside of ios project

* helper script

* going to be a lot of these commits to squash...backing up

* save

* start of an expo plugin

* create info.plist

* copy the view controller

* maybe working

* working

* wait working now

* working plugin

* use current scheme

* update intent path

* use better params

* support text in uri

* build

* use better encoding

* handle images

* cleanup ios plugin

* android

* move bash script to /scripts

* handle cases where loaded data is uiimage rather than uri

* remove unnecessary logic, allow more than 4 images and just take first 4

* android build plugin

* limit images to four on android

* use js for plugins, no need to build

* revert changes to app config

* use correct scheme on android

* android readme

* move ios extension to /modules

* remove unnecessary event

* revert typo

* plugin readme

* scripts readme

* add configurable scheme to .env, default to `bluesky`

* remove debug

* revert .gitignore change

* add comment about updating .env to app.config.js for those modifying scheme

* modify .env

* update android module to use the proper url

* update ios extension

* remove comment

* parse and validate incoming image uris

* fix types

* rm oops

* fix a few typos
Diffstat (limited to 'plugins')
-rw-r--r--plugins/shareExtension/README.md22
-rw-r--r--plugins/shareExtension/withAppEntitlements.js13
-rw-r--r--plugins/shareExtension/withExtensionEntitlements.js33
-rw-r--r--plugins/shareExtension/withExtensionInfoPlist.js39
-rw-r--r--plugins/shareExtension/withExtensionViewController.js31
-rw-r--r--plugins/shareExtension/withIntentFilters.js89
-rw-r--r--plugins/shareExtension/withShareExtensions.js47
-rw-r--r--plugins/shareExtension/withXcodeTarget.js55
8 files changed, 329 insertions, 0 deletions
diff --git a/plugins/shareExtension/README.md b/plugins/shareExtension/README.md
new file mode 100644
index 000000000..2b57e624a
--- /dev/null
+++ b/plugins/shareExtension/README.md
@@ -0,0 +1,22 @@
+# Share extension plugin for Expo
+
+This plugin handles moving the necessary files into their respective iOS and Android targets and updating the build
+phases, plists, manifests, etc.
+
+## Steps
+
+### ios
+
+1. Update entitlements
+2. Set the app group to group.<identifier>
+3. Add the extension plist
+4. Add the view controller
+5. Update the xcode project's build phases
+
+### android
+
+1. Update the manifest with the intents the app can receive
+
+## Credits
+
+Adapted from https://github.com/andrew-levy/react-native-safari-extension and https://github.com/timedtext/expo-config-plugin-ios-share-extension/blob/master/src/withShareExtensionXcodeTarget.ts
diff --git a/plugins/shareExtension/withAppEntitlements.js b/plugins/shareExtension/withAppEntitlements.js
new file mode 100644
index 000000000..6f9136c37
--- /dev/null
+++ b/plugins/shareExtension/withAppEntitlements.js
@@ -0,0 +1,13 @@
+const {withEntitlementsPlist} = require('@expo/config-plugins')
+
+const withAppEntitlements = config => {
+  // eslint-disable-next-line no-shadow
+  return withEntitlementsPlist(config, async config => {
+    config.modResults['com.apple.security.application-groups'] = [
+      `group.${config.ios.bundleIdentifier}`,
+    ]
+    return config
+  })
+}
+
+module.exports = {withAppEntitlements}
diff --git a/plugins/shareExtension/withExtensionEntitlements.js b/plugins/shareExtension/withExtensionEntitlements.js
new file mode 100644
index 000000000..e6bbf9d23
--- /dev/null
+++ b/plugins/shareExtension/withExtensionEntitlements.js
@@ -0,0 +1,33 @@
+const {withInfoPlist} = require('@expo/config-plugins')
+const plist = require('@expo/plist')
+const path = require('path')
+const fs = require('fs')
+
+const withExtensionEntitlements = (config, {extensionName}) => {
+  // eslint-disable-next-line no-shadow
+  return withInfoPlist(config, config => {
+    const extensionEntitlementsPath = path.join(
+      config.modRequest.platformProjectRoot,
+      extensionName,
+      `${extensionName}.entitlements`,
+    )
+
+    const shareExtensionEntitlements = {
+      'com.apple.security.application-groups': [
+        `group.${config.ios?.bundleIdentifier}`,
+      ],
+    }
+
+    fs.mkdirSync(path.dirname(extensionEntitlementsPath), {
+      recursive: true,
+    })
+    fs.writeFileSync(
+      extensionEntitlementsPath,
+      plist.default.build(shareExtensionEntitlements),
+    )
+
+    return config
+  })
+}
+
+module.exports = {withExtensionEntitlements}
diff --git a/plugins/shareExtension/withExtensionInfoPlist.js b/plugins/shareExtension/withExtensionInfoPlist.js
new file mode 100644
index 000000000..9afc4d5f9
--- /dev/null
+++ b/plugins/shareExtension/withExtensionInfoPlist.js
@@ -0,0 +1,39 @@
+const {withInfoPlist} = require('@expo/config-plugins')
+const plist = require('@expo/plist')
+const path = require('path')
+const fs = require('fs')
+
+const withExtensionInfoPlist = (config, {extensionName}) => {
+  // eslint-disable-next-line no-shadow
+  return withInfoPlist(config, config => {
+    const plistPath = path.join(
+      config.modRequest.projectRoot,
+      'modules',
+      extensionName,
+      'Info.plist',
+    )
+    const targetPath = path.join(
+      config.modRequest.platformProjectRoot,
+      extensionName,
+      'Info.plist',
+    )
+
+    const extPlist = plist.default.parse(fs.readFileSync(plistPath).toString())
+
+    extPlist.MainAppScheme = config.scheme
+    extPlist.CFBundleName = '$(PRODUCT_NAME)'
+    extPlist.CFBundleDisplayName = 'Extension'
+    extPlist.CFBundleIdentifier = '$(PRODUCT_BUNDLE_IDENTIFIER)'
+    extPlist.CFBundleVersion = '$(CURRENT_PROJECT_VERSION)'
+    extPlist.CFBundleExecutable = '$(EXECUTABLE_NAME)'
+    extPlist.CFBundlePackageType = '$(PRODUCT_BUNDLE_PACKAGE_TYPE)'
+    extPlist.CFBundleShortVersionString = '$(MARKETING_VERSION)'
+
+    fs.mkdirSync(path.dirname(targetPath), {recursive: true})
+    fs.writeFileSync(targetPath, plist.default.build(extPlist))
+
+    return config
+  })
+}
+
+module.exports = {withExtensionInfoPlist}
diff --git a/plugins/shareExtension/withExtensionViewController.js b/plugins/shareExtension/withExtensionViewController.js
new file mode 100644
index 000000000..cd29bea7d
--- /dev/null
+++ b/plugins/shareExtension/withExtensionViewController.js
@@ -0,0 +1,31 @@
+const {withXcodeProject} = require('@expo/config-plugins')
+const path = require('path')
+const fs = require('fs')
+
+const withExtensionViewController = (
+  config,
+  {controllerName, extensionName},
+) => {
+  // eslint-disable-next-line no-shadow
+  return withXcodeProject(config, config => {
+    const controllerPath = path.join(
+      config.modRequest.projectRoot,
+      'modules',
+      extensionName,
+      `${controllerName}.swift`,
+    )
+
+    const targetPath = path.join(
+      config.modRequest.platformProjectRoot,
+      extensionName,
+      `${controllerName}.swift`,
+    )
+
+    fs.mkdirSync(path.dirname(targetPath), {recursive: true})
+    fs.copyFileSync(controllerPath, targetPath)
+
+    return config
+  })
+}
+
+module.exports = {withExtensionViewController}
diff --git a/plugins/shareExtension/withIntentFilters.js b/plugins/shareExtension/withIntentFilters.js
new file mode 100644
index 000000000..605fcfd05
--- /dev/null
+++ b/plugins/shareExtension/withIntentFilters.js
@@ -0,0 +1,89 @@
+const {withAndroidManifest} = require('@expo/config-plugins')
+
+const withIntentFilters = config => {
+  // eslint-disable-next-line no-shadow
+  return withAndroidManifest(config, config => {
+    const intents = [
+      {
+        action: [
+          {
+            $: {
+              'android:name': 'android.intent.action.SEND',
+            },
+          },
+        ],
+        category: [
+          {
+            $: {
+              'android:name': 'android.intent.category.DEFAULT',
+            },
+          },
+        ],
+        data: [
+          {
+            $: {
+              'android:mimeType': 'image/*',
+            },
+          },
+        ],
+      },
+      {
+        action: [
+          {
+            $: {
+              'android:name': 'android.intent.action.SEND',
+            },
+          },
+        ],
+        category: [
+          {
+            $: {
+              'android:name': 'android.intent.category.DEFAULT',
+            },
+          },
+        ],
+        data: [
+          {
+            $: {
+              'android:mimeType': 'text/plain',
+            },
+          },
+        ],
+      },
+      {
+        action: [
+          {
+            $: {
+              'android:name': 'android.intent.action.SEND_MULTIPLE',
+            },
+          },
+        ],
+        category: [
+          {
+            $: {
+              'android:name': 'android.intent.category.DEFAULT',
+            },
+          },
+        ],
+        data: [
+          {
+            $: {
+              'android:mimeType': 'image/*',
+            },
+          },
+        ],
+      },
+    ]
+
+    const intentFilter =
+      config.modResults.manifest.application?.[0].activity?.[0]['intent-filter']
+
+    if (intentFilter) {
+      intentFilter.push(...intents)
+    }
+
+    return config
+  })
+}
+
+module.exports = {withIntentFilters}
diff --git a/plugins/shareExtension/withShareExtensions.js b/plugins/shareExtension/withShareExtensions.js
new file mode 100644
index 000000000..55a26c75e
--- /dev/null
+++ b/plugins/shareExtension/withShareExtensions.js
@@ -0,0 +1,47 @@
+const {withPlugins} = require('@expo/config-plugins')
+const {withAppEntitlements} = require('./withAppEntitlements')
+const {withXcodeTarget} = require('./withXcodeTarget')
+const {withExtensionEntitlements} = require('./withExtensionEntitlements')
+const {withExtensionInfoPlist} = require('./withExtensionInfoPlist')
+const {withExtensionViewController} = require('./withExtensionViewController')
+const {withIntentFilters} = require('./withIntentFilters')
+
+const SHARE_EXTENSION_NAME = 'Share-with-Bluesky'
+const SHARE_EXTENSION_CONTROLLER_NAME = 'ShareViewController'
+
+const withShareExtensions = config => {
+  return withPlugins(config, [
+    // IOS
+    withAppEntitlements,
+    [
+      withExtensionEntitlements,
+      {
+        extensionName: SHARE_EXTENSION_NAME,
+      },
+    ],
+    [
+      withExtensionInfoPlist,
+      {
+        extensionName: SHARE_EXTENSION_NAME,
+      },
+    ],
+    [
+      withExtensionViewController,
+      {
+        extensionName: SHARE_EXTENSION_NAME,
+        controllerName: SHARE_EXTENSION_CONTROLLER_NAME,
+      },
+    ],
+    [
+      withXcodeTarget,
+      {
+        extensionName: SHARE_EXTENSION_NAME,
+        controllerName: SHARE_EXTENSION_CONTROLLER_NAME,
+      },
+    ],
+    // Android
+    withIntentFilters,
+  ])
+}
+
+module.exports = withShareExtensions
diff --git a/plugins/shareExtension/withXcodeTarget.js b/plugins/shareExtension/withXcodeTarget.js
new file mode 100644
index 000000000..4f43c0926
--- /dev/null
+++ b/plugins/shareExtension/withXcodeTarget.js
@@ -0,0 +1,55 @@
+const {withXcodeProject} = require('@expo/config-plugins')
+
+const withXcodeTarget = (config, {extensionName, controllerName}) => {
+  // eslint-disable-next-line no-shadow
+  return withXcodeProject(config, config => {
+    const pbxProject = config.modResults
+
+    const target = pbxProject.addTarget(
+      extensionName,
+      'app_extension',
+      extensionName,
+    )
+    pbxProject.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', target.uuid)
+    pbxProject.addBuildPhase(
+      [],
+      'PBXResourcesBuildPhase',
+      'Resources',
+      target.uuid,
+    )
+    const pbxGroupKey = pbxProject.pbxCreateGroup(extensionName, extensionName)
+    pbxProject.addFile(`${extensionName}/Info.plist`, pbxGroupKey)
+    pbxProject.addSourceFile(
+      `${extensionName}/${controllerName}.swift`,
+      {target: target.uuid},
+      pbxGroupKey,
+    )
+
+    var configurations = pbxProject.pbxXCBuildConfigurationSection()
+    for (var key in configurations) {
+      if (typeof configurations[key].buildSettings !== 'undefined') {
+        var buildSettingsObj = configurations[key].buildSettings
+        if (
+          typeof buildSettingsObj.PRODUCT_NAME !== 'undefined' &&
+          buildSettingsObj.PRODUCT_NAME === `"${extensionName}"`
+        ) {
+          buildSettingsObj.CLANG_ENABLE_MODULES = 'YES'
+          buildSettingsObj.INFOPLIST_FILE = `"${extensionName}/Info.plist"`
+          buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${extensionName}/${extensionName}.entitlements"`
+          buildSettingsObj.CODE_SIGN_STYLE = 'Automatic'
+          buildSettingsObj.CURRENT_PROJECT_VERSION = `"${config.ios?.buildNumber}"`
+          buildSettingsObj.GENERATE_INFOPLIST_FILE = 'YES'
+          buildSettingsObj.MARKETING_VERSION = `"${config.version}"`
+          buildSettingsObj.PRODUCT_BUNDLE_IDENTIFIER = `"${config.ios?.bundleIdentifier}.${extensionName}"`
+          buildSettingsObj.SWIFT_EMIT_LOC_STRINGS = 'YES'
+          buildSettingsObj.SWIFT_VERSION = '5.0'
+          buildSettingsObj.TARGETED_DEVICE_FAMILY = `"1,2"`
+        }
+      }
+    }
+
+    return config
+  })
+}
+
+module.exports = {withXcodeTarget}