Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add demo mode entry in onboarding screen #6453

Merged
merged 11 commits into from
Jan 31, 2025
9 changes: 8 additions & 1 deletion locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2857,5 +2857,12 @@
"tokenAndLocalAmount": "{{tokenAmount}} {{tokenSymbol}} <0>({{localCurrencySymbol}}{{localAmount}})</0>",
"tokenAmountApprox": "≈ {{tokenAmount}} {{tokenSymbol}}",
"localAmountApprox": "≈ {{localCurrencySymbol}}{{localAmount}}",
"tokenAndLocalAmountApprox": "≈ {{tokenAmount}} {{tokenSymbol}} <0>({{localCurrencySymbol}}{{localAmount}})</0>"
"tokenAndLocalAmountApprox": "≈ {{tokenAmount}} {{tokenSymbol}} <0>({{localCurrencySymbol}}{{localAmount}})</0>",
"demoMode": {
"confirmEnter": {
"title": "Demo Mode",
"info": "You are about to enter into demo mode. You are free to explore the features of the app without completing any transactions",
"cta": "Enter Demo"
}
}
}
1 change: 1 addition & 0 deletions src/dapps/DappShortcutsRewards.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createMockStore } from 'test/utils'
import { mockCusdAddress, mockCusdTokenId, mockPositions, mockShortcuts } from 'test/values'

jest.mock('src/statsig', () => ({
...jest.requireActual('src/statsig'),
getFeatureGate: jest.fn(() => true),
}))
jest.mock('src/web3/networkConfig', () => {
Expand Down
1 change: 1 addition & 0 deletions src/keylessBackup/keychain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { generatePrivateKey } from 'viem/accounts'

jest.mock('src/pincode/authentication')
jest.mock('src/storage/keychain')
jest.mock('src/statsig')

describe(storeSECP256k1PrivateKey, () => {
beforeEach(() => {
Expand Down
38 changes: 28 additions & 10 deletions src/navigator/NavigatorWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SeverityLevel } from '@sentry/types'
import * as React from 'react'
import { StyleSheet, View } from 'react-native'
import DeviceInfo from 'react-native-device-info'
import LinearGradient from 'react-native-linear-gradient'
import ShakeForSupport from 'src/account/ShakeForSupport'
import AlertBanner from 'src/alert/AlertBanner'
import AppAnalytics from 'src/analytics/AppAnalytics'
Expand All @@ -30,9 +31,11 @@ import { getDynamicConfigParams } from 'src/statsig'
import { DynamicConfigs } from 'src/statsig/constants'
import { StatsigDynamicConfigs } from 'src/statsig/types'
import appTheme from 'src/styles/appTheme'
import Colors from 'src/styles/colors'
import Logger from 'src/utils/Logger'
import { userInSanctionedCountrySelector } from 'src/utils/countryFeatures'
import { isVersionBelowMinimum } from 'src/utils/versionCheck'
import { demoModeEnabledSelector } from 'src/web3/selectors'

// This uses RN Navigation's experimental nav state persistence
// to improve the hot reloading experience when in DEV mode
Expand Down Expand Up @@ -62,6 +65,7 @@ export const NavigatorWrapper = () => {
)
const routeNameRef = React.useRef<string>()
const inSanctionedCountry = useSelector(userInSanctionedCountrySelector)
const demoModeEnabled = useSelector(demoModeEnabledSelector)

const dispatch = useDispatch()

Expand Down Expand Up @@ -174,16 +178,26 @@ export const NavigatorWrapper = () => {
initialState={initialState}
theme={appTheme}
>
<View style={styles.container}>
<Navigator />
<HooksPreviewModeBanner />
{(appLocked || updateRequired) && (
<View style={styles.locked}>{updateRequired ? <UpgradeScreen /> : <PincodeLock />}</View>
)}
<AlertBanner />
<ShakeForSupport />
<JumpstartClaimStatusToasts />
</View>
<LinearGradient
colors={[Colors.brandGradientLeft, Colors.brandGradientRight]}
locations={[0, 0.8915]}
useAngle={true}
angle={90}
style={styles.linearGradientBackground}
>
<View style={[styles.container, { borderWidth: demoModeEnabled ? 3 : 0 }]}>
<Navigator />
<HooksPreviewModeBanner />
{(appLocked || updateRequired) && (
<View style={styles.locked}>
{updateRequired ? <UpgradeScreen /> : <PincodeLock />}
</View>
)}
<AlertBanner />
<ShakeForSupport />
<JumpstartClaimStatusToasts />
</View>
</LinearGradient>
</NavigationContainer>
)
}
Expand All @@ -194,6 +208,10 @@ const styles = StyleSheet.create({
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'flex-start',
borderColor: 'transparent',
},
linearGradientBackground: {
flex: 1,
},
locked: {
position: 'absolute',
Expand Down
1 change: 1 addition & 0 deletions src/navigator/SettingsMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createMockStore } from 'test/utils'
import { mockE164Number } from 'test/values'

jest.mock('src/statsig', () => ({
...jest.requireActual('src/statsig'),
getFeatureGate: jest.fn().mockReturnValue(false),
getMultichainFeatures: jest.fn(() => ({
showBalances: ['celo-alfajores'],
Expand Down
42 changes: 40 additions & 2 deletions src/onboarding/welcome/Welcome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@ import { Provider } from 'react-redux'
import { chooseCreateAccount, chooseRestoreAccount } from 'src/account/actions'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { OnboardingEvents } from 'src/analytics/Events'
import { navigate } from 'src/navigator/NavigationService'
import { navigate, navigateHome } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { firstOnboardingScreen } from 'src/onboarding/steps'
import Welcome from 'src/onboarding/welcome/Welcome'
import { patchUpdateStatsigUser } from 'src/statsig'
import { getDynamicConfigParams, patchUpdateStatsigUser } from 'src/statsig'
import { DynamicConfigs } from 'src/statsig/constants'
import { StatsigDynamicConfigs } from 'src/statsig/types'
import { demoModeToggled } from 'src/web3/actions'
import { createMockStore } from 'test/utils'

jest.mock('src/onboarding/steps')
jest.mock('src/statsig', () => ({
patchUpdateStatsigUser: jest.fn(),
getDynamicConfigParams: jest.fn(),
}))

describe('Welcome', () => {
beforeAll(() => {
jest.spyOn(Date, 'now').mockImplementation(() => 123)
jest.mocked(firstOnboardingScreen).mockReturnValue(Screens.PincodeSet)
jest.mocked(getDynamicConfigParams).mockReturnValue({})
})

beforeEach(() => {
Expand Down Expand Up @@ -129,5 +134,38 @@ describe('Welcome', () => {
expect(AppAnalytics.track).toHaveBeenCalledWith(event)
}
)
it('dispatches the correct actions to launch demo mode', async () => {
jest.mocked(getDynamicConfigParams).mockImplementation((configName) => {
if (configName === DynamicConfigs[StatsigDynamicConfigs.DEMO_MODE_CONFIG]) {
return { enabledInOnboarding: true }
}
throw new Error('Unexpected config name')
})

const store = createMockStore()
const { getByText, rerender } = render(
<Provider store={store}>
<Welcome />
</Provider>
)

fireEvent.press(getByText('demoMode.confirmEnter.cta'))

expect(store.getActions()).toEqual([demoModeToggled(true)])
await waitFor(() => expect(navigateHome).not.toHaveBeenCalled())

rerender(
<Provider
store={createMockStore({
web3: {
demoModeEnabled: true,
},
})}
>
<Welcome />
</Provider>
)
await waitFor(() => expect(navigateHome).toHaveBeenCalled())
})
})
})
60 changes: 56 additions & 4 deletions src/onboarding/welcome/Welcome.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,59 @@
import React from 'react'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { ImageBackground, StyleSheet, View } from 'react-native'
import { TouchableWithoutFeedback } from 'react-native-gesture-handler'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { chooseCreateAccount, chooseRestoreAccount } from 'src/account/actions'
import { recoveringFromStoreWipeSelector } from 'src/account/selectors'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { OnboardingEvents } from 'src/analytics/Events'
import BottomSheet, { BottomSheetModalRefType } from 'src/components/BottomSheet'
import Button, { BtnSizes, BtnTypes } from 'src/components/Button'
import { welcomeBackground } from 'src/images/Images'
import WelcomeLogo from 'src/images/WelcomeLogo'
import { nuxNavigationOptions } from 'src/navigator/Headers'
import { navigate } from 'src/navigator/NavigationService'
import { navigate, navigateHome } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import LanguageButton from 'src/onboarding/LanguageButton'
import { firstOnboardingScreen } from 'src/onboarding/steps'
import { useDispatch, useSelector } from 'src/redux/hooks'
import { patchUpdateStatsigUser } from 'src/statsig'
import { getDynamicConfigParams, patchUpdateStatsigUser } from 'src/statsig'
import { DynamicConfigs } from 'src/statsig/constants'
import { StatsigDynamicConfigs } from 'src/statsig/types'
import { Spacing } from 'src/styles/styles'
import { demoModeToggled } from 'src/web3/actions'
import { demoModeEnabledSelector } from 'src/web3/selectors'

const DEMO_MODE_LONG_PRESS_DELAY_MS = 4_000

export default function Welcome() {
const demoModeBottomSheetRef = useRef<BottomSheetModalRefType>(null)

const { t } = useTranslation()
const dispatch = useDispatch()
const acceptedTerms = useSelector((state) => state.account.acceptedTerms)
const startOnboardingTime = useSelector((state) => state.account.startOnboardingTime)
const insets = useSafeAreaInsets()
const recoveringFromStoreWipe = useSelector(recoveringFromStoreWipeSelector)
const demoModeEnabled = useSelector(demoModeEnabledSelector)

const { enabledInOnboarding } = getDynamicConfigParams(
DynamicConfigs[StatsigDynamicConfigs.DEMO_MODE_CONFIG]
)

useEffect(() => {
if (demoModeEnabled) {
navigateHome()
}
}, [demoModeEnabled])

const onActivateDemoMode = () => {
dispatch(demoModeToggled(true))
}

const onRequestActivateDemoMode = () => {
demoModeBottomSheetRef.current?.snapToIndex(0)
}

const startOnboarding = () => {
navigate(
Expand Down Expand Up @@ -65,7 +94,14 @@ export default function Welcome() {
<SafeAreaView style={styles.container}>
<ImageBackground source={welcomeBackground} resizeMode="stretch" style={styles.image}>
<View style={styles.contentContainer}>
<WelcomeLogo />
<TouchableWithoutFeedback
disabled={!enabledInOnboarding}
delayLongPress={DEMO_MODE_LONG_PRESS_DELAY_MS}
onLongPress={onRequestActivateDemoMode}
testID="Welcome/RequestActivateDemoMode"
>
<WelcomeLogo />
</TouchableWithoutFeedback>
</View>
<View style={{ ...styles.buttonView, marginBottom: Math.max(0, 40 - insets.bottom) }}>
<Button
Expand All @@ -85,6 +121,19 @@ export default function Welcome() {
/>
</View>
</ImageBackground>
<BottomSheet
forwardedRef={demoModeBottomSheetRef}
title={t('demoMode.confirmEnter.title')}
description={t('demoMode.confirmEnter.info')}
>
<Button
onPress={onActivateDemoMode}
text={t('demoMode.confirmEnter.cta')}
size={BtnSizes.FULL}
type={BtnTypes.PRIMARY}
style={styles.demoModeButton}
/>
</BottomSheet>
</SafeAreaView>
)
}
Expand Down Expand Up @@ -114,4 +163,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
marginTop: Spacing.XLarge48,
},
demoModeButton: {
marginTop: Spacing.Thick24,
},
})
7 changes: 7 additions & 0 deletions src/redux/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1986,4 +1986,11 @@ export const migrations = {
...state,
app: _.omit(state.app, 'showSwapMenuInDrawerMenu'),
}),
240: (state: any) => ({
...state,
web3: {
...state.web3,
demoModeEnabled: false,
},
}),
}
3 changes: 2 additions & 1 deletion src/redux/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe('store state', () => {
{
"_persist": {
"rehydrated": true,
"version": 239,
"version": 240,
},
"account": {
"acceptedTerms": false,
Expand Down Expand Up @@ -381,6 +381,7 @@ describe('store state', () => {
},
"web3": {
"account": "0x0000000000000000000000000000000000007E57",
"demoModeEnabled": false,
},
}
`)
Expand Down
2 changes: 1 addition & 1 deletion src/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const persistConfig: PersistConfig<ReducersRootState> = {
key: 'root',
// default is -1, increment as we make migrations
// See https://github.com/valora-inc/wallet/tree/main/WALLET.md#redux-state-migration
version: 239,
version: 240,
keyPrefix: `reduxStore-`, // the redux-persist default is `persist:` which doesn't work with some file systems.
storage: FSStorage(),
blacklist: ['networkInfo', 'alert', 'imports', 'keylessBackup', transactionFeedV2Api.reducerPath],
Expand Down
7 changes: 7 additions & 0 deletions src/statsig/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ export const DynamicConfigs = {
supportedAppIds: [] as string[],
},
},
[StatsigDynamicConfigs.DEMO_MODE_CONFIG]: {
configName: StatsigDynamicConfigs.DEMO_MODE_CONFIG,
defaultValues: {
enabledInOnboarding: false,
demoWalletAddress: '',
},
},
} satisfies {
[key in StatsigDynamicConfigs | StatsigMultiNetworkDynamicConfig]: {
configName: key
Expand Down
1 change: 1 addition & 0 deletions src/statsig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum StatsigDynamicConfigs {
NFT_CELEBRATION_CONFIG = 'nft_celebration_config',
APP_CONFIG = 'app_config',
EARN_CONFIG = 'earn_config',
DEMO_MODE_CONFIG = 'demo_mode_config',
}

// Separating into different enum from StatsigDynamicConfigs to allow for more strict typing
Expand Down
15 changes: 14 additions & 1 deletion src/web3/actions.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
export enum Actions {
SET_ACCOUNT = 'WEB3/SET_ACCOUNT',
DEMO_MODE_TOGGLED = 'WEB3/DEMO_MODE_TOGGLED',
}

export interface SetAccountAction {
type: Actions.SET_ACCOUNT
address: string
}

export type ActionTypes = SetAccountAction
interface DemoModeToggled {
type: Actions.DEMO_MODE_TOGGLED
enabled: boolean
}

export type ActionTypes = SetAccountAction | DemoModeToggled

export const setAccount = (address: string): SetAccountAction => {
return {
type: Actions.SET_ACCOUNT,
address: address.toLowerCase(),
}
}

export const demoModeToggled = (enabled: boolean): DemoModeToggled => {
return {
type: Actions.DEMO_MODE_TOGGLED,
enabled,
}
}
Loading
Loading