docs: added customer storefront guides (#7685)
* added customer guides * fixes to sidebar * remove old customer registration guide * fix build error * generate files * run linter
This commit is contained in:
@@ -54,7 +54,7 @@ export const highlights = [
|
||||
]
|
||||
|
||||
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["21"], ["22"], ["23"], ["24"], ["25"], ["26"]]}
|
||||
import { defineRouteConfig } from "@medusajs/admin-shared";
|
||||
import { defineRouteConfig } from "@medusajs/admin-shared"
|
||||
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
||||
|
||||
const CustomPage = () => {
|
||||
@@ -125,7 +125,7 @@ export const config = defineRouteConfig({
|
||||
label: "Custom",
|
||||
})
|
||||
|
||||
export default CustomSettingPage;
|
||||
export default CustomSettingPage
|
||||
```
|
||||
|
||||
This adds a page under the path `/app/settings/custom`. An item is also added to the settings sidebar with the label `Custom`.
|
||||
|
||||
@@ -80,7 +80,7 @@ import { defineWidgetConfig } from "@medusajs/admin-shared"
|
||||
import { DetailWidgetProps, AdminProduct } from "@medusajs/types"
|
||||
|
||||
const ProductWidget = ({
|
||||
data
|
||||
data,
|
||||
}: DetailWidgetProps<AdminProduct>) => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -24,7 +24,7 @@ module.exports = defineConfig({
|
||||
adminCors: "http://localhost:7001",
|
||||
// ...
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ For example, create the file `src/scripts/my-script.ts` with the following conte
|
||||
```ts title="src/scripts/my-script.ts"
|
||||
import {
|
||||
ExecArgs,
|
||||
IProductModuleService
|
||||
IProductModuleService,
|
||||
} from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
export default async function myScript ({
|
||||
container
|
||||
export default async function myScript({
|
||||
container,
|
||||
}: ExecArgs) {
|
||||
const productModuleService: IProductModuleService =
|
||||
container.resolve(ModuleRegistrationName.PRODUCT)
|
||||
@@ -60,8 +60,8 @@ For example:
|
||||
```ts
|
||||
import { ExecArgs } from "@medusajs/types"
|
||||
|
||||
export default async function myScript ({
|
||||
args
|
||||
export default async function myScript({
|
||||
args,
|
||||
}: ExecArgs) {
|
||||
console.log(`The arguments you passed: ${args}`)
|
||||
}
|
||||
|
||||
@@ -222,10 +222,10 @@ module.exports = defineConfig({
|
||||
helloModuleService: {
|
||||
// ...
|
||||
definition: {
|
||||
isQueryable: true
|
||||
}
|
||||
}
|
||||
}
|
||||
isQueryable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ module.exports = defineConfig({
|
||||
options: {
|
||||
capitalize: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
@@ -68,8 +68,8 @@ module.exports = defineConfig({
|
||||
modules: {
|
||||
helloModuleService: {
|
||||
resolve: "./modules/hello",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -66,9 +66,9 @@ To test out your scheduled job:
|
||||
```js title="medusa-config.js"
|
||||
module.exports = defineConfig({
|
||||
projectConfig: {
|
||||
redisUrl: REDIS_URL
|
||||
redisUrl: REDIS_URL,
|
||||
// ...
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -166,6 +166,6 @@ module.exports = defineConfig({
|
||||
ttl: 30,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -44,7 +44,7 @@ module.exports = defineConfig({
|
||||
// optional options
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ module.exports = defineConfig({
|
||||
redisUrl: process.env.CACHE_REDIS_URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -120,6 +120,6 @@ module.exports = defineConfig({
|
||||
// any options
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@ module.exports = defineConfig({
|
||||
// ...
|
||||
modules: {
|
||||
[Modules.EVENT_BUS]: true,
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ module.exports = defineConfig({
|
||||
redisUrl: process.env.EVENTS_REDIS_URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ module.exports = defineConfig({
|
||||
options: {
|
||||
config: {
|
||||
local: {
|
||||
channels: ["email"]
|
||||
channels: ["email"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -58,7 +58,7 @@ module.exports = defineConfig({
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ module.exports = {
|
||||
options: {
|
||||
config: {
|
||||
local: {
|
||||
channels: ["email"]
|
||||
channels: ["email"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ import { INotificationModuleService } from "@medusajs/types"
|
||||
|
||||
export default async function productCreateHandler({
|
||||
data,
|
||||
container
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const notificationModuleService: INotificationModuleService =
|
||||
container.resolve(
|
||||
@@ -41,7 +41,7 @@ export default async function productCreateHandler({
|
||||
to: "shahednasser@gmail.com",
|
||||
channel: "email",
|
||||
template: "product-created",
|
||||
data: "data" in data ? data.data : data
|
||||
data: "data" in data ? data.data : data,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ module.exports = defineConfig({
|
||||
sendgrid: {
|
||||
channels: ["email"],
|
||||
api_key: process.env.SENDGRID_API_KEY,
|
||||
from: process.env.SENDGRID_FROM
|
||||
from: process.env.SENDGRID_FROM,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -62,7 +62,7 @@ module.exports = defineConfig({
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -156,7 +156,7 @@ import { INotificationModuleService } from "@medusajs/types"
|
||||
|
||||
export default async function productCreateHandler({
|
||||
data,
|
||||
container
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const notificationModuleService: INotificationModuleService =
|
||||
container.resolve(
|
||||
@@ -167,7 +167,7 @@ export default async function productCreateHandler({
|
||||
to: "test@gmail.com",
|
||||
channel: "email",
|
||||
template: "product-created",
|
||||
data: "data" in data ? data.data : data
|
||||
data: "data" in data ? data.data : data,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,6 @@ module.exports = defineConfig({
|
||||
// ...
|
||||
modules: {
|
||||
[Modules.WORKFLOW_ENGINE]: true,
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -46,7 +46,7 @@ module.exports = defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -122,11 +122,11 @@ In this guide, you’ll find common examples of how you can use the API Key Modu
|
||||
```ts collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { IApiKeyModuleService } from "@medusajs/types"
|
||||
import {
|
||||
ModuleRegistrationName
|
||||
ModuleRegistrationName,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
export async function POST(
|
||||
@@ -139,7 +139,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu
|
||||
const revokedKey = await apiKeyModuleService.revoke(
|
||||
request.params.id,
|
||||
{
|
||||
revoked_by: request.auth_context.actor_id
|
||||
revoked_by: request.auth_context.actor_id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -175,7 +175,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu
|
||||
const revokedKey = await apiKeyModuleService.revoke(
|
||||
params.id,
|
||||
{
|
||||
revoked_by: params.user_id
|
||||
revoked_by: params.user_id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -260,7 +260,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu
|
||||
```ts collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { IApiKeyModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
@@ -275,7 +275,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu
|
||||
const revokedKey = await apiKeyModuleService.revoke(
|
||||
request.params.id,
|
||||
{
|
||||
revoked_by: request.auth_context.actor_id
|
||||
revoked_by: request.auth_context.actor_id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -315,7 +315,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu
|
||||
const apiKeyModuleService = await initializeApiKeyModule()
|
||||
|
||||
const revokedKey = await apiKeyModuleService.revoke(params.id, {
|
||||
revoked_by: params.user_id
|
||||
revoked_by: params.user_id,
|
||||
})
|
||||
|
||||
const newKey = await apiKeyModuleService.create({
|
||||
|
||||
@@ -53,7 +53,7 @@ Revoke keys to disable their use permenantly.
|
||||
|
||||
```ts
|
||||
const revokedKey = await apiKeyModuleService.revoke("apk_1", {
|
||||
revoked_by: "user_123"
|
||||
revoked_by: "user_123",
|
||||
})
|
||||
```
|
||||
|
||||
@@ -63,7 +63,7 @@ Roll API keys by revoking a key then re-creating it.
|
||||
|
||||
```ts
|
||||
const revokedKey = await apiKeyModuleService.revoke("apk_1", {
|
||||
revoked_by: "user_123"
|
||||
revoked_by: "user_123",
|
||||
})
|
||||
|
||||
const newKey = await apiKeyModuleService.create({
|
||||
|
||||
@@ -57,9 +57,9 @@ const modules = {
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||
successRedirectUrl:
|
||||
process.env.GOOGLE_SUCCESS_REDIRECT_URL,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -36,8 +36,8 @@ const modules = {
|
||||
config: {
|
||||
emailpass: {
|
||||
// options...
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -92,4 +92,4 @@ const modules = {
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [How to register a customer using email and password](../../../customer/register-customer-email/page.mdx)
|
||||
- [How to register a customer using email and password](../../../../storefront-development/customers/register/page.mdx)
|
||||
|
||||
@@ -32,12 +32,12 @@ module.exports = defineConfig({
|
||||
http: {
|
||||
authMethodsPerActor: {
|
||||
user: ["google"],
|
||||
customer: ["emailpass"]
|
||||
customer: ["emailpass"],
|
||||
},
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -32,6 +32,6 @@ There are two ways the returned authentication token is useful:
|
||||
|
||||
<Note title="Example">
|
||||
|
||||
[How to register Customers using the authentication route](../../customer/register-customer-email/page.mdx).
|
||||
[How to register Customers using the authentication route](../../../storefront-development/customers/register/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
@@ -60,10 +60,10 @@ export const workflowHighlights = [
|
||||
import {
|
||||
createWorkflow,
|
||||
createStep,
|
||||
StepResponse
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
setAuthAppMetadataStep
|
||||
setAuthAppMetadataStep,
|
||||
} from "@medusajs/core-flows"
|
||||
import ManagerModuleService from "../modules/manager/service"
|
||||
|
||||
@@ -86,7 +86,7 @@ type CreateManagerWorkflowOutput = {
|
||||
const createManagerStep = createStep(
|
||||
"create-manager-step",
|
||||
async ({
|
||||
manager: managerData
|
||||
manager: managerData,
|
||||
}: Pick<CreateManagerWorkflowInput, "manager">,
|
||||
{ container }) => {
|
||||
const managerModuleService: ManagerModuleService =
|
||||
@@ -106,13 +106,13 @@ const createManagerWorkflow = createWorkflow<
|
||||
"create-manager",
|
||||
function (input) {
|
||||
const manager = createManagerStep({
|
||||
manager: input.manager
|
||||
manager: input.manager,
|
||||
})
|
||||
|
||||
setAuthAppMetadataStep({
|
||||
authIdentityId: input.authIdentityId,
|
||||
actorType: "manager",
|
||||
value: manager.id
|
||||
value: manager.id,
|
||||
})
|
||||
|
||||
return manager
|
||||
@@ -146,7 +146,7 @@ export const createRouteHighlights = [
|
||||
```ts title="src/api/manager/route.ts" highlights={createRouteHighlights}
|
||||
import type {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import createManagerWorkflow from "../../workflows/create-manager"
|
||||
@@ -174,8 +174,8 @@ export async function POST(
|
||||
.run({
|
||||
input: {
|
||||
manager: req.body,
|
||||
authIdentityId: req.auth_context.auth_identity_id
|
||||
}
|
||||
authIdentityId: req.auth_context.auth_identity_id,
|
||||
},
|
||||
})
|
||||
|
||||
res.status(200).json({ manager: result })
|
||||
@@ -213,7 +213,7 @@ export const config: MiddlewaresConfig = {
|
||||
method: "POST",
|
||||
middlewares: [
|
||||
authenticate("manager", ["session", "bearer"], {
|
||||
allowUnregistered: true
|
||||
allowUnregistered: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -239,9 +239,9 @@ For example, create the file `src/api/manager/me/route.ts` with the following co
|
||||
```ts title="src/api/manager/me/route.ts"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse
|
||||
} from "@medusajs/medusa";
|
||||
import ManagerModuleService from "../../../modules/manager/service";
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import ManagerModuleService from "../../../modules/manager/service"
|
||||
|
||||
export async function GET(
|
||||
req: AuthenticatedMedusaRequest,
|
||||
|
||||
@@ -37,8 +37,8 @@ module.exports = defineConfig({
|
||||
config: {
|
||||
google: {
|
||||
// provider options...
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -84,11 +84,11 @@ For example:
|
||||
```ts title="src/api/store/custom/route.ts"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import {
|
||||
ModuleRegistrationName
|
||||
ModuleRegistrationName,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
export async function GET(
|
||||
@@ -111,7 +111,7 @@ For example:
|
||||
import { SubscriberArgs } from "@medusajs/medusa"
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import {
|
||||
ModuleRegistrationName
|
||||
ModuleRegistrationName,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
export default async function subscriberHandler({
|
||||
@@ -131,7 +131,7 @@ For example:
|
||||
import { createStep } from "@medusajs/workflows-sdk"
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import {
|
||||
ModuleRegistrationName
|
||||
ModuleRegistrationName,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
export const metadata = {
|
||||
title: `How to Register a Customer with Email and Password`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this guide, you'll learn the steps to register and authenticate a customer.
|
||||
|
||||
<Note title="Steps Summary">
|
||||
|
||||
1. Send a `POST` request to `/auth/customer/emailpass`.
|
||||
2. Send a request to the [Create Customer API route](!api!/store#customers_postcustomers) passing the token obtained from the first step as a bearer token.
|
||||
3. Send a `POST` request to `/auth/customer/emailpass` to log-in.
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Obtain Authentication Token
|
||||
|
||||
Before registering and creating the customer, you must create an authentication identity that's associated with that customer.
|
||||
|
||||
To do that, use the `/auth/customer/{provider}` API route, where `{provider}` is the auth provider to use to handle authentication.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about the `/auth` route in [this document](../../auth/authentication-route/page.mdx)
|
||||
|
||||
</Note>
|
||||
|
||||
For example, send a `POST` request to `/auth/customer/emailpass`:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/auth/customer/emailpass' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"email": "customer@gmail.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Register Customer
|
||||
|
||||
Then, use the returned token in the header of the [Create Customer API route](!api!/store#customers_postcustomers):
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/store/customers' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer {token}' \
|
||||
--data-raw '{
|
||||
"email": "customer@gmail.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe"
|
||||
}'
|
||||
```
|
||||
|
||||
This creates and returns the customer.
|
||||
|
||||
---
|
||||
|
||||
## 3. Login Customer
|
||||
|
||||
Finally, to log-in the customer, send a `POST` request again to `/auth/customer/emailpass`:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/auth/customer/emailpass' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"email": "customer@gmail.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
You can now use the returned token to send authenticated requests as a customer.
|
||||
@@ -38,14 +38,14 @@ module.exports = defineConfig({
|
||||
config: {
|
||||
manual: {
|
||||
// provider options...
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ module.exports = defineConfig({
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -60,14 +60,14 @@ module.exports = defineConfig({
|
||||
apiKey: process.env.STRIPE_USD_API_KEY,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu
|
||||
request.scope.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
const data = await productModuleService.list({
|
||||
handle: "shirt"
|
||||
handle: "shirt",
|
||||
})
|
||||
|
||||
res.json({ product: data[0] })
|
||||
@@ -234,7 +234,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu
|
||||
const productModuleService = await initializeProductModule()
|
||||
|
||||
const data = await productModuleService.list({
|
||||
handle: "shirt"
|
||||
handle: "shirt",
|
||||
})
|
||||
|
||||
return NextResponse.json({ product: data[0] })
|
||||
|
||||
@@ -24,7 +24,7 @@ const promotion = await promotionModuleService.create({
|
||||
type: "percentage",
|
||||
target_type: "order",
|
||||
value: "10",
|
||||
currency_code: "usd"
|
||||
currency_code: "usd",
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -41,7 +41,7 @@ const promotion = await promotionModuleService.create({
|
||||
type: "percentage",
|
||||
target_type: "order",
|
||||
value: "10",
|
||||
currency_code: "usd"
|
||||
currency_code: "usd",
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
|
||||
@@ -60,7 +60,7 @@ const regions = await regionModuleService.create([
|
||||
name: "United States of America",
|
||||
currency_code: "usd",
|
||||
countries: ["us"],
|
||||
payment_providers: ["stripe"]
|
||||
payment_providers: ["stripe"],
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ const stores = await storeModuleService.create([
|
||||
{
|
||||
name: "Europe Store",
|
||||
supported_currency_codes: ["eur"],
|
||||
}
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ module.exports = defineConfig({
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ module.exports = defineConfig({
|
||||
jwt_secret: process.env.JWT_SECRET,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -245,16 +245,16 @@ You can create a B2B module that adds necessary data models to represent a B2B c
|
||||
Next, create the migration in the file `src/modules/b2b/migrations/Migration20240516081502.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/b2b/migrations/Migration20240516081502.ts"
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240516081502 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "company" ("id" text not null, "name" text not null, "city" text not null, "country_code" text not null, "customer_group_id" text not null, constraint "company_pkey" primary key ("id"));');
|
||||
this.addSql("create table if not exists \"company\" (\"id\" text not null, \"name\" text not null, \"city\" text not null, \"country_code\" text not null, \"customer_group_id\" text not null, constraint \"company_pkey\" primary key (\"id\"));")
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "company" cascade;');
|
||||
this.addSql("drop table if exists \"company\" cascade;")
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -295,9 +295,9 @@ export const mainServiceHighlights = [
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { ModuleJoinerConfig, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { Company } from "./models/company";
|
||||
import { CompanyDTO, CreateCompanyDTO } from "../../types/b2b";
|
||||
|
||||
import { Company } from "./models/company"
|
||||
import { CompanyDTO, CreateCompanyDTO } from "../../types/b2b"
|
||||
|
||||
type InjectedDependencies = {
|
||||
companyService: ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
@@ -329,9 +329,9 @@ export const mainServiceHighlights = [
|
||||
{
|
||||
name: ["company"],
|
||||
args: {
|
||||
entity: Company.name
|
||||
}
|
||||
}
|
||||
entity: Company.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
@@ -340,14 +340,14 @@ export const mainServiceHighlights = [
|
||||
primaryKey: "id",
|
||||
foreignKey: "customer_group_id",
|
||||
args: {
|
||||
methodSuffix: "CustomerGroups"
|
||||
}
|
||||
}
|
||||
]
|
||||
methodSuffix: "CustomerGroups",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async create (data: CreateCompanyDTO): Promise<CompanyDTO> {
|
||||
async create(data: CreateCompanyDTO): Promise<CompanyDTO> {
|
||||
const company = this.companyService_.create(data)
|
||||
|
||||
return company
|
||||
@@ -365,8 +365,8 @@ export const mainServiceHighlights = [
|
||||
Next, create the module definition at `src/modules/b2b/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/b2b/index.ts"
|
||||
import B2bModuleService from "./service";
|
||||
|
||||
import B2bModuleService from "./service"
|
||||
|
||||
export default {
|
||||
service: B2bModuleService,
|
||||
}
|
||||
@@ -381,10 +381,10 @@ export const mainServiceHighlights = [
|
||||
b2bModuleService: {
|
||||
resolve: "./modules/b2b",
|
||||
definition: {
|
||||
isQueryable: true
|
||||
}
|
||||
isQueryable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -411,15 +411,15 @@ export const workflowHighlights = [
|
||||
import {
|
||||
StepResponse,
|
||||
createStep,
|
||||
createWorkflow
|
||||
} from "@medusajs/workflows-sdk";
|
||||
createWorkflow,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
createCustomerGroupsWorkflow
|
||||
createCustomerGroupsWorkflow,
|
||||
} from "@medusajs/core-flows"
|
||||
import { CreateCustomerGroupDTO } from "@medusajs/types";
|
||||
import { CompanyDTO, CreateCompanyDTO } from "../types/b2b";
|
||||
import B2bModuleService from "../modules/b2b/service";
|
||||
|
||||
import { CreateCustomerGroupDTO } from "@medusajs/types"
|
||||
import { CompanyDTO, CreateCompanyDTO } from "../types/b2b"
|
||||
import B2bModuleService from "../modules/b2b/service"
|
||||
|
||||
export type CreateCompanyWorkflowInput = CreateCompanyDTO & {
|
||||
customer_group?: CreateCustomerGroupDTO
|
||||
}
|
||||
@@ -447,8 +447,8 @@ export const workflowHighlights = [
|
||||
container
|
||||
).run({
|
||||
input: {
|
||||
customersData: [customer_group]
|
||||
}
|
||||
customersData: [customer_group],
|
||||
},
|
||||
})
|
||||
|
||||
company.customer_group_id = result[0].id
|
||||
@@ -486,7 +486,7 @@ export const workflowHighlights = [
|
||||
"create-company",
|
||||
function (input) {
|
||||
const {
|
||||
company: companyData
|
||||
company: companyData,
|
||||
} = tryToCreateCustomerGroupStep(input)
|
||||
|
||||
const company = createCompanyStep({ companyData })
|
||||
@@ -506,11 +506,11 @@ export const workflowHighlights = [
|
||||
```ts title="src/api/admin/b2b/company/route.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse
|
||||
} from "@medusajs/medusa";
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
CreateCompanyWorkflowInput,
|
||||
createCompanyWorkflow
|
||||
createCompanyWorkflow,
|
||||
} from "../../../../workflows/create-company"
|
||||
|
||||
type CreateCompanyReq = CreateCompanyWorkflowInput
|
||||
@@ -521,11 +521,11 @@ export const workflowHighlights = [
|
||||
) {
|
||||
const { result } = await createCompanyWorkflow(req.scope)
|
||||
.run({
|
||||
input: req.body
|
||||
input: req.body,
|
||||
})
|
||||
|
||||
res.json({
|
||||
company: result.company
|
||||
company: result.company,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -698,11 +698,11 @@ export const checkCustomerHighlights = [
|
||||
]
|
||||
|
||||
```ts title="src/api/store/b2b/check-customer/route.ts" highlights={checkCustomerHighlights} collapsibleLines="1-5" expandButtonLabel="Show Imports"
|
||||
import type { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk";
|
||||
import { ICustomerModuleService } from "@medusajs/types";
|
||||
import B2bModuleService from "../../../../modules/b2b/service";
|
||||
|
||||
import type { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { ICustomerModuleService } from "@medusajs/types"
|
||||
import B2bModuleService from "../../../../modules/b2b/service"
|
||||
|
||||
export async function GET(
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
@@ -714,15 +714,15 @@ export const checkCustomerHighlights = [
|
||||
)
|
||||
|
||||
const customer = await customerModuleService.retrieve(req.auth.actor_id, {
|
||||
relations: ["groups"]
|
||||
relations: ["groups"],
|
||||
})
|
||||
|
||||
const companies = await b2bModuleService.list({
|
||||
customer_group_id: customer.groups.map((group) => group.id)
|
||||
customer_group_id: customer.groups.map((group) => group.id),
|
||||
})
|
||||
|
||||
res.json({
|
||||
is_b2b: companies.length > 0
|
||||
is_b2b: companies.length > 0,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -743,7 +743,7 @@ export const checkCustomerHighlights = [
|
||||
{
|
||||
matcher: "/store/b2b*",
|
||||
middlewares: [
|
||||
authenticate("store", ["bearer", "session"])
|
||||
authenticate("store", ["bearer", "session"]),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -96,9 +96,9 @@ export const restockModelHighlights = [
|
||||
import {
|
||||
Entity,
|
||||
PrimaryKey,
|
||||
Property
|
||||
} from "@mikro-orm/core";
|
||||
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
export class RestockNotification extends BaseEntity {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
@@ -126,16 +126,16 @@ export const restockModelHighlights = [
|
||||
Next, create the file `src/modules/restock-notification/migrations/Migration20240516140616.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/restock-notification/migrations/Migration20240516140616.ts"
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240516140616 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "restock_notification" ("id" text not null, "email" text not null, "variant_id" text not null, "sales_channel_id" text not null, constraint "restock_notification_pkey" primary key ("id"));');
|
||||
this.addSql("create table if not exists \"restock_notification\" (\"id\" text not null, \"email\" text not null, \"variant_id\" text not null, \"sales_channel_id\" text not null, constraint \"restock_notification_pkey\" primary key (\"id\"));")
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "restock_notification" cascade;');
|
||||
this.addSql("drop table if exists \"restock_notification\" cascade;")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -211,9 +211,9 @@ export const restockModuleService = [
|
||||
{
|
||||
name: "restock_notification",
|
||||
args: {
|
||||
entity: RestockNotification.name
|
||||
}
|
||||
}
|
||||
entity: RestockNotification.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
@@ -222,20 +222,20 @@ export const restockModuleService = [
|
||||
primaryKey: "id",
|
||||
foreignKey: "variant_id",
|
||||
args: {
|
||||
methodSuffix: "Variants"
|
||||
}
|
||||
methodSuffix: "Variants",
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: Modules.SALES_CHANNEL,
|
||||
alias: "sales_channel",
|
||||
primaryKey: "id",
|
||||
foreignKey: "sales_channel_id",
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async create (data: CreateRestockNotificationDTO): Promise<RestockNotificationDTO> {
|
||||
async create(data: CreateRestockNotificationDTO): Promise<RestockNotificationDTO> {
|
||||
const restockNotification = await this.restockNotificationService_.create(
|
||||
data
|
||||
)
|
||||
@@ -257,10 +257,10 @@ export const restockModuleService = [
|
||||
Next, create the module's definition file `src/modules/restock-notification/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/restock-notification/index.ts"
|
||||
import RestockNotificationModuleService from "./service";
|
||||
|
||||
import RestockNotificationModuleService from "./service"
|
||||
|
||||
export default {
|
||||
service: RestockNotificationModuleService
|
||||
service: RestockNotificationModuleService,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -273,10 +273,10 @@ export const restockModuleService = [
|
||||
"restockNotificationModuleService": {
|
||||
resolve: "./modules/restock-notification",
|
||||
definition: {
|
||||
isQueryable: true
|
||||
}
|
||||
isQueryable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -293,14 +293,14 @@ export const restockModuleService = [
|
||||
```ts title="src/api/store/restock-notification/route.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse
|
||||
} from "@medusajs/medusa";
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import RestockNotificationModuleService
|
||||
from "../../../modules/restock-notification/service";
|
||||
from "../../../modules/restock-notification/service"
|
||||
import {
|
||||
CreateRestockNotificationDTO
|
||||
} from "../../../types/restock-notification";
|
||||
|
||||
CreateRestockNotificationDTO,
|
||||
} from "../../../types/restock-notification"
|
||||
|
||||
type RestockNotificationReq = CreateRestockNotificationDTO
|
||||
|
||||
export async function POST(
|
||||
@@ -317,7 +317,7 @@ export const restockModuleService = [
|
||||
)
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -357,18 +357,18 @@ export const subscriberHighlights = [
|
||||
import {
|
||||
IInventoryServiceNext,
|
||||
INotificationModuleService,
|
||||
RemoteQueryFunction
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
Modules,
|
||||
remoteQueryObjectFromString
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
RestockNotificationDTO
|
||||
RestockNotificationDTO,
|
||||
} from "../types/restock-notification"
|
||||
import {
|
||||
RemoteLink
|
||||
RemoteLink,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import RestockNotificationModuleService
|
||||
from "../modules/restock-notification/service"
|
||||
@@ -376,7 +376,7 @@ export const subscriberHighlights = [
|
||||
// subscriber function
|
||||
export default async function inventoryItemUpdateHandler({
|
||||
data,
|
||||
container
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const remoteQuery: RemoteQueryFunction = container.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
@@ -406,7 +406,7 @@ export const subscriberHighlights = [
|
||||
|
||||
const inventoryVariantItems =
|
||||
await inventoryVariantLinkService.list({
|
||||
inventory_item_id: [inventoryItemId]
|
||||
inventory_item_id: [inventoryItemId],
|
||||
}) as {
|
||||
variant_id: string,
|
||||
inventory_item_id: string
|
||||
@@ -421,13 +421,13 @@ export const subscriberHighlights = [
|
||||
entryPoint: "restock_notification",
|
||||
fields: [
|
||||
"email",
|
||||
"variant.name"
|
||||
"variant.name",
|
||||
],
|
||||
variables: {
|
||||
filters: {
|
||||
variant_id: inventoryVariantItems[0].variant_id
|
||||
}
|
||||
}
|
||||
variant_id: inventoryVariantItems[0].variant_id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const restockNotifications: RestockNotificationDTO[] =
|
||||
@@ -444,8 +444,8 @@ export const subscriberHighlights = [
|
||||
const salesChannelLocations =
|
||||
await salesChannelLocationService.list({
|
||||
sales_channel_id: [
|
||||
restockNotification.sales_channel_id
|
||||
]
|
||||
restockNotification.sales_channel_id,
|
||||
],
|
||||
}) as {
|
||||
stock_location_id: string
|
||||
sales_channel_id: string
|
||||
@@ -474,9 +474,9 @@ export const subscriberHighlights = [
|
||||
template: "test_template",
|
||||
data: {
|
||||
variant_id: restockNotification.variant_id,
|
||||
variant_name: restockNotification.variant.title
|
||||
variant_name: restockNotification.variant.title,
|
||||
// other data...
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// delete the restock notification
|
||||
@@ -582,17 +582,17 @@ export const syncProductsWorkflowHighlight = [
|
||||
]
|
||||
|
||||
```ts title="src/workflows/sync-products.ts" highlights={syncProductsWorkflowHighlight}
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk";
|
||||
import { IProductModuleService, IStoreModuleService, ProductDTO, StoreDTO } from "@medusajs/types";
|
||||
import { StepResponse, createStep, createWorkflow } from "@medusajs/workflows-sdk";
|
||||
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService, IStoreModuleService, ProductDTO, StoreDTO } from "@medusajs/types"
|
||||
import { StepResponse, createStep, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
|
||||
type RetrieveStoreStepInput = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const retrieveStoreStep = createStep(
|
||||
"retrieve-store-step",
|
||||
async ({ id }: RetrieveStoreStepInput, {container}) => {
|
||||
async ({ id }: RetrieveStoreStepInput, { container }) => {
|
||||
const storeModuleService: IStoreModuleService =
|
||||
container.resolve(ModuleRegistrationName.STORE)
|
||||
|
||||
@@ -614,8 +614,8 @@ export const syncProductsWorkflowHighlight = [
|
||||
|
||||
const products = await productModuleService.list({
|
||||
updated_at: {
|
||||
$gt: last_sync_date
|
||||
}
|
||||
$gt: last_sync_date,
|
||||
},
|
||||
})
|
||||
|
||||
return new StepResponse({ products })
|
||||
@@ -634,7 +634,7 @@ export const syncProductsWorkflowHighlight = [
|
||||
)
|
||||
|
||||
const productsBeforeSync = await productSyncModuleService.list({
|
||||
id: products.map((product) => product.id)
|
||||
id: products.map((product) => product.id),
|
||||
})
|
||||
|
||||
for (const product of products) {
|
||||
@@ -642,7 +642,7 @@ export const syncProductsWorkflowHighlight = [
|
||||
}
|
||||
|
||||
return new StepResponse({}, {
|
||||
products: productsBeforeSync
|
||||
products: productsBeforeSync,
|
||||
})
|
||||
},
|
||||
async ({ products }, { container }) => {
|
||||
@@ -670,13 +670,13 @@ export const syncProductsWorkflowHighlight = [
|
||||
|
||||
await storeModuleService.update(store.id, {
|
||||
metadata: {
|
||||
last_sync_date: (new Date()).toString()
|
||||
}
|
||||
last_sync_date: (new Date()).toString(),
|
||||
},
|
||||
})
|
||||
|
||||
return new StepResponse({}, {
|
||||
id: store.id,
|
||||
last_sync_date: prevLastSyncDate
|
||||
last_sync_date: prevLastSyncDate,
|
||||
})
|
||||
},
|
||||
async ({ id, last_sync_date }, { container }) => {
|
||||
@@ -685,8 +685,8 @@ export const syncProductsWorkflowHighlight = [
|
||||
|
||||
await storeModuleService.update(id, {
|
||||
metadata: {
|
||||
last_sync_date
|
||||
}
|
||||
last_sync_date,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -701,19 +701,19 @@ export const syncProductsWorkflowHighlight = [
|
||||
"sync-products-workflow",
|
||||
function ({ store_id }: SyncProductsWorkflowInput) {
|
||||
const { store } = retrieveStoreStep({
|
||||
id: store_id
|
||||
id: store_id,
|
||||
})
|
||||
|
||||
const { products } = retrieveProductsToUpdateStep({
|
||||
last_sync_date: store.metadata.last_sync_date
|
||||
last_sync_date: store.metadata.last_sync_date,
|
||||
})
|
||||
|
||||
syncProductsStep({
|
||||
products
|
||||
products,
|
||||
})
|
||||
|
||||
updateStoreLastSyncStep({
|
||||
store
|
||||
store,
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -857,16 +857,16 @@ The `order.placed` event is currently not emitted.
|
||||
SubscriberConfig,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ModuleRegistrationName
|
||||
ModuleRegistrationName,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ICustomerModuleService,
|
||||
IOrderModuleService
|
||||
IOrderModuleService,
|
||||
} from "@medusajs/types"
|
||||
|
||||
export default async function orderCreatedHandler({
|
||||
data,
|
||||
container
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const orderId = "data" in data ? data.data.id : data.id
|
||||
|
||||
@@ -881,9 +881,9 @@ The `order.placed` event is currently not emitted.
|
||||
// check if VIP group exists
|
||||
const vipGroup = await customerModuleService
|
||||
.listCustomerGroups({
|
||||
name: "VIP"
|
||||
name: "VIP",
|
||||
}, {
|
||||
relations: ["customers"]
|
||||
relations: ["customers"],
|
||||
})
|
||||
|
||||
if (!vipGroup.length) {
|
||||
@@ -902,7 +902,7 @@ The `order.placed` event is currently not emitted.
|
||||
}
|
||||
|
||||
const [, count] = await orderModuleService.listAndCount({
|
||||
customer_id: order.customer_id
|
||||
customer_id: order.customer_id,
|
||||
})
|
||||
|
||||
if (count < 20) {
|
||||
@@ -912,7 +912,7 @@ The `order.placed` event is currently not emitted.
|
||||
// add customer to VIP group
|
||||
await customerModuleService.addCustomerToGroup({
|
||||
customer_group_id: vipGroup[0].id,
|
||||
customer_id: order.customer_id
|
||||
customer_id: order.customer_id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -986,20 +986,20 @@ export const newsletterHighlights = [
|
||||
SubscriberConfig,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ModuleRegistrationName
|
||||
ModuleRegistrationName,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import {
|
||||
NotificationModuleService
|
||||
NotificationModuleService,
|
||||
} from "@medusajs/notification"
|
||||
import {
|
||||
ICustomerModuleService,
|
||||
IProductModuleService,
|
||||
IStoreModuleService
|
||||
IStoreModuleService,
|
||||
} from "@medusajs/types"
|
||||
|
||||
export default async function productCreateHandler({
|
||||
data,
|
||||
container
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const productModuleService: IProductModuleService =
|
||||
container.resolve(ModuleRegistrationName.PRODUCT)
|
||||
@@ -1019,8 +1019,8 @@ export const newsletterHighlights = [
|
||||
|
||||
const products = await productModuleService.list({
|
||||
created_at: {
|
||||
$gt: store.metadata.last_newsletter_send_date
|
||||
}
|
||||
$gt: store.metadata.last_newsletter_send_date,
|
||||
},
|
||||
})
|
||||
|
||||
if (products.length < 10) {
|
||||
@@ -1035,15 +1035,15 @@ export const newsletterHighlights = [
|
||||
channel: "email",
|
||||
template: "newsletter_template",
|
||||
data: {
|
||||
products
|
||||
}
|
||||
products,
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
await storeModuleService.update(store.id, {
|
||||
metadata: {
|
||||
last_newsletter_send_date: (new Date()).toString()
|
||||
}
|
||||
last_newsletter_send_date: (new Date()).toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -130,9 +130,9 @@ The module will hold your custom data models and the service implementing digita
|
||||
// ...
|
||||
modules: {
|
||||
digitalProductModuleService: {
|
||||
resolve: "./modules/digital-product"
|
||||
}
|
||||
}
|
||||
resolve: "./modules/digital-product",
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -216,17 +216,17 @@ To represent a digital product, it's recommended to create a data model that has
|
||||
Create the file `src/modules/digital-product/migrations/Migration20240509093233.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/digital-product/migrations/Migration20240509093233.ts"
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240509093233 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "product_media" ("id" text not null, "name" text not null, "type" text check ("type" in (\'main\', \'preview\')) not null, "file_key" text not null, "mime_type" text not null, "variant_id" text not null, constraint "product_media_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_product_media_variant_id" ON "product_media" (variant_id);');
|
||||
this.addSql("create table if not exists \"product_media\" (\"id\" text not null, \"name\" text not null, \"type\" text check (\"type\" in ('main', 'preview')) not null, \"file_key\" text not null, \"mime_type\" text not null, \"variant_id\" text not null, constraint \"product_media_pkey\" primary key (\"id\"));")
|
||||
this.addSql("CREATE INDEX IF NOT EXISTS \"IDX_product_media_variant_id\" ON \"product_media\" (variant_id);")
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "product_media" cascade;');
|
||||
this.addSql("drop table if exists \"product_media\" cascade;")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -316,7 +316,7 @@ Medusa facilitates implementing data-management features by providing a service
|
||||
import {
|
||||
CreateProductMediaDTO,
|
||||
ProductMediaDTO,
|
||||
UpdateProductMediaDTO
|
||||
UpdateProductMediaDTO,
|
||||
} from "../../types/digital-product/product-media"
|
||||
|
||||
type InjectedDependencies = {
|
||||
@@ -340,8 +340,8 @@ Medusa facilitates implementing data-management features by providing a service
|
||||
|
||||
constructor(
|
||||
{
|
||||
productMediaService
|
||||
}: InjectedDependencies,
|
||||
productMediaService,
|
||||
}: InjectedDependencies
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
@@ -350,11 +350,11 @@ Medusa facilitates implementing data-management features by providing a service
|
||||
}
|
||||
|
||||
async create(
|
||||
data: CreateProductMediaDTO,
|
||||
data: CreateProductMediaDTO
|
||||
): Promise<ProductMediaDTO> {
|
||||
const productMedia = await this.productMediaService_
|
||||
.create(
|
||||
data,
|
||||
data
|
||||
)
|
||||
|
||||
return productMedia
|
||||
@@ -362,12 +362,12 @@ Medusa facilitates implementing data-management features by providing a service
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: UpdateProductMediaDTO,
|
||||
data: UpdateProductMediaDTO
|
||||
): Promise<ProductMediaDTO> {
|
||||
const productMedia = await this.productMediaService_
|
||||
.update({
|
||||
...data,
|
||||
id
|
||||
id,
|
||||
})
|
||||
|
||||
return productMedia
|
||||
@@ -430,9 +430,9 @@ The Medusa application resolves module relationships without creating an actual
|
||||
{
|
||||
name: "product_media",
|
||||
args: {
|
||||
entity: ProductMedia.name
|
||||
}
|
||||
}
|
||||
entity: ProductMedia.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
@@ -441,10 +441,10 @@ The Medusa application resolves module relationships without creating an actual
|
||||
primaryKey: "id",
|
||||
foreignKey: "variant_id",
|
||||
args: {
|
||||
methodSuffix: "Variants"
|
||||
}
|
||||
}
|
||||
]
|
||||
methodSuffix: "Variants",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -467,10 +467,10 @@ The Medusa application resolves module relationships without creating an actual
|
||||
digitalProductModuleService: {
|
||||
resolve: "./modules/digital-product",
|
||||
definition: {
|
||||
isQueryable: true
|
||||
}
|
||||
}
|
||||
}
|
||||
isQueryable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -518,7 +518,7 @@ To utilize the relationship to the Product Module, you use the remote query to f
|
||||
```ts title="src/types/digital-product/product-media.ts"
|
||||
import {
|
||||
ProductVariantDTO,
|
||||
CreateProductWorkflowInputDTO
|
||||
CreateProductWorkflowInputDTO,
|
||||
} from "@medusajs/types"
|
||||
|
||||
export enum MediaType {
|
||||
@@ -574,20 +574,20 @@ To utilize the relationship to the Product Module, you use the remote query to f
|
||||
createWorkflow,
|
||||
WorkflowData,
|
||||
createStep,
|
||||
StepResponse
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { createProductsWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
CreateProductMediaDTO,
|
||||
CreateProductMediaWorkflowInput,
|
||||
ProductMediaDTO
|
||||
ProductMediaDTO,
|
||||
} from "../../types/digital-product/product-media"
|
||||
import DigitalProductModuleService from
|
||||
"../../modules/digital-product/service"
|
||||
import { RemoteQueryFunction } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
const tryToCreateProductVariantStep = createStep(
|
||||
@@ -597,9 +597,9 @@ To utilize the relationship to the Product Module, you use the remote query to f
|
||||
const { result, errors } = await createProductsWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
products: [input.product]
|
||||
products: [input.product],
|
||||
},
|
||||
throwOnError: false
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (errors.length) {
|
||||
@@ -646,13 +646,13 @@ To utilize the relationship to the Product Module, you use the remote query to f
|
||||
"type",
|
||||
"file_key",
|
||||
"mime_type",
|
||||
"variant.*"
|
||||
"variant.*",
|
||||
],
|
||||
variables: {
|
||||
filters: {
|
||||
id: input.id
|
||||
}
|
||||
}
|
||||
id: input.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
@@ -690,14 +690,14 @@ To utilize the relationship to the Product Module, you use the remote query to f
|
||||
```ts title="src/api/admin/digital-products/route.ts" collapsibleLines="1-12" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import {
|
||||
CreateProductMediaWorkflowInput
|
||||
CreateProductMediaWorkflowInput,
|
||||
} from "../../../types/digital-product/product-media"
|
||||
import {
|
||||
createProductMediaWorkflow
|
||||
createProductMediaWorkflow,
|
||||
} from "../../../workflows/digital-product/create"
|
||||
|
||||
type CreateProductMediaReq = CreateProductMediaWorkflowInput
|
||||
@@ -709,15 +709,15 @@ To utilize the relationship to the Product Module, you use the remote query to f
|
||||
// validation omitted for simplicity
|
||||
const {
|
||||
result,
|
||||
errors
|
||||
errors,
|
||||
} = await createProductMediaWorkflow(req.scope)
|
||||
.run({
|
||||
input: {
|
||||
data: {
|
||||
...req.body
|
||||
}
|
||||
...req.body,
|
||||
},
|
||||
},
|
||||
throwOnError: false
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (errors.length) {
|
||||
@@ -728,7 +728,7 @@ To utilize the relationship to the Product Module, you use the remote query to f
|
||||
}
|
||||
|
||||
res.json({
|
||||
product_media: result
|
||||
product_media: result,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -808,7 +808,7 @@ To utilize the relationship to the Product Module, you use the remote query to f
|
||||
import { RemoteQueryFunction } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
@@ -830,14 +830,14 @@ To utilize the relationship to the Product Module, you use the remote query to f
|
||||
"file_key",
|
||||
"mime_type",
|
||||
"variant.*",
|
||||
"variant.product.*"
|
||||
"variant.product.*",
|
||||
],
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
|
||||
res.json({
|
||||
product_medias: result
|
||||
product_medias: result,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -888,8 +888,8 @@ In your customizations, you send requests to the API routes you created to creat
|
||||
To create the UI route, create the file `src/admin/routes/product-media/page.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/routes/product-media/page.tsx" badgeLabel="Medusa Application" collapsibleLines="1-13" expandButtonLabel="Show Imports"
|
||||
import { defineRouteConfig } from "@medusajs/admin-shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { defineRouteConfig } from "@medusajs/admin-shared"
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
Container,
|
||||
Heading,
|
||||
@@ -993,7 +993,7 @@ In your customizations, you send requests to the API routes you created to creat
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
Drawer
|
||||
Drawer,
|
||||
} from "@medusajs/ui"
|
||||
|
||||
type Props = {
|
||||
@@ -1017,13 +1017,13 @@ In your customizations, you send requests to the API routes you created to creat
|
||||
setLoading(true)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("files", file);
|
||||
|
||||
formData.append("files", file)
|
||||
|
||||
// upload file
|
||||
fetch(`/admin/uploads`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ files }) => {
|
||||
@@ -1032,7 +1032,7 @@ In your customizations, you send requests to the API routes you created to creat
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
@@ -1043,11 +1043,11 @@ In your customizations, you send requests to the API routes you created to creat
|
||||
title: productName,
|
||||
variants: [
|
||||
{
|
||||
title: name
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
title: name,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(() => {
|
||||
@@ -1216,7 +1216,7 @@ In the subscriber, you can send a notification, such as an email, to the custome
|
||||
import {
|
||||
IOrderModuleService,
|
||||
IFileModuleService,
|
||||
INotificationModuleService
|
||||
INotificationModuleService,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ModuleRegistrationName,
|
||||
@@ -1245,7 +1245,7 @@ In the subscriber, you can send a notification, such as an email, to the custome
|
||||
const orderId = "data" in data ? data.data.id : data.id
|
||||
|
||||
const order = await orderModuleService.retrieve(orderId, {
|
||||
relations: ["items"]
|
||||
relations: ["items"],
|
||||
})
|
||||
|
||||
// find product medias in the order
|
||||
@@ -1253,7 +1253,7 @@ In the subscriber, you can send a notification, such as an email, to the custome
|
||||
for (const item of order.items) {
|
||||
const productMedias = await digitalProductModuleService
|
||||
.list({
|
||||
variant_id: [item.variant_id]
|
||||
variant_id: [item.variant_id],
|
||||
})
|
||||
|
||||
const downloadUrls = await Promise.all(
|
||||
@@ -1325,12 +1325,12 @@ To implement this, create a storefront API Route that allows you to fetch the di
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
MediaType
|
||||
MediaType,
|
||||
} from "../../../types/digital-product/product-media"
|
||||
import { RemoteQueryFunction } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
export const GET = async (
|
||||
@@ -1347,20 +1347,20 @@ To implement this, create a storefront API Route that allows you to fetch the di
|
||||
"name",
|
||||
"file_key",
|
||||
"mime_type",
|
||||
"variant.*"
|
||||
"variant.*",
|
||||
],
|
||||
variables: {
|
||||
filters: {
|
||||
type: MediaType.PREVIEW
|
||||
type: MediaType.PREVIEW,
|
||||
},
|
||||
// omitting pagination for simplicity
|
||||
skip: 0
|
||||
}
|
||||
skip: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
rows,
|
||||
metadata: { count }
|
||||
metadata: { count },
|
||||
} = await remoteQuery(query)
|
||||
|
||||
res.json({
|
||||
@@ -2383,11 +2383,11 @@ After the customer purchases the digital product you can show a download button
|
||||
createWorkflow,
|
||||
transform,
|
||||
createStep,
|
||||
StepResponse
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
IOrderModuleService,
|
||||
IFileModuleService
|
||||
IFileModuleService,
|
||||
} from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import DigitalProductModuleService from "../../modules/digital-product/service"
|
||||
@@ -2405,9 +2405,9 @@ After the customer purchases the digital product you can show a download button
|
||||
"check-variant-purchased-step",
|
||||
async ({
|
||||
variant_id,
|
||||
customer_id
|
||||
customer_id,
|
||||
}: CheckVariantStepInput, {
|
||||
container
|
||||
container,
|
||||
}) => {
|
||||
const orderModuleService: IOrderModuleService =
|
||||
container.resolve(ModuleRegistrationName.ORDER)
|
||||
@@ -2489,22 +2489,22 @@ After the customer purchases the digital product you can show a download button
|
||||
checkVariantPurchasedStep(input)
|
||||
|
||||
const { product_media } = getProductMediaStep({
|
||||
variant_id: input.variant_id
|
||||
variant_id: input.variant_id,
|
||||
})
|
||||
|
||||
const url = getFileUrlStep({
|
||||
id: product_media.file_key
|
||||
id: product_media.file_key,
|
||||
})
|
||||
|
||||
const result = transform(
|
||||
{
|
||||
url,
|
||||
product_media
|
||||
product_media,
|
||||
},
|
||||
(transformInput) => ({
|
||||
url: transformInput.url,
|
||||
name: transformInput.product_media.name,
|
||||
mime_type: transformInput.product_media.mime_type
|
||||
mime_type: transformInput.product_media.mime_type,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -2528,7 +2528,7 @@ After the customer purchases the digital product you can show a download button
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
getPurchasedProductMediaUrlWorkflow
|
||||
getPurchasedProductMediaUrlWorkflow,
|
||||
} from "../../../../../workflows/digital-product/get-url"
|
||||
|
||||
export const GET = async (
|
||||
@@ -2540,8 +2540,8 @@ After the customer purchases the digital product you can show a download button
|
||||
).run({
|
||||
input: {
|
||||
variant_id: req.params.variant_id,
|
||||
customer_id: req.user.customer_id
|
||||
}
|
||||
customer_id: req.user.customer_id,
|
||||
},
|
||||
})
|
||||
|
||||
res.json(result)
|
||||
@@ -2553,7 +2553,7 @@ After the customer purchases the digital product you can show a download button
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import {
|
||||
type MiddlewaresConfig,
|
||||
authenticate
|
||||
authenticate,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
@@ -2561,7 +2561,7 @@ After the customer purchases the digital product you can show a download button
|
||||
{
|
||||
matcher: "/store/product-media/download/*",
|
||||
middlewares: [
|
||||
authenticate("store", ["session", "bearer"])
|
||||
authenticate("store", ["session", "bearer"]),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -67,12 +67,12 @@ export const serviceHighlights = [
|
||||
this.client_ = axios.create({
|
||||
baseURL: `https://api.erp-example.com`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`
|
||||
}
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async getProductData (id: string) {
|
||||
async getProductData(id: string) {
|
||||
const { data: erpProduct } = await this.client_.get(`/product/${id}`)
|
||||
|
||||
return erpProduct
|
||||
@@ -109,10 +109,10 @@ export const serviceHighlights = [
|
||||
Then, create the module's definition file at `src/modules/erp/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/erp/index.ts"
|
||||
import ErpModuleService from "./service";
|
||||
|
||||
import ErpModuleService from "./service"
|
||||
|
||||
export default {
|
||||
service: ErpModuleService
|
||||
service: ErpModuleService,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -125,10 +125,10 @@ export const serviceHighlights = [
|
||||
erpModuleService: {
|
||||
resolve: "./modules/erp",
|
||||
options: {
|
||||
apiKey: process.env.ERP_API_KEY
|
||||
}
|
||||
apiKey: process.env.ERP_API_KEY,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ Create a Marketplace Module that holds and manages these relationships.
|
||||
Then, create the file `src/modules/marketplace/models/store-user.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/marketplace/models/store-user.ts"
|
||||
import { BaseEntity } from "@medusajs/utils";
|
||||
import { BaseEntity } from "@medusajs/utils"
|
||||
import { Entity, PrimaryKey, Property } from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
@@ -83,7 +83,7 @@ Create a Marketplace Module that holds and manages these relationships.
|
||||
Next, create the file `src/modules/marketplace/models/store-product.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/marketplace/models/store-product.ts"
|
||||
import { BaseEntity } from "@medusajs/utils";
|
||||
import { BaseEntity } from "@medusajs/utils"
|
||||
import { Entity, PrimaryKey, Property } from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
@@ -106,7 +106,7 @@ Create a Marketplace Module that holds and manages these relationships.
|
||||
Finally, create the file `src/modules/marketplace/models/store-order.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/marketplace/models/store-order.ts"
|
||||
import { BaseEntity } from "@medusajs/utils";
|
||||
import { BaseEntity } from "@medusajs/utils"
|
||||
import { Entity, PrimaryKey, Property } from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
@@ -138,24 +138,24 @@ Create a Marketplace Module that holds and manages these relationships.
|
||||
To reflect these changes on the database, create the migration `src/modules/marketplace/migrations/Migration20240514143248.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/marketplace/migrations/Migration20240514143248.ts"
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240514143248 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "store_order" ("id" varchar(255) not null, "store_id" text not null, "order_id" text not null, "parent_order_id" text not null, constraint "store_order_pkey" primary key ("id"));');
|
||||
this.addSql("create table if not exists \"store_order\" (\"id\" varchar(255) not null, \"store_id\" text not null, \"order_id\" text not null, \"parent_order_id\" text not null, constraint \"store_order_pkey\" primary key (\"id\"));")
|
||||
|
||||
this.addSql('create table if not exists "store_product" ("id" varchar(255) not null, "store_id" text not null, "product_id" text not null, constraint "store_product_pkey" primary key ("id"));');
|
||||
this.addSql("create table if not exists \"store_product\" (\"id\" varchar(255) not null, \"store_id\" text not null, \"product_id\" text not null, constraint \"store_product_pkey\" primary key (\"id\"));")
|
||||
|
||||
this.addSql('create table if not exists "store_user" ("id" varchar(255) not null, "store_id" text not null, "user_id" text not null, constraint "store_user_pkey" primary key ("id"));');
|
||||
this.addSql("create table if not exists \"store_user\" (\"id\" varchar(255) not null, \"store_id\" text not null, \"user_id\" text not null, constraint \"store_user_pkey\" primary key (\"id\"));")
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "store_order" cascade;');
|
||||
this.addSql("drop table if exists \"store_order\" cascade;")
|
||||
|
||||
this.addSql('drop table if exists "store_product" cascade;');
|
||||
this.addSql("drop table if exists \"store_product\" cascade;")
|
||||
|
||||
this.addSql('drop table if exists "store_user" cascade;');
|
||||
this.addSql("drop table if exists \"store_user\" cascade;")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -173,7 +173,7 @@ Create a Marketplace Module that holds and manages these relationships.
|
||||
StoreDTO,
|
||||
UserDTO,
|
||||
ProductDTO,
|
||||
OrderDTO
|
||||
OrderDTO,
|
||||
} from "@medusajs/types"
|
||||
|
||||
export type StoreUserDTO = {
|
||||
@@ -231,21 +231,21 @@ export const mainServiceHighlights = [
|
||||
|
||||
```ts title="src/modules/marketplace/service.ts" highlights={mainServiceHighlights} collapsibleLines="1-17" expandButtonLabel="Show Imports"
|
||||
import { ModulesSdkUtils, Modules } from "@medusajs/utils"
|
||||
import StoreUser from "./models/store-user";
|
||||
import StoreProduct from "./models/store-product";
|
||||
import StoreOrder from "./models/store-order";
|
||||
import StoreUser from "./models/store-user"
|
||||
import StoreProduct from "./models/store-product"
|
||||
import StoreOrder from "./models/store-order"
|
||||
import {
|
||||
CreateStoreOrderDTO,
|
||||
CreateStoreProductDTO,
|
||||
CreateStoreUserDTO,
|
||||
StoreOrderDTO,
|
||||
StoreProductDTO,
|
||||
StoreUserDTO
|
||||
} from "../../types/marketplace";
|
||||
StoreUserDTO,
|
||||
} from "../../types/marketplace"
|
||||
import {
|
||||
ModuleJoinerConfig,
|
||||
ModulesSdkTypes
|
||||
} from "@medusajs/types";
|
||||
ModulesSdkTypes,
|
||||
} from "@medusajs/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
storeUserService: ModulesSdkTypes.InternalModuleService<
|
||||
@@ -273,7 +273,7 @@ export const mainServiceHighlights = [
|
||||
|
||||
const generateMethodsFor = [
|
||||
StoreProduct,
|
||||
StoreOrder
|
||||
StoreOrder,
|
||||
]
|
||||
|
||||
class MarketplaceModuleService extends ModulesSdkUtils
|
||||
@@ -297,7 +297,7 @@ export const mainServiceHighlights = [
|
||||
constructor({
|
||||
storeUserService,
|
||||
storeProductService,
|
||||
storeOrderService
|
||||
storeOrderService,
|
||||
}: InjectedDependencies) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
@@ -313,50 +313,50 @@ export const mainServiceHighlights = [
|
||||
{
|
||||
name: ["store_user"],
|
||||
args: {
|
||||
entity: StoreUser.name
|
||||
}
|
||||
entity: StoreUser.name,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["store_product"],
|
||||
args: {
|
||||
entity: StoreProduct.name,
|
||||
methodSuffix: "StoreProducts"
|
||||
}
|
||||
methodSuffix: "StoreProducts",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["store_order"],
|
||||
args: {
|
||||
entity: StoreOrder.name,
|
||||
methodSuffix: "StoreOrders"
|
||||
}
|
||||
}
|
||||
methodSuffix: "StoreOrders",
|
||||
},
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.STORE,
|
||||
alias: "store",
|
||||
primaryKey: "id",
|
||||
foreignKey: "store_id"
|
||||
foreignKey: "store_id",
|
||||
},
|
||||
{
|
||||
serviceName: Modules.USER,
|
||||
alias: "user",
|
||||
primaryKey: "id",
|
||||
foreignKey: "user_id"
|
||||
foreignKey: "user_id",
|
||||
},
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
alias: "product",
|
||||
primaryKey: "id",
|
||||
foreignKey: "product_id"
|
||||
foreignKey: "product_id",
|
||||
},
|
||||
{
|
||||
serviceName: Modules.ORDER,
|
||||
alias: "order",
|
||||
primaryKey: "id",
|
||||
foreignKey: "order_id"
|
||||
}
|
||||
]
|
||||
foreignKey: "order_id",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ export const mainServiceHighlights = [
|
||||
Finally, create the module definition at `src/modules/marketplace/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/marketplace/index.ts"
|
||||
import MarketplaceModuleService from "./service";
|
||||
import MarketplaceModuleService from "./service"
|
||||
|
||||
export default {
|
||||
service: MarketplaceModuleService,
|
||||
@@ -417,10 +417,10 @@ export const mainServiceHighlights = [
|
||||
marketplaceModuleService: {
|
||||
resolve: "./modules/marketplace",
|
||||
definition: {
|
||||
isQueryable: true
|
||||
}
|
||||
isQueryable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -463,13 +463,13 @@ export const userSubscriberHighlights = [
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
IUserModuleService,
|
||||
IStoreModuleService
|
||||
IStoreModuleService,
|
||||
} from "@medusajs/types"
|
||||
import MarketplaceModuleService from "../modules/marketplace/service"
|
||||
|
||||
export default async function userCreatedHandler({
|
||||
data,
|
||||
container
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const { id } = data.data || { data }
|
||||
const userModuleService: IUserModuleService = container.resolve(
|
||||
@@ -485,12 +485,12 @@ export const userSubscriberHighlights = [
|
||||
const user = await userModuleService.retrieve(id)
|
||||
|
||||
const store = await storeModuleService.create({
|
||||
name: `${user.first_name}'s Store`
|
||||
name: `${user.first_name}'s Store`,
|
||||
})
|
||||
|
||||
const storeUser = await marketplaceModuleService.create({
|
||||
store_id: store.id,
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
})
|
||||
|
||||
console.log(`Created StoreUser ${storeUser.id}`)
|
||||
@@ -551,7 +551,7 @@ export const productSubscriberHighlights = [
|
||||
|
||||
export default async function productCreateHandler({
|
||||
data,
|
||||
container
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const { id } = data.data || data
|
||||
const productModuleService: IProductModuleService = container.resolve(
|
||||
@@ -570,7 +570,7 @@ export const productSubscriberHighlights = [
|
||||
|
||||
await marketplaceModuleService.createStoreProduct({
|
||||
store_id: product.metadata.store_id as string,
|
||||
product_id: id
|
||||
product_id: id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -646,13 +646,13 @@ export const productRoutesHighlights = [
|
||||
```ts title="src/api/admin/marketplace/products/route.ts" highlights={productRoutesHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { RemoteQueryFunction } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
remoteQueryObjectFromString,
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError
|
||||
MedusaError,
|
||||
} from "@medusajs/utils"
|
||||
import MarketplaceModuleService
|
||||
from "../../../../modules/marketplace/service"
|
||||
@@ -670,7 +670,7 @@ export const productRoutesHighlights = [
|
||||
)
|
||||
|
||||
const storeUsers = await marketplaceModuleService.list({
|
||||
user_id: req.auth.actor_id
|
||||
user_id: req.auth.actor_id,
|
||||
})
|
||||
|
||||
if (!storeUsers.length) {
|
||||
@@ -683,20 +683,20 @@ export const productRoutesHighlights = [
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "store_product",
|
||||
fields: [
|
||||
"product.*"
|
||||
"product.*",
|
||||
],
|
||||
variables: {
|
||||
filters: {
|
||||
store_id: storeUsers[0].store_id
|
||||
}
|
||||
}
|
||||
store_id: storeUsers[0].store_id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
|
||||
res.json({
|
||||
store_id: storeUsers[0].store_id,
|
||||
products: result.map((data) => data.product)
|
||||
products: result.map((data) => data.product),
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -712,7 +712,7 @@ export const productRoutesHighlights = [
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import {
|
||||
MiddlewaresConfig,
|
||||
authenticate
|
||||
authenticate,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
@@ -779,12 +779,12 @@ export const orderSubscriberHighlights = [
|
||||
```ts title="src/subscribers/order-created.ts" highlights={orderSubscriberHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
SubscriberArgs,
|
||||
SubscriberConfig
|
||||
SubscriberConfig,
|
||||
} from "@medusajs/medusa"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
IOrderModuleService,
|
||||
CreateOrderDTO
|
||||
CreateOrderDTO,
|
||||
} from "@medusajs/types"
|
||||
import MarketplaceModuleService
|
||||
from "../modules/marketplace/service"
|
||||
@@ -792,7 +792,7 @@ export const orderSubscriberHighlights = [
|
||||
|
||||
export default async function orderCreatedHandler({
|
||||
data,
|
||||
container
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const { id } = data.data || data
|
||||
const orderModuleService: IOrderModuleService =
|
||||
@@ -808,13 +808,13 @@ export const orderSubscriberHighlights = [
|
||||
const storeToOrders: Record<string, CreateOrderDTO> = {}
|
||||
|
||||
const order = await orderModuleService.retrieve(id, {
|
||||
relations: ["items"]
|
||||
relations: ["items"],
|
||||
})
|
||||
|
||||
await Promise.all(order.items?.map(async (item) => {
|
||||
const storeProduct = await marketplaceModuleService
|
||||
.listStoreProducts({
|
||||
product_id: item.product_id
|
||||
product_id: item.product_id,
|
||||
})
|
||||
|
||||
if (!storeProduct.length) {
|
||||
@@ -827,7 +827,7 @@ export const orderSubscriberHighlights = [
|
||||
const { id, ...orderDetails } = order
|
||||
storeToOrders[storeId] = {
|
||||
...orderDetails,
|
||||
items: []
|
||||
items: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,7 +850,7 @@ export const orderSubscriberHighlights = [
|
||||
// associate the order as-is with the store.
|
||||
await marketplaceModuleService.createStoreOrder({
|
||||
store_id: storeToOrdersKeys[0],
|
||||
order_id: order.id
|
||||
order_id: order.id,
|
||||
})
|
||||
|
||||
return
|
||||
@@ -861,13 +861,13 @@ export const orderSubscriberHighlights = [
|
||||
storeToOrdersKeys.map(async (storeId) => {
|
||||
const { result } = await createOrdersWorkflow(container)
|
||||
.run({
|
||||
input: storeToOrders[storeId]
|
||||
input: storeToOrders[storeId],
|
||||
})
|
||||
|
||||
await marketplaceModuleService.createStoreOrder({
|
||||
store_id: storeId,
|
||||
order_id: result.id,
|
||||
parent_order_id: order.id
|
||||
parent_order_id: order.id,
|
||||
})
|
||||
})
|
||||
)
|
||||
@@ -928,7 +928,7 @@ export const orderRoutesHighlights = [
|
||||
```ts title="src/api/admin/marketplace/orders/route.ts" highlights={orderRoutesHighlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { RemoteQueryFunction } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
@@ -951,25 +951,25 @@ export const orderRoutesHighlights = [
|
||||
)
|
||||
|
||||
const storeUsers = await marketplaceModuleService.list({
|
||||
user_id: req.auth.actor_id
|
||||
user_id: req.auth.actor_id,
|
||||
})
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "store_order",
|
||||
fields: [
|
||||
"order.*"
|
||||
"order.*",
|
||||
],
|
||||
variables: {
|
||||
filters: {
|
||||
store_id: storeUsers[0].store_id
|
||||
}
|
||||
}
|
||||
store_id: storeUsers[0].store_id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
|
||||
res.json({
|
||||
orders: result.map((data) => data.order)
|
||||
orders: result.map((data) => data.order),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
useState,
|
||||
} from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useRegion } from "./region"
|
||||
@@ -73,11 +73,11 @@ export const CartProvider = ({ children }: CartProviderProps) => {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
region_id: region.id,
|
||||
})
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart: dataCart }) => {
|
||||
@@ -87,7 +87,7 @@ export const CartProvider = ({ children }: CartProviderProps) => {
|
||||
} else {
|
||||
// retrieve cart
|
||||
fetch(`http://localhost:9000/store/carts/${cartId}`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart: dataCart }) => {
|
||||
@@ -105,7 +105,7 @@ export const CartProvider = ({ children }: CartProviderProps) => {
|
||||
<CartContext.Provider value={{
|
||||
cart,
|
||||
setCart,
|
||||
refreshCart
|
||||
refreshCart,
|
||||
}}>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
@@ -147,7 +147,7 @@ const inter = Inter({ subsets: ["latin"] })
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
|
||||
@@ -31,11 +31,11 @@ export const fetchHighlights = [
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
region_id: region.id,
|
||||
})
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
@@ -58,9 +58,9 @@ export const highlights = [
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react"
|
||||
// other imports...
|
||||
|
||||
|
||||
export default function Home() {
|
||||
// TODO assuming you have the region retrieved
|
||||
const region = {
|
||||
@@ -82,11 +82,11 @@ export const highlights = [
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
region_id: region.id,
|
||||
})
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
@@ -130,8 +130,8 @@ fetch(`http://localhost:9000/store/carts`, {
|
||||
// ...
|
||||
body: JSON.stringify({
|
||||
// ...
|
||||
customer_id: customer.id
|
||||
})
|
||||
customer_id: customer.id,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
|
||||
@@ -34,12 +34,12 @@ const addToCart = (variant_id: string) => {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
variant_id,
|
||||
quantity: 1,
|
||||
})
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
@@ -89,11 +89,11 @@ const updateQuantity = (
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
quantity
|
||||
})
|
||||
quantity,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const fetchHighlights = [
|
||||
|
||||
```ts highlights={fetchHighlights}
|
||||
fetch(`http://localhost:9000/store/carts/${cartId}`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
@@ -47,7 +47,7 @@ export const highlights = [
|
||||
import { useEffect, useState } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function Cart () {
|
||||
export default function Cart() {
|
||||
const [cart, setCart] = useState<
|
||||
HttpTypes.StoreCart
|
||||
>()
|
||||
@@ -64,7 +64,7 @@ export const highlights = [
|
||||
}
|
||||
|
||||
fetch(`http://localhost:9000/store/carts/${cartId}`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart: dataCart }) => {
|
||||
|
||||
@@ -29,11 +29,11 @@ fetch(`http://localhost:9000/store/carts/${cartId}`, {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
region_id: "new_id"
|
||||
})
|
||||
region_id: "new_id",
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
@@ -61,11 +61,11 @@ fetch(`http://localhost:9000/store/carts/${cartId}`, {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
customer_id: "logged_in_id"
|
||||
})
|
||||
customer_id: "logged_in_id",
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
|
||||
@@ -29,19 +29,19 @@ For example:
|
||||
city,
|
||||
country_code,
|
||||
province,
|
||||
phone
|
||||
phone,
|
||||
}
|
||||
|
||||
fetch(`http://localhost:9000/store/carts/${cartId}`, {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shipping_address: address,
|
||||
billing_address: address
|
||||
})
|
||||
billing_address: address,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
@@ -64,10 +64,10 @@ export const highlights = [
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCart } from "../../../providers/cart";
|
||||
|
||||
export default function CheckoutAddressStep () {
|
||||
import { useState } from "react"
|
||||
import { useCart } from "../../../providers/cart"
|
||||
|
||||
export default function CheckoutAddressStep() {
|
||||
const { cart, setCart } = useCart()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [firstName, setFirstName] = useState("")
|
||||
@@ -99,19 +99,19 @@ export const highlights = [
|
||||
city,
|
||||
country_code: countryCode,
|
||||
province,
|
||||
phone: phoneNumber
|
||||
phone: phoneNumber,
|
||||
}
|
||||
|
||||
fetch(`http://localhost:9000/store/carts/${cart.id}`, {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shipping_address: address,
|
||||
billing_address: address
|
||||
})
|
||||
billing_address: address,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart: updatedCart }) => {
|
||||
|
||||
@@ -17,7 +17,7 @@ fetch(
|
||||
`http://localhost:9000/store/carts/${cartId}/complete`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST"
|
||||
method: "POST",
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
@@ -65,7 +65,7 @@ export const highlights = [
|
||||
import { useState } from "react"
|
||||
import { useCart } from "../../providers/cart"
|
||||
|
||||
export default function SystemDefaultPayment () {
|
||||
export default function SystemDefaultPayment() {
|
||||
const { cart, refreshCart } = useCart()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function SystemDefaultPayment () {
|
||||
`http://localhost:9000/store/carts/${cart.id}/complete`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST"
|
||||
method: "POST",
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
@@ -26,11 +26,11 @@ For example:
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email
|
||||
})
|
||||
email,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
@@ -55,7 +55,7 @@ export const highlights = [
|
||||
import { useState } from "react"
|
||||
import { useCart } from "../../../providers/cart"
|
||||
|
||||
export default function CheckoutEmailStep () {
|
||||
export default function CheckoutEmailStep() {
|
||||
const { cart, setCart } = useCart()
|
||||
const [email, setEmail] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -80,11 +80,11 @@ export const highlights = [
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email
|
||||
})
|
||||
email,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart: updatedCart }) => {
|
||||
|
||||
@@ -57,7 +57,7 @@ export const fetchHighlights = [
|
||||
`http://localhost:9000/store/payment-providers?region_id=${
|
||||
cart.region_id
|
||||
}`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
@@ -77,14 +77,14 @@ export const fetchHighlights = [
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cart_id: cart.id,
|
||||
region_id: cart.region_id,
|
||||
currency_code: cart.currency_code,
|
||||
amount: cart.total
|
||||
})
|
||||
amount: cart.total,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
@@ -100,21 +100,21 @@ export const fetchHighlights = [
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider_id: selectedPaymentProviderId
|
||||
})
|
||||
provider_id: selectedPaymentProviderId,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
// re-fetch cart
|
||||
const {
|
||||
cart: updatedCart
|
||||
cart: updatedCart,
|
||||
} = await fetch(
|
||||
`http://localhost:9000/store/carts/${cart.id}`,
|
||||
{
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
@@ -171,14 +171,14 @@ export const highlights = [
|
||||
import { useCart } from "../../../providers/cart"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function CheckoutPaymentStep () {
|
||||
export default function CheckoutPaymentStep() {
|
||||
const { cart, setCart } = useCart()
|
||||
const [paymentProviders, setPaymentProviders] = useState<
|
||||
HttpTypes.StorePaymentProvider[]
|
||||
>([])
|
||||
const [
|
||||
selectedPaymentProvider,
|
||||
setSelectedPaymentProvider
|
||||
setSelectedPaymentProvider,
|
||||
] = useState<string | undefined>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
@@ -190,7 +190,7 @@ export const highlights = [
|
||||
fetch(`http://localhost:9000/store/payment-providers?region_id=${
|
||||
cart.region_id
|
||||
}`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ payment_providers }) => {
|
||||
@@ -221,14 +221,14 @@ export const highlights = [
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cart_id: cart.id,
|
||||
region_id: cart.region_id,
|
||||
currency_code: cart.currency_code,
|
||||
amount: cart.total
|
||||
})
|
||||
amount: cart.total,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
@@ -244,21 +244,21 @@ export const highlights = [
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider_id: selectedPaymentProvider
|
||||
})
|
||||
provider_id: selectedPaymentProvider,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
// re-fetch cart
|
||||
const {
|
||||
cart: updatedCart
|
||||
cart: updatedCart,
|
||||
} = await fetch(
|
||||
`http://localhost:9000/store/carts/${cart.id}`,
|
||||
{
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
@@ -75,7 +75,7 @@ import {
|
||||
CardElement,
|
||||
Elements,
|
||||
useElements,
|
||||
useStripe
|
||||
useStripe,
|
||||
} from "@stripe/react-stripe-js"
|
||||
import { loadStripe } from "@stripe/stripe-js"
|
||||
import { useCart } from "../../providers/cart"
|
||||
@@ -102,7 +102,7 @@ export default function StripePayment() {
|
||||
}
|
||||
|
||||
const StripeForm = ({
|
||||
clientSecret
|
||||
clientSecret,
|
||||
}: {
|
||||
clientSecret: string | undefined
|
||||
}) => {
|
||||
@@ -157,7 +157,7 @@ const StripeForm = ({
|
||||
`http://localhost:9000/store/carts/${cart.id}/complete`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST"
|
||||
method: "POST",
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
@@ -32,7 +32,7 @@ export const fetchHighlights = [
|
||||
`http://localhost:9000/store/shipping-options?cart_id=${
|
||||
cart.id
|
||||
}`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
@@ -49,15 +49,15 @@ export const fetchHighlights = [
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
option_id: selectedShippingOptionId,
|
||||
data: {
|
||||
// TODO add any data necessary for
|
||||
// fulfillment provider
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart }) => {
|
||||
@@ -84,7 +84,7 @@ export const highlights = [
|
||||
import { useCart } from "../../../providers/cart"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function CheckoutShippingStep () {
|
||||
export default function CheckoutShippingStep() {
|
||||
const { cart, setCart } = useCart()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [shippingOptions, setShippingOptions] = useState<
|
||||
@@ -92,7 +92,7 @@ export const highlights = [
|
||||
>([])
|
||||
const [
|
||||
selectedShippingOption,
|
||||
setSelectedShippingOption
|
||||
setSelectedShippingOption,
|
||||
] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -102,7 +102,7 @@ export const highlights = [
|
||||
fetch(`http://localhost:9000/store/shipping-options?cart_id=${
|
||||
cart.id
|
||||
}`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ shipping_options }) => {
|
||||
@@ -126,15 +126,15 @@ export const highlights = [
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
option_id: selectedShippingOption,
|
||||
data: {
|
||||
// TODO add any data necessary for
|
||||
// fulfillment provider
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart: updatedCart }) => {
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Manage Customer Addresses in Storefront`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you'll learn how to manage a customer's addresses in a storefront.
|
||||
|
||||
## List Customer Addresses
|
||||
|
||||
To retrieve the list of customer addresses, send a request to the [List Customer Addresses API route](!api!/store#customers_getcustomersmeaddressesaddress_id):
|
||||
|
||||
<CodeTabs group="store-request">
|
||||
<CodeTab label="Fetch API" value="fetch">
|
||||
|
||||
export const fetchHighlights = [
|
||||
["2", "limit", "The number of addresses to retrieve"],
|
||||
["3", "offset", "The number of addresses to skip before those retrieved."],
|
||||
]
|
||||
|
||||
```ts highlights={fetchHighlights}
|
||||
const searchParams = new URLSearchParams({
|
||||
limit: `${limit}`,
|
||||
offset: `${offset}`,
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/customers/me/addresses?${
|
||||
searchParams.toString()
|
||||
}`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ addresses, count }) => {
|
||||
// use addresses...
|
||||
console.log(addresses, count)
|
||||
})
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="React" value="react">
|
||||
|
||||
export const highlights = [
|
||||
["20", "offset", "Calculate the number of addresses to skip based on the current page and limit."],
|
||||
["27", "fetch", "Send a request to retrieve the addresses."],
|
||||
["28", "searchParams.toString()", "Pass the pagination parameters in the query."],
|
||||
["33", "count", "The total number of addresses in the Medusa application."],
|
||||
["45", "setHasMorePages", "Set whether there are more pages based on the total count."],
|
||||
["59", "", "Using only two address fields for simplicity."],
|
||||
["65", "button", "Show a button to load more addresses if there are more pages."]
|
||||
]
|
||||
|
||||
```tsx highlights={highlights} collapsibleLines="50-77" expandButtonLabel="Show render"
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export default function Addresses() {
|
||||
const [addresses, setAddresses] = useState<
|
||||
HttpTypes.StoreCustomerAddress[]
|
||||
>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const limit = 20
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [hasMorePages, setHasMorePages] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
return
|
||||
}
|
||||
|
||||
const offset = (currentPage - 1) * limit
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
limit: `${limit}`,
|
||||
offset: `${offset}`,
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/customers/me/addresses?${
|
||||
searchParams.toString()
|
||||
}`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ addresses: addressesData, count }) => {
|
||||
setAddresses((prev) => {
|
||||
if (prev.length > offset) {
|
||||
// addresses already added because
|
||||
// the same request has already been sent
|
||||
return prev
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
...addressesData,
|
||||
]
|
||||
})
|
||||
setHasMorePages(count > limit * currentPage)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [loading])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading && <span>Loading...</span>}
|
||||
{!loading && !addresses.length && (
|
||||
<span>You have no addresses</span>
|
||||
)}
|
||||
<ul>
|
||||
{addresses.map((address) => (
|
||||
<li key={address.id}>
|
||||
City: {address.city} -
|
||||
Country: {address.country_code}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{!loading && hasMorePages && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setCurrentPage((prev) => prev + 1)
|
||||
setLoading(true)
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
The List Customer Addresses API route accepts pagination parameters to paginate the address.
|
||||
|
||||
{/* TODO add a link to the address object */}
|
||||
|
||||
It returns in the response the `addresses` field, which is an array of addresses.
|
||||
|
||||
---
|
||||
|
||||
## Add Customer Address
|
||||
|
||||
To add a new address for the customer, send a request to the [Add Customer Address API route](!api!/store#customers_postcustomersmeaddresses):
|
||||
|
||||
<CodeTabs group="store-request">
|
||||
<CodeTab label="Fetch API" value="fetch">
|
||||
|
||||
```ts
|
||||
fetch(`http://localhost:9000/store/customers/me/addresses`, {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
address_1: address1,
|
||||
company,
|
||||
postal_code: postalCode,
|
||||
city,
|
||||
country_code: countryCode,
|
||||
province,
|
||||
phone: phoneNumber,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ customer }) => {
|
||||
// use customer
|
||||
console.log(customer)
|
||||
})
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="React" value="react">
|
||||
|
||||
export const addHighlights = [
|
||||
["4", "useRegion", "Use the hook defined in the Region Context guide."],
|
||||
["5", "useCustomer", "Use the hook defined in the Customer Context guide."],
|
||||
["28"], ["29"], ["30"], ["31"], ["32"], ["33"], ["34"], ["35"], ["36"], ["37"],
|
||||
["38"], ["39"], ["40"], ["41"], ["42"], ["43"], ["44"], ["45"], ["46"], ["47"],
|
||||
["48"], ["49"]
|
||||
]
|
||||
|
||||
```tsx highlights={addHighlights} collapsibleLines="53-124" expandButtonLabel="Show form"
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRegion } from "../../../../providers/region"
|
||||
import { useCustomer } from "../../../../providers/customer"
|
||||
|
||||
export default function AddAddress() {
|
||||
const { region } = useRegion()
|
||||
const { setCustomer } = useCustomer()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [firstName, setFirstName] = useState("")
|
||||
const [lastName, setLastName] = useState("")
|
||||
const [address1, setAddress1] = useState("")
|
||||
const [company, setCompany] = useState("")
|
||||
const [postalCode, setPostalCode] = useState("")
|
||||
const [city, setCity] = useState("")
|
||||
const [countryCode, setCountryCode] = useState("")
|
||||
const [province, setProvince] = useState("")
|
||||
const [phoneNumber, setPhoneNumber] = useState("")
|
||||
|
||||
const handleAdd = (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
e.preventDefault()
|
||||
setLoading(false)
|
||||
|
||||
fetch(`http://localhost:9000/store/customers/me/addresses`, {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
address_1: address1,
|
||||
company,
|
||||
postal_code: postalCode,
|
||||
city,
|
||||
country_code: countryCode,
|
||||
province,
|
||||
phone: phoneNumber,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ customer }) => {
|
||||
setCustomer(customer)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="First Name"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Last Name"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Address Line"
|
||||
value={address1}
|
||||
onChange={(e) => setAddress1(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Company"
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Postal Code"
|
||||
value={postalCode}
|
||||
onChange={(e) => setPostalCode(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="City"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
value={countryCode}
|
||||
onChange={(e) => setCountryCode(e.target.value)}
|
||||
>
|
||||
{region?.countries?.map((country) => (
|
||||
<option
|
||||
key={country.iso_2}
|
||||
value={country.iso_2}
|
||||
>
|
||||
{country.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Province"
|
||||
value={province}
|
||||
onChange={(e) => setProvince(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="Phone Number"
|
||||
value={phoneNumber}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
The Add Address API route returns in the response a `customer` field, which is a [customer object](!api!/store#customers_customer_schema).
|
||||
|
||||
---
|
||||
|
||||
## Edit an Address
|
||||
|
||||
To edit an address, send a request to the [Update Customer Address API route](!api!/store#customers_postcustomersmeaddressesaddress_id):
|
||||
|
||||
<CodeTabs group="store-request">
|
||||
<CodeTab label="Fetch API" value="fetch">
|
||||
|
||||
```ts
|
||||
fetch(
|
||||
`http://localhost:9000/store/customers/me/addresses/${
|
||||
address.id
|
||||
}`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
address_1: address1,
|
||||
company,
|
||||
postal_code: postalCode,
|
||||
city,
|
||||
country_code: countryCode,
|
||||
province,
|
||||
phone: phoneNumber,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then(({ customer }) => {
|
||||
// use customer...
|
||||
console.log(customer)
|
||||
})
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="React" value="react">
|
||||
|
||||
export const editHighlights = [
|
||||
["4", "useRegion", "Use the hook defined in the Region Context guide."],
|
||||
["5", "useCustomer", "Use the hook defined in the Customer Context guide."],
|
||||
["14", "{ params: { id }}", "This is based on Next.js which passes the path parameters as a prop."],
|
||||
["19", "address", "Retrieve the address from the customer's `addresses` property."],
|
||||
["60"], ["61"], ["62"], ["63"], ["64"], ["65"], ["66"], ["67"], ["68"], ["69"],
|
||||
["70"], ["71"], ["72"], ["73"], ["74"], ["75"], ["76"], ["77"], ["78"], ["79"],
|
||||
["80"], ["81"]
|
||||
]
|
||||
|
||||
```tsx highlights={editHighlights} collapsibleLines="90-161" expandButtonLabel="Show form"
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRegion } from "../../../../../providers/region"
|
||||
import { useCustomer } from "../../../../../providers/customer"
|
||||
|
||||
type Params = {
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function EditAddress(
|
||||
{ params: { id } }: Params
|
||||
) {
|
||||
const { customer, setCustomer } = useCustomer()
|
||||
const { region } = useRegion()
|
||||
|
||||
const address = customer?.addresses.find(
|
||||
(address) => address.id === id
|
||||
)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [firstName, setFirstName] = useState(
|
||||
address?.first_name || ""
|
||||
)
|
||||
const [lastName, setLastName] = useState(
|
||||
address?.last_name || ""
|
||||
)
|
||||
const [address1, setAddress1] = useState(
|
||||
address?.address_1 || ""
|
||||
)
|
||||
const [company, setCompany] = useState(
|
||||
address?.company || ""
|
||||
)
|
||||
const [postalCode, setPostalCode] = useState(
|
||||
address?.postal_code || ""
|
||||
)
|
||||
const [city, setCity] = useState(
|
||||
address?.city || ""
|
||||
)
|
||||
const [countryCode, setCountryCode] = useState(
|
||||
address?.country_code || ""
|
||||
)
|
||||
const [province, setProvince] = useState(
|
||||
address?.province || ""
|
||||
)
|
||||
const [phoneNumber, setPhoneNumber] = useState(
|
||||
address?.phone || ""
|
||||
)
|
||||
|
||||
const handleEdit = (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
e.preventDefault()
|
||||
if (!customer || !address) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
|
||||
fetch(
|
||||
`http://localhost:9000/store/customers/me/addresses/${
|
||||
address.id
|
||||
}`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
address_1: address1,
|
||||
company,
|
||||
postal_code: postalCode,
|
||||
city,
|
||||
country_code: countryCode,
|
||||
province,
|
||||
phone: phoneNumber,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then(({ customer }) => {
|
||||
setCustomer(customer)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="First Name"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Last Name"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Address Line"
|
||||
value={address1}
|
||||
onChange={(e) => setAddress1(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Company"
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Postal Code"
|
||||
value={postalCode}
|
||||
onChange={(e) => setPostalCode(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="City"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
value={countryCode}
|
||||
onChange={(e) => setCountryCode(e.target.value)}
|
||||
>
|
||||
{region?.countries?.map((country) => (
|
||||
<option
|
||||
key={country.iso_2}
|
||||
value={country.iso_2}
|
||||
>
|
||||
{country.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Province"
|
||||
value={province}
|
||||
onChange={(e) => setProvince(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="Phone Number"
|
||||
value={phoneNumber}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={handleEdit}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
The Update Address API route returns in the response a `customer` field, which is a [customer object](!api!/store#customers_customer_schema).
|
||||
|
||||
---
|
||||
|
||||
## Delete Customer Address
|
||||
|
||||
To delete a customer's address, send a request to the [Delete Customer Address API route](!api!/store#customers_deletecustomersmeaddressesaddress_id):
|
||||
|
||||
export const deleteHighlights = [
|
||||
["3", "addrId", "The ID of the address to delete."]
|
||||
]
|
||||
|
||||
```ts highlights={deleteHighlights}
|
||||
fetch(
|
||||
`http://localhost:9000/store/customers/me/addresses/${
|
||||
addrId
|
||||
}`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "DELETE",
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then(({ parent: customer }) => {
|
||||
// use customer...
|
||||
console.log(customer)
|
||||
})
|
||||
```
|
||||
|
||||
The Delete Customer Address API route returns a `parent` field in the response, which is a [customer object](!api!/store#customers_customer_schema).
|
||||
@@ -0,0 +1,160 @@
|
||||
export const metadata = {
|
||||
title: `Customer Context in Storefront`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
Throughout your storefront, you'll need to access the logged-in customer to perform different actions, such as associate it with a cart.
|
||||
|
||||
So, if your storefront is React-based, create a customer context and add it at the top of your components tree. Then, you can access the logged-in customer anywhere in your storefront.
|
||||
|
||||
## Create Customer Context Provider
|
||||
|
||||
For example, create the following file that exports a `CustomerProvider` component and a `useCustomer` hook:
|
||||
|
||||
export const highlights = [
|
||||
["12", "customer", "Expose customer to children of the context provider."],
|
||||
["13", "setCustomer", "Allow the context provider's\nchildren to change the logged-in customer."],
|
||||
["24", "CustomerProvider", "The provider component to use in your component tree."],
|
||||
["36", "fetch", "Try to retrieve the customer's details,\nif the customer is authentiated."],
|
||||
["37", `credentials: "include"`, "Important to include this option for cookie session authentication.\nFor token authentication, pass the authorization header instead."],
|
||||
["58", "useCustomer", "The hook that child components of the provider use to access the customer."]
|
||||
]
|
||||
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
type CustomerContextType = {
|
||||
customer: HttpTypes.StoreCustomer | undefined
|
||||
setCustomer: React.Dispatch<
|
||||
React.SetStateAction<HttpTypes.StoreCustomer | undefined>
|
||||
>
|
||||
}
|
||||
|
||||
const CustomerContext = createContext<CustomerContextType | null>(null)
|
||||
|
||||
type CustomerProviderProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const CustomerProvider = ({
|
||||
children,
|
||||
}: CustomerProviderProps) => {
|
||||
const [customer, setCustomer] = useState<
|
||||
HttpTypes.StoreCustomer
|
||||
>()
|
||||
|
||||
useEffect(() => {
|
||||
if (customer) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`http://localhost:9000/store/customers/me`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ customer }) => {
|
||||
setCustomer(customer)
|
||||
})
|
||||
.catch((err) => {
|
||||
// customer isn't logged in
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<CustomerContext.Provider value={{
|
||||
customer,
|
||||
setCustomer,
|
||||
}}>
|
||||
{children}
|
||||
</CustomerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCustomer = () => {
|
||||
const context = useContext(CustomerContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCustomer must be used within a CustomerProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
```
|
||||
|
||||
The `CustomerProvider` handles retrieving the authenticated customer from the Medusa application.
|
||||
|
||||
The `useCustomer` hook returns the value of the `CustomerContext`. Child components of `CustomerProvider` use this hook to access `customer` or `setCustomer`.
|
||||
|
||||
---
|
||||
|
||||
## Use CustomerProvider in Component Tree
|
||||
|
||||
To use the customer context's value, add the `CustomerProvider` high in your component tree.
|
||||
|
||||
For example, if you're using Next.js, add it to the `app/layout.tsx` or `src/app/layout.tsx` file:
|
||||
|
||||
```tsx title="app/layout.tsx" collapsibleLines="1-14" highlights={[["24"]]}
|
||||
import type { Metadata } from "next"
|
||||
import { Inter } from "next/font/google"
|
||||
import "./globals.css"
|
||||
import { CartProvider } from "../providers/cart"
|
||||
import { RegionProvider } from "../providers/region"
|
||||
import { CustomerProvider } from "../providers/customer"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<RegionProvider>
|
||||
<CustomerProvider>
|
||||
{/* Other providers... */}
|
||||
<CartProvider>
|
||||
{children}
|
||||
</CartProvider>
|
||||
</CustomerProvider>
|
||||
</RegionProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use useCustomer Hook
|
||||
|
||||
Now, you can use the `useCustomer` hook in child components of `CustomerProvider`.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
"use client" // include with Next.js 13+
|
||||
// ...
|
||||
import { useCustomer } from "../providers/customer"
|
||||
|
||||
export default function Profile() {
|
||||
const { customer } = useCustomer()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,89 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Log-out Customer in Storefront`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you'll learn how to log-out a customer in the storefront based on the authentication method.
|
||||
|
||||
## Log-Out for JWT Token
|
||||
|
||||
If you're authenticating the customer with their JWT token, remove the stored token from the browser.
|
||||
|
||||
For example, if you've stored the JWT token in the `localStorage`, remove the item from it:
|
||||
|
||||
```ts
|
||||
localStorage.removeItem(`token`)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Log-Out for Cookie Session
|
||||
|
||||
If you're authenticating the customer with their cookie session ID, send a `DELETE` request to the `/auth/session`.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs group="store-request">
|
||||
<CodeTab label="Fetch API" value="fetch">
|
||||
|
||||
```ts
|
||||
fetch(`http://localhost:9000/auth/session`, {
|
||||
credentials: "include",
|
||||
method: "DELETE",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(() => {
|
||||
// TODO redirect customer to login page
|
||||
})
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="React" value="react">
|
||||
|
||||
export const highlights = [
|
||||
["3", "useCustomer", "Use the hook defined in the Customer Context guide."],
|
||||
["9"], ["10"], ["11"], ["12"], ["13"], ["14"], ["15"], ["16"], ["17"], ["18"],
|
||||
["19"],
|
||||
]
|
||||
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useCustomer } from "../../../providers/customer"
|
||||
|
||||
export default function Profile() {
|
||||
const { setCustomer } = useCustomer()
|
||||
|
||||
const handleLogOut = () => {
|
||||
fetch(`http://localhost:9000/auth/session`, {
|
||||
credentials: "include",
|
||||
method: "DELETE",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(() => {
|
||||
setCustomer(undefined)
|
||||
// TODO redirect to login page
|
||||
alert("Logged out")
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Profile details... */}
|
||||
<button
|
||||
onClick={handleLogOut}
|
||||
>
|
||||
Log Out
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
The API route returns nothing in the response. If the request was successful, you can perform any necessary work to unset the customer and redirect them to the login page.
|
||||
@@ -0,0 +1,347 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Login Customer in Storefront`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you'll learn about the two ways to login a customer in a storefront.
|
||||
|
||||
## 1. Using a JWT Token
|
||||
|
||||
Using the `/auth/customer/emailpass` API route, you obtain a JSON Web Token (JWT) for the customer. Then, use that token as a bearer token in the authorization header of subsequent requests, and the customer is considered authenticated.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs group="store-request">
|
||||
<CodeTab label="Fetch API" value="fetch">
|
||||
|
||||
export const fetchHighlights = [
|
||||
["3", "fetch", "Send a request to obtain a JWT token."],
|
||||
["21", "fetch", "Send a request as an authenticated customer."],
|
||||
["27", "token", "Pass as a Bearer token in the authorization header."],
|
||||
]
|
||||
|
||||
```ts highlights={fetchHighlights}
|
||||
const handleLogin = async () => {
|
||||
// obtain JWT token
|
||||
const { token } = await fetch(
|
||||
`http://localhost:9000/auth/customer/emailpass`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
// use token in the authorization header of
|
||||
// all follow up requests. For example:
|
||||
const { customer } = await fetch(
|
||||
`http://localhost:9000/store/customers/me`,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
console.log(customer)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="React" value="react">
|
||||
|
||||
export const highlights = [
|
||||
["21", "fetch", "Send a request to obtain a JWT token."],
|
||||
["39", "fetch", "Send a request as an authenticated customer."],
|
||||
["45", "token", "Pass as a Bearer token in the authorization header."],
|
||||
]
|
||||
|
||||
```tsx highlights={highlights} collapsibleLines="55-79" expandButtonLabel="Show form"
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
export default function Login() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
|
||||
const handleLogin = async (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
e.preventDefault()
|
||||
if (!email || !password) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
// obtain JWT token
|
||||
const { token } = await fetch(
|
||||
`http://localhost:9000/auth/customer/emailpass`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
// use token in the authorization header of
|
||||
// all follow up requests. For example:
|
||||
const { customer } = await fetch(
|
||||
`http://localhost:9000/store/customers/me`,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
console.log(customer)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={email}
|
||||
placeholder="Email"
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={handleLogin}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
In the example above, you:
|
||||
|
||||
1. Create a `handleLogin` function that logs in a customer.
|
||||
2. In the function, you obtain a JWT token by sending a request to the `/auth/customer/emailpass`.
|
||||
3. You can then use that token in the authorization header of subsequent requests, and the customer is considered authenticated. As an example, you send a request to obtain the customer's details.
|
||||
|
||||
---
|
||||
|
||||
## 2. Using a Cookie Session
|
||||
|
||||
Authenticating the customer with a cookie session means the customer is authenticated in subsequent requests that use that cookie.
|
||||
|
||||
If you're using the Fetch API, using the `credentials: include` option ensures that your cookie session is passed in every request.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs group="store-request">
|
||||
<CodeTab label="Fetch API" value="fetch">
|
||||
|
||||
export const fetchSessionHighlights = [
|
||||
["3", "fetch", "Send a request to obtain a JWT token."],
|
||||
["20", "fetch", "Send a request to set the authenticated session ID in the cookies."],
|
||||
["27", "token", "Pass as a Bearer token in the authorization header."],
|
||||
["35", "fetch", "Retrieve the customer's details as an example of testing authentication."],
|
||||
]
|
||||
|
||||
```ts highlights={fetchSessionHighlights}
|
||||
const handleLogin = async () => {
|
||||
// obtain JWT token
|
||||
const { token } = await fetch(
|
||||
`http://localhost:9000/auth/customer/emailpass`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
// set session
|
||||
await fetch(
|
||||
`http://localhost:9000/auth/session`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
// customer is now authenticated using the
|
||||
// cookie session. For example
|
||||
const { customer } = await fetch(
|
||||
`http://localhost:9000/store/customers/me`,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
console.log(customer)
|
||||
setLoading(false)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="React" value="react">
|
||||
|
||||
export const sessionHighlights = [
|
||||
["21", "fetch", "Send a request to obtain a JWT token."],
|
||||
["38", "fetch", "Send a request to set the authenticated session ID in the cookies."],
|
||||
["45", "token", "Pass as a Bearer token in the authorization header."],
|
||||
["53", "fetch", "Retrieve the customer's details as an example of testing authentication."],
|
||||
]
|
||||
|
||||
```tsx highlights={sessionHighlights} collapsibleLines="68-92" expandButtonLabel="Show form"
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
export default function Login() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
|
||||
const handleLogin = async (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
e.preventDefault()
|
||||
if (!email || !password) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
// obtain JWT token
|
||||
const { token } = await fetch(
|
||||
`http://localhost:9000/auth/customer/emailpass`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
// set session
|
||||
await fetch(
|
||||
`http://localhost:9000/auth/session`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
// customer is now authenticated using the
|
||||
// cookie session. For example
|
||||
const { customer } = await fetch(
|
||||
`http://localhost:9000/store/customers/me`,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
console.log(customer)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={email}
|
||||
placeholder="Email"
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={handleLogin}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
In the example above, you:
|
||||
|
||||
1. Create a `handleLogin` function that logs in a customer.
|
||||
2. In the function, you obtain a JWT token by sending a request to the `/auth/customer/emailpass`.
|
||||
3. You send a request to the `/auth/session` API route passing in the authorization header the token as a Bearer token. This sets the authenticated session ID in the cookies.
|
||||
4. You can now send authenticated requests, as long as you include the `credentials: include` option in your fetch requests. For example, you send a request to retrieve the customer's details.
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ChildDocs } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Customers in Storefront`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
A customer can register, manage their account, keep track of their orders, and more.
|
||||
|
||||
<ChildDocs type="item" onlyTopLevel={true} />
|
||||
@@ -0,0 +1,145 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Edit Customer Profile in Storefront`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
To edit the customer's profile in the storefront, send a request to the [Update Customer API route](!api!/store#customers_postcustomersme).
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs group="store-request">
|
||||
<CodeTab label="Fetch API" value="fetch">
|
||||
|
||||
```ts
|
||||
fetch(`http://localhost:9000/store/customers/me`, {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name,
|
||||
last_name,
|
||||
company_name,
|
||||
phone,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ customer }) => {
|
||||
// use customer...
|
||||
console.log(customer)
|
||||
})
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="React" value="react">
|
||||
|
||||
export const highlights = [
|
||||
["4", "useCustomer", "Use the hook defined in the Customer Context guide."],
|
||||
["33"], ["34"], ["35"], ["36"], ["37"], ["38"], ["39"], ["40"], ["41"], ["42"],
|
||||
["43"], ["44"], ["45"], ["46"], ["47"], ["48"], ["49"]
|
||||
]
|
||||
|
||||
```tsx highlights={highlights} collapsibleLines="53-91" expandButtonLabel="Show form"
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useState } from "react"
|
||||
import { useCustomer } from "../../../providers/customer"
|
||||
|
||||
export default function EditProfile() {
|
||||
const { customer, setCustomer } = useCustomer()
|
||||
const [firstName, setFirstName] = useState(
|
||||
customer?.first_name || ""
|
||||
)
|
||||
const [lastName, setLastName] = useState(
|
||||
customer?.last_name || ""
|
||||
)
|
||||
const [company, setCompany] = useState(
|
||||
customer?.company_name || ""
|
||||
)
|
||||
const [phone, setPhone] = useState(
|
||||
customer?.phone || ""
|
||||
)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleEdit = (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!customer) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
fetch(`http://localhost:9000/store/customers/me`, {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
company_name: company,
|
||||
phone,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ customer: updatedCustomer }) => {
|
||||
setCustomer(updatedCustomer)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
value={firstName}
|
||||
placeholder="First Name"
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
value={lastName}
|
||||
placeholder="Last Name"
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="company"
|
||||
value={company}
|
||||
placeholder="Company"
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="phone"
|
||||
value={phone}
|
||||
placeholder="Phone Number"
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={handleEdit}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
In the example above, you send a request to the Update Customer API route to update the customer's details.
|
||||
|
||||
The response of the request has a `customer` field which is a [customer object](!api!/store#customers_customer_schema).
|
||||
@@ -0,0 +1,190 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Register Customer in Storefront`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
To register a customer, you implement the following steps:
|
||||
|
||||
1. Show the customer a form to enter their details.
|
||||
2. Send a `POST` request to the `/auth/customer/emailpass` API route to obtain a JWT token.
|
||||
3. Send a request to the [Create Customer API route](!api!/store#customers_postcustomers) pass the JWT token in the header.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs group="store-request">
|
||||
<CodeTab label="Fetch API" value="fetch">
|
||||
|
||||
export const fetchHighlights = [
|
||||
["3", "fetch", "Send a request to obtain a JWT token."],
|
||||
["20", "fetch", "Send a request to create the customer."],
|
||||
["27", "token", "Pass as a Bearer token in the authorization header."],
|
||||
["39", "TODO", "Redirect the customer to the log in page."]
|
||||
]
|
||||
|
||||
```ts highlights={fetchHighlights}
|
||||
const handleRegistration = async () => {
|
||||
// obtain JWT token
|
||||
const { token } = await fetch(
|
||||
`http://localhost:9000/auth/customer/emailpass`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
// create customer
|
||||
const { customer } = await fetch(
|
||||
`http://localhost:9000/store/customers`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
console.log(customer)
|
||||
// TODO redirect to login page
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="React" value="react">
|
||||
|
||||
export const highlights = [
|
||||
["22", "fetch", "Send a request to obtain a JWT token."],
|
||||
["39", "fetch", "Send a request to create the customer."],
|
||||
["46", "token", "Pass as a Bearer token in the authorization header."],
|
||||
["59", "TODO", "Redirect the customer to the log in page."]
|
||||
]
|
||||
|
||||
```tsx highlights={highlights} collapsibleLines="61-100" expandButtonLabel="Show form"
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
export default function Register() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [firstName, setFirstName] = useState("")
|
||||
const [lastName, setLastName] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
|
||||
const handleRegistration = async (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
e.preventDefault()
|
||||
if (!firstName || !lastName || !email || !password) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
|
||||
// obtain JWT token
|
||||
const { token } = await fetch(
|
||||
`http://localhost:9000/auth/customer/emailpass`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
// create customer
|
||||
const { customer } = await fetch(
|
||||
`http://localhost:9000/store/customers`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email,
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
console.log(customer)
|
||||
setLoading(false)
|
||||
// TODO redirect to login page
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
value={firstName}
|
||||
placeholder="First Name"
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
value={lastName}
|
||||
placeholder="Last Name"
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={email}
|
||||
placeholder="Email"
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={handleRegistration}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
In the above example, you create a `handleRegistration` function that:
|
||||
|
||||
- Obtains a JWT token from the `/auth/customer/emailpass` API route.
|
||||
- Send a request to the Create Customer API route, and pass the JWT token as a Bearer token in the authorization header.
|
||||
- Once the customer is registered successfully, you can either redirect the customer to the login page or log them in automatically as explained in this guide.
|
||||
@@ -0,0 +1,66 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Retrieve Customer in Storefront`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
To retrieve a customer after it's been authenticated in your storefront, send a request to the [Get Customer API route](!api!/store#customers_getcustomersme):
|
||||
|
||||
<CodeTabs group="authenticated-request">
|
||||
<CodeTab label="Using Bearer Token" value="bearer">
|
||||
|
||||
export const bearerHighlights = [
|
||||
["7", "", "Pass JWT token as bearer token in authorization header."],
|
||||
]
|
||||
|
||||
```ts highlights={bearerHighlights}
|
||||
fetch(
|
||||
`http://localhost:9000/store/customers/me`,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then(({ customer }) => {
|
||||
// use customer...
|
||||
console.log(customer)
|
||||
})
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Using Cookie Session" value="session">
|
||||
|
||||
export const sessionHighlights = [
|
||||
["4", "", "Pass this option to ensure the cookie session is passed in the request."],
|
||||
]
|
||||
|
||||
```ts highlights={sessionHighlights}
|
||||
fetch(
|
||||
`http://localhost:9000/store/customers/me`,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then(({ customer }) => {
|
||||
// use customer...
|
||||
console.log(customer)
|
||||
})
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
- If you authenticate the customer with bearer authorization, pass the token in the authorization header of the request.
|
||||
- If you authenticate the customer with cookie session, pass the `credentials: include` option to the `fetch` function.
|
||||
|
||||
The Get Customer API route returns a `customer` field, which is a [customer object](!api!/store#customers_customer_schema).
|
||||
@@ -37,7 +37,7 @@ export const highlights = [
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function Categories() {
|
||||
@@ -75,7 +75,7 @@ export const highlights = [
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -108,7 +108,7 @@ export const paginateHighlights = [
|
||||
```tsx highlights={paginateHighlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function Categories() {
|
||||
@@ -147,7 +147,7 @@ export default function Categories() {
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
...product_categories
|
||||
...product_categories,
|
||||
]
|
||||
})
|
||||
setHasMorePages(count > limit * currentPage)
|
||||
@@ -180,7 +180,7 @@ export default function Categories() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -196,7 +196,7 @@ For example, to run a query on the product categories:
|
||||
|
||||
```ts
|
||||
const searchParams = new URLSearchParams({
|
||||
q: "Shirt"
|
||||
q: "Shirt",
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/product-categories?${
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ export const fetchHighlights = [
|
||||
|
||||
```ts highlights={fetchHighlights}
|
||||
const searchParams = new URLSearchParams({
|
||||
fields: "*category_children"
|
||||
fields: "*category_children",
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/product-categories/${id}?${
|
||||
@@ -71,7 +71,7 @@ export const highlights = [
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
fields: "*category_children"
|
||||
fields: "*category_children",
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/product-categories/${id}?${
|
||||
|
||||
@@ -19,14 +19,14 @@ export const fetchHighlights = [
|
||||
```ts highlights={fetchHighlights}
|
||||
const searchParams = new URLSearchParams({
|
||||
// other query params...
|
||||
"category_id[]": categoryId
|
||||
"category_id[]": categoryId,
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ products, count }) => {
|
||||
@@ -60,7 +60,7 @@ export const highlights = [
|
||||
}
|
||||
|
||||
export default function CategoryProducts({
|
||||
params: { categoryId }
|
||||
params: { categoryId },
|
||||
}: Params) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [products, setProducts] = useState<
|
||||
@@ -80,14 +80,14 @@ export const highlights = [
|
||||
const searchParams = new URLSearchParams({
|
||||
limit: `${limit}`,
|
||||
offset: `${offset}`,
|
||||
"category_id[]": categoryId
|
||||
"category_id[]": categoryId,
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ products: dataProducts, count }) => {
|
||||
@@ -98,7 +98,7 @@ export const highlights = [
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
...dataProducts
|
||||
...dataProducts,
|
||||
]
|
||||
})
|
||||
setHasMorePages(count > limit * currentPage)
|
||||
@@ -131,7 +131,7 @@ export const highlights = [
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export const highlights = [
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function Collections() {
|
||||
@@ -75,7 +75,7 @@ export const highlights = [
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -110,7 +110,7 @@ export const paginateHighlights = [
|
||||
```tsx highlights={paginateHighlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function Collections() {
|
||||
@@ -149,7 +149,7 @@ export default function Collections() {
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
...dataCollections
|
||||
...dataCollections,
|
||||
]
|
||||
})
|
||||
setHasMorePages(count > limit * currentPage)
|
||||
@@ -182,7 +182,7 @@ export default function Collections() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -198,7 +198,7 @@ For example:
|
||||
|
||||
```ts
|
||||
const searchParams = new URLSearchParams({
|
||||
title: "test"
|
||||
title: "test",
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/collections?${
|
||||
|
||||
+9
-9
@@ -19,14 +19,14 @@ export const fetchHighlights = [
|
||||
```ts highlights={fetchHighlights}
|
||||
const searchParams = new URLSearchParams({
|
||||
// other query params...
|
||||
"collection_id[]": collectionId
|
||||
"collection_id[]": collectionId,
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ products, count }) => {
|
||||
@@ -60,7 +60,7 @@ export const highlights = [
|
||||
}
|
||||
|
||||
export default function CollectionProducts({
|
||||
params: { collectionId }
|
||||
params: { collectionId },
|
||||
}: Params) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [products, setProducts] = useState<
|
||||
@@ -80,7 +80,7 @@ export const highlights = [
|
||||
const searchParams = new URLSearchParams({
|
||||
limit: `${limit}`,
|
||||
offset: `${offset}`,
|
||||
"collection_id[]": collectionId
|
||||
"collection_id[]": collectionId,
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/products?${
|
||||
@@ -88,8 +88,8 @@ export const highlights = [
|
||||
}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ products: dataProducts, count }) => {
|
||||
@@ -100,7 +100,7 @@ export const highlights = [
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
...dataProducts
|
||||
...dataProducts,
|
||||
]
|
||||
})
|
||||
setHasMorePages(count > limit * currentPage)
|
||||
@@ -133,7 +133,7 @@ export const highlights = [
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ export const fetchHighlights = [
|
||||
fetch(`http://localhost:9000/store/products`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
@@ -45,7 +45,7 @@ export const highlights = [
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function Products() {
|
||||
@@ -62,8 +62,8 @@ export const highlights = [
|
||||
fetch(`http://localhost:9000/store/products`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
@@ -84,7 +84,7 @@ export const highlights = [
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -119,7 +119,7 @@ export const paginateHighlights = [
|
||||
```tsx highlights={paginateHighlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function Products() {
|
||||
@@ -146,8 +146,8 @@ export default function Products() {
|
||||
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ products: dataProducts, count }) => {
|
||||
@@ -158,7 +158,7 @@ export default function Products() {
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
...dataProducts
|
||||
...dataProducts,
|
||||
]
|
||||
})
|
||||
setHasMorePages(count > limit * currentPage)
|
||||
@@ -189,7 +189,7 @@ export default function Products() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -206,14 +206,14 @@ For example, to run a query on the products:
|
||||
```ts
|
||||
const searchParams = new URLSearchParams({
|
||||
// other params...
|
||||
q: "Shirt"
|
||||
q: "Shirt",
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ products: dataProducts, count }) => {
|
||||
|
||||
@@ -74,14 +74,14 @@ export default function Product({ params: { id } }: Params) {
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
fields: `*variants.calculated_price`,
|
||||
region_id: region.id
|
||||
region_id: region.id,
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ product: dataProduct }) => {
|
||||
@@ -152,7 +152,7 @@ export default function Product({ params: { id } }: Params) {
|
||||
setSelectedOptions((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[option.id!]: optionValue.value!
|
||||
[option.id!]: optionValue.value!,
|
||||
}
|
||||
})
|
||||
}}
|
||||
@@ -254,14 +254,14 @@ export default function Product({ params: { id } }: Params) {
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
fields: `*variants.calculated_price`,
|
||||
region_id: region.id
|
||||
region_id: region.id,
|
||||
})
|
||||
|
||||
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ product: dataProduct }) => {
|
||||
@@ -350,7 +350,7 @@ export default function Product({ params: { id } }: Params) {
|
||||
setSelectedOptions((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[option.id!]: optionValue.value!
|
||||
[option.id!]: optionValue.value!,
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
@@ -29,8 +29,8 @@ export const fetchHighlights = [
|
||||
fetch(`http://localhost:9000/store/products/${id}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ product }) => {
|
||||
@@ -75,8 +75,8 @@ export const highlights = [
|
||||
fetch(`http://localhost:9000/store/products/${id}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ product: dataProduct }) => {
|
||||
@@ -140,8 +140,8 @@ export const handleFetchHighlights = [
|
||||
fetch(`http://localhost:9000/store/products?handle=${handle}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ products }) => {
|
||||
@@ -190,8 +190,8 @@ export const handleHighlights = [
|
||||
fetch(`http://localhost:9000/store/products?handle=${handle}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ products }) => {
|
||||
|
||||
@@ -57,8 +57,8 @@ export default function Product({ params: { id } }: Params) {
|
||||
fetch(`http://localhost:9000/store/products/${id}`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
|
||||
}
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ product: dataProduct }) => {
|
||||
@@ -99,7 +99,7 @@ export default function Product({ params: { id } }: Params) {
|
||||
setSelectedOptions((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[option.id!]: optionValue.value!
|
||||
[option.id!]: optionValue.value!,
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
useState,
|
||||
} from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
@@ -68,7 +68,7 @@ export const RegionProvider = (
|
||||
if (!regionId) {
|
||||
// retrieve regions and select the first one
|
||||
fetch(`http://localhost:9000/store/regions`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ regions }) => {
|
||||
@@ -77,7 +77,7 @@ export const RegionProvider = (
|
||||
} else {
|
||||
// retrieve selected region
|
||||
fetch(`http://localhost:9000/store/regions/${regionId}`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ region: dataRegion }) => {
|
||||
@@ -89,7 +89,7 @@ export const RegionProvider = (
|
||||
return (
|
||||
<RegionContext.Provider value={{
|
||||
region,
|
||||
setRegion
|
||||
setRegion,
|
||||
}}>
|
||||
{children}
|
||||
</RegionContext.Provider>
|
||||
@@ -126,12 +126,12 @@ import "./globals.css"
|
||||
import { CartProvider } from "../providers/cart"
|
||||
import { RegionProvider } from "../providers/region"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -147,7 +147,7 @@ export default function RootLayout({
|
||||
</RegionProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ To list regions in your storefront, send a request to the [List Regions API rout
|
||||
|
||||
```ts
|
||||
fetch(`http://localhost:9000/store/regions`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ regions }) => {
|
||||
@@ -32,7 +32,7 @@ export const highlights = [
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function Regions() {
|
||||
@@ -47,7 +47,7 @@ export const highlights = [
|
||||
}
|
||||
|
||||
fetch(`http://localhost:9000/store/regions`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ regions: dataRegions }) => {
|
||||
@@ -68,7 +68,7 @@ export const highlights = [
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ To retrieve the selected region, use the [Retrieve Region API route](!api!/store
|
||||
const regionId = localStorage.getItem("region_id")
|
||||
|
||||
fetch(`http://localhost:9000/store/regions/${regionId}`, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ region }) => {
|
||||
|
||||
@@ -46,8 +46,8 @@ module.exports = defineConfig({
|
||||
http: {
|
||||
storeCors: "http://localhost:3000",
|
||||
// ...
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
@@ -12,8 +12,8 @@ module.exports = defineConfig({
|
||||
http: {
|
||||
storeCors: process.env.STORE_CORS,
|
||||
adminCors: process.env.ADMIN_CORS,
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -323,10 +323,6 @@ export const filesMap = [
|
||||
"filePath": "/www/apps/resources/app/commerce-modules/customer/page.mdx",
|
||||
"pathname": "/commerce-modules/customer"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/commerce-modules/customer/register-customer-email/page.mdx",
|
||||
"pathname": "/commerce-modules/customer/register-customer-email"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/commerce-modules/customer/relations-to-other-modules/page.mdx",
|
||||
"pathname": "/commerce-modules/customer/relations-to-other-modules"
|
||||
@@ -923,6 +919,38 @@ export const filesMap = [
|
||||
"filePath": "/www/apps/resources/app/storefront-development/checkout/shipping/page.mdx",
|
||||
"pathname": "/storefront-development/checkout/shipping"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/customers/addresses/page.mdx",
|
||||
"pathname": "/storefront-development/customers/addresses"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/customers/context/page.mdx",
|
||||
"pathname": "/storefront-development/customers/context"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/customers/log-out/page.mdx",
|
||||
"pathname": "/storefront-development/customers/log-out"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/customers/login/page.mdx",
|
||||
"pathname": "/storefront-development/customers/login"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/customers/page.mdx",
|
||||
"pathname": "/storefront-development/customers"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/customers/profile/page.mdx",
|
||||
"pathname": "/storefront-development/customers/profile"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/customers/register/page.mdx",
|
||||
"pathname": "/storefront-development/customers/register"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/customers/retrieve/page.mdx",
|
||||
"pathname": "/storefront-development/customers/retrieve"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/page.mdx",
|
||||
"pathname": "/storefront-development"
|
||||
|
||||
@@ -1030,20 +1030,6 @@ export const generatedSidebar = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"title": "Guides",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/commerce-modules/customer/register-customer-email",
|
||||
"title": "Register a Customer with Email",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
@@ -7140,49 +7126,6 @@ export const generatedSidebar = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart",
|
||||
"title": "Carts",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/create",
|
||||
"title": "Create Cart",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/retrieve",
|
||||
"title": "Retrieve Cart",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/context",
|
||||
"title": "Cart React Context",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/update",
|
||||
"title": "Update Cart",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/manage-items",
|
||||
"title": "Manage Line Items",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
@@ -7284,6 +7227,49 @@ export const generatedSidebar = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart",
|
||||
"title": "Carts",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/create",
|
||||
"title": "Create Cart",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/retrieve",
|
||||
"title": "Retrieve Cart",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/context",
|
||||
"title": "Cart React Context",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/update",
|
||||
"title": "Update Cart",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/cart/manage-items",
|
||||
"title": "Manage Line Items",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
@@ -7334,6 +7320,63 @@ export const generatedSidebar = [
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/customers",
|
||||
"title": "Customers",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/customers/register",
|
||||
"title": "Register Customer",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/customers/login",
|
||||
"title": "Login Customer",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/customers/retrieve",
|
||||
"title": "Retrieve Customer",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/customers/context",
|
||||
"title": "Customer React Context",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/customers/profile",
|
||||
"title": "Edit Customer Profile",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/customers/addresses",
|
||||
"title": "Manage Customer Addresses",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/storefront-development/customers/log-out",
|
||||
"title": "Log-out Customer",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -310,15 +310,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Guides",
|
||||
children: [
|
||||
{
|
||||
path: "/commerce-modules/customer/register-customer-email",
|
||||
title: "Register a Customer with Email",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "References",
|
||||
children: [
|
||||
@@ -1809,32 +1800,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart",
|
||||
title: "Carts",
|
||||
children: [
|
||||
{
|
||||
path: "/storefront-development/cart/create",
|
||||
title: "Create Cart",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart/retrieve",
|
||||
title: "Retrieve Cart",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart/context",
|
||||
title: "Cart React Context",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart/update",
|
||||
title: "Update Cart",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart/manage-items",
|
||||
title: "Manage Line Items",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/products",
|
||||
title: "Products",
|
||||
@@ -1897,6 +1862,32 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart",
|
||||
title: "Carts",
|
||||
children: [
|
||||
{
|
||||
path: "/storefront-development/cart/create",
|
||||
title: "Create Cart",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart/retrieve",
|
||||
title: "Retrieve Cart",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart/context",
|
||||
title: "Cart React Context",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart/update",
|
||||
title: "Update Cart",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/cart/manage-items",
|
||||
title: "Manage Line Items",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/checkout",
|
||||
title: "Checkout",
|
||||
@@ -1929,6 +1920,40 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/customers",
|
||||
title: "Customers",
|
||||
children: [
|
||||
{
|
||||
path: "/storefront-development/customers/register",
|
||||
title: "Register Customer",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/customers/login",
|
||||
title: "Login Customer",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/customers/retrieve",
|
||||
title: "Retrieve Customer",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/customers/context",
|
||||
title: "Customer React Context",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/customers/profile",
|
||||
title: "Edit Customer Profile",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/customers/addresses",
|
||||
title: "Manage Customer Addresses",
|
||||
},
|
||||
{
|
||||
path: "/storefront-development/customers/log-out",
|
||||
title: "Log-out Customer",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user