Initial commit

main
duc1607 2 years ago
commit d805cefeae

@ -0,0 +1,12 @@
let ignore = [`**/dist`]
// Jest needs to compile this code, but generally we don't want this copied
// to output folders
if (process.env.NODE_ENV !== `test`) {
ignore.push(`**/__tests__`)
}
module.exports = {
presets: [["babel-preset-medusa-package"], ["@babel/preset-typescript"]],
ignore,
}

@ -0,0 +1,5 @@
JWT_SECRET=something
COOKIE_SECRET=something
DATABASE_TYPE="postgres"
REDIS_URL=redis://localhost:6379

@ -0,0 +1,21 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-type: production
groups:
medusa:
patterns:
- "@medusajs*"
- "medusa*"
update-types:
- "minor"
- "patch"
ignore:
- dependency-name: "@medusajs*"
update-types: ["version-update:semver-major"]
- dependency-name: "medusa*"
update-types: ["version-update:semver-major"]

27
.gitignore vendored

@ -0,0 +1,27 @@
/dist
.env
.DS_Store
/uploads
/node_modules
yarn-error.log
.idea
.vscode
coverage
!src/**
./tsconfig.tsbuildinfo
package-lock.json
yarn.lock
medusa-db.sql
build
.cache
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

@ -0,0 +1 @@
nodeLinker: node-modules

@ -0,0 +1,70 @@
<p align="center">
<a href="https://www.medusajs.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/59018053/229103275-b5e482bb-4601-46e6-8142-244f531cebdb.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
<img alt="Medusa logo" src="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
</picture>
</a>
</p>
<h1 align="center">
Medusa
</h1>
<h4 align="center">
<a href="https://docs.medusajs.com">Documentation</a> |
<a href="https://www.medusajs.com">Website</a>
</h4>
<p align="center">
Building blocks for digital commerce
</p>
<p align="center">
<a href="https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs welcome!" />
</a>
<a href="https://www.producthunt.com/posts/medusa"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Day-%23DA552E" alt="Product Hunt"></a>
<a href="https://discord.gg/xpCwq3Kfn8">
<img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" />
</a>
<a href="https://twitter.com/intent/follow?screen_name=medusajs">
<img src="https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs" alt="Follow @medusajs" />
</a>
</p>
## Compatibility
This starter is compatible with versions >= 1.8.0 of `@medusajs/medusa`.
## Getting Started
Visit the [Quickstart Guide](https://docs.medusajs.com/create-medusa-app) to set up a server.
Visit the [Docs](https://docs.medusajs.com/development/backend/prepare-environment) to learn more about our system requirements.
## What is Medusa
Medusa is a set of commerce modules and tools that allow you to build rich, reliable, and performant commerce applications without reinventing core commerce logic. The modules can be customized and used to build advanced ecommerce stores, marketplaces, or any product that needs foundational commerce primitives. All modules are open-source and freely available on npm.
Learn more about [Medusas architecture](https://docs.medusajs.com/development/fundamentals/architecture-overview) and [commerce modules](https://docs.medusajs.com/modules/overview) in the Docs.
## Roadmap, Upgrades & Plugins
You can view the planned, started and completed features in the [Roadmap discussion](https://github.com/medusajs/medusa/discussions/categories/roadmap).
Follow the [Upgrade Guides](https://docs.medusajs.com/upgrade-guides/) to keep your Medusa project up-to-date.
Check out all [available Medusa plugins](https://medusajs.com/plugins/).
## Community & Contributions
The community and core team are available in [GitHub Discussions](https://github.com/medusajs/medusa/discussions), where you can ask for support, discuss roadmap, and share ideas.
Join our [Discord server](https://discord.com/invite/medusajs) to meet other community members.
## Other channels
- [GitHub Issues](https://github.com/medusajs/medusa/issues)
- [Twitter](https://twitter.com/medusajs)
- [LinkedIn](https://www.linkedin.com/company/medusajs)
- [Medusa Blog](https://medusajs.com/blog/)

@ -0,0 +1,141 @@
{
"store": {
"currencies": [
"eur",
"usd"
]
},
"users": [],
"regions": [
{
"id": "test-region-eu",
"name": "EU",
"currency_code": "eur",
"tax_rate": 0,
"payment_providers": [
"manual"
],
"fulfillment_providers": [
"manual"
],
"countries": [
"gb",
"de",
"dk",
"se",
"fr",
"es",
"it"
]
},
{
"id": "test-region-na",
"name": "NA",
"currency_code": "usd",
"tax_rate": 0,
"payment_providers": [
"manual"
],
"fulfillment_providers": [
"manual"
],
"countries": [
"us",
"ca"
]
}
],
"shipping_options": [
{
"name": "PostFake Standard",
"region_id": "test-region-eu",
"provider_id": "manual",
"data": {
"id": "manual-fulfillment"
},
"price_type": "flat_rate",
"amount": 1000
},
{
"name": "PostFake Express",
"region_id": "test-region-eu",
"provider_id": "manual",
"data": {
"id": "manual-fulfillment"
},
"price_type": "flat_rate",
"amount": 1500
},
{
"name": "PostFake Return",
"region_id": "test-region-eu",
"provider_id": "manual",
"data": {
"id": "manual-fulfillment"
},
"price_type": "flat_rate",
"is_return": true,
"amount": 1000
},
{
"name": "I want to return it myself",
"region_id": "test-region-eu",
"provider_id": "manual",
"data": {
"id": "manual-fulfillment"
},
"price_type": "flat_rate",
"is_return": true,
"amount": 0
},
{
"name": "FakeEx Standard",
"region_id": "test-region-na",
"provider_id": "manual",
"data": {
"id": "manual-fulfillment"
},
"price_type": "flat_rate",
"amount": 800
},
{
"name": "FakeEx Express",
"region_id": "test-region-na",
"provider_id": "manual",
"data": {
"id": "manual-fulfillment"
},
"price_type": "flat_rate",
"amount": 1200
},
{
"name": "FakeEx Return",
"region_id": "test-region-na",
"provider_id": "manual",
"data": {
"id": "manual-fulfillment"
},
"price_type": "flat_rate",
"is_return": true,
"amount": 800
},
{
"name": "I want to return it myself",
"region_id": "test-region-na",
"provider_id": "manual",
"data": {
"id": "manual-fulfillment"
},
"price_type": "flat_rate",
"is_return": true,
"amount": 0
}
],
"products": [],
"categories": [],
"publishable_api_keys": [
{
"title": "Development"
}
]
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,50 @@
const express = require("express")
const { GracefulShutdownServer } = require("medusa-core-utils")
const loaders = require("@medusajs/medusa/dist/loaders/index").default
;(async() => {
async function start() {
const app = express()
const directory = process.cwd()
try {
const { container } = await loaders({
directory,
expressApp: app
})
const configModule = container.resolve("configModule")
const port = process.env.PORT ?? configModule.projectConfig.port ?? 9000
const server = GracefulShutdownServer.create(
app.listen(port, (err) => {
if (err) {
return
}
console.log(`Server is ready on port: ${port}`)
})
)
// Handle graceful shutdown
const gracefulShutDown = () => {
server
.shutdown()
.then(() => {
console.info("Gracefully stopping the server.")
process.exit(0)
})
.catch((e) => {
console.error("Error received when shutting down the server.", e)
process.exit(1)
})
}
process.on("SIGTERM", gracefulShutDown)
process.on("SIGINT", gracefulShutDown)
} catch (err) {
console.error("Error starting server", err)
process.exit(1)
}
}
await start()
})()

@ -0,0 +1,88 @@
const dotenv = require("dotenv");
let ENV_FILE_NAME = "";
switch (process.env.NODE_ENV) {
case "production":
ENV_FILE_NAME = ".env.production";
break;
case "staging":
ENV_FILE_NAME = ".env.staging";
break;
case "test":
ENV_FILE_NAME = ".env.test";
break;
case "development":
default:
ENV_FILE_NAME = ".env";
break;
}
try {
dotenv.config({ path: process.cwd() + "/" + ENV_FILE_NAME });
} catch (e) {}
// CORS when consuming Medusa from admin
const ADMIN_CORS =
process.env.ADMIN_CORS || "http://localhost:7000,http://localhost:7001";
// CORS to avoid issues when consuming Medusa from a client
const STORE_CORS = process.env.STORE_CORS || "http://localhost:8000";
const DATABASE_URL =
process.env.DATABASE_URL || "postgres://localhost/medusa-starter-default";
const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
const plugins = [
`medusa-fulfillment-manual`,
`medusa-payment-manual`,
{
resolve: `@medusajs/file-local`,
options: {
upload_dir: "uploads",
},
},
{
resolve: "@medusajs/admin",
/** @type {import('@medusajs/admin').PluginOptions} */
options: {
autoRebuild: true,
develop: {
open: process.env.OPEN_BROWSER !== "false",
},
},
},
];
const modules = {
/*eventBus: {
resolve: "@medusajs/event-bus-redis",
options: {
redisUrl: REDIS_URL
}
},
cacheService: {
resolve: "@medusajs/cache-redis",
options: {
redisUrl: REDIS_URL
}
},*/
};
/** @type {import('@medusajs/medusa').ConfigModule["projectConfig"]} */
const projectConfig = {
jwtSecret: process.env.JWT_SECRET,
cookieSecret: process.env.COOKIE_SECRET,
store_cors: STORE_CORS,
database_url: DATABASE_URL,
admin_cors: ADMIN_CORS,
// Uncomment the following lines to enable REDIS
// redis_url: REDIS_URL
};
/** @type {import('@medusajs/medusa').ConfigModule} */
module.exports = {
projectConfig,
plugins,
modules,
};

@ -0,0 +1,90 @@
{
"name": "medusa-starter-default",
"version": "0.0.1",
"description": "A starter for Medusa projects.",
"author": "Medusa (https://medusajs.com)",
"license": "MIT",
"keywords": [
"sqlite",
"postgres",
"typescript",
"ecommerce",
"headless",
"medusa"
],
"scripts": {
"clean": "cross-env ./node_modules/.bin/rimraf dist",
"build": "cross-env npm run clean && npm run build:server && npm run build:admin",
"build:server": "cross-env npm run clean && tsc -p tsconfig.server.json",
"build:admin": "cross-env medusa-admin build",
"watch": "cross-env tsc --watch",
"test": "cross-env jest",
"seed": "cross-env medusa seed -f ./data/seed.json",
"start": "cross-env npm run build && medusa start",
"start:custom": "cross-env npm run build && node --preserve-symlinks --trace-warnings index.js",
"dev": "cross-env npm run build:server && medusa develop"
},
"dependencies": {
"@medusajs/admin": "7.1.8",
"@medusajs/cache-inmemory": "^1.8.9",
"@medusajs/cache-redis": "^1.8.9",
"@medusajs/event-bus-local": "^1.9.7",
"@medusajs/event-bus-redis": "^1.8.10",
"@medusajs/file-local": "^1.0.2",
"@medusajs/medusa": "1.18.1",
"@tanstack/react-query": "4.22.0",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "16.3.1",
"express": "^4.17.2",
"medusa-fulfillment-manual": "^1.1.38",
"medusa-interfaces": "^1.3.7",
"medusa-payment-manual": "^1.0.24",
"medusa-payment-stripe": "^6.0.5",
"prism-react-renderer": "^2.0.4",
"typeorm": "^0.3.16"
},
"devDependencies": {
"@babel/cli": "^7.14.3",
"@babel/core": "^7.14.3",
"@babel/preset-typescript": "^7.21.4",
"@medusajs/medusa-cli": "^1.3.21",
"@types/express": "^4.17.13",
"@types/jest": "^27.4.0",
"@types/node": "^17.0.8",
"babel-preset-medusa-package": "^1.1.19",
"cross-env": "^7.0.3",
"eslint": "^6.8.0",
"jest": "^27.3.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.0.7",
"ts-loader": "^9.2.6",
"typescript": "^4.5.2"
},
"jest": {
"globals": {
"ts-jest": {
"tsconfig": "tsconfig.spec.json"
}
},
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"testPathIgnorePatterns": [
"/node_modules/",
"<rootDir>/node_modules/"
],
"rootDir": "src",
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$",
"transform": {
".ts": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "./coverage",
"testEnvironment": "node"
}
}

@ -0,0 +1,136 @@
import React from "react";
import {
ComputerDesktopSolid,
CurrencyDollarSolid,
NextJs
} from "@medusajs/icons";
import { IconBadge, Heading, Text } from "@medusajs/ui";
const OrderDetailDefault = () => {
return (
<>
<Text size="small" className="mb-6">
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.
</Text>
<Heading
level="h2"
className="text-ui-fg-base pt-6 border-t border-ui-border-base border-solid mb-2"
>
Start developing with Medusa
</Heading>
<Text size="small">
Medusa is a completely customizable commerce solution. We've curated
some essential guides to kickstart your development with Medusa.
</Text>
<div className="grid grid-cols-3 gap-4 mt-6 pb-6 mb-6 border-b border-ui-border-base border-solid auto-rows-fr">
<a
href={`https://docs.medusajs.com/modules/overview?ref=onboarding`}
target="_blank"
className="flex"
>
<div className="p-3 rounded-rounded items-start flex bg-ui-bg-subtle shadow-elevation-card-rest hover:shadow-elevation-card-hover">
<div className="mr-4">
<div className="bg-ui-bg-base rounded-lg border border-ui-border-strong p-1 flex justify-center items-center">
<IconBadge>
<CurrencyDollarSolid />
</IconBadge>
</div>
</div>
<div>
<Text
size="xsmall"
weight="plus"
className="mb-1 text-ui-fg-base"
>
Add Commerce Features
</Text>
<Text size="small">
Learn about all available commerce features and how to
add them in your storefront
</Text>
</div>
</div>
</a>
<a
href="https://docs.medusajs.com/recipes/?ref=onboarding"
target="_blank"
className="flex"
>
<div className="p-3 rounded-rounded items-start flex bg-ui-bg-subtle shadow-elevation-card-rest hover:shadow-elevation-card-hover">
<div className="mr-4">
<div className="bg-ui-bg-base rounded-lg border border-ui-border-strong p-1 flex justify-center items-center">
<IconBadge>
<ComputerDesktopSolid />
</IconBadge>
</div>
</div>
<div>
<Text
size="xsmall"
weight="plus"
className="mb-1 text-ui-fg-base"
>
Build Custom Use Cases
</Text>
<Text size="small">
Build a marketplace, subscription-based purchases,
or your custom use-cases.
</Text>
</div>
</div>
</a>
<a
href={`https://docs.medusajs.com/starters/nextjs-medusa-starter?ref=onboarding`}
target="_blank"
className="flex"
>
<div className="p-3 rounded-rounded items-start flex bg-ui-bg-subtle shadow-elevation-card-rest hover:shadow-elevation-card-hover">
<div className="mr-4">
<div className="bg-ui-bg-base rounded-lg border border-ui-border-strong p-1 flex justify-center items-center">
<IconBadge>
<NextJs />
</IconBadge>
</div>
</div>
<div>
<Text
size="xsmall"
weight="plus"
className="mb-1 text-ui-fg-base"
>
Install Next.js Quickstart
</Text>
<Text size="small">
Install and use the Next.js storefront with
your commerce store.
</Text>
</div>
</div>
</a>
</div>
<div>
You can find more useful guides in{" "}
<a
href="https://docs.medusajs.com/?ref=onboarding"
target="_blank"
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
>
our documentation
</a>
. If you like Medusa, please{" "}
<a
href="https://github.com/medusajs/medusa"
target="_blank"
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
>
star us on GitHub
</a>
.
</div>
</>
);
};
export default OrderDetailDefault;

@ -0,0 +1,81 @@
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 (
<>
<div className="mb-6">
<Text className="mb-2">
The last step is to create a sample order using the product you just created. You can then view your orders details, process its payment, fulfillment, inventory, and more.
</Text>
<Text>
By clicking the Create a Sample Order button, well generate an order using the product you created and default configurations.
</Text>
</div>
<div className="flex gap-2">
{!isComplete && (
<Button
variant="primary"
size="base"
onClick={() => createOrder()}
isLoading={isLoading}
>
Create a sample order
</Button>
)}
</div>
</>
);
};
export default OrdersListDefault;

@ -0,0 +1,90 @@
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://location:9000"
useEffect(() => {
if (!isLoading && !keys?.length) {
createPublishableApiKey.mutate({
"title": "Development"
}, {
onSuccess: () => {
refetch()
}
})
}
}, [isLoading, keys])
return (
<div>
<div className="flex flex-col gap-2">
<Text>On this page, you can view your product's details and edit them.</Text>
<Text>
You can preview your product using Medusa's Store APIs. You can copy any
of the following code snippets to try it out.
</Text>
</div>
<div>
{!isLoading && (
<CodeBlock snippets={[
{
label: "cURL",
language: "bash",
code: `curl "${backendUrl}/store/products/${data?.product_id}"${api_key ? ` -H "x-publishable-key: ${api_key}"` : ``}`,
},
{
label: "Medusa JS Client",
language: "js",
code: `// Install the JS Client in your storefront project: @medusajs/medusa-js\n\nimport Medusa from "@medusajs/medusa-js"\n\nconst medusa = new Medusa(${api_key ? `{ publishableApiKey: "${api_key}"}` : ``})\nconst product = await medusa.products.retrieve("${data?.product_id}")\nconsole.log(product.id)`,
},
{
label: "Medusa React",
language: "tsx",
code: `// Install the React SDK and required dependencies in your storefront project:\n// medusa-react @tanstack/react-query @medusajs/medusa\n\nimport { useProduct } from "medusa-react"\n\nconst { product } = useProduct("${data?.product_id}")\nconsole.log(product.id)`,
},
{
label: "@medusajs/product",
language: "tsx",
code: `// Install the Product module in a serverless project, such as a Next.js storefront: @medusajs/product\n\nimport {\ninitialize as initializeProductModule,\n} from "@medusajs/product"\n\n// in an async function, or you can use promises\nasync () => {\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">
<CodeBlock.Header />
<CodeBlock.Body />
</CodeBlock>
)}
</div>
<div className="flex gap-2">
<a
href={`${backendUrl}/store/products/${data?.product_id}`}
target="_blank"
>
<Button variant="secondary" size="base">
Open preview in browser
</Button>
</a>
{!isComplete && (
<Button variant="primary" size="base" onClick={() => onNext()}>
Next step
</Button>
)}
</div>
</div>
);
};
export default ProductDetailDefault;

@ -0,0 +1,72 @@
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 (
<div>
<Text className="mb-2">
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.
</Text>
<Text>
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.
</Text>
{!isComplete && (
<div className="flex gap-2 mt-6">
<Button
variant="primary"
size="base"
onClick={() => createSample()}
isLoading={isLoading}
>
Create sample product
</Button>
</div>
)}
</div>
);
};
export default ProductsListDefault;

@ -0,0 +1,138 @@
import React from "react";
import { CurrencyDollarSolid, NextJs, ComputerDesktopSolid } 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 (
<>
<Text size="small" className="mb-6">
You finished the setup guide 🎉. You now have 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.
</Text>
<Heading
level="h2"
className="text-ui-fg-base pt-6 border-t border-ui-border-base border-solid mb-2"
>
Continue Building your Ecommerce Store
</Heading>
<Text size="small">
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 storefronts look and feel
to support your use case.
</Text>
<div className="grid grid-cols-3 gap-4 mt-6 pb-6 mb-6 border-b border-ui-border-base border-solid auto-rows-fr">
<a
href={`https://docs.medusajs.com/starters/nextjs-medusa-starter${queryParams}`}
target="_blank"
className="flex"
>
<div className="p-3 rounded-rounded items-start flex bg-ui-bg-subtle shadow-elevation-card-rest hover:shadow-elevation-card-hover">
<div className="mr-4">
<div className="bg-ui-bg-base rounded-lg border border-ui-border-strong p-1 flex justify-center items-center">
<IconBadge>
<NextJs />
</IconBadge>
</div>
</div>
<div>
<Text
size="xsmall"
weight="plus"
className="mb-1 text-ui-fg-base"
>
Build with the Next.js Storefront
</Text>
<Text size="small">
Learn about the Next.js starter storefronts features and how to
customize it.
</Text>
</div>
</div>
</a>
<a
href={`https://docs.medusajs.com/modules/overview${queryParams}`}
target="_blank"
className="flex"
>
<div className="p-3 rounded-rounded items-start flex bg-ui-bg-subtle shadow-elevation-card-rest hover:shadow-elevation-card-hover">
<div className="mr-4">
<div className="bg-ui-bg-base rounded-lg border border-ui-border-strong p-1 flex justify-center items-center">
<IconBadge>
<CurrencyDollarSolid />
</IconBadge>
</div>
</div>
<div>
<Text
size="xsmall"
weight="plus"
className="mb-1 text-ui-fg-base"
>
Add Commerce Features
</Text>
<Text size="small">
Learn about all available commerce features and how to
add them in your storefront
</Text>
</div>
</div>
</a>
<a
href={`https://docs.medusajs.com/recipes${queryParams}`}
target="_blank"
className="flex"
>
<div className="p-3 rounded-rounded flex items-start bg-ui-bg-subtle shadow-elevation-card-rest hover:shadow-elevation-card-hover">
<div className="mr-4">
<div className="bg-ui-bg-base rounded-lg border border-ui-border-strong p-1 flex justify-center items-center">
<IconBadge>
<ComputerDesktopSolid />
</IconBadge>
</div>
</div>
<div>
<Text
size="xsmall"
weight="plus"
className="mb-1 text-ui-fg-base"
>
Build Custom Use Cases
</Text>
<Text size="small">
Build a marketplace, subscription-based purchases,
or your custom use-cases.
</Text>
</div>
</div>
</a>
</div>
<div>
You can find more useful guides in{" "}
<a
href="https://docs.medusajs.com/?ref=onboarding"
target="_blank"
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
>
our documentation
</a>
. If you like Medusa, please{" "}
<a
href="https://github.com/medusajs/medusa"
target="_blank"
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
>
star us on GitHub
</a>
.
</div>
</>
);
};
export default OrderDetailNextjs;

@ -0,0 +1,71 @@
import React, { useState, useEffect } 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 [cartId, setCartId] = useState<string | null>(null)
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
}
]
})
setCartId(cart?.id)
} catch (e) {
console.error(e);
}
}
useEffect(() => {
if (!cartId && product) {
prepareNextjsCheckout()
}
}, [cartId, product])
return (
<>
<div className="mb-6 flex flex-col gap-2">
<Text>
The last step is to create a sample order using one of your products. You can then view your orders details, process its payment, fulfillment, inventory, and more.
</Text>
<Text>
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, youll be directed back here to view the orders details.
</Text>
</div>
<div className="flex gap-2">
{!isComplete && (
<a
href={`http://localhost:8000/checkout?cart_id=${cartId}&onboarding=true`}
target="_blank"
>
<Button variant="primary" size="base" isLoading={!cartId || cartIsLoading}>
Place an order in your storefront
</Button>
</a>
)}
</div>
</>
);
};
export default OrdersListNextjs

@ -0,0 +1,54 @@
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 (
<div>
<div className="flex flex-col gap-2">
<Text>
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.
</Text>
<Text>
To view the products in your store, you can visit the Next.js Storefront that was installed with <code>create-medusa-app</code>.
</Text>
<Text>
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.
</Text>
<Text>
Click the button below to view the products in your Next.js Storefront.
</Text>
<Text>
Having trouble? Click{" "}
<a
href="https://docs.medusajs.com/starters/nextjs-medusa-starter#troubleshooting-nextjs-storefront-not-working"
target="_blank"
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
>
here
</a>.
</Text>
</div>
<div className="flex gap-2 mt-6">
<a
href={`http://localhost:8000/products/${product?.handle}?onboarding=true`}
target="_blank"
>
<Button variant="primary" size="base" isLoading={productIsLoading}>
Open Next.js Storefront
</Button>
</a>
{!isComplete && (
<Button variant="secondary" size="base" onClick={() => onNext()}>
Next step
</Button>
)}
</div>
</div>
);
};
export default ProductDetailNextjs

@ -0,0 +1,84 @@
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<Product | null> => {
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 (
<div>
<Text className="mb-2">
Products is 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.
</Text>
<Text>
Click the button below to create sample products.
</Text>
{!isComplete && (
<div className="flex gap-2 mt-6">
<Button
variant="primary"
size="base"
onClick={() => createSample()}
isLoading={isLoading}
>
Create sample products
</Button>
</div>
)}
</div>
);
};
export default ProductsListNextjs;

@ -0,0 +1,123 @@
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<HTMLDivElement>)
| (AccordionPrimitive.AccordionMultipleProps &
React.RefAttributes<HTMLDivElement>)
> & {
Item: React.FC<AccordionItemProps>;
} = ({ children, ...props }) => {
return (
<AccordionPrimitive.Root {...props}>{children}</AccordionPrimitive.Root>
);
};
const Item: React.FC<AccordionItemProps> = ({
title,
subtitle,
description,
required,
tooltip,
children,
className,
complete,
headingSize = "large",
customTrigger = undefined,
forceMountContent = undefined,
active,
triggerable,
...props
}) => {
return (
<AccordionPrimitive.Item
{...props}
className={clx(
"border-grey-20 group border-t last:mb-0",
"py-1 px-8",
className
)}
>
<AccordionPrimitive.Header className="px-1">
<div className="flex flex-col">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center justify-center p-[10px]">
{complete ? (
<CheckCircleSolid className="text-ui-fg-interactive" />
) : (
<>
{active && (
<ActiveCircleDottedLine
size={20}
className="text-ui-fg-interactive rounded-full"
/>
)}
{!active && (
<CircleMiniSolid className="text-ui-fg-muted" />
)}
</>
)}
</div>
<Heading level="h3" className={clx("text-ui-fg-base")}>
{title}
</Heading>
</div>
<AccordionPrimitive.Trigger>
{customTrigger || <MorphingTrigger />}
</AccordionPrimitive.Trigger>
</div>
{subtitle && (
<Text as="span" size="small" className="mt-1">
{subtitle}
</Text>
)}
</div>
</AccordionPrimitive.Header>
<AccordionPrimitive.Content
forceMount={forceMountContent}
className={clx(
"radix-state-closed:animate-accordion-close radix-state-open:animate-accordion-open radix-state-closed:pointer-events-none px-1"
)}
>
<div className="inter-base-regular group-radix-state-closed:animate-accordion-close">
{description && <Text>{description}</Text>}
<div className="w-full">{children}</div>
</div>
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>
);
};
Accordion.Item = Item;
const MorphingTrigger = () => {
return (
<div className="btn-ghost rounded-rounded group relative p-[6px]">
<div className="h-5 w-5">
<span className="bg-grey-50 rounded-circle group-radix-state-open:rotate-90 absolute inset-y-[31.75%] left-[48%] right-1/2 w-[1.5px] duration-300" />
<span className="bg-grey-50 rounded-circle group-radix-state-open:rotate-90 group-radix-state-open:left-1/2 group-radix-state-open:right-1/2 absolute inset-x-[31.75%] top-[48%] bottom-1/2 h-[1.5px] duration-300" />
</div>
</div>
);
};
export default Accordion;

@ -0,0 +1,27 @@
import { Text, clx } from "@medusajs/ui"
type CardProps = {
icon?: React.ReactNode
children?: React.ReactNode
className?: string
}
const Card = ({
icon,
children,
className
}: CardProps) => {
return (
<div className={clx(
"p-4 rounded-lg gap-3",
"flex items-start shadow-elevation-card-rest",
"bg-ui-bg-subtle",
className
)}>
{icon}
<Text size="base" className="text-ui-fg-subtle">{children}</Text>
</div>
)
}
export default Card

@ -0,0 +1,37 @@
import React from "react";
import IconProps from "../../../types/icon-type";
const ActiveCircleDottedLine: React.FC<IconProps> = ({
size = "24",
color = "currentColor",
...attributes
}) => {
return (
<svg width={size} height={size} viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg" {...attributes}>
<g filter="url(#filter0_d_8860_2802)">
<rect x="3" y="3" width="20" height="20" rx="10" fill="white"/>
<path d="M15.5 5.93589C13.884 5.3547 12.116 5.3547 10.5 5.93589" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M5.63037 11.632C5.94283 9.94471 6.82561 8.41606 8.13082 7.30209" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8.13082 18.6979C6.82563 17.5839 5.94286 16.0552 5.63037 14.368" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10.5 20.0641C12.116 20.6453 13.884 20.6453 15.5 20.0641" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M20.3696 11.632C20.0571 9.94471 19.1744 8.41606 17.8691 7.30209" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M17.8691 18.6979C19.1743 17.5839 20.0571 16.0552 20.3696 14.368" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</g>
<defs>
<filter id="filter0_d_8860_2802" x="0" y="0" width="26" height="26" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="3" operator="dilate" in="SourceAlpha" result="effect1_dropShadow_8860_2802"/>
<feOffset/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.231373 0 0 0 0 0.509804 0 0 0 0 0.964706 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_8860_2802"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_8860_2802" result="shape"/>
</filter>
</defs>
</svg>
);
};
export default ActiveCircleDottedLine;

File diff suppressed because one or more lines are too long

@ -0,0 +1,8 @@
import React from "react"
type IconProps = {
color?: string
size?: string | number
} & React.SVGAttributes<SVGElement>
export default IconProps

@ -0,0 +1,42 @@
import { Store } from "@medusajs/medusa"
import type Medusa from "@medusajs/medusa-js"
import { ExtendedStoreDTO } from "@medusajs/medusa/dist/types/store"
export default async function prepareRegions (client: Medusa) {
let { regions } = await client.admin.regions.list()
if (!regions.length) {
let { store } = await client.admin.store.retrieve()
if (!store.currencies) {
store = (await client.admin.store.update({
currencies: ["eur"]
})).store as ExtendedStoreDTO
}
regions = [(await client.admin.regions.create(getSampleRegion(store))).region]
}
return regions
}
function getSampleRegion (store: Store) {
return {
name: "EU",
currency_code: store.currencies[0].code,
tax_rate: 0,
payment_providers: [
"manual"
],
fulfillment_providers: [
"manual"
],
countries: [
"gb",
"de",
"dk",
"se",
"fr",
"es",
"it"
]
}
}

@ -0,0 +1,20 @@
import { Region } from "@medusajs/medusa";
import type Medusa from "@medusajs/medusa-js"
export default async function prepareShippingOptions (client: Medusa, region: Region) {
let { shipping_options } = await client.admin.shippingOptions.list()
if (!shipping_options.length) {
shipping_options = [(await client.admin.shippingOptions.create({
"name": "PostFake Standard",
"region_id": region.id,
"provider_id": "manual",
"data": {
"id": "manual-fulfillment"
},
"price_type": "flat_rate",
"amount": 1000
})).shipping_option]
}
return shipping_options
}

@ -0,0 +1,666 @@
import { AdminPostProductsReq, Region } from "@medusajs/medusa"
type SampleProductsOptions = {
regions: Region[]
collection_id?: string
}
// can't use the ProductStatus imported
// from the core within admin cusotmizations
enum ProductStatus {
PUBLISHED = "published"
}
export default function getSampleProducts ({
regions,
collection_id
}: SampleProductsOptions): AdminPostProductsReq[] {
return [
{
title: "Medusa T-Shirt",
status: ProductStatus.PUBLISHED,
collection_id,
discountable: true,
subtitle: null,
description: "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.",
handle: "medusa-t-shirt",
is_giftcard: false,
weight: 400,
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",
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png",
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png"
],
options: [
{
title: "Size",
},
{
title: "Color",
}
],
variants: [
{
title: "S / Black",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2200
}
}),
options: [
{
value: "S"
},
{
value: "Black"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "S / White",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2200
}
}),
options: [
{
value: "S"
},
{
value: "White"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "M / Black",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2200
}
}),
options: [
{
value: "M"
},
{
value: "Black"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "M / White",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2200
}
}),
options: [
{
value: "M"
},
{
value: "White"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "L / Black",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2200
}
}),
options: [
{
value: "L"
},
{
value: "Black"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "L / White",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2200
}
}),
options: [
{
value: "L"
},
{
value: "White"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "XL / Black",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2200
}
}),
options: [
{
value: "XL"
},
{
value: "Black"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "XL / White",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2200
}
}),
options: [
{
value: "XL"
},
{
value: "White"
}
],
inventory_quantity: 100,
manage_inventory: true
}
]
},
{
title: "Medusa Sweatshirt",
status: ProductStatus.PUBLISHED,
discountable: true,
collection_id,
subtitle: null,
description: "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
handle: "sweatshirt",
is_giftcard: false,
weight: 400,
images: [
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png"
],
options: [
{
title: "Size",
}
],
variants: [
{
title: "S",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 3350
}
}),
options: [
{
value: "S"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "M",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 3350
}
}),
options: [
{
value: "M"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "L",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 3350
}
}),
options: [
{
value: "L"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "XL",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 3350
}
}),
options: [
{
value: "XL"
}
],
inventory_quantity: 100,
manage_inventory: true
}
]
},
{
title: "Medusa Sweatpants",
status: ProductStatus.PUBLISHED,
discountable: true,
collection_id,
subtitle: null,
description: "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.",
handle: "sweatpants",
is_giftcard: false,
weight: 400,
images: [
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png",
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png"
],
options: [
{
title: "Size",
}
],
variants: [
{
title: "S",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 3350
}
}),
options: [
{
value: "S"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "M",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 3350
}
}),
options: [
{
value: "M"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "L",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 3350
}
}),
options: [
{
value: "L"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "XL",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 3350
}
}),
options: [
{
value: "XL"
}
],
inventory_quantity: 100,
manage_inventory: true
}
]
},
{
title: "Medusa Shorts",
status: ProductStatus.PUBLISHED,
discountable: true,
collection_id,
subtitle: null,
description: "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.",
handle: "shorts",
is_giftcard: false,
weight: 400,
images: [
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png",
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png"
],
options: [
{
title: "Size",
}
],
variants: [
{
title: "S",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2850
}
}),
options: [
{
value: "S"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "M",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2850
}
}),
options: [
{
value: "M"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "L",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2850
}
}),
options: [
{
value: "L"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "XL",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 2850
}
}),
options: [
{
value: "XL"
}
],
inventory_quantity: 100,
manage_inventory: true
}
]
},
{
title: "Medusa Hoodie",
status: ProductStatus.PUBLISHED,
discountable: true,
collection_id,
subtitle: null,
description: "Reimagine the feeling of a classic hoodie. With our cotton hoodie, everyday essentials no longer have to be ordinary.",
handle: "hoodie",
is_giftcard: false,
weight: 400,
images: [
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_front.png",
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_back.png"
],
options: [
{
title: "Size",
}
],
variants: [
{
title: "S",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 4150
}
}),
options: [
{
value: "S"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "M",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 4150
}
}),
options: [
{
value: "M"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "L",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 4150
}
}),
options: [
{
value: "L"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "XL",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 4150
}
}),
options: [
{
value: "XL"
}
],
inventory_quantity: 100,
manage_inventory: true
}
]
},
{
title: "Medusa Longsleeve",
status: ProductStatus.PUBLISHED,
discountable: true,
collection_id,
subtitle: null,
description: "Reimagine the feeling of a classic longsleeve. With our cotton longsleeve, everyday essentials no longer have to be ordinary.",
handle: "longsleeve",
is_giftcard: false,
weight: 400,
images: [
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-front.png",
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-back.png"
],
options: [
{
title: "Size",
}
],
variants: [
{
title: "S",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 4150
}
}),
options: [
{
value: "S"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "M",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 4150
}
}),
options: [
{
value: "M"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "L",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 4150
}
}),
options: [
{
value: "L"
}
],
inventory_quantity: 100,
manage_inventory: true
},
{
title: "XL",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 4150
}
}),
options: [
{
value: "XL"
}
],
inventory_quantity: 100,
manage_inventory: true
}
]
},
{
title: "Medusa Coffee Mug",
status: ProductStatus.PUBLISHED,
discountable: true,
collection_id,
subtitle: null,
description: "Every programmer's best friend.",
handle: "coffee-mug",
is_giftcard: false,
weight: 400,
images: [
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png"
],
options: [
{
title: "Size",
}
],
variants: [
{
title: "One Size",
prices: regions.map((region) => {
return {
currency_code: region.currency_code,
amount: 1200
}
}),
options: [
{
value: "One Size"
}
],
inventory_quantity: 100,
manage_inventory: true
}
]
}
]
}

@ -0,0 +1,502 @@
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?: Function;
isComplete?: boolean;
data?: OnboardingState;
};
type Step = {
id: STEP_ID;
title: string;
component: React.FC<StepContentProps>;
onNext?: Function;
};
const QUERY_KEY = ["onboarding_state"];
const OnboardingFlow = (props: OnboardingWidgetProps) => {
// create custom hooks for custom endpoints
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 endpoint
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 endpoints
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 (
<>
<Container className={clx(
"text-ui-fg-subtle px-0 pt-0 pb-4",
{
"mb-4": completed
}
)}>
<Accordion
type="single"
value={openStep}
onValueChange={(value) => setOpenStep(value as STEP_ID)}
>
<div className={clx(
"flex py-6 px-8",
{
"items-start": completed,
"items-center": !completed
}
)}>
<div className="w-12 h-12 p-1 flex justify-center items-center rounded-full bg-ui-bg-base shadow-elevation-card-rest mr-4">
<GetStarted />
</div>
{!completed ? (
<>
<div>
<Heading level="h1" className="text-ui-fg-base">Get started</Heading>
<Text>
{getStartedText()}
</Text>
</div>
<div className="ml-auto flex items-start gap-2">
{!!currentStep ? (
<>
{currentStep === steps[steps.length - 1].id ? (
<Button
variant="primary"
size="base"
onClick={() => onComplete()}
>
Complete Setup
</Button>
) : (
<Button
variant="secondary"
size="base"
onClick={() => onHide()}
>
Cancel Setup
</Button>
)}
</>
) : (
<>
<Button
variant="secondary"
size="base"
onClick={() => onHide()}
>
Close
</Button>
<Button
variant="primary"
size="base"
onClick={() => onStart()}
>
Begin setup
</Button>
</>
)}
</div>
</>
) : (
<>
<div>
<Heading level="h1" className="text-ui-fg-base">
Thank you for completing the setup guide!
</Heading>
<Text>
This whole experience was built using our new{" "}
<strong>widgets</strong> feature.
<br /> You can find out more details and build your own by
following{" "}
<a
href="https://docs.medusajs.com/admin/onboarding?ref=onboarding"
target="_blank"
className="text-blue-500 font-semibold"
>
our guide
</a>
.
</Text>
</div>
<div className="ml-auto flex items-start gap-2">
<Button
variant="secondary"
size="base"
onClick={() => onHide()}
>
Close
</Button>
</div>
</>
)}
</div>
{
<div>
{(!completed ? steps : steps.slice(-1)).map((step) => {
const isComplete = isStepComplete(step.id);
const isCurrent = currentStep === step.id;
return (
<Accordion.Item
title={step.title}
value={step.id}
headingSize="medium"
active={isCurrent}
complete={isComplete}
disabled={!isComplete && !isCurrent}
key={step.id}
{...(!isComplete &&
!isCurrent && {
customTrigger: <></>,
})}
>
<div className="pl-14 pb-6 pr-7">
<step.component
onNext={step.onNext}
isComplete={isComplete}
data={data?.status}
{...props}
/>
</div>
</Accordion.Item>
);
})}
</div>
}
</Accordion>
</Container>
</>
);
};
export const config: WidgetConfig = {
zone: [
"product.list.before",
"product.details.before",
"order.list.before",
"order.details.before",
],
};
export default OnboardingFlow;

@ -0,0 +1,179 @@
# Custom API Routes
You may define custom API Routes by putting files in the `/api` directory that export functions returning an express router or a collection of express routers.
Medusa supports adding custom API Routes using a file based approach. This means that you can add files in the `/api` directory and the files path will be used as the API Route path. For example, if you add a file called `/api/store/custom/route.ts` it will be available on the `/store/custom` API Route.
```ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
export async function GET(req: MedusaRequest, res: MedusaResponse) {
res.json({
message: "Hello world!",
});
}
```
## Supported HTTP methods
The file based routing supports the following HTTP methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
- HEAD
You can define a handler for each of these methods by exporting a function with the name of the method in the paths `route.ts` file. For example, if you want to define a handler for the `GET`, `POST`, and `PUT` methods, you can do so by exporting functions with the names `GET`, `POST`, and `PUT`:
```ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
export async function GET(req: MedusaRequest, res: MedusaResponse) {
// Handle GET requests
}
export async function POST(req: MedusaRequest, res: MedusaResponse) {
// Handle POST requests
}
export async function PUT(req: MedusaRequest, res: MedusaResponse) {
// Handle PUT requests
}
```
## Parameters
You can define parameters in the path of your route by using wrapping the parameter name in square brackets. For example, if you want to define a route that takes a `productId` parameter, you can do so by creating a file called `/api/products/[productId]/route.ts`:
```ts
import type {
MedusaRequest,
MedusaResponse,
ProductService,
} from "@medusajs/medusa";
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const { productId } = req.params;
const productService: ProductService = req.scope.resolve("productService");
const product = await productService.retrieve(productId);
res.json({
product,
});
}
```
If you want to define a route that takes multiple parameters, you can do so by adding multiple parameters in the path. It is important that each parameter is given a unique name. For example, if you want to define a route that takes both a `productId` and a `variantId` parameter, you can do so by creating a file called `/api/products/[productId]/variants/[variantId]/route.ts`. Duplicate parameter names are not allowed, and will result in an error.
## Using the container
A global container is available on `req.scope` to allow you to use any of the registered services from the core, installed plugins or your local project:
```ts
import type {
MedusaRequest,
MedusaResponse,
ProductService,
} from "@medusajs/medusa";
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const productService: ProductService = req.scope.resolve("productService");
const products = await productService.list();
res.json({
products,
});
}
```
## Middleware
You can apply middleware to your routes by creating a file called `/api/middlewares.ts`. This file should export a configuration object with what middleware you want to apply to which routes. For example, if you want to apply a custom middleware function to the `/store/custom` route, you can do so by adding the following to your `/api/middlewares.ts` file:
```ts
import type {
MiddlewaresConfig,
MedusaRequest,
MedusaResponse,
MedusaNextFunction,
} from "@medusajs/medusa";
async function logger(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) {
console.log("Request received");
next();
}
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/store/custom",
middlewares: [logger],
},
],
};
```
The `matcher` property can be either a string or a regular expression. The `middlewares` property accepts an array of middleware functions.
You might only want to apply middleware to certain HTTP methods. You can do so by adding a `method` property to the route configuration object:
```ts
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/store/custom",
method: "GET",
middlewares: [logger],
},
],
};
```
The `method` property can be either a HTTP method or an array of HTTP methods. By default the middlewares will apply to all HTTP methods for the given `matcher`.
### Default middleware
Some middleware functions are applied per default:
#### Global middleware
JSON parsing is applied to all routes. This means that you can access the request body as `req.body` and it will be parsed as JSON, if the request has a `Content-Type` header of `application/json`.
If you want to use a different parser for a specific route, such as `urlencoded`, you can do so by adding the following export to your `route.ts` file:
```ts
import { urlencoded } from "express";
export const config: MiddlewaresConfig = {
routes: [
{
method: "POST",
matcher: "/store/custom",
middlewares: [urlencoded()],
},
],
};
```
#### Store middleware
For all `/store` routes, the appropriate CORS settings are applied. The STORE_CORS value can be configured in your `medusa-config.js` file.
#### Admin middleware
For all `/admin` routes, the appropriate CORS settings are applied. The ADMIN_CORS value can be configured in your `medusa-config.js` file.
All `/admin` routes also have admin authentication applied per default. If you want to disable this for a specific route, you can do so by adding the following export to your `route.ts` file:
```ts
export const AUTHENTICATE = false;
```

@ -0,0 +1,8 @@
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
res.sendStatus(200);
}

@ -0,0 +1,27 @@
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 });
}

@ -0,0 +1,8 @@
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
res.sendStatus(200);
}

@ -0,0 +1,32 @@
# Custom scheduled jobs
You may define custom scheduled jobs (cron jobs) by creating files in the `/jobs` directory.
```ts
import {
ProductService,
ScheduledJobArgs,
ScheduledJobConfig,
} from "@medusajs/medusa";
export default async function myCustomJob({ container }: ScheduledJobArgs) {
const productService: ProductService = container.resolve("productService");
const products = await productService.listAndCount();
// Do something with the products
}
export const config: ScheduledJobConfig = {
name: "daily-product-report",
schedule: "0 0 * * *", // Every day at midnight
};
```
A scheduled job is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when the job is scheduled. The `config` is an object which defines the name of the job, the schedule, and an optional data object.
The `handler` is a function which takes one parameter, an `object` of type `ScheduledJobArgs` with the following properties:
- `container` - a `MedusaContainer` instance which can be used to resolve services.
- `data` - an `object` containing data passed to the job when it was scheduled. This object is passed in the `config` object.
- `pluginOptions` - an `object` containing plugin options, if the job is defined in a plugin.

@ -0,0 +1,19 @@
# Custom loader
The loader allows you have access to the Medusa service container. This allows you to access the database and the services registered on the container.
you can register custom registrations in the container or run custom code on startup.
```ts
// src/loaders/my-loader.ts
import { AwilixContainer } from 'awilix'
/**
*
* @param container The container in which the registrations are made
* @param config The options of the plugin or the entire config object
*/
export default (container: AwilixContainer, config: Record<string, unknown>): void | Promise<void> => {
/* Implement your own loader. */
}
```

@ -0,0 +1,21 @@
import { generateEntityId } from "@medusajs/utils";
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateOnboarding1685715079776 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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)`
);
await queryRunner.query(
`INSERT INTO "onboarding_state" ("id", "current_step", "is_complete") VALUES ('${generateEntityId(
"",
"onboarding"
)}' , NULL, false)`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "onboarding_state"`);
}
}

@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddOnboardingProduct1686062614694 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "onboarding_state" ADD COLUMN "product_id" character varying NULL`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "onboarding_state" DROP COLUMN "product_id"`
);
}
}

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CorrectOnboardingFields1690996567455 implements MigrationInterface {
name = 'CorrectOnboardingFields1690996567455'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "onboarding_state" ADD CONSTRAINT "PK_891b72628471aada55d7b8c9410" PRIMARY KEY ("id")`);
await queryRunner.query(`ALTER TABLE "onboarding_state" ALTER COLUMN "is_complete" SET NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "onboarding_state" ALTER COLUMN "is_complete" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "onboarding_state" DROP CONSTRAINT "PK_891b72628471aada55d7b8c9410"`);
}
}

@ -0,0 +1,29 @@
# Custom migrations
You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`.
In that case you also need to provide a migration in order to create the table in the database.
## Example
### 1. Create the migration
See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation.
```ts
// src/migration/my-migration.ts
import { MigrationInterface, QueryRunner } from "typeorm"
export class MyMigration1617703530229 implements MigrationInterface {
name = "myMigration1617703530229"
public async up(queryRunner: QueryRunner): Promise<void> {
// write you migration here
}
public async down(queryRunner: QueryRunner): Promise<void> {
// write you migration here
}
}
```

@ -0,0 +1,46 @@
# Custom models
You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`.
## Example
### 1. Create the Entity
```ts
// src/models/post.ts
import { BeforeInsert, Column, Entity, PrimaryColumn } from "typeorm";
import { generateEntityId } from "@medusajs/utils";
import { BaseEntity } from "@medusajs/medusa";
@Entity()
export class Post extends BaseEntity {
@Column({type: 'varchar'})
title: string | null;
@BeforeInsert()
private beforeInsert(): void {
this.id = generateEntityId(this.id, "post")
}
}
```
### 2. Create the Migration
You also need to create a Migration to create the new table in the database. See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation.
### 3. Create a Repository
Entities data can be easily accessed and modified using [TypeORM Repositories](https://typeorm.io/working-with-repository). To create a repository, create a file in `src/repositories`. For example, heres a repository `PostRepository` for the `Post` entity:
```ts
// src/repositories/post.ts
import { EntityRepository, Repository } from "typeorm"
import { Post } from "../models/post"
@EntityRepository(Post)
export class PostRepository extends Repository<Post> { }
```
See more about defining and accesing your custom [Entities](https://docs.medusajs.com/advanced/backend/entities/overview) in the documentation.

@ -0,0 +1,14 @@
import { BaseEntity } from "@medusajs/medusa";
import { Column, Entity } from "typeorm";
@Entity()
export class OnboardingState extends BaseEntity {
@Column({ nullable: true })
current_step: string;
@Column()
is_complete: boolean;
@Column({ nullable: true })
product_id: string;
}

@ -0,0 +1,6 @@
import { dataSource } from "@medusajs/medusa/dist/loaders/database";
import { OnboardingState } from "../models/onboarding";
const OnboardingRepository = dataSource.getRepository(OnboardingState);
export default OnboardingRepository;

@ -0,0 +1,49 @@
# Custom services
You may define custom services that will be registered on the global container by creating files in the `/services` directory that export an instance of `BaseService`.
```ts
// src/services/my-custom.ts
import { Lifetime } from "awilix"
import { TransactionBaseService } from "@medusajs/medusa";
import { IEventBusService } from "@medusajs/types";
export default class MyCustomService extends TransactionBaseService {
static LIFE_TIME = Lifetime.SCOPED
protected readonly eventBusService_: IEventBusService
constructor(
{ eventBusService }: { eventBusService: IEventBusService },
options: Record<string, unknown>
) {
// @ts-ignore
super(...arguments)
this.eventBusService_ = eventBusService
}
}
```
The first argument to the `constructor` is the global giving you access to easy dependency injection. The container holds all registered services from the core, installed plugins and from other files in the `/services` directory. The registration name is a camelCased version of the file name with the type appended i.e.: `my-custom.js` is registered as `myCustomService`, `custom-thing.js` is registered as `customThingService`.
You may use the services you define here in custom endpoints by resolving the services defined.
```js
import { Router } from "express"
export default () => {
const router = Router()
router.get("/hello-product", async (req, res) => {
const myService = req.scope.resolve("myCustomService")
res.json({
message: await myService.getProductMessage()
})
})
return router;
}
```

@ -0,0 +1,5 @@
describe('MyService', () => {
it('should do this', async () => {
expect(true).toBe(true)
})
})

@ -0,0 +1,52 @@
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<OnboardingState | undefined> {
const onboardingRepo = this.activeManager_.withRepository(
this.onboardingRepository_
);
const status = await onboardingRepo.findOne({
where: { id: Not(IsNull()) },
});
return status;
}
async update(data: UpdateOnboardingStateInput): Promise<OnboardingState> {
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;

@ -0,0 +1,44 @@
# Custom subscribers
You may define custom eventhandlers, `subscribers` by creating files in the `/subscribers` directory.
```ts
import MyCustomService from "../services/my-custom";
import {
OrderService,
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/medusa";
type OrderPlacedEvent = {
id: string;
no_notification: boolean;
};
export default async function orderPlacedHandler({
data,
eventName,
container,
}: SubscriberArgs<OrderPlacedEvent>) {
const orderService: OrderService = container.resolve(OrderService);
const order = await orderService.retrieve(data.id, {
relations: ["items", "items.variant", "items.variant.product"],
});
// Do something with the order
}
export const config: SubscriberConfig = {
event: OrderService.Events.PLACED,
};
```
A subscriber is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when an event is emitted. The `config` is an object which defines which event(s) the subscriber should subscribe to.
The `handler` is a function which takes one parameter, an `object` of type `SubscriberArgs<T>` with the following properties:
- `data` - an `object` of type `T` containing information about the event.
- `eventName` - a `string` containing the name of the event.
- `container` - a `MedusaContainer` instance which can be used to resolve services.
- `pluginOptions` - an `object` containing plugin options, if the subscriber is defined in a plugin.

@ -0,0 +1,13 @@
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;
};

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext"
},
"include": ["src/admin"],
"exclude": ["**/*.spec.js"]
}

@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "es2019",
"allowJs": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"declaration": true,
"sourceMap": false,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": ".",
"jsx": "react-jsx",
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"checkJs": false
},
"include": ["src/"],
"exclude": [
"**/__tests__",
"**/__fixtures__",
"node_modules",
"build",
".cache"
]
}

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
/* Emit a single file with source maps instead of having a separate file. */
"inlineSourceMap": true
},
"exclude": ["src/admin", "**/*.spec.js"]
}

@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["dist", "node_modules"]
}
Loading…
Cancel
Save