From 83e8522e0a89be28b1733f4c50dbd4379d98d03b Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Jul 2024 18:37:43 -0700 Subject: [PATCH] Create shared preferences API (#4654) --- __e2e__/flows/shared-prefs.yml | 31 +++ .../ExpoBlueskyDevicePrefsModule.kt | 11 - .../ExpoBlueskySharedPrefsModule.kt | 69 +++++ .../sharedprefs/SharedPrefs.kt | 241 ++++++++++++++++++ .../expo-module.config.json | 4 +- modules/expo-bluesky-swiss-army/index.ts | 4 +- .../ExpoBlueskyDevicePrefsModule.swift | 23 -- .../ExpoBlueskySharedPrefsModule.swift | 62 +++++ .../ios/SharedPrefs/SharedPrefs.swift | 89 +++++++ .../src/DevicePrefs/index.ios.ts | 18 -- .../src/DevicePrefs/index.ts | 16 -- .../src/SharedPrefs/index.native.ts | 51 ++++ .../src/SharedPrefs/index.ts | 36 +++ package.json | 1 + src/Navigation.tsx | 6 + .../hooks/useStarterPackEntry.native.ts | 14 +- src/lib/routes/types.ts | 1 + .../E2E/SharedPreferencesTesterScreen.tsx | 113 ++++++++ src/view/screens/Storybook/Dialogs.tsx | 13 + 19 files changed, 722 insertions(+), 81 deletions(-) create mode 100644 __e2e__/flows/shared-prefs.yml delete mode 100644 modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt create mode 100644 modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt create mode 100644 modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt delete mode 100644 modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift create mode 100644 modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift create mode 100644 modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift delete mode 100644 modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts delete mode 100644 modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts create mode 100644 modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts create mode 100644 modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts create mode 100644 src/screens/E2E/SharedPreferencesTesterScreen.tsx diff --git a/__e2e__/flows/shared-prefs.yml b/__e2e__/flows/shared-prefs.yml new file mode 100644 index 00000000..73a06682 --- /dev/null +++ b/__e2e__/flows/shared-prefs.yml @@ -0,0 +1,31 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users&posts&feeds" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" +- tapOn: "/sys/debug" +- tapOn: + id: "sharedPrefsTestOpenBtn" +- tapOn: + id: "setStringBtn" +- assertVisible: "Hello" +- tapOn: + id: "removeStringBtn" +- assertVisible: "null" +- tapOn: + id: "setBoolBtn" +- assertVisible: "true" +- tapOn: + id: "setNumberBtn" +- assertVisible: "123" +- tapOn: + id: "addToSetBtn" +- assertVisible: "true" +- tapOn: + id: "removeFromSetBtn" +- assertVisible: "false" 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 51f9fe45..00000000 --- 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 00000000..27195687 --- /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 00000000..38d79abf --- /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( + "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) + } + + @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): 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/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json index 730bc611..1111f8a0 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 1b2f8924..89cea00a 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 b13a9fe3..00000000 --- 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 00000000..8549e5b4 --- /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 00000000..a11a7183 --- /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 42718508..00000000 --- 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 { - return NativeModule.getStringValueAsync(key, useAppGroup) -} - -export function setStringValueAsync( - key: string, - value: string | null, - useAppGroup?: boolean, -): Promise { - 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 f1eee6c2..00000000 --- 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 { - throw new NotImplementedError({key, useAppGroup}) -} - -export function setStringValueAsync( - key: string, - value: string | null, - useAppGroup?: boolean, -): Promise { - 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 00000000..0cea7c53 --- /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 00000000..76934400 --- /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}) +} diff --git a/package.json b/package.json index 141646d1..b56dd9e3 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "typecheck": "tsc --project ./tsconfig.check.json", "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts", "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", + "e2e:metro-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android", "e2e:run": "maestro test __e2e__", "perf:test": "NODE_ENV=test maestro test", "perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 4ecf3fff..49543512 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -39,6 +39,7 @@ import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed' import {PreferencesThreads} from 'view/screens/PreferencesThreads' import {SavedFeeds} from 'view/screens/SavedFeeds' +import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen' import HashtagScreen from '#/screens/Hashtag' import {ModerationScreen} from '#/screens/Moderation' import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' @@ -233,6 +234,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => DebugModScreen} options={{title: title(msg`Moderation states`), requireAuth: true}} /> + SharedPreferencesTesterScreen} + options={{title: title(msg`Shared Preferences Tester`)}} + /> LogScreen} diff --git a/src/components/hooks/useStarterPackEntry.native.ts b/src/components/hooks/useStarterPackEntry.native.ts index b6e4ab05..212ecae7 100644 --- a/src/components/hooks/useStarterPackEntry.native.ts +++ b/src/components/hooks/useStarterPackEntry.native.ts @@ -7,7 +7,7 @@ import { import {isAndroid} from 'platform/detection' import {useHasCheckedForStarterPack} from 'state/preferences/used-starter-packs' import {useSetActiveStarterPack} from 'state/shell/starter-pack' -import {DevicePrefs, Referrer} from '../../../modules/expo-bluesky-swiss-army' +import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army' export function useStarterPackEntry() { const [ready, setReady] = React.useState(false) @@ -39,14 +39,10 @@ export function useStarterPackEntry() { uri = createStarterPackLinkFromAndroidReferrer(res.installReferrer) } } else { - const res = await DevicePrefs.getStringValueAsync( - 'starterPackUri', - true, - ) - - if (res) { - uri = httpStarterPackUriToAtUri(res) - DevicePrefs.setStringValueAsync('starterPackUri', null, true) + const starterPackUri = SharedPrefs.getString('starterPackUri') + if (starterPackUri) { + uri = httpStarterPackUriToAtUri(starterPackUri) + SharedPrefs.setValue('starterPackUri', null) } } diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 9d102f24..bda93fb4 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -25,6 +25,7 @@ export type CommonNavigatorParams = { ProfileLabelerLikedBy: {name: string} Debug: undefined DebugMod: undefined + SharedPreferencesTester: undefined Log: undefined Support: undefined PrivacyPolicy: undefined diff --git a/src/screens/E2E/SharedPreferencesTesterScreen.tsx b/src/screens/E2E/SharedPreferencesTesterScreen.tsx new file mode 100644 index 00000000..380f1080 --- /dev/null +++ b/src/screens/E2E/SharedPreferencesTesterScreen.tsx @@ -0,0 +1,113 @@ +import React from 'react' +import {View} from 'react-native' + +import {ScrollView} from 'view/com/util/Views' +import {atoms as a} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {Text} from '#/components/Typography' +import {SharedPrefs} from '../../../modules/expo-bluesky-swiss-army' + +export function SharedPreferencesTesterScreen() { + const [currentTestOutput, setCurrentTestOutput] = React.useState('') + + return ( + + + + {currentTestOutput} + + + + + + + + + + + + ) +} diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index 6d166d4b..ca2420fe 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -1,7 +1,9 @@ import React from 'react' import {View} from 'react-native' +import {useNavigation} from '@react-navigation/native' import {useDialogStateControlContext} from '#/state/dialogs' +import {NavigationProp} from 'lib/routes/types' import {atoms as a} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -18,6 +20,7 @@ export function Dialogs() { const [shouldRenderUnmountTest, setShouldRenderUnmountTest] = React.useState(false) const unmountTestInterval = React.useRef() + const navigation = useNavigation() const onUnmountTestStartPressWithClose = () => { setShouldRenderUnmountTest(true) @@ -134,6 +137,16 @@ export function Dialogs() { End Unmount Test + + This is a prompt