diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 7f04da1267..dc40a34963 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -1,17 +1,17 @@ import React from 'react' import {Platform} from 'react-native' -import {AppState, AppStateStatus} from 'react-native' +import {AppState, type AppStateStatus} from 'react-native' import {Statsig, StatsigProvider} from 'statsig-react-native-expo' import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info' import {logger} from '#/logger' -import {MetricEvents} from '#/logger/metrics' +import {type MetricEvents} from '#/logger/metrics' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' import {useSession} from '../../state/session' import {timeout} from '../async/timeout' import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback' -import {Gate} from './gates' +import {type Gate} from './gates' const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV' @@ -180,25 +180,26 @@ export function useGate(): (gateName: Gate, options?: GateOptions) => boolean { } /** - * Debugging tool to override a gate. USE ONLY IN E2E TESTS! + * Tool to override a gate on the local device */ -export function useDangerousSetGate(): ( +export function useSetLocalGateOverride(): ( gateName: Gate, value: boolean, ) => void { const cache = React.useContext(GateCache) if (!cache) { throw Error( - 'useDangerousSetGate() cannot be called outside StatsigProvider.', + 'useSetLocalOverride() cannot be called outside StatsigProvider.', ) } - const dangerousSetGate = React.useCallback( + const setGate = React.useCallback( (gateName: Gate, value: boolean) => { cache.set(gateName, value) + persisted.write('gateOverrides', Object.fromEntries(cache.entries())) }, [cache], ) - return dangerousSetGate + return setGate } function toStatsigUser(did: string | undefined): StatsigUser { @@ -286,11 +287,11 @@ export function Provider({children}: {children: React.ReactNode}) { // Have our own cache in front of Statsig. // This ensures the results remain stable until the active DID changes. - const [gateCache, setGateCache] = React.useState(() => new Map()) + const [gateCache, setGateCache] = React.useState(() => createGateCache()) const [prevDid, setPrevDid] = React.useState(did) if (did !== prevDid) { setPrevDid(did) - setGateCache(new Map()) + setGateCache(createGateCache()) } // Periodically poll Statsig to get the current rule evaluations for all stored accounts. @@ -323,3 +324,7 @@ export function Provider({children}: {children: React.ReactNode}) { ) } + +function createGateCache(): Map { + return new Map(Object.entries(persisted.get('gateOverrides') || {})) +} diff --git a/src/screens/Settings/LabSettings/FeatureGateDialog.tsx b/src/screens/Settings/LabSettings/FeatureGateDialog.tsx index dd2eb98c7d..9905cab6da 100644 --- a/src/screens/Settings/LabSettings/FeatureGateDialog.tsx +++ b/src/screens/Settings/LabSettings/FeatureGateDialog.tsx @@ -1,9 +1,11 @@ import {useState} from 'react' import {View} from 'react-native' +import * as Updates from 'expo-updates' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {type Gate, useGateDescriptions} from '#/lib/statsig/gates' +import {useGate, useSetLocalGateOverride} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -14,6 +16,7 @@ import { EmojiSad_Stroke2_Corner0_Rounded as EmojiSad, EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile, } from '#/components/icons/Emoji' +import {Loader} from '#/components/Loader' import {P, Text} from '#/components/Typography' enum FeedbackToken { @@ -37,10 +40,20 @@ export function FeatureGateDialog({ }) { const {_} = useLingui() const t = useTheme() - const descriptions = useGateDescriptions() + const gateApi = useGate() + const setGateApi = useSetLocalGateOverride() + + const [originalEnabled] = useState(() => gateApi(gate)) + const [enabled, setEnabled] = useState(originalEnabled) + const [isRestarting, setIsRestarting] = useState(false) const desc = descriptions[gate] + const onToggleGate = v => { + setGateApi(gate, v) + setEnabled(v) + } + if (!desc) { return null } @@ -66,16 +79,36 @@ export function FeatureGateDialog({ name="quoteposts" type="checkbox" label={_(msg`Tap to toggle this experiment.`)} - value={true} - onChange={_v => {}} + value={enabled} + onChange={onToggleGate} style={[a.justify_between]}> - Enable on this device + Enabled on this device + {enabled !== originalEnabled ? ( + + ) : null} + {desc.help ? ( <> diff --git a/src/screens/Settings/LabSettings/index.tsx b/src/screens/Settings/LabSettings/index.tsx index 713f653632..f5446cb6f6 100644 --- a/src/screens/Settings/LabSettings/index.tsx +++ b/src/screens/Settings/LabSettings/index.tsx @@ -4,6 +4,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type CommonNavigatorParams} from '#/lib/routes/types' import {type Gate, useGateDescriptions} from '#/lib/statsig/gates' +import {useGate} from '#/lib/statsig/statsig' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' @@ -41,7 +42,7 @@ export function LabSettingsScreen({}: Props) { {gates.map(gate => ( - + ))} @@ -49,10 +50,12 @@ export function LabSettingsScreen({}: Props) { ) } -function ExperimentButton({gate, enabled}: {gate: Gate; enabled: boolean}) { +function ExperimentButton({gate}: {gate: Gate}) { const t = useTheme() const ctrl = useDialogControl() const descriptions = useGateDescriptions() + const gateApi = useGate() + const enabled = gateApi(gate) return ( <> diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index f840081f3d..acaab5a998 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -127,6 +127,7 @@ const schema = z.object({ mutedThreads: z.array(z.string()), trendingDisabled: z.boolean().optional(), trendingVideoDisabled: z.boolean().optional(), + gateOverrides: z.object({}).catchall(z.boolean()).optional(), }) export type Schema = z.infer @@ -174,6 +175,7 @@ export const defaults: Schema = { subtitlesEnabled: true, trendingDisabled: false, trendingVideoDisabled: false, + gateOverrides: {}, } export function tryParse(rawData: string): Schema | undefined {