--- title: 'Example: How to Create Onboarding Widget' description: 'Learn how to build the onboarding widget available in the admin dashboard the first time you install a Medusa project.' addHowToData: true badge: variant: orange text: beta --- In this guide, you’ll learn how to build the onboarding widget available in the admin dashboard the first time you install a Medusa project. :::note The onboarding widget is already implemented within the codebase of your Medusa backend. This guide is helpful if you want to understand how it was implemented or you want an example of customizing the Medusa admin and backend. ::: ## What you’ll be Building By following this tutorial, 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 [Admin Widgets](./widgets.md). - Keep track of the current step the user has reached by creating a table in the database and an API endpoint that the admin widget uses to retrieve and update the current step. These customizations will be applied to the backend. ![Onboarding Widget Demo](https://res.cloudinary.com/dza7lstvk/image/upload/v1686839259/Medusa%20Docs/Screenshots/onboarding-gif_nalqps.gif) --- ## Prerequisites Before you follow along this tutorial, you must have a Medusa backend installed with the `beta` version of the `@medusajs/admin` package. If not, you can use the following command to get started: ```bash npx create-medusa-app@latest ``` Please refer to the [create-medusa-app documentation](../create-medusa-app) for more details on this command, including prerequisites and troubleshooting. --- ## Preparation Steps The steps in this section are used to prepare for the custom functionalities you’ll be creating in this tutorial. ### (Optional) TypeScript Configurations and package.json If you're using TypeScript in your project, it's highly recommended to setup your TypeScript configurations and package.json as mentioned in [this guide](./widgets.md#optional-typescript-preparations). ### Install Medusa React [Medusa React](../medusa-react/overview) is a React library that facilitates using Medusa’s endpoints within your React application. It also provides the utility to register and use custom endpoints. To install Medusa React and its required dependencies, run the following command in the root directory of the Medusa backend: ```bash npm2yarn npm install medusa-react @tanstack/react-query ``` ### 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.
src/admin/types/icon-type.ts ```tsx title=src/admin/types/icon-type.ts import React from "react" type IconProps = { color?: string size?: string | number } & React.SVGAttributes export default IconProps ```
src/admin/components/shared/icons/check-circle-fill-icon.tsx ```tsx title=src/admin/components/shared/icons/check-circle-fill-icon.tsx import React from "react" import IconProps from "../../../types/icon-type" const CheckCircleFillIcon: React.FC = ({ size = "24", color = "currentColor", ...attributes }) => { return ( ) } export default CheckCircleFillIcon ```
src/admin/components/shared/icons/cross-icon.tsx ```tsx title=src/admin/components/shared/icons/cross-icon.tsx import React from "react" import IconProps from "../../../types/icon-type" const CrossIcon: React.FC = ({ size = "20", color = "currentColor", ...attributes }) => { return ( ) } export default CrossIcon ```
src/admin/components/shared/icons/get-started-icon.tsx ```tsx title=src/admin/components/shared/icons/get-started-icon.tsx import React from "react" import IconProps from "../../../types/icon-type" const GetStartedIcon: React.FC = () => ( ) export default GetStartedIcon ```
src/admin/components/shared/icons/clipboard-copy-icon.tsx ```tsx title=src/admin/components/shared/icons/clipboard-copy-icon.tsx import React from "react" import IconProps from "../../../types/icon-type" const ClipboardCopyIcon: React.FC = ({ size = "20", color = "currentColor", ...attributes }) => { return ( ) } export default ClipboardCopyIcon ```
src/admin/components/shared/icons/computer-desktop-icon.tsx ```tsx title=src/admin/components/shared/icons/computer-desktop-icon.tsx import React from "react" import IconProps from "../../../types/icon-type" const ComputerDesktopIcon: React.FC = ({ size = "24", color = "currentColor", ...attributes }) => { return ( ) } export default ComputerDesktopIcon ```
src/admin/components/shared/icons/dollar-sign-icon.tsx ```tsx title=src/admin/components/shared/icons/dollar-sign-icon.tsx import React from "react" import IconProps from "../../../types/icon-type" const DollarSignIcon: React.FC = ({ size = "24", color = "currentColor", ...attributes }) => { return ( ) } export default DollarSignIcon ```
src/admin/components/shared/accordion.tsx ```tsx title=src/admin/components/shared/accordion.tsx import * as AccordionPrimitive from "@radix-ui/react-accordion" import clsx from "clsx" import React from "react" import CheckCircleFillIcon from "./icons/check-circle-fill-icon" 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 }) => { const headerClass = clsx({ "inter-small-semibold": headingSize === "small", "inter-base-medium": headingSize === "medium", "inter-large-semibold": headingSize === "large", }) const paddingClasses = clsx({ "pb-0 mb-3 pt-3 ": headingSize === "medium", "pb-5 radix-state-open:pb-5xlarge mb-5 ": headingSize === "large", }) return (
{complete ? ( ) : ( )}
{title} {required && *}
{customTrigger || }
{subtitle && ( {subtitle} )}
{description &&

{description}

}
{children}
) } Accordion.Item = Item const MorphingTrigger = () => { return (
) } export default Accordion ```
src/admin/components/shared/spinner.tsx ```tsx title=src/admin/components/shared/spinner.tsx import clsx from "clsx" import React from "react" type SpinnerProps = { size?: "large" | "medium" | "small"; variant?: "primary" | "secondary"; }; const Spinner: React.FC = ({ size = "large", variant = "primary", }) => { return (
) } export default Spinner ```
src/admin/components/shared/button.tsx ```tsx title=src/admin/components/shared/button.tsx import clsx from "clsx" import React, { Children } from "react" import Spinner from "./spinner" export type ButtonProps = { variant: "primary" | "secondary" | "ghost" | "danger" | "nuclear"; size?: "small" | "medium" | "large"; loading?: boolean; } & React.ButtonHTMLAttributes; const Button = React.forwardRef( ( { variant = "primary", size = "large", loading = false, children, ...attributes }, ref ) => { const handleClick = (e) => { if (!loading && attributes.onClick) { attributes.onClick(e) } } const variantClassname = clsx({ ["btn-primary"]: variant === "primary", ["btn-secondary"]: variant === "secondary", ["btn-ghost"]: variant === "ghost", ["btn-danger"]: variant === "danger", ["btn-nuclear"]: variant === "nuclear", }) const sizeClassname = clsx({ ["btn-large"]: size === "large", ["btn-medium"]: size === "medium", ["btn-small"]: size === "small", }) return ( ) } ) export default Button ```
src/admin/components/shared/code-snippets.tsx ```tsx title=src/admin/components/shared/code-snippets.tsx import React, { useState } from "react" import clsx from "clsx" import copy from "copy-to-clipboard" import { Highlight, themes } from "prism-react-renderer" import ClipboardCopyIcon from "./icons/clipboard-copy-icon" import CheckCircleFillIcon from "./icons/check-circle-fill-icon" const CodeSnippets = ({ snippets, }: { snippets: { label: string; language: string; code: string; }[]; }) => { const [active, setActive] = useState(snippets[0]) const [copied, setCopied] = useState(false) const copyToClipboard = () => { setCopied(true) copy(active.code) setTimeout(() => { setCopied(false) }, 3000) } return (
{snippets.map((snippet) => (
setActive(snippet)} > {snippet.label}
))}
{copied ? ( ) : ( )}
{({ style, tokens, getLineProps, getTokenProps }) => (
                {tokens.map((line, i) => (
                  
{line.map((token, key) => ( ))}
))}
)}
) } export default CodeSnippets ```
src/admin/components/shared/container.tsx ```tsx title=src/admin/components/shared/container.tsx import React, { PropsWithChildren } from "react" type Props = PropsWithChildren<{ title?: string; description?: string; }>; export const Container = ({ title, description, children }: Props) => { return (
{title && (

{title}

)}
{description && (

{description}

)}
{children}
) } ```
src/admin/components/shared/badge.tsx ```tsx title=src/admin/components/shared/badge.tsx import clsx from "clsx" import React from "react" type BadgeProps = { variant: | "primary" | "danger" | "success" | "warning" | "ghost" | "default" | "disabled" | "new-feature" } & React.HTMLAttributes const Badge: React.FC = ({ children, variant, onClick, className, ...props }) => { const variantClassname = clsx({ ["badge-primary"]: variant === "primary", ["badge-danger"]: variant === "danger", ["badge-success"]: variant === "success", ["badge-warning"]: variant === "warning", ["badge-ghost"]: variant === "ghost", ["badge-default"]: variant === "default", ["badge-disabled"]: variant === "disabled", ["bg-blue-10 border-blue-30 border font-normal text-blue-50"]: variant === "new-feature", }) return (
{children}
) } export default Badge ```
src/admin/components/shared/icon-badge.tsx ```tsx title=src/admin/components/shared/icon-badge.tsx import clsx from "clsx" import React from "react" import Badge from "./badge" type IconBadgeProps = { variant?: | "primary" | "danger" | "success" | "warning" | "ghost" | "default" | "disabled"; } & React.HTMLAttributes; const IconBadge: React.FC = ({ children, variant, className, ...rest }) => { return ( {children} ) } export default IconBadge ```
--- ## Step 1: Customize Medusa Backend :::note If you’re not interested in learning about backend customizations, you can skip to [step 2](#step-2-create-onboarding-widget). ::: In this step, you’ll customize the Medusa backend 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 a new endpoint that allows to retrieve and update the current onboarding step. This requires creating a new service and endpoint. ### Create Entity An [entity](../development/entities/overview.mdx) represents a table in the database. It’s based on Typeorm, so it requires creating a repository and a migration to be used in the backend. To create the entity, 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 } ``` Then, create the file `src/repositories/onboarding.ts` that holds the repository of the entity with the following content: ```ts title=src/repositories/onboarding.ts import { dataSource, } from "@medusajs/medusa/dist/loaders/database" import { OnboardingState } from "../models/onboarding" const OnboardingRepository = dataSource.getRepository( OnboardingState ) export default OnboardingRepository ``` You can learn more about entities and repositories in [this documentation](../development/entities/overview.mdx). ### Create Migration A [migration](../development/entities/migrations/overview.mdx) is used to reflect database changes in your database schema. To create a migration, run the following command in the root of your Medusa backend: ```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"`) } } ``` :::warning 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 @medusajs/medusa-cli migrations run ``` You can learn more about migrations in [this guide](../development/entities/migrations/overview.mdx). ### Create Service A [service](../development/services/overview.mdx) is a class that holds helper methods related to an entity. For example, methods to create or retrieve a record of that entity. Services are used by other resources, such as endpoints, to perform functionalities related to an entity. So, before you add the endpoints that allow retrieving and updating the onboarding state, you need to add the service that implements these helper functionalities. 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. You can learn more about services in [this documentation](../development/services/overview.mdx). ### Create Endpoint The last part of this step is to create the [endpoints](../development/endpoints/overview) that you’ll consume in the admin widget. There will be two endpoints: Get Onboarding State and Update Onboarding State. To add the Get Onboarding State endpoint, create the file `src/api/routes/admin/onboarding/get-status.ts` with the following content: ```ts title=src/api/routes/admin/onboarding/get-status.ts import { Request, Response } from "express" import OnboardingService from "../../../../services/onboarding" export default async function getOnboardingStatus( req: Request, res: Response ) { const onboardingService: OnboardingService = req.scope.resolve("onboardingService") const status = await onboardingService.retrieve() res.status(200).json({ status }) } ``` Notice how this endpoint uses the `OnboardingService`'s `retrieve` method to retrieve the current onboarding state. It resolves the `OnboardingService` using the [Dependency Container.](../development/fundamentals/dependency-injection.md) To add the Update Onboarding State, create the file `src/api/routes/admin/onboarding/update-status.ts` with the following content: ```ts title=src/api/routes/admin/onboarding/update-status.ts import { Request, Response } from "express" import { EntityManager } from "typeorm" import OnboardingService from "../../../../services/onboarding" export default async function updateOnboardingStatus( req: Request, res: Response ) { 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 }) } ``` Notice how this endpoint uses the `OnboardingService`'s `update` method to update the current onboarding state. After creating the endpoints, you need to register them in a router and export the router for the Medusa core to load. To do that, start by creating the file `src/api/routes/admin/onboarding/index.ts` with the following content: ```ts title=src/api/routes/admin/onboarding/index.ts import { wrapHandler } from "@medusajs/utils" import { Router } from "express" import getOnboardingStatus from "./get-status" import updateOnboardingStatus from "./update-status" const router = Router() export default (adminRouter: Router) => { adminRouter.use("/onboarding", router) router.get("/", wrapHandler(getOnboardingStatus)) router.post("/", wrapHandler(updateOnboardingStatus)) } ``` This file creates a router that registers the Get Onboarding State and Update Onboarding State endpoints. Next, create or change the content of the file `src/api/routes/admin/index.ts` to the following: ```ts title=src/api/routes/admin/index.ts import { Router } from "express" import { wrapHandler } from "@medusajs/medusa" import onboardingRoutes from "./onboarding" import customRouteHandler from "./custom-route-handler" // Initialize a custom router const router = Router() export function attachAdminRoutes(adminRouter: Router) { // Attach our router to a custom path on the admin router adminRouter.use("/custom", router) // Define a GET endpoint on the root route of our custom path router.get("/", wrapHandler(customRouteHandler)) // Attach routes for onboarding experience, defined separately onboardingRoutes(adminRouter) } ``` This file exports the router created in `src/api/routes/admin/onboarding/index.ts`. Finally, create or change the content of the file `src/api/index.ts` to the following content: ```ts title=src/api/index.ts import { Router } from "express" import cors from "cors" import bodyParser from "body-parser" import { authenticate, ConfigModule } from "@medusajs/medusa" import { getConfigFile } from "medusa-core-utils" import { attachStoreRoutes } from "./routes/store" import { attachAdminRoutes } from "./routes/admin" export default (rootDirectory: string): Router | Router[] => { // Read currently-loaded medusa config const { configModule } = getConfigFile( rootDirectory, "medusa-config" ) const { projectConfig } = configModule // Set up our CORS options objects, based on config const storeCorsOptions = { origin: projectConfig.store_cors.split(","), credentials: true, } const adminCorsOptions = { origin: projectConfig.admin_cors.split(","), credentials: true, } // Set up express router const router = Router() // Set up root routes for store and admin endpoints, // with appropriate CORS settings router.use( "/store", cors(storeCorsOptions), bodyParser.json() ) router.use( "/admin", cors(adminCorsOptions), bodyParser.json() ) // Add authentication to all admin routes *except* // auth and account invite ones router.use( /\/admin\/((?!auth)(?!invites).*)/, authenticate() ) // Set up routers for store and admin endpoints const storeRouter = Router() const adminRouter = Router() // Attach these routers to the root routes router.use("/store", storeRouter) router.use("/admin", adminRouter) // Attach custom routes to these routers attachStoreRoutes(storeRouter) attachAdminRoutes(adminRouter) return router } ``` This is the file that the Medusa core loads the endpoints from. In this file, you export a router that registers store and admin endpoints, including the `onboarding` endpoints you just added. You can learn more about endpoints in [this documentation](../development/endpoints/overview.mdx). --- ## Step 2: 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 tutorial. 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 React, { useState, useEffect, } from "react" import { Container, } from "../../components/shared/container" import Button from "../../components/shared/button" import { WidgetConfig, } from "@medusajs/admin" import Accordion from "../../components/shared/accordion" import GetStartedIcon from "../../components/shared/icons/get-started-icon" import { OnboardingState, } from "../../../models/onboarding" import { useNavigate, } from "react-router-dom" import { AdminOnboardingUpdateStateReq, OnboardingStateRes, UpdateOnboardingStateInput, } from "../../../types/onboarding" type STEP_ID = | "create_product" | "preview_product" | "create_order" | "setup_finished"; export type StepContentProps = any & { onNext?: Function; isComplete?: boolean; data?: OnboardingState; } & any; type Step = { id: STEP_ID; title: string; component: React.FC; onNext?: Function; }; const STEP_FLOW: STEP_ID[] = [ "create_product", "preview_product", "create_order", "setup_finished", ] const OnboardingFlow = (props: any) => { const navigate = useNavigate() // TODO change based on state in backend const currentStep: STEP_ID | undefined = "create_product" as STEP_ID const [openStep, setOpenStep] = useState(currentStep) const [completed, setCompleted] = useState(false) useEffect(() => { setOpenStep(currentStep) if (currentStep === STEP_FLOW[STEP_FLOW.length - 1]) {setCompleted(true)} }, [currentStep]) const updateServerState = (payload: any) => { // TODO update state in the backend } const onStart = () => { // TODO update state in the backend navigate(`/a/products`) } const setStepComplete = ({ step_id, extraData, onComplete, }: { step_id: STEP_ID; extraData?: UpdateOnboardingStateInput; onComplete?: () => void; }) => { // TODO update state in the backend } const goToProductView = (product: any) => { setStepComplete({ step_id: "create_product", extraData: { product_id: product.id }, onComplete: () => navigate(`/a/products/${product.id}`), }) } const goToOrders = () => { setStepComplete({ step_id: "preview_product", onComplete: () => navigate(`/a/orders`), }) } const goToOrderView = (order: any) => { setStepComplete({ step_id: "create_order", onComplete: () => navigate(`/a/orders/${order.id}`), }) } const onComplete = () => { setCompleted(true) } const onHide = () => { updateServerState({ is_complete: true }) } // TODO add steps const Steps: Step[] = [] const isStepComplete = (step_id: STEP_ID) => STEP_FLOW.indexOf(currentStep) > STEP_FLOW.indexOf(step_id) return ( <> setOpenStep(value as STEP_ID)} >
{!completed ? ( <>

Get started

Learn the basics of Medusa by creating your first order.

{currentStep ? ( <> {currentStep === STEP_FLOW[STEP_FLOW.length - 1] ? ( ) : ( )} ) : ( <> )}
) : ( <>

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, index) => { 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 ``` 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 backend. Some parts are left as `TODO` until you add the components for each step, and you implement customizations in the backend. You can learn more about Admin Widgets in [this documentation](./widgets.md). --- ## Step 3: 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.
ProductsList component The `ProductsList` component is used in the first step of the onboarding widget. It allows the user to either open the Create Product modal or create a sample product. Create the file `src/admin/components/onboarding-flow/products/products-list.tsx` with the following content: ```tsx title=src/admin/components/onboarding-flow/products/products-list.tsx import React from "react" import Button from "../../shared/button" import { useAdminCreateProduct, useAdminCreateCollection, } from "medusa-react" import { useAdminRegions, } from "medusa-react" import { StepContentProps, } from "../../../widgets/onboarding-flow/onboarding-flow" enum ProductStatus { PUBLISHED = "published", } const ProductsList = ({ onNext, isComplete }: StepContentProps) => { const { mutateAsync: createCollection, isLoading: collectionLoading } = useAdminCreateCollection() const { mutateAsync: createProduct, isLoading: productLoading } = useAdminCreateProduct() const { regions } = useAdminRegions() const isLoading = collectionLoading || productLoading const createSample = async () => { try { const { collection } = await createCollection({ title: "Merch", handle: "merch", }) const { product } = await createProduct({ title: "Medusa T-Shirt", description: "Comfy t-shirt with Medusa logo", subtitle: "Black", is_giftcard: false, discountable: false, options: [{ title: "Size" }], images: [ "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", ], collection_id: collection.id, variants: [ { title: "Small", inventory_quantity: 25, manage_inventory: true, prices: regions.map((region) => ({ amount: 5000, currency_code: region.currency_code, })), options: [{ value: "S" }], }, { title: "Medium", inventory_quantity: 10, manage_inventory: true, prices: regions.map((region) => ({ amount: 5000, currency_code: region.currency_code, })), options: [{ value: "M" }], }, { title: "Large", inventory_quantity: 17, manage_inventory: true, prices: regions.map((region) => ({ amount: 5000, currency_code: region.currency_code, })), options: [{ value: "L" }], }, { title: "Extra Large", inventory_quantity: 22, manage_inventory: true, prices: regions.map((region) => ({ amount: 5000, currency_code: region.currency_code, })), options: [{ value: "XL" }], }, ], status: ProductStatus.PUBLISHED, }) 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.

If you're not ready to create a product, we can create a sample product for you.

{!isComplete && (
)}
) } export default ProductsList ```
ProductDetail component The `ProductDetail` component is used in the second step of the onboarding. It shows the user a code snippet to preview the product they created in the first step. Create the file `src/admin/components/onboarding-flow/products/product-detail.tsx` with the following content: ```tsx title=src/admin/components/onboarding-flow/products/product-detail.tsx import React from "react" import { useAdminPublishableApiKeys, } from "medusa-react" import Button from "../../shared/button" import CodeSnippets from "../../shared/code-snippets" import { StepContentProps, } from "../../../widgets/onboarding-flow/onboarding-flow" const ProductDetail = ({ onNext, isComplete, data }: StepContentProps) => { const { publishable_api_keys: keys, isLoading, } = useAdminPublishableApiKeys({ offset: 0, limit: 1, }) const api_key = keys?.[0]?.id || "pk_01H0PY648BTMEJR34ZDATXZTD9" 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 && ( )}
{!isComplete && ( )}
) } export default ProductDetail ```
OrdersList component The `OrdersList` component is used in the third step of the onboarding. It allows the user to create a sample order. Create the file `src/admin/components/onboarding-flow/orders/orders-list.tsx` with the following content: ```tsx title=src/admin/components/onboarding-flow/orders/orders-list.tsx import React from "react" import Button from "../../shared/button" import { useAdminProduct, } from "medusa-react" import { useAdminCreateDraftOrder, } from "medusa-react" import { useAdminShippingOptions, } from "medusa-react" import { useAdminRegions, } from "medusa-react" import { useMedusa, } from "medusa-react" import { StepContentProps, } from "../../../widgets/onboarding-flow/onboarding-flow" const OrdersList = ({ onNext, isComplete, data }: StepContentProps) => { const { product } = useAdminProduct(data.product_id) const { mutateAsync: createDraftOrder, isLoading } = useAdminCreateDraftOrder() const { client } = useMedusa() const { regions } = useAdminRegions() const { shipping_options } = useAdminShippingOptions() const createOrder = async () => { const variant = product.variants[0] ?? null try { 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 ( <>

With a Product created, we can now place an Order. Click the button below to create a sample order.

{!isComplete && ( )}
) } export default OrdersList ```
OrderDetail component The `OrderDetail` component is used in the fourth and final step of the onboarding. It educates the user on the next steps when developing with Medusa. Create the file `src/admin/components/onboarding-flow/orders/order-detail.tsx` with the following content: ```tsx title=src/admin/components/onboarding-flow/orders/order-detail.tsx import React from "react" import IconBadge from "../../shared/icon-badge" import ComputerDesktopIcon from "../../shared/icons/computer-desktop-icon" import DollarSignIcon from "../../shared/icons/dollar-sign-icon" const OrderDetail = () => { 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 .
) } export default OrderDetail ```
After creating the above components, import them at the top of `src/admin/widgets/onboarding-flow/onboarding-flow.tsx`: ```tsx title=src/admin/widgets/onboarding-flow/onboarding-flow.tsx import ProductsList from "../../components/onboarding-flow/products/products-list" import ProductDetail from "../../components/onboarding-flow/products/product-detail" import OrdersList from "../../components/onboarding-flow/orders/orders-list" import OrderDetail from "../../components/onboarding-flow/orders/order-detail" ``` Then, replace the initialization of the `Steps` variable within the `OnboardingFlow` widget with the following: ```tsx title=src/admin/widgets/onboarding-flow/onboarding-flow.tsx // TODO add steps const Steps: Step[] = [ { id: "create_product", title: "Create Product", component: ProductsList, onNext: goToProductView, }, { id: "preview_product", title: "Preview Product", component: ProductDetail, onNext: goToOrders, }, { id: "create_order", title: "Create an Order", component: OrdersList, onNext: goToOrderView, }, { id: "setup_finished", title: "Setup Finished: Start developing with Medusa", component: OrderDetail, }, ] ``` The next step is to retrieve the current step of the onboarding flow from the Medusa backend. --- ## Step 4: Use Endpoints From Widget In this section, you’ll implement the `TODO`s in the `OnboardingFlow` that require communicating with the backend. There are different ways you can consume custom backend endpoints. The Medusa React library provides a utility method `createCustomAdminHooks` that allows you to create a hook similar to those available by default in the library. You can then utilize these hooks to send requests to custom backend endpoints. Create the file `src/admin/components/shared/hooks.tsx` that exports custom hooks for the custom endpoints you created in the previous step: ```tsx title=src/admin/components/shared/hooks.tsx import { createCustomAdminHooks } from "medusa-react" const { useAdminEntity: useAdminOnboardingState, useAdminUpdateMutation: useAdminUpdateOnboardingStateMutation, } = createCustomAdminHooks("onboarding", "onboarding_state") export { useAdminOnboardingState, useAdminUpdateOnboardingStateMutation, } ``` You can now use `useAdminOnboardingState` to retrieve the onboarding state from the backend, and `useAdminUpdateOnboardingStateMutation` to update the onboarding state in the backend. Learn more about Medusa React in [this documentation](../medusa-react/overview.md). Then, add the following imports at the top of `src/admin/widgets/onboarding-flow/onboarding-flow.tsx`: ```tsx title=src/admin/widgets/onboarding-flow/onboarding-flow.tsx import { useAdminOnboardingState, useAdminUpdateOnboardingStateMutation, } from "../../components/shared/hooks" ``` Next, add the following at the top of the `OnboardingFlow` component: ```tsx title=src/admin/widgets/onboarding-flow/onboarding-flow.tsx const OnboardingFlow = (props: any) => { const { data, isLoading, } = useAdminOnboardingState("") const { mutate } = useAdminUpdateOnboardingStateMutation< AdminOnboardingUpdateStateReq, OnboardingStateRes >("") // ... } ``` `data` now holds the current onboarding state from the backend, and `mutate` can be used to update the onboarding state in the backend. After that, replace the declarations within `OnboardingFlow` that had a `TODO` comment with the following: ```tsx title=src/admin/widgets/onboarding-flow/onboarding-flow.tsx const OnboardingFlow = (props: ExtensionProps) => { // ... const currentStep: STEP_ID | undefined = data?.status ?.current_step as STEP_ID if ( !isLoading && data?.status?.is_complete && !localStorage.getItem("override_onboarding_finish") ) {return null} const updateServerState = ( payload: UpdateOnboardingStateInput, onSuccess: () => void = () => {} ) => { mutate(payload, { onSuccess }) } const onStart = () => { updateServerState({ current_step: STEP_FLOW[0] }) navigate(`/a/products`) } const setStepComplete = ({ step_id, extraData, onComplete, }: { step_id: STEP_ID; extraData?: UpdateOnboardingStateInput; onComplete?: () => void; }) => { const next = STEP_FLOW[ STEP_FLOW.findIndex((step) => step === step_id) + 1 ] updateServerState({ current_step: next, ...extraData, }, onComplete) } // ... } ``` `currentStep` now holds the current step retrieve from the backend; `updateServerState` updates the current step in the backend; `onStart` updates the current step in the backend to the first step; and `setStepComplete` completes the current step by updating the current step in the backend to the following step. Finally, in the returned JSX, update the `TODO` in the `` component to pass the component the necessary `data`: ```tsx title=src/admin/widgets/onboarding-flow/onboarding-flow.tsx ``` --- ## Step 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 build npx @medusajs/medusa-cli 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! --- ## Next Steps: Continue Development - [Learn more about Admin Widgets](./widgets.md) - [Learn how you can start custom development in your backend](../recipes/index.mdx)