├── .babelrc.js ├── .env.template ├── .github └── dependabot.yml ├── .gitignore ├── .r ├── dark.png ├── light.png └── logo.png ├── .vscode └── settings.json ├── .yarnrc.yml ├── LICENSE ├── README.md ├── data ├── seed-onboarding.json └── seed.json ├── index.js ├── medusa-config.js ├── package.json ├── src ├── admin │ ├── components │ │ ├── onboarding-flow │ │ │ ├── default │ │ │ │ ├── orders │ │ │ │ │ ├── order-detail.tsx │ │ │ │ │ └── orders-list.tsx │ │ │ │ └── products │ │ │ │ │ ├── product-detail.tsx │ │ │ │ │ └── products-list.tsx │ │ │ └── nextjs │ │ │ │ ├── orders │ │ │ │ ├── order-detail.tsx │ │ │ │ └── orders-list.tsx │ │ │ │ └── products │ │ │ │ ├── product-detail.tsx │ │ │ │ └── products-list.tsx │ │ └── shared │ │ │ ├── accordion.tsx │ │ │ ├── card.tsx │ │ │ └── icons │ │ │ ├── active-circle-dotted-line.tsx │ │ │ └── get-started.tsx │ ├── types │ │ └── icon-type.ts │ ├── utils │ │ ├── prepare-region.ts │ │ ├── prepare-shipping-options.ts │ │ └── sample-products.ts │ └── widgets │ │ └── onboarding-flow │ │ └── onboarding-flow.tsx ├── api │ ├── README.md │ ├── admin │ │ ├── custom │ │ │ └── route.ts │ │ └── onboarding │ │ │ └── route.ts │ └── store │ │ └── custom │ │ └── route.ts ├── jobs │ └── README.md ├── loaders │ └── README.md ├── migrations │ ├── 1685715079776-CreateOnboarding.ts │ ├── 1686062614694-AddOnboardingProduct.ts │ ├── 1690996567455-CorrectOnboardingFields.ts │ └── README.md ├── models │ ├── README.md │ └── onboarding.ts ├── repositories │ └── onboarding.ts ├── services │ ├── README.md │ ├── __tests__ │ │ └── test-service.spec.ts │ └── onboarding.ts ├── subscribers │ └── README.md └── types │ └── onboarding.ts ├── tsconfig.admin.json ├── tsconfig.json ├── tsconfig.server.json └── tsconfig.spec.json /.babelrc.js: -------------------------------------------------------------------------------- 1 | let ignore = [`**/dist`] 2 | 3 | // Jest needs to compile this code, but generally we don't want this copied 4 | // to output folders 5 | if (process.env.NODE_ENV !== `test`) { 6 | ignore.push(`**/__tests__`) 7 | } 8 | 9 | module.exports = { 10 | presets: [["babel-preset-medusa-package"], ["@babel/preset-typescript"]], 11 | ignore, 12 | } 13 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | JWT_SECRET=something 2 | COOKIE_SECRET=something 3 | 4 | DATABASE_TYPE="postgres" 5 | REDIS_URL=redis://localhost:6379 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | allow: 8 | - dependency-type: production 9 | groups: 10 | medusa: 11 | patterns: 12 | - "@medusajs*" 13 | - "medusa*" 14 | update-types: 15 | - "minor" 16 | - "patch" 17 | ignore: 18 | - dependency-name: "@medusajs*" 19 | update-types: ["version-update:semver-major"] 20 | - dependency-name: "medusa*" 21 | update-types: ["version-update:semver-major"] 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | .env 3 | .DS_Store 4 | /uploads 5 | /node_modules 6 | yarn-error.log 7 | 8 | .idea 9 | 10 | coverage 11 | 12 | !src/** 13 | 14 | ./tsconfig.tsbuildinfo 15 | package-lock.json 16 | yarn.lock 17 | medusa-db.sql 18 | build 19 | .cache 20 | 21 | .yarn/* 22 | !.yarn/patches 23 | !.yarn/plugins 24 | !.yarn/releases 25 | !.yarn/sdks 26 | !.yarn/versions 27 | -------------------------------------------------------------------------------- /.r/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perseidesjs/marketplace-api/eb12aac0272e11e853be5c5bfcb168f8290aa9bb/.r/dark.png -------------------------------------------------------------------------------- /.r/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perseidesjs/marketplace-api/eb12aac0272e11e853be5c5bfcb168f8290aa9bb/.r/light.png -------------------------------------------------------------------------------- /.r/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perseidesjs/marketplace-api/eb12aac0272e11e853be5c5bfcb168f8290aa9bb/.r/logo.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 perseides 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | Perseides logo 7 | 8 | 9 |

10 |

11 | perseides / marketplace 12 |

13 | 14 |

15 | Blog | 16 | Website | 17 | Medusa 18 |

19 | 20 | > [!IMPORTANT] 21 | > **This repo has been archived and only concerns version 1.x of Medusa**. 22 | > **If you still want to apply it to your project, please follow the blog ["here"](https://blog.perseides.org/marketplace-lets-follow-the-recipe)** 23 | 24 |

25 | Initial repository for the perseides / marketplace series of blog posts. 26 |

27 | -------------------------------------------------------------------------------- /data/seed-onboarding.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "currencies": [ 4 | "eur", 5 | "usd" 6 | ] 7 | }, 8 | "users": [], 9 | "regions": [ 10 | { 11 | "id": "test-region-eu", 12 | "name": "EU", 13 | "currency_code": "eur", 14 | "tax_rate": 0, 15 | "payment_providers": [ 16 | "manual" 17 | ], 18 | "fulfillment_providers": [ 19 | "manual" 20 | ], 21 | "countries": [ 22 | "gb", 23 | "de", 24 | "dk", 25 | "se", 26 | "fr", 27 | "es", 28 | "it" 29 | ] 30 | }, 31 | { 32 | "id": "test-region-na", 33 | "name": "NA", 34 | "currency_code": "usd", 35 | "tax_rate": 0, 36 | "payment_providers": [ 37 | "manual" 38 | ], 39 | "fulfillment_providers": [ 40 | "manual" 41 | ], 42 | "countries": [ 43 | "us", 44 | "ca" 45 | ] 46 | } 47 | ], 48 | "shipping_options": [ 49 | { 50 | "name": "PostFake Standard", 51 | "region_id": "test-region-eu", 52 | "provider_id": "manual", 53 | "data": { 54 | "id": "manual-fulfillment" 55 | }, 56 | "price_type": "flat_rate", 57 | "amount": 1000 58 | }, 59 | { 60 | "name": "PostFake Express", 61 | "region_id": "test-region-eu", 62 | "provider_id": "manual", 63 | "data": { 64 | "id": "manual-fulfillment" 65 | }, 66 | "price_type": "flat_rate", 67 | "amount": 1500 68 | }, 69 | { 70 | "name": "PostFake Return", 71 | "region_id": "test-region-eu", 72 | "provider_id": "manual", 73 | "data": { 74 | "id": "manual-fulfillment" 75 | }, 76 | "price_type": "flat_rate", 77 | "is_return": true, 78 | "amount": 1000 79 | }, 80 | { 81 | "name": "I want to return it myself", 82 | "region_id": "test-region-eu", 83 | "provider_id": "manual", 84 | "data": { 85 | "id": "manual-fulfillment" 86 | }, 87 | "price_type": "flat_rate", 88 | "is_return": true, 89 | "amount": 0 90 | }, 91 | { 92 | "name": "FakeEx Standard", 93 | "region_id": "test-region-na", 94 | "provider_id": "manual", 95 | "data": { 96 | "id": "manual-fulfillment" 97 | }, 98 | "price_type": "flat_rate", 99 | "amount": 800 100 | }, 101 | { 102 | "name": "FakeEx Express", 103 | "region_id": "test-region-na", 104 | "provider_id": "manual", 105 | "data": { 106 | "id": "manual-fulfillment" 107 | }, 108 | "price_type": "flat_rate", 109 | "amount": 1200 110 | }, 111 | { 112 | "name": "FakeEx Return", 113 | "region_id": "test-region-na", 114 | "provider_id": "manual", 115 | "data": { 116 | "id": "manual-fulfillment" 117 | }, 118 | "price_type": "flat_rate", 119 | "is_return": true, 120 | "amount": 800 121 | }, 122 | { 123 | "name": "I want to return it myself", 124 | "region_id": "test-region-na", 125 | "provider_id": "manual", 126 | "data": { 127 | "id": "manual-fulfillment" 128 | }, 129 | "price_type": "flat_rate", 130 | "is_return": true, 131 | "amount": 0 132 | } 133 | ], 134 | "products": [], 135 | "categories": [], 136 | "publishable_api_keys": [ 137 | { 138 | "title": "Development" 139 | } 140 | ] 141 | } -------------------------------------------------------------------------------- /data/seed.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "currencies": [ 4 | "eur", 5 | "usd" 6 | ] 7 | }, 8 | "users": [ 9 | { 10 | "email": "admin@medusa-test.com", 11 | "password": "supersecret" 12 | } 13 | ], 14 | "regions": [ 15 | { 16 | "id": "test-region-eu", 17 | "name": "EU", 18 | "currency_code": "eur", 19 | "tax_rate": 0, 20 | "payment_providers": [ 21 | "manual" 22 | ], 23 | "fulfillment_providers": [ 24 | "manual" 25 | ], 26 | "countries": [ 27 | "gb", 28 | "de", 29 | "dk", 30 | "se", 31 | "fr", 32 | "es", 33 | "it" 34 | ] 35 | }, 36 | { 37 | "id": "test-region-na", 38 | "name": "NA", 39 | "currency_code": "usd", 40 | "tax_rate": 0, 41 | "payment_providers": [ 42 | "manual" 43 | ], 44 | "fulfillment_providers": [ 45 | "manual" 46 | ], 47 | "countries": [ 48 | "us", 49 | "ca" 50 | ] 51 | } 52 | ], 53 | "shipping_options": [ 54 | { 55 | "name": "PostFake Standard", 56 | "region_id": "test-region-eu", 57 | "provider_id": "manual", 58 | "data": { 59 | "id": "manual-fulfillment" 60 | }, 61 | "price_type": "flat_rate", 62 | "amount": 1000 63 | }, 64 | { 65 | "name": "PostFake Express", 66 | "region_id": "test-region-eu", 67 | "provider_id": "manual", 68 | "data": { 69 | "id": "manual-fulfillment" 70 | }, 71 | "price_type": "flat_rate", 72 | "amount": 1500 73 | }, 74 | { 75 | "name": "PostFake Return", 76 | "region_id": "test-region-eu", 77 | "provider_id": "manual", 78 | "data": { 79 | "id": "manual-fulfillment" 80 | }, 81 | "price_type": "flat_rate", 82 | "is_return": true, 83 | "amount": 1000 84 | }, 85 | { 86 | "name": "I want to return it myself", 87 | "region_id": "test-region-eu", 88 | "provider_id": "manual", 89 | "data": { 90 | "id": "manual-fulfillment" 91 | }, 92 | "price_type": "flat_rate", 93 | "is_return": true, 94 | "amount": 0 95 | }, 96 | { 97 | "name": "FakeEx Standard", 98 | "region_id": "test-region-na", 99 | "provider_id": "manual", 100 | "data": { 101 | "id": "manual-fulfillment" 102 | }, 103 | "price_type": "flat_rate", 104 | "amount": 800 105 | }, 106 | { 107 | "name": "FakeEx Express", 108 | "region_id": "test-region-na", 109 | "provider_id": "manual", 110 | "data": { 111 | "id": "manual-fulfillment" 112 | }, 113 | "price_type": "flat_rate", 114 | "amount": 1200 115 | }, 116 | { 117 | "name": "FakeEx Return", 118 | "region_id": "test-region-na", 119 | "provider_id": "manual", 120 | "data": { 121 | "id": "manual-fulfillment" 122 | }, 123 | "price_type": "flat_rate", 124 | "is_return": true, 125 | "amount": 800 126 | }, 127 | { 128 | "name": "I want to return it myself", 129 | "region_id": "test-region-na", 130 | "provider_id": "manual", 131 | "data": { 132 | "id": "manual-fulfillment" 133 | }, 134 | "price_type": "flat_rate", 135 | "is_return": true, 136 | "amount": 0 137 | } 138 | ], 139 | "products": [ 140 | { 141 | "title": "Medusa T-Shirt", 142 | "categories": [ 143 | { 144 | "id": "pcat_shirts" 145 | } 146 | ], 147 | "subtitle": null, 148 | "description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", 149 | "handle": "t-shirt", 150 | "is_giftcard": false, 151 | "weight": 400, 152 | "images": [ 153 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", 154 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", 155 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", 156 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png" 157 | ], 158 | "options": [ 159 | { 160 | "title": "Size", 161 | "values": [ 162 | "S", 163 | "M", 164 | "L", 165 | "XL" 166 | ] 167 | }, 168 | { 169 | "title": "Color", 170 | "values": [ 171 | "Black", 172 | "White" 173 | ] 174 | } 175 | ], 176 | "variants": [ 177 | { 178 | "title": "S / Black", 179 | "prices": [ 180 | { 181 | "currency_code": "eur", 182 | "amount": 1950 183 | }, 184 | { 185 | "currency_code": "usd", 186 | "amount": 2200 187 | } 188 | ], 189 | "options": [ 190 | { 191 | "value": "S" 192 | }, 193 | { 194 | "value": "Black" 195 | } 196 | ], 197 | "inventory_quantity": 100, 198 | "manage_inventory": true 199 | }, 200 | { 201 | "title": "S / White", 202 | "prices": [ 203 | { 204 | "currency_code": "eur", 205 | "amount": 1950 206 | }, 207 | { 208 | "currency_code": "usd", 209 | "amount": 2200 210 | } 211 | ], 212 | "options": [ 213 | { 214 | "value": "S" 215 | }, 216 | { 217 | "value": "White" 218 | } 219 | ], 220 | "inventory_quantity": 100, 221 | "manage_inventory": true 222 | }, 223 | { 224 | "title": "M / Black", 225 | "prices": [ 226 | { 227 | "currency_code": "eur", 228 | "amount": 1950 229 | }, 230 | { 231 | "currency_code": "usd", 232 | "amount": 2200 233 | } 234 | ], 235 | "options": [ 236 | { 237 | "value": "M" 238 | }, 239 | { 240 | "value": "Black" 241 | } 242 | ], 243 | "inventory_quantity": 100, 244 | "manage_inventory": true 245 | }, 246 | { 247 | "title": "M / White", 248 | "prices": [ 249 | { 250 | "currency_code": "eur", 251 | "amount": 1950 252 | }, 253 | { 254 | "currency_code": "usd", 255 | "amount": 2200 256 | } 257 | ], 258 | "options": [ 259 | { 260 | "value": "M" 261 | }, 262 | { 263 | "value": "White" 264 | } 265 | ], 266 | "inventory_quantity": 100, 267 | "manage_inventory": true 268 | }, 269 | { 270 | "title": "L / Black", 271 | "prices": [ 272 | { 273 | "currency_code": "eur", 274 | "amount": 1950 275 | }, 276 | { 277 | "currency_code": "usd", 278 | "amount": 2200 279 | } 280 | ], 281 | "options": [ 282 | { 283 | "value": "L" 284 | }, 285 | { 286 | "value": "Black" 287 | } 288 | ], 289 | "inventory_quantity": 100, 290 | "manage_inventory": true 291 | }, 292 | { 293 | "title": "L / White", 294 | "prices": [ 295 | { 296 | "currency_code": "eur", 297 | "amount": 1950 298 | }, 299 | { 300 | "currency_code": "usd", 301 | "amount": 2200 302 | } 303 | ], 304 | "options": [ 305 | { 306 | "value": "L" 307 | }, 308 | { 309 | "value": "White" 310 | } 311 | ], 312 | "inventory_quantity": 100, 313 | "manage_inventory": true 314 | }, 315 | { 316 | "title": "XL / Black", 317 | "prices": [ 318 | { 319 | "currency_code": "eur", 320 | "amount": 1950 321 | }, 322 | { 323 | "currency_code": "usd", 324 | "amount": 2200 325 | } 326 | ], 327 | "options": [ 328 | { 329 | "value": "XL" 330 | }, 331 | { 332 | "value": "Black" 333 | } 334 | ], 335 | "inventory_quantity": 100, 336 | "manage_inventory": true 337 | }, 338 | { 339 | "title": "XL / White", 340 | "prices": [ 341 | { 342 | "currency_code": "eur", 343 | "amount": 1950 344 | }, 345 | { 346 | "currency_code": "usd", 347 | "amount": 2200 348 | } 349 | ], 350 | "options": [ 351 | { 352 | "value": "XL" 353 | }, 354 | { 355 | "value": "White" 356 | } 357 | ], 358 | "inventory_quantity": 100, 359 | "manage_inventory": true 360 | } 361 | ] 362 | }, 363 | { 364 | "title": "Medusa Sweatshirt", 365 | "categories": [ 366 | { 367 | "id": "pcat_shirts" 368 | } 369 | ], 370 | "subtitle": null, 371 | "description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", 372 | "handle": "sweatshirt", 373 | "is_giftcard": false, 374 | "weight": 400, 375 | "images": [ 376 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", 377 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png" 378 | ], 379 | "options": [ 380 | { 381 | "title": "Size", 382 | "values": [ 383 | "S", 384 | "M", 385 | "L", 386 | "XL" 387 | ] 388 | } 389 | ], 390 | "variants": [ 391 | { 392 | "title": "S", 393 | "prices": [ 394 | { 395 | "currency_code": "eur", 396 | "amount": 2950 397 | }, 398 | { 399 | "currency_code": "usd", 400 | "amount": 3350 401 | } 402 | ], 403 | "options": [ 404 | { 405 | "value": "S" 406 | } 407 | ], 408 | "inventory_quantity": 100, 409 | "manage_inventory": true 410 | }, 411 | { 412 | "title": "M", 413 | "prices": [ 414 | { 415 | "currency_code": "eur", 416 | "amount": 2950 417 | }, 418 | { 419 | "currency_code": "usd", 420 | "amount": 3350 421 | } 422 | ], 423 | "options": [ 424 | { 425 | "value": "M" 426 | } 427 | ], 428 | "inventory_quantity": 100, 429 | "manage_inventory": true 430 | }, 431 | { 432 | "title": "L", 433 | "prices": [ 434 | { 435 | "currency_code": "eur", 436 | "amount": 2950 437 | }, 438 | { 439 | "currency_code": "usd", 440 | "amount": 3350 441 | } 442 | ], 443 | "options": [ 444 | { 445 | "value": "L" 446 | } 447 | ], 448 | "inventory_quantity": 100, 449 | "manage_inventory": true 450 | }, 451 | { 452 | "title": "XL", 453 | "prices": [ 454 | { 455 | "currency_code": "eur", 456 | "amount": 2950 457 | }, 458 | { 459 | "currency_code": "usd", 460 | "amount": 3350 461 | } 462 | ], 463 | "options": [ 464 | { 465 | "value": "XL" 466 | } 467 | ], 468 | "inventory_quantity": 100, 469 | "manage_inventory": true 470 | } 471 | ] 472 | }, 473 | { 474 | "title": "Medusa Sweatpants", 475 | "categories": [ 476 | { 477 | "id": "pcat_pants" 478 | } 479 | ], 480 | "subtitle": null, 481 | "description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", 482 | "handle": "sweatpants", 483 | "is_giftcard": false, 484 | "weight": 400, 485 | "images": [ 486 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", 487 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png" 488 | ], 489 | "options": [ 490 | { 491 | "title": "Size", 492 | "values": [ 493 | "S", 494 | "M", 495 | "L", 496 | "XL" 497 | ] 498 | } 499 | ], 500 | "variants": [ 501 | { 502 | "title": "S", 503 | "prices": [ 504 | { 505 | "currency_code": "eur", 506 | "amount": 2950 507 | }, 508 | { 509 | "currency_code": "usd", 510 | "amount": 3350 511 | } 512 | ], 513 | "options": [ 514 | { 515 | "value": "S" 516 | } 517 | ], 518 | "inventory_quantity": 100, 519 | "manage_inventory": true 520 | }, 521 | { 522 | "title": "M", 523 | "prices": [ 524 | { 525 | "currency_code": "eur", 526 | "amount": 2950 527 | }, 528 | { 529 | "currency_code": "usd", 530 | "amount": 3350 531 | } 532 | ], 533 | "options": [ 534 | { 535 | "value": "M" 536 | } 537 | ], 538 | "inventory_quantity": 100, 539 | "manage_inventory": true 540 | }, 541 | { 542 | "title": "L", 543 | "prices": [ 544 | { 545 | "currency_code": "eur", 546 | "amount": 2950 547 | }, 548 | { 549 | "currency_code": "usd", 550 | "amount": 3350 551 | } 552 | ], 553 | "options": [ 554 | { 555 | "value": "L" 556 | } 557 | ], 558 | "inventory_quantity": 100, 559 | "manage_inventory": true 560 | }, 561 | { 562 | "title": "XL", 563 | "prices": [ 564 | { 565 | "currency_code": "eur", 566 | "amount": 2950 567 | }, 568 | { 569 | "currency_code": "usd", 570 | "amount": 3350 571 | } 572 | ], 573 | "options": [ 574 | { 575 | "value": "XL" 576 | } 577 | ], 578 | "inventory_quantity": 100, 579 | "manage_inventory": true 580 | } 581 | ] 582 | }, 583 | { 584 | "title": "Medusa Shorts", 585 | "categories": [ 586 | { 587 | "id": "pcat_merch" 588 | } 589 | ], 590 | "subtitle": null, 591 | "description": "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", 592 | "handle": "shorts", 593 | "is_giftcard": false, 594 | "weight": 400, 595 | "images": [ 596 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", 597 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png" 598 | ], 599 | "options": [ 600 | { 601 | "title": "Size", 602 | "values": [ 603 | "S", 604 | "M", 605 | "L", 606 | "XL" 607 | ] 608 | } 609 | ], 610 | "variants": [ 611 | { 612 | "title": "S", 613 | "prices": [ 614 | { 615 | "currency_code": "eur", 616 | "amount": 2500 617 | }, 618 | { 619 | "currency_code": "usd", 620 | "amount": 2850 621 | } 622 | ], 623 | "options": [ 624 | { 625 | "value": "S" 626 | } 627 | ], 628 | "inventory_quantity": 100, 629 | "manage_inventory": true 630 | }, 631 | { 632 | "title": "M", 633 | "prices": [ 634 | { 635 | "currency_code": "eur", 636 | "amount": 2500 637 | }, 638 | { 639 | "currency_code": "usd", 640 | "amount": 2850 641 | } 642 | ], 643 | "options": [ 644 | { 645 | "value": "M" 646 | } 647 | ], 648 | "inventory_quantity": 100, 649 | "manage_inventory": true 650 | }, 651 | { 652 | "title": "L", 653 | "prices": [ 654 | { 655 | "currency_code": "eur", 656 | "amount": 2500 657 | }, 658 | { 659 | "currency_code": "usd", 660 | "amount": 2850 661 | } 662 | ], 663 | "options": [ 664 | { 665 | "value": "L" 666 | } 667 | ], 668 | "inventory_quantity": 100, 669 | "manage_inventory": true 670 | }, 671 | { 672 | "title": "XL", 673 | "prices": [ 674 | { 675 | "currency_code": "eur", 676 | "amount": 2500 677 | }, 678 | { 679 | "currency_code": "usd", 680 | "amount": 2850 681 | } 682 | ], 683 | "options": [ 684 | { 685 | "value": "XL" 686 | } 687 | ], 688 | "inventory_quantity": 100, 689 | "manage_inventory": true 690 | } 691 | ] 692 | }, 693 | { 694 | "title": "Medusa Hoodie", 695 | "categories": [ 696 | { 697 | "id": "pcat_merch" 698 | }, 699 | { 700 | "id": "pcat_hidden_featured" 701 | } 702 | ], 703 | "subtitle": null, 704 | "description": "Reimagine the feeling of a classic hoodie. With our cotton hoodie, everyday essentials no longer have to be ordinary.", 705 | "handle": "hoodie", 706 | "is_giftcard": false, 707 | "weight": 400, 708 | "images": [ 709 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_front.png", 710 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_back.png" 711 | ], 712 | "options": [ 713 | { 714 | "title": "Size", 715 | "values": [ 716 | "S", 717 | "M", 718 | "L", 719 | "XL" 720 | ] 721 | } 722 | ], 723 | "variants": [ 724 | { 725 | "title": "S", 726 | "prices": [ 727 | { 728 | "currency_code": "eur", 729 | "amount": 3650 730 | }, 731 | { 732 | "currency_code": "usd", 733 | "amount": 4150 734 | } 735 | ], 736 | "options": [ 737 | { 738 | "value": "S" 739 | } 740 | ], 741 | "inventory_quantity": 100, 742 | "manage_inventory": true 743 | }, 744 | { 745 | "title": "M", 746 | "prices": [ 747 | { 748 | "currency_code": "eur", 749 | "amount": 3650 750 | }, 751 | { 752 | "currency_code": "usd", 753 | "amount": 4150 754 | } 755 | ], 756 | "options": [ 757 | { 758 | "value": "M" 759 | } 760 | ], 761 | "inventory_quantity": 100, 762 | "manage_inventory": true 763 | }, 764 | { 765 | "title": "L", 766 | "prices": [ 767 | { 768 | "currency_code": "eur", 769 | "amount": 3650 770 | }, 771 | { 772 | "currency_code": "usd", 773 | "amount": 4150 774 | } 775 | ], 776 | "options": [ 777 | { 778 | "value": "L" 779 | } 780 | ], 781 | "inventory_quantity": 100, 782 | "manage_inventory": true 783 | }, 784 | { 785 | "title": "XL", 786 | "prices": [ 787 | { 788 | "currency_code": "eur", 789 | "amount": 3650 790 | }, 791 | { 792 | "currency_code": "usd", 793 | "amount": 4150 794 | } 795 | ], 796 | "options": [ 797 | { 798 | "value": "XL" 799 | } 800 | ], 801 | "inventory_quantity": 100, 802 | "manage_inventory": true 803 | } 804 | ] 805 | }, 806 | { 807 | "title": "Medusa Longsleeve", 808 | "categories": [ 809 | { 810 | "id": "pcat_shirts" 811 | }, 812 | { 813 | "id": "pcat_hidden_featured" 814 | } 815 | ], 816 | "subtitle": null, 817 | "description": "Reimagine the feeling of a classic longsleeve. With our cotton longsleeve, everyday essentials no longer have to be ordinary.", 818 | "handle": "longsleeve", 819 | "is_giftcard": false, 820 | "weight": 400, 821 | "images": [ 822 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-front.png", 823 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-back.png" 824 | ], 825 | "options": [ 826 | { 827 | "title": "Size", 828 | "values": [ 829 | "S", 830 | "M", 831 | "L", 832 | "XL" 833 | ] 834 | } 835 | ], 836 | "variants": [ 837 | { 838 | "title": "S", 839 | "prices": [ 840 | { 841 | "currency_code": "eur", 842 | "amount": 3650 843 | }, 844 | { 845 | "currency_code": "usd", 846 | "amount": 4150 847 | } 848 | ], 849 | "options": [ 850 | { 851 | "value": "S" 852 | } 853 | ], 854 | "inventory_quantity": 100, 855 | "manage_inventory": true 856 | }, 857 | { 858 | "title": "M", 859 | "prices": [ 860 | { 861 | "currency_code": "eur", 862 | "amount": 3650 863 | }, 864 | { 865 | "currency_code": "usd", 866 | "amount": 4150 867 | } 868 | ], 869 | "options": [ 870 | { 871 | "value": "M" 872 | } 873 | ], 874 | "inventory_quantity": 100, 875 | "manage_inventory": true 876 | }, 877 | { 878 | "title": "L", 879 | "prices": [ 880 | { 881 | "currency_code": "eur", 882 | "amount": 3650 883 | }, 884 | { 885 | "currency_code": "usd", 886 | "amount": 4150 887 | } 888 | ], 889 | "options": [ 890 | { 891 | "value": "L" 892 | } 893 | ], 894 | "inventory_quantity": 100, 895 | "manage_inventory": true 896 | }, 897 | { 898 | "title": "XL", 899 | "prices": [ 900 | { 901 | "currency_code": "eur", 902 | "amount": 3650 903 | }, 904 | { 905 | "currency_code": "usd", 906 | "amount": 4150 907 | } 908 | ], 909 | "options": [ 910 | { 911 | "value": "XL" 912 | } 913 | ], 914 | "inventory_quantity": 100, 915 | "manage_inventory": true 916 | } 917 | ] 918 | }, 919 | { 920 | "title": "Medusa Coffee Mug", 921 | "categories": [ 922 | { 923 | "id": "pcat_merch" 924 | }, 925 | { 926 | "id": "pcat_hidden_featured" 927 | } 928 | ], 929 | "subtitle": null, 930 | "description": "Every programmer's best friend.", 931 | "handle": "coffee-mug", 932 | "is_giftcard": false, 933 | "weight": 400, 934 | "images": [ 935 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png" 936 | ], 937 | "options": [ 938 | { 939 | "title": "Size", 940 | "values": [ 941 | "One Size" 942 | ] 943 | } 944 | ], 945 | "variants": [ 946 | { 947 | "title": "One Size", 948 | "prices": [ 949 | { 950 | "currency_code": "eur", 951 | "amount": 1000 952 | }, 953 | { 954 | "currency_code": "usd", 955 | "amount": 1200 956 | } 957 | ], 958 | "options": [ 959 | { 960 | "value": "One Size" 961 | } 962 | ], 963 | "inventory_quantity": 100, 964 | "manage_inventory": true 965 | } 966 | ] 967 | } 968 | ], 969 | "categories": [ 970 | { 971 | "id": "pcat_pants", 972 | "name": "Pants", 973 | "rank": 0, 974 | "category_children": [], 975 | "handle": "pants" 976 | }, 977 | { 978 | "id": "pcat_shirts", 979 | "name": "Shirts", 980 | "rank": 0, 981 | "category_children": [], 982 | "handle": "shirts" 983 | }, 984 | { 985 | "id": "pcat_merch", 986 | "name": "Merch", 987 | "rank": 0, 988 | "category_children": [], 989 | "handle": "merch" 990 | }, 991 | { 992 | "id": "pcat_hidden_carousel", 993 | "name": "Hidden homepage carousel", 994 | "rank": 0, 995 | "category_children": [], 996 | "handle": "hidden-homepage-carousel" 997 | }, 998 | { 999 | "id": "pcat_hidden_featured", 1000 | "name": "Hidden homepage featured", 1001 | "rank": 0, 1002 | "category_children": [], 1003 | "handle": "hidden-homepage-featured-items" 1004 | } 1005 | ] 1006 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | const { GracefulShutdownServer } = require("medusa-core-utils") 3 | 4 | const loaders = require("@medusajs/medusa/dist/loaders/index").default 5 | 6 | ;(async() => { 7 | async function start() { 8 | const app = express() 9 | const directory = process.cwd() 10 | 11 | try { 12 | const { container } = await loaders({ 13 | directory, 14 | expressApp: app 15 | }) 16 | const configModule = container.resolve("configModule") 17 | const port = process.env.PORT ?? configModule.projectConfig.port ?? 9000 18 | 19 | const server = GracefulShutdownServer.create( 20 | app.listen(port, (err) => { 21 | if (err) { 22 | return 23 | } 24 | console.log(`Server is ready on port: ${port}`) 25 | }) 26 | ) 27 | 28 | // Handle graceful shutdown 29 | const gracefulShutDown = () => { 30 | server 31 | .shutdown() 32 | .then(() => { 33 | console.info("Gracefully stopping the server.") 34 | process.exit(0) 35 | }) 36 | .catch((e) => { 37 | console.error("Error received when shutting down the server.", e) 38 | process.exit(1) 39 | }) 40 | } 41 | process.on("SIGTERM", gracefulShutDown) 42 | process.on("SIGINT", gracefulShutDown) 43 | } catch (err) { 44 | console.error("Error starting server", err) 45 | process.exit(1) 46 | } 47 | } 48 | 49 | await start() 50 | })() 51 | -------------------------------------------------------------------------------- /medusa-config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | 3 | let ENV_FILE_NAME = ""; 4 | switch (process.env.NODE_ENV) { 5 | case "production": 6 | ENV_FILE_NAME = ".env.production"; 7 | break; 8 | case "staging": 9 | ENV_FILE_NAME = ".env.staging"; 10 | break; 11 | case "test": 12 | ENV_FILE_NAME = ".env.test"; 13 | break; 14 | case "development": 15 | default: 16 | ENV_FILE_NAME = ".env"; 17 | break; 18 | } 19 | 20 | try { 21 | dotenv.config({ path: process.cwd() + "/" + ENV_FILE_NAME }); 22 | } catch (e) {} 23 | 24 | // CORS when consuming Medusa from admin 25 | const ADMIN_CORS = 26 | process.env.ADMIN_CORS || "http://localhost:7000,http://localhost:7001"; 27 | 28 | // CORS to avoid issues when consuming Medusa from a client 29 | const STORE_CORS = process.env.STORE_CORS || "http://localhost:8000"; 30 | 31 | const DATABASE_URL = 32 | process.env.DATABASE_URL || "postgres://localhost/medusa-starter-default"; 33 | 34 | const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379"; 35 | 36 | const plugins = [ 37 | `medusa-fulfillment-manual`, 38 | `medusa-payment-manual`, 39 | { 40 | resolve: `@medusajs/file-local`, 41 | options: { 42 | upload_dir: "uploads", 43 | }, 44 | }, 45 | { 46 | resolve: "@medusajs/admin", 47 | /** @type {import('@medusajs/admin').PluginOptions} */ 48 | options: { 49 | autoRebuild: true, 50 | develop: { 51 | open: process.env.OPEN_BROWSER !== "false", 52 | }, 53 | }, 54 | }, 55 | ]; 56 | 57 | const modules = { 58 | /*eventBus: { 59 | resolve: "@medusajs/event-bus-redis", 60 | options: { 61 | redisUrl: REDIS_URL 62 | } 63 | }, 64 | cacheService: { 65 | resolve: "@medusajs/cache-redis", 66 | options: { 67 | redisUrl: REDIS_URL 68 | } 69 | },*/ 70 | }; 71 | 72 | /** @type {import('@medusajs/medusa').ConfigModule["projectConfig"]} */ 73 | const projectConfig = { 74 | jwtSecret: process.env.JWT_SECRET, 75 | cookieSecret: process.env.COOKIE_SECRET, 76 | store_cors: STORE_CORS, 77 | database_url: DATABASE_URL, 78 | admin_cors: ADMIN_CORS, 79 | // Uncomment the following lines to enable REDIS 80 | // redis_url: REDIS_URL 81 | }; 82 | 83 | /** @type {import('@medusajs/medusa').ConfigModule} */ 84 | module.exports = { 85 | projectConfig, 86 | plugins, 87 | modules, 88 | }; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medusa-starter-default", 3 | "version": "0.0.1", 4 | "description": "A starter for Medusa projects.", 5 | "author": "Medusa (https://medusajs.com)", 6 | "license": "MIT", 7 | "keywords": [ 8 | "sqlite", 9 | "postgres", 10 | "typescript", 11 | "ecommerce", 12 | "headless", 13 | "medusa" 14 | ], 15 | "scripts": { 16 | "clean": "cross-env ./node_modules/.bin/rimraf dist", 17 | "build": "cross-env npm run clean && npm run build:server && npm run build:admin", 18 | "build:server": "cross-env npm run clean && tsc -p tsconfig.server.json", 19 | "build:admin": "cross-env medusa-admin build", 20 | "watch": "cross-env tsc --watch", 21 | "test": "cross-env jest", 22 | "seed": "cross-env medusa seed -f ./data/seed.json", 23 | "start": "cross-env npm run build && medusa start", 24 | "start:custom": "cross-env npm run build && node --preserve-symlinks --trace-warnings index.js", 25 | "dev": "cross-env npm run build:server && medusa develop" 26 | }, 27 | "dependencies": { 28 | "@medusajs/admin": "^7.1.14", 29 | "@medusajs/cache-inmemory": "latest", 30 | "@medusajs/cache-redis": "^1.9.1", 31 | "@medusajs/event-bus-local": "latest", 32 | "@medusajs/event-bus-redis": "^1.8.13", 33 | "@medusajs/file-local": "latest", 34 | "@medusajs/medusa": "^1.20.6", 35 | "@tanstack/react-query": "4.22.0", 36 | "body-parser": "^1.19.0", 37 | "cors": "^2.8.5", 38 | "dotenv": "16.3.1", 39 | "express": "^4.17.2", 40 | "medusa-fulfillment-manual": "latest", 41 | "medusa-interfaces": "latest", 42 | "medusa-payment-manual": "latest", 43 | "medusa-payment-stripe": "latest", 44 | "prism-react-renderer": "^2.0.4", 45 | "typeorm": "^0.3.16" 46 | }, 47 | "devDependencies": { 48 | "@babel/cli": "^7.14.3", 49 | "@babel/core": "^7.14.3", 50 | "@babel/preset-typescript": "^7.21.4", 51 | "@medusajs/medusa-cli": "latest", 52 | "@stdlib/number-float64-base-normalize": "0.0.8", 53 | "@types/express": "^4.17.13", 54 | "@types/jest": "^27.4.0", 55 | "@types/mime": "1.3.5", 56 | "@types/node": "^17.0.8", 57 | "babel-preset-medusa-package": "^1.1.19", 58 | "cross-env": "^7.0.3", 59 | "eslint": "^6.8.0", 60 | "jest": "^27.3.1", 61 | "rimraf": "^3.0.2", 62 | "ts-jest": "^27.0.7", 63 | "ts-loader": "^9.2.6", 64 | "typescript": "^4.5.2" 65 | }, 66 | "jest": { 67 | "globals": { 68 | "ts-jest": { 69 | "tsconfig": "tsconfig.spec.json" 70 | } 71 | }, 72 | "moduleFileExtensions": [ 73 | "js", 74 | "json", 75 | "ts" 76 | ], 77 | "testPathIgnorePatterns": [ 78 | "/node_modules/", 79 | "/node_modules/" 80 | ], 81 | "rootDir": "src", 82 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$", 83 | "transform": { 84 | ".ts": "ts-jest" 85 | }, 86 | "collectCoverageFrom": [ 87 | "**/*.(t|j)s" 88 | ], 89 | "coverageDirectory": "./coverage", 90 | "testEnvironment": "node" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/admin/components/onboarding-flow/default/orders/order-detail.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | ComputerDesktopSolid, 4 | CurrencyDollarSolid, 5 | NextJs 6 | } from "@medusajs/icons"; 7 | import { IconBadge, Heading, Text } from "@medusajs/ui"; 8 | 9 | const OrderDetailDefault = () => { 10 | return ( 11 | <> 12 | 13 | You finished the setup guide 🎉 You now have your first order. Feel free 14 | to play around with the order management functionalities, such as 15 | capturing payment, creating fulfillments, and more. 16 | 17 | 21 | Start developing with Medusa 22 | 23 | 24 | Medusa is a completely customizable commerce solution. We've curated 25 | some essential guides to kickstart your development with Medusa. 26 | 27 |
28 | 33 |
34 |
35 |
36 | 37 | 38 | 39 |
40 |
41 |
42 | 47 | Add Commerce Features 48 | 49 | 50 | Learn about all available commerce features and how to 51 | add them in your storefront 52 | 53 |
54 |
55 |
56 | 61 |
62 |
63 |
64 | 65 | 66 | 67 |
68 |
69 |
70 | 75 | Build Custom Use Cases 76 | 77 | 78 | Build a marketplace, subscription-based purchases, 79 | or your custom use-cases. 80 | 81 |
82 |
83 |
84 | 89 |
90 |
91 |
92 | 93 | 94 | 95 |
96 |
97 |
98 | 103 | Install Next.js Quickstart 104 | 105 | 106 | Install and use the Next.js storefront with 107 | your commerce store. 108 | 109 |
110 |
111 |
112 |
113 |
114 | You can find more useful guides in{" "} 115 | 120 | our documentation 121 | 122 | . If you like Medusa, please{" "} 123 | 128 | star us on GitHub 129 | 130 | . 131 |
132 | 133 | ); 134 | }; 135 | 136 | export default OrderDetailDefault; 137 | -------------------------------------------------------------------------------- /src/admin/components/onboarding-flow/default/orders/orders-list.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | useAdminProduct, 4 | useAdminCreateDraftOrder, 5 | useMedusa 6 | } from "medusa-react"; 7 | import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; 8 | import { Button, Text } from "@medusajs/ui"; 9 | import prepareRegions from "../../../../utils/prepare-region"; 10 | import prepareShippingOptions from "../../../../utils/prepare-shipping-options"; 11 | 12 | const OrdersListDefault = ({ onNext, isComplete, data }: StepContentProps) => { 13 | const { product } = useAdminProduct(data.product_id); 14 | const { mutateAsync: createDraftOrder, isLoading } = 15 | useAdminCreateDraftOrder(); 16 | const { client } = useMedusa(); 17 | 18 | const createOrder = async () => { 19 | const variant = product.variants[0] ?? null; 20 | try { 21 | // check if there is a shipping option and a region 22 | // and if not, create demo ones 23 | const regions = await prepareRegions(client) 24 | const shipping_options = await prepareShippingOptions(client, regions[0]) 25 | 26 | const { draft_order } = await createDraftOrder({ 27 | email: "customer@medusajs.com", 28 | items: [ 29 | variant 30 | ? { 31 | quantity: 1, 32 | variant_id: variant?.id, 33 | } 34 | : { 35 | quantity: 1, 36 | title: product.title, 37 | unit_price: 50, 38 | }, 39 | ], 40 | shipping_methods: [ 41 | { 42 | option_id: shipping_options[0].id, 43 | }, 44 | ], 45 | region_id: regions[0].id, 46 | }); 47 | 48 | const { order } = await client.admin.draftOrders.markPaid(draft_order.id); 49 | 50 | onNext(order); 51 | } catch (e) { 52 | console.error(e); 53 | } 54 | }; 55 | return ( 56 | <> 57 |
58 | 59 | The last step is to create a sample order using the product you just created. You can then view your order’s details, process its payment, fulfillment, inventory, and more. 60 | 61 | 62 | By clicking the “Create a Sample Order” button, we’ll generate an order using the product you created and default configurations. 63 | 64 |
65 |
66 | {!isComplete && ( 67 | 75 | )} 76 |
77 | 78 | ); 79 | }; 80 | 81 | export default OrdersListDefault; 82 | -------------------------------------------------------------------------------- /src/admin/components/onboarding-flow/default/products/product-detail.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from "react" 2 | import { 3 | useAdminPublishableApiKeys, 4 | useAdminCreatePublishableApiKey 5 | } from "medusa-react" 6 | import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow" 7 | import { Button, CodeBlock, Text } from "@medusajs/ui" 8 | 9 | const ProductDetailDefault = ({ onNext, isComplete, data }: StepContentProps) => { 10 | const { publishable_api_keys: keys, isLoading, refetch } = useAdminPublishableApiKeys({ 11 | offset: 0, 12 | limit: 1, 13 | }); 14 | const createPublishableApiKey = useAdminCreatePublishableApiKey() 15 | 16 | const api_key = useMemo(() => keys?.[0]?.id || "", [keys]) 17 | const backendUrl = process.env.MEDUSA_BACKEND_URL === "/" || process.env.MEDUSA_ADMIN_BACKEND_URL === "/" ? 18 | location.origin : 19 | process.env.MEDUSA_BACKEND_URL || process.env.MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000" 20 | 21 | useEffect(() => { 22 | if (!isLoading && !keys?.length) { 23 | createPublishableApiKey.mutate({ 24 | "title": "Development" 25 | }, { 26 | onSuccess: () => { 27 | refetch() 28 | } 29 | }) 30 | } 31 | }, [isLoading, keys]) 32 | 33 | return ( 34 |
35 |
36 | On this page, you can view your product's details and edit them. 37 | 38 | You can preview your product using Medusa's Store APIs. You can copy any 39 | of the following code snippets to try it out. 40 | 41 |
42 |
43 | {!isLoading && ( 44 | {\n // ...\n const productService = await initializeProductModule()\n const products = await productService.list({\n id: "${data?.product_id}",\n })\n\n console.log(products[0])\n}`, 64 | }, 65 | ]} className="my-6"> 66 | 67 | 68 | 69 | )} 70 |
71 |
72 | 76 | 79 | 80 | {!isComplete && ( 81 | 84 | )} 85 |
86 |
87 | ); 88 | }; 89 | 90 | export default ProductDetailDefault; 91 | -------------------------------------------------------------------------------- /src/admin/components/onboarding-flow/default/products/products-list.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { 3 | useAdminCreateProduct, 4 | useAdminCreateCollection, 5 | useMedusa 6 | } from "medusa-react"; 7 | import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; 8 | import { Button, Text } from "@medusajs/ui"; 9 | import getSampleProducts from "../../../../utils/sample-products"; 10 | import prepareRegions from "../../../../utils/prepare-region"; 11 | 12 | const ProductsListDefault = ({ onNext, isComplete }: StepContentProps) => { 13 | const { mutateAsync: createCollection, isLoading: collectionLoading } = 14 | useAdminCreateCollection(); 15 | const { mutateAsync: createProduct, isLoading: productLoading } = 16 | useAdminCreateProduct(); 17 | const { client } = useMedusa() 18 | 19 | const isLoading = useMemo(() => 20 | collectionLoading || productLoading, 21 | [collectionLoading, productLoading] 22 | ); 23 | 24 | const createSample = async () => { 25 | try { 26 | const { collection } = await createCollection({ 27 | title: "Merch", 28 | handle: "merch", 29 | }); 30 | 31 | const regions = await prepareRegions(client) 32 | 33 | const sampleProducts = getSampleProducts({ 34 | regions, 35 | collection_id: collection.id 36 | }) 37 | const { product } = await createProduct(sampleProducts[0]); 38 | onNext(product); 39 | } catch (e) { 40 | console.error(e); 41 | } 42 | }; 43 | 44 | return ( 45 |
46 | 47 | Create a product and set its general details such as title and 48 | description, its price, options, variants, images, and more. You'll then 49 | use the product to create a sample order. 50 | 51 | 52 | You can create a product by clicking the "New Product" button below. 53 | Alternatively, if you're not ready to create your own product, we can 54 | create a sample one for you. 55 | 56 | {!isComplete && ( 57 |
58 | 66 |
67 | )} 68 |
69 | ); 70 | }; 71 | 72 | export default ProductsListDefault; 73 | -------------------------------------------------------------------------------- /src/admin/components/onboarding-flow/nextjs/orders/order-detail.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CurrencyDollarSolid, NextJs, ComputerDesktopSolid } from "@medusajs/icons"; 3 | import { IconBadge, Heading, Text } from "@medusajs/ui"; 4 | 5 | const OrderDetailNextjs = () => { 6 | const queryParams = `?ref=onboarding&type=${ 7 | process.env.MEDUSA_ADMIN_ONBOARDING_TYPE || "nextjs" 8 | }`; 9 | return ( 10 | <> 11 | 12 | You finished the setup guide 🎉. You now have a complete ecommerce store 13 | with a backend, admin, and a Next.js storefront. Feel free to play 14 | around with each of these components to experience all commerce features 15 | that Medusa provides. 16 | 17 | 21 | Continue Building your Ecommerce Store 22 | 23 | 24 | Your ecommerce store provides all basic ecommerce features you need to 25 | start selling. You can add more functionalities, add plugins for 26 | third-party integrations, and customize the storefront’s look and feel 27 | to support your use case. 28 | 29 |
30 | 35 |
36 |
37 |
38 | 39 | 40 | 41 |
42 |
43 |
44 | 49 | Build with the Next.js Storefront 50 | 51 | 52 | Learn about the Next.js starter storefront’s features and how to 53 | customize it. 54 | 55 |
56 |
57 |
58 | 63 |
64 |
65 |
66 | 67 | 68 | 69 |
70 |
71 |
72 | 77 | Add Commerce Features 78 | 79 | 80 | Learn about all available commerce features and how to 81 | add them in your storefront 82 | 83 |
84 |
85 |
86 | 91 |
92 |
93 |
94 | 95 | 96 | 97 |
98 |
99 |
100 | 105 | Build Custom Use Cases 106 | 107 | 108 | Build a marketplace, subscription-based purchases, 109 | or your custom use-cases. 110 | 111 |
112 |
113 |
114 |
115 |
116 | You can find more useful guides in{" "} 117 | 122 | our documentation 123 | 124 | . If you like Medusa, please{" "} 125 | 130 | star us on GitHub 131 | 132 | . 133 |
134 | 135 | ); 136 | }; 137 | 138 | export default OrderDetailNextjs; 139 | -------------------------------------------------------------------------------- /src/admin/components/onboarding-flow/nextjs/orders/orders-list.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { 3 | useAdminProduct, 4 | useCreateCart, 5 | useMedusa 6 | } from "medusa-react"; 7 | import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; 8 | import { Button, Text } from "@medusajs/ui"; 9 | import prepareRegions from "../../../../utils/prepare-region"; 10 | import prepareShippingOptions from "../../../../utils/prepare-shipping-options"; 11 | 12 | const OrdersListNextjs = ({ isComplete, data }: StepContentProps) => { 13 | const { product } = useAdminProduct(data.product_id); 14 | const { mutateAsync: createCart, isLoading: cartIsLoading } = useCreateCart() 15 | const { client } = useMedusa() 16 | const [cartId, setCartId] = useState(null) 17 | 18 | const prepareNextjsCheckout = async () => { 19 | const variant = product.variants[0] ?? null; 20 | try { 21 | const regions = await prepareRegions(client) 22 | await prepareShippingOptions(client, regions[0]) 23 | const { cart } = await createCart({ 24 | region_id: regions[0]?.id, 25 | items: [ 26 | { 27 | variant_id: variant?.id, 28 | quantity: 1 29 | } 30 | ] 31 | }) 32 | 33 | setCartId(cart?.id) 34 | } catch (e) { 35 | console.error(e); 36 | } 37 | } 38 | 39 | useEffect(() => { 40 | if (!cartId && product) { 41 | prepareNextjsCheckout() 42 | } 43 | }, [cartId, product]) 44 | 45 | return ( 46 | <> 47 |
48 | 49 | The last step is to create a sample order using one of your products. You can then view your order’s details, process its payment, fulfillment, inventory, and more. 50 | 51 | 52 | You can use the button below to experience hand-first the checkout flow in the Next.js storefront. After placing the order in the storefront, you’ll be directed back here to view the order’s details. 53 | 54 |
55 |
56 | {!isComplete && ( 57 | 61 | 64 | 65 | )} 66 |
67 | 68 | ); 69 | }; 70 | 71 | export default OrdersListNextjs 72 | -------------------------------------------------------------------------------- /src/admin/components/onboarding-flow/nextjs/products/product-detail.tsx: -------------------------------------------------------------------------------- 1 | import { useAdminProduct } from "medusa-react"; 2 | import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; 3 | import { Button, Text } from "@medusajs/ui"; 4 | 5 | const ProductDetailNextjs = ({ onNext, isComplete, data }: StepContentProps) => { 6 | const { product, isLoading: productIsLoading } = useAdminProduct(data?.product_id) 7 | return ( 8 |
9 |
10 | 11 | We have now created a few sample products in your Medusa store. You can scroll down to see what the Product Detail view looks like in the Admin dashboard. 12 | This is also the view you use to edit existing products. 13 | 14 | 15 | To view the products in your store, you can visit the Next.js Storefront that was installed with create-medusa-app. 16 | 17 | 18 | The Next.js Storefront Starter is a template that helps you start building an ecommerce store with Medusa. 19 | You control the code for the storefront and you can customize it further to fit your specific needs. 20 | 21 | 22 | Click the button below to view the products in your Next.js Storefront. 23 | 24 | 25 | Having trouble? Click{" "} 26 | 31 | here 32 | . 33 | 34 |
35 |
36 | 40 | 43 | 44 | {!isComplete && ( 45 | 48 | )} 49 |
50 |
51 | ); 52 | }; 53 | 54 | export default ProductDetailNextjs 55 | -------------------------------------------------------------------------------- /src/admin/components/onboarding-flow/nextjs/products/products-list.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | useAdminCreateProduct, 4 | useAdminCreateCollection, 5 | useMedusa 6 | } from "medusa-react"; 7 | import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; 8 | import { Button, Text } from "@medusajs/ui"; 9 | import { AdminPostProductsReq, Product } from "@medusajs/medusa"; 10 | import getSampleProducts from "../../../../utils/sample-products"; 11 | import prepareRegions from "../../../../utils/prepare-region"; 12 | 13 | const ProductsListNextjs = ({ onNext, isComplete }: StepContentProps) => { 14 | const { mutateAsync: createCollection, isLoading: collectionLoading } = 15 | useAdminCreateCollection(); 16 | const { mutateAsync: createProduct, isLoading: productLoading } = 17 | useAdminCreateProduct(); 18 | const { client } = useMedusa() 19 | 20 | const isLoading = collectionLoading || productLoading; 21 | 22 | const createSample = async () => { 23 | try { 24 | const { collection } = await createCollection({ 25 | title: "Merch", 26 | handle: "merch", 27 | }); 28 | 29 | const regions = await prepareRegions(client) 30 | 31 | const tryCreateProduct = async (sampleProduct: AdminPostProductsReq): Promise => { 32 | try { 33 | return (await createProduct(sampleProduct)).product 34 | } catch { 35 | // ignore if product already exists 36 | return null 37 | } 38 | } 39 | 40 | let product: Product 41 | const sampleProducts = getSampleProducts({ 42 | regions, 43 | collection_id: collection.id 44 | }) 45 | await Promise.all( 46 | sampleProducts.map(async (sampleProduct, index) => { 47 | const createdProduct = await tryCreateProduct(sampleProduct) 48 | if (index === 0 && createProduct) { 49 | product = createdProduct 50 | } 51 | }) 52 | ) 53 | onNext(product); 54 | } catch (e) { 55 | console.error(e); 56 | } 57 | }; 58 | 59 | return ( 60 |
61 | 62 | Products in Medusa represent the products you sell. You can set their general details including a 63 | title and description. Each product has options and variants, and you can set a price for each variant. 64 | 65 | 66 | Click the button below to create sample products. 67 | 68 | {!isComplete && ( 69 |
70 | 78 |
79 | )} 80 |
81 | ); 82 | }; 83 | 84 | export default ProductsListNextjs; 85 | -------------------------------------------------------------------------------- /src/admin/components/shared/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 2 | import React from "react"; 3 | import { CheckCircleSolid, CircleMiniSolid } from "@medusajs/icons"; 4 | import { Heading, Text, clx } from "@medusajs/ui"; 5 | import ActiveCircleDottedLine from "./icons/active-circle-dotted-line"; 6 | 7 | type AccordionItemProps = AccordionPrimitive.AccordionItemProps & { 8 | title: string; 9 | subtitle?: string; 10 | description?: string; 11 | required?: boolean; 12 | tooltip?: string; 13 | forceMountContent?: true; 14 | headingSize?: "small" | "medium" | "large"; 15 | customTrigger?: React.ReactNode; 16 | complete?: boolean; 17 | active?: boolean; 18 | triggerable?: boolean; 19 | }; 20 | 21 | const Accordion: React.FC< 22 | | (AccordionPrimitive.AccordionSingleProps & 23 | React.RefAttributes) 24 | | (AccordionPrimitive.AccordionMultipleProps & 25 | React.RefAttributes) 26 | > & { 27 | Item: React.FC; 28 | } = ({ children, ...props }) => { 29 | return ( 30 | {children} 31 | ); 32 | }; 33 | 34 | const Item: React.FC = ({ 35 | title, 36 | subtitle, 37 | description, 38 | required, 39 | tooltip, 40 | children, 41 | className, 42 | complete, 43 | headingSize = "large", 44 | customTrigger = undefined, 45 | forceMountContent = undefined, 46 | active, 47 | triggerable, 48 | ...props 49 | }) => { 50 | return ( 51 | 59 | 60 |
61 |
62 |
63 |
64 | {complete ? ( 65 | 66 | ) : ( 67 | <> 68 | {active && ( 69 | 73 | )} 74 | {!active && ( 75 | 76 | )} 77 | 78 | )} 79 |
80 | 81 | {title} 82 | 83 |
84 | 85 | {customTrigger || } 86 | 87 |
88 | {subtitle && ( 89 | 90 | {subtitle} 91 | 92 | )} 93 |
94 |
95 | 101 |
102 | {description && {description}} 103 |
{children}
104 |
105 |
106 |
107 | ); 108 | }; 109 | 110 | Accordion.Item = Item; 111 | 112 | const MorphingTrigger = () => { 113 | return ( 114 |
115 |
116 | 117 | 118 |
119 |
120 | ); 121 | }; 122 | 123 | export default Accordion; 124 | -------------------------------------------------------------------------------- /src/admin/components/shared/card.tsx: -------------------------------------------------------------------------------- 1 | import { Text, clx } from "@medusajs/ui" 2 | 3 | type CardProps = { 4 | icon?: React.ReactNode 5 | children?: React.ReactNode 6 | className?: string 7 | } 8 | 9 | const Card = ({ 10 | icon, 11 | children, 12 | className 13 | }: CardProps) => { 14 | return ( 15 |
21 | {icon} 22 | {children} 23 |
24 | ) 25 | } 26 | 27 | export default Card -------------------------------------------------------------------------------- /src/admin/components/shared/icons/active-circle-dotted-line.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import IconProps from "../../../types/icon-type"; 3 | 4 | const ActiveCircleDottedLine: React.FC = ({ 5 | size = "24", 6 | color = "currentColor", 7 | ...attributes 8 | }) => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default ActiveCircleDottedLine; 38 | -------------------------------------------------------------------------------- /src/admin/components/shared/icons/get-started.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import IconProps from "../../../types/icon-type"; 3 | 4 | const GetStarted: React.FC = ({ 5 | size = "40", 6 | color = "currentColor", 7 | ...attributes 8 | }) => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default GetStarted; 25 | -------------------------------------------------------------------------------- /src/admin/types/icon-type.ts: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | type IconProps = { 4 | color?: string 5 | size?: string | number 6 | } & React.SVGAttributes 7 | 8 | export default IconProps 9 | -------------------------------------------------------------------------------- /src/admin/utils/prepare-region.ts: -------------------------------------------------------------------------------- 1 | import { Store } from "@medusajs/medusa" 2 | import type Medusa from "@medusajs/medusa-js" 3 | import { ExtendedStoreDTO } from "@medusajs/medusa/dist/types/store" 4 | 5 | export default async function prepareRegions (client: Medusa) { 6 | let { regions } = await client.admin.regions.list() 7 | if (!regions.length) { 8 | let { store } = await client.admin.store.retrieve() 9 | if (!store.currencies) { 10 | store = (await client.admin.store.update({ 11 | currencies: ["eur"] 12 | })).store as ExtendedStoreDTO 13 | } 14 | 15 | regions = [(await client.admin.regions.create(getSampleRegion(store))).region] 16 | } 17 | 18 | return regions 19 | } 20 | 21 | function getSampleRegion (store: Store) { 22 | return { 23 | name: "EU", 24 | currency_code: store.currencies[0].code, 25 | tax_rate: 0, 26 | payment_providers: [ 27 | "manual" 28 | ], 29 | fulfillment_providers: [ 30 | "manual" 31 | ], 32 | countries: [ 33 | "gb", 34 | "de", 35 | "dk", 36 | "se", 37 | "fr", 38 | "es", 39 | "it" 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /src/admin/utils/prepare-shipping-options.ts: -------------------------------------------------------------------------------- 1 | import { Region } from "@medusajs/medusa"; 2 | import type Medusa from "@medusajs/medusa-js" 3 | 4 | export default async function prepareShippingOptions (client: Medusa, region: Region) { 5 | let { shipping_options } = await client.admin.shippingOptions.list({ 6 | region_id: region.id 7 | }) 8 | if (!shipping_options.length) { 9 | shipping_options = [(await client.admin.shippingOptions.create({ 10 | "name": "PostFake Standard", 11 | "region_id": region.id, 12 | "provider_id": "manual", 13 | "data": { 14 | "id": "manual-fulfillment" 15 | }, 16 | // @ts-ignore 17 | "price_type": "flat_rate", 18 | "amount": 1000 19 | })).shipping_option] 20 | } 21 | 22 | return shipping_options 23 | } -------------------------------------------------------------------------------- /src/admin/utils/sample-products.ts: -------------------------------------------------------------------------------- 1 | import { AdminPostProductsReq, Region } from "@medusajs/medusa" 2 | 3 | type SampleProductsOptions = { 4 | regions: Region[] 5 | collection_id?: string 6 | } 7 | 8 | // can't use the ProductStatus imported 9 | // from the core within admin cusotmizations 10 | enum ProductStatus { 11 | PUBLISHED = "published" 12 | } 13 | 14 | export default function getSampleProducts ({ 15 | regions, 16 | collection_id 17 | }: SampleProductsOptions): AdminPostProductsReq[] { 18 | return [ 19 | { 20 | title: "Medusa T-Shirt", 21 | status: ProductStatus.PUBLISHED, 22 | collection_id, 23 | discountable: true, 24 | subtitle: null, 25 | description: "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", 26 | handle: "medusa-t-shirt", 27 | is_giftcard: false, 28 | weight: 400, 29 | images: [ 30 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", 31 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", 32 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", 33 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png" 34 | ], 35 | options: [ 36 | { 37 | title: "Size", 38 | }, 39 | { 40 | title: "Color", 41 | } 42 | ], 43 | variants: [ 44 | { 45 | title: "S / Black", 46 | prices: regions.map((region) => { 47 | return { 48 | currency_code: region.currency_code, 49 | amount: 2200 50 | } 51 | }), 52 | options: [ 53 | { 54 | value: "S" 55 | }, 56 | { 57 | value: "Black" 58 | } 59 | ], 60 | inventory_quantity: 100, 61 | manage_inventory: true 62 | }, 63 | { 64 | title: "S / White", 65 | prices: regions.map((region) => { 66 | return { 67 | currency_code: region.currency_code, 68 | amount: 2200 69 | } 70 | }), 71 | options: [ 72 | { 73 | value: "S" 74 | }, 75 | { 76 | value: "White" 77 | } 78 | ], 79 | inventory_quantity: 100, 80 | manage_inventory: true 81 | }, 82 | { 83 | title: "M / Black", 84 | prices: regions.map((region) => { 85 | return { 86 | currency_code: region.currency_code, 87 | amount: 2200 88 | } 89 | }), 90 | options: [ 91 | { 92 | value: "M" 93 | }, 94 | { 95 | value: "Black" 96 | } 97 | ], 98 | inventory_quantity: 100, 99 | manage_inventory: true 100 | }, 101 | { 102 | title: "M / White", 103 | prices: regions.map((region) => { 104 | return { 105 | currency_code: region.currency_code, 106 | amount: 2200 107 | } 108 | }), 109 | options: [ 110 | { 111 | value: "M" 112 | }, 113 | { 114 | value: "White" 115 | } 116 | ], 117 | inventory_quantity: 100, 118 | manage_inventory: true 119 | }, 120 | { 121 | title: "L / Black", 122 | prices: regions.map((region) => { 123 | return { 124 | currency_code: region.currency_code, 125 | amount: 2200 126 | } 127 | }), 128 | options: [ 129 | { 130 | value: "L" 131 | }, 132 | { 133 | value: "Black" 134 | } 135 | ], 136 | inventory_quantity: 100, 137 | manage_inventory: true 138 | }, 139 | { 140 | title: "L / White", 141 | prices: regions.map((region) => { 142 | return { 143 | currency_code: region.currency_code, 144 | amount: 2200 145 | } 146 | }), 147 | options: [ 148 | { 149 | value: "L" 150 | }, 151 | { 152 | value: "White" 153 | } 154 | ], 155 | inventory_quantity: 100, 156 | manage_inventory: true 157 | }, 158 | { 159 | title: "XL / Black", 160 | prices: regions.map((region) => { 161 | return { 162 | currency_code: region.currency_code, 163 | amount: 2200 164 | } 165 | }), 166 | options: [ 167 | { 168 | value: "XL" 169 | }, 170 | { 171 | value: "Black" 172 | } 173 | ], 174 | inventory_quantity: 100, 175 | manage_inventory: true 176 | }, 177 | { 178 | title: "XL / White", 179 | prices: regions.map((region) => { 180 | return { 181 | currency_code: region.currency_code, 182 | amount: 2200 183 | } 184 | }), 185 | options: [ 186 | { 187 | value: "XL" 188 | }, 189 | { 190 | value: "White" 191 | } 192 | ], 193 | inventory_quantity: 100, 194 | manage_inventory: true 195 | } 196 | ] 197 | }, 198 | { 199 | title: "Medusa Sweatshirt", 200 | status: ProductStatus.PUBLISHED, 201 | discountable: true, 202 | collection_id, 203 | subtitle: null, 204 | description: "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", 205 | handle: "sweatshirt", 206 | is_giftcard: false, 207 | weight: 400, 208 | images: [ 209 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", 210 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png" 211 | ], 212 | options: [ 213 | { 214 | title: "Size", 215 | } 216 | ], 217 | variants: [ 218 | { 219 | title: "S", 220 | prices: regions.map((region) => { 221 | return { 222 | currency_code: region.currency_code, 223 | amount: 3350 224 | } 225 | }), 226 | options: [ 227 | { 228 | value: "S" 229 | } 230 | ], 231 | inventory_quantity: 100, 232 | manage_inventory: true 233 | }, 234 | { 235 | title: "M", 236 | prices: regions.map((region) => { 237 | return { 238 | currency_code: region.currency_code, 239 | amount: 3350 240 | } 241 | }), 242 | options: [ 243 | { 244 | value: "M" 245 | } 246 | ], 247 | inventory_quantity: 100, 248 | manage_inventory: true 249 | }, 250 | { 251 | title: "L", 252 | prices: regions.map((region) => { 253 | return { 254 | currency_code: region.currency_code, 255 | amount: 3350 256 | } 257 | }), 258 | options: [ 259 | { 260 | value: "L" 261 | } 262 | ], 263 | inventory_quantity: 100, 264 | manage_inventory: true 265 | }, 266 | { 267 | title: "XL", 268 | prices: regions.map((region) => { 269 | return { 270 | currency_code: region.currency_code, 271 | amount: 3350 272 | } 273 | }), 274 | options: [ 275 | { 276 | value: "XL" 277 | } 278 | ], 279 | inventory_quantity: 100, 280 | manage_inventory: true 281 | } 282 | ] 283 | }, 284 | { 285 | title: "Medusa Sweatpants", 286 | status: ProductStatus.PUBLISHED, 287 | discountable: true, 288 | collection_id, 289 | subtitle: null, 290 | description: "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", 291 | handle: "sweatpants", 292 | is_giftcard: false, 293 | weight: 400, 294 | images: [ 295 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", 296 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png" 297 | ], 298 | options: [ 299 | { 300 | title: "Size", 301 | } 302 | ], 303 | variants: [ 304 | { 305 | title: "S", 306 | prices: regions.map((region) => { 307 | return { 308 | currency_code: region.currency_code, 309 | amount: 3350 310 | } 311 | }), 312 | options: [ 313 | { 314 | value: "S" 315 | } 316 | ], 317 | inventory_quantity: 100, 318 | manage_inventory: true 319 | }, 320 | { 321 | title: "M", 322 | prices: regions.map((region) => { 323 | return { 324 | currency_code: region.currency_code, 325 | amount: 3350 326 | } 327 | }), 328 | options: [ 329 | { 330 | value: "M" 331 | } 332 | ], 333 | inventory_quantity: 100, 334 | manage_inventory: true 335 | }, 336 | { 337 | title: "L", 338 | prices: regions.map((region) => { 339 | return { 340 | currency_code: region.currency_code, 341 | amount: 3350 342 | } 343 | }), 344 | options: [ 345 | { 346 | value: "L" 347 | } 348 | ], 349 | inventory_quantity: 100, 350 | manage_inventory: true 351 | }, 352 | { 353 | title: "XL", 354 | prices: regions.map((region) => { 355 | return { 356 | currency_code: region.currency_code, 357 | amount: 3350 358 | } 359 | }), 360 | options: [ 361 | { 362 | value: "XL" 363 | } 364 | ], 365 | inventory_quantity: 100, 366 | manage_inventory: true 367 | } 368 | ] 369 | }, 370 | { 371 | title: "Medusa Shorts", 372 | status: ProductStatus.PUBLISHED, 373 | discountable: true, 374 | collection_id, 375 | subtitle: null, 376 | description: "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", 377 | handle: "shorts", 378 | is_giftcard: false, 379 | weight: 400, 380 | images: [ 381 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", 382 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png" 383 | ], 384 | options: [ 385 | { 386 | title: "Size", 387 | } 388 | ], 389 | variants: [ 390 | { 391 | title: "S", 392 | prices: regions.map((region) => { 393 | return { 394 | currency_code: region.currency_code, 395 | amount: 2850 396 | } 397 | }), 398 | options: [ 399 | { 400 | value: "S" 401 | } 402 | ], 403 | inventory_quantity: 100, 404 | manage_inventory: true 405 | }, 406 | { 407 | title: "M", 408 | prices: regions.map((region) => { 409 | return { 410 | currency_code: region.currency_code, 411 | amount: 2850 412 | } 413 | }), 414 | options: [ 415 | { 416 | value: "M" 417 | } 418 | ], 419 | inventory_quantity: 100, 420 | manage_inventory: true 421 | }, 422 | { 423 | title: "L", 424 | prices: regions.map((region) => { 425 | return { 426 | currency_code: region.currency_code, 427 | amount: 2850 428 | } 429 | }), 430 | options: [ 431 | { 432 | value: "L" 433 | } 434 | ], 435 | inventory_quantity: 100, 436 | manage_inventory: true 437 | }, 438 | { 439 | title: "XL", 440 | prices: regions.map((region) => { 441 | return { 442 | currency_code: region.currency_code, 443 | amount: 2850 444 | } 445 | }), 446 | options: [ 447 | { 448 | value: "XL" 449 | } 450 | ], 451 | inventory_quantity: 100, 452 | manage_inventory: true 453 | } 454 | ] 455 | }, 456 | { 457 | title: "Medusa Hoodie", 458 | status: ProductStatus.PUBLISHED, 459 | discountable: true, 460 | collection_id, 461 | subtitle: null, 462 | description: "Reimagine the feeling of a classic hoodie. With our cotton hoodie, everyday essentials no longer have to be ordinary.", 463 | handle: "hoodie", 464 | is_giftcard: false, 465 | weight: 400, 466 | images: [ 467 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_front.png", 468 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_back.png" 469 | ], 470 | options: [ 471 | { 472 | title: "Size", 473 | } 474 | ], 475 | variants: [ 476 | { 477 | title: "S", 478 | prices: regions.map((region) => { 479 | return { 480 | currency_code: region.currency_code, 481 | amount: 4150 482 | } 483 | }), 484 | options: [ 485 | { 486 | value: "S" 487 | } 488 | ], 489 | inventory_quantity: 100, 490 | manage_inventory: true 491 | }, 492 | { 493 | title: "M", 494 | prices: regions.map((region) => { 495 | return { 496 | currency_code: region.currency_code, 497 | amount: 4150 498 | } 499 | }), 500 | options: [ 501 | { 502 | value: "M" 503 | } 504 | ], 505 | inventory_quantity: 100, 506 | manage_inventory: true 507 | }, 508 | { 509 | title: "L", 510 | prices: regions.map((region) => { 511 | return { 512 | currency_code: region.currency_code, 513 | amount: 4150 514 | } 515 | }), 516 | options: [ 517 | { 518 | value: "L" 519 | } 520 | ], 521 | inventory_quantity: 100, 522 | manage_inventory: true 523 | }, 524 | { 525 | title: "XL", 526 | prices: regions.map((region) => { 527 | return { 528 | currency_code: region.currency_code, 529 | amount: 4150 530 | } 531 | }), 532 | options: [ 533 | { 534 | value: "XL" 535 | } 536 | ], 537 | inventory_quantity: 100, 538 | manage_inventory: true 539 | } 540 | ] 541 | }, 542 | { 543 | title: "Medusa Longsleeve", 544 | status: ProductStatus.PUBLISHED, 545 | discountable: true, 546 | collection_id, 547 | subtitle: null, 548 | description: "Reimagine the feeling of a classic longsleeve. With our cotton longsleeve, everyday essentials no longer have to be ordinary.", 549 | handle: "longsleeve", 550 | is_giftcard: false, 551 | weight: 400, 552 | images: [ 553 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-front.png", 554 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-back.png" 555 | ], 556 | options: [ 557 | { 558 | title: "Size", 559 | } 560 | ], 561 | variants: [ 562 | { 563 | title: "S", 564 | prices: regions.map((region) => { 565 | return { 566 | currency_code: region.currency_code, 567 | amount: 4150 568 | } 569 | }), 570 | options: [ 571 | { 572 | value: "S" 573 | } 574 | ], 575 | inventory_quantity: 100, 576 | manage_inventory: true 577 | }, 578 | { 579 | title: "M", 580 | prices: regions.map((region) => { 581 | return { 582 | currency_code: region.currency_code, 583 | amount: 4150 584 | } 585 | }), 586 | options: [ 587 | { 588 | value: "M" 589 | } 590 | ], 591 | inventory_quantity: 100, 592 | manage_inventory: true 593 | }, 594 | { 595 | title: "L", 596 | prices: regions.map((region) => { 597 | return { 598 | currency_code: region.currency_code, 599 | amount: 4150 600 | } 601 | }), 602 | options: [ 603 | { 604 | value: "L" 605 | } 606 | ], 607 | inventory_quantity: 100, 608 | manage_inventory: true 609 | }, 610 | { 611 | title: "XL", 612 | prices: regions.map((region) => { 613 | return { 614 | currency_code: region.currency_code, 615 | amount: 4150 616 | } 617 | }), 618 | options: [ 619 | { 620 | value: "XL" 621 | } 622 | ], 623 | inventory_quantity: 100, 624 | manage_inventory: true 625 | } 626 | ] 627 | }, 628 | { 629 | title: "Medusa Coffee Mug", 630 | status: ProductStatus.PUBLISHED, 631 | discountable: true, 632 | collection_id, 633 | subtitle: null, 634 | description: "Every programmer's best friend.", 635 | handle: "coffee-mug", 636 | is_giftcard: false, 637 | weight: 400, 638 | images: [ 639 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png" 640 | ], 641 | options: [ 642 | { 643 | title: "Size", 644 | } 645 | ], 646 | variants: [ 647 | { 648 | title: "One Size", 649 | prices: regions.map((region) => { 650 | return { 651 | currency_code: region.currency_code, 652 | amount: 1200 653 | } 654 | }), 655 | options: [ 656 | { 657 | value: "One Size" 658 | } 659 | ], 660 | inventory_quantity: 100, 661 | manage_inventory: true 662 | } 663 | ] 664 | } 665 | ] 666 | } -------------------------------------------------------------------------------- /src/admin/widgets/onboarding-flow/onboarding-flow.tsx: -------------------------------------------------------------------------------- 1 | import { OrderDetailsWidgetProps, ProductDetailsWidgetProps, WidgetConfig, WidgetProps } from "@medusajs/admin"; 2 | import { useAdminCustomPost, useAdminCustomQuery, useMedusa } from "medusa-react"; 3 | import React, { useEffect, useState, useMemo, useCallback } from "react"; 4 | import { useNavigate, useSearchParams, useLocation } from "react-router-dom"; 5 | import { OnboardingState } from "../../../models/onboarding"; 6 | import { 7 | AdminOnboardingUpdateStateReq, 8 | OnboardingStateRes, 9 | UpdateOnboardingStateInput, 10 | } from "../../../types/onboarding"; 11 | import OrderDetailDefault from "../../components/onboarding-flow/default/orders/order-detail"; 12 | import OrdersListDefault from "../../components/onboarding-flow/default/orders/orders-list"; 13 | import ProductDetailDefault from "../../components/onboarding-flow/default/products/product-detail"; 14 | import ProductsListDefault from "../../components/onboarding-flow/default/products/products-list"; 15 | import { Button, Container, Heading, Text, clx } from "@medusajs/ui"; 16 | import Accordion from "../../components/shared/accordion"; 17 | import GetStarted from "../../components/shared/icons/get-started"; 18 | import { Order, Product } from "@medusajs/medusa"; 19 | import ProductsListNextjs from "../../components/onboarding-flow/nextjs/products/products-list"; 20 | import ProductDetailNextjs from "../../components/onboarding-flow/nextjs/products/product-detail"; 21 | import OrdersListNextjs from "../../components/onboarding-flow/nextjs/orders/orders-list"; 22 | import OrderDetailNextjs from "../../components/onboarding-flow/nextjs/orders/order-detail"; 23 | 24 | type STEP_ID = 25 | | "create_product" 26 | | "preview_product" 27 | | "create_order" 28 | | "setup_finished" 29 | | "create_product_nextjs" 30 | | "preview_product_nextjs" 31 | | "create_order_nextjs" 32 | | "setup_finished_nextjs" 33 | 34 | type OnboardingWidgetProps = WidgetProps | ProductDetailsWidgetProps | OrderDetailsWidgetProps 35 | 36 | export type StepContentProps = OnboardingWidgetProps & { 37 | onNext?: Function; 38 | isComplete?: boolean; 39 | data?: OnboardingState; 40 | }; 41 | 42 | type Step = { 43 | id: STEP_ID; 44 | title: string; 45 | component: React.FC; 46 | onNext?: Function; 47 | }; 48 | 49 | const QUERY_KEY = ["onboarding_state"]; 50 | 51 | const OnboardingFlow = (props: OnboardingWidgetProps) => { 52 | // create custom hooks for custom endpoints 53 | const { data, isLoading } = useAdminCustomQuery< 54 | undefined, 55 | OnboardingStateRes 56 | >("/onboarding", QUERY_KEY); 57 | const { mutate } = useAdminCustomPost< 58 | AdminOnboardingUpdateStateReq, 59 | OnboardingStateRes 60 | >("/onboarding", QUERY_KEY); 61 | 62 | const navigate = useNavigate(); 63 | const location = useLocation(); 64 | // will be used if onboarding step 65 | // is passed as a path parameter 66 | const { client } = useMedusa(); 67 | 68 | // get current step from custom endpoint 69 | const currentStep: STEP_ID | undefined = useMemo(() => { 70 | return data?.status 71 | ?.current_step as STEP_ID 72 | }, [data]); 73 | 74 | // initialize some state 75 | const [openStep, setOpenStep] = useState(currentStep); 76 | const [completed, setCompleted] = useState(false); 77 | 78 | // this method is used to move from one step to the next 79 | const setStepComplete = ({ 80 | step_id, 81 | extraData, 82 | onComplete, 83 | }: { 84 | step_id: STEP_ID; 85 | extraData?: UpdateOnboardingStateInput; 86 | onComplete?: () => void; 87 | }) => { 88 | const next = steps[findStepIndex(step_id) + 1]; 89 | mutate({ current_step: next.id, ...extraData }, { 90 | onSuccess: onComplete 91 | }); 92 | }; 93 | 94 | // this is useful if you want to change the current step 95 | // using a path parameter. It can only be changed if the passed 96 | // step in the path parameter is the next step. 97 | const [ searchParams ] = useSearchParams() 98 | 99 | // the steps are set based on the 100 | // onboarding type 101 | const steps: Step[] = useMemo(() => { 102 | { 103 | switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) { 104 | case 'nextjs': 105 | return [ 106 | { 107 | id: "create_product_nextjs", 108 | title: "Create Products", 109 | component: ProductsListNextjs, 110 | onNext: (product: Product) => { 111 | setStepComplete({ 112 | step_id: "create_product_nextjs", 113 | extraData: { product_id: product.id }, 114 | onComplete: () => { 115 | if (!location.pathname.startsWith(`/a/products/${product.id}`)) { 116 | navigate(`/a/products/${product.id}`) 117 | } 118 | }, 119 | }); 120 | }, 121 | }, 122 | { 123 | id: "preview_product_nextjs", 124 | title: "Preview Product in your Next.js Storefront", 125 | component: ProductDetailNextjs, 126 | onNext: () => { 127 | setStepComplete({ 128 | step_id: "preview_product_nextjs", 129 | onComplete: () => navigate(`/a/orders`), 130 | }); 131 | }, 132 | }, 133 | { 134 | id: "create_order_nextjs", 135 | title: "Create an Order using your Next.js Storefront", 136 | component: OrdersListNextjs, 137 | onNext: (order: Order) => { 138 | setStepComplete({ 139 | step_id: "create_order_nextjs", 140 | onComplete: () => { 141 | if (!location.pathname.startsWith(`/a/orders/${order.id}`)) { 142 | navigate(`/a/orders/${order.id}`) 143 | } 144 | }, 145 | }); 146 | }, 147 | }, 148 | { 149 | id: "setup_finished_nextjs", 150 | title: "Setup Finished: Continue Building your Ecommerce Store", 151 | component: OrderDetailNextjs, 152 | }, 153 | ] 154 | default: 155 | return [ 156 | { 157 | id: "create_product", 158 | title: "Create Product", 159 | component: ProductsListDefault, 160 | onNext: (product: Product) => { 161 | setStepComplete({ 162 | step_id: "create_product", 163 | extraData: { product_id: product.id }, 164 | onComplete: () => { 165 | if (!location.pathname.startsWith(`/a/products/${product.id}`)) { 166 | navigate(`/a/products/${product.id}`) 167 | } 168 | }, 169 | }); 170 | }, 171 | }, 172 | { 173 | id: "preview_product", 174 | title: "Preview Product", 175 | component: ProductDetailDefault, 176 | onNext: () => { 177 | setStepComplete({ 178 | step_id: "preview_product", 179 | onComplete: () => navigate(`/a/orders`), 180 | }); 181 | }, 182 | }, 183 | { 184 | id: "create_order", 185 | title: "Create an Order", 186 | component: OrdersListDefault, 187 | onNext: (order: Order) => { 188 | setStepComplete({ 189 | step_id: "create_order", 190 | onComplete: () => { 191 | if (!location.pathname.startsWith(`/a/orders/${order.id}`)) { 192 | navigate(`/a/orders/${order.id}`) 193 | } 194 | }, 195 | }); 196 | }, 197 | }, 198 | { 199 | id: "setup_finished", 200 | title: "Setup Finished: Start developing with Medusa", 201 | component: OrderDetailDefault, 202 | }, 203 | ] 204 | } 205 | } 206 | }, [location.pathname]) 207 | 208 | // used to retrieve the index of a step by its ID 209 | const findStepIndex = useCallback((step_id: STEP_ID) => { 210 | return steps.findIndex((step) => step.id === step_id) 211 | }, [steps]) 212 | 213 | // used to check if a step is completed 214 | const isStepComplete = useCallback((step_id: STEP_ID) => { 215 | return findStepIndex(currentStep) > findStepIndex(step_id) 216 | }, [findStepIndex, currentStep]); 217 | 218 | // this is used to retrieve the data necessary 219 | // to move to the next onboarding step 220 | const getOnboardingParamStepData = useCallback(async (onboardingStep: string, data?: { 221 | orderId?: string, 222 | productId?: string, 223 | }) => { 224 | switch (onboardingStep) { 225 | case "setup_finished_nextjs": 226 | case "setup_finished": 227 | if (!data?.orderId && "order" in props) { 228 | return props.order 229 | } 230 | const orderId = data?.orderId || searchParams.get("order_id") 231 | if (orderId) { 232 | return (await client.admin.orders.retrieve(orderId)).order 233 | } 234 | 235 | throw new Error ("Required `order_id` parameter was not passed as a parameter") 236 | case "preview_product_nextjs": 237 | case "preview_product": 238 | if (!data?.productId && "product" in props) { 239 | return props.product 240 | } 241 | const productId = data?.productId || searchParams.get("product_id") 242 | if (productId) { 243 | return (await client.admin.products.retrieve(productId)).product 244 | } 245 | 246 | throw new Error ("Required `product_id` parameter was not passed as a parameter") 247 | default: 248 | return undefined 249 | } 250 | }, [searchParams, props]) 251 | 252 | const isProductCreateStep = useMemo(() => { 253 | return currentStep === "create_product" || 254 | currentStep === "create_product_nextjs" 255 | }, [currentStep]) 256 | 257 | const isOrderCreateStep = useMemo(() => { 258 | return currentStep === "create_order" || 259 | currentStep === "create_order_nextjs" 260 | }, [currentStep]) 261 | 262 | // used to change the open step when the current 263 | // step is retrieved from custom endpoints 264 | useEffect(() => { 265 | setOpenStep(currentStep); 266 | 267 | if (findStepIndex(currentStep) === steps.length - 1) setCompleted(true); 268 | }, [currentStep, findStepIndex]); 269 | 270 | // used to check if the user created a product and has entered its details page 271 | // the step is changed to the next one 272 | useEffect(() => { 273 | if (location.pathname.startsWith("/a/products/prod_") && isProductCreateStep && "product" in props) { 274 | // change to the preview product step 275 | const currentStepIndex = findStepIndex(currentStep) 276 | steps[currentStepIndex].onNext?.(props.product) 277 | } 278 | }, [location.pathname, isProductCreateStep]) 279 | 280 | // used to check if the user created an order and has entered its details page 281 | // the step is changed to the next one. 282 | useEffect(() => { 283 | if (location.pathname.startsWith("/a/orders/order_") && isOrderCreateStep && "order" in props) { 284 | // change to the preview product step 285 | const currentStepIndex = findStepIndex(currentStep) 286 | steps[currentStepIndex].onNext?.(props.order) 287 | } 288 | }, [location.pathname, isOrderCreateStep]) 289 | 290 | // used to check if the `onboarding_step` path 291 | // parameter is passed and, if so, moves to that step 292 | // only if it's the next step and its necessary data is passed 293 | useEffect(() => { 294 | const onboardingStep = searchParams.get("onboarding_step") as STEP_ID 295 | const onboardingStepIndex = findStepIndex(onboardingStep) 296 | if (onboardingStep && onboardingStepIndex !== -1 && onboardingStep !== openStep) { 297 | // change current step to the onboarding step 298 | const openStepIndex = findStepIndex(openStep) 299 | 300 | if (onboardingStepIndex !== openStepIndex + 1) { 301 | // can only go forward one step 302 | return 303 | } 304 | 305 | // retrieve necessary data and trigger the next function 306 | getOnboardingParamStepData(onboardingStep) 307 | .then((data) => { 308 | steps[openStepIndex].onNext?.(data) 309 | }) 310 | .catch((e) => console.error(e)) 311 | } 312 | }, [searchParams, openStep, getOnboardingParamStepData]) 313 | 314 | if ( 315 | !isLoading && 316 | data?.status?.is_complete && 317 | !localStorage.getItem("override_onboarding_finish") 318 | ) 319 | return null; 320 | 321 | // a method that will be triggered when 322 | // the setup is started 323 | const onStart = () => { 324 | mutate({ current_step: steps[0].id }); 325 | navigate(`/a/products`); 326 | }; 327 | 328 | // a method that will be triggered when 329 | // the setup is completed 330 | const onComplete = () => { 331 | setCompleted(true); 332 | }; 333 | 334 | // a method that will be triggered when 335 | // the setup is closed 336 | const onHide = () => { 337 | mutate({ is_complete: true }); 338 | }; 339 | 340 | // used to get text for get started header 341 | const getStartedText = () => { 342 | switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) { 343 | case "nextjs": 344 | return "Learn the basics of Medusa by creating your first order using the Next.js storefront." 345 | default: 346 | return "Learn the basics of Medusa by creating your first order." 347 | } 348 | } 349 | 350 | return ( 351 | <> 352 | 358 | setOpenStep(value as STEP_ID)} 362 | > 363 |
370 |
371 | 372 |
373 | {!completed ? ( 374 | <> 375 |
376 | Get started 377 | 378 | {getStartedText()} 379 | 380 |
381 |
382 | {!!currentStep ? ( 383 | <> 384 | {currentStep === steps[steps.length - 1].id ? ( 385 | 392 | ) : ( 393 | 400 | )} 401 | 402 | ) : ( 403 | <> 404 | 411 | 418 | 419 | )} 420 |
421 | 422 | ) : ( 423 | <> 424 |
425 | 426 | Thank you for completing the setup guide! 427 | 428 | 429 | This whole experience was built using our new{" "} 430 | widgets feature. 431 |
You can find out more details and build your own by 432 | following{" "} 433 | 438 | our guide 439 | 440 | . 441 |
442 |
443 |
444 | 451 |
452 | 453 | )} 454 |
455 | { 456 |
457 | {(!completed ? steps : steps.slice(-1)).map((step) => { 458 | const isComplete = isStepComplete(step.id); 459 | const isCurrent = currentStep === step.id; 460 | return ( 461 | , 472 | })} 473 | > 474 |
475 | 481 |
482 |
483 | ); 484 | })} 485 |
486 | } 487 |
488 |
489 | 490 | ); 491 | }; 492 | 493 | export const config: WidgetConfig = { 494 | zone: [ 495 | "product.list.before", 496 | "product.details.before", 497 | "order.list.before", 498 | "order.details.before", 499 | ], 500 | }; 501 | 502 | export default OnboardingFlow; 503 | -------------------------------------------------------------------------------- /src/api/README.md: -------------------------------------------------------------------------------- 1 | # Custom API Routes 2 | 3 | You may define custom API Routes by putting files in the `/api` directory that export functions returning an express router or a collection of express routers. 4 | Medusa supports adding custom API Routes using a file based approach. This means that you can add files in the `/api` directory and the files path will be used as the API Route path. For example, if you add a file called `/api/store/custom/route.ts` it will be available on the `/store/custom` API Route. 5 | 6 | ```ts 7 | import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; 8 | 9 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 10 | res.json({ 11 | message: "Hello world!", 12 | }); 13 | } 14 | ``` 15 | 16 | ## Supported HTTP methods 17 | 18 | The file based routing supports the following HTTP methods: 19 | 20 | - GET 21 | - POST 22 | - PUT 23 | - PATCH 24 | - DELETE 25 | - OPTIONS 26 | - HEAD 27 | 28 | You can define a handler for each of these methods by exporting a function with the name of the method in the paths `route.ts` file. For example, if you want to define a handler for the `GET`, `POST`, and `PUT` methods, you can do so by exporting functions with the names `GET`, `POST`, and `PUT`: 29 | 30 | ```ts 31 | import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; 32 | 33 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 34 | // Handle GET requests 35 | } 36 | 37 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 38 | // Handle POST requests 39 | } 40 | 41 | export async function PUT(req: MedusaRequest, res: MedusaResponse) { 42 | // Handle PUT requests 43 | } 44 | ``` 45 | 46 | ## Parameters 47 | 48 | You can define parameters in the path of your route by using wrapping the parameter name in square brackets. For example, if you want to define a route that takes a `productId` parameter, you can do so by creating a file called `/api/products/[productId]/route.ts`: 49 | 50 | ```ts 51 | import type { 52 | MedusaRequest, 53 | MedusaResponse, 54 | ProductService, 55 | } from "@medusajs/medusa"; 56 | 57 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 58 | const { productId } = req.params; 59 | 60 | const productService: ProductService = req.scope.resolve("productService"); 61 | 62 | const product = await productService.retrieve(productId); 63 | 64 | res.json({ 65 | product, 66 | }); 67 | } 68 | ``` 69 | 70 | If you want to define a route that takes multiple parameters, you can do so by adding multiple parameters in the path. It is important that each parameter is given a unique name. For example, if you want to define a route that takes both a `productId` and a `variantId` parameter, you can do so by creating a file called `/api/products/[productId]/variants/[variantId]/route.ts`. Duplicate parameter names are not allowed, and will result in an error. 71 | 72 | ## Using the container 73 | 74 | A global container is available on `req.scope` to allow you to use any of the registered services from the core, installed plugins or your local project: 75 | 76 | ```ts 77 | import type { 78 | MedusaRequest, 79 | MedusaResponse, 80 | ProductService, 81 | } from "@medusajs/medusa"; 82 | 83 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 84 | const productService: ProductService = req.scope.resolve("productService"); 85 | 86 | const products = await productService.list(); 87 | 88 | res.json({ 89 | products, 90 | }); 91 | } 92 | ``` 93 | 94 | ## Middleware 95 | 96 | You can apply middleware to your routes by creating a file called `/api/middlewares.ts`. This file should export a configuration object with what middleware you want to apply to which routes. For example, if you want to apply a custom middleware function to the `/store/custom` route, you can do so by adding the following to your `/api/middlewares.ts` file: 97 | 98 | ```ts 99 | import type { 100 | MiddlewaresConfig, 101 | MedusaRequest, 102 | MedusaResponse, 103 | MedusaNextFunction, 104 | } from "@medusajs/medusa"; 105 | 106 | async function logger( 107 | req: MedusaRequest, 108 | res: MedusaResponse, 109 | next: MedusaNextFunction 110 | ) { 111 | console.log("Request received"); 112 | next(); 113 | } 114 | 115 | export const config: MiddlewaresConfig = { 116 | routes: [ 117 | { 118 | matcher: "/store/custom", 119 | middlewares: [logger], 120 | }, 121 | ], 122 | }; 123 | ``` 124 | 125 | The `matcher` property can be either a string or a regular expression. The `middlewares` property accepts an array of middleware functions. 126 | 127 | You might only want to apply middleware to certain HTTP methods. You can do so by adding a `method` property to the route configuration object: 128 | 129 | ```ts 130 | export const config: MiddlewaresConfig = { 131 | routes: [ 132 | { 133 | matcher: "/store/custom", 134 | method: "GET", 135 | middlewares: [logger], 136 | }, 137 | ], 138 | }; 139 | ``` 140 | 141 | The `method` property can be either a HTTP method or an array of HTTP methods. By default the middlewares will apply to all HTTP methods for the given `matcher`. 142 | 143 | ### Default middleware 144 | 145 | Some middleware functions are applied per default: 146 | 147 | #### Global middleware 148 | 149 | JSON parsing is applied to all routes. This means that you can access the request body as `req.body` and it will be parsed as JSON, if the request has a `Content-Type` header of `application/json`. 150 | 151 | If you want to use a different parser for a specific route, such as `urlencoded`, you can do so by adding the following export to your `route.ts` file: 152 | 153 | ```ts 154 | import { urlencoded } from "express"; 155 | 156 | export const config: MiddlewaresConfig = { 157 | routes: [ 158 | { 159 | method: "POST", 160 | matcher: "/store/custom", 161 | middlewares: [urlencoded()], 162 | }, 163 | ], 164 | }; 165 | ``` 166 | 167 | #### Store middleware 168 | 169 | For all `/store` routes, the appropriate CORS settings are applied. The STORE_CORS value can be configured in your `medusa-config.js` file. 170 | 171 | #### Admin middleware 172 | 173 | For all `/admin` routes, the appropriate CORS settings are applied. The ADMIN_CORS value can be configured in your `medusa-config.js` file. 174 | 175 | All `/admin` routes also have admin authentication applied per default. If you want to disable this for a specific route, you can do so by adding the following export to your `route.ts` file: 176 | 177 | ```ts 178 | export const AUTHENTICATE = false; 179 | ``` 180 | -------------------------------------------------------------------------------- /src/api/admin/custom/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; 2 | 3 | export async function GET( 4 | req: MedusaRequest, 5 | res: MedusaResponse 6 | ): Promise { 7 | res.sendStatus(200); 8 | } 9 | -------------------------------------------------------------------------------- /src/api/admin/onboarding/route.ts: -------------------------------------------------------------------------------- 1 | import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; 2 | import { EntityManager } from "typeorm"; 3 | 4 | import OnboardingService from "../../../services/onboarding"; 5 | 6 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 7 | const onboardingService: OnboardingService = 8 | req.scope.resolve("onboardingService"); 9 | 10 | const status = await onboardingService.retrieve(); 11 | 12 | res.status(200).json({ status }); 13 | } 14 | 15 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 16 | const onboardingService: OnboardingService = 17 | req.scope.resolve("onboardingService"); 18 | const manager: EntityManager = req.scope.resolve("manager"); 19 | 20 | const status = await manager.transaction(async (transactionManager) => { 21 | return await onboardingService 22 | .withTransaction(transactionManager) 23 | .update(req.body); 24 | }); 25 | 26 | res.status(200).json({ status }); 27 | } 28 | -------------------------------------------------------------------------------- /src/api/store/custom/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; 2 | 3 | export async function GET( 4 | req: MedusaRequest, 5 | res: MedusaResponse 6 | ): Promise { 7 | res.sendStatus(200); 8 | } 9 | -------------------------------------------------------------------------------- /src/jobs/README.md: -------------------------------------------------------------------------------- 1 | # Custom scheduled jobs 2 | 3 | You may define custom scheduled jobs (cron jobs) by creating files in the `/jobs` directory. 4 | 5 | ```ts 6 | import { 7 | ProductService, 8 | ScheduledJobArgs, 9 | ScheduledJobConfig, 10 | } from "@medusajs/medusa"; 11 | 12 | export default async function myCustomJob({ container }: ScheduledJobArgs) { 13 | const productService: ProductService = container.resolve("productService"); 14 | 15 | const products = await productService.listAndCount(); 16 | 17 | // Do something with the products 18 | } 19 | 20 | export const config: ScheduledJobConfig = { 21 | name: "daily-product-report", 22 | schedule: "0 0 * * *", // Every day at midnight 23 | }; 24 | ``` 25 | 26 | A scheduled job is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when the job is scheduled. The `config` is an object which defines the name of the job, the schedule, and an optional data object. 27 | 28 | The `handler` is a function which takes one parameter, an `object` of type `ScheduledJobArgs` with the following properties: 29 | 30 | - `container` - a `MedusaContainer` instance which can be used to resolve services. 31 | - `data` - an `object` containing data passed to the job when it was scheduled. This object is passed in the `config` object. 32 | - `pluginOptions` - an `object` containing plugin options, if the job is defined in a plugin. 33 | -------------------------------------------------------------------------------- /src/loaders/README.md: -------------------------------------------------------------------------------- 1 | # Custom loader 2 | 3 | The loader allows you have access to the Medusa service container. This allows you to access the database and the services registered on the container. 4 | you can register custom registrations in the container or run custom code on startup. 5 | 6 | ```ts 7 | // src/loaders/my-loader.ts 8 | 9 | import { AwilixContainer } from 'awilix' 10 | 11 | /** 12 | * 13 | * @param container The container in which the registrations are made 14 | * @param config The options of the plugin or the entire config object 15 | */ 16 | export default (container: AwilixContainer, config: Record): void | Promise => { 17 | /* Implement your own loader. */ 18 | } 19 | ``` -------------------------------------------------------------------------------- /src/migrations/1685715079776-CreateOnboarding.ts: -------------------------------------------------------------------------------- 1 | import { generateEntityId } from "@medusajs/utils"; 2 | import { MigrationInterface, QueryRunner } from "typeorm"; 3 | 4 | export class CreateOnboarding1685715079776 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query( 7 | `CREATE TABLE "onboarding_state" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "current_step" character varying NULL, "is_complete" boolean)` 8 | ); 9 | 10 | await queryRunner.query( 11 | `INSERT INTO "onboarding_state" ("id", "current_step", "is_complete") VALUES ('${generateEntityId( 12 | "", 13 | "onboarding" 14 | )}' , NULL, false)` 15 | ); 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | await queryRunner.query(`DROP TABLE "onboarding_state"`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/migrations/1686062614694-AddOnboardingProduct.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class AddOnboardingProduct1686062614694 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "onboarding_state" ADD COLUMN "product_id" character varying NULL` 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query( 12 | `ALTER TABLE "onboarding_state" DROP COLUMN "product_id"` 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migrations/1690996567455-CorrectOnboardingFields.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class CorrectOnboardingFields1690996567455 implements MigrationInterface { 4 | name = 'CorrectOnboardingFields1690996567455' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "onboarding_state" ADD CONSTRAINT "PK_891b72628471aada55d7b8c9410" PRIMARY KEY ("id")`); 8 | await queryRunner.query(`ALTER TABLE "onboarding_state" ALTER COLUMN "is_complete" SET NOT NULL`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "onboarding_state" ALTER COLUMN "is_complete" DROP NOT NULL`); 13 | await queryRunner.query(`ALTER TABLE "onboarding_state" DROP CONSTRAINT "PK_891b72628471aada55d7b8c9410"`); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/migrations/README.md: -------------------------------------------------------------------------------- 1 | # Custom migrations 2 | 3 | You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`. 4 | In that case you also need to provide a migration in order to create the table in the database. 5 | 6 | ## Example 7 | 8 | ### 1. Create the migration 9 | 10 | See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation. 11 | 12 | ```ts 13 | // src/migration/my-migration.ts 14 | 15 | import { MigrationInterface, QueryRunner } from "typeorm" 16 | 17 | export class MyMigration1617703530229 implements MigrationInterface { 18 | name = "myMigration1617703530229" 19 | 20 | public async up(queryRunner: QueryRunner): Promise { 21 | // write you migration here 22 | } 23 | 24 | public async down(queryRunner: QueryRunner): Promise { 25 | // write you migration here 26 | } 27 | } 28 | 29 | ``` -------------------------------------------------------------------------------- /src/models/README.md: -------------------------------------------------------------------------------- 1 | # Custom models 2 | 3 | You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`. 4 | 5 | ## Example 6 | 7 | ### 1. Create the Entity 8 | 9 | ```ts 10 | // src/models/post.ts 11 | 12 | import { BeforeInsert, Column, Entity, PrimaryColumn } from "typeorm"; 13 | import { generateEntityId } from "@medusajs/utils"; 14 | import { BaseEntity } from "@medusajs/medusa"; 15 | 16 | @Entity() 17 | export class Post extends BaseEntity { 18 | @Column({type: 'varchar'}) 19 | title: string | null; 20 | 21 | @BeforeInsert() 22 | private beforeInsert(): void { 23 | this.id = generateEntityId(this.id, "post") 24 | } 25 | } 26 | ``` 27 | 28 | ### 2. Create the Migration 29 | 30 | You also need to create a Migration to create the new table in the database. See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation. 31 | 32 | ### 3. Create a Repository 33 | Entities data can be easily accessed and modified using [TypeORM Repositories](https://typeorm.io/working-with-repository). To create a repository, create a file in `src/repositories`. For example, here’s a repository `PostRepository` for the `Post` entity: 34 | 35 | ```ts 36 | // src/repositories/post.ts 37 | 38 | import { EntityRepository, Repository } from "typeorm" 39 | 40 | import { Post } from "../models/post" 41 | 42 | @EntityRepository(Post) 43 | export class PostRepository extends Repository { } 44 | ``` 45 | 46 | See more about defining and accesing your custom [Entities](https://docs.medusajs.com/advanced/backend/entities/overview) in the documentation. -------------------------------------------------------------------------------- /src/models/onboarding.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from "@medusajs/medusa"; 2 | import { Column, Entity } from "typeorm"; 3 | 4 | @Entity() 5 | export class OnboardingState extends BaseEntity { 6 | @Column({ nullable: true }) 7 | current_step: string; 8 | 9 | @Column() 10 | is_complete: boolean; 11 | 12 | @Column({ nullable: true }) 13 | product_id: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/repositories/onboarding.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from "@medusajs/medusa/dist/loaders/database"; 2 | import { OnboardingState } from "../models/onboarding"; 3 | 4 | const OnboardingRepository = dataSource.getRepository(OnboardingState); 5 | 6 | export default OnboardingRepository; 7 | -------------------------------------------------------------------------------- /src/services/README.md: -------------------------------------------------------------------------------- 1 | # Custom services 2 | 3 | You may define custom services that will be registered on the global container by creating files in the `/services` directory that export an instance of `BaseService`. 4 | 5 | ```ts 6 | // src/services/my-custom.ts 7 | 8 | import { Lifetime } from "awilix" 9 | import { TransactionBaseService } from "@medusajs/medusa"; 10 | import { IEventBusService } from "@medusajs/types"; 11 | 12 | export default class MyCustomService extends TransactionBaseService { 13 | static LIFE_TIME = Lifetime.SCOPED 14 | protected readonly eventBusService_: IEventBusService 15 | 16 | constructor( 17 | { eventBusService }: { eventBusService: IEventBusService }, 18 | options: Record 19 | ) { 20 | // @ts-ignore 21 | super(...arguments) 22 | 23 | this.eventBusService_ = eventBusService 24 | } 25 | } 26 | 27 | ``` 28 | 29 | The first argument to the `constructor` is the global giving you access to easy dependency injection. The container holds all registered services from the core, installed plugins and from other files in the `/services` directory. The registration name is a camelCased version of the file name with the type appended i.e.: `my-custom.js` is registered as `myCustomService`, `custom-thing.js` is registered as `customThingService`. 30 | 31 | You may use the services you define here in custom endpoints by resolving the services defined. 32 | 33 | ```js 34 | import { Router } from "express" 35 | 36 | export default () => { 37 | const router = Router() 38 | 39 | router.get("/hello-product", async (req, res) => { 40 | const myService = req.scope.resolve("myCustomService") 41 | 42 | res.json({ 43 | message: await myService.getProductMessage() 44 | }) 45 | }) 46 | 47 | return router; 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /src/services/__tests__/test-service.spec.ts: -------------------------------------------------------------------------------- 1 | describe('MyService', () => { 2 | it('should do this', async () => { 3 | expect(true).toBe(true) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /src/services/onboarding.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBaseService } from "@medusajs/medusa"; 2 | import OnboardingRepository from "../repositories/onboarding"; 3 | import { OnboardingState } from "../models/onboarding"; 4 | import { EntityManager, IsNull, Not } from "typeorm"; 5 | import { UpdateOnboardingStateInput } from "../types/onboarding"; 6 | 7 | type InjectedDependencies = { 8 | manager: EntityManager; 9 | onboardingRepository: typeof OnboardingRepository; 10 | }; 11 | 12 | class OnboardingService extends TransactionBaseService { 13 | protected onboardingRepository_: typeof OnboardingRepository; 14 | 15 | constructor({ onboardingRepository }: InjectedDependencies) { 16 | super(arguments[0]); 17 | 18 | this.onboardingRepository_ = onboardingRepository; 19 | } 20 | 21 | async retrieve(): Promise { 22 | const onboardingRepo = this.activeManager_.withRepository( 23 | this.onboardingRepository_ 24 | ); 25 | 26 | const status = await onboardingRepo.findOne({ 27 | where: { id: Not(IsNull()) }, 28 | }); 29 | 30 | return status; 31 | } 32 | 33 | async update(data: UpdateOnboardingStateInput): Promise { 34 | return await this.atomicPhase_( 35 | async (transactionManager: EntityManager) => { 36 | const onboardingRepository = transactionManager.withRepository( 37 | this.onboardingRepository_ 38 | ); 39 | 40 | const status = await this.retrieve(); 41 | 42 | for (const [key, value] of Object.entries(data)) { 43 | status[key] = value; 44 | } 45 | 46 | return await onboardingRepository.save(status); 47 | } 48 | ); 49 | } 50 | } 51 | 52 | export default OnboardingService; 53 | -------------------------------------------------------------------------------- /src/subscribers/README.md: -------------------------------------------------------------------------------- 1 | # Custom subscribers 2 | 3 | You may define custom eventhandlers, `subscribers` by creating files in the `/subscribers` directory. 4 | 5 | ```ts 6 | import MyCustomService from "../services/my-custom"; 7 | import { 8 | OrderService, 9 | SubscriberArgs, 10 | SubscriberConfig, 11 | } from "@medusajs/medusa"; 12 | 13 | type OrderPlacedEvent = { 14 | id: string; 15 | no_notification: boolean; 16 | }; 17 | 18 | export default async function orderPlacedHandler({ 19 | data, 20 | eventName, 21 | container, 22 | }: SubscriberArgs) { 23 | const orderService: OrderService = container.resolve(OrderService); 24 | 25 | const order = await orderService.retrieve(data.id, { 26 | relations: ["items", "items.variant", "items.variant.product"], 27 | }); 28 | 29 | // Do something with the order 30 | } 31 | 32 | export const config: SubscriberConfig = { 33 | event: OrderService.Events.PLACED, 34 | }; 35 | ``` 36 | 37 | A subscriber is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when an event is emitted. The `config` is an object which defines which event(s) the subscriber should subscribe to. 38 | 39 | The `handler` is a function which takes one parameter, an `object` of type `SubscriberArgs` with the following properties: 40 | 41 | - `data` - an `object` of type `T` containing information about the event. 42 | - `eventName` - a `string` containing the name of the event. 43 | - `container` - a `MedusaContainer` instance which can be used to resolve services. 44 | - `pluginOptions` - an `object` containing plugin options, if the subscriber is defined in a plugin. 45 | -------------------------------------------------------------------------------- /src/types/onboarding.ts: -------------------------------------------------------------------------------- 1 | import { OnboardingState } from "../models/onboarding"; 2 | 3 | export type UpdateOnboardingStateInput = { 4 | current_step?: string; 5 | is_complete?: boolean; 6 | product_id?: string; 7 | }; 8 | 9 | export interface AdminOnboardingUpdateStateReq {} 10 | 11 | export type OnboardingStateRes = { 12 | status: OnboardingState; 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext" 5 | }, 6 | "include": ["src/admin"], 7 | "exclude": ["**/*.spec.js"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "allowJs": true, 5 | "esModuleInterop": true, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "skipDefaultLibCheck": true, 12 | "declaration": true, 13 | "sourceMap": false, 14 | "outDir": "./dist", 15 | "rootDir": "./src", 16 | "baseUrl": ".", 17 | "jsx": "react-jsx", 18 | "forceConsistentCasingInFileNames": true, 19 | "resolveJsonModule": true, 20 | "checkJs": false 21 | }, 22 | "include": ["src/"], 23 | "exclude": [ 24 | "**/__tests__", 25 | "**/__fixtures__", 26 | "node_modules", 27 | "build", 28 | ".cache" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | /* Emit a single file with source maps instead of having a separate file. */ 5 | "inlineSourceMap": true 6 | }, 7 | "exclude": ["src/admin", "**/*.spec.js"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["dist", "node_modules"] 5 | } 6 | --------------------------------------------------------------------------------