about summary refs log tree commit diff
path: root/plugins
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-05-15 11:49:07 -0700
committerGitHub <noreply@github.com>2024-05-15 11:49:07 -0700
commitbf7b66d5c1c0d7f7bdcc1c1aa43b6881d797d1e8 (patch)
treedfbbd3babc73b4251883b8dc786b1dbea7f08c1a /plugins
parent31868b255f7be001821e03033b3bdd1070ea28cf (diff)
downloadvoidsky-bf7b66d5c1c0d7f7bdcc1c1aa43b6881d797d1e8.tar.zst
Add push notification extensions (#4005)
* add wav

* add sound to config

* add extension to `updateExtensions.sh`

* add ios source files

* add a build extension

* add a new module

* use correct type on ios

* update the build plugin

* add android handler

* create a patch for expo-notifications

* basic android implementation

* add entitlements for notifications extension

* add some generic logic for ios

* add age check logic

* add extension to app config

* remove dash

* move directory

* rename again

* update privacy manifest

* add prefs storage ios

* better types

* create interface for setting and getting prefs

* add notifications prefs for android

* add functions to module

* add types to js

* add prefs context

* add web stub

* wrap the app

* fix types

* more preferences for ios

* add a test toggle

* swap vars

* update patch

* fix patch error

* fix typo

* sigh

* sigh

* get stored prefs on launch

* anotehr type

* simplify

* about finished

* comment

* adjust plugin

* use supported file types

* update NSE

* futureproof ios

* futureproof android

* update sound file name

* handle initialization

* more cleanup

* update js types

* strict js types

* set the notification channel

* rm

* add silent channel

* add mute logic

* update patch

* podfile

* adjust channels

* fix android channel

* update readme

* oreo or higher

* nit

* don't use getValue

* nit
Diffstat (limited to 'plugins')
-rw-r--r--plugins/notificationsExtension/README.md17
-rw-r--r--plugins/notificationsExtension/withAppEntitlements.js13
-rw-r--r--plugins/notificationsExtension/withExtensionEntitlements.js31
-rw-r--r--plugins/notificationsExtension/withExtensionInfoPlist.js39
-rw-r--r--plugins/notificationsExtension/withExtensionViewController.js31
-rw-r--r--plugins/notificationsExtension/withNotificationsExtension.js55
-rw-r--r--plugins/notificationsExtension/withSounds.js27
-rw-r--r--plugins/notificationsExtension/withXcodeTarget.js76
8 files changed, 289 insertions, 0 deletions
diff --git a/plugins/notificationsExtension/README.md b/plugins/notificationsExtension/README.md
new file mode 100644
index 000000000..31b8bfe7d
--- /dev/null
+++ b/plugins/notificationsExtension/README.md
@@ -0,0 +1,17 @@
+# Notifications extension plugin for Expo
+
+This plugin handles moving the necessary files into their respective iOS directories
+
+## 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
+
+## 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/notificationsExtension/withAppEntitlements.js b/plugins/notificationsExtension/withAppEntitlements.js
new file mode 100644
index 000000000..4ce81ea61
--- /dev/null
+++ b/plugins/notificationsExtension/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.app.bsky`,
+    ]
+    return config
+  })
+}
+
+module.exports = {withAppEntitlements}
diff --git a/plugins/notificationsExtension/withExtensionEntitlements.js b/plugins/notificationsExtension/withExtensionEntitlements.js
new file mode 100644
index 000000000..0cc1c4ca8
--- /dev/null
+++ b/plugins/notificationsExtension/withExtensionEntitlements.js
@@ -0,0 +1,31 @@
+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 notificationsExtensionEntitlements = {
+      'com.apple.security.application-groups': [`group.app.bsky`],
+    }
+
+    fs.mkdirSync(path.dirname(extensionEntitlementsPath), {
+      recursive: true,
+    })
+    fs.writeFileSync(
+      extensionEntitlementsPath,
+      plist.default.build(notificationsExtensionEntitlements),
+    )
+
+    return config
+  })
+}
+
+module.exports = {withExtensionEntitlements}
diff --git a/plugins/notificationsExtension/withExtensionInfoPlist.js b/plugins/notificationsExtension/withExtensionInfoPlist.js
new file mode 100644
index 000000000..b0c6cfa89
--- /dev/null
+++ b/plugins/notificationsExtension/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 = 'Bluesky Notifications'
+    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/notificationsExtension/withExtensionViewController.js b/plugins/notificationsExtension/withExtensionViewController.js
new file mode 100644
index 000000000..cd29bea7d
--- /dev/null
+++ b/plugins/notificationsExtension/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/notificationsExtension/withNotificationsExtension.js b/plugins/notificationsExtension/withNotificationsExtension.js
new file mode 100644
index 000000000..6a00cfd23
--- /dev/null
+++ b/plugins/notificationsExtension/withNotificationsExtension.js
@@ -0,0 +1,55 @@
+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 {withSounds} = require('./withSounds')
+
+const EXTENSION_NAME = 'BlueskyNSE'
+const EXTENSION_CONTROLLER_NAME = 'NotificationService'
+
+const withNotificationsExtension = config => {
+  const soundFiles = ['dm.aiff']
+
+  return withPlugins(config, [
+    // IOS
+    withAppEntitlements,
+    [
+      withExtensionEntitlements,
+      {
+        extensionName: EXTENSION_NAME,
+      },
+    ],
+    [
+      withExtensionInfoPlist,
+      {
+        extensionName: EXTENSION_NAME,
+      },
+    ],
+    [
+      withExtensionViewController,
+      {
+        extensionName: EXTENSION_NAME,
+        controllerName: EXTENSION_CONTROLLER_NAME,
+      },
+    ],
+    [
+      withSounds,
+      {
+        extensionName: EXTENSION_NAME,
+        soundFiles,
+      },
+    ],
+    [
+      withXcodeTarget,
+      {
+        extensionName: EXTENSION_NAME,
+        controllerName: EXTENSION_CONTROLLER_NAME,
+        soundFiles,
+      },
+    ],
+  ])
+}
+
+module.exports = withNotificationsExtension
diff --git a/plugins/notificationsExtension/withSounds.js b/plugins/notificationsExtension/withSounds.js
new file mode 100644
index 000000000..652afd545
--- /dev/null
+++ b/plugins/notificationsExtension/withSounds.js
@@ -0,0 +1,27 @@
+const {withXcodeProject} = require('@expo/config-plugins')
+const path = require('path')
+const fs = require('fs')
+
+const withSounds = (config, {extensionName, soundFiles}) => {
+  // eslint-disable-next-line no-shadow
+  return withXcodeProject(config, config => {
+    for (const file of soundFiles) {
+      const soundPath = path.join(config.modRequest.projectRoot, 'assets', file)
+
+      const targetPath = path.join(
+        config.modRequest.platformProjectRoot,
+        extensionName,
+        file,
+      )
+
+      if (!fs.existsSync(path.dirname(targetPath))) {
+        fs.mkdirSync(path.dirname(targetPath), {recursive: true})
+      }
+      fs.copyFileSync(soundPath, targetPath)
+    }
+
+    return config
+  })
+}
+
+module.exports = {withSounds}
diff --git a/plugins/notificationsExtension/withXcodeTarget.js b/plugins/notificationsExtension/withXcodeTarget.js
new file mode 100644
index 000000000..e9c7dae39
--- /dev/null
+++ b/plugins/notificationsExtension/withXcodeTarget.js
@@ -0,0 +1,76 @@
+const {withXcodeProject, IOSConfig} = require('@expo/config-plugins')
+const path = require('path')
+const PBXFile = require('xcode/lib/pbxFile')
+
+const withXcodeTarget = (
+  config,
+  {extensionName, controllerName, soundFiles},
+) => {
+  // eslint-disable-next-line no-shadow
+  return withXcodeProject(config, config => {
+    let 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,
+    )
+
+    for (const file of soundFiles) {
+      pbxProject.addSourceFile(
+        `${extensionName}/${file}`,
+        {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"`
+          buildSettingsObj.DEVELOPMENT_TEAM = 'B3LX46C5HS'
+        }
+      }
+    }
+
+    pbxProject.addTargetAttribute(
+      'DevelopmentTeam',
+      'B3LX46C5HS',
+      extensionName,
+    )
+    pbxProject.addTargetAttribute('DevelopmentTeam', 'B3LX46C5HS')
+
+    return config
+  })
+}
+
+module.exports = {withXcodeTarget}