diff --git a/locales/base/translation.json b/locales/base/translation.json index bdb650b507b..d129d171043 100644 --- a/locales/base/translation.json +++ b/locales/base/translation.json @@ -2857,5 +2857,12 @@ "tokenAndLocalAmount": "{{tokenAmount}} {{tokenSymbol}} <0>({{localCurrencySymbol}}{{localAmount}})", "tokenAmountApprox": "≈ {{tokenAmount}} {{tokenSymbol}}", "localAmountApprox": "≈ {{localCurrencySymbol}}{{localAmount}}", - "tokenAndLocalAmountApprox": "≈ {{tokenAmount}} {{tokenSymbol}} <0>({{localCurrencySymbol}}{{localAmount}})" + "tokenAndLocalAmountApprox": "≈ {{tokenAmount}} {{tokenSymbol}} <0>({{localCurrencySymbol}}{{localAmount}})", + "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" + } + } } diff --git a/src/dapps/DappShortcutsRewards.test.tsx b/src/dapps/DappShortcutsRewards.test.tsx index 7a7a7c28ae4..c789c384e8e 100644 --- a/src/dapps/DappShortcutsRewards.test.tsx +++ b/src/dapps/DappShortcutsRewards.test.tsx @@ -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', () => { diff --git a/src/keylessBackup/keychain.test.ts b/src/keylessBackup/keychain.test.ts index 0f1cf007ddb..b66f1ef39ab 100644 --- a/src/keylessBackup/keychain.test.ts +++ b/src/keylessBackup/keychain.test.ts @@ -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(() => { diff --git a/src/navigator/NavigatorWrapper.tsx b/src/navigator/NavigatorWrapper.tsx index 8607a720407..2539a13d940 100644 --- a/src/navigator/NavigatorWrapper.tsx +++ b/src/navigator/NavigatorWrapper.tsx @@ -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' @@ -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 @@ -62,6 +65,7 @@ export const NavigatorWrapper = () => { ) const routeNameRef = React.useRef() const inSanctionedCountry = useSelector(userInSanctionedCountrySelector) + const demoModeEnabled = useSelector(demoModeEnabledSelector) const dispatch = useDispatch() @@ -174,16 +178,26 @@ export const NavigatorWrapper = () => { initialState={initialState} theme={appTheme} > - - - - {(appLocked || updateRequired) && ( - {updateRequired ? : } - )} - - - - + + + + + {(appLocked || updateRequired) && ( + + {updateRequired ? : } + + )} + + + + + ) } @@ -194,6 +208,10 @@ const styles = StyleSheet.create({ flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-start', + borderColor: 'transparent', + }, + linearGradientBackground: { + flex: 1, }, locked: { position: 'absolute', diff --git a/src/navigator/SettingsMenu.test.tsx b/src/navigator/SettingsMenu.test.tsx index d9670e3af13..98c0c2c5e3e 100644 --- a/src/navigator/SettingsMenu.test.tsx +++ b/src/navigator/SettingsMenu.test.tsx @@ -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'], diff --git a/src/onboarding/welcome/Welcome.test.tsx b/src/onboarding/welcome/Welcome.test.tsx index 47d2b019655..dc73e5f4bb8 100644 --- a/src/onboarding/welcome/Welcome.test.tsx +++ b/src/onboarding/welcome/Welcome.test.tsx @@ -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(() => { @@ -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( + + + + ) + + fireEvent.press(getByText('demoMode.confirmEnter.cta')) + + expect(store.getActions()).toEqual([demoModeToggled(true)]) + await waitFor(() => expect(navigateHome).not.toHaveBeenCalled()) + + rerender( + + + + ) + await waitFor(() => expect(navigateHome).toHaveBeenCalled()) + }) }) }) diff --git a/src/onboarding/welcome/Welcome.tsx b/src/onboarding/welcome/Welcome.tsx index a8023b24348..272a553bd93 100644 --- a/src/onboarding/welcome/Welcome.tsx +++ b/src/onboarding/welcome/Welcome.tsx @@ -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(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( @@ -65,7 +94,14 @@ export default function Welcome() { - + + +