diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskyDevicePrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskyDevicePrefsModule.kt index b68dc62445..6d81449e50 100644 --- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskyDevicePrefsModule.kt +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskyDevicePrefsModule.kt @@ -16,22 +16,20 @@ class ExpoBlueskySharedPrefsModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoBlueskySharedPrefs") - AsyncFunction("setStringAsync") { key: String, value: String -> - return@AsyncFunction Preferences(getContext()).setValue(key, value) + Function("setString") { key: String, value: String -> + return@Function SharedPrefs(getContext()).setValue(key, value) } - AsyncFunction("setValueAsync") { key: String, value: JavaScriptValue, promise: Promise -> + Function("setValue") { key: String, value: JavaScriptValue -> val context = getContext() + Log.d("ExpoBlueskySharedPrefs", "Setting value for key: $key") try { if (value.isNumber()) { - Preferences(context).setValue(key, value.getFloat()) - promise.resolve() + SharedPrefs(context).setValue(key, value.getFloat()) } else if (value.isBool()) { - Preferences(context).setValue(key, value.getBool()) - promise.resolve() + SharedPrefs(context).setValue(key, value.getBool()) } else if (value.isNull() || value.isUndefined()) { - Preferences(context).removeValue(key) - promise.resolve() + SharedPrefs(context).removeValue(key) } else { Log.d(NAME, "Unsupported type: ${value.kind()}") promise.reject("UNSUPPORTED_TYPE_ERROR", "Attempted to set an unsupported type", null) @@ -42,32 +40,32 @@ class ExpoBlueskySharedPrefsModule : Module() { } } - AsyncFunction("removeValueAsync") { key: String -> - return@AsyncFunction Preferences(getContext()).removeValue(key) + Function("removeValue") { key: String -> + return@Function SharedPrefs(getContext()).removeValue(key) } - AsyncFunction("getStringAsync") { key: String -> - return@AsyncFunction Preferences(getContext()).getString(key) + Function("getString") { key: String -> + return@Function SharedPrefs(getContext()).getString(key) } - AsyncFunction("getNumberAsync") { key: String -> - return@AsyncFunction Preferences(getContext()).getFloat(key) + Function("getNumber") { key: String -> + return@Function SharedPrefs(getContext()).getFloat(key) } - AsyncFunction("getBoolAsync") { key: String -> - return@AsyncFunction Preferences(getContext()).getBoolean(key) + Function("getBool") { key: String -> + return@Function SharedPrefs(getContext()).getBoolean(key) } - AsyncFunction("addToSetAsync") { key: String, value: String -> - return@AsyncFunction Preferences(getContext()).addToSet(key, value) + Function("addToSet") { key: String, value: String -> + return@Function SharedPrefs(getContext()).addToSet(key, value) } - AsyncFunction("removeFromSetAsync") { key: String, value: String -> - return@AsyncFunction Preferences(getContext()).removeFromSet(key, value) + Function("removeFromSet") { key: String, value: String -> + return@Function SharedPrefs(getContext()).removeFromSet(key, value) } - AsyncFunction("setContainsAsync") { key: String, value: String -> - return@AsyncFunction Preferences(getContext()).setContains(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 0000000000..2ed60c3fab --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt @@ -0,0 +1,193 @@ +package expo.modules.blueskyswissarmy.sharedprefs + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log + +val DEFAULTS = mapOf( + "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) { + 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) + } + + 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): Map { + 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/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift index 1f8dde1bee..8549e5b48b 100644 --- a/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift +++ b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift @@ -15,45 +15,47 @@ public class ExpoBlueskySharedPrefsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoBlueskySharedPrefs") - AsyncFunction("setValueAsync") { (key: String, value: JavaScriptValue, promise: Promise) in - if value.isString() { - SharedPrefs.shared.setValue(key, value.getString()) - } else if value.isNumber() { + // 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) - } else { - promise.reject("UNSUPPORTED_TYPE_ERROR", "Attempted to set an unsupported type") } } - AsyncFunction("removeValueAsync") { (key: String) in + Function("removeValue") { (key: String) in SharedPrefs.shared.removeValue(key) } - AsyncFunction("getStringAsync") { (key: String) in + Function("getString") { (key: String) in return SharedPrefs.shared.getString(key) } - AsyncFunction("getBoolAsync") { (key: String) in + Function("getBool") { (key: String) in return SharedPrefs.shared.getBool(key) } - AsyncFunction("getNumberAsync") { (key: String) in + Function("getNumber") { (key: String) in return SharedPrefs.shared.getNumber(key) } - AsyncFunction("addToSetAsync") { (key: String, value: String) in + Function("addToSet") { (key: String, value: String) in SharedPrefs.shared.addToSet(key, value) } - AsyncFunction("removeFromSetAsync") { (key: String, value: String) in + Function("removeFromSet") { (key: String, value: String) in SharedPrefs.shared.removeFromSet(key, value) } - AsyncFunction("setContainsAsync") { (key: String, value: String) in + 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 index 8128a4b2e4..a11a71834c 100644 --- a/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift +++ b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift @@ -52,20 +52,20 @@ public class SharedPrefs { } public func addToSet(_ key: String, _ value: String) { - var dict: [String:Bool]? - if var currDict = getDefaults(key)?.dictionary(forKey: key) as? [String:Bool] { + var dict: [String: Bool]? + if var currDict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] { currDict[value] = true dict = currDict } else { dict = [ - value : true + 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 { + guard var dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else { return } dict.removeValue(forKey: value) @@ -73,7 +73,7 @@ public class SharedPrefs { } public func setContains(_ key: String, _ value: String) -> Bool { - guard let dict = getDefaults(key)?.dictionary(forKey: key) as? [String:Bool] else { + guard let dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else { return false } return dict[value] == true @@ -83,7 +83,7 @@ public class SharedPrefs { return getDefaults(key)?.value(forKey: key) != nil } - public func getValues(_ keys: [String]) -> [String:Any?]? { + public func getValues(_ keys: [String]) -> [String: Any?]? { return getDefaults("keys:\(keys)")?.dictionaryWithValues(forKeys: keys) } } diff --git a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts index 0e3ff97b10..589b074099 100644 --- a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts +++ b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts @@ -1,44 +1,50 @@ -import {Platform} from 'react-native' import {requireNativeModule} from 'expo-modules-core' const NativeModule = requireNativeModule('ExpoBlueskySharedPrefs') -export function setValueAsync( +export function setValue( key: string, value: string | number | boolean | null | undefined, -): Promise { +): 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 (Platform.OS === 'android' && typeof value === 'string') { - return NativeModule.setStringAsync(key, value) + if (typeof value === 'string') { + return NativeModule.setString(key, value) } - return NativeModule.setValueAsync(key, value) + return NativeModule.setValue(key, value) } -export function removeValueAsync(key: string): Promise { - return NativeModule.removeValueAsync(key) +export function removeValue(key: string): void { + return NativeModule.removeValue(key) } -export function getStringAsync(key: string): Promise { - return NativeModule.getStringAsync(key) +export function getString(key: string): string | undefined { + return nullToUndefined(NativeModule.getString(key)) } -export function getNumberAsync(key: string): Promise { - return NativeModule.getNumberAsync(key) +export function getNumber(key: string): number | undefined { + return nullToUndefined(NativeModule.getNumber(key)) } -export function getBoolAsync(key: string): Promise { - return NativeModule.getBoolAsync(key) +export function getBool(key: string): boolean | undefined { + return nullToUndefined(NativeModule.getBool(key)) } -export function addToSetAsync(key: string, value: string): Promise { - return NativeModule.addToSetAsync(key, value) +export function addToSet(key: string, value: string): void { + return NativeModule.addToSet(key, value) } -export function removeFromSetAsync(key: string, value: string): Promise { - return NativeModule.removeFromSetAsync(key, value) +export function removeFromSet(key: string, value: string): void { + return NativeModule.removeFromSet(key, value) } -export function setContainsAsync(key: string, value: string): Promise { - return NativeModule.setContainsAsync(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 + } } diff --git a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts index c6134ebcb9..769344007b 100644 --- a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts +++ b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts @@ -1,36 +1,36 @@ import {NotImplementedError} from '../NotImplemented' -export function setValueAsync( +export function setValue( key: string, value: string | number | boolean | null | undefined, -): Promise { +): void { throw new NotImplementedError({key, value}) } -export function removeValueAsync(key: string): Promise { +export function removeValue(key: string): void { throw new NotImplementedError({key}) } -export function getStringAsync(key: string): Promise { +export function getString(key: string): string | null { throw new NotImplementedError({key}) } -export function getNumberAsync(key: string): Promise { +export function getNumber(key: string): number | null { throw new NotImplementedError({key}) } -export function getBoolAsync(key: string): Promise { +export function getBool(key: string): boolean | null { throw new NotImplementedError({key}) } -export function addToSetAsync(key: string, value: string): Promise { +export function addToSet(key: string, value: string): void { throw new NotImplementedError({key, value}) } -export function removeFromSetAsync(key: string, value: string): Promise { +export function removeFromSet(key: string, value: string): void { throw new NotImplementedError({key, value}) } -export function setContainsAsync(key: string, value: string): Promise { +export function setContains(key: string, value: string): boolean { throw new NotImplementedError({key, value}) } diff --git a/src/components/hooks/useStarterPackEntry.native.ts b/src/components/hooks/useStarterPackEntry.native.ts index e507aa4e01..212ecae71d 100644 --- a/src/components/hooks/useStarterPackEntry.native.ts +++ b/src/components/hooks/useStarterPackEntry.native.ts @@ -39,11 +39,10 @@ export function useStarterPackEntry() { uri = createStarterPackLinkFromAndroidReferrer(res.installReferrer) } } else { - const res = await SharedPrefs.getStringAsync('starterPackUri') - - if (res) { - uri = httpStarterPackUriToAtUri(res) - SharedPrefs.setValueAsync('starterPackUri', null) + const starterPackUri = SharedPrefs.getString('starterPackUri') + if (starterPackUri) { + uri = httpStarterPackUriToAtUri(starterPackUri) + SharedPrefs.setValue('starterPackUri', null) } } diff --git a/src/screens/E2E/SharedPreferencesTesterScreen.tsx b/src/screens/E2E/SharedPreferencesTesterScreen.tsx index 921f4f8ca3..380f1080b0 100644 --- a/src/screens/E2E/SharedPreferencesTesterScreen.tsx +++ b/src/screens/E2E/SharedPreferencesTesterScreen.tsx @@ -25,10 +25,11 @@ export function SharedPreferencesTesterScreen() { color="primary" size="xsmall" onPress={async () => { - await SharedPrefs.removeValueAsync('testerString') - await SharedPrefs.setValueAsync('testerString', 'Hello') - const res = await SharedPrefs.getStringAsync('testerString') - setCurrentTestOutput(`${res}`) + SharedPrefs.removeValue('testerString') + SharedPrefs.setValue('testerString', 'Hello') + const str = SharedPrefs.getString('testerString') + console.log(JSON.stringify(str)) + setCurrentTestOutput(`${str}`) }}> Set String @@ -40,9 +41,9 @@ export function SharedPreferencesTesterScreen() { color="primary" size="xsmall" onPress={async () => { - await SharedPrefs.removeValueAsync('testerString') - const res = await SharedPrefs.getStringAsync('testerString') - setCurrentTestOutput(`${res}`) + SharedPrefs.removeValue('testerString') + const str = SharedPrefs.getString('testerString') + setCurrentTestOutput(`${str}`) }}> Remove String @@ -54,10 +55,10 @@ export function SharedPreferencesTesterScreen() { color="primary" size="xsmall" onPress={async () => { - await SharedPrefs.removeValueAsync('testerBool') - await SharedPrefs.setValueAsync('testerBool', true) - const res = await SharedPrefs.getBoolAsync('testerBool') - setCurrentTestOutput(`${res}`) + SharedPrefs.removeValue('testerBool') + SharedPrefs.setValue('testerBool', true) + const bool = SharedPrefs.getBool('testerBool') + setCurrentTestOutput(`${bool}`) }}> Set Bool @@ -69,10 +70,10 @@ export function SharedPreferencesTesterScreen() { color="primary" size="xsmall" onPress={async () => { - await SharedPrefs.removeValueAsync('testerNumber') - await SharedPrefs.setValueAsync('testerNumber', 123) - const res = await SharedPrefs.getNumberAsync('testerNumber') - setCurrentTestOutput(`${res}`) + SharedPrefs.removeValue('testerNumber') + SharedPrefs.setValue('testerNumber', 123) + const num = SharedPrefs.getNumber('testerNumber') + setCurrentTestOutput(`${num}`) }}> Set Number @@ -84,13 +85,10 @@ export function SharedPreferencesTesterScreen() { color="primary" size="xsmall" onPress={async () => { - await SharedPrefs.removeFromSetAsync('testerSet', 'Hello!') - await SharedPrefs.addToSetAsync('testerSet', 'Hello!') - const res = await SharedPrefs.setContainsAsync( - 'testerSet', - 'Hello!', - ) - setCurrentTestOutput(`${res}`) + SharedPrefs.removeFromSet('testerSet', 'Hello!') + SharedPrefs.addToSet('testerSet', 'Hello!') + const contains = SharedPrefs.setContains('testerSet', 'Hello!') + setCurrentTestOutput(`${contains}`) }}> Add to Set @@ -102,12 +100,9 @@ export function SharedPreferencesTesterScreen() { color="primary" size="xsmall" onPress={async () => { - await SharedPrefs.removeFromSetAsync('testerSet', 'Hello!') - const res = await SharedPrefs.setContainsAsync( - 'testerSet', - 'Hello!', - ) - setCurrentTestOutput(`${res}`) + SharedPrefs.removeFromSet('testerSet', 'Hello!') + const contains = SharedPrefs.setContains('testerSet', 'Hello!') + setCurrentTestOutput(`${contains}`) }}> Remove from Set