Create shared preferences API (#4654)

zio/stable
Hailey 2024-07-11 18:37:43 -07:00 committed by GitHub
parent 2397104ad6
commit 83e8522e0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 722 additions and 81 deletions

View File

@ -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"

View File

@ -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")
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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"
]
}

View File

@ -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}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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})
}

View File

@ -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
}

View File

@ -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})
}

View File

@ -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",

View File

@ -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}}
/>
<Stack.Screen
name="SharedPreferencesTester"
getComponent={() => SharedPreferencesTesterScreen}
options={{title: title(msg`Shared Preferences Tester`)}}
/>
<Stack.Screen
name="Log"
getComponent={() => LogScreen}

View File

@ -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)
}
}

View File

@ -25,6 +25,7 @@ export type CommonNavigatorParams = {
ProfileLabelerLikedBy: {name: string}
Debug: undefined
DebugMod: undefined
SharedPreferencesTester: undefined
Log: undefined
Support: undefined
PrivacyPolicy: undefined

View File

@ -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<string>('')
return (
<ScrollView contentContainerStyle={{backgroundColor: 'red'}}>
<View style={[a.flex_1]}>
<View>
<Text testID="testOutput">{currentTestOutput}</Text>
</View>
<View style={[a.flex_wrap]}>
<Button
label="btn"
testID="setStringBtn"
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
onPress={async () => {
SharedPrefs.removeValue('testerString')
SharedPrefs.setValue('testerString', 'Hello')
const str = SharedPrefs.getString('testerString')
console.log(JSON.stringify(str))
setCurrentTestOutput(`${str}`)
}}>
<ButtonText>Set String</ButtonText>
</Button>
<Button
label="btn"
testID="removeStringBtn"
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
onPress={async () => {
SharedPrefs.removeValue('testerString')
const str = SharedPrefs.getString('testerString')
setCurrentTestOutput(`${str}`)
}}>
<ButtonText>Remove String</ButtonText>
</Button>
<Button
label="btn"
testID="setBoolBtn"
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
onPress={async () => {
SharedPrefs.removeValue('testerBool')
SharedPrefs.setValue('testerBool', true)
const bool = SharedPrefs.getBool('testerBool')
setCurrentTestOutput(`${bool}`)
}}>
<ButtonText>Set Bool</ButtonText>
</Button>
<Button
label="btn"
testID="setNumberBtn"
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
onPress={async () => {
SharedPrefs.removeValue('testerNumber')
SharedPrefs.setValue('testerNumber', 123)
const num = SharedPrefs.getNumber('testerNumber')
setCurrentTestOutput(`${num}`)
}}>
<ButtonText>Set Number</ButtonText>
</Button>
<Button
label="btn"
testID="addToSetBtn"
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
onPress={async () => {
SharedPrefs.removeFromSet('testerSet', 'Hello!')
SharedPrefs.addToSet('testerSet', 'Hello!')
const contains = SharedPrefs.setContains('testerSet', 'Hello!')
setCurrentTestOutput(`${contains}`)
}}>
<ButtonText>Add to Set</ButtonText>
</Button>
<Button
label="btn"
testID="removeFromSetBtn"
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
onPress={async () => {
SharedPrefs.removeFromSet('testerSet', 'Hello!')
const contains = SharedPrefs.setContains('testerSet', 'Hello!')
setCurrentTestOutput(`${contains}`)
}}>
<ButtonText>Remove from Set</ButtonText>
</Button>
</View>
</View>
</ScrollView>
)
}

View File

@ -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<number>()
const navigation = useNavigation<NavigationProp>()
const onUnmountTestStartPressWithClose = () => {
setShouldRenderUnmountTest(true)
@ -134,6 +137,16 @@ export function Dialogs() {
<ButtonText>End Unmount Test</ButtonText>
</Button>
<Button
variant="solid"
color="primary"
size="small"
onPress={() => navigation.navigate('SharedPreferencesTester')}
label="two"
testID="sharedPrefsTestOpenBtn">
<ButtonText>Open Shared Prefs Tester</ButtonText>
</Button>
<Prompt.Outer control={prompt}>
<Prompt.TitleText>This is a prompt</Prompt.TitleText>
<Prompt.DescriptionText>