From 52b0c30528fd8e8f380620da1f919ee7ec0a0710 Mon Sep 17 00:00:00 2001 From: fbwoolf Date: Mon, 30 May 2022 16:51:58 -0500 Subject: [PATCH] refactor: move steps to feature --- .../components/add-funds-step.tsx | 41 ++++++ .../components/back-up-secret-key-step.tsx | 42 ++++++ .../components/buy-nft-step.tsx | 42 ++++++ .../components/explore-apps-step.tsx | 51 +++++++ .../components/step-done-badge.tsx | 30 ++--- .../components/step-full-page.tsx | 81 ++++++++++++ .../components/step-popup.tsx | 60 +++++++++ .../components/suggested-first-step.tsx | 32 +++++ .../hooks/use-suggested-first-steps.ts} | 37 +++--- .../suggested-first-steps.layout.tsx | 62 +++++++++ .../suggested-first-steps.tsx | 33 +++++ .../fund/components/fast-checkout-badge.tsx | 26 ++-- .../components/skip-fund-account-button.tsx | 4 +- .../components/zero-percent-fees-badge.tsx | 26 ++-- .../home/components/onboarding-step-item.tsx | 125 ------------------ .../home/components/onboarding-steps-list.tsx | 110 --------------- .../pages/home/components/onboarding-steps.ts | 73 ---------- src/app/pages/home/home.tsx | 10 +- .../onboarding/sign-in/hooks/use-sign-in.ts | 4 +- .../store/onboarding/onboarding.selectors.ts | 20 ++- src/app/store/onboarding/onboarding.slice.ts | 30 ++--- src/shared/models/onboarding-types.ts | 27 ++-- .../integration/onboarding/onboarding.spec.ts | 10 +- tests/page-objects/wallet.page.ts | 10 +- 24 files changed, 559 insertions(+), 427 deletions(-) create mode 100644 src/app/features/suggested-first-steps/components/add-funds-step.tsx create mode 100644 src/app/features/suggested-first-steps/components/back-up-secret-key-step.tsx create mode 100644 src/app/features/suggested-first-steps/components/buy-nft-step.tsx create mode 100644 src/app/features/suggested-first-steps/components/explore-apps-step.tsx rename src/app/{pages/home => features/suggested-first-steps}/components/step-done-badge.tsx (55%) create mode 100644 src/app/features/suggested-first-steps/components/step-full-page.tsx create mode 100644 src/app/features/suggested-first-steps/components/step-popup.tsx create mode 100644 src/app/features/suggested-first-steps/components/suggested-first-step.tsx rename src/app/{pages/home/hooks/use-onboarding-steps.ts => features/suggested-first-steps/hooks/use-suggested-first-steps.ts} (52%) create mode 100644 src/app/features/suggested-first-steps/suggested-first-steps.layout.tsx create mode 100644 src/app/features/suggested-first-steps/suggested-first-steps.tsx delete mode 100644 src/app/pages/home/components/onboarding-step-item.tsx delete mode 100644 src/app/pages/home/components/onboarding-steps-list.tsx delete mode 100644 src/app/pages/home/components/onboarding-steps.ts diff --git a/src/app/features/suggested-first-steps/components/add-funds-step.tsx b/src/app/features/suggested-first-steps/components/add-funds-step.tsx new file mode 100644 index 00000000000..337f669b172 --- /dev/null +++ b/src/app/features/suggested-first-steps/components/add-funds-step.tsx @@ -0,0 +1,41 @@ +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { useSuggestedFirstStepsStatus } from '@app/store/onboarding/onboarding.selectors'; +import AddFundsFull from '@assets/images/onboarding/steps/add-funds-light.png'; +import AddFundsFullDone from '@assets/images/onboarding/steps/add-funds-light-done.png'; +import AddFundsPopup from '@assets/images/onboarding/steps/add-funds-light-sm.png'; +import AddFundsPopupDone from '@assets/images/onboarding/steps/add-funds-light-done-sm.png'; +import { SuggestedFirstSteps, SuggestedFirstStepStatus } from '@shared/models/onboarding-types'; +import { RouteUrls } from '@shared/route-urls'; + +import { SuggestedFirstStep } from './suggested-first-step'; + +export function AddFundsStep() { + const analytics = useAnalytics(); + const navigate = useNavigate(); + const suggestedFirstStepsStatus = useSuggestedFirstStepsStatus(); + + const onSelectStep = useCallback(() => { + void analytics.track('select_next_step', { step: SuggestedFirstSteps.AddFunds }); + navigate(RouteUrls.Fund); + }, [analytics, navigate]); + + return ( + + ); +} diff --git a/src/app/features/suggested-first-steps/components/back-up-secret-key-step.tsx b/src/app/features/suggested-first-steps/components/back-up-secret-key-step.tsx new file mode 100644 index 00000000000..8cb760014ad --- /dev/null +++ b/src/app/features/suggested-first-steps/components/back-up-secret-key-step.tsx @@ -0,0 +1,42 @@ +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { useSuggestedFirstStepsStatus } from '@app/store/onboarding/onboarding.selectors'; +import BackUpSecretKeyFull from '@assets/images/onboarding/steps/backup-key-light.png'; +import BackUpSecretKeyFullDone from '@assets/images/onboarding/steps/backup-key-light-done.png'; +import BackUpSecretKeyPopup from '@assets/images/onboarding/steps/backup-key-light-sm.png'; +import BackUpSecretKeyPopupDone from '@assets/images/onboarding/steps/backup-key-light-done-sm.png'; +import { SuggestedFirstSteps, SuggestedFirstStepStatus } from '@shared/models/onboarding-types'; +import { RouteUrls } from '@shared/route-urls'; + +import { SuggestedFirstStep } from './suggested-first-step'; + +export function BackUpSecretKeyStep() { + const analytics = useAnalytics(); + const navigate = useNavigate(); + const suggestedFirstStepsStatus = useSuggestedFirstStepsStatus(); + + const onSelectStep = useCallback(() => { + void analytics.track('select_next_step', { step: SuggestedFirstSteps.BackUpSecretKey }); + navigate(RouteUrls.ViewSecretKey); + }, [analytics, navigate]); + + return ( + + ); +} diff --git a/src/app/features/suggested-first-steps/components/buy-nft-step.tsx b/src/app/features/suggested-first-steps/components/buy-nft-step.tsx new file mode 100644 index 00000000000..292c57c4c3e --- /dev/null +++ b/src/app/features/suggested-first-steps/components/buy-nft-step.tsx @@ -0,0 +1,42 @@ +import { useCallback } from 'react'; + +import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { openInNewTab } from '@app/common/utils/open-in-new-tab'; +import { useSuggestedFirstStepsStatus } from '@app/store/onboarding/onboarding.selectors'; +import BuyNftFull from '@assets/images/onboarding/steps/buy-nft-light.png'; +import BuyNftFullDone from '@assets/images/onboarding/steps/buy-nft-light-done.png'; +import BuyNftPopup from '@assets/images/onboarding/steps/buy-nft-light-sm.png'; +import BuyNftPopupDone from '@assets/images/onboarding/steps/buy-nft-light-done-sm.png'; +import { SuggestedFirstSteps, SuggestedFirstStepStatus } from '@shared/models/onboarding-types'; + +import { SuggestedFirstStep } from './suggested-first-step'; + +const buyNftExternalRoute = 'https://www.hiro.so/wallet-faq/nfts'; + +export function BuyNftStep() { + const analytics = useAnalytics(); + const suggestedFirstStepsStatus = useSuggestedFirstStepsStatus(); + + const onSelectStep = useCallback(() => { + void analytics.track('select_next_step', { step: SuggestedFirstSteps.BuyNft }); + openInNewTab(buyNftExternalRoute); + }, [analytics]); + + return ( + + ); +} diff --git a/src/app/features/suggested-first-steps/components/explore-apps-step.tsx b/src/app/features/suggested-first-steps/components/explore-apps-step.tsx new file mode 100644 index 00000000000..d84806a67bd --- /dev/null +++ b/src/app/features/suggested-first-steps/components/explore-apps-step.tsx @@ -0,0 +1,51 @@ +import { useCallback } from 'react'; + +import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { openInNewTab } from '@app/common/utils/open-in-new-tab'; +import { useAppDispatch } from '@app/store'; +import { useSuggestedFirstStepsStatus } from '@app/store/onboarding/onboarding.selectors'; +import ExploreAppsFull from '@assets/images/onboarding/steps/explore-apps-light.png'; +import ExploreAppsFullDone from '@assets/images/onboarding/steps/explore-apps-light-done.png'; +import ExploreAppsPopup from '@assets/images/onboarding/steps/explore-apps-light-sm.png'; +import ExploreAppsPopupDone from '@assets/images/onboarding/steps/explore-apps-light-done-sm.png'; +import { SuggestedFirstSteps, SuggestedFirstStepStatus } from '@shared/models/onboarding-types'; + +import { SuggestedFirstStep } from './suggested-first-step'; +import { onboardingActions } from '@app/store/onboarding/onboarding.actions'; + +const exploreAppsExternalRoute = 'https://www.stacks.co/explore/discover-apps#apps'; + +export function ExploreAppsStep() { + const analytics = useAnalytics(); + const dispatch = useAppDispatch(); + const suggestedFirstStepsStatus = useSuggestedFirstStepsStatus(); + + const onSelectStep = useCallback(() => { + void analytics.track('select_next_step', { step: SuggestedFirstSteps.ExploreApps }); + dispatch( + onboardingActions.updateSuggestedFirstStepsStatus({ + ...suggestedFirstStepsStatus, + [SuggestedFirstSteps.ExploreApps]: SuggestedFirstStepStatus.Done, + }) + ); + openInNewTab(exploreAppsExternalRoute); + }, [analytics, dispatch, suggestedFirstStepsStatus]); + + return ( + + ); +} diff --git a/src/app/pages/home/components/step-done-badge.tsx b/src/app/features/suggested-first-steps/components/step-done-badge.tsx similarity index 55% rename from src/app/pages/home/components/step-done-badge.tsx rename to src/app/features/suggested-first-steps/components/step-done-badge.tsx index affe386eeac..4dd10b1757e 100644 --- a/src/app/pages/home/components/step-done-badge.tsx +++ b/src/app/features/suggested-first-steps/components/step-done-badge.tsx @@ -1,32 +1,30 @@ import { FiCheck } from 'react-icons/fi'; -import { Box, color, Stack } from '@stacks/ui'; +import { color, Stack } from '@stacks/ui'; import { Caption } from '@app/components/typography'; import { OnboardingSelectors } from '@tests/integration/onboarding/onboarding.selectors'; export function StepDoneBadge() { return ( - - - - - Done - - - + + + Done + + ); } diff --git a/src/app/features/suggested-first-steps/components/step-full-page.tsx b/src/app/features/suggested-first-steps/components/step-full-page.tsx new file mode 100644 index 00000000000..d094729f10a --- /dev/null +++ b/src/app/features/suggested-first-steps/components/step-full-page.tsx @@ -0,0 +1,81 @@ +import { FiArrowRight } from 'react-icons/fi'; +import { Box, color, Flex, Stack } from '@stacks/ui'; + +import { Tooltip } from '@app/components/tooltip'; +import { Body, Text, Title } from '@app/components/typography'; +import { Link } from '@app/components/link'; +import { OnboardingSelectors } from '@tests/integration/onboarding/onboarding.selectors'; + +import { StepDoneBadge } from './step-done-badge'; +import { externalLinkInfo, StepIllustration } from './suggested-first-step'; + +interface StepFullPageProps { + action: string; + body: string; + imageFull: string; + imageFullDone: string; + isDone: boolean; + isExternalRoute?: boolean; + onClick(): void; + title: string; +} +export function StepFullPage(props: StepFullPageProps) { + const { action, body, imageFull, imageFullDone, isDone, isExternalRoute, onClick, title } = props; + + return ( + + + + + + + {title} + + + {body} + + + {isDone ? ( + + ) : ( + + + + + + {action} + + {isExternalRoute ? : null} + + + + + )} + + ); +} diff --git a/src/app/features/suggested-first-steps/components/step-popup.tsx b/src/app/features/suggested-first-steps/components/step-popup.tsx new file mode 100644 index 00000000000..09d171b4cde --- /dev/null +++ b/src/app/features/suggested-first-steps/components/step-popup.tsx @@ -0,0 +1,60 @@ +import { FiArrowRight } from 'react-icons/fi'; +import { Box, color, Flex, Stack } from '@stacks/ui'; + +import { Tooltip } from '@app/components/tooltip'; +import { Body, Title } from '@app/components/typography'; + +import { externalLinkInfo, StepIllustration } from './suggested-first-step'; + +interface StepPopupProps { + body: string; + imagePopup: string; + imagePopupDone: string; + isDone: boolean; + isExternalRoute?: boolean; + onClick(): void; + title: string; +} +export function StepPopup(props: StepPopupProps) { + const { body, imagePopup, imagePopupDone, isDone, isExternalRoute, onClick, title } = props; + + return ( + + + + + + + + {title} + + {isExternalRoute ? : null} + + {body} + + + + + ); +} diff --git a/src/app/features/suggested-first-steps/components/suggested-first-step.tsx b/src/app/features/suggested-first-steps/components/suggested-first-step.tsx new file mode 100644 index 00000000000..32906950032 --- /dev/null +++ b/src/app/features/suggested-first-steps/components/suggested-first-step.tsx @@ -0,0 +1,32 @@ +import { useMediaQuery } from '@stacks/ui'; + +import { DESKTOP_VIEWPORT_MIN_WIDTH } from '@app/components/global-styles/full-page-styles'; + +import { StepFullPage } from './step-full-page'; +import { StepPopup } from './step-popup'; + +export const externalLinkInfo = + 'This link will take you to an external third-party website that is not affiliated with Hiro Systems PBC.'; + +interface StepIllustrationProps { + image: string; +} +export const StepIllustration = ({ image }: StepIllustrationProps) => ; + +interface SuggestedFirstStepProps { + action: string; + body: string; + imageFull: string; + imageFullDone: string; + imagePopup: string; + imagePopupDone: string; + isDone: boolean; + isExternalRoute?: boolean; + onClick(): void; + title: string; +} +export function SuggestedFirstStep(props: SuggestedFirstStepProps) { + const [desktopViewport] = useMediaQuery(`(min-width: ${DESKTOP_VIEWPORT_MIN_WIDTH})`); + + return desktopViewport ? : ; +} diff --git a/src/app/pages/home/hooks/use-onboarding-steps.ts b/src/app/features/suggested-first-steps/hooks/use-suggested-first-steps.ts similarity index 52% rename from src/app/pages/home/hooks/use-onboarding-steps.ts rename to src/app/features/suggested-first-steps/hooks/use-suggested-first-steps.ts index 25636e7114b..b105c7c4b4c 100644 --- a/src/app/pages/home/hooks/use-onboarding-steps.ts +++ b/src/app/features/suggested-first-steps/hooks/use-suggested-first-steps.ts @@ -5,15 +5,18 @@ import { useCurrentAccountAvailableStxBalance, } from '@app/store/accounts/account.hooks'; import { useAppDispatch } from '@app/store'; -import { useHideSteps, useStepsStatus } from '@app/store/onboarding/onboarding.selectors'; +import { + useHideSuggestedFirstSteps, + useSuggestedFirstStepsStatus, +} from '@app/store/onboarding/onboarding.selectors'; import { onboardingActions } from '@app/store/onboarding/onboarding.actions'; import { useCurrentAccountUnanchoredBalances } from '@app/query/balance/balance.hooks'; -import { OnboardingSteps, OnboardingStepStatus } from '@shared/models/onboarding-types'; +import { SuggestedFirstSteps, SuggestedFirstStepStatus } from '@shared/models/onboarding-types'; -export function useOnboardingSteps() { +export function useSuggestedFirstSteps() { const dispatch = useAppDispatch(); - const hasHiddenSteps = useHideSteps(); - const onboardingStepsStatus = useStepsStatus(); + const hideSuggestedFirstSteps = useHideSuggestedFirstSteps(); + const suggestedFirstStepsStatus = useSuggestedFirstStepsStatus(); const availableStxBalance = useCurrentAccountAvailableStxBalance(); const { data: balances } = useCurrentAccountUnanchoredBalances(); const accounts = useAccounts(); @@ -21,18 +24,18 @@ export function useOnboardingSteps() { useEffect(() => { if (availableStxBalance?.isGreaterThan(0)) { dispatch( - onboardingActions.updateStepsStatus({ - ...onboardingStepsStatus, - [OnboardingSteps.AddFunds]: OnboardingStepStatus.Done, + onboardingActions.updateSuggestedFirstStepsStatus({ + ...suggestedFirstStepsStatus, + [SuggestedFirstSteps.AddFunds]: SuggestedFirstStepStatus.Done, }) ); } if (balances && Object.keys(balances?.non_fungible_tokens).length > 0) { dispatch( - onboardingActions.updateStepsStatus({ - ...onboardingStepsStatus, - [OnboardingSteps.BuyNft]: OnboardingStepStatus.Done, + onboardingActions.updateSuggestedFirstStepsStatus({ + ...suggestedFirstStepsStatus, + [SuggestedFirstSteps.BuyNft]: SuggestedFirstStepStatus.Done, }) ); } @@ -40,13 +43,15 @@ export function useOnboardingSteps() { }, [availableStxBalance, balances?.non_fungible_tokens]); const hasCompletedOnboardingSteps = useMemo(() => { - return Object.values(onboardingStepsStatus).every(val => val === OnboardingStepStatus.Done); - }, [onboardingStepsStatus]); + return Object.values(suggestedFirstStepsStatus).every( + val => val === SuggestedFirstStepStatus.Done + ); + }, [suggestedFirstStepsStatus]); - const showOnboardingSteps = - accounts?.length === 1 && !hasCompletedOnboardingSteps && !hasHiddenSteps; + const showSuggestedFirstSteps = + accounts?.length === 1 && !hasCompletedOnboardingSteps && !hideSuggestedFirstSteps; return { - showOnboardingSteps, + showSuggestedFirstSteps, }; } diff --git a/src/app/features/suggested-first-steps/suggested-first-steps.layout.tsx b/src/app/features/suggested-first-steps/suggested-first-steps.layout.tsx new file mode 100644 index 00000000000..3a215399ecb --- /dev/null +++ b/src/app/features/suggested-first-steps/suggested-first-steps.layout.tsx @@ -0,0 +1,62 @@ +import { FiX } from 'react-icons/fi'; +import { Circle, color, Flex, Grid, GridProps, Stack } from '@stacks/ui'; + +import { HOME_FULL_PAGE_MAX_WIDTH } from '@app/components/global-styles/full-page-styles'; +import { SpaceBetween } from '@app/components/space-between'; +import { Text, Title } from '@app/components/typography'; +import { OnboardingSelectors } from '@tests/integration/onboarding/onboarding.selectors'; + +interface SuggestedFirstStepsLayoutProps extends GridProps { + onDismissSteps(): void; +} +export function SuggestedFirstStepsLayout({ + children, + onDismissSteps, +}: SuggestedFirstStepsLayoutProps) { + return ( + <> + + + Welcome to Stacks 👋 + Next steps for you + + + + + + + + {children} + + + + ); +} diff --git a/src/app/features/suggested-first-steps/suggested-first-steps.tsx b/src/app/features/suggested-first-steps/suggested-first-steps.tsx new file mode 100644 index 00000000000..0cb81b87147 --- /dev/null +++ b/src/app/features/suggested-first-steps/suggested-first-steps.tsx @@ -0,0 +1,33 @@ +import { useCallback } from 'react'; + +import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { useAppDispatch } from '@app/store'; +import { onboardingActions } from '@app/store/onboarding/onboarding.actions'; + +import { useSuggestedFirstSteps } from './hooks/use-suggested-first-steps'; + +import { SuggestedFirstStepsLayout } from './suggested-first-steps.layout'; +import { AddFundsStep } from './components/add-funds-step'; +import { BackUpSecretKeyStep } from './components/back-up-secret-key-step'; +import { ExploreAppsStep } from './components/explore-apps-step'; +import { BuyNftStep } from './components/buy-nft-step'; + +export function SuggestedFirstSteps() { + const analytics = useAnalytics(); + const dispatch = useAppDispatch(); + useSuggestedFirstSteps(); + + const onDismissSteps = useCallback(() => { + void analytics.track('dismiss_suggested_first_steps'); + dispatch(onboardingActions.hideSuggestedFirstSteps(true)); + }, [analytics, dispatch]); + + return ( + + + + + + + ); +} diff --git a/src/app/pages/fund/components/fast-checkout-badge.tsx b/src/app/pages/fund/components/fast-checkout-badge.tsx index 7f1a3e75451..ed2a1476c2c 100644 --- a/src/app/pages/fund/components/fast-checkout-badge.tsx +++ b/src/app/pages/fund/components/fast-checkout-badge.tsx @@ -1,31 +1,27 @@ import { FiZap } from 'react-icons/fi'; -import { Box, color, Stack } from '@stacks/ui'; +import { color, Stack } from '@stacks/ui'; import { Caption } from '@app/components/typography'; export function FastCheckoutBadge() { return ( - - - - - Fast checkout - - - + + + Fast checkout + + ); } diff --git a/src/app/pages/fund/components/skip-fund-account-button.tsx b/src/app/pages/fund/components/skip-fund-account-button.tsx index 00049e93571..c367a03cc6d 100644 --- a/src/app/pages/fund/components/skip-fund-account-button.tsx +++ b/src/app/pages/fund/components/skip-fund-account-button.tsx @@ -8,9 +8,7 @@ export function SkipFundAccountButton({ onSkipFundAccount }: SkipFundAccountButt return (