about summary refs log tree commit diff
path: root/modules
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-07-11 18:37:43 -0700
committerGitHub <noreply@github.com>2024-07-11 18:37:43 -0700
commit83e8522e0a89be28b1733f4c50dbd4379d98d03b (patch)
treec51a1054ffa8f1b226412a77fa7d69f5c891f7ae /modules
parent2397104ad6169ced02b1acd9fbbbe426f4cc4da0 (diff)
downloadvoidsky-83e8522e0a89be28b1733f4c50dbd4379d98d03b.tar.zst
Create shared preferences API (#4654)
Diffstat (limited to 'modules')
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt11
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt69
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt241
-rw-r--r--modules/expo-bluesky-swiss-army/expo-module.config.json4
-rw-r--r--modules/expo-bluesky-swiss-army/index.ts4
-rw-r--r--modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift23
-rw-r--r--modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift62
-rw-r--r--modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift89
-rw-r--r--modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts18
-rw-r--r--modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts16
-rw-r--r--modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts51
-rw-r--r--modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts36
12 files changed, 552 insertions, 72 deletions
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt
deleted file mode 100644
index 51f9fe45d..000000000
--- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package expo.modules.blueskyswissarmy.deviceprefs
-
-import expo.modules.kotlin.modules.Module
-import expo.modules.kotlin.modules.ModuleDefinition
-
-class ExpoBlueskyDevicePrefsModule : Module() {
-  override fun definition() =
-    ModuleDefinition {
-      Name("ExpoBlueskyDevicePrefs")
-    }
-}
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt
new file mode 100644
index 000000000..271956878
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt
@@ -0,0 +1,69 @@
+package expo.modules.blueskyswissarmy.sharedprefs
+
+import android.content.Context
+import android.util.Log
+import expo.modules.kotlin.jni.JavaScriptValue
+import expo.modules.kotlin.modules.Module
+import expo.modules.kotlin.modules.ModuleDefinition
+
+class ExpoBlueskySharedPrefsModule : Module() {
+  private fun getContext(): Context {
+    val context = appContext.reactContext ?: throw Error("Context is null")
+    return context
+  }
+
+  override fun definition() =
+    ModuleDefinition {
+      Name("ExpoBlueskySharedPrefs")
+
+      Function("setString") { key: String, value: String ->
+        return@Function SharedPrefs(getContext()).setValue(key, value)
+      }
+
+      Function("setValue") { key: String, value: JavaScriptValue ->
+        val context = getContext()
+        Log.d("ExpoBlueskySharedPrefs", "Setting value for key: $key")
+        try {
+          if (value.isNumber()) {
+            SharedPrefs(context).setValue(key, value.getFloat())
+          } else if (value.isBool()) {
+            SharedPrefs(context).setValue(key, value.getBool())
+          } else if (value.isNull() || value.isUndefined()) {
+            SharedPrefs(context).removeValue(key)
+          } else {
+            Log.d(NAME, "Unsupported type: ${value.kind()}")
+          }
+        } catch (e: Error) {
+          Log.d(NAME, "Error setting value: $e")
+        }
+      }
+
+      Function("removeValue") { key: String ->
+        return@Function SharedPrefs(getContext()).removeValue(key)
+      }
+
+      Function("getString") { key: String ->
+        return@Function SharedPrefs(getContext()).getString(key)
+      }
+
+      Function("getNumber") { key: String ->
+        return@Function SharedPrefs(getContext()).getFloat(key)
+      }
+
+      Function("getBool") { key: String ->
+        return@Function SharedPrefs(getContext()).getBoolean(key)
+      }
+
+      Function("addToSet") { key: String, value: String ->
+        return@Function SharedPrefs(getContext()).addToSet(key, value)
+      }
+
+      Function("removeFromSet") { key: String, value: String ->
+        return@Function SharedPrefs(getContext()).removeFromSet(key, value)
+      }
+
+      Function("setContains") { key: String, value: String ->
+        return@Function SharedPrefs(getContext()).setContains(key, value)
+      }
+    }
+}
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt
new file mode 100644
index 000000000..38d79abf1
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt
@@ -0,0 +1,241 @@
+package expo.modules.blueskyswissarmy.sharedprefs
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+
+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,
+    "badgeCount" to 0,
+  )
+
+const val NAME = "SharedPrefs"
+
+class SharedPrefs(
+  private val context: Context,
+) {
+  companion object {
+    private var hasInitialized = false
+
+    private var instance: SharedPreferences? = null
+
+    fun getInstance(
+      context: Context,
+      info: String? = "(no info)",
+    ): SharedPreferences {
+      if (instance == null) {
+        Log.d(NAME, "No preferences instance found, creating one.")
+        instance = context.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
+      }
+
+      val safeInstance = instance ?: throw Error("Preferences is null: $info")
+
+      if (!hasInitialized) {
+        Log.d(NAME, "Preferences instance has not been initialized yet.")
+        initialize(safeInstance)
+        hasInitialized = true
+        Log.d(NAME, "Preferences instance has been initialized.")
+      }
+
+      return safeInstance
+    }
+
+    private fun initialize(instance: SharedPreferences) {
+      instance
+        .edit()
+        .apply {
+          DEFAULTS.forEach { (key, value) ->
+            if (instance.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 setValue(
+    key: String,
+    value: String,
+  ) {
+    val safeInstance = getInstance(context)
+    safeInstance
+      .edit()
+      .apply {
+        putString(key, value)
+      }.apply()
+  }
+
+  fun setValue(
+    key: String,
+    value: Float,
+  ) {
+    val safeInstance = getInstance(context)
+    safeInstance
+      .edit()
+      .apply {
+        putFloat(key, value)
+      }.apply()
+  }
+
+  fun setValue(
+    key: String,
+    value: Boolean,
+  ) {
+    val safeInstance = getInstance(context)
+    safeInstance
+      .edit()
+      .apply {
+        putBoolean(key, value)
+      }.apply()
+  }
+
+  fun setValue(
+    key: String,
+    value: Set<String>,
+  ) {
+    val safeInstance = getInstance(context)
+    safeInstance
+      .edit()
+      .apply {
+        putStringSet(key, value)
+      }.apply()
+  }
+
+  fun removeValue(key: String) {
+    val safeInstance = getInstance(context)
+    safeInstance
+      .edit()
+      .apply {
+        remove(key)
+      }.apply()
+  }
+
+  fun getString(key: String): String? {
+    val safeInstance = getInstance(context)
+    return safeInstance.getString(key, null)
+  }
+
+  fun getFloat(key: String): Float? {
+    val safeInstance = getInstance(context)
+    if (!safeInstance.contains(key)) {
+      return null
+    }
+    return safeInstance.getFloat(key, 0.0f)
+  }
+
+  @Suppress("ktlint:standard:function-naming")
+  fun _setAnyValue(
+    key: String,
+    value: Any,
+  ) {
+    val safeInstance = getInstance(context)
+    safeInstance
+      .edit()
+      .apply {
+        when (value) {
+          is String -> putString(key, value)
+          is Float -> putFloat(key, value)
+          is Boolean -> putBoolean(key, value)
+          is Set<*> -> putStringSet(key, value.map { it.toString() }.toSet())
+          else -> throw Error("Unsupported type: ${value::class.java}")
+        }
+      }.apply()
+  }
+
+  fun getBoolean(key: String): Boolean? {
+    val safeInstance = getInstance(context)
+    if (!safeInstance.contains(key)) {
+      return null
+    }
+    Log.d(NAME, "Getting boolean for key: $key")
+    val res = safeInstance.getBoolean(key, false)
+    Log.d(NAME, "Got boolean for key: $key, value: $res")
+    return res
+  }
+
+  fun addToSet(
+    key: String,
+    value: String,
+  ) {
+    val safeInstance = getInstance(context)
+    val set = safeInstance.getStringSet(key, setOf()) ?: setOf()
+    val newSet =
+      set.toMutableSet().apply {
+        add(value)
+      }
+    safeInstance
+      .edit()
+      .apply {
+        putStringSet(key, newSet)
+      }.apply()
+  }
+
+  fun removeFromSet(
+    key: String,
+    value: String,
+  ) {
+    val safeInstance = getInstance(context)
+    val set = safeInstance.getStringSet(key, setOf()) ?: setOf()
+    val newSet =
+      set.toMutableSet().apply {
+        remove(value)
+      }
+    safeInstance
+      .edit()
+      .apply {
+        putStringSet(key, newSet)
+      }.apply()
+  }
+
+  fun setContains(
+    key: String,
+    value: String,
+  ): Boolean {
+    val safeInstance = getInstance(context)
+    val set = safeInstance.getStringSet(key, setOf()) ?: setOf()
+    return set.contains(value)
+  }
+
+  fun hasValue(key: String): Boolean {
+    val safeInstance = getInstance(context)
+    return safeInstance.contains(key)
+  }
+
+  fun getValues(keys: Set<String>): Map<String, Any?> {
+    val safeInstance = getInstance(context)
+    return keys.associateWith { key ->
+      when (val value = safeInstance.all[key]) {
+        is String -> value
+        is Float -> value
+        is Boolean -> value
+        is Set<*> -> value
+        else -> null
+      }
+    }
+  }
+}
diff --git a/modules/expo-bluesky-swiss-army/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json
index 730bc6114..1111f8a0b 100644
--- a/modules/expo-bluesky-swiss-army/expo-module.config.json
+++ b/modules/expo-bluesky-swiss-army/expo-module.config.json
@@ -1,11 +1,11 @@
 {
   "platforms": ["ios", "tvos", "android", "web"],
   "ios": {
-    "modules": ["ExpoBlueskyDevicePrefsModule", "ExpoBlueskyReferrerModule"]
+    "modules": ["ExpoBlueskySharedPrefsModule", "ExpoBlueskyReferrerModule"]
   },
   "android": {
     "modules": [
-      "expo.modules.blueskyswissarmy.deviceprefs.ExpoBlueskyDevicePrefsModule",
+      "expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule",
       "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule"
     ]
   }
diff --git a/modules/expo-bluesky-swiss-army/index.ts b/modules/expo-bluesky-swiss-army/index.ts
index 1b2f89249..89cea00a2 100644
--- a/modules/expo-bluesky-swiss-army/index.ts
+++ b/modules/expo-bluesky-swiss-army/index.ts
@@ -1,4 +1,4 @@
-import * as DevicePrefs from './src/DevicePrefs'
 import * as Referrer from './src/Referrer'
+import * as SharedPrefs from './src/SharedPrefs'
 
-export {DevicePrefs, Referrer}
+export {Referrer, SharedPrefs}
diff --git a/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift b/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift
deleted file mode 100644
index b13a9fe3f..000000000
--- a/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-import ExpoModulesCore
-
-public class ExpoBlueskyDevicePrefsModule: Module {
-  func getDefaults(_ useAppGroup: Bool) -> UserDefaults? {
-    if useAppGroup {
-      return UserDefaults(suiteName: "group.app.bsky")
-    } else {
-      return UserDefaults.standard
-    }
-  }
-
-  public func definition() -> ModuleDefinition {
-    Name("ExpoBlueskyDevicePrefs")
-
-    AsyncFunction("getStringValueAsync") { (key: String, useAppGroup: Bool) in
-      return self.getDefaults(useAppGroup)?.string(forKey: key)
-    }
-
-    AsyncFunction("setStringValueAsync") { (key: String, value: String?, useAppGroup: Bool) in
-      self.getDefaults(useAppGroup)?.setValue(value, forKey: key)
-    }
-  }
-}
diff --git a/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift
new file mode 100644
index 000000000..8549e5b48
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift
@@ -0,0 +1,62 @@
+import Foundation
+import ExpoModulesCore
+
+public class ExpoBlueskySharedPrefsModule: Module {
+  let defaults = UserDefaults(suiteName: "group.app.bsky")
+
+  func getDefaults(_ info: String = "(no info)") -> UserDefaults? {
+    guard let defaults = self.defaults else {
+      NSLog("Failed to get defaults for app group: \(info)")
+      return nil
+    }
+    return defaults
+  }
+
+  public func definition() -> ModuleDefinition {
+    Name("ExpoBlueskySharedPrefs")
+
+    // JavaScripValue causes a crash when trying to check `isString()`. Let's
+    // explicitly define setString instead.
+    Function("setString") { (key: String, value: String?) in
+      SharedPrefs.shared.setValue(key, value)
+    }
+
+    Function("setValue") { (key: String, value: JavaScriptValue) in
+      if value.isNumber() {
+        SharedPrefs.shared.setValue(key, value.getDouble())
+      } else if value.isBool() {
+        SharedPrefs.shared.setValue(key, value.getBool())
+      } else if value.isNull() || value.isUndefined() {
+        SharedPrefs.shared.removeValue(key)
+      }
+    }
+
+    Function("removeValue") { (key: String) in
+      SharedPrefs.shared.removeValue(key)
+    }
+
+    Function("getString") { (key: String) in
+      return SharedPrefs.shared.getString(key)
+    }
+
+    Function("getBool") { (key: String) in
+      return SharedPrefs.shared.getBool(key)
+    }
+
+    Function("getNumber") { (key: String) in
+      return SharedPrefs.shared.getNumber(key)
+    }
+
+    Function("addToSet") { (key: String, value: String) in
+      SharedPrefs.shared.addToSet(key, value)
+    }
+
+    Function("removeFromSet") { (key: String, value: String) in
+      SharedPrefs.shared.removeFromSet(key, value)
+    }
+
+    Function("setContains") { (key: String, value: String) in
+      return SharedPrefs.shared.setContains(key, value)
+    }
+  }
+}
diff --git a/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift
new file mode 100644
index 000000000..a11a71834
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift
@@ -0,0 +1,89 @@
+import Foundation
+
+public class SharedPrefs {
+  public static let shared = SharedPrefs()
+
+  private let defaults = UserDefaults(suiteName: "group.app.bsky")
+
+  init() {
+    if defaults == nil {
+      NSLog("Failed to get user defaults for app group.")
+    }
+  }
+
+  private func getDefaults(_ info: String = "(no info)") -> UserDefaults? {
+    guard let defaults = self.defaults else {
+      NSLog("Failed to get defaults for app group: \(info)")
+      return nil
+    }
+    return defaults
+  }
+
+  public func setValue(_ key: String, _ value: String?) {
+    getDefaults(key)?.setValue(value, forKey: key)
+  }
+
+  public func setValue(_ key: String, _ value: Double?) {
+    getDefaults(key)?.setValue(value, forKey: key)
+  }
+
+  public func setValue(_ key: String, _ value: Bool?) {
+    getDefaults(key)?.setValue(value, forKey: key)
+  }
+
+  public func _setAnyValue(_ key: String, _ value: Any?) {
+    getDefaults(key)?.setValue(value, forKey: key)
+  }
+
+  public func removeValue(_ key: String) {
+    getDefaults(key)?.removeObject(forKey: key)
+  }
+
+  public func getString(_ key: String) -> String? {
+    return getDefaults(key)?.string(forKey: key)
+  }
+
+  public func getNumber(_ key: String) -> Double? {
+    return getDefaults(key)?.double(forKey: key)
+  }
+
+  public func getBool(_ key: String) -> Bool? {
+    return getDefaults(key)?.bool(forKey: key)
+  }
+
+  public func addToSet(_ key: String, _ value: String) {
+    var dict: [String: Bool]?
+    if var currDict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] {
+      currDict[value] = true
+      dict = currDict
+    } else {
+      dict = [
+        value: true
+      ]
+    }
+    getDefaults(key)?.setValue(dict, forKey: key)
+  }
+
+  public func removeFromSet(_ key: String, _ value: String) {
+    guard var dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else {
+      return
+    }
+    dict.removeValue(forKey: value)
+    getDefaults(key)?.setValue(dict, forKey: key)
+  }
+
+  public func setContains(_ key: String, _ value: String) -> Bool {
+    guard let dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else {
+      return false
+    }
+    return dict[value] == true
+  }
+
+  public func hasValue(_ key: String) -> Bool {
+    return getDefaults(key)?.value(forKey: key) != nil
+  }
+
+  public func getValues(_ keys: [String]) -> [String: Any?]? {
+    return getDefaults("keys:\(keys)")?.dictionaryWithValues(forKeys: keys)
+  }
+}
diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts
deleted file mode 100644
index 427185086..000000000
--- a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {requireNativeModule} from 'expo-modules-core'
-
-const NativeModule = requireNativeModule('ExpoBlueskyDevicePrefs')
-
-export function getStringValueAsync(
-  key: string,
-  useAppGroup?: boolean,
-): Promise<string | null> {
-  return NativeModule.getStringValueAsync(key, useAppGroup)
-}
-
-export function setStringValueAsync(
-  key: string,
-  value: string | null,
-  useAppGroup?: boolean,
-): Promise<void> {
-  return NativeModule.setStringValueAsync(key, value, useAppGroup)
-}
diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts
deleted file mode 100644
index f1eee6c28..000000000
--- a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {NotImplementedError} from '../NotImplemented'
-
-export function getStringValueAsync(
-  key: string,
-  useAppGroup?: boolean,
-): Promise<string | null> {
-  throw new NotImplementedError({key, useAppGroup})
-}
-
-export function setStringValueAsync(
-  key: string,
-  value: string | null,
-  useAppGroup?: boolean,
-): Promise<string | null> {
-  throw new NotImplementedError({key, value, useAppGroup})
-}
diff --git a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts
new file mode 100644
index 000000000..0cea7c53b
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts
@@ -0,0 +1,51 @@
+import {requireNativeModule} from 'expo-modules-core'
+
+const NativeModule = requireNativeModule('ExpoBlueskySharedPrefs')
+
+export function setValue(
+  key: string,
+  value: string | number | boolean | null | undefined,
+): void {
+  // A bug on Android causes `JavaScripValue.isString()` to cause a crash on some occasions, seemingly because of a
+  // memory violation. Instead, we will use a specific function to set strings on this platform.
+  if (typeof value === 'string') {
+    return NativeModule.setString(key, value)
+  }
+  return NativeModule.setValue(key, value)
+}
+
+export function removeValue(key: string): void {
+  return NativeModule.removeValue(key)
+}
+
+export function getString(key: string): string | undefined {
+  return nullToUndefined(NativeModule.getString(key))
+}
+
+export function getNumber(key: string): number | undefined {
+  return nullToUndefined(NativeModule.getNumber(key))
+}
+
+export function getBool(key: string): boolean | undefined {
+  return nullToUndefined(NativeModule.getBool(key))
+}
+
+export function addToSet(key: string, value: string): void {
+  return NativeModule.addToSet(key, value)
+}
+
+export function removeFromSet(key: string, value: string): void {
+  return NativeModule.removeFromSet(key, value)
+}
+
+export function setContains(key: string, value: string): boolean {
+  return NativeModule.setContains(key, value)
+}
+
+// iOS returns `null` if a value does not exist, and Android returns `undefined. Normalize these here for JS types
+function nullToUndefined(value: any) {
+  if (value == null) {
+    return undefined
+  }
+  return value
+}
diff --git a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts
new file mode 100644
index 000000000..769344007
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts
@@ -0,0 +1,36 @@
+import {NotImplementedError} from '../NotImplemented'
+
+export function setValue(
+  key: string,
+  value: string | number | boolean | null | undefined,
+): void {
+  throw new NotImplementedError({key, value})
+}
+
+export function removeValue(key: string): void {
+  throw new NotImplementedError({key})
+}
+
+export function getString(key: string): string | null {
+  throw new NotImplementedError({key})
+}
+
+export function getNumber(key: string): number | null {
+  throw new NotImplementedError({key})
+}
+
+export function getBool(key: string): boolean | null {
+  throw new NotImplementedError({key})
+}
+
+export function addToSet(key: string, value: string): void {
+  throw new NotImplementedError({key, value})
+}
+
+export function removeFromSet(key: string, value: string): void {
+  throw new NotImplementedError({key, value})
+}
+
+export function setContains(key: string, value: string): boolean {
+  throw new NotImplementedError({key, value})
+}