From b75341314cf7551644d12a7281ab22c855c78233 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 25 Jun 2024 10:34:17 -0700 Subject: [PATCH 1/5] tweak app clip AASA (#4641) --- bskylink/src/routes/siteAssociation.ts | 9 +++++++++ bskyweb/static/.well-known/apple-app-site-association | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/bskylink/src/routes/siteAssociation.ts b/bskylink/src/routes/siteAssociation.ts index ae3b42e304..7b71ccdd57 100644 --- a/bskylink/src/routes/siteAssociation.ts +++ b/bskylink/src/routes/siteAssociation.ts @@ -5,6 +5,15 @@ import {AppContext} from '../context.js' export default function (ctx: AppContext, app: Express) { return app.get('/.well-known/apple-app-site-association', (req, res) => { res.json({ + applinks: { + apps: [], + details: [ + { + appID: 'B3LX46C5HS.xyz.blueskyweb.app', + paths: ['*'], + }, + ], + }, appclips: { apps: ['B3LX46C5HS.xyz.blueskyweb.app.AppClip'], }, diff --git a/bskyweb/static/.well-known/apple-app-site-association b/bskyweb/static/.well-known/apple-app-site-association index 0a05fa35f4..f5752d7c15 100644 --- a/bskyweb/static/.well-known/apple-app-site-association +++ b/bskyweb/static/.well-known/apple-app-site-association @@ -1,8 +1,5 @@ { "applinks": { - "appclips": { - "apps": ["B3LX46C5HS.xyz.blueskyweb.app.AppClip"] - }, "details": [ { "appID": "B3LX46C5HS.xyz.blueskyweb.app", @@ -11,5 +8,8 @@ ] } ] + }, + "appclips": { + "apps": ["B3LX46C5HS.xyz.blueskyweb.app.AppClip"] } } From 7c5ff79c8e545962502e87f3c6c734fb9b05970c Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 25 Jun 2024 11:31:02 -0700 Subject: [PATCH 2/5] `workflow_dispatch` link (#4642) --- .github/workflows/build-and-push-link-aws.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build-and-push-link-aws.yaml b/.github/workflows/build-and-push-link-aws.yaml index f91af48770..1c17d6e190 100644 --- a/.github/workflows/build-and-push-link-aws.yaml +++ b/.github/workflows/build-and-push-link-aws.yaml @@ -1,8 +1,6 @@ name: build-and-push-link-aws on: - push: - branches: - - divy/bskylink + workflow_dispatch: env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} From dd2e173514b9aa22b898c5539f4f83f10d9b09f4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 25 Jun 2024 17:43:41 -0500 Subject: [PATCH 3/5] Add back pin action for feed cards (#4643) --- src/components/FeedCard.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index ecf0a1b91a..e0fc7ef54b 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -233,7 +233,11 @@ export function Action({ purpose?: AppBskyGraphDefs.ListView['purpose'] }) { const {hasSession} = useSession() - if (!hasSession || purpose !== 'app.bsky.graph.defs#curatelist') return null + if ( + !hasSession || + (type === 'list' && purpose !== 'app.bsky.graph.defs#curatelist') + ) + return null return } From b23f11268992b420b064d1a45429f8871a21d452 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 25 Jun 2024 17:02:43 -0700 Subject: [PATCH 4/5] Remove starterpack gate (#4645) --- src/lib/statsig/gates.ts | 1 - src/view/screens/Profile.tsx | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index bf2484ccb9..46ef934ef6 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -5,4 +5,3 @@ export type Gate = | 'request_notifications_permission_after_onboarding_v2' | 'show_avi_follow_button' | 'show_follow_back_label_v2' - | 'starter_packs_enabled' diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 946f6ac543..37111c02e5 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -27,15 +27,12 @@ import {useAgent, useSession} from '#/state/session' import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' import {useAnalytics} from 'lib/analytics/analytics' -import {IS_DEV, IS_TESTFLIGHT} from 'lib/app-info' import {useSetTitle} from 'lib/hooks/useSetTitle' import {ComposeIcon2} from 'lib/icons' import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' -import {useGate} from 'lib/statsig/statsig' import {combinedDisplayName} from 'lib/strings/display-names' import {isInvalidHandle} from 'lib/strings/handles' import {colors, s} from 'lib/styles' -import {isWeb} from 'platform/detection' import {listenSoftReset} from 'state/events' import {useActorStarterPacksQuery} from 'state/queries/actor-starter-packs' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' @@ -170,9 +167,6 @@ function ProfileScreenLoaded({ const [currentPage, setCurrentPage] = React.useState(0) const {_} = useLingui() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() - const gate = useGate() - const starterPacksEnabled = - IS_DEV || IS_TESTFLIGHT || (!isWeb && gate('starter_packs_enabled')) const [scrollViewTag, setScrollViewTag] = React.useState(null) @@ -205,8 +199,7 @@ function ProfileScreenLoaded({ const showLikesTab = isMe const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0 const showStarterPacksTab = - starterPacksEnabled && - (isMe || !!starterPacksQuery.data?.pages?.[0].starterPacks.length) + isMe || !!starterPacksQuery.data?.pages?.[0].starterPacks.length const showListsTab = hasSession && (isMe || (profile.associated?.lists || 0) > 0) From ffe15a981a0a9696e4e1a463e758f118c762490e Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 26 Jun 2024 01:23:30 -0700 Subject: [PATCH 5/5] shared preferences api --- __e2e__/flows/shared-prefs.yml | 31 +++ .../ExpoBlueskyDevicePrefsModule.kt | 10 - .../ExpoBlueskyDevicePrefsModule.kt | 69 +++++++ .../sharedprefs/SharedPrefs.kt | 193 ++++++++++++++++++ .../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 | 50 +++++ .../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, 673 insertions(+), 80 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/ExpoBlueskyDevicePrefsModule.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 0000000000..73a0668298 --- /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 29017f17aa..0000000000 --- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt +++ /dev/null @@ -1,10 +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/ExpoBlueskyDevicePrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskyDevicePrefsModule.kt new file mode 100644 index 0000000000..cb1df4db30 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskyDevicePrefsModule.kt @@ -0,0 +1,69 @@ +package expo.modules.blueskyswissarmy.sharedprefs + +import android.content.Context +import android.util.Log +import expo.modules.kotlin.Promise +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 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/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json index 730bc6114f..1111f8a0be 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 1b2f892494..89cea00a28 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 b13a9fe3fa..0000000000 --- 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 0000000000..8549e5b48b --- /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 0000000000..a11a71834c --- /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 4271850866..0000000000 --- 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 f1eee6c282..0000000000 --- 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 0000000000..589b074099 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts @@ -0,0 +1,50 @@ +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 + } +} 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 0000000000..769344007b --- /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 f3e2e0c662..830ec91d7f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,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 5cb4f4105f..3d8e0f1e6c 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' @@ -230,6 +231,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 b6e4ab05b1..212ecae71d 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 8a173b6756..58069f5aa9 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 0000000000..380f1080b0 --- /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 6d166d4b64..ca2420fed0 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