export const metadata = { title: `${pageNumber} Example: How to Create Onboarding Widget`, } # {metadata.title} Admin customizations are coming soon. In this chapter, you’ll learn how to build the onboarding widget available in the admin dashboard the first time you install a Medusa application. The onboarding widget is already implemented within the codebase of your Medusa application. This chapter is useful if you want to understand how it was implemented using Medusa Admin customizations. ## What you’ll be Building By following this chapter, you’ll: - Build an onboarding flow in the admin that takes the user through creating a sample product and order. This flow has four steps and navigates the user between four pages in the admin before completing the guide. This will be implemented using widgets. - Keep track of the current step the user has reached by creating a table in the database and an API Route that the admin widget uses to retrieve and update the current step. - Medusa React --- ## 1. Implement Helper Resources The resources in this section are used for typing, layout, and design purposes, and they’re used in other essential components in this tutorial. Each of the collapsible elements below hold the path to the file that you should create, and the content of that file.
```ts title="src/admin/types/icon-type.ts" import React from "react" type IconProps = { color?: string size?: string | number } & React.SVGAttributes export default IconProps ```
```tsx title="src/admin/components/shared/icons/get-started.tsx" import React from "react" import IconProps from "../../../types/icon-type" const GetStarted: React.FC = ({ size = "40", color = "currentColor", ...attributes }) => { return ( ) } export default GetStarted ```
```tsx title="src/admin/components/shared/icons/dollar-sign-icon.tsx" import React from "react" import IconProps from "../../../types/icon-type" const ActiveCircleDottedLine: React.FC = ({ size = "24", color = "currentColor", ...attributes }) => { return ( ) } export default ActiveCircleDottedLine ```
```tsx title="src/admin/components/shared/accordion.tsx" import * as AccordionPrimitive from "@radix-ui/react-accordion" import React from "react" import { CheckCircleSolid, CircleMiniSolid } from "@medusajs/icons" import { Heading, Text, clx } from "@medusajs/ui" import ActiveCircleDottedLine from "./icons/active-circle-dotted-line" type AccordionItemProps = AccordionPrimitive.AccordionItemProps & { title: string; subtitle?: string; description?: string; required?: boolean; tooltip?: string; forceMountContent?: true; headingSize?: "small" | "medium" | "large"; customTrigger?: React.ReactNode; complete?: boolean; active?: boolean; triggerable?: boolean; }; const Accordion: React.FC< | (AccordionPrimitive.AccordionSingleProps & React.RefAttributes) | (AccordionPrimitive.AccordionMultipleProps & React.RefAttributes) > & { Item: React.FC; } = ({ children, ...props }) => { return ( {children} ) } const Item: React.FC = ({ title, subtitle, description, required, tooltip, children, className, complete, headingSize = "large", customTrigger = undefined, forceMountContent = undefined, active, triggerable, ...props }) => { return (
{complete ? ( ) : ( <> {active && ( )} {!active && ( )} )}
{title}
{customTrigger || }
{subtitle && ( {subtitle} )}
{description && {description}}
{children}
) } Accordion.Item = Item const MorphingTrigger = () => { return (
) } export default Accordion ```
```tsx title="src/admin/components/shared/card.tsx" import { Text, clx } from "@medusajs/ui" type CardProps = { icon?: React.ReactNode children?: React.ReactNode className?: string } const Card = ({ icon, children, className, }: CardProps) => { return (
{icon} {children}
) } export default Card ```
--- ## 2. Medusa Application Customizations In this step, you’ll customize the Medusa application to: 1. Add a new table in the database that stores the current onboarding step. This requires creating a new entity, repository, and migration. 2. Add new API Routes that allow retrieving and updating the current onboarding step. This also requires creating a new service. ### Create Data Model To create the data model, create the file `src/models/onboarding.ts` with the following content: ```ts title="src/models/onboarding.ts" import { BaseEntity } from "@medusajs/medusa" import { Column, Entity } from "typeorm" @Entity() export class OnboardingState extends BaseEntity { @Column() current_step: string @Column() is_complete: boolean @Column() product_id: string } ``` ### Create Migration To create a migration, run the following command: ```bash npx typeorm migration:create src/migrations/CreateOnboarding ``` This will create a file in the `src/migrations` directory with the name formatted as `-CreateOnboarding.ts`. In that file, import the `generateEntityId` utility method at the top of the file: ```ts import { generateEntityId } from "@medusajs/utils" ``` Then, replace the `up` and `down` methods in the migration class with the following content: ```ts export class CreateOnboarding1685715079776 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( `CREATE TABLE "onboarding_state" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "current_step" character varying NULL, "is_complete" boolean, "product_id" character varying NULL)` ) await queryRunner.query( `INSERT INTO "onboarding_state" ("id", "current_step", "is_complete") VALUES ('${generateEntityId( "", "onboarding" )}' , NULL, false)` ) } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`DROP TABLE "onboarding_state"`) } } ``` Don’t copy the name of the class in the code snippet above. Keep the name you have in the file. Finally, to reflect the migration in the database, run the `build` and `migration` commands: ```bash npm2yarn npm run build npx medusa migrations run ``` ### Create Service Start by creating the file `src/types/onboarding.ts` with the following content: ```ts title="src/types/onboarding.ts" import { OnboardingState } from "../models/onboarding" export type UpdateOnboardingStateInput = { current_step?: string; is_complete?: boolean; product_id?: string; }; export interface AdminOnboardingUpdateStateReq {} export type OnboardingStateRes = { status: OnboardingState; }; ``` This file holds the necessary types that will be used within the service you’ll create, and later in your onboarding flow widget. Then, create the file `src/services/onboarding.ts` with the following content: ```ts title="src/services/onboarding.ts" import { TransactionBaseService } from "@medusajs/medusa" import OnboardingRepository from "../repositories/onboarding" import { OnboardingState } from "../models/onboarding" import { EntityManager, IsNull, Not } from "typeorm" import { UpdateOnboardingStateInput } from "../types/onboarding" type InjectedDependencies = { manager: EntityManager; onboardingRepository: typeof OnboardingRepository; }; class OnboardingService extends TransactionBaseService { protected onboardingRepository_: typeof OnboardingRepository constructor({ onboardingRepository }: InjectedDependencies) { super(arguments[0]) this.onboardingRepository_ = onboardingRepository } async retrieve(): Promise { const onboardingRepo = this.activeManager_.withRepository( this.onboardingRepository_ ) const status = await onboardingRepo.findOne({ where: { id: Not(IsNull()) }, }) return status } async update( data: UpdateOnboardingStateInput ): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { const onboardingRepository = transactionManager.withRepository( this.onboardingRepository_ ) const status = await this.retrieve() for (const [key, value] of Object.entries(data)) { status[key] = value } return await onboardingRepository.save(status) } ) } } export default OnboardingService ``` This service class implements two methods `retrieve` to retrieve the current onboarding state, and `update` to update the current onboarding state. ### Create API Routes To add the API Routes, create the file `src/api/admin/onboarding/route.ts` with the following content: ```ts title="src/api/admin/onboarding/route.ts" import type { MedusaRequest, MedusaResponse, } from "@medusajs/medusa" import { EntityManager } from "typeorm" import OnboardingService from "../../../services/onboarding" export async function GET( req: MedusaRequest, res: MedusaResponse ) { const onboardingService: OnboardingService = req.scope.resolve("onboardingService") const status = await onboardingService.retrieve() res.status(200).json({ status }) } export async function POST( req: MedusaRequest, res: MedusaResponse ) { const onboardingService: OnboardingService = req.scope.resolve("onboardingService") const manager: EntityManager = req.scope.resolve("manager") const status = await manager.transaction( async (transactionManager) => { return await onboardingService .withTransaction(transactionManager) .update(req.body) } ) res.status(200).json({ status }) } ``` These API routes resolve the `OnboardingService` to use its functionalities. --- ## 3. Create Onboarding Widget In this step, you’ll create the onboarding widget with a general implementation. Some implementation details will be added later in the chapter. Create the file `src/admin/widgets/onboarding-flow/onboarding-flow.tsx` with the following content: ```tsx title="src/admin/widgets/onboarding-flow/onboarding-flow.tsx" import { OrderDetailsWidgetProps, ProductDetailsWidgetProps, WidgetConfig, WidgetProps } from "@medusajs/admin" import { useAdminCustomPost, useAdminCustomQuery, useMedusa } from "medusa-react" import React, { useEffect, useState, useMemo, useCallback } from "react" import { useNavigate, useSearchParams, useLocation } from "react-router-dom" import { OnboardingState } from "../../../models/onboarding" import { AdminOnboardingUpdateStateReq, OnboardingStateRes, UpdateOnboardingStateInput, } from "../../../types/onboarding" import OrderDetailDefault from "../../components/onboarding-flow/default/orders/order-detail" import OrdersListDefault from "../../components/onboarding-flow/default/orders/orders-list" import ProductDetailDefault from "../../components/onboarding-flow/default/products/product-detail" import ProductsListDefault from "../../components/onboarding-flow/default/products/products-list" import { Button, Container, Heading, Text, clx } from "@medusajs/ui" import Accordion from "../../components/shared/accordion" import GetStarted from "../../components/shared/icons/get-started" import { Order, Product } from "@medusajs/medusa" import ProductsListNextjs from "../../components/onboarding-flow/nextjs/products/products-list" import ProductDetailNextjs from "../../components/onboarding-flow/nextjs/products/product-detail" import OrdersListNextjs from "../../components/onboarding-flow/nextjs/orders/orders-list" import OrderDetailNextjs from "../../components/onboarding-flow/nextjs/orders/order-detail" type STEP_ID = | "create_product" | "preview_product" | "create_order" | "setup_finished" | "create_product_nextjs" | "preview_product_nextjs" | "create_order_nextjs" | "setup_finished_nextjs" type OnboardingWidgetProps = WidgetProps | ProductDetailsWidgetProps | OrderDetailsWidgetProps export type StepContentProps = OnboardingWidgetProps & { onNext?: (data?: any) => void; isComplete?: boolean; data?: OnboardingState; }; type Step = { id: STEP_ID; title: string; component: React.FC; onNext?: (data?: any) => void; }; const QUERY_KEY = ["onboarding_state"] const OnboardingFlow = (props: OnboardingWidgetProps) => { // create custom hooks for custom API Routes const { data, isLoading } = useAdminCustomQuery< undefined, OnboardingStateRes >("/onboarding", QUERY_KEY) const { mutate } = useAdminCustomPost< AdminOnboardingUpdateStateReq, OnboardingStateRes >("/onboarding", QUERY_KEY) const navigate = useNavigate() const location = useLocation() // will be used if onboarding step // is passed as a path parameter const { client } = useMedusa() // get current step from custom API Route const currentStep: STEP_ID | undefined = useMemo(() => { return data?.status ?.current_step as STEP_ID }, [data]) // initialize some state const [openStep, setOpenStep] = useState(currentStep) const [completed, setCompleted] = useState(false) // this method is used to move from one step to the next const setStepComplete = ({ step_id, extraData, onComplete, }: { step_id: STEP_ID; extraData?: UpdateOnboardingStateInput; onComplete?: () => void; }) => { const next = steps[findStepIndex(step_id) + 1] mutate({ current_step: next.id, ...extraData }, { onSuccess: onComplete, }) } // this is useful if you want to change the current step // using a path parameter. It can only be changed if the passed // step in the path parameter is the next step. const [ searchParams ] = useSearchParams() // the steps are set based on the // onboarding type const steps: Step[] = useMemo(() => { { switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) { case "nextjs": return [ { id: "create_product_nextjs", title: "Create Products", component: ProductsListNextjs, onNext: (product: Product) => { setStepComplete({ step_id: "create_product_nextjs", extraData: { product_id: product.id }, onComplete: () => { if (!location.pathname.startsWith(`/a/products/${product.id}`)) { navigate(`/a/products/${product.id}`) } }, }) }, }, { id: "preview_product_nextjs", title: "Preview Product in your Next.js Storefront", component: ProductDetailNextjs, onNext: () => { setStepComplete({ step_id: "preview_product_nextjs", onComplete: () => navigate(`/a/orders`), }) }, }, { id: "create_order_nextjs", title: "Create an Order using your Next.js Storefront", component: OrdersListNextjs, onNext: (order: Order) => { setStepComplete({ step_id: "create_order_nextjs", onComplete: () => { if (!location.pathname.startsWith(`/a/orders/${order.id}`)) { navigate(`/a/orders/${order.id}`) } }, }) }, }, { id: "setup_finished_nextjs", title: "Setup Finished: Continue Building your Ecommerce Store", component: OrderDetailNextjs, }, ] default: return [ { id: "create_product", title: "Create Product", component: ProductsListDefault, onNext: (product: Product) => { setStepComplete({ step_id: "create_product", extraData: { product_id: product.id }, onComplete: () => { if (!location.pathname.startsWith(`/a/products/${product.id}`)) { navigate(`/a/products/${product.id}`) } }, }) }, }, { id: "preview_product", title: "Preview Product", component: ProductDetailDefault, onNext: () => { setStepComplete({ step_id: "preview_product", onComplete: () => navigate(`/a/orders`), }) }, }, { id: "create_order", title: "Create an Order", component: OrdersListDefault, onNext: (order: Order) => { setStepComplete({ step_id: "create_order", onComplete: () => { if (!location.pathname.startsWith(`/a/orders/${order.id}`)) { navigate(`/a/orders/${order.id}`) } }, }) }, }, { id: "setup_finished", title: "Setup Finished: Start developing with Medusa", component: OrderDetailDefault, }, ] } } }, [location.pathname]) // used to retrieve the index of a step by its ID const findStepIndex = useCallback((step_id: STEP_ID) => { return steps.findIndex((step) => step.id === step_id) }, [steps]) // used to check if a step is completed const isStepComplete = useCallback((step_id: STEP_ID) => { return findStepIndex(currentStep) > findStepIndex(step_id) }, [findStepIndex, currentStep]) // this is used to retrieve the data necessary // to move to the next onboarding step const getOnboardingParamStepData = useCallback(async (onboardingStep: string, data?: { orderId?: string, productId?: string, }) => { switch (onboardingStep) { case "setup_finished_nextjs": case "setup_finished": if (!data?.orderId && "order" in props) { return props.order } const orderId = data?.orderId || searchParams.get("order_id") if (orderId) { return (await client.admin.orders.retrieve(orderId)).order } throw new Error ("Required `order_id` parameter was not passed as a parameter") case "preview_product_nextjs": case "preview_product": if (!data?.productId && "product" in props) { return props.product } const productId = data?.productId || searchParams.get("product_id") if (productId) { return (await client.admin.products.retrieve(productId)).product } throw new Error ("Required `product_id` parameter was not passed as a parameter") default: return undefined } }, [searchParams, props]) const isProductCreateStep = useMemo(() => { return currentStep === "create_product" || currentStep === "create_product_nextjs" }, [currentStep]) const isOrderCreateStep = useMemo(() => { return currentStep === "create_order" || currentStep === "create_order_nextjs" }, [currentStep]) // used to change the open step when the current // step is retrieved from custom API Routes useEffect(() => { setOpenStep(currentStep) if (findStepIndex(currentStep) === steps.length - 1) {setCompleted(true)} }, [currentStep, findStepIndex]) // used to check if the user created a product and has entered its details page // the step is changed to the next one useEffect(() => { if (location.pathname.startsWith("/a/products/prod_") && isProductCreateStep && "product" in props) { // change to the preview product step const currentStepIndex = findStepIndex(currentStep) steps[currentStepIndex].onNext?.(props.product) } }, [location.pathname, isProductCreateStep]) // used to check if the user created an order and has entered its details page // the step is changed to the next one. useEffect(() => { if (location.pathname.startsWith("/a/orders/order_") && isOrderCreateStep && "order" in props) { // change to the preview product step const currentStepIndex = findStepIndex(currentStep) steps[currentStepIndex].onNext?.(props.order) } }, [location.pathname, isOrderCreateStep]) // used to check if the `onboarding_step` path // parameter is passed and, if so, moves to that step // only if it's the next step and its necessary data is passed useEffect(() => { const onboardingStep = searchParams.get("onboarding_step") as STEP_ID const onboardingStepIndex = findStepIndex(onboardingStep) if (onboardingStep && onboardingStepIndex !== -1 && onboardingStep !== openStep) { // change current step to the onboarding step const openStepIndex = findStepIndex(openStep) if (onboardingStepIndex !== openStepIndex + 1) { // can only go forward one step return } // retrieve necessary data and trigger the next function getOnboardingParamStepData(onboardingStep) .then((data) => { steps[openStepIndex].onNext?.(data) }) .catch((e) => console.error(e)) } }, [searchParams, openStep, getOnboardingParamStepData]) if ( !isLoading && data?.status?.is_complete && !localStorage.getItem("override_onboarding_finish") ) {return null} // a method that will be triggered when // the setup is started const onStart = () => { mutate({ current_step: steps[0].id }) navigate(`/a/products`) } // a method that will be triggered when // the setup is completed const onComplete = () => { setCompleted(true) } // a method that will be triggered when // the setup is closed const onHide = () => { mutate({ is_complete: true }) } // used to get text for get started header const getStartedText = () => { switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) { case "nextjs": return "Learn the basics of Medusa by creating your first order using the Next.js storefront." default: return "Learn the basics of Medusa by creating your first order." } } return ( <> setOpenStep(value as STEP_ID)} >
{!completed ? ( <>
Get started {getStartedText()}
{!!currentStep ? ( <> {currentStep === steps[steps.length - 1].id ? ( ) : ( )} ) : ( <> )}
) : ( <>
Thank you for completing the setup guide! This whole experience was built using our new{" "} widgets feature.
You can find out more details and build your own by following{" "} our guide .
)}
{
{(!completed ? steps : steps.slice(-1)).map((step) => { const isComplete = isStepComplete(step.id) const isCurrent = currentStep === step.id return ( , })} >
) })}
}
) } export const config: WidgetConfig = { zone: [ "product.list.before", "product.details.before", "order.list.before", "order.details.before", ], } export default OnboardingFlow ``` you'll see errors related to components not being defined. You'll create these components in upcoming sections. There are three important details to ensure that Medusa reads this file as a widget: 1. The file is placed under the `src/admin/widget` directory. 2. The file exports a `config` object of type `WidgetConfig`, which is imported from `@medusajs/admin`. 3. The file default exports a React component, which in this case is `OnboardingFlow` The extension uses `react-router-dom`, which is available as a dependency of the `@medusajs/admin` package, to navigate to other pages in the dashboard. The `OnboardingFlow` widget also implements functionalities related to handling the steps of the onboarding flow, including navigating between them and updating the current step in the application. To use the custom API Routes created in a previous step, you use the `useAdminCustomQuery` and `useAdminCustomPost` hooks from the `medusa-react` package. --- ## 4. Create Step Components In this section, you’ll create the components for each step in the onboarding flow. You’ll then update the `OnboardingFlow` widget to use these components. Notice that as there are two types of flows, you'll be creating the components for the default flow and for the Next.js flow.
The `ProductsListDefault` component is used in the first step of the onboarding widget's default flow. It allows the user to create a sample product. Create the file `src/admin/components/onboarding-flow/default/products/products-list.tsx` with the following content: ```tsx title="src/admin/components/onboarding-flow/default/products/products-list.tsx" import React, { useMemo } from "react" import { useAdminCreateProduct, useAdminCreateCollection, useMedusa, } from "medusa-react" import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow" import { Button, Text } from "@medusajs/ui" import getSampleProducts from "../../../../utils/sample-products" import prepareRegions from "../../../../utils/prepare-region" const ProductsListDefault = ({ onNext, isComplete }: StepContentProps) => { const { mutateAsync: createCollection, isLoading: collectionLoading } = useAdminCreateCollection() const { mutateAsync: createProduct, isLoading: productLoading } = useAdminCreateProduct() const { client } = useMedusa() const isLoading = useMemo(() => collectionLoading || productLoading, [collectionLoading, productLoading] ) const createSample = async () => { try { const { collection } = await createCollection({ title: "Merch", handle: "merch", }) const regions = await prepareRegions(client) const sampleProducts = getSampleProducts({ regions, collection_id: collection.id, }) const { product } = await createProduct(sampleProducts[0]) onNext(product) } catch (e) { console.error(e) } } return (
Create a product and set its general details such as title and description, its price, options, variants, images, and more. You'll then use the product to create a sample order. You can create a product by clicking the "New Product" button below. Alternatively, if you're not ready to create your own product, we can create a sample one for you. {!isComplete && (
)}
) } export default ProductsListDefault ```
The `ProductDetailDefault` component is used in the second step of the onboarding's default flow. It shows the user a code snippet to preview the product created in the first step. Create the file `src/admin/components/onboarding-flow/default/products/product-detail.tsx` with the following content: ```tsx title="src/admin/components/onboarding-flow/default/products/product-detail.tsx" import React, { useEffect, useMemo } from "react" import { useAdminPublishableApiKeys, useAdminCreatePublishableApiKey, } from "medusa-react" import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow" import { Button, CodeBlock, Text } from "@medusajs/ui" const ProductDetailDefault = ({ onNext, isComplete, data }: StepContentProps) => { const { publishable_api_keys: keys, isLoading, refetch } = useAdminPublishableApiKeys({ offset: 0, limit: 1, }) const createPublishableApiKey = useAdminCreatePublishableApiKey() const api_key = useMemo(() => keys?.[0]?.id || "", [keys]) const backendUrl = process.env.MEDUSA_BACKEND_URL === "/" || process.env.MEDUSA_ADMIN_BACKEND_URL === "/" ? location.origin : process.env.MEDUSA_BACKEND_URL || process.env.MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000" useEffect(() => { if (!isLoading && !keys?.length) { createPublishableApiKey.mutate({ "title": "Development", }, { onSuccess: () => { refetch() }, }) } }, [isLoading, keys]) return (
On this page, you can view your product's details and edit them. You can preview your product using Medusa's Store APIs. You can copy any of the following code snippets to try it out.
{!isLoading && ( {\n // ...\n const productService = await initializeProductModule()\n const products = await productService.list({\n id: "${data?.product_id}",\n })\n\n console.log(products[0])\n}`, }, ]} className="my-6"> )}
{!isComplete && ( )}
) } export default ProductDetailDefault ```
The `OrdersListDefault` component is used in the third step of the onboarding's default flow. It allows the user to create a sample order. Create the file `src/admin/components/onboarding-flow/default/orders/orders-list.tsx` with the following content: ```tsx title="src/admin/components/onboarding-flow/default/orders/orders-list.tsx" import React from "react" import { useAdminProduct, useAdminCreateDraftOrder, useMedusa, } from "medusa-react" import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow" import { Button, Text } from "@medusajs/ui" import prepareRegions from "../../../../utils/prepare-region" import prepareShippingOptions from "../../../../utils/prepare-shipping-options" const OrdersListDefault = ({ onNext, isComplete, data }: StepContentProps) => { const { product } = useAdminProduct(data.product_id) const { mutateAsync: createDraftOrder, isLoading } = useAdminCreateDraftOrder() const { client } = useMedusa() const createOrder = async () => { const variant = product.variants[0] ?? null try { // check if there is a shipping option and a region // and if not, create demo ones const regions = await prepareRegions(client) const shipping_options = await prepareShippingOptions(client, regions[0]) const { draft_order } = await createDraftOrder({ email: "customer@medusajs.com", items: [ variant ? { quantity: 1, variant_id: variant?.id, } : { quantity: 1, title: product.title, unit_price: 50, }, ], shipping_methods: [ { option_id: shipping_options[0].id, }, ], region_id: regions[0].id, }) const { order } = await client.admin.draftOrders.markPaid(draft_order.id) onNext(order) } catch (e) { console.error(e) } } return ( <>
The last step is to create a sample order using the product you just created. You can then view your order’s details, process its payment, fulfillment, inventory, and more. By clicking the “Create a Sample Order” button, we’ll generate an order using the product you created and default configurations.
{!isComplete && ( )}
) } export default OrdersListDefault ```
The `OrderDetailDefault` component is used in the fourth and final step of the onboarding's default flow. It educates the user on the next steps when developing with Medusa. Create the file `src/admin/components/onboarding-flow/default/orders/order-detail.tsx` with the following content: ```tsx title="src/admin/components/onboarding-flow/default/orders/order-detail.tsx" import React from "react" import { ComputerDesktopSolid, CurrencyDollarSolid, ToolsSolid, } from "@medusajs/icons" import { IconBadge, Heading, Text } from "@medusajs/ui" const OrderDetailDefault = () => { return ( <> You finished the setup guide 🎉 You now have your first order. Feel free to play around with the order management functionalities, such as capturing payment, creating fulfillments, and more. Start developing with Medusa Medusa is a completely customizable commerce solution. We've curated some essential guides to kickstart your development with Medusa.
You can find more useful guides in{" "} our documentation . If you like Medusa, please{" "} star us on GitHub .
) } export default OrderDetailDefault ```
The `ProductsListNextjs` component is used in the first step of the onboarding widget's Next.js flow. It allows the user to create sample products. Create the file `src/admin/components/onboarding-flow/nextjs/products/products-list.tsx` with the following content: ```tsx title="src/admin/components/onboarding-flow/nextjs/products/products-list.tsx" import React from "react" import { useAdminCreateProduct, useAdminCreateCollection, useMedusa, } from "medusa-react" import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow" import { Button, Text } from "@medusajs/ui" import { AdminPostProductsReq, Product } from "@medusajs/medusa" import getSampleProducts from "../../../../utils/sample-products" import prepareRegions from "../../../../utils/prepare-region" const ProductsListNextjs = ({ onNext, isComplete }: StepContentProps) => { const { mutateAsync: createCollection, isLoading: collectionLoading } = useAdminCreateCollection() const { mutateAsync: createProduct, isLoading: productLoading } = useAdminCreateProduct() const { client } = useMedusa() const isLoading = collectionLoading || productLoading const createSample = async () => { try { const { collection } = await createCollection({ title: "Merch", handle: "merch", }) const regions = await prepareRegions(client) const tryCreateProduct = async (sampleProduct: AdminPostProductsReq): Promise => { try { return (await createProduct(sampleProduct)).product } catch { // ignore if product already exists return null } } let product: Product const sampleProducts = getSampleProducts({ regions, collection_id: collection.id, }) await Promise.all( sampleProducts.map(async (sampleProduct, index) => { const createdProduct = await tryCreateProduct(sampleProduct) if (index === 0 && createProduct) { product = createdProduct } }) ) onNext(product) } catch (e) { console.error(e) } } return (
Products in Medusa represent the products you sell. You can set their general details including a title and description. Each product has options and variants, and you can set a price for each variant. Click the button below to create sample products. {!isComplete && (
)}
) } export default ProductsListNextjs ```
The `ProductDetailNextjs` component is used in the second step of the onboarding's Next.js flow. It shows the user a button to preview the product created in the first step using the Next.js starter. Create the file `src/admin/components/onboarding-flow/nextjs/products/product-detail.tsx` with the following content: ```tsx title="src/admin/components/onboarding-flow/nextjs/products/product-detail.tsx" import { useAdminProduct } from "medusa-react" import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow" import { Button, Text } from "@medusajs/ui" const ProductDetailNextjs = ({ onNext, isComplete, data }: StepContentProps) => { const { product, isLoading: productIsLoading } = useAdminProduct(data?.product_id) return (
We have now created a few sample products in your Medusa store. You can scroll down to see what the Product Detail view looks like in the Admin dashboard. This is also the view you use to edit existing products. To view the products in your store, you can visit the Next.js Storefront that was installed with create-medusa-app. The Next.js Storefront Starter is a template that helps you start building an ecommerce store with Medusa. You control the code for the storefront and you can customize it further to fit your specific needs. Click the button below to view the products in your Next.js Storefront. Having trouble? Click{" "} here .
{!isComplete && ( )}
) } export default ProductDetailNextjs ```
The `OrdersListNextjs` component is used in the third step of the onboarding's Next.js flow. It links the user to the checkout flow in the Next.js storefront so that they can create an order. Create the file `src/admin/components/onboarding-flow/nextjs/orders/orders-list.tsx` with the following content: ```tsx title="src/admin/components/onboarding-flow/nextjs/orders/orders-list.tsx" import React from "react" import { useAdminProduct, useCreateCart, useMedusa, } from "medusa-react" import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow" import { Button, Text } from "@medusajs/ui" import prepareRegions from "../../../../utils/prepare-region" import prepareShippingOptions from "../../../../utils/prepare-shipping-options" const OrdersListNextjs = ({ isComplete, data }: StepContentProps) => { const { product } = useAdminProduct(data.product_id) const { mutateAsync: createCart, isLoading: cartIsLoading } = useCreateCart() const { client } = useMedusa() const prepareNextjsCheckout = async () => { const variant = product.variants[0] ?? null try { const regions = await prepareRegions(client) await prepareShippingOptions(client, regions[0]) const { cart } = await createCart({ region_id: regions[0]?.id, items: [ { variant_id: variant?.id, quantity: 1, }, ], }) window.open(`http://localhost:8000/checkout?cart_id=${cart?.id}&onboarding=true`, "_blank") } catch (e) { console.error(e) } } return ( <>
The last step is to create a sample order using one of your products. You can then view your order’s details, process its payment, fulfillment, inventory, and more. You can use the button below to experience hand-first the checkout flow in the Next.js storefront. After placing the order in the storefront, you’ll be directed back here to view the order’s details.
{!isComplete && ( <> )}
) } export default OrdersListNextjs ```
The `OrderDetailNextjs` component is used in the fourth and final step of the onboarding's default flow. It educates the user on the next steps when developing with Medusa. Create the file `src/admin/components/onboarding-flow/nextjs/orders/order-detail.tsx` with the following content: ```tsx title="src/admin/components/onboarding-flow/nextjs/orders/order-detail.tsx" import React from "react" import { CurrencyDollarSolid, NextJs, SquaresPlusSolid } from "@medusajs/icons" import { IconBadge, Heading, Text } from "@medusajs/ui" const OrderDetailNextjs = () => { const queryParams = `?ref=onboarding&type=${ process.env.MEDUSA_ADMIN_ONBOARDING_TYPE || "nextjs" }` return ( <> You finished the setup guide 🎉. You have now a complete ecommerce store with a backend, admin, and a Next.js storefront. Feel free to play around with each of these components to experience all commerce features that Medusa provides. Continue Building your Ecommerce Store Your ecommerce store provides all basic ecommerce features you need to start selling. You can add more functionalities, add plugins for third-party integrations, and customize the storefront’s look and feel to support your use case.
You can find more useful guides in{" "} our documentation . If you like Medusa, please{" "} star us on GitHub .
) } export default OrderDetailNextjs ```
--- ## 5. Test it Out You’ve now implemented everything necessary for the onboarding flow! You can test it out by building the changes and running the `develop` command: ```bash npm2yarn npm run develop ``` If you open the admin at `localhost:7001` and log in, you’ll see the onboarding widget in the Products listing page. You can try using it and see your implementation in action!