diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 0ca280bd10..fd9bdc0971 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -24,7 +24,9 @@ const NetworkSelector = (): ReactElement => { trackEvent({ ...OVERVIEW_EVENTS.SWITCH_NETWORK, label: selectedChainId }) - const shouldKeepPath = [AppRoutes.load, AppRoutes.open].includes(router.pathname) + const shouldKeepPath = [AppRoutes.load, AppRoutes.open, AppRoutes.newSafe.create, AppRoutes.newSafe.add].includes( + router.pathname, + ) const newRoute = { pathname: shouldKeepPath ? router.pathname : '/', diff --git a/src/components/safe-apps/SafeAppLandingPage/AppActions.tsx b/src/components/safe-apps/SafeAppLandingPage/AppActions.tsx index 27574d9e23..76bd42eb51 100644 --- a/src/components/safe-apps/SafeAppLandingPage/AppActions.tsx +++ b/src/components/safe-apps/SafeAppLandingPage/AppActions.tsx @@ -15,6 +15,7 @@ import { AppRoutes } from '@/config/routes' import useOwnedSafes from '@/hooks/useOwnedSafes' import { CTA_BUTTON_WIDTH, CTA_HEIGHT } from '@/components/safe-apps/SafeAppLandingPage/constants' import CreateNewSafeSVG from '@/public/images/open/safe-creation.svg' +import useNewSafeRoutes from '@/hooks/useNewSafeRoutes' type Props = { appUrl: string @@ -30,6 +31,7 @@ const AppActions = ({ wallet, onConnectWallet, chain, appUrl, app }: Props): Rea const lastUsedSafe = useLastSafe() const ownedSafes = useOwnedSafes() const addressBook = useAppSelector(selectAllAddressBooks) + const { createSafe } = useNewSafeRoutes() const chains = useAppSelector(selectChains) const compatibleChains = app.chainIds @@ -71,7 +73,7 @@ const AppActions = ({ wallet, onConnectWallet, chain, appUrl, app }: Props): Rea case shouldCreateSafe: const redirect = `${AppRoutes.apps}?appUrl=${appUrl}` const createSafeHrefWithRedirect: UrlObject = { - pathname: AppRoutes.open, + pathname: createSafe, query: { safeViewRedirectURL: redirect, chain: chain.shortName }, } button = ( diff --git a/src/components/welcome/index.tsx b/src/components/welcome/index.tsx index 89b2be5346..f1b445139d 100644 --- a/src/components/welcome/index.tsx +++ b/src/components/welcome/index.tsx @@ -3,9 +3,11 @@ import { Button, Divider, Grid, Paper, Typography } from '@mui/material' import { useRouter } from 'next/router' import { CREATE_SAFE_EVENTS, LOAD_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' import Track from '../common/Track' +import useNewSafeRoutes from '@/hooks/useNewSafeRoutes' const NewSafe = () => { const router = useRouter() + const { createSafe, addSafe } = useNewSafeRoutes() return ( <> @@ -28,7 +30,7 @@ const NewSafe = () => { for creating your new Safe. - @@ -43,7 +45,7 @@ const NewSafe = () => { address. - diff --git a/src/config/routes.ts b/src/config/routes.ts index 8696da8c5a..9f7b10bcd7 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -5,13 +5,16 @@ export const AppRoutes = { load: '/load', index: '/', home: '/home', - createSafe: '/create-safe', apps: '/apps', addressBook: '/address-book', balances: { nfts: '/balances/nfts', index: '/balances', }, + newSafe: { + create: '/new-safe/create', + add: '/new-safe/add', + }, settings: { spendingLimits: '/settings/spending-limits', setup: '/settings/setup', diff --git a/src/hooks/useABTest.ts b/src/hooks/useABTest.ts new file mode 100644 index 0000000000..f1c2b4ad02 --- /dev/null +++ b/src/hooks/useABTest.ts @@ -0,0 +1,18 @@ +import { localItem } from '@/services/local-storage/local' +import useLocalStorage from '@/services/local-storage/useLocalStorage' + +const getAbTestKey = (name: string) => { + return `AB__${name}` +} + +export const getAbTestIsB = (name: string) => { + return localItem(getAbTestKey(name)).get() +} + +const useABTest = (name: string): boolean => { + const [isB] = useLocalStorage(getAbTestKey(name), Math.random() > 0.5, true) + + return isB +} + +export default useABTest diff --git a/src/hooks/useNewSafeRoutes.ts b/src/hooks/useNewSafeRoutes.ts new file mode 100644 index 0000000000..6bf66dc024 --- /dev/null +++ b/src/hooks/useNewSafeRoutes.ts @@ -0,0 +1,15 @@ +import useABTest from '@/hooks/useABTest' +import { AppRoutes } from '@/config/routes' + +export const NEW_SAFE_AB_TEST_NAME = 'newSafe' + +const useNewSafeRoutes = () => { + const shouldUseNewRoute = useABTest(NEW_SAFE_AB_TEST_NAME) + + return { + createSafe: shouldUseNewRoute ? AppRoutes.newSafe.create : AppRoutes.open, + addSafe: shouldUseNewRoute ? AppRoutes.newSafe.add : AppRoutes.load, + } +} + +export default useNewSafeRoutes diff --git a/src/pages/demo.tsx b/src/pages/demo.tsx deleted file mode 100644 index 20c09006d8..0000000000 --- a/src/pages/demo.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { NextPage } from 'next' -import Head from 'next/head' -import CreateSafe from '@/components/new-safe/CreateSafe' - -const Open: NextPage = () => { - return ( -
- - Safe – Create Safe - - - -
- ) -} - -export default Open diff --git a/src/pages/new-safe/add.tsx b/src/pages/new-safe/add.tsx new file mode 100644 index 0000000000..9355127e8a --- /dev/null +++ b/src/pages/new-safe/add.tsx @@ -0,0 +1,3 @@ +import Load from '../load' + +export default diff --git a/src/pages/create-safe.tsx b/src/pages/new-safe/create.tsx similarity index 100% rename from src/pages/create-safe.tsx rename to src/pages/new-safe/create.tsx diff --git a/src/services/analytics/events/createLoadSafe.ts b/src/services/analytics/events/createLoadSafe.ts index e210008f20..dd443177f4 100644 --- a/src/services/analytics/events/createLoadSafe.ts +++ b/src/services/analytics/events/createLoadSafe.ts @@ -1,59 +1,77 @@ +import { getAbTestIsB } from '@/hooks/useABTest' +import { NEW_SAFE_AB_TEST_NAME } from '@/hooks/useNewSafeRoutes' import { EventType } from '@/services/analytics/types' export const CREATE_SAFE_CATEGORY = 'create-safe' +// We cannot read the value immediately as the option may not have been decided yet +const isB = () => { + return getAbTestIsB(NEW_SAFE_AB_TEST_NAME) ? 'New' : 'Old' +} + export const CREATE_SAFE_EVENTS = { CREATE_BUTTON: { action: 'Open stepper', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, NAME_SAFE: { event: EventType.META, action: 'Name Safe', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, OWNERS: { event: EventType.META, action: 'Owners', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, THRESHOLD: { event: EventType.META, action: 'Threshold', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, SUBMIT_CREATE_SAFE: { event: EventType.META, action: 'Submit Safe creation', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, REJECT_CREATE_SAFE: { event: EventType.META, action: 'Reject Safe creation', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, RETRY_CREATE_SAFE: { event: EventType.META, action: 'Retry Safe creation', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, CANCEL_CREATE_SAFE: { event: EventType.META, action: 'Cancel Safe creation', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, CREATED_SAFE: { event: EventType.META, action: 'Created Safe', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, GET_STARTED: { action: 'Load Safe', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, GO_TO_SAFE: { action: 'Open Safe', category: CREATE_SAFE_CATEGORY, + abTest: isB, }, } diff --git a/src/services/analytics/gtm.ts b/src/services/analytics/gtm.ts index 6520a1b9ed..ed56ccd556 100644 --- a/src/services/analytics/gtm.ts +++ b/src/services/analytics/gtm.ts @@ -91,6 +91,7 @@ type ActionGtmEvent = GtmEvent & { eventCategory: string eventAction: string eventLabel?: EventLabel + abTest?: string } type PageviewGtmEvent = GtmEvent & { @@ -118,6 +119,10 @@ export const gtmTrack = (eventData: AnalyticsEvent): void => { gtmEvent.eventLabel = eventData.label } + if (eventData.abTest) { + gtmEvent.abTest = eventData.abTest() + } + gtmSend(gtmEvent) } diff --git a/src/services/analytics/types.ts b/src/services/analytics/types.ts index 1898ed3bda..2307e586d1 100644 --- a/src/services/analytics/types.ts +++ b/src/services/analytics/types.ts @@ -15,6 +15,7 @@ export type AnalyticsEvent = { category: string action: string label?: EventLabel + abTest?: () => string } export type SafeAppEvent = { diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 94f3f68300..0ed61ef316 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -1,19 +1,38 @@ import type { Dispatch, SetStateAction } from 'react' -import { useState, useCallback, useEffect } from 'react' +import { useState, useCallback } from 'react' import local from './local' -const useLocalStorage = (key: string, initialState: T): [T, Dispatch>] => { - const [cache, setCache] = useState(initialState) +/** + * Locally persisted equivalent of `useState` that saves to `localStorage` when `setNewValue` is called + * or (initially) when `shouldPersistInitialState` is `true` + * + * @param key `localStorage` key to store under + * @param initialState default state to return if no `localStorage` value exists + * @param shouldPersistInitialState if no `localStorage` value exists, persist the `initialState` in `localStorage` + * @returns persisted state if it exists, otherwise `initialState` + */ - useEffect(() => { - const initialValue = local.getItem(key) - if (initialValue !== undefined) { - setCache(initialValue) +const useLocalStorage = ( + key: string, + initialState: T, + shouldPersistInitialState = false, +): [T, Dispatch>] => { + const [cache, setCache] = useState(() => { + const value = local.getItem(key) + + if (value !== undefined) { + return value + } + + if (shouldPersistInitialState) { + local.setItem(key, initialState) } - }, [setCache, key]) - const setNewValue = useCallback( - (value: T | ((prevState: T) => T)) => { + return initialState + }) + + const setNewValue: Dispatch> = useCallback( + (value) => { setCache((prevState) => { const newState = value instanceof Function ? value(prevState) : value