diff --git a/.changeset/fresh-ears-buy.md b/.changeset/fresh-ears-buy.md new file mode 100644 index 00000000000..ded0551ae9f --- /dev/null +++ b/.changeset/fresh-ears-buy.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +User onboardng steps are now checking when user does required actions diff --git a/.env.template b/.env.template index a4b75334b9c..f7a6ed1e703 100644 --- a/.env.template +++ b/.env.template @@ -9,4 +9,6 @@ MAILPITURL=xxxx #For playwright E2E_USER_NAME=xxxx E2E_USER_PASSWORD=xxxx E2E_PERMISSIONS_USERS_PASSWORD=xxxx -BASE_URL=http://localhost:9000/ \ No newline at end of file +BASE_URL=http://localhost:9000/ + +ONBOARDING_USER_JOINED_DATE_THRESHOLD=2024-07-01 \ No newline at end of file diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 2da5bbae4c0..fb4500c3a3d 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -3486,6 +3486,9 @@ "context": "tooltip", "string": "This feature is in a preview state and can be subject to changes at later point" }, + "KHI/qv": { + "string": "You don't have permission to manage products" + }, "KHZlmi": { "string": "Discount Type" }, @@ -4510,10 +4513,6 @@ "context": "info text", "string": "Set up an end date of preorder. When end date will be reached product will be automatically taken from preorder to standard selling" }, - "RFXT9O": { - "context": "tooltip message", - "string": "You don't have permission to invite staff members" - }, "RH+aOF": { "context": "use attribute in filtering", "string": "Use in Filtering" @@ -6913,6 +6912,10 @@ "grkY2V": { "string": "You don't have access to any channels" }, + "gt05TH": { + "context": "tooltip message", + "string": "You don't have permission to manage staff" + }, "gvOzOl": { "string": "Page Title" }, @@ -9425,6 +9428,9 @@ "context": "dialog title", "string": "Delete category" }, + "xol6jX": { + "string": "You don't have permission to manage orders" + }, "xoyCZ/": { "context": "error message", "string": "Improper value" diff --git a/src/components/DevModePanel/DevModePanel.tsx b/src/components/DevModePanel/DevModePanel.tsx index f7ca6627d62..92d20c92455 100644 --- a/src/components/DevModePanel/DevModePanel.tsx +++ b/src/components/DevModePanel/DevModePanel.tsx @@ -1,7 +1,9 @@ // @ts-strict-ignore import { useDashboardTheme } from "@dashboard/components/GraphiQL/styles"; import { DashboardModal } from "@dashboard/components/Modal"; -import { createGraphiQLFetcher } from "@graphiql/toolkit"; +import { useFlag } from "@dashboard/featureFlags"; +import { useOnboarding } from "@dashboard/newHome/homeOnboarding/onboardingContext"; +import { createGraphiQLFetcher, FetcherOpts, FetcherParams } from "@graphiql/toolkit"; import { createFetch } from "@saleor/sdk"; import React from "react"; import { useIntl } from "react-intl"; @@ -15,11 +17,22 @@ const authorizedFetch = createFetch(); export const DevModePanel: React.FC = () => { const intl = useIntl(); const { rootStyle } = useDashboardTheme(); + const { markOnboardingStepAsCompleted } = useOnboarding(); + const newHomePageFlag = useFlag("new_home_page"); const { isDevModeVisible, variables, devModeContent, setDevModeVisibility } = useDevModeContext(); - const fetcher = createGraphiQLFetcher({ + const baseFetcher = createGraphiQLFetcher({ url: process.env.API_URL, fetch: authorizedFetch, }); + const fetcher = async (graphQLParams: FetcherParams, opts: FetcherOpts) => { + if (graphQLParams.operationName !== "IntrospectionQuery" && newHomePageFlag.enabled) { + markOnboardingStepAsCompleted("graphql-playground"); + } + + const result = await baseFetcher(graphQLParams, opts); // Call the base fetcher + + return result; + }; const overwriteCodeMirrorCSSVariables = { __html: ` .graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, reach-portal{ diff --git a/src/custom-apps/views/CustomAppList.tsx b/src/custom-apps/views/CustomAppList.tsx index dca409a1487..021b6eb56cc 100644 --- a/src/custom-apps/views/CustomAppList.tsx +++ b/src/custom-apps/views/CustomAppList.tsx @@ -3,6 +3,7 @@ import { useApolloClient } from "@apollo/client"; import AppDeleteDialog from "@dashboard/apps/components/AppDeleteDialog"; import { EXTENSION_LIST_QUERY } from "@dashboard/apps/queries"; import { WindowTitle } from "@dashboard/components/WindowTitle"; +import { useFlag } from "@dashboard/featureFlags"; import { AppSortField, AppTypeEnum, @@ -14,9 +15,10 @@ import useNavigator from "@dashboard/hooks/useNavigator"; import useNotifier from "@dashboard/hooks/useNotifier"; import { sectionNames } from "@dashboard/intl"; import { findById } from "@dashboard/misc"; +import { useOnboarding } from "@dashboard/newHome/homeOnboarding/onboardingContext"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import { mapEdgesToItems } from "@dashboard/utils/maps"; -import React from "react"; +import React, { useEffect } from "react"; import { useIntl } from "react-intl"; import CustomAppListPage from "../components/CustomAppListPage"; @@ -32,6 +34,15 @@ export const CustomAppList: React.FC = ({ params }) => { const notify = useNotifier(); const intl = useIntl(); const client = useApolloClient(); + const { markOnboardingStepAsCompleted } = useOnboarding(); + const newHomePageFlag = useFlag("new_home_page"); + + useEffect(() => { + if (newHomePageFlag) { + markOnboardingStepAsCompleted("view-webhooks"); + } + }, []); + const [openModal, closeModal] = createDialogActionHandlers< CustomAppListUrlDialog, CustomAppListUrlQueryParams diff --git a/src/fragments/auth.ts b/src/fragments/auth.ts index 8d1804c362c..4f1f2145d74 100644 --- a/src/fragments/auth.ts +++ b/src/fragments/auth.ts @@ -23,6 +23,7 @@ export const fragmentUser = gql` firstName lastName isStaff + dateJoined metadata { key value diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 0001a6efb82..6f34781fb11 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -212,6 +212,7 @@ export const UserFragmentDoc = gql` firstName lastName isStaff + dateJoined metadata { key value diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 005cd85bef6..2e61ff84f34 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -9172,7 +9172,7 @@ export type AvailableExternalAuthenticationsQuery = { __typename: 'Query', shop: export type UserDetailsQueryVariables = Exact<{ [key: string]: never; }>; -export type UserDetailsQuery = { __typename: 'Query', me: { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, restrictedAccessToChannels: boolean, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null } | null }; +export type UserDetailsQuery = { __typename: 'Query', me: { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, dateJoined: any, restrictedAccessToChannels: boolean, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null } | null }; export type CategoryDeleteMutationVariables = Exact<{ id: Scalars['ID']; @@ -9996,7 +9996,7 @@ export type UserPermissionFragment = { __typename: 'UserPermission', code: Permi export type UserUserPermissionWithSourcePermissionGroupsFragment = { __typename: 'UserPermission', code: PermissionEnum, name: string, sourcePermissionGroups: Array<{ __typename: 'Group', id: string }> | null }; -export type UserFragment = { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, restrictedAccessToChannels: boolean, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null }; +export type UserFragment = { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, dateJoined: any, restrictedAccessToChannels: boolean, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null }; export type UserBaseFragment = { __typename: 'User', id: string, firstName: string, lastName: string }; diff --git a/src/newHome/homeOnboarding/HomeCheckGraphQLButton.tsx b/src/newHome/homeOnboarding/HomeCheckGraphQLButton.tsx index d68c9ca1c43..c7f104f9611 100644 --- a/src/newHome/homeOnboarding/HomeCheckGraphQLButton.tsx +++ b/src/newHome/homeOnboarding/HomeCheckGraphQLButton.tsx @@ -1,42 +1,10 @@ import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks"; -import { Button, Tooltip } from "@saleor/macaw-ui-next"; +import { Button } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage } from "react-intl"; -import { HomeFakeDisabledButton } from "./HomeFakeDisabledButton"; - export const HomeCheckGraphQLButton = () => { const context = useDevModeContext(); - const getTooltipContent = () => { - return { - reason: "", - message: "", - }; - }; - - const canViewGraphQLPlayground = true; - - if (!canViewGraphQLPlayground) { - const { message } = getTooltipContent(); - - return ( - - - - - - - - - {message} - - - ); - } return ( + + + ); }; diff --git a/src/newHome/homeOnboarding/HomeInviteStaffButton.tsx b/src/newHome/homeOnboarding/HomeInviteStaffButton.tsx index 5257eac4de8..11091822eba 100644 --- a/src/newHome/homeOnboarding/HomeInviteStaffButton.tsx +++ b/src/newHome/homeOnboarding/HomeInviteStaffButton.tsx @@ -1,4 +1,7 @@ -import { staffListPath } from "@dashboard/staff/urls"; +import { useUser } from "@dashboard/auth"; +import { hasPermissions } from "@dashboard/components/RequirePermissions"; +import { PermissionEnum } from "@dashboard/graphql"; +import { staffListUrl } from "@dashboard/staff/urls"; import { Button, Tooltip } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage } from "react-intl"; @@ -7,9 +10,11 @@ import { Link } from "react-router-dom"; import { HomeFakeDisabledButton } from "./HomeFakeDisabledButton"; export const HomeInviteStaffButton = () => { - const canInviteStaffMembers = true; + const { user } = useUser(); + const userPermissions = user?.userPermissions || []; + const hasPermissionToManageStaff = hasPermissions(userPermissions, [PermissionEnum.MANAGE_STAFF]); - if (!canInviteStaffMembers) { + if (!hasPermissionToManageStaff) { return ( @@ -20,8 +25,8 @@ export const HomeInviteStaffButton = () => { @@ -29,10 +34,8 @@ export const HomeInviteStaffButton = () => { ); } - const inviteTeamMembersPath = staffListPath; - return ( - + diff --git a/src/newHome/homeOnboarding/HomeOnboarding.tsx b/src/newHome/homeOnboarding/HomeOnboarding.tsx index d04c51e6d25..237abaacde8 100644 --- a/src/newHome/homeOnboarding/HomeOnboarding.tsx +++ b/src/newHome/homeOnboarding/HomeOnboarding.tsx @@ -1,4 +1,5 @@ import { DashboardCard } from "@dashboard/components/Card"; +import { TOTAL_STEPS_COUNT } from "@dashboard/newHome/homeOnboarding/onboardingContext/initialOnboardingState"; import { Accordion, Box, Button, ChervonDownIcon, Text } from "@saleor/macaw-ui-next"; import * as React from "react"; import { FormattedMessage } from "react-intl"; @@ -20,13 +21,12 @@ const HomeOnboarding = () => { const isOnboardingExpanded = onboardingState.onboardingExpanded; const status = { - done: onboardingState.steps.filter(step => step.completed).length, - total: onboardingState.steps.length, + done: onboardingState.stepsCompleted.length, + total: TOTAL_STEPS_COUNT, }; const handleMarkAllAsCompleted = () => { markAllAsCompleted(); - toggleOnboarding(false); }; return ( diff --git a/src/newHome/homeOnboarding/HomeOnboardingAccordion.tsx b/src/newHome/homeOnboarding/HomeOnboardingAccordion.tsx index 71498d6459e..b6427872d2e 100644 --- a/src/newHome/homeOnboarding/HomeOnboardingAccordion.tsx +++ b/src/newHome/homeOnboarding/HomeOnboardingAccordion.tsx @@ -4,8 +4,8 @@ import { Accordion, Box, ChervonDownIcon, Skeleton, Text } from "@saleor/macaw-u import React from "react"; import SVG from "react-inlinesvg"; +import { useOnboardingData } from "./hooks/useOnboardingData"; import { useOnboarding } from "./onboardingContext/OnboardingContext"; -import { useOnboardingData } from "./useOnboardingData"; export const HomeOnboardingAccordion = () => { const { toggleExpandedOnboardingStep, extendedStepId, loading } = useOnboarding(); diff --git a/src/newHome/homeOnboarding/HomeOrdersButton.tsx b/src/newHome/homeOnboarding/HomeOrdersButton.tsx index a747fe6c1e4..3b5699587ae 100644 --- a/src/newHome/homeOnboarding/HomeOrdersButton.tsx +++ b/src/newHome/homeOnboarding/HomeOrdersButton.tsx @@ -1,23 +1,22 @@ +import { useUser } from "@dashboard/auth"; +import { hasPermissions } from "@dashboard/components/RequirePermissions"; +import { PermissionEnum } from "@dashboard/graphql"; import { orderListUrl } from "@dashboard/orders/urls"; import { Button, Tooltip } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage } from "react-intl"; +import { Link } from "react-router-dom"; import { HomeFakeDisabledButton } from "./HomeFakeDisabledButton"; export const HomeOrdersButton = () => { - const getTooltipContent = () => { - return { - reason: "", - message: "", - }; - }; - - const canViewOrders = true; - - if (!canViewOrders) { - const { message } = getTooltipContent(); + const { user } = useUser(); + const userPermissions = user?.userPermissions || []; + const hasPermissionToManageOrders = hasPermissions(userPermissions, [ + PermissionEnum.MANAGE_ORDERS, + ]); + if (!hasPermissionToManageOrders) { return ( @@ -27,17 +26,20 @@ export const HomeOrdersButton = () => { - {message} + ); } - const goToOrdersUrl = orderListUrl(); - return ( - + + + ); }; diff --git a/src/newHome/homeOnboarding/HomeWebhooksButton.tsx b/src/newHome/homeOnboarding/HomeWebhooksButton.tsx index 7a6fa3faf0f..ebd97a5035f 100644 --- a/src/newHome/homeOnboarding/HomeWebhooksButton.tsx +++ b/src/newHome/homeOnboarding/HomeWebhooksButton.tsx @@ -1,49 +1,15 @@ import { CustomAppSections } from "@dashboard/custom-apps/urls"; -import { Button, Tooltip } from "@saleor/macaw-ui-next"; +import { Button } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage } from "react-intl"; - -import { HomeFakeDisabledButton } from "./HomeFakeDisabledButton"; +import { Link } from "react-router-dom"; export const HomeWebhooksButton = () => { - const getTooltipContent = () => { - return { - reason: "", - message: "", - }; - }; - - const canAccessWebhooks = true; - - if (!canAccessWebhooks) { - const { message } = getTooltipContent(); - - return ( - - - - - - - - - {message} - - - ); - } - - const goToWebhooksUrl = CustomAppSections.appsSection; - return ( - + + + ); }; diff --git a/src/newHome/homeOnboarding/hooks/useNewUserCheck.test.ts b/src/newHome/homeOnboarding/hooks/useNewUserCheck.test.ts new file mode 100644 index 00000000000..2a5c724e6ac --- /dev/null +++ b/src/newHome/homeOnboarding/hooks/useNewUserCheck.test.ts @@ -0,0 +1,95 @@ +import { useUser } from "@dashboard/auth"; +import { renderHook } from "@testing-library/react-hooks"; + +import { useNewUserCheck } from "./useNewUserCheck"; + +jest.mock("@dashboard/auth"); + +describe("useNewUserCheck", () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); // Clear the module cache to ensure no env conflicts. + process.env = { ...originalEnv }; // Reset the process.env before each test. + }); + + afterAll(() => { + process.env = originalEnv; // Restore original process.env. + }); + + it("should return isNewUser as false if user is not defined", () => { + // Arrange + (useUser as jest.Mock).mockReturnValue({ user: null }); + + process.env.ONBOARDING_USER_JOINED_DATE_THRESHOLD = "2023-01-01"; + + // Act + const { result } = renderHook(() => useNewUserCheck()); + + // Assert + expect(result.current).toEqual({ isNewUser: false, isUserLoading: true }); + }); + + it("should return isNewUser as false if ONBOARDING_USER_JOINED_DATE_THRESHOLD is not set", () => { + // Arrange + (useUser as jest.Mock).mockReturnValue({ user: { dateJoined: "2023-02-01" } }); + + delete process.env.ONBOARDING_USER_JOINED_DATE_THRESHOLD; + + // Act + const { result } = renderHook(() => useNewUserCheck()); + + // Assert + expect(result.current).toEqual({ isNewUser: false, isUserLoading: false }); + }); + + it("should return isNewUser as true if user joined after the threshold date", () => { + // Arrange + (useUser as jest.Mock).mockReturnValue({ user: { dateJoined: "2023-02-01" } }); + + process.env.ONBOARDING_USER_JOINED_DATE_THRESHOLD = "2023-01-01"; + + // Act + const { result } = renderHook(() => useNewUserCheck()); + + // Assert + expect(result.current).toEqual({ isNewUser: true, isUserLoading: false }); + }); + + it("should return isNewUser as false if user joined before the threshold date", () => { + // Arrange + (useUser as jest.Mock).mockReturnValue({ user: { dateJoined: "2022-12-31" } }); + + process.env.ONBOARDING_USER_JOINED_DATE_THRESHOLD = "2023-01-01"; + + // Act + const { result } = renderHook(() => useNewUserCheck()); + + // Assert + expect(result.current).toEqual({ isNewUser: false, isUserLoading: false }); + }); + + it("should return isNewUser as false if threshold date is invalid", () => { + // Arrange + (useUser as jest.Mock).mockReturnValue({ user: { dateJoined: "2023-01-01" } }); + + process.env.ONBOARDING_USER_JOINED_DATE_THRESHOLD = "123456789"; + + // Act + const { result } = renderHook(() => useNewUserCheck()); + + // Assert + expect(result.current).toEqual({ isNewUser: false, isUserLoading: false }); + }); + + it("should return isNewUser as false and isUserLoading true when uer is loading", () => { + // Arrange + (useUser as jest.Mock).mockReturnValue({ user: null, isUserLoading: true }); + + // Act + const { result } = renderHook(() => useNewUserCheck()); + + // Assert + expect(result.current).toEqual({ isNewUser: false, isUserLoading: true }); + }); +}); diff --git a/src/newHome/homeOnboarding/hooks/useNewUserCheck.ts b/src/newHome/homeOnboarding/hooks/useNewUserCheck.ts new file mode 100644 index 00000000000..f4c630d9f44 --- /dev/null +++ b/src/newHome/homeOnboarding/hooks/useNewUserCheck.ts @@ -0,0 +1,36 @@ +import { useUser } from "@dashboard/auth"; +import moment from "moment"; + +export const useNewUserCheck = () => { + const { user } = useUser(); + const thresholdDateString = process.env.ONBOARDING_USER_JOINED_DATE_THRESHOLD || "2024-01-01"; + + if (!user) { + return { + isNewUser: false, + isUserLoading: true, + }; + } + + if (!thresholdDateString) { + return { + isNewUser: false, + isUserLoading: false, + }; + } + + const userJoinedDate = moment(user.dateJoined); + const thresholdDate = moment(thresholdDateString); + + if (!userJoinedDate.isValid() || !thresholdDate.isValid()) { + return { + isNewUser: false, + isUserLoading: false, + }; + } + + return { + isNewUser: userJoinedDate.isAfter(thresholdDate), + isUserLoading: false, + }; +}; diff --git a/src/newHome/homeOnboarding/useOnboardingData.tsx b/src/newHome/homeOnboarding/hooks/useOnboardingData.tsx similarity index 91% rename from src/newHome/homeOnboarding/useOnboardingData.tsx rename to src/newHome/homeOnboarding/hooks/useOnboardingData.tsx index 4460125b7a9..02ccc651e54 100644 --- a/src/newHome/homeOnboarding/useOnboardingData.tsx +++ b/src/newHome/homeOnboarding/hooks/useOnboardingData.tsx @@ -2,13 +2,13 @@ import { Button } from "@saleor/macaw-ui-next"; import React, { ReactNode } from "react"; import { FormattedMessage, IntlShape, useIntl } from "react-intl"; -import { HomeCheckGraphQLButton } from "./HomeCheckGraphQLButton"; -import { HomeCreateProductButton } from "./HomeCreateProductButton"; -import { HomeInviteStaffButton } from "./HomeInviteStaffButton"; -import { HomeOrdersButton } from "./HomeOrdersButton"; -import { HomeWebhooksButton } from "./HomeWebhooksButton"; -import { useOnboarding } from "./onboardingContext"; -import { OnboardingStepsIDs } from "./onboardingContext/types"; +import { HomeCheckGraphQLButton } from "../HomeCheckGraphQLButton"; +import { HomeCreateProductButton } from "../HomeCreateProductButton"; +import { HomeInviteStaffButton } from "../HomeInviteStaffButton"; +import { HomeOrdersButton } from "../HomeOrdersButton"; +import { HomeWebhooksButton } from "../HomeWebhooksButton"; +import { useOnboarding } from "../onboardingContext"; +import { OnboardingStepsIDs } from "../onboardingContext/types"; interface OnboardingStepData { id: OnboardingStepsIDs; @@ -180,8 +180,7 @@ export const useOnboardingData = () => { const steps = getStepsData({ intl, - isStepCompleted: (step: OnboardingStepsIDs) => - onboardingState.steps.find(s => s.id === step)?.completed ?? false, + isStepCompleted: (step: OnboardingStepsIDs) => onboardingState.stepsCompleted.includes(step), onStepComplete: (step: OnboardingStepsIDs) => { markOnboardingStepAsCompleted(step); }, diff --git a/src/newHome/homeOnboarding/onboardingContext/OnboardingContext.tsx b/src/newHome/homeOnboarding/onboardingContext/OnboardingContext.tsx index df6186549ef..ce15815a6c9 100644 --- a/src/newHome/homeOnboarding/onboardingContext/OnboardingContext.tsx +++ b/src/newHome/homeOnboarding/onboardingContext/OnboardingContext.tsx @@ -1,5 +1,15 @@ +import { + handleStateChangeAfterStepCompleted, + handleStateChangeAfterToggle, +} from "@dashboard/newHome/homeOnboarding/onboardingContext/utils"; import React from "react"; +import { useNewUserCheck } from "../hooks/useNewUserCheck"; +import { + getInitialOnboardingState, + initialOnboardingSteps, + TOTAL_STEPS_COUNT, +} from "./initialOnboardingState"; import { OnboardingContextType, OnboardingProviderProps, @@ -10,118 +20,54 @@ import { useExpandedOnboardingId } from "./useExpandedOnboardingId"; const OnboardingContext = React.createContext(null); -const initialOnboardingState: OnboardingState = { - steps: [ - { - id: "get-started", - completed: false, - expanded: true, - }, - { - id: "create-product", - completed: false, - expanded: false, - }, - { - id: "explore-orders", - completed: false, - expanded: false, - }, - { - id: "graphql-playground", - completed: false, - expanded: false, - }, - { - id: "view-webhooks", - completed: false, - expanded: false, - }, - { - id: "invite-staff", - completed: false, - expanded: false, - }, - ], - onboardingExpanded: true, -}; - export const OnboardingProvider = ({ children, storageService }: OnboardingProviderProps) => { - const [onboardingState, setOnboardingState] = - React.useState(initialOnboardingState); + const [onboardingState, setOnboardingState] = React.useState({ + onboardingExpanded: true, + stepsCompleted: [], + stepsExpanded: {} as OnboardingState["stepsExpanded"], + }); const [loaded, setLoaded] = React.useState(false); + const { isNewUser, isUserLoading } = useNewUserCheck(); React.useEffect(() => { - if (loaded) return; + if (loaded || isUserLoading) return; const onboardingStateLS = storageService.getOnboardingState(); // When first time load there is not data in local storage, so use initial state if (!onboardingStateLS) { - setOnboardingState(initialOnboardingState); - setLoaded(true); - - return; + setOnboardingState(getInitialOnboardingState(isNewUser)); + } else { + setOnboardingState(onboardingStateLS); } - if (!onboardingStateLS) { - return; - } - - setOnboardingState(onboardingStateLS); setLoaded(true); - }, [loaded, storageService]); + }, [isNewUser, isUserLoading, loaded, storageService]); React.useEffect(() => { - storageService.saveOnboardingState(onboardingState); - }, [onboardingState, storageService]); + if (loaded) { + storageService.saveOnboardingState(onboardingState); + } + }, [loaded, onboardingState, storageService]); - const isOnboardingCompleted = onboardingState.steps.every(step => step.completed); + // For old users, onboarding is always completed, for new one we need to calculate it + const isOnboardingCompleted = isNewUser + ? onboardingState.stepsCompleted.length === TOTAL_STEPS_COUNT + : true; const extendedStepId = useExpandedOnboardingId(onboardingState, loaded); const markOnboardingStepAsCompleted = (id: OnboardingStepsIDs) => { - setOnboardingState(({ steps, ...rest }) => { - const findIndex = steps.findIndex(step => step.id === id); - const findNextToExpand = steps.find((step, index) => index > findIndex && !step.completed); - - const newSteps = steps.map(step => { - const isNextToExpand = findNextToExpand?.id === step.id; - - if (isNextToExpand) { - return { - ...step, - expanded: true, - }; - } - - // Always mark get-started as completed when complete other steps - if (step.id === "get-started") { - return { - ...step, - completed: true, - expanded: false, - }; - } - - return { - ...step, - completed: step.id === id ? true : step.completed, - expanded: step.id === id ? false : step.expanded, - }; - }); - - return { - ...rest, - steps: newSteps, - }; - }); + setOnboardingState(prevOnboardingState => + handleStateChangeAfterStepCompleted(prevOnboardingState, id), + ); }; const markAllAsCompleted = () => { - setOnboardingState(prev => ({ - ...prev, - steps: prev.steps.map(step => ({ ...step, completed: true, expanded: false })), + setOnboardingState(prevOnboardingState => ({ + ...prevOnboardingState, + stepsCompleted: initialOnboardingSteps.map(step => step.id), + stepsExpanded: {} as OnboardingState["stepsExpanded"], })); }; @@ -130,29 +76,22 @@ export const OnboardingProvider = ({ children, storageService }: OnboardingProvi // In case that step was collapse we get empty string as id const expandedId = id || currentExpandedId; - setOnboardingState(prev => ({ - ...prev, - steps: prev.steps.map(step => { - if (step.id === expandedId) { - return { - ...step, - expanded: !step.expanded, - }; - } - - return { - ...step, - expanded: false, - }; - }), - })); + setOnboardingState(prev => + handleStateChangeAfterToggle(prev, expandedId as OnboardingStepsIDs, id), + ); }; const toggleOnboarding = (value: boolean) => { - setOnboardingState(prev => ({ - ...prev, - onboardingExpanded: value, - })); + setOnboardingState(prev => { + const newState = { + ...prev, + onboardingExpanded: value, + }; + + storageService.saveOnboardingState(newState); + + return newState; + }); }; return ( diff --git a/src/newHome/homeOnboarding/onboardingContext/initialOnboardingState.ts b/src/newHome/homeOnboarding/onboardingContext/initialOnboardingState.ts new file mode 100644 index 00000000000..90e1298c860 --- /dev/null +++ b/src/newHome/homeOnboarding/onboardingContext/initialOnboardingState.ts @@ -0,0 +1,55 @@ +import { + OnboardingState, + OnboardingStep, +} from "@dashboard/newHome/homeOnboarding/onboardingContext/types"; + +export const initialOnboardingSteps: OnboardingStep[] = [ + { + id: "get-started", + completed: false, + expanded: undefined, + }, + { + id: "create-product", + completed: false, + expanded: undefined, + }, + { + id: "explore-orders", + completed: false, + expanded: undefined, + }, + { + id: "graphql-playground", + completed: false, + expanded: undefined, + }, + { + id: "view-webhooks", + completed: false, + expanded: undefined, + }, + { + id: "invite-staff", + completed: false, + expanded: undefined, + }, +]; + +export const TOTAL_STEPS_COUNT = initialOnboardingSteps.length; + +export const getInitialOnboardingState = (isNewUser: boolean): OnboardingState => { + if (isNewUser) { + return { + onboardingExpanded: true, + stepsCompleted: [], + stepsExpanded: {} as OnboardingState["stepsExpanded"], + }; + } + + return { + onboardingExpanded: false, + stepsCompleted: initialOnboardingSteps.map(step => step.id), + stepsExpanded: {} as OnboardingState["stepsExpanded"], + }; +}; diff --git a/src/newHome/homeOnboarding/onboardingContext/types.ts b/src/newHome/homeOnboarding/onboardingContext/types.ts index d2bdf8ad802..4ad0d325cbd 100644 --- a/src/newHome/homeOnboarding/onboardingContext/types.ts +++ b/src/newHome/homeOnboarding/onboardingContext/types.ts @@ -8,12 +8,15 @@ export type OnboardingStepsIDs = | "view-webhooks" | "invite-staff"; +export type OnboardingStep = { + id: OnboardingStepsIDs; + completed: boolean; + expanded: boolean | undefined; +}; + export type OnboardingState = { - steps: Array<{ - id: OnboardingStepsIDs; - completed: boolean; - expanded: boolean | undefined; - }>; + stepsCompleted: OnboardingStepsIDs[]; + stepsExpanded: Record; onboardingExpanded: boolean; }; diff --git a/src/newHome/homeOnboarding/onboardingContext/useExpandedOnboardingId.test.ts b/src/newHome/homeOnboarding/onboardingContext/useExpandedOnboardingId.test.ts new file mode 100644 index 00000000000..86bc66912d7 --- /dev/null +++ b/src/newHome/homeOnboarding/onboardingContext/useExpandedOnboardingId.test.ts @@ -0,0 +1,136 @@ +import { OnboardingState } from "@dashboard/newHome/homeOnboarding/onboardingContext/types"; +import { renderHook } from "@testing-library/react-hooks"; + +import { useExpandedOnboardingId } from "./useExpandedOnboardingId"; + +describe("useExpandedOnboardingId", () => { + it("should return first expanded step on init if exists", () => { + // Arrange + const onboardingState = { + stepsCompleted: ["get-started"], + stepsExpanded: { + "create-product": true, + }, + } as OnboardingState; + const loaded = true; + + // Act + const expandedStepId = renderHook(() => useExpandedOnboardingId(onboardingState, loaded)).result + .current; + + // assert + expect(expandedStepId).toBe("create-product"); + }); + + it("should return first not completed step when no one with expanded state", () => { + // Arrange + const onboardingState = { + stepsCompleted: ["get-started", "create-product"], + stepsExpanded: { "get-started": false }, + } as OnboardingState; + const loaded = true; + + // Act + const expandedStepId = renderHook(() => useExpandedOnboardingId(onboardingState, loaded)).result + .current; + + // Assert + expect(expandedStepId).toBe("explore-orders"); + }); + + it("should return empty string when all steps are collapsed", () => { + // Arrange + const onboardingState = { + onboardingExpanded: true, + stepsCompleted: [], + stepsExpanded: { + "get-started": false, + "create-product": false, + "explore-orders": false, + "graphql-playground": false, + "invite-staff": false, + "view-webhooks": false, + }, + } as OnboardingState; + const loaded = true; + + // Act + const expandedStepId = renderHook(() => useExpandedOnboardingId(onboardingState, loaded)).result + .current; + + // Assert + expect(expandedStepId).toBe(""); + }); + + it("should return first not completed step after step competed", async () => { + // Arrange + const onboardingState = { + onboardingExpanded: true, + stepsCompleted: [], + stepsExpanded: { + "get-started": false, + }, + } as unknown as OnboardingState; + const onboardingStateChanged = { + onboardingExpanded: true, + stepsCompleted: ["create-product"], + stepsExpanded: { + "get-started": false, + }, + } as OnboardingState; + const loaded = true; + + // Act + const { rerender, result } = renderHook( + ({ onboardingState, loaded }) => useExpandedOnboardingId(onboardingState, loaded), + { + initialProps: { + onboardingState, + loaded, + }, + }, + ); + + rerender({ onboardingState: onboardingStateChanged, loaded }); + + // Assert + expect(result.current).toBe("explore-orders"); + }); + + it("should return first expanded step after expand step toggle", async () => { + // Arrange + const onboardingState = { + onboardingExpanded: true, + stepsCompleted: ["get-started", "create-product"], + stepsExpanded: { + "get-started": false, + }, + } as unknown as OnboardingState; + + const onboardingStateChanged = { + onboardingExpanded: true, + stepsCompleted: ["get-started", "create-product"], + stepsExpanded: { + "get-started": false, + "explore-orders": true, + }, + } as OnboardingState; + const loaded = true; + + // Act + const { rerender, result } = renderHook( + ({ onboardingState, loaded }) => useExpandedOnboardingId(onboardingState, loaded), + { + initialProps: { + onboardingState, + loaded, + }, + }, + ); + + rerender({ onboardingState: onboardingStateChanged, loaded }); + + // Assert + expect(result.current).toBe("explore-orders"); + }); +}); diff --git a/src/newHome/homeOnboarding/onboardingContext/useExpandedOnboardingId.ts b/src/newHome/homeOnboarding/onboardingContext/useExpandedOnboardingId.ts index ad4363e9628..b45e32896a4 100644 --- a/src/newHome/homeOnboarding/onboardingContext/useExpandedOnboardingId.ts +++ b/src/newHome/homeOnboarding/onboardingContext/useExpandedOnboardingId.ts @@ -1,24 +1,49 @@ -import React from "react"; +import React, { useEffect } from "react"; -import { OnboardingState } from "./types"; +import { OnboardingState, OnboardingStepsIDs } from "./types"; +import { + getFirstExpanderStepId, + getFirstNotCompletedAndNotExpandedStep, + getNextStepToExpand, +} from "./utils"; export const useExpandedOnboardingId = (onboardingState: OnboardingState, loaded: boolean) => { - const hasBeenCall = React.useRef(false); + const hasBeenCalled = React.useRef(false); + const [expandedStepId, setExpandedStepId] = React.useState(""); - const allExpandedSteps = onboardingState.steps.filter(step => step.expanded); - const extendedStepId = allExpandedSteps.length ? allExpandedSteps[0].id : ""; + useEffect(() => { + if (hasBeenCalled.current) { + const firstExpandedStepId = getFirstExpanderStepId(onboardingState); - if (extendedStepId) { - return extendedStepId; - } + if (firstExpandedStepId) { + setExpandedStepId(getFirstExpanderStepId(onboardingState)); + } else { + setExpandedStepId(""); + } + } + }, [onboardingState.stepsExpanded]); - if (loaded && !hasBeenCall.current) { - hasBeenCall.current = true; + useEffect(() => { + // On every state change + if (hasBeenCalled.current) { + setExpandedStepId(getNextStepToExpand(onboardingState)); + } + }, [onboardingState.stepsCompleted]); - return ( - onboardingState.steps.find(step => !step.completed && step.expanded === undefined)?.id ?? "" - ); - } + useEffect(() => { + // On context first load + if (loaded && !hasBeenCalled.current) { + hasBeenCalled.current = true; - return ""; + const firstExpandedStep = getFirstExpanderStepId(onboardingState); + + if (firstExpandedStep) { + setExpandedStepId(firstExpandedStep); + } else { + setExpandedStepId(getFirstNotCompletedAndNotExpandedStep(onboardingState)); + } + } + }, [loaded, onboardingState]); + + return expandedStepId; }; diff --git a/src/newHome/homeOnboarding/onboardingContext/utils.test.ts b/src/newHome/homeOnboarding/onboardingContext/utils.test.ts new file mode 100644 index 00000000000..0667534bb86 --- /dev/null +++ b/src/newHome/homeOnboarding/onboardingContext/utils.test.ts @@ -0,0 +1,247 @@ +import { OnboardingState } from "@dashboard/newHome/homeOnboarding/onboardingContext/types"; + +import { + getFirstExpanderStepId, + getFirstNotCompletedAndNotExpandedStep, + getNextStepToExpand, + handleStateChangeAfterStepCompleted, + handleStateChangeAfterToggle, + mapInitialStepsWithState, +} from "./utils"; + +describe("handleStateChangeAfterStepCompleted", () => { + it("should add the step to the completed steps", () => { + // Arrange + const state = { + onboardingExpanded: true, + stepsCompleted: [], + stepsExpanded: {}, + } as unknown as OnboardingState; + const id = "get-started"; + + // Act + const newState = handleStateChangeAfterStepCompleted(state, id); + + // Assert + expect(newState.stepsCompleted).toEqual(["get-started"]); + }); + + it("should add the step to the completed steps and add get-started if not already there", () => { + // Arrange + const state = { + onboardingExpanded: true, + stepsCompleted: [], + stepsExpanded: {}, + } as unknown as OnboardingState; + const id = "create-product"; + + // Act + const newState = handleStateChangeAfterStepCompleted(state, id); + + // Assert + expect(newState.stepsCompleted).toEqual(["get-started", "create-product"]); + }); +}); + +describe("handleStateChangeAfterToggle", () => { + it("should set the expanded step id", () => { + // Arrange + const state = { + onboardingExpanded: true, + stepsCompleted: [], + stepsExpanded: {}, + } as unknown as OnboardingState; + const expandedId = "get-started"; + const id = "get-started"; + + // Act + const newState = handleStateChangeAfterToggle(state, expandedId, id); + + // Assert + expect(newState.stepsExpanded).toEqual({ "get-started": true }); + }); + + it("should toggle expanded step", () => { + // Arrange + const state = { + onboardingExpanded: true, + stepsCompleted: [], + stepsExpanded: { "get-started": true }, + } as unknown as OnboardingState; + const expandedId = "get-started"; + const id = ""; + + // Act + const newState = handleStateChangeAfterToggle(state, expandedId, id); + + // Assert + expect(newState.stepsExpanded).toEqual({ "get-started": false }); + }); + + it("should set the expanded step id and remove the previous one", () => { + // Arrange + const state = { + onboardingExpanded: true, + stepsCompleted: [], + stepsExpanded: { "get-started": true, "invite-staff": false }, + } as unknown as OnboardingState; + const expandedId = "create-product"; + const id = "create-product"; + + // Act + const newState = handleStateChangeAfterToggle(state, expandedId, id); + + // Assert + expect(newState.stepsExpanded).toEqual({ "create-product": true, "invite-staff": false }); + }); +}); + +describe("getFirstExpanderStepId", () => { + it("should return the first expanded step id", () => { + // Arrange + const onboardingState = { + stepsCompleted: ["get-started"], + stepsExpanded: { + "create-product": true, + }, + } as OnboardingState; + + // Act + const firstExpandedStepId = getFirstExpanderStepId(onboardingState); + + // Assert + expect(firstExpandedStepId).toBe("create-product"); + }); + + it("should return empty string when no step is expanded", () => { + // Arrange + const onboardingState = { + stepsCompleted: ["get-started", "create-product"], + stepsExpanded: { "get-started": false }, + } as OnboardingState; + + // Act + const firstExpandedStepId = getFirstExpanderStepId(onboardingState); + + // Assert + expect(firstExpandedStepId).toBe(""); + }); +}); + +describe("mapInitialStepsWithState", () => { + it("should map initial steps with state", () => { + // Arrange + const onboardingState = { + stepsCompleted: ["get-started"], + stepsExpanded: { + "create-product": true, + }, + } as OnboardingState; + + // Act + const mappedSteps = mapInitialStepsWithState(onboardingState); + + // Assert + expect(mappedSteps).toEqual([ + { + id: "get-started", + completed: true, + expanded: undefined, + }, + { + id: "create-product", + completed: false, + expanded: true, + }, + { + id: "explore-orders", + completed: false, + expanded: undefined, + }, + { + id: "graphql-playground", + completed: false, + expanded: undefined, + }, + { + id: "view-webhooks", + completed: false, + expanded: undefined, + }, + { + id: "invite-staff", + completed: false, + expanded: undefined, + }, + ]); + }); +}); + +describe("getFirstNotCompletedAndNotExpandedStep", () => { + it("should return the first not completed and not expanded step", () => { + // Arrange + const onboardingState = { + stepsCompleted: ["get-started"], + stepsExpanded: { + "create-product": false, + }, + } as OnboardingState; + + // Act + const firstNotCompletedStep = getFirstNotCompletedAndNotExpandedStep(onboardingState); + + // Assert + expect(firstNotCompletedStep).toEqual("explore-orders"); + }); + + it("should return empty string when all steps are completed", () => { + // Arrange + const onboardingState = { + stepsCompleted: [ + "get-started", + "create-product", + "explore-orders", + "graphql-playground", + "view-webhooks", + "invite-staff", + ], + stepsExpanded: {}, + } as OnboardingState; + + // Act + const firstNotCompletedStep = getFirstNotCompletedAndNotExpandedStep(onboardingState); + + // Assert + expect(firstNotCompletedStep).toEqual(""); + }); +}); + +describe("getNextStepToExpand", () => { + it("should return the first step after last completed", () => { + // Arrange + const onboardingState = { + stepsCompleted: ["create-product"], + stepsExpanded: {}, + } as OnboardingState; + + // Act + const nextStep = getNextStepToExpand(onboardingState); + + // Assert + expect(nextStep).toEqual("explore-orders"); + }); + + it("should return empty string when no next step after last completed", () => { + // Arrange + const onboardingState = { + stepsCompleted: ["invite-staff"], + stepsExpanded: {}, + } as OnboardingState; + + // Act + const nextStep = getNextStepToExpand(onboardingState); + + // Assert + expect(nextStep).toEqual(""); + }); +}); diff --git a/src/newHome/homeOnboarding/onboardingContext/utils.ts b/src/newHome/homeOnboarding/onboardingContext/utils.ts new file mode 100644 index 00000000000..a837e52770f --- /dev/null +++ b/src/newHome/homeOnboarding/onboardingContext/utils.ts @@ -0,0 +1,79 @@ +import { initialOnboardingSteps } from "./initialOnboardingState"; +import { OnboardingState, OnboardingStepsIDs } from "./types"; + +export const handleStateChangeAfterStepCompleted = ( + state: OnboardingState, + id: OnboardingStepsIDs, +): OnboardingState => { + const newCompletedSteps = [...state.stepsCompleted]; + const stepsExpanded = { ...state.stepsExpanded }; + + if (!newCompletedSteps.includes("get-started") && id !== "get-started") { + newCompletedSteps.push("get-started"); + } + + newCompletedSteps.push(id); + + if (stepsExpanded[id]) { + delete stepsExpanded[id]; + } + + return { + ...state, + stepsExpanded, + stepsCompleted: newCompletedSteps, + }; +}; + +export const handleStateChangeAfterToggle = ( + state: OnboardingState, + expandedId: OnboardingStepsIDs, + id: string, +): OnboardingState => { + const stepsExpanded = { ...state.stepsExpanded }; + + for (const key in stepsExpanded) { + if (stepsExpanded[key as OnboardingStepsIDs]) { + delete stepsExpanded[key as OnboardingStepsIDs]; + } + } + + stepsExpanded[expandedId as OnboardingStepsIDs] = id !== ""; + + return { + ...state, + stepsExpanded, + }; +}; + +export const getFirstExpanderStepId = (onboardingState: OnboardingState) => { + const stepsExpandedEntries = Object.entries(onboardingState.stepsExpanded); + + return (stepsExpandedEntries.find(([_, value]) => value)?.[0] ?? "") as OnboardingStepsIDs; +}; + +export const mapInitialStepsWithState = (onboardingState: OnboardingState) => + [...initialOnboardingSteps].map(step => ({ + ...step, + completed: onboardingState.stepsCompleted.includes(step.id), + expanded: onboardingState.stepsExpanded[step.id], + })); + +export const getFirstNotCompletedAndNotExpandedStep = (onboardingState: OnboardingState) => { + return ( + mapInitialStepsWithState(onboardingState).filter( + step => !step.completed && step.expanded !== false, + )[0]?.id ?? "" + ); +}; + +export const getNextStepToExpand = (onboardingState: OnboardingState) => { + const lastCompletedStepId = + onboardingState.stepsCompleted[onboardingState.stepsCompleted.length - 1]; + const steps = mapInitialStepsWithState(onboardingState); + const stepIndex = steps.findIndex(step => step.id === lastCompletedStepId); + + return ( + steps.slice(stepIndex + 1).find(step => !step.completed && step.expanded !== false)?.id ?? "" + ); +}; diff --git a/src/orders/views/OrderList/OrderList.tsx b/src/orders/views/OrderList/OrderList.tsx index 3ec959e767e..5a4803e9017 100644 --- a/src/orders/views/OrderList/OrderList.tsx +++ b/src/orders/views/OrderList/OrderList.tsx @@ -6,6 +6,7 @@ import { useConditionalFilterContext } from "@dashboard/components/ConditionalFi import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog"; import { useShopLimitsQuery } from "@dashboard/components/Shop/queries"; +import { useFlag } from "@dashboard/featureFlags"; import { useOrderDraftCreateMutation, useOrderListQuery } from "@dashboard/graphql"; import { useFilterHandlers } from "@dashboard/hooks/useFilterHandlers"; import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; @@ -17,12 +18,13 @@ import usePaginator, { createPaginationState, PaginatorContext, } from "@dashboard/hooks/usePaginator"; +import { useOnboarding } from "@dashboard/newHome/homeOnboarding/onboardingContext"; import { ListViews } from "@dashboard/types"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createSortHandler from "@dashboard/utils/handlers/sortHandler"; import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps"; import { getSortParams } from "@dashboard/utils/sort"; -import React from "react"; +import React, { useEffect } from "react"; import { useIntl } from "react-intl"; import OrderListPage from "../../components/OrderListPage/OrderListPage"; @@ -46,6 +48,15 @@ export const OrderList: React.FC = ({ params }) => { const { updateListSettings, settings } = useListSettings(ListViews.ORDER_LIST); const { valueProvider } = useConditionalFilterContext(); + const { markOnboardingStepAsCompleted } = useOnboarding(); + const newHomePageFlag = useFlag("new_home_page"); + + useEffect(() => { + if (newHomePageFlag) { + markOnboardingStepAsCompleted("explore-orders"); + } + }, []); + const { hasPresetsChanged, onPresetChange, diff --git a/src/products/views/ProductCreate/ProductCreate.tsx b/src/products/views/ProductCreate/ProductCreate.tsx index 57039a010a5..54ef9dbc210 100644 --- a/src/products/views/ProductCreate/ProductCreate.tsx +++ b/src/products/views/ProductCreate/ProductCreate.tsx @@ -5,6 +5,7 @@ import { AttributeInput } from "@dashboard/components/Attributes"; import ChannelsAvailabilityDialog from "@dashboard/components/ChannelsAvailabilityDialog"; import { WindowTitle } from "@dashboard/components/WindowTitle"; import { DEFAULT_INITIAL_SEARCH_DATA, VALUES_PAGINATE_BY } from "@dashboard/config"; +import { useFlag } from "@dashboard/featureFlags"; import { ProductChannelListingErrorFragment, ProductErrorWithAttributesFragment, @@ -23,6 +24,7 @@ import useNavigator from "@dashboard/hooks/useNavigator"; import useNotifier from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { getMutationErrors } from "@dashboard/misc"; +import { useOnboarding } from "@dashboard/newHome/homeOnboarding/onboardingContext"; import ProductCreatePage, { ProductCreateData, } from "@dashboard/products/components/ProductCreatePage"; @@ -59,6 +61,8 @@ export const ProductCreateView: React.FC = ({ params }) => { const navigate = useNavigator(); const notify = useNotifier(); const shop = useShop(); + const { markOnboardingStepAsCompleted } = useOnboarding(); + const newHomePageFlag = useFlag("new_home_page"); const intl = useIntl(); const [productCreateComplete, setProductCreateComplete] = React.useState(false); const selectedProductTypeId = params["product-type-id"]; @@ -207,6 +211,7 @@ export const ProductCreateView: React.FC = ({ params }) => { )(data); if (!errors?.length) { + newHomePageFlag.enabled && markOnboardingStepAsCompleted("create-product"); setProductCreateComplete(true); } diff --git a/src/staff/views/StaffList/StaffList.tsx b/src/staff/views/StaffList/StaffList.tsx index 0b668c0edbe..d24ae2d0860 100644 --- a/src/staff/views/StaffList/StaffList.tsx +++ b/src/staff/views/StaffList/StaffList.tsx @@ -3,6 +3,7 @@ import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog"; import { useShopLimitsQuery } from "@dashboard/components/Shop/queries"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@dashboard/config"; +import { useFlag } from "@dashboard/featureFlags"; import { useStaffListQuery, useStaffMemberAddMutation } from "@dashboard/graphql"; import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; @@ -14,6 +15,7 @@ import usePaginator, { PaginatorContext, } from "@dashboard/hooks/usePaginator"; import { commonMessages } from "@dashboard/intl"; +import { useOnboarding } from "@dashboard/newHome/homeOnboarding/onboardingContext"; import usePermissionGroupSearch from "@dashboard/searches/usePermissionGroupSearch"; import { ListViews } from "@dashboard/types"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; @@ -46,6 +48,8 @@ export const StaffList: React.FC = ({ params }) => { const notify = useNotifier(); const { updateListSettings, settings } = useListSettings(ListViews.STAFF_MEMBERS_LIST); const intl = useIntl(); + const { markOnboardingStepAsCompleted } = useOnboarding(); + const newHomePageFlag = useFlag("new_home_page"); usePaginationReset(staffListUrl, params, settings.rowNumber); @@ -117,8 +121,10 @@ export const StaffList: React.FC = ({ params }) => { } = usePermissionGroupSearch({ variables: DEFAULT_INITIAL_SEARCH_DATA, }); - const handleStaffMemberAdd = (variables: AddMemberFormData) => - addStaffMember({ + const handleStaffMemberAdd = (variables: AddMemberFormData) => { + newHomePageFlag.enabled && markOnboardingStepAsCompleted("invite-staff"); + + return addStaffMember({ variables: { input: { addGroups: variables.permissionGroups, @@ -133,6 +139,7 @@ export const StaffList: React.FC = ({ params }) => { }, }, }); + }; return ( diff --git a/vite.config.js b/vite.config.js index 7ece9d75f06..5ffc95b53ea 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,10 +14,7 @@ const copyNoopSW = () => ({ apply: "build", writeBundle: () => { mkdirSync(path.resolve("build", "dashboard"), { recursive: true }); - copyFileSync( - path.resolve("assets", "sw.js"), - path.resolve("build", "dashboard", "sw.js"), - ); + copyFileSync(path.resolve("assets", "sw.js"), path.resolve("build", "dashboard", "sw.js")); }, }); @@ -26,10 +23,7 @@ const copyOgImage = () => ({ apply: "build", writeBundle: () => { mkdirSync(path.resolve("build", "dashboard"), { recursive: true }); - copyFileSync( - path.resolve("assets", "og.png"), - path.resolve("build", "dashboard", "og.png"), - ); + copyFileSync(path.resolve("assets", "og.png"), path.resolve("build", "dashboard", "og.png")); }, }); @@ -61,6 +55,7 @@ export default defineConfig(({ command, mode }) => { SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT, + ONBOARDING_USER_JOINED_DATE_THRESHOLD, // eslint-disable-next-line camelcase npm_package_version, } = env; @@ -90,6 +85,7 @@ export default defineConfig(({ command, mode }) => { LOCALE_CODE, POSTHOG_KEY, POSTHOG_HOST, + ONBOARDING_USER_JOINED_DATE_THRESHOLD, injectOgTags: DEMO_MODE && ` @@ -166,6 +162,7 @@ export default defineConfig(({ command, mode }) => { STATIC_URL, POSTHOG_KEY, POSTHOG_HOST, + ONBOARDING_USER_JOINED_DATE_THRESHOLD, // eslint-disable-next-line camelcase RELEASE_NAME: npm_package_version, }, @@ -221,10 +218,7 @@ export default defineConfig(({ command, mode }) => { Vite resolves it by using jsnext:main https://github.com/moment/moment/blob/develop/package.json#L26. We enforce to use a different path, ignoring jsnext:main field. */ - moment: path.resolve( - __dirname, - "./node_modules/moment/min/moment-with-locales.js", - ), + moment: path.resolve(__dirname, "./node_modules/moment/min/moment-with-locales.js"), }, }, plugins,