about summary refs log tree commit diff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/BlueskyNSE/BlueskyNSE.entitlements10
-rw-r--r--modules/BlueskyNSE/Info.plist29
-rw-r--r--modules/BlueskyNSE/NotificationService.swift51
-rw-r--r--modules/Share-with-Bluesky/Info.plist2
-rw-r--r--modules/Share-with-Bluesky/Share-with-Bluesky.entitlements2
-rw-r--r--modules/expo-background-notification-handler/android/build.gradle93
-rw-r--r--modules/expo-background-notification-handler/android/src/main/AndroidManifest.xml2
-rw-r--r--modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt39
-rw-r--r--modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandlerInterface.kt7
-rw-r--r--modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt70
-rw-r--r--modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt134
-rw-r--r--modules/expo-background-notification-handler/expo-module.config.json9
-rw-r--r--modules/expo-background-notification-handler/index.ts2
-rw-r--r--modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandler.podspec21
-rw-r--r--modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift116
-rw-r--r--modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx70
-rw-r--r--modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts40
-rw-r--r--modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.ts8
-rw-r--r--modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts27
19 files changed, 730 insertions, 2 deletions
diff --git a/modules/BlueskyNSE/BlueskyNSE.entitlements b/modules/BlueskyNSE/BlueskyNSE.entitlements
new file mode 100644
index 000000000..4954bdb33
--- /dev/null
+++ b/modules/BlueskyNSE/BlueskyNSE.entitlements
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>com.apple.security.application-groups</key>
+    <array>
+      <string>group.app.bsky</string>
+    </array>
+  </dict>
+</plist>
\ No newline at end of file
diff --git a/modules/BlueskyNSE/Info.plist b/modules/BlueskyNSE/Info.plist
new file mode 100644
index 000000000..c2dd7eda6
--- /dev/null
+++ b/modules/BlueskyNSE/Info.plist
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>NSExtension</key>
+    <dict>
+      <key>NSExtensionPointIdentifier</key>
+      <string>com.apple.usernotifications.service</string>
+      <key>NSExtensionPrincipalClass</key>
+      <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
+    </dict>
+    <key>MainAppScheme</key>
+    <string>bluesky</string>
+    <key>CFBundleName</key>
+    <string>$(PRODUCT_NAME)</string>
+    <key>CFBundleDisplayName</key>
+    <string>Bluesky Notifications</string>
+    <key>CFBundleIdentifier</key>
+    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+    <key>CFBundleVersion</key>
+    <string>$(CURRENT_PROJECT_VERSION)</string>
+    <key>CFBundleExecutable</key>
+    <string>$(EXECUTABLE_NAME)</string>
+    <key>CFBundlePackageType</key>
+    <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+    <key>CFBundleShortVersionString</key>
+    <string>$(MARKETING_VERSION)</string>
+  </dict>
+</plist>
\ No newline at end of file
diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift
new file mode 100644
index 000000000..c6f391e00
--- /dev/null
+++ b/modules/BlueskyNSE/NotificationService.swift
@@ -0,0 +1,51 @@
+import UserNotifications
+
+let APP_GROUP = "group.app.bsky"
+
+class NotificationService: UNNotificationServiceExtension {
+  var prefs = UserDefaults(suiteName: APP_GROUP)
+
+  override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+    guard var bestAttempt = createCopy(request.content),
+          let reason = request.content.userInfo["reason"] as? String
+    else {
+      contentHandler(request.content)
+      return
+    }
+    
+    if reason == "chat-message" {
+      mutateWithChatMessage(bestAttempt)
+    }
+    
+    // The badge should always be incremented when in the background
+    mutateWithBadge(bestAttempt)
+    
+    contentHandler(bestAttempt)
+  }
+  
+  override func serviceExtensionTimeWillExpire() {
+    // If for some reason the alloted time expires, we don't actually want to display a notification
+  }
+  
+  func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
+    return content.mutableCopy() as? UNMutableNotificationContent
+  }
+  
+  func mutateWithBadge(_ content: UNMutableNotificationContent) {
+    content.badge = 1
+  }
+  
+  func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
+    if self.prefs?.bool(forKey: "playSoundChat") == true {
+      mutateWithDmSound(content)
+    }
+  }
+  
+  func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
+    content.sound = UNNotificationSound.default
+  }
+  
+  func mutateWithDmSound(_ content: UNMutableNotificationContent) {
+    content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
+  }
+}
diff --git a/modules/Share-with-Bluesky/Info.plist b/modules/Share-with-Bluesky/Info.plist
index 90fe92345..421abb3c4 100644
--- a/modules/Share-with-Bluesky/Info.plist
+++ b/modules/Share-with-Bluesky/Info.plist
@@ -38,4 +38,4 @@
     <key>CFBundleShortVersionString</key>
     <string>$(MARKETING_VERSION)</string>
   </dict>
-</plist>
+</plist>
\ No newline at end of file
diff --git a/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements b/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements
index d2253d31f..4954bdb33 100644
--- a/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements
+++ b/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements
@@ -7,4 +7,4 @@
       <string>group.app.bsky</string>
     </array>
   </dict>
-</plist>
+</plist>
\ No newline at end of file
diff --git a/modules/expo-background-notification-handler/android/build.gradle b/modules/expo-background-notification-handler/android/build.gradle
new file mode 100644
index 000000000..e18eee934
--- /dev/null
+++ b/modules/expo-background-notification-handler/android/build.gradle
@@ -0,0 +1,93 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'maven-publish'
+
+group = 'expo.modules.backgroundnotificationhandler'
+version = '0.5.0'
+
+buildscript {
+  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
+  if (expoModulesCorePlugin.exists()) {
+    apply from: expoModulesCorePlugin
+    applyKotlinExpoModulesCorePlugin()
+  }
+
+  // Simple helper that allows the root project to override versions declared by this library.
+  ext.safeExtGet = { prop, fallback ->
+    rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+  }
+
+  // Ensures backward compatibility
+  ext.getKotlinVersion = {
+    if (ext.has("kotlinVersion")) {
+      ext.kotlinVersion()
+    } else {
+      ext.safeExtGet("kotlinVersion", "1.8.10")
+    }
+  }
+
+  repositories {
+    mavenCentral()
+  }
+
+  dependencies {
+    classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
+  }
+}
+
+afterEvaluate {
+  publishing {
+    publications {
+      release(MavenPublication) {
+        from components.release
+      }
+    }
+    repositories {
+      maven {
+        url = mavenLocal().url
+      }
+    }
+  }
+}
+
+android {
+  compileSdkVersion safeExtGet("compileSdkVersion", 33)
+
+  def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
+  if (agpVersion.tokenize('.')[0].toInteger() < 8) {
+    compileOptions {
+      sourceCompatibility JavaVersion.VERSION_11
+      targetCompatibility JavaVersion.VERSION_11
+    }
+
+    kotlinOptions {
+      jvmTarget = JavaVersion.VERSION_11.majorVersion
+    }
+  }
+
+  namespace "expo.modules.backgroundnotificationhandler"
+  defaultConfig {
+    minSdkVersion safeExtGet("minSdkVersion", 21)
+    targetSdkVersion safeExtGet("targetSdkVersion", 34)
+    versionCode 1
+    versionName "0.5.0"
+  }
+  lintOptions {
+    abortOnError false
+  }
+  publishing {
+    singleVariant("release") {
+      withSourcesJar()
+    }
+  }
+}
+
+repositories {
+  mavenCentral()
+}
+
+dependencies {
+  implementation project(':expo-modules-core')
+  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
+  implementation 'com.google.firebase:firebase-messaging-ktx:24.0.0'
+}
diff --git a/modules/expo-background-notification-handler/android/src/main/AndroidManifest.xml b/modules/expo-background-notification-handler/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..bdae66c8f
--- /dev/null
+++ b/modules/expo-background-notification-handler/android/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<manifest>
+</manifest>
diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt
new file mode 100644
index 000000000..344508523
--- /dev/null
+++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt
@@ -0,0 +1,39 @@
+package expo.modules.backgroundnotificationhandler
+
+import android.content.Context
+import com.google.firebase.messaging.RemoteMessage
+
+class BackgroundNotificationHandler(
+  private val context: Context,
+  private val notifInterface: BackgroundNotificationHandlerInterface
+) {
+  fun handleMessage(remoteMessage: RemoteMessage) {
+    if (ExpoBackgroundNotificationHandlerModule.isForegrounded) {
+      // We'll let expo-notifications handle the notification if the app is foregrounded
+      return
+    }
+
+    if (remoteMessage.data["reason"] == "chat-message") {
+      mutateWithChatMessage(remoteMessage)
+    }
+
+    notifInterface.showMessage(remoteMessage)
+  }
+
+  private fun mutateWithChatMessage(remoteMessage: RemoteMessage) {
+    if (NotificationPrefs(context).getBoolean("playSoundChat")) {
+      // If oreo or higher
+      if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+        remoteMessage.data["channelId"] = "chat-messages"
+      } else {
+        remoteMessage.data["sound"] = "dm.mp3"
+      }
+    } else {
+      if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+        remoteMessage.data["channelId"] = "chat-messages-muted"
+      } else {
+        remoteMessage.data["sound"] = null
+      }
+    }
+  }
+}
diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandlerInterface.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandlerInterface.kt
new file mode 100644
index 000000000..41fb65eb6
--- /dev/null
+++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandlerInterface.kt
@@ -0,0 +1,7 @@
+package expo.modules.backgroundnotificationhandler
+
+import com.google.firebase.messaging.RemoteMessage
+
+interface BackgroundNotificationHandlerInterface {
+  fun showMessage(remoteMessage: RemoteMessage)
+}
diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt
new file mode 100644
index 000000000..083ff1223
--- /dev/null
+++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt
@@ -0,0 +1,70 @@
+package expo.modules.backgroundnotificationhandler
+
+import expo.modules.kotlin.modules.Module
+import expo.modules.kotlin.modules.ModuleDefinition
+
+class ExpoBackgroundNotificationHandlerModule : Module() {
+  companion object {
+    var isForegrounded = false
+  }
+
+  override fun definition() = ModuleDefinition {
+    Name("ExpoBackgroundNotificationHandler")
+
+    OnCreate {
+      NotificationPrefs(appContext.reactContext).initialize()
+    }
+
+    OnActivityEntersForeground {
+      isForegrounded = true
+    }
+
+    OnActivityEntersBackground {
+      isForegrounded = false
+    }
+
+    AsyncFunction("getAllPrefsAsync") {
+      return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
+    }
+
+    AsyncFunction("getBoolAsync") { forKey: String ->
+      return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
+    }
+
+    AsyncFunction("getStringAsync") { forKey: String ->
+      return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
+    }
+
+    AsyncFunction("getStringArrayAsync") { forKey: String ->
+      return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
+    }
+
+    AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
+      NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
+    }
+
+    AsyncFunction("setStringAsync") { forKey: String, value: String ->
+      NotificationPrefs(appContext.reactContext).setString(forKey, value)
+    }
+
+    AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> ->
+      NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
+    }
+
+    AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
+      NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
+    }
+
+    AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
+      NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
+    }
+
+    AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> ->
+      NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
+    }
+
+    AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
+      NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
+    }
+  }
+}
diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt
new file mode 100644
index 000000000..17ef9205e
--- /dev/null
+++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt
@@ -0,0 +1,134 @@
+package expo.modules.backgroundnotificationhandler
+
+import android.content.Context
+
+val DEFAULTS = mapOf<String, Any>(
+  "playSoundChat" to true,
+  "playSoundFollow" to false,
+  "playSoundLike" to false,
+  "playSoundMention" to false,
+  "playSoundQuote" to false,
+  "playSoundReply" to false,
+  "playSoundRepost" to false,
+  "mutedThreads" to mapOf<String, List<String>>()
+)
+
+class NotificationPrefs (private val context: Context?) {
+  private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
+    ?: throw Error("Context is null")
+
+  fun initialize() {
+    prefs
+      .edit()
+      .apply {
+        DEFAULTS.forEach { (key, value) ->
+          if (prefs.contains(key)) {
+            return@forEach
+          }
+
+          when (value) {
+            is Boolean -> {
+              putBoolean(key, value)
+            }
+            is String -> {
+              putString(key, value)
+            }
+            is Array<*> -> {
+              putStringSet(key, value.map { it.toString() }.toSet())
+            }
+            is Map<*, *> -> {
+              putStringSet(key, value.map { it.toString() }.toSet())
+            }
+          }
+        }
+      }
+      .apply()
+  }
+
+  fun getAllPrefs(): MutableMap<String, *> {
+    return prefs.all
+  }
+
+  fun getBoolean(key: String): Boolean {
+    return prefs.getBoolean(key, false)
+  }
+
+  fun getString(key: String): String? {
+    return prefs.getString(key, null)
+  }
+
+  fun getStringArray(key: String): Array<String>? {
+    return prefs.getStringSet(key, null)?.toTypedArray()
+  }
+
+  fun setBoolean(key: String, value: Boolean) {
+    prefs
+      .edit()
+      .apply {
+        putBoolean(key, value)
+      }
+      .apply()
+  }
+
+  fun setString(key: String, value: String) {
+    prefs
+      .edit()
+      .apply {
+        putString(key, value)
+      }
+      .apply()
+  }
+
+  fun setStringArray(key: String, value: Array<String>) {
+    prefs
+      .edit()
+      .apply {
+        putStringSet(key, value.toSet())
+      }
+      .apply()
+  }
+
+  fun addToStringArray(key: String, string: String) {
+    prefs
+      .edit()
+      .apply {
+        val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
+        set.add(string)
+        putStringSet(key, set)
+      }
+      .apply()
+  }
+
+  fun removeFromStringArray(key: String, string: String) {
+    prefs
+      .edit()
+      .apply {
+        val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
+        set.remove(string)
+        putStringSet(key, set)
+      }
+      .apply()
+  }
+
+  fun addManyToStringArray(key: String, strings: Array<String>) {
+    prefs
+      .edit()
+      .apply {
+        val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
+        set.addAll(strings.toSet())
+        putStringSet(key, set)
+      }
+      .apply()
+  }
+
+  fun removeManyFromStringArray(key: String, strings: Array<String>) {
+    prefs
+      .edit()
+      .apply {
+        val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
+        set.removeAll(strings.toSet())
+        putStringSet(key, set)
+      }
+      .apply()
+  }
+}
\ No newline at end of file
diff --git a/modules/expo-background-notification-handler/expo-module.config.json b/modules/expo-background-notification-handler/expo-module.config.json
new file mode 100644
index 000000000..9e5c9d550
--- /dev/null
+++ b/modules/expo-background-notification-handler/expo-module.config.json
@@ -0,0 +1,9 @@
+{
+  "platforms": ["ios", "android"],
+  "ios": {
+    "modules": ["ExpoBackgroundNotificationHandlerModule"]
+  },
+  "android": {
+    "modules": ["expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule"]
+  }
+}
diff --git a/modules/expo-background-notification-handler/index.ts b/modules/expo-background-notification-handler/index.ts
new file mode 100644
index 000000000..680c6c13f
--- /dev/null
+++ b/modules/expo-background-notification-handler/index.ts
@@ -0,0 +1,2 @@
+import {BackgroundNotificationHandler} from './src/ExpoBackgroundNotificationHandlerModule'
+export default BackgroundNotificationHandler
diff --git a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandler.podspec b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandler.podspec
new file mode 100644
index 000000000..363c7b5e6
--- /dev/null
+++ b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandler.podspec
@@ -0,0 +1,21 @@
+Pod::Spec.new do |s|
+  s.name           = 'ExpoBackgroundNotificationHandler'
+  s.version        = '1.0.0'
+  s.summary        = 'Interface for BlueskyNSE preferences'
+  s.description    = 'Interface for BlueskyNSE preferenes'
+  s.author         = ''
+  s.homepage       = 'https://github.com/bluesky-social/social-app'
+  s.platforms      = { :ios => '13.4', :tvos => '13.4' }
+  s.source         = { git: '' }
+  s.static_framework = true
+
+  s.dependency 'ExpoModulesCore'
+
+  # Swift/Objective-C compatibility
+  s.pod_target_xcconfig = {
+    'DEFINES_MODULE' => 'YES',
+    'SWIFT_COMPILATION_MODE' => 'wholemodule'
+  }
+
+  s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
+end
diff --git a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift
new file mode 100644
index 000000000..08972a04c
--- /dev/null
+++ b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift
@@ -0,0 +1,116 @@
+import ExpoModulesCore
+
+let APP_GROUP = "group.app.bsky"
+
+let DEFAULTS: [String:Any] = [
+  "playSoundChat" : true,
+  "playSoundFollow": false,
+  "playSoundLike": false,
+  "playSoundMention": false,
+  "playSoundQuote": false,
+  "playSoundReply": false,
+  "playSoundRepost": false,
+  "mutedThreads": [:] as! [String:[String]]
+]
+
+/*
+ * The purpose of this module is to store values that are needed by the notification service
+ * extension. Since we would rather get and store values such as age or user mute state
+ * while the app is foregrounded, we should use this module liberally. We should aim to keep
+ * background fetches to a minimum (two or three times per hour) while the app is backgrounded
+ * or killed
+ */
+public class ExpoBackgroundNotificationHandlerModule: Module {
+  let userDefaults = UserDefaults(suiteName: APP_GROUP)
+  
+  public func definition() -> ModuleDefinition {
+    Name("ExpoBackgroundNotificationHandler")
+    
+    OnCreate {
+      DEFAULTS.forEach { p in
+        if userDefaults?.value(forKey: p.key) == nil {
+          userDefaults?.setValue(p.value, forKey: p.key)
+        }
+      }
+    }
+    
+    AsyncFunction("getAllPrefsAsync") { () -> [String:Any]? in
+      var keys: [String] = []
+      DEFAULTS.forEach { p in
+        keys.append(p.key)
+      }
+      return userDefaults?.dictionaryWithValues(forKeys: keys)
+    }
+    
+    AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in
+      if let pref = userDefaults?.bool(forKey: forKey) {
+        return pref
+      }
+      return false
+    }
+    
+    AsyncFunction("getStringAsync") { (forKey: String) -> String? in
+      if let pref = userDefaults?.string(forKey: forKey) {
+        return pref
+      }
+      return nil
+    }
+    
+    AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in
+      if let pref = userDefaults?.stringArray(forKey: forKey) {
+        return pref
+      }
+      return nil
+    }
+    
+    AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) -> Void in
+      userDefaults?.setValue(value, forKey: forKey)
+    }
+    
+    AsyncFunction("setStringAsync") { (forKey: String, value: String) -> Void in
+      userDefaults?.setValue(value, forKey: forKey)
+    }
+    
+    AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) -> Void in
+      userDefaults?.setValue(value, forKey: forKey)
+    }
+    
+    AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in
+      if var curr = userDefaults?.stringArray(forKey: forKey),
+         !curr.contains(string)
+      {
+        curr.append(string)
+        userDefaults?.setValue(curr, forKey: forKey)
+      }
+    }
+    
+    AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in
+      if var curr = userDefaults?.stringArray(forKey: forKey) {
+        curr.removeAll { s in
+          return s == string
+        }
+        userDefaults?.setValue(curr, forKey: forKey)
+      }
+    }
+    
+    AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in
+      if var curr = userDefaults?.stringArray(forKey: forKey) {
+        strings.forEach { s in
+          if !curr.contains(s) {
+            curr.append(s)
+          }
+        }
+        userDefaults?.setValue(curr, forKey: forKey)
+      }
+    }
+    
+    AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in
+      if var curr = userDefaults?.stringArray(forKey: forKey) {
+        strings.forEach { s in
+          curr.removeAll(where: { $0 == s })
+        }
+        userDefaults?.setValue(curr, forKey: forKey)
+      }
+    }
+  }
+}
diff --git a/modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx b/modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx
new file mode 100644
index 000000000..6ecdd1d47
--- /dev/null
+++ b/modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx
@@ -0,0 +1,70 @@
+import React from 'react'
+
+import {BackgroundNotificationHandlerPreferences} from './ExpoBackgroundNotificationHandler.types'
+import {BackgroundNotificationHandler} from './ExpoBackgroundNotificationHandlerModule'
+
+interface BackgroundNotificationPreferencesContext {
+  preferences: BackgroundNotificationHandlerPreferences
+  setPref: <Key extends keyof BackgroundNotificationHandlerPreferences>(
+    key: Key,
+    value: BackgroundNotificationHandlerPreferences[Key],
+  ) => void
+}
+
+const Context = React.createContext<BackgroundNotificationPreferencesContext>(
+  {} as BackgroundNotificationPreferencesContext,
+)
+export const useBackgroundNotificationPreferences = () =>
+  React.useContext(Context)
+
+export function BackgroundNotificationPreferencesProvider({
+  children,
+}: {
+  children: React.ReactNode
+}) {
+  const [preferences, setPreferences] =
+    React.useState<BackgroundNotificationHandlerPreferences>({
+      playSoundChat: true,
+    })
+
+  React.useEffect(() => {
+    ;(async () => {
+      const prefs = await BackgroundNotificationHandler.getAllPrefsAsync()
+      setPreferences(prefs)
+    })()
+  }, [])
+
+  const value = React.useMemo(
+    () => ({
+      preferences,
+      setPref: async <
+        Key extends keyof BackgroundNotificationHandlerPreferences,
+      >(
+        k: Key,
+        v: BackgroundNotificationHandlerPreferences[Key],
+      ) => {
+        switch (typeof v) {
+          case 'boolean': {
+            await BackgroundNotificationHandler.setBoolAsync(k, v)
+            break
+          }
+          case 'string': {
+            await BackgroundNotificationHandler.setStringAsync(k, v)
+            break
+          }
+          default: {
+            throw new Error(`Invalid type for value: ${typeof v}`)
+          }
+        }
+
+        setPreferences(prev => ({
+          ...prev,
+          [k]: v,
+        }))
+      },
+    }),
+    [preferences],
+  )
+
+  return <Context.Provider value={value}>{children}</Context.Provider>
+}
diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts
new file mode 100644
index 000000000..5fbd302da
--- /dev/null
+++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts
@@ -0,0 +1,40 @@
+export type ExpoBackgroundNotificationHandlerModule = {
+  getAllPrefsAsync: () => Promise<BackgroundNotificationHandlerPreferences>
+  getBoolAsync: (forKey: string) => Promise<boolean>
+  getStringAsync: (forKey: string) => Promise<string>
+  getStringArrayAsync: (forKey: string) => Promise<string[]>
+  setBoolAsync: (
+    forKey: keyof BackgroundNotificationHandlerPreferences,
+    value: boolean,
+  ) => Promise<void>
+  setStringAsync: (
+    forKey: keyof BackgroundNotificationHandlerPreferences,
+    value: string,
+  ) => Promise<void>
+  setStringArrayAsync: (
+    forKey: keyof BackgroundNotificationHandlerPreferences,
+    value: string[],
+  ) => Promise<void>
+  addToStringArrayAsync: (
+    forKey: keyof BackgroundNotificationHandlerPreferences,
+    value: string,
+  ) => Promise<void>
+  removeFromStringArrayAsync: (
+    forKey: keyof BackgroundNotificationHandlerPreferences,
+    value: string,
+  ) => Promise<void>
+  addManyToStringArrayAsync: (
+    forKey: keyof BackgroundNotificationHandlerPreferences,
+    value: string[],
+  ) => Promise<void>
+  removeManyFromStringArrayAsync: (
+    forKey: keyof BackgroundNotificationHandlerPreferences,
+    value: string[],
+  ) => Promise<void>
+}
+
+// TODO there are more preferences in the native code, however they have not been added here yet.
+// Don't add them until the native logic also handles the notifications for those preference types.
+export type BackgroundNotificationHandlerPreferences = {
+  playSoundChat: boolean
+}
diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.ts
new file mode 100644
index 000000000..d6517893a
--- /dev/null
+++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.ts
@@ -0,0 +1,8 @@
+import {requireNativeModule} from 'expo-modules-core'
+
+import {ExpoBackgroundNotificationHandlerModule} from './ExpoBackgroundNotificationHandler.types'
+
+export const BackgroundNotificationHandler =
+  requireNativeModule<ExpoBackgroundNotificationHandlerModule>(
+    'ExpoBackgroundNotificationHandler',
+  )
diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts
new file mode 100644
index 000000000..29e27fd0f
--- /dev/null
+++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts
@@ -0,0 +1,27 @@
+import {
+  BackgroundNotificationHandlerPreferences,
+  ExpoBackgroundNotificationHandlerModule,
+} from './ExpoBackgroundNotificationHandler.types'
+
+// Stub for web
+export const BackgroundNotificationHandler = {
+  getAllPrefsAsync: async () => {
+    return {} as BackgroundNotificationHandlerPreferences
+  },
+  getBoolAsync: async (_: string) => {
+    return false
+  },
+  getStringAsync: async (_: string) => {
+    return ''
+  },
+  getStringArrayAsync: async (_: string) => {
+    return []
+  },
+  setBoolAsync: async (_: string, __: boolean) => {},
+  setStringAsync: async (_: string, __: string) => {},
+  setStringArrayAsync: async (_: string, __: string[]) => {},
+  addToStringArrayAsync: async (_: string, __: string) => {},
+  removeFromStringArrayAsync: async (_: string, __: string) => {},
+  addManyToStringArrayAsync: async (_: string, __: string[]) => {},
+  removeManyFromStringArrayAsync: async (_: string, __: string[]) => {},
+} as ExpoBackgroundNotificationHandlerModule