From e0d72fff9d3a8300b96c198723438af5ead3cd00 Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Thu, 25 Jan 2024 22:14:53 +0000 Subject: [PATCH 1/3] feat: setup base --- examples/backend/.babelrc.js | 12 + examples/backend/.env.template | 5 + examples/backend/.github/dependabot.yml | 21 + examples/backend/.gitignore | 26 + examples/backend/.vscode/settings.json | 2 + examples/backend/.yarnrc.yml | 1 + examples/backend/README.md | 70 ++ examples/backend/data/seed-onboarding.json | 141 +++ examples/backend/data/seed.json | 1006 +++++++++++++++++ examples/backend/index.js | 50 + examples/backend/medusa-config.js | 88 ++ examples/backend/package.json | 90 ++ .../default/orders/order-detail.tsx | 136 +++ .../default/orders/orders-list.tsx | 81 ++ .../default/products/product-detail.tsx | 90 ++ .../default/products/products-list.tsx | 72 ++ .../nextjs/orders/order-detail.tsx | 138 +++ .../nextjs/orders/orders-list.tsx | 71 ++ .../nextjs/products/product-detail.tsx | 54 + .../nextjs/products/products-list.tsx | 84 ++ .../src/admin/components/shared/accordion.tsx | 123 ++ .../src/admin/components/shared/card.tsx | 27 + .../icons/active-circle-dotted-line.tsx | 37 + .../components/shared/icons/get-started.tsx | 24 + examples/backend/src/admin/types/icon-type.ts | 8 + .../backend/src/admin/utils/prepare-region.ts | 42 + .../admin/utils/prepare-shipping-options.ts | 20 + .../src/admin/utils/sample-products.ts | 666 +++++++++++ .../onboarding-flow/onboarding-flow.tsx | 502 ++++++++ examples/backend/src/api/README.md | 179 +++ .../backend/src/api/admin/custom/route.ts | 8 + .../backend/src/api/admin/onboarding/route.ts | 27 + .../backend/src/api/store/custom/route.ts | 8 + examples/backend/src/jobs/README.md | 32 + examples/backend/src/loaders/README.md | 19 + .../1685715079776-CreateOnboarding.ts | 21 + .../1686062614694-AddOnboardingProduct.ts | 15 + .../1690996567455-CorrectOnboardingFields.ts | 16 + examples/backend/src/migrations/README.md | 29 + examples/backend/src/models/README.md | 46 + examples/backend/src/models/onboarding.ts | 14 + .../backend/src/repositories/onboarding.ts | 6 + examples/backend/src/services/README.md | 49 + .../services/__tests__/test-service.spec.ts | 5 + examples/backend/src/services/onboarding.ts | 52 + examples/backend/src/subscribers/README.md | 44 + examples/backend/src/types/onboarding.ts | 13 + examples/backend/tsconfig.admin.json | 8 + examples/backend/tsconfig.json | 30 + examples/backend/tsconfig.server.json | 8 + examples/backend/tsconfig.spec.json | 5 + 51 files changed, 4321 insertions(+) create mode 100644 examples/backend/.babelrc.js create mode 100644 examples/backend/.env.template create mode 100644 examples/backend/.github/dependabot.yml create mode 100644 examples/backend/.gitignore create mode 100644 examples/backend/.vscode/settings.json create mode 100644 examples/backend/.yarnrc.yml create mode 100644 examples/backend/README.md create mode 100644 examples/backend/data/seed-onboarding.json create mode 100644 examples/backend/data/seed.json create mode 100644 examples/backend/index.js create mode 100644 examples/backend/medusa-config.js create mode 100644 examples/backend/package.json create mode 100644 examples/backend/src/admin/components/onboarding-flow/default/orders/order-detail.tsx create mode 100644 examples/backend/src/admin/components/onboarding-flow/default/orders/orders-list.tsx create mode 100644 examples/backend/src/admin/components/onboarding-flow/default/products/product-detail.tsx create mode 100644 examples/backend/src/admin/components/onboarding-flow/default/products/products-list.tsx create mode 100644 examples/backend/src/admin/components/onboarding-flow/nextjs/orders/order-detail.tsx create mode 100644 examples/backend/src/admin/components/onboarding-flow/nextjs/orders/orders-list.tsx create mode 100644 examples/backend/src/admin/components/onboarding-flow/nextjs/products/product-detail.tsx create mode 100644 examples/backend/src/admin/components/onboarding-flow/nextjs/products/products-list.tsx create mode 100644 examples/backend/src/admin/components/shared/accordion.tsx create mode 100644 examples/backend/src/admin/components/shared/card.tsx create mode 100644 examples/backend/src/admin/components/shared/icons/active-circle-dotted-line.tsx create mode 100644 examples/backend/src/admin/components/shared/icons/get-started.tsx create mode 100644 examples/backend/src/admin/types/icon-type.ts create mode 100644 examples/backend/src/admin/utils/prepare-region.ts create mode 100644 examples/backend/src/admin/utils/prepare-shipping-options.ts create mode 100644 examples/backend/src/admin/utils/sample-products.ts create mode 100644 examples/backend/src/admin/widgets/onboarding-flow/onboarding-flow.tsx create mode 100644 examples/backend/src/api/README.md create mode 100644 examples/backend/src/api/admin/custom/route.ts create mode 100644 examples/backend/src/api/admin/onboarding/route.ts create mode 100644 examples/backend/src/api/store/custom/route.ts create mode 100644 examples/backend/src/jobs/README.md create mode 100644 examples/backend/src/loaders/README.md create mode 100644 examples/backend/src/migrations/1685715079776-CreateOnboarding.ts create mode 100644 examples/backend/src/migrations/1686062614694-AddOnboardingProduct.ts create mode 100644 examples/backend/src/migrations/1690996567455-CorrectOnboardingFields.ts create mode 100644 examples/backend/src/migrations/README.md create mode 100644 examples/backend/src/models/README.md create mode 100644 examples/backend/src/models/onboarding.ts create mode 100644 examples/backend/src/repositories/onboarding.ts create mode 100644 examples/backend/src/services/README.md create mode 100644 examples/backend/src/services/__tests__/test-service.spec.ts create mode 100644 examples/backend/src/services/onboarding.ts create mode 100644 examples/backend/src/subscribers/README.md create mode 100644 examples/backend/src/types/onboarding.ts create mode 100644 examples/backend/tsconfig.admin.json create mode 100644 examples/backend/tsconfig.json create mode 100644 examples/backend/tsconfig.server.json create mode 100644 examples/backend/tsconfig.spec.json diff --git a/examples/backend/.babelrc.js b/examples/backend/.babelrc.js new file mode 100644 index 0000000..ae10f02 --- /dev/null +++ b/examples/backend/.babelrc.js @@ -0,0 +1,12 @@ +let ignore = [`**/dist`] + +// Jest needs to compile this code, but generally we don't want this copied +// to output folders +if (process.env.NODE_ENV !== `test`) { + ignore.push(`**/__tests__`) +} + +module.exports = { + presets: [["babel-preset-medusa-package"], ["@babel/preset-typescript"]], + ignore, +} diff --git a/examples/backend/.env.template b/examples/backend/.env.template new file mode 100644 index 0000000..68fe3ef --- /dev/null +++ b/examples/backend/.env.template @@ -0,0 +1,5 @@ +JWT_SECRET=something +COOKIE_SECRET=something + +DATABASE_TYPE="postgres" +REDIS_URL=redis://localhost:6379 diff --git a/examples/backend/.github/dependabot.yml b/examples/backend/.github/dependabot.yml new file mode 100644 index 0000000..d499e13 --- /dev/null +++ b/examples/backend/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: production + groups: + medusa: + patterns: + - "@medusajs*" + - "medusa*" + update-types: + - "minor" + - "patch" + ignore: + - dependency-name: "@medusajs*" + update-types: ["version-update:semver-major"] + - dependency-name: "medusa*" + update-types: ["version-update:semver-major"] diff --git a/examples/backend/.gitignore b/examples/backend/.gitignore new file mode 100644 index 0000000..6d5c399 --- /dev/null +++ b/examples/backend/.gitignore @@ -0,0 +1,26 @@ +/dist +.env +.DS_Store +/uploads +/node_modules +yarn-error.log + +.idea + +coverage + +!src/** + +./tsconfig.tsbuildinfo +package-lock.json +yarn.lock +medusa-db.sql +build +.cache + +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/examples/backend/.vscode/settings.json b/examples/backend/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/examples/backend/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/examples/backend/.yarnrc.yml b/examples/backend/.yarnrc.yml new file mode 100644 index 0000000..8b757b2 --- /dev/null +++ b/examples/backend/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules \ No newline at end of file diff --git a/examples/backend/README.md b/examples/backend/README.md new file mode 100644 index 0000000..8b4c0c1 --- /dev/null +++ b/examples/backend/README.md @@ -0,0 +1,70 @@ +

+ + + + + Medusa logo + + +

+

+ Medusa +

+ +

+ Documentation | + Website +

+ +

+ Building blocks for digital commerce +

+

+ + PRs welcome! + + Product Hunt + + Discord Chat + + + Follow @medusajs + +

+ +## Compatibility + +This starter is compatible with versions >= 1.8.0 of `@medusajs/medusa`. + +## Getting Started + +Visit the [Quickstart Guide](https://docs.medusajs.com/create-medusa-app) to set up a server. + +Visit the [Docs](https://docs.medusajs.com/development/backend/prepare-environment) to learn more about our system requirements. + +## What is Medusa + +Medusa is a set of commerce modules and tools that allow you to build rich, reliable, and performant commerce applications without reinventing core commerce logic. The modules can be customized and used to build advanced ecommerce stores, marketplaces, or any product that needs foundational commerce primitives. All modules are open-source and freely available on npm. + +Learn more about [Medusa’s architecture](https://docs.medusajs.com/development/fundamentals/architecture-overview) and [commerce modules](https://docs.medusajs.com/modules/overview) in the Docs. + +## Roadmap, Upgrades & Plugins + +You can view the planned, started and completed features in the [Roadmap discussion](https://github.com/medusajs/medusa/discussions/categories/roadmap). + +Follow the [Upgrade Guides](https://docs.medusajs.com/upgrade-guides/) to keep your Medusa project up-to-date. + +Check out all [available Medusa plugins](https://medusajs.com/plugins/). + +## Community & Contributions + +The community and core team are available in [GitHub Discussions](https://github.com/medusajs/medusa/discussions), where you can ask for support, discuss roadmap, and share ideas. + +Join our [Discord server](https://discord.com/invite/medusajs) to meet other community members. + +## Other channels + +- [GitHub Issues](https://github.com/medusajs/medusa/issues) +- [Twitter](https://twitter.com/medusajs) +- [LinkedIn](https://www.linkedin.com/company/medusajs) +- [Medusa Blog](https://medusajs.com/blog/) diff --git a/examples/backend/data/seed-onboarding.json b/examples/backend/data/seed-onboarding.json new file mode 100644 index 0000000..4641c39 --- /dev/null +++ b/examples/backend/data/seed-onboarding.json @@ -0,0 +1,141 @@ +{ + "store": { + "currencies": [ + "eur", + "usd" + ] + }, + "users": [], + "regions": [ + { + "id": "test-region-eu", + "name": "EU", + "currency_code": "eur", + "tax_rate": 0, + "payment_providers": [ + "manual" + ], + "fulfillment_providers": [ + "manual" + ], + "countries": [ + "gb", + "de", + "dk", + "se", + "fr", + "es", + "it" + ] + }, + { + "id": "test-region-na", + "name": "NA", + "currency_code": "usd", + "tax_rate": 0, + "payment_providers": [ + "manual" + ], + "fulfillment_providers": [ + "manual" + ], + "countries": [ + "us", + "ca" + ] + } + ], + "shipping_options": [ + { + "name": "PostFake Standard", + "region_id": "test-region-eu", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "amount": 1000 + }, + { + "name": "PostFake Express", + "region_id": "test-region-eu", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "amount": 1500 + }, + { + "name": "PostFake Return", + "region_id": "test-region-eu", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "is_return": true, + "amount": 1000 + }, + { + "name": "I want to return it myself", + "region_id": "test-region-eu", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "is_return": true, + "amount": 0 + }, + { + "name": "FakeEx Standard", + "region_id": "test-region-na", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "amount": 800 + }, + { + "name": "FakeEx Express", + "region_id": "test-region-na", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "amount": 1200 + }, + { + "name": "FakeEx Return", + "region_id": "test-region-na", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "is_return": true, + "amount": 800 + }, + { + "name": "I want to return it myself", + "region_id": "test-region-na", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "is_return": true, + "amount": 0 + } + ], + "products": [], + "categories": [], + "publishable_api_keys": [ + { + "title": "Development" + } + ] +} \ No newline at end of file diff --git a/examples/backend/data/seed.json b/examples/backend/data/seed.json new file mode 100644 index 0000000..b99f3a3 --- /dev/null +++ b/examples/backend/data/seed.json @@ -0,0 +1,1006 @@ +{ + "store": { + "currencies": [ + "eur", + "usd" + ] + }, + "users": [ + { + "email": "admin@medusa-test.com", + "password": "supersecret" + } + ], + "regions": [ + { + "id": "test-region-eu", + "name": "EU", + "currency_code": "eur", + "tax_rate": 0, + "payment_providers": [ + "manual" + ], + "fulfillment_providers": [ + "manual" + ], + "countries": [ + "gb", + "de", + "dk", + "se", + "fr", + "es", + "it" + ] + }, + { + "id": "test-region-na", + "name": "NA", + "currency_code": "usd", + "tax_rate": 0, + "payment_providers": [ + "manual" + ], + "fulfillment_providers": [ + "manual" + ], + "countries": [ + "us", + "ca" + ] + } + ], + "shipping_options": [ + { + "name": "PostFake Standard", + "region_id": "test-region-eu", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "amount": 1000 + }, + { + "name": "PostFake Express", + "region_id": "test-region-eu", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "amount": 1500 + }, + { + "name": "PostFake Return", + "region_id": "test-region-eu", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "is_return": true, + "amount": 1000 + }, + { + "name": "I want to return it myself", + "region_id": "test-region-eu", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "is_return": true, + "amount": 0 + }, + { + "name": "FakeEx Standard", + "region_id": "test-region-na", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "amount": 800 + }, + { + "name": "FakeEx Express", + "region_id": "test-region-na", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "amount": 1200 + }, + { + "name": "FakeEx Return", + "region_id": "test-region-na", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "is_return": true, + "amount": 800 + }, + { + "name": "I want to return it myself", + "region_id": "test-region-na", + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "is_return": true, + "amount": 0 + } + ], + "products": [ + { + "title": "Medusa T-Shirt", + "categories": [ + { + "id": "pcat_shirts" + } + ], + "subtitle": null, + "description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "handle": "t-shirt", + "is_giftcard": false, + "weight": 400, + "images": [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png" + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL" + ] + }, + { + "title": "Color", + "values": [ + "Black", + "White" + ] + } + ], + "variants": [ + { + "title": "S / Black", + "prices": [ + { + "currency_code": "eur", + "amount": 1950 + }, + { + "currency_code": "usd", + "amount": 2200 + } + ], + "options": [ + { + "value": "S" + }, + { + "value": "Black" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "S / White", + "prices": [ + { + "currency_code": "eur", + "amount": 1950 + }, + { + "currency_code": "usd", + "amount": 2200 + } + ], + "options": [ + { + "value": "S" + }, + { + "value": "White" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "M / Black", + "prices": [ + { + "currency_code": "eur", + "amount": 1950 + }, + { + "currency_code": "usd", + "amount": 2200 + } + ], + "options": [ + { + "value": "M" + }, + { + "value": "Black" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "M / White", + "prices": [ + { + "currency_code": "eur", + "amount": 1950 + }, + { + "currency_code": "usd", + "amount": 2200 + } + ], + "options": [ + { + "value": "M" + }, + { + "value": "White" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "L / Black", + "prices": [ + { + "currency_code": "eur", + "amount": 1950 + }, + { + "currency_code": "usd", + "amount": 2200 + } + ], + "options": [ + { + "value": "L" + }, + { + "value": "Black" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "L / White", + "prices": [ + { + "currency_code": "eur", + "amount": 1950 + }, + { + "currency_code": "usd", + "amount": 2200 + } + ], + "options": [ + { + "value": "L" + }, + { + "value": "White" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "XL / Black", + "prices": [ + { + "currency_code": "eur", + "amount": 1950 + }, + { + "currency_code": "usd", + "amount": 2200 + } + ], + "options": [ + { + "value": "XL" + }, + { + "value": "Black" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "XL / White", + "prices": [ + { + "currency_code": "eur", + "amount": 1950 + }, + { + "currency_code": "usd", + "amount": 2200 + } + ], + "options": [ + { + "value": "XL" + }, + { + "value": "White" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + } + ] + }, + { + "title": "Medusa Sweatshirt", + "categories": [ + { + "id": "pcat_shirts" + } + ], + "subtitle": null, + "description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "handle": "sweatshirt", + "is_giftcard": false, + "weight": 400, + "images": [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png" + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL" + ] + } + ], + "variants": [ + { + "title": "S", + "prices": [ + { + "currency_code": "eur", + "amount": 2950 + }, + { + "currency_code": "usd", + "amount": 3350 + } + ], + "options": [ + { + "value": "S" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "M", + "prices": [ + { + "currency_code": "eur", + "amount": 2950 + }, + { + "currency_code": "usd", + "amount": 3350 + } + ], + "options": [ + { + "value": "M" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "L", + "prices": [ + { + "currency_code": "eur", + "amount": 2950 + }, + { + "currency_code": "usd", + "amount": 3350 + } + ], + "options": [ + { + "value": "L" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "XL", + "prices": [ + { + "currency_code": "eur", + "amount": 2950 + }, + { + "currency_code": "usd", + "amount": 3350 + } + ], + "options": [ + { + "value": "XL" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + } + ] + }, + { + "title": "Medusa Sweatpants", + "categories": [ + { + "id": "pcat_pants" + } + ], + "subtitle": null, + "description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "handle": "sweatpants", + "is_giftcard": false, + "weight": 400, + "images": [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png" + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL" + ] + } + ], + "variants": [ + { + "title": "S", + "prices": [ + { + "currency_code": "eur", + "amount": 2950 + }, + { + "currency_code": "usd", + "amount": 3350 + } + ], + "options": [ + { + "value": "S" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "M", + "prices": [ + { + "currency_code": "eur", + "amount": 2950 + }, + { + "currency_code": "usd", + "amount": 3350 + } + ], + "options": [ + { + "value": "M" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "L", + "prices": [ + { + "currency_code": "eur", + "amount": 2950 + }, + { + "currency_code": "usd", + "amount": 3350 + } + ], + "options": [ + { + "value": "L" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "XL", + "prices": [ + { + "currency_code": "eur", + "amount": 2950 + }, + { + "currency_code": "usd", + "amount": 3350 + } + ], + "options": [ + { + "value": "XL" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + } + ] + }, + { + "title": "Medusa Shorts", + "categories": [ + { + "id": "pcat_merch" + } + ], + "subtitle": null, + "description": "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", + "handle": "shorts", + "is_giftcard": false, + "weight": 400, + "images": [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png" + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL" + ] + } + ], + "variants": [ + { + "title": "S", + "prices": [ + { + "currency_code": "eur", + "amount": 2500 + }, + { + "currency_code": "usd", + "amount": 2850 + } + ], + "options": [ + { + "value": "S" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "M", + "prices": [ + { + "currency_code": "eur", + "amount": 2500 + }, + { + "currency_code": "usd", + "amount": 2850 + } + ], + "options": [ + { + "value": "M" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "L", + "prices": [ + { + "currency_code": "eur", + "amount": 2500 + }, + { + "currency_code": "usd", + "amount": 2850 + } + ], + "options": [ + { + "value": "L" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "XL", + "prices": [ + { + "currency_code": "eur", + "amount": 2500 + }, + { + "currency_code": "usd", + "amount": 2850 + } + ], + "options": [ + { + "value": "XL" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + } + ] + }, + { + "title": "Medusa Hoodie", + "categories": [ + { + "id": "pcat_merch" + }, + { + "id": "pcat_hidden_featured" + } + ], + "subtitle": null, + "description": "Reimagine the feeling of a classic hoodie. With our cotton hoodie, everyday essentials no longer have to be ordinary.", + "handle": "hoodie", + "is_giftcard": false, + "weight": 400, + "images": [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_back.png" + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL" + ] + } + ], + "variants": [ + { + "title": "S", + "prices": [ + { + "currency_code": "eur", + "amount": 3650 + }, + { + "currency_code": "usd", + "amount": 4150 + } + ], + "options": [ + { + "value": "S" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "M", + "prices": [ + { + "currency_code": "eur", + "amount": 3650 + }, + { + "currency_code": "usd", + "amount": 4150 + } + ], + "options": [ + { + "value": "M" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "L", + "prices": [ + { + "currency_code": "eur", + "amount": 3650 + }, + { + "currency_code": "usd", + "amount": 4150 + } + ], + "options": [ + { + "value": "L" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "XL", + "prices": [ + { + "currency_code": "eur", + "amount": 3650 + }, + { + "currency_code": "usd", + "amount": 4150 + } + ], + "options": [ + { + "value": "XL" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + } + ] + }, + { + "title": "Medusa Longsleeve", + "categories": [ + { + "id": "pcat_shirts" + }, + { + "id": "pcat_hidden_featured" + } + ], + "subtitle": null, + "description": "Reimagine the feeling of a classic longsleeve. With our cotton longsleeve, everyday essentials no longer have to be ordinary.", + "handle": "longsleeve", + "is_giftcard": false, + "weight": 400, + "images": [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-back.png" + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL" + ] + } + ], + "variants": [ + { + "title": "S", + "prices": [ + { + "currency_code": "eur", + "amount": 3650 + }, + { + "currency_code": "usd", + "amount": 4150 + } + ], + "options": [ + { + "value": "S" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "M", + "prices": [ + { + "currency_code": "eur", + "amount": 3650 + }, + { + "currency_code": "usd", + "amount": 4150 + } + ], + "options": [ + { + "value": "M" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "L", + "prices": [ + { + "currency_code": "eur", + "amount": 3650 + }, + { + "currency_code": "usd", + "amount": 4150 + } + ], + "options": [ + { + "value": "L" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + }, + { + "title": "XL", + "prices": [ + { + "currency_code": "eur", + "amount": 3650 + }, + { + "currency_code": "usd", + "amount": 4150 + } + ], + "options": [ + { + "value": "XL" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + } + ] + }, + { + "title": "Medusa Coffee Mug", + "categories": [ + { + "id": "pcat_merch" + }, + { + "id": "pcat_hidden_featured" + } + ], + "subtitle": null, + "description": "Every programmer's best friend.", + "handle": "coffee-mug", + "is_giftcard": false, + "weight": 400, + "images": [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png" + ], + "options": [ + { + "title": "Size", + "values": [ + "One Size" + ] + } + ], + "variants": [ + { + "title": "One Size", + "prices": [ + { + "currency_code": "eur", + "amount": 1000 + }, + { + "currency_code": "usd", + "amount": 1200 + } + ], + "options": [ + { + "value": "One Size" + } + ], + "inventory_quantity": 100, + "manage_inventory": true + } + ] + } + ], + "categories": [ + { + "id": "pcat_pants", + "name": "Pants", + "rank": 0, + "category_children": [], + "handle": "pants" + }, + { + "id": "pcat_shirts", + "name": "Shirts", + "rank": 0, + "category_children": [], + "handle": "shirts" + }, + { + "id": "pcat_merch", + "name": "Merch", + "rank": 0, + "category_children": [], + "handle": "merch" + }, + { + "id": "pcat_hidden_carousel", + "name": "Hidden homepage carousel", + "rank": 0, + "category_children": [], + "handle": "hidden-homepage-carousel" + }, + { + "id": "pcat_hidden_featured", + "name": "Hidden homepage featured", + "rank": 0, + "category_children": [], + "handle": "hidden-homepage-featured-items" + } + ] +} \ No newline at end of file diff --git a/examples/backend/index.js b/examples/backend/index.js new file mode 100644 index 0000000..e44fcfe --- /dev/null +++ b/examples/backend/index.js @@ -0,0 +1,50 @@ +const express = require("express") +const { GracefulShutdownServer } = require("medusa-core-utils") + +const loaders = require("@medusajs/medusa/dist/loaders/index").default + +;(async() => { + async function start() { + const app = express() + const directory = process.cwd() + + try { + const { container } = await loaders({ + directory, + expressApp: app + }) + const configModule = container.resolve("configModule") + const port = process.env.PORT ?? configModule.projectConfig.port ?? 9000 + + const server = GracefulShutdownServer.create( + app.listen(port, (err) => { + if (err) { + return + } + console.log(`Server is ready on port: ${port}`) + }) + ) + + // Handle graceful shutdown + const gracefulShutDown = () => { + server + .shutdown() + .then(() => { + console.info("Gracefully stopping the server.") + process.exit(0) + }) + .catch((e) => { + console.error("Error received when shutting down the server.", e) + process.exit(1) + }) + } + process.on("SIGTERM", gracefulShutDown) + process.on("SIGINT", gracefulShutDown) + } catch (err) { + console.error("Error starting server", err) + process.exit(1) + } + } + + await start() +})() diff --git a/examples/backend/medusa-config.js b/examples/backend/medusa-config.js new file mode 100644 index 0000000..d29c875 --- /dev/null +++ b/examples/backend/medusa-config.js @@ -0,0 +1,88 @@ +const dotenv = require("dotenv"); + +let ENV_FILE_NAME = ""; +switch (process.env.NODE_ENV) { + case "production": + ENV_FILE_NAME = ".env.production"; + break; + case "staging": + ENV_FILE_NAME = ".env.staging"; + break; + case "test": + ENV_FILE_NAME = ".env.test"; + break; + case "development": + default: + ENV_FILE_NAME = ".env"; + break; +} + +try { + dotenv.config({ path: process.cwd() + "/" + ENV_FILE_NAME }); +} catch (e) {} + +// CORS when consuming Medusa from admin +const ADMIN_CORS = + process.env.ADMIN_CORS || "http://localhost:7000,http://localhost:7001"; + +// CORS to avoid issues when consuming Medusa from a client +const STORE_CORS = process.env.STORE_CORS || "http://localhost:8000"; + +const DATABASE_URL = + process.env.DATABASE_URL || "postgres://localhost/medusa-starter-default"; + +const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379"; + +const plugins = [ + `medusa-fulfillment-manual`, + `medusa-payment-manual`, + { + resolve: `@medusajs/file-local`, + options: { + upload_dir: "uploads", + }, + }, + { + resolve: "@medusajs/admin", + /** @type {import('@medusajs/admin').PluginOptions} */ + options: { + autoRebuild: true, + develop: { + open: process.env.OPEN_BROWSER !== "false", + }, + }, + }, +]; + +const modules = { + /*eventBus: { + resolve: "@medusajs/event-bus-redis", + options: { + redisUrl: REDIS_URL + } + }, + cacheService: { + resolve: "@medusajs/cache-redis", + options: { + redisUrl: REDIS_URL + } + },*/ +}; + +/** @type {import('@medusajs/medusa').ConfigModule["projectConfig"]} */ +const projectConfig = { + jwtSecret: process.env.JWT_SECRET, + cookieSecret: process.env.COOKIE_SECRET, + store_cors: STORE_CORS, + database_url: DATABASE_URL, + admin_cors: ADMIN_CORS, + // Uncomment the following lines to enable REDIS + // redis_url: REDIS_URL +}; + +/** @type {import('@medusajs/medusa').ConfigModule} */ +module.exports = { + projectConfig, + plugins, + modules, +}; diff --git a/examples/backend/package.json b/examples/backend/package.json new file mode 100644 index 0000000..2a8b5e9 --- /dev/null +++ b/examples/backend/package.json @@ -0,0 +1,90 @@ +{ + "name": "medusa-starter-default", + "version": "0.0.1", + "description": "A starter for Medusa projects.", + "author": "Medusa (https://medusajs.com)", + "license": "MIT", + "keywords": [ + "sqlite", + "postgres", + "typescript", + "ecommerce", + "headless", + "medusa" + ], + "scripts": { + "clean": "cross-env ./node_modules/.bin/rimraf dist", + "build": "cross-env npm run clean && npm run build:server && npm run build:admin", + "build:server": "cross-env npm run clean && tsc -p tsconfig.server.json", + "build:admin": "cross-env medusa-admin build", + "watch": "cross-env tsc --watch", + "test": "cross-env jest", + "seed": "cross-env medusa seed -f ./data/seed.json", + "start": "cross-env npm run build && medusa start", + "start:custom": "cross-env npm run build && node --preserve-symlinks --trace-warnings index.js", + "dev": "cross-env npm run build:server && medusa develop" + }, + "dependencies": { + "@medusajs/admin": "7.1.10", + "@medusajs/cache-inmemory": "^1.8.9", + "@medusajs/cache-redis": "^1.8.9", + "@medusajs/event-bus-local": "^1.9.7", + "@medusajs/event-bus-redis": "^1.8.10", + "@medusajs/file-local": "^1.0.3", + "@medusajs/medusa": "1.20.1", + "@tanstack/react-query": "4.22.0", + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "dotenv": "16.3.1", + "express": "^4.17.2", + "medusa-fulfillment-manual": "^1.1.39", + "medusa-interfaces": "^1.3.8", + "medusa-payment-manual": "^1.0.24", + "medusa-payment-stripe": "^6.0.7", + "prism-react-renderer": "^2.0.4", + "typeorm": "^0.3.16" + }, + "devDependencies": { + "@babel/cli": "^7.14.3", + "@babel/core": "^7.14.3", + "@babel/preset-typescript": "^7.21.4", + "@medusajs/medusa-cli": "^1.3.21", + "@types/express": "^4.17.13", + "@types/jest": "^27.4.0", + "@types/node": "^17.0.8", + "babel-preset-medusa-package": "^1.1.19", + "cross-env": "^7.0.3", + "eslint": "^6.8.0", + "jest": "^27.3.1", + "rimraf": "^3.0.2", + "ts-jest": "^27.0.7", + "ts-loader": "^9.2.6", + "typescript": "^4.5.2" + }, + "jest": { + "globals": { + "ts-jest": { + "tsconfig": "tsconfig.spec.json" + } + }, + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/node_modules/" + ], + "rootDir": "src", + "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$", + "transform": { + ".ts": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "./coverage", + "testEnvironment": "node" + } +} diff --git a/examples/backend/src/admin/components/onboarding-flow/default/orders/order-detail.tsx b/examples/backend/src/admin/components/onboarding-flow/default/orders/order-detail.tsx new file mode 100644 index 0000000..2c596dc --- /dev/null +++ b/examples/backend/src/admin/components/onboarding-flow/default/orders/order-detail.tsx @@ -0,0 +1,136 @@ +import React from "react"; +import { + ComputerDesktopSolid, + CurrencyDollarSolid, + NextJs +} from "@medusajs/icons"; +import { IconBadge, Heading, Text } from "@medusajs/ui"; + +const OrderDetailDefault = () => { + return ( + <> + + You finished the setup guide 🎉 You now have your first order. Feel free + to play around with the order management functionalities, such as + capturing payment, creating fulfillments, and more. + + + Start developing with Medusa + + + Medusa is a completely customizable commerce solution. We've curated + some essential guides to kickstart your development with Medusa. + +
+ +
+
+
+ + + +
+
+
+ + Add Commerce Features + + + Learn about all available commerce features and how to + add them in your storefront + +
+
+
+ +
+
+
+ + + +
+
+
+ + Build Custom Use Cases + + + Build a marketplace, subscription-based purchases, + or your custom use-cases. + +
+
+
+ +
+
+
+ + + +
+
+
+ + Install Next.js Quickstart + + + Install and use the Next.js storefront with + your commerce store. + +
+
+
+
+
+ You can find more useful guides in{" "} + + our documentation + + . If you like Medusa, please{" "} + + star us on GitHub + + . +
+ + ); +}; + +export default OrderDetailDefault; diff --git a/examples/backend/src/admin/components/onboarding-flow/default/orders/orders-list.tsx b/examples/backend/src/admin/components/onboarding-flow/default/orders/orders-list.tsx new file mode 100644 index 0000000..c7007d6 --- /dev/null +++ b/examples/backend/src/admin/components/onboarding-flow/default/orders/orders-list.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { + useAdminProduct, + useAdminCreateDraftOrder, + useMedusa +} from "medusa-react"; +import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; +import { Button, Text } from "@medusajs/ui"; +import prepareRegions from "../../../../utils/prepare-region"; +import prepareShippingOptions from "../../../../utils/prepare-shipping-options"; + +const OrdersListDefault = ({ onNext, isComplete, data }: StepContentProps) => { + const { product } = useAdminProduct(data.product_id); + const { mutateAsync: createDraftOrder, isLoading } = + useAdminCreateDraftOrder(); + const { client } = useMedusa(); + + const createOrder = async () => { + const variant = product.variants[0] ?? null; + try { + // check if there is a shipping option and a region + // and if not, create demo ones + const regions = await prepareRegions(client) + const shipping_options = await prepareShippingOptions(client, regions[0]) + + const { draft_order } = await createDraftOrder({ + email: "customer@medusajs.com", + items: [ + variant + ? { + quantity: 1, + variant_id: variant?.id, + } + : { + quantity: 1, + title: product.title, + unit_price: 50, + }, + ], + shipping_methods: [ + { + option_id: shipping_options[0].id, + }, + ], + region_id: regions[0].id, + }); + + const { order } = await client.admin.draftOrders.markPaid(draft_order.id); + + onNext(order); + } catch (e) { + console.error(e); + } + }; + return ( + <> +
+ + The last step is to create a sample order using the product you just created. You can then view your order’s details, process its payment, fulfillment, inventory, and more. + + + By clicking the “Create a Sample Order” button, we’ll generate an order using the product you created and default configurations. + +
+
+ {!isComplete && ( + + )} +
+ + ); +}; + +export default OrdersListDefault; diff --git a/examples/backend/src/admin/components/onboarding-flow/default/products/product-detail.tsx b/examples/backend/src/admin/components/onboarding-flow/default/products/product-detail.tsx new file mode 100644 index 0000000..f20041e --- /dev/null +++ b/examples/backend/src/admin/components/onboarding-flow/default/products/product-detail.tsx @@ -0,0 +1,90 @@ +import React, { useEffect, useMemo } from "react" +import { + useAdminPublishableApiKeys, + useAdminCreatePublishableApiKey +} from "medusa-react" +import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow" +import { Button, CodeBlock, Text } from "@medusajs/ui" + +const ProductDetailDefault = ({ onNext, isComplete, data }: StepContentProps) => { + const { publishable_api_keys: keys, isLoading, refetch } = useAdminPublishableApiKeys({ + offset: 0, + limit: 1, + }); + const createPublishableApiKey = useAdminCreatePublishableApiKey() + + const api_key = useMemo(() => keys?.[0]?.id || "", [keys]) + const backendUrl = process.env.MEDUSA_BACKEND_URL === "/" || process.env.MEDUSA_ADMIN_BACKEND_URL === "/" ? + location.origin : + process.env.MEDUSA_BACKEND_URL || process.env.MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000" + + useEffect(() => { + if (!isLoading && !keys?.length) { + createPublishableApiKey.mutate({ + "title": "Development" + }, { + onSuccess: () => { + refetch() + } + }) + } + }, [isLoading, keys]) + + return ( +
+
+ On this page, you can view your product's details and edit them. + + You can preview your product using Medusa's Store APIs. You can copy any + of the following code snippets to try it out. + +
+
+ {!isLoading && ( + {\n // ...\n const productService = await initializeProductModule()\n const products = await productService.list({\n id: "${data?.product_id}",\n })\n\n console.log(products[0])\n}`, + }, + ]} className="my-6"> + + + + )} +
+
+ + + + {!isComplete && ( + + )} +
+
+ ); +}; + +export default ProductDetailDefault; diff --git a/examples/backend/src/admin/components/onboarding-flow/default/products/products-list.tsx b/examples/backend/src/admin/components/onboarding-flow/default/products/products-list.tsx new file mode 100644 index 0000000..7f059c8 --- /dev/null +++ b/examples/backend/src/admin/components/onboarding-flow/default/products/products-list.tsx @@ -0,0 +1,72 @@ +import React, { useMemo } from "react"; +import { + useAdminCreateProduct, + useAdminCreateCollection, + useMedusa +} from "medusa-react"; +import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; +import { Button, Text } from "@medusajs/ui"; +import getSampleProducts from "../../../../utils/sample-products"; +import prepareRegions from "../../../../utils/prepare-region"; + +const ProductsListDefault = ({ onNext, isComplete }: StepContentProps) => { + const { mutateAsync: createCollection, isLoading: collectionLoading } = + useAdminCreateCollection(); + const { mutateAsync: createProduct, isLoading: productLoading } = + useAdminCreateProduct(); + const { client } = useMedusa() + + const isLoading = useMemo(() => + collectionLoading || productLoading, + [collectionLoading, productLoading] + ); + + const createSample = async () => { + try { + const { collection } = await createCollection({ + title: "Merch", + handle: "merch", + }); + + const regions = await prepareRegions(client) + + const sampleProducts = getSampleProducts({ + regions, + collection_id: collection.id + }) + const { product } = await createProduct(sampleProducts[0]); + onNext(product); + } catch (e) { + console.error(e); + } + }; + + return ( +
+ + Create a product and set its general details such as title and + description, its price, options, variants, images, and more. You'll then + use the product to create a sample order. + + + You can create a product by clicking the "New Product" button below. + Alternatively, if you're not ready to create your own product, we can + create a sample one for you. + + {!isComplete && ( +
+ +
+ )} +
+ ); +}; + +export default ProductsListDefault; diff --git a/examples/backend/src/admin/components/onboarding-flow/nextjs/orders/order-detail.tsx b/examples/backend/src/admin/components/onboarding-flow/nextjs/orders/order-detail.tsx new file mode 100644 index 0000000..2d62391 --- /dev/null +++ b/examples/backend/src/admin/components/onboarding-flow/nextjs/orders/order-detail.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import { CurrencyDollarSolid, NextJs, ComputerDesktopSolid } from "@medusajs/icons"; +import { IconBadge, Heading, Text } from "@medusajs/ui"; + +const OrderDetailNextjs = () => { + const queryParams = `?ref=onboarding&type=${ + process.env.MEDUSA_ADMIN_ONBOARDING_TYPE || "nextjs" + }`; + return ( + <> + + You finished the setup guide 🎉. You now have a complete ecommerce store + with a backend, admin, and a Next.js storefront. Feel free to play + around with each of these components to experience all commerce features + that Medusa provides. + + + Continue Building your Ecommerce Store + + + Your ecommerce store provides all basic ecommerce features you need to + start selling. You can add more functionalities, add plugins for + third-party integrations, and customize the storefront’s look and feel + to support your use case. + +
+ +
+
+
+ + + +
+
+
+ + Build with the Next.js Storefront + + + Learn about the Next.js starter storefront’s features and how to + customize it. + +
+
+
+ +
+
+
+ + + +
+
+
+ + Add Commerce Features + + + Learn about all available commerce features and how to + add them in your storefront + +
+
+
+ +
+
+
+ + + +
+
+
+ + Build Custom Use Cases + + + Build a marketplace, subscription-based purchases, + or your custom use-cases. + +
+
+
+
+
+ You can find more useful guides in{" "} + + our documentation + + . If you like Medusa, please{" "} + + star us on GitHub + + . +
+ + ); +}; + +export default OrderDetailNextjs; diff --git a/examples/backend/src/admin/components/onboarding-flow/nextjs/orders/orders-list.tsx b/examples/backend/src/admin/components/onboarding-flow/nextjs/orders/orders-list.tsx new file mode 100644 index 0000000..9fa5158 --- /dev/null +++ b/examples/backend/src/admin/components/onboarding-flow/nextjs/orders/orders-list.tsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from "react"; +import { + useAdminProduct, + useCreateCart, + useMedusa +} from "medusa-react"; +import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; +import { Button, Text } from "@medusajs/ui"; +import prepareRegions from "../../../../utils/prepare-region"; +import prepareShippingOptions from "../../../../utils/prepare-shipping-options"; + +const OrdersListNextjs = ({ isComplete, data }: StepContentProps) => { + const { product } = useAdminProduct(data.product_id); + const { mutateAsync: createCart, isLoading: cartIsLoading } = useCreateCart() + const { client } = useMedusa() + const [cartId, setCartId] = useState(null) + + const prepareNextjsCheckout = async () => { + const variant = product.variants[0] ?? null; + try { + const regions = await prepareRegions(client) + await prepareShippingOptions(client, regions[0]) + const { cart } = await createCart({ + region_id: regions[0]?.id, + items: [ + { + variant_id: variant?.id, + quantity: 1 + } + ] + }) + + setCartId(cart?.id) + } catch (e) { + console.error(e); + } + } + + useEffect(() => { + if (!cartId && product) { + prepareNextjsCheckout() + } + }, [cartId, product]) + + return ( + <> +
+ + The last step is to create a sample order using one of your products. You can then view your order’s details, process its payment, fulfillment, inventory, and more. + + + You can use the button below to experience hand-first the checkout flow in the Next.js storefront. After placing the order in the storefront, you’ll be directed back here to view the order’s details. + +
+
+ {!isComplete && ( + + + + )} +
+ + ); +}; + +export default OrdersListNextjs diff --git a/examples/backend/src/admin/components/onboarding-flow/nextjs/products/product-detail.tsx b/examples/backend/src/admin/components/onboarding-flow/nextjs/products/product-detail.tsx new file mode 100644 index 0000000..2231d1f --- /dev/null +++ b/examples/backend/src/admin/components/onboarding-flow/nextjs/products/product-detail.tsx @@ -0,0 +1,54 @@ +import { useAdminProduct } from "medusa-react"; +import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; +import { Button, Text } from "@medusajs/ui"; + +const ProductDetailNextjs = ({ onNext, isComplete, data }: StepContentProps) => { + const { product, isLoading: productIsLoading } = useAdminProduct(data?.product_id) + return ( +
+
+ + We have now created a few sample products in your Medusa store. You can scroll down to see what the Product Detail view looks like in the Admin dashboard. + This is also the view you use to edit existing products. + + + To view the products in your store, you can visit the Next.js Storefront that was installed with create-medusa-app. + + + The Next.js Storefront Starter is a template that helps you start building an ecommerce store with Medusa. + You control the code for the storefront and you can customize it further to fit your specific needs. + + + Click the button below to view the products in your Next.js Storefront. + + + Having trouble? Click{" "} + + here + . + +
+
+ + + + {!isComplete && ( + + )} +
+
+ ); +}; + +export default ProductDetailNextjs diff --git a/examples/backend/src/admin/components/onboarding-flow/nextjs/products/products-list.tsx b/examples/backend/src/admin/components/onboarding-flow/nextjs/products/products-list.tsx new file mode 100644 index 0000000..34b96ca --- /dev/null +++ b/examples/backend/src/admin/components/onboarding-flow/nextjs/products/products-list.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { + useAdminCreateProduct, + useAdminCreateCollection, + useMedusa +} from "medusa-react"; +import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"; +import { Button, Text } from "@medusajs/ui"; +import { AdminPostProductsReq, Product } from "@medusajs/medusa"; +import getSampleProducts from "../../../../utils/sample-products"; +import prepareRegions from "../../../../utils/prepare-region"; + +const ProductsListNextjs = ({ onNext, isComplete }: StepContentProps) => { + const { mutateAsync: createCollection, isLoading: collectionLoading } = + useAdminCreateCollection(); + const { mutateAsync: createProduct, isLoading: productLoading } = + useAdminCreateProduct(); + const { client } = useMedusa() + + const isLoading = collectionLoading || productLoading; + + const createSample = async () => { + try { + const { collection } = await createCollection({ + title: "Merch", + handle: "merch", + }); + + const regions = await prepareRegions(client) + + const tryCreateProduct = async (sampleProduct: AdminPostProductsReq): Promise => { + try { + return (await createProduct(sampleProduct)).product + } catch { + // ignore if product already exists + return null + } + } + + let product: Product + const sampleProducts = getSampleProducts({ + regions, + collection_id: collection.id + }) + await Promise.all( + sampleProducts.map(async (sampleProduct, index) => { + const createdProduct = await tryCreateProduct(sampleProduct) + if (index === 0 && createProduct) { + product = createdProduct + } + }) + ) + onNext(product); + } catch (e) { + console.error(e); + } + }; + + return ( +
+ + Products in Medusa represent the products you sell. You can set their general details including a + title and description. Each product has options and variants, and you can set a price for each variant. + + + Click the button below to create sample products. + + {!isComplete && ( +
+ +
+ )} +
+ ); +}; + +export default ProductsListNextjs; diff --git a/examples/backend/src/admin/components/shared/accordion.tsx b/examples/backend/src/admin/components/shared/accordion.tsx new file mode 100644 index 0000000..ed38d0c --- /dev/null +++ b/examples/backend/src/admin/components/shared/accordion.tsx @@ -0,0 +1,123 @@ +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import React from "react"; +import { CheckCircleSolid, CircleMiniSolid } from "@medusajs/icons"; +import { Heading, Text, clx } from "@medusajs/ui"; +import ActiveCircleDottedLine from "./icons/active-circle-dotted-line"; + +type AccordionItemProps = AccordionPrimitive.AccordionItemProps & { + title: string; + subtitle?: string; + description?: string; + required?: boolean; + tooltip?: string; + forceMountContent?: true; + headingSize?: "small" | "medium" | "large"; + customTrigger?: React.ReactNode; + complete?: boolean; + active?: boolean; + triggerable?: boolean; +}; + +const Accordion: React.FC< + | (AccordionPrimitive.AccordionSingleProps & + React.RefAttributes) + | (AccordionPrimitive.AccordionMultipleProps & + React.RefAttributes) +> & { + Item: React.FC; +} = ({ children, ...props }) => { + return ( + {children} + ); +}; + +const Item: React.FC = ({ + title, + subtitle, + description, + required, + tooltip, + children, + className, + complete, + headingSize = "large", + customTrigger = undefined, + forceMountContent = undefined, + active, + triggerable, + ...props +}) => { + return ( + + +
+
+
+
+ {complete ? ( + + ) : ( + <> + {active && ( + + )} + {!active && ( + + )} + + )} +
+ + {title} + +
+ + {customTrigger || } + +
+ {subtitle && ( + + {subtitle} + + )} +
+
+ +
+ {description && {description}} +
{children}
+
+
+
+ ); +}; + +Accordion.Item = Item; + +const MorphingTrigger = () => { + return ( +
+
+ + +
+
+ ); +}; + +export default Accordion; diff --git a/examples/backend/src/admin/components/shared/card.tsx b/examples/backend/src/admin/components/shared/card.tsx new file mode 100644 index 0000000..6d18169 --- /dev/null +++ b/examples/backend/src/admin/components/shared/card.tsx @@ -0,0 +1,27 @@ +import { Text, clx } from "@medusajs/ui" + +type CardProps = { + icon?: React.ReactNode + children?: React.ReactNode + className?: string +} + +const Card = ({ + icon, + children, + className +}: CardProps) => { + return ( +
+ {icon} + {children} +
+ ) +} + +export default Card \ No newline at end of file diff --git a/examples/backend/src/admin/components/shared/icons/active-circle-dotted-line.tsx b/examples/backend/src/admin/components/shared/icons/active-circle-dotted-line.tsx new file mode 100644 index 0000000..bab6888 --- /dev/null +++ b/examples/backend/src/admin/components/shared/icons/active-circle-dotted-line.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import IconProps from "../../../types/icon-type"; + +const ActiveCircleDottedLine: React.FC = ({ + size = "24", + color = "currentColor", + ...attributes +}) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ActiveCircleDottedLine; diff --git a/examples/backend/src/admin/components/shared/icons/get-started.tsx b/examples/backend/src/admin/components/shared/icons/get-started.tsx new file mode 100644 index 0000000..850b22f --- /dev/null +++ b/examples/backend/src/admin/components/shared/icons/get-started.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import IconProps from "../../../types/icon-type"; + +const GetStarted: React.FC = ({ + size = "40", + color = "currentColor", + ...attributes +}) => { + return ( + + + + + + + + + + + + ); +}; + +export default GetStarted; diff --git a/examples/backend/src/admin/types/icon-type.ts b/examples/backend/src/admin/types/icon-type.ts new file mode 100644 index 0000000..782681a --- /dev/null +++ b/examples/backend/src/admin/types/icon-type.ts @@ -0,0 +1,8 @@ +import React from "react" + +type IconProps = { + color?: string + size?: string | number +} & React.SVGAttributes + +export default IconProps diff --git a/examples/backend/src/admin/utils/prepare-region.ts b/examples/backend/src/admin/utils/prepare-region.ts new file mode 100644 index 0000000..6901912 --- /dev/null +++ b/examples/backend/src/admin/utils/prepare-region.ts @@ -0,0 +1,42 @@ +import { Store } from "@medusajs/medusa" +import type Medusa from "@medusajs/medusa-js" +import { ExtendedStoreDTO } from "@medusajs/medusa/dist/types/store" + +export default async function prepareRegions (client: Medusa) { + let { regions } = await client.admin.regions.list() + if (!regions.length) { + let { store } = await client.admin.store.retrieve() + if (!store.currencies) { + store = (await client.admin.store.update({ + currencies: ["eur"] + })).store as ExtendedStoreDTO + } + + regions = [(await client.admin.regions.create(getSampleRegion(store))).region] + } + + return regions +} + +function getSampleRegion (store: Store) { + return { + name: "EU", + currency_code: store.currencies[0].code, + tax_rate: 0, + payment_providers: [ + "manual" + ], + fulfillment_providers: [ + "manual" + ], + countries: [ + "gb", + "de", + "dk", + "se", + "fr", + "es", + "it" + ] + } +} \ No newline at end of file diff --git a/examples/backend/src/admin/utils/prepare-shipping-options.ts b/examples/backend/src/admin/utils/prepare-shipping-options.ts new file mode 100644 index 0000000..5ce35e6 --- /dev/null +++ b/examples/backend/src/admin/utils/prepare-shipping-options.ts @@ -0,0 +1,20 @@ +import { Region } from "@medusajs/medusa"; +import type Medusa from "@medusajs/medusa-js" + +export default async function prepareShippingOptions (client: Medusa, region: Region) { + let { shipping_options } = await client.admin.shippingOptions.list() + if (!shipping_options.length) { + shipping_options = [(await client.admin.shippingOptions.create({ + "name": "PostFake Standard", + "region_id": region.id, + "provider_id": "manual", + "data": { + "id": "manual-fulfillment" + }, + "price_type": "flat_rate", + "amount": 1000 + })).shipping_option] + } + + return shipping_options +} \ No newline at end of file diff --git a/examples/backend/src/admin/utils/sample-products.ts b/examples/backend/src/admin/utils/sample-products.ts new file mode 100644 index 0000000..edbe559 --- /dev/null +++ b/examples/backend/src/admin/utils/sample-products.ts @@ -0,0 +1,666 @@ +import { AdminPostProductsReq, Region } from "@medusajs/medusa" + +type SampleProductsOptions = { + regions: Region[] + collection_id?: string +} + +// can't use the ProductStatus imported +// from the core within admin cusotmizations +enum ProductStatus { + PUBLISHED = "published" +} + +export default function getSampleProducts ({ + regions, + collection_id +}: SampleProductsOptions): AdminPostProductsReq[] { + return [ + { + title: "Medusa T-Shirt", + status: ProductStatus.PUBLISHED, + collection_id, + discountable: true, + subtitle: null, + description: "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + handle: "medusa-t-shirt", + is_giftcard: false, + weight: 400, + images: [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png" + ], + options: [ + { + title: "Size", + }, + { + title: "Color", + } + ], + variants: [ + { + title: "S / Black", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2200 + } + }), + options: [ + { + value: "S" + }, + { + value: "Black" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "S / White", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2200 + } + }), + options: [ + { + value: "S" + }, + { + value: "White" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "M / Black", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2200 + } + }), + options: [ + { + value: "M" + }, + { + value: "Black" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "M / White", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2200 + } + }), + options: [ + { + value: "M" + }, + { + value: "White" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "L / Black", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2200 + } + }), + options: [ + { + value: "L" + }, + { + value: "Black" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "L / White", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2200 + } + }), + options: [ + { + value: "L" + }, + { + value: "White" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "XL / Black", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2200 + } + }), + options: [ + { + value: "XL" + }, + { + value: "Black" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "XL / White", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2200 + } + }), + options: [ + { + value: "XL" + }, + { + value: "White" + } + ], + inventory_quantity: 100, + manage_inventory: true + } + ] + }, + { + title: "Medusa Sweatshirt", + status: ProductStatus.PUBLISHED, + discountable: true, + collection_id, + subtitle: null, + description: "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + handle: "sweatshirt", + is_giftcard: false, + weight: 400, + images: [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png" + ], + options: [ + { + title: "Size", + } + ], + variants: [ + { + title: "S", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 3350 + } + }), + options: [ + { + value: "S" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "M", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 3350 + } + }), + options: [ + { + value: "M" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "L", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 3350 + } + }), + options: [ + { + value: "L" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "XL", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 3350 + } + }), + options: [ + { + value: "XL" + } + ], + inventory_quantity: 100, + manage_inventory: true + } + ] + }, + { + title: "Medusa Sweatpants", + status: ProductStatus.PUBLISHED, + discountable: true, + collection_id, + subtitle: null, + description: "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + handle: "sweatpants", + is_giftcard: false, + weight: 400, + images: [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png" + ], + options: [ + { + title: "Size", + } + ], + variants: [ + { + title: "S", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 3350 + } + }), + options: [ + { + value: "S" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "M", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 3350 + } + }), + options: [ + { + value: "M" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "L", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 3350 + } + }), + options: [ + { + value: "L" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "XL", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 3350 + } + }), + options: [ + { + value: "XL" + } + ], + inventory_quantity: 100, + manage_inventory: true + } + ] + }, + { + title: "Medusa Shorts", + status: ProductStatus.PUBLISHED, + discountable: true, + collection_id, + subtitle: null, + description: "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", + handle: "shorts", + is_giftcard: false, + weight: 400, + images: [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png" + ], + options: [ + { + title: "Size", + } + ], + variants: [ + { + title: "S", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2850 + } + }), + options: [ + { + value: "S" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "M", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2850 + } + }), + options: [ + { + value: "M" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "L", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2850 + } + }), + options: [ + { + value: "L" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "XL", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 2850 + } + }), + options: [ + { + value: "XL" + } + ], + inventory_quantity: 100, + manage_inventory: true + } + ] + }, + { + title: "Medusa Hoodie", + status: ProductStatus.PUBLISHED, + discountable: true, + collection_id, + subtitle: null, + description: "Reimagine the feeling of a classic hoodie. With our cotton hoodie, everyday essentials no longer have to be ordinary.", + handle: "hoodie", + is_giftcard: false, + weight: 400, + images: [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_back.png" + ], + options: [ + { + title: "Size", + } + ], + variants: [ + { + title: "S", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 4150 + } + }), + options: [ + { + value: "S" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "M", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 4150 + } + }), + options: [ + { + value: "M" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "L", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 4150 + } + }), + options: [ + { + value: "L" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "XL", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 4150 + } + }), + options: [ + { + value: "XL" + } + ], + inventory_quantity: 100, + manage_inventory: true + } + ] + }, + { + title: "Medusa Longsleeve", + status: ProductStatus.PUBLISHED, + discountable: true, + collection_id, + subtitle: null, + description: "Reimagine the feeling of a classic longsleeve. With our cotton longsleeve, everyday essentials no longer have to be ordinary.", + handle: "longsleeve", + is_giftcard: false, + weight: 400, + images: [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-front.png", + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-back.png" + ], + options: [ + { + title: "Size", + } + ], + variants: [ + { + title: "S", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 4150 + } + }), + options: [ + { + value: "S" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "M", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 4150 + } + }), + options: [ + { + value: "M" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "L", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 4150 + } + }), + options: [ + { + value: "L" + } + ], + inventory_quantity: 100, + manage_inventory: true + }, + { + title: "XL", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 4150 + } + }), + options: [ + { + value: "XL" + } + ], + inventory_quantity: 100, + manage_inventory: true + } + ] + }, + { + title: "Medusa Coffee Mug", + status: ProductStatus.PUBLISHED, + discountable: true, + collection_id, + subtitle: null, + description: "Every programmer's best friend.", + handle: "coffee-mug", + is_giftcard: false, + weight: 400, + images: [ + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png" + ], + options: [ + { + title: "Size", + } + ], + variants: [ + { + title: "One Size", + prices: regions.map((region) => { + return { + currency_code: region.currency_code, + amount: 1200 + } + }), + options: [ + { + value: "One Size" + } + ], + inventory_quantity: 100, + manage_inventory: true + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/backend/src/admin/widgets/onboarding-flow/onboarding-flow.tsx b/examples/backend/src/admin/widgets/onboarding-flow/onboarding-flow.tsx new file mode 100644 index 0000000..e2aa08f --- /dev/null +++ b/examples/backend/src/admin/widgets/onboarding-flow/onboarding-flow.tsx @@ -0,0 +1,502 @@ +import { OrderDetailsWidgetProps, ProductDetailsWidgetProps, WidgetConfig, WidgetProps } from "@medusajs/admin"; +import { useAdminCustomPost, useAdminCustomQuery, useMedusa } from "medusa-react"; +import React, { useEffect, useState, useMemo, useCallback } from "react"; +import { useNavigate, useSearchParams, useLocation } from "react-router-dom"; +import { OnboardingState } from "../../../models/onboarding"; +import { + AdminOnboardingUpdateStateReq, + OnboardingStateRes, + UpdateOnboardingStateInput, +} from "../../../types/onboarding"; +import OrderDetailDefault from "../../components/onboarding-flow/default/orders/order-detail"; +import OrdersListDefault from "../../components/onboarding-flow/default/orders/orders-list"; +import ProductDetailDefault from "../../components/onboarding-flow/default/products/product-detail"; +import ProductsListDefault from "../../components/onboarding-flow/default/products/products-list"; +import { Button, Container, Heading, Text, clx } from "@medusajs/ui"; +import Accordion from "../../components/shared/accordion"; +import GetStarted from "../../components/shared/icons/get-started"; +import { Order, Product } from "@medusajs/medusa"; +import ProductsListNextjs from "../../components/onboarding-flow/nextjs/products/products-list"; +import ProductDetailNextjs from "../../components/onboarding-flow/nextjs/products/product-detail"; +import OrdersListNextjs from "../../components/onboarding-flow/nextjs/orders/orders-list"; +import OrderDetailNextjs from "../../components/onboarding-flow/nextjs/orders/order-detail"; + +type STEP_ID = + | "create_product" + | "preview_product" + | "create_order" + | "setup_finished" + | "create_product_nextjs" + | "preview_product_nextjs" + | "create_order_nextjs" + | "setup_finished_nextjs" + +type OnboardingWidgetProps = WidgetProps | ProductDetailsWidgetProps | OrderDetailsWidgetProps + +export type StepContentProps = OnboardingWidgetProps & { + onNext?: Function; + isComplete?: boolean; + data?: OnboardingState; +}; + +type Step = { + id: STEP_ID; + title: string; + component: React.FC; + onNext?: Function; +}; + +const QUERY_KEY = ["onboarding_state"]; + +const OnboardingFlow = (props: OnboardingWidgetProps) => { + // create custom hooks for custom endpoints + const { data, isLoading } = useAdminCustomQuery< + undefined, + OnboardingStateRes + >("/onboarding", QUERY_KEY); + const { mutate } = useAdminCustomPost< + AdminOnboardingUpdateStateReq, + OnboardingStateRes + >("/onboarding", QUERY_KEY); + + const navigate = useNavigate(); + const location = useLocation(); + // will be used if onboarding step + // is passed as a path parameter + const { client } = useMedusa(); + + // get current step from custom endpoint + const currentStep: STEP_ID | undefined = useMemo(() => { + return data?.status + ?.current_step as STEP_ID + }, [data]); + + // initialize some state + const [openStep, setOpenStep] = useState(currentStep); + const [completed, setCompleted] = useState(false); + + // this method is used to move from one step to the next + const setStepComplete = ({ + step_id, + extraData, + onComplete, + }: { + step_id: STEP_ID; + extraData?: UpdateOnboardingStateInput; + onComplete?: () => void; + }) => { + const next = steps[findStepIndex(step_id) + 1]; + mutate({ current_step: next.id, ...extraData }, { + onSuccess: onComplete + }); + }; + + // this is useful if you want to change the current step + // using a path parameter. It can only be changed if the passed + // step in the path parameter is the next step. + const [ searchParams ] = useSearchParams() + + // the steps are set based on the + // onboarding type + const steps: Step[] = useMemo(() => { + { + switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) { + case 'nextjs': + return [ + { + id: "create_product_nextjs", + title: "Create Products", + component: ProductsListNextjs, + onNext: (product: Product) => { + setStepComplete({ + step_id: "create_product_nextjs", + extraData: { product_id: product.id }, + onComplete: () => { + if (!location.pathname.startsWith(`/a/products/${product.id}`)) { + navigate(`/a/products/${product.id}`) + } + }, + }); + }, + }, + { + id: "preview_product_nextjs", + title: "Preview Product in your Next.js Storefront", + component: ProductDetailNextjs, + onNext: () => { + setStepComplete({ + step_id: "preview_product_nextjs", + onComplete: () => navigate(`/a/orders`), + }); + }, + }, + { + id: "create_order_nextjs", + title: "Create an Order using your Next.js Storefront", + component: OrdersListNextjs, + onNext: (order: Order) => { + setStepComplete({ + step_id: "create_order_nextjs", + onComplete: () => { + if (!location.pathname.startsWith(`/a/orders/${order.id}`)) { + navigate(`/a/orders/${order.id}`) + } + }, + }); + }, + }, + { + id: "setup_finished_nextjs", + title: "Setup Finished: Continue Building your Ecommerce Store", + component: OrderDetailNextjs, + }, + ] + default: + return [ + { + id: "create_product", + title: "Create Product", + component: ProductsListDefault, + onNext: (product: Product) => { + setStepComplete({ + step_id: "create_product", + extraData: { product_id: product.id }, + onComplete: () => { + if (!location.pathname.startsWith(`/a/products/${product.id}`)) { + navigate(`/a/products/${product.id}`) + } + }, + }); + }, + }, + { + id: "preview_product", + title: "Preview Product", + component: ProductDetailDefault, + onNext: () => { + setStepComplete({ + step_id: "preview_product", + onComplete: () => navigate(`/a/orders`), + }); + }, + }, + { + id: "create_order", + title: "Create an Order", + component: OrdersListDefault, + onNext: (order: Order) => { + setStepComplete({ + step_id: "create_order", + onComplete: () => { + if (!location.pathname.startsWith(`/a/orders/${order.id}`)) { + navigate(`/a/orders/${order.id}`) + } + }, + }); + }, + }, + { + id: "setup_finished", + title: "Setup Finished: Start developing with Medusa", + component: OrderDetailDefault, + }, + ] + } + } + }, [location.pathname]) + + // used to retrieve the index of a step by its ID + const findStepIndex = useCallback((step_id: STEP_ID) => { + return steps.findIndex((step) => step.id === step_id) + }, [steps]) + + // used to check if a step is completed + const isStepComplete = useCallback((step_id: STEP_ID) => { + return findStepIndex(currentStep) > findStepIndex(step_id) + }, [findStepIndex, currentStep]); + + // this is used to retrieve the data necessary + // to move to the next onboarding step + const getOnboardingParamStepData = useCallback(async (onboardingStep: string, data?: { + orderId?: string, + productId?: string, + }) => { + switch (onboardingStep) { + case "setup_finished_nextjs": + case "setup_finished": + if (!data?.orderId && "order" in props) { + return props.order + } + const orderId = data?.orderId || searchParams.get("order_id") + if (orderId) { + return (await client.admin.orders.retrieve(orderId)).order + } + + throw new Error ("Required `order_id` parameter was not passed as a parameter") + case "preview_product_nextjs": + case "preview_product": + if (!data?.productId && "product" in props) { + return props.product + } + const productId = data?.productId || searchParams.get("product_id") + if (productId) { + return (await client.admin.products.retrieve(productId)).product + } + + throw new Error ("Required `product_id` parameter was not passed as a parameter") + default: + return undefined + } + }, [searchParams, props]) + + const isProductCreateStep = useMemo(() => { + return currentStep === "create_product" || + currentStep === "create_product_nextjs" + }, [currentStep]) + + const isOrderCreateStep = useMemo(() => { + return currentStep === "create_order" || + currentStep === "create_order_nextjs" + }, [currentStep]) + + // used to change the open step when the current + // step is retrieved from custom endpoints + useEffect(() => { + setOpenStep(currentStep); + + if (findStepIndex(currentStep) === steps.length - 1) setCompleted(true); + }, [currentStep, findStepIndex]); + + // used to check if the user created a product and has entered its details page + // the step is changed to the next one + useEffect(() => { + if (location.pathname.startsWith("/a/products/prod_") && isProductCreateStep && "product" in props) { + // change to the preview product step + const currentStepIndex = findStepIndex(currentStep) + steps[currentStepIndex].onNext?.(props.product) + } + }, [location.pathname, isProductCreateStep]) + + // used to check if the user created an order and has entered its details page + // the step is changed to the next one. + useEffect(() => { + if (location.pathname.startsWith("/a/orders/order_") && isOrderCreateStep && "order" in props) { + // change to the preview product step + const currentStepIndex = findStepIndex(currentStep) + steps[currentStepIndex].onNext?.(props.order) + } + }, [location.pathname, isOrderCreateStep]) + + // used to check if the `onboarding_step` path + // parameter is passed and, if so, moves to that step + // only if it's the next step and its necessary data is passed + useEffect(() => { + const onboardingStep = searchParams.get("onboarding_step") as STEP_ID + const onboardingStepIndex = findStepIndex(onboardingStep) + if (onboardingStep && onboardingStepIndex !== -1 && onboardingStep !== openStep) { + // change current step to the onboarding step + const openStepIndex = findStepIndex(openStep) + + if (onboardingStepIndex !== openStepIndex + 1) { + // can only go forward one step + return + } + + // retrieve necessary data and trigger the next function + getOnboardingParamStepData(onboardingStep) + .then((data) => { + steps[openStepIndex].onNext?.(data) + }) + .catch((e) => console.error(e)) + } + }, [searchParams, openStep, getOnboardingParamStepData]) + + if ( + !isLoading && + data?.status?.is_complete && + !localStorage.getItem("override_onboarding_finish") + ) + return null; + + // a method that will be triggered when + // the setup is started + const onStart = () => { + mutate({ current_step: steps[0].id }); + navigate(`/a/products`); + }; + + // a method that will be triggered when + // the setup is completed + const onComplete = () => { + setCompleted(true); + }; + + // a method that will be triggered when + // the setup is closed + const onHide = () => { + mutate({ is_complete: true }); + }; + + // used to get text for get started header + const getStartedText = () => { + switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) { + case "nextjs": + return "Learn the basics of Medusa by creating your first order using the Next.js storefront." + default: + return "Learn the basics of Medusa by creating your first order." + } + } + + return ( + <> + + setOpenStep(value as STEP_ID)} + > +
+
+ +
+ {!completed ? ( + <> +
+ Get started + + {getStartedText()} + +
+
+ {!!currentStep ? ( + <> + {currentStep === steps[steps.length - 1].id ? ( + + ) : ( + + )} + + ) : ( + <> + + + + )} +
+ + ) : ( + <> +
+ + Thank you for completing the setup guide! + + + This whole experience was built using our new{" "} + widgets feature. +
You can find out more details and build your own by + following{" "} + + our guide + + . +
+
+
+ +
+ + )} +
+ { +
+ {(!completed ? steps : steps.slice(-1)).map((step) => { + const isComplete = isStepComplete(step.id); + const isCurrent = currentStep === step.id; + return ( + , + })} + > +
+ +
+
+ ); + })} +
+ } +
+
+ + ); +}; + +export const config: WidgetConfig = { + zone: [ + "product.list.before", + "product.details.before", + "order.list.before", + "order.details.before", + ], +}; + +export default OnboardingFlow; diff --git a/examples/backend/src/api/README.md b/examples/backend/src/api/README.md new file mode 100644 index 0000000..5b91e50 --- /dev/null +++ b/examples/backend/src/api/README.md @@ -0,0 +1,179 @@ +# Custom API Routes + +You may define custom API Routes by putting files in the `/api` directory that export functions returning an express router or a collection of express routers. +Medusa supports adding custom API Routes using a file based approach. This means that you can add files in the `/api` directory and the files path will be used as the API Route path. For example, if you add a file called `/api/store/custom/route.ts` it will be available on the `/store/custom` API Route. + +```ts +import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; + +export async function GET(req: MedusaRequest, res: MedusaResponse) { + res.json({ + message: "Hello world!", + }); +} +``` + +## Supported HTTP methods + +The file based routing supports the following HTTP methods: + +- GET +- POST +- PUT +- PATCH +- DELETE +- OPTIONS +- HEAD + +You can define a handler for each of these methods by exporting a function with the name of the method in the paths `route.ts` file. For example, if you want to define a handler for the `GET`, `POST`, and `PUT` methods, you can do so by exporting functions with the names `GET`, `POST`, and `PUT`: + +```ts +import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; + +export async function GET(req: MedusaRequest, res: MedusaResponse) { + // Handle GET requests +} + +export async function POST(req: MedusaRequest, res: MedusaResponse) { + // Handle POST requests +} + +export async function PUT(req: MedusaRequest, res: MedusaResponse) { + // Handle PUT requests +} +``` + +## Parameters + +You can define parameters in the path of your route by using wrapping the parameter name in square brackets. For example, if you want to define a route that takes a `productId` parameter, you can do so by creating a file called `/api/products/[productId]/route.ts`: + +```ts +import type { + MedusaRequest, + MedusaResponse, + ProductService, +} from "@medusajs/medusa"; + +export async function GET(req: MedusaRequest, res: MedusaResponse) { + const { productId } = req.params; + + const productService: ProductService = req.scope.resolve("productService"); + + const product = await productService.retrieve(productId); + + res.json({ + product, + }); +} +``` + +If you want to define a route that takes multiple parameters, you can do so by adding multiple parameters in the path. It is important that each parameter is given a unique name. For example, if you want to define a route that takes both a `productId` and a `variantId` parameter, you can do so by creating a file called `/api/products/[productId]/variants/[variantId]/route.ts`. Duplicate parameter names are not allowed, and will result in an error. + +## Using the container + +A global container is available on `req.scope` to allow you to use any of the registered services from the core, installed plugins or your local project: + +```ts +import type { + MedusaRequest, + MedusaResponse, + ProductService, +} from "@medusajs/medusa"; + +export async function GET(req: MedusaRequest, res: MedusaResponse) { + const productService: ProductService = req.scope.resolve("productService"); + + const products = await productService.list(); + + res.json({ + products, + }); +} +``` + +## Middleware + +You can apply middleware to your routes by creating a file called `/api/middlewares.ts`. This file should export a configuration object with what middleware you want to apply to which routes. For example, if you want to apply a custom middleware function to the `/store/custom` route, you can do so by adding the following to your `/api/middlewares.ts` file: + +```ts +import type { + MiddlewaresConfig, + MedusaRequest, + MedusaResponse, + MedusaNextFunction, +} from "@medusajs/medusa"; + +async function logger( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction +) { + console.log("Request received"); + next(); +} + +export const config: MiddlewaresConfig = { + routes: [ + { + matcher: "/store/custom", + middlewares: [logger], + }, + ], +}; +``` + +The `matcher` property can be either a string or a regular expression. The `middlewares` property accepts an array of middleware functions. + +You might only want to apply middleware to certain HTTP methods. You can do so by adding a `method` property to the route configuration object: + +```ts +export const config: MiddlewaresConfig = { + routes: [ + { + matcher: "/store/custom", + method: "GET", + middlewares: [logger], + }, + ], +}; +``` + +The `method` property can be either a HTTP method or an array of HTTP methods. By default the middlewares will apply to all HTTP methods for the given `matcher`. + +### Default middleware + +Some middleware functions are applied per default: + +#### Global middleware + +JSON parsing is applied to all routes. This means that you can access the request body as `req.body` and it will be parsed as JSON, if the request has a `Content-Type` header of `application/json`. + +If you want to use a different parser for a specific route, such as `urlencoded`, you can do so by adding the following export to your `route.ts` file: + +```ts +import { urlencoded } from "express"; + +export const config: MiddlewaresConfig = { + routes: [ + { + method: "POST", + matcher: "/store/custom", + middlewares: [urlencoded()], + }, + ], +}; +``` + +#### Store middleware + +For all `/store` routes, the appropriate CORS settings are applied. The STORE_CORS value can be configured in your `medusa-config.js` file. + +#### Admin middleware + +For all `/admin` routes, the appropriate CORS settings are applied. The ADMIN_CORS value can be configured in your `medusa-config.js` file. + +All `/admin` routes also have admin authentication applied per default. If you want to disable this for a specific route, you can do so by adding the following export to your `route.ts` file: + +```ts +export const AUTHENTICATE = false; +``` diff --git a/examples/backend/src/api/admin/custom/route.ts b/examples/backend/src/api/admin/custom/route.ts new file mode 100644 index 0000000..708bcb4 --- /dev/null +++ b/examples/backend/src/api/admin/custom/route.ts @@ -0,0 +1,8 @@ +import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +): Promise { + res.sendStatus(200); +} diff --git a/examples/backend/src/api/admin/onboarding/route.ts b/examples/backend/src/api/admin/onboarding/route.ts new file mode 100644 index 0000000..9a5d990 --- /dev/null +++ b/examples/backend/src/api/admin/onboarding/route.ts @@ -0,0 +1,27 @@ +import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; +import { EntityManager } from "typeorm"; + +import OnboardingService from "../../../services/onboarding"; + +export async function GET(req: MedusaRequest, res: MedusaResponse) { + const onboardingService: OnboardingService = + req.scope.resolve("onboardingService"); + + const status = await onboardingService.retrieve(); + + res.status(200).json({ status }); +} + +export async function POST(req: MedusaRequest, res: MedusaResponse) { + const onboardingService: OnboardingService = + req.scope.resolve("onboardingService"); + const manager: EntityManager = req.scope.resolve("manager"); + + const status = await manager.transaction(async (transactionManager) => { + return await onboardingService + .withTransaction(transactionManager) + .update(req.body); + }); + + res.status(200).json({ status }); +} diff --git a/examples/backend/src/api/store/custom/route.ts b/examples/backend/src/api/store/custom/route.ts new file mode 100644 index 0000000..708bcb4 --- /dev/null +++ b/examples/backend/src/api/store/custom/route.ts @@ -0,0 +1,8 @@ +import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +): Promise { + res.sendStatus(200); +} diff --git a/examples/backend/src/jobs/README.md b/examples/backend/src/jobs/README.md new file mode 100644 index 0000000..45917c5 --- /dev/null +++ b/examples/backend/src/jobs/README.md @@ -0,0 +1,32 @@ +# Custom scheduled jobs + +You may define custom scheduled jobs (cron jobs) by creating files in the `/jobs` directory. + +```ts +import { + ProductService, + ScheduledJobArgs, + ScheduledJobConfig, +} from "@medusajs/medusa"; + +export default async function myCustomJob({ container }: ScheduledJobArgs) { + const productService: ProductService = container.resolve("productService"); + + const products = await productService.listAndCount(); + + // Do something with the products +} + +export const config: ScheduledJobConfig = { + name: "daily-product-report", + schedule: "0 0 * * *", // Every day at midnight +}; +``` + +A scheduled job is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when the job is scheduled. The `config` is an object which defines the name of the job, the schedule, and an optional data object. + +The `handler` is a function which takes one parameter, an `object` of type `ScheduledJobArgs` with the following properties: + +- `container` - a `MedusaContainer` instance which can be used to resolve services. +- `data` - an `object` containing data passed to the job when it was scheduled. This object is passed in the `config` object. +- `pluginOptions` - an `object` containing plugin options, if the job is defined in a plugin. diff --git a/examples/backend/src/loaders/README.md b/examples/backend/src/loaders/README.md new file mode 100644 index 0000000..a5dcfd2 --- /dev/null +++ b/examples/backend/src/loaders/README.md @@ -0,0 +1,19 @@ +# Custom loader + +The loader allows you have access to the Medusa service container. This allows you to access the database and the services registered on the container. +you can register custom registrations in the container or run custom code on startup. + +```ts +// src/loaders/my-loader.ts + +import { AwilixContainer } from 'awilix' + +/** + * + * @param container The container in which the registrations are made + * @param config The options of the plugin or the entire config object + */ +export default (container: AwilixContainer, config: Record): void | Promise => { + /* Implement your own loader. */ +} +``` \ No newline at end of file diff --git a/examples/backend/src/migrations/1685715079776-CreateOnboarding.ts b/examples/backend/src/migrations/1685715079776-CreateOnboarding.ts new file mode 100644 index 0000000..05e0972 --- /dev/null +++ b/examples/backend/src/migrations/1685715079776-CreateOnboarding.ts @@ -0,0 +1,21 @@ +import { generateEntityId } from "@medusajs/utils"; +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateOnboarding1685715079776 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "onboarding_state" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "current_step" character varying NULL, "is_complete" boolean)` + ); + + await queryRunner.query( + `INSERT INTO "onboarding_state" ("id", "current_step", "is_complete") VALUES ('${generateEntityId( + "", + "onboarding" + )}' , NULL, false)` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "onboarding_state"`); + } +} diff --git a/examples/backend/src/migrations/1686062614694-AddOnboardingProduct.ts b/examples/backend/src/migrations/1686062614694-AddOnboardingProduct.ts new file mode 100644 index 0000000..66706f2 --- /dev/null +++ b/examples/backend/src/migrations/1686062614694-AddOnboardingProduct.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddOnboardingProduct1686062614694 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "onboarding_state" ADD COLUMN "product_id" character varying NULL` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "onboarding_state" DROP COLUMN "product_id"` + ); + } +} diff --git a/examples/backend/src/migrations/1690996567455-CorrectOnboardingFields.ts b/examples/backend/src/migrations/1690996567455-CorrectOnboardingFields.ts new file mode 100644 index 0000000..bc145c1 --- /dev/null +++ b/examples/backend/src/migrations/1690996567455-CorrectOnboardingFields.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CorrectOnboardingFields1690996567455 implements MigrationInterface { + name = 'CorrectOnboardingFields1690996567455' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "onboarding_state" ADD CONSTRAINT "PK_891b72628471aada55d7b8c9410" PRIMARY KEY ("id")`); + await queryRunner.query(`ALTER TABLE "onboarding_state" ALTER COLUMN "is_complete" SET NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "onboarding_state" ALTER COLUMN "is_complete" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "onboarding_state" DROP CONSTRAINT "PK_891b72628471aada55d7b8c9410"`); + } + +} diff --git a/examples/backend/src/migrations/README.md b/examples/backend/src/migrations/README.md new file mode 100644 index 0000000..6964714 --- /dev/null +++ b/examples/backend/src/migrations/README.md @@ -0,0 +1,29 @@ +# Custom migrations + +You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`. +In that case you also need to provide a migration in order to create the table in the database. + +## Example + +### 1. Create the migration + +See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation. + +```ts +// src/migration/my-migration.ts + +import { MigrationInterface, QueryRunner } from "typeorm" + +export class MyMigration1617703530229 implements MigrationInterface { + name = "myMigration1617703530229" + + public async up(queryRunner: QueryRunner): Promise { + // write you migration here + } + + public async down(queryRunner: QueryRunner): Promise { + // write you migration here + } +} + +``` \ No newline at end of file diff --git a/examples/backend/src/models/README.md b/examples/backend/src/models/README.md new file mode 100644 index 0000000..3087242 --- /dev/null +++ b/examples/backend/src/models/README.md @@ -0,0 +1,46 @@ +# Custom models + +You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`. + +## Example + +### 1. Create the Entity + +```ts +// src/models/post.ts + +import { BeforeInsert, Column, Entity, PrimaryColumn } from "typeorm"; +import { generateEntityId } from "@medusajs/utils"; +import { BaseEntity } from "@medusajs/medusa"; + +@Entity() +export class Post extends BaseEntity { + @Column({type: 'varchar'}) + title: string | null; + + @BeforeInsert() + private beforeInsert(): void { + this.id = generateEntityId(this.id, "post") + } +} +``` + +### 2. Create the Migration + +You also need to create a Migration to create the new table in the database. See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation. + +### 3. Create a Repository +Entities data can be easily accessed and modified using [TypeORM Repositories](https://typeorm.io/working-with-repository). To create a repository, create a file in `src/repositories`. For example, here’s a repository `PostRepository` for the `Post` entity: + +```ts +// src/repositories/post.ts + +import { EntityRepository, Repository } from "typeorm" + +import { Post } from "../models/post" + +@EntityRepository(Post) +export class PostRepository extends Repository { } +``` + +See more about defining and accesing your custom [Entities](https://docs.medusajs.com/advanced/backend/entities/overview) in the documentation. \ No newline at end of file diff --git a/examples/backend/src/models/onboarding.ts b/examples/backend/src/models/onboarding.ts new file mode 100644 index 0000000..af7893f --- /dev/null +++ b/examples/backend/src/models/onboarding.ts @@ -0,0 +1,14 @@ +import { BaseEntity } from "@medusajs/medusa"; +import { Column, Entity } from "typeorm"; + +@Entity() +export class OnboardingState extends BaseEntity { + @Column({ nullable: true }) + current_step: string; + + @Column() + is_complete: boolean; + + @Column({ nullable: true }) + product_id: string; +} diff --git a/examples/backend/src/repositories/onboarding.ts b/examples/backend/src/repositories/onboarding.ts new file mode 100644 index 0000000..5ff6124 --- /dev/null +++ b/examples/backend/src/repositories/onboarding.ts @@ -0,0 +1,6 @@ +import { dataSource } from "@medusajs/medusa/dist/loaders/database"; +import { OnboardingState } from "../models/onboarding"; + +const OnboardingRepository = dataSource.getRepository(OnboardingState); + +export default OnboardingRepository; diff --git a/examples/backend/src/services/README.md b/examples/backend/src/services/README.md new file mode 100644 index 0000000..941ac35 --- /dev/null +++ b/examples/backend/src/services/README.md @@ -0,0 +1,49 @@ +# Custom services + +You may define custom services that will be registered on the global container by creating files in the `/services` directory that export an instance of `BaseService`. + +```ts +// src/services/my-custom.ts + +import { Lifetime } from "awilix" +import { TransactionBaseService } from "@medusajs/medusa"; +import { IEventBusService } from "@medusajs/types"; + +export default class MyCustomService extends TransactionBaseService { + static LIFE_TIME = Lifetime.SCOPED + protected readonly eventBusService_: IEventBusService + + constructor( + { eventBusService }: { eventBusService: IEventBusService }, + options: Record + ) { + // @ts-ignore + super(...arguments) + + this.eventBusService_ = eventBusService + } +} + +``` + +The first argument to the `constructor` is the global giving you access to easy dependency injection. The container holds all registered services from the core, installed plugins and from other files in the `/services` directory. The registration name is a camelCased version of the file name with the type appended i.e.: `my-custom.js` is registered as `myCustomService`, `custom-thing.js` is registered as `customThingService`. + +You may use the services you define here in custom endpoints by resolving the services defined. + +```js +import { Router } from "express" + +export default () => { + const router = Router() + + router.get("/hello-product", async (req, res) => { + const myService = req.scope.resolve("myCustomService") + + res.json({ + message: await myService.getProductMessage() + }) + }) + + return router; +} +``` diff --git a/examples/backend/src/services/__tests__/test-service.spec.ts b/examples/backend/src/services/__tests__/test-service.spec.ts new file mode 100644 index 0000000..82ef916 --- /dev/null +++ b/examples/backend/src/services/__tests__/test-service.spec.ts @@ -0,0 +1,5 @@ +describe('MyService', () => { + it('should do this', async () => { + expect(true).toBe(true) + }) +}) diff --git a/examples/backend/src/services/onboarding.ts b/examples/backend/src/services/onboarding.ts new file mode 100644 index 0000000..921fdb8 --- /dev/null +++ b/examples/backend/src/services/onboarding.ts @@ -0,0 +1,52 @@ +import { TransactionBaseService } from "@medusajs/medusa"; +import OnboardingRepository from "../repositories/onboarding"; +import { OnboardingState } from "../models/onboarding"; +import { EntityManager, IsNull, Not } from "typeorm"; +import { UpdateOnboardingStateInput } from "../types/onboarding"; + +type InjectedDependencies = { + manager: EntityManager; + onboardingRepository: typeof OnboardingRepository; +}; + +class OnboardingService extends TransactionBaseService { + protected onboardingRepository_: typeof OnboardingRepository; + + constructor({ onboardingRepository }: InjectedDependencies) { + super(arguments[0]); + + this.onboardingRepository_ = onboardingRepository; + } + + async retrieve(): Promise { + const onboardingRepo = this.activeManager_.withRepository( + this.onboardingRepository_ + ); + + const status = await onboardingRepo.findOne({ + where: { id: Not(IsNull()) }, + }); + + return status; + } + + async update(data: UpdateOnboardingStateInput): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const onboardingRepository = transactionManager.withRepository( + this.onboardingRepository_ + ); + + const status = await this.retrieve(); + + for (const [key, value] of Object.entries(data)) { + status[key] = value; + } + + return await onboardingRepository.save(status); + } + ); + } +} + +export default OnboardingService; diff --git a/examples/backend/src/subscribers/README.md b/examples/backend/src/subscribers/README.md new file mode 100644 index 0000000..1e4333f --- /dev/null +++ b/examples/backend/src/subscribers/README.md @@ -0,0 +1,44 @@ +# Custom subscribers + +You may define custom eventhandlers, `subscribers` by creating files in the `/subscribers` directory. + +```ts +import MyCustomService from "../services/my-custom"; +import { + OrderService, + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/medusa"; + +type OrderPlacedEvent = { + id: string; + no_notification: boolean; +}; + +export default async function orderPlacedHandler({ + data, + eventName, + container, +}: SubscriberArgs) { + const orderService: OrderService = container.resolve(OrderService); + + const order = await orderService.retrieve(data.id, { + relations: ["items", "items.variant", "items.variant.product"], + }); + + // Do something with the order +} + +export const config: SubscriberConfig = { + event: OrderService.Events.PLACED, +}; +``` + +A subscriber is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when an event is emitted. The `config` is an object which defines which event(s) the subscriber should subscribe to. + +The `handler` is a function which takes one parameter, an `object` of type `SubscriberArgs` with the following properties: + +- `data` - an `object` of type `T` containing information about the event. +- `eventName` - a `string` containing the name of the event. +- `container` - a `MedusaContainer` instance which can be used to resolve services. +- `pluginOptions` - an `object` containing plugin options, if the subscriber is defined in a plugin. diff --git a/examples/backend/src/types/onboarding.ts b/examples/backend/src/types/onboarding.ts new file mode 100644 index 0000000..d96d6ce --- /dev/null +++ b/examples/backend/src/types/onboarding.ts @@ -0,0 +1,13 @@ +import { OnboardingState } from "../models/onboarding"; + +export type UpdateOnboardingStateInput = { + current_step?: string; + is_complete?: boolean; + product_id?: string; +}; + +export interface AdminOnboardingUpdateStateReq {} + +export type OnboardingStateRes = { + status: OnboardingState; +}; diff --git a/examples/backend/tsconfig.admin.json b/examples/backend/tsconfig.admin.json new file mode 100644 index 0000000..b109ee6 --- /dev/null +++ b/examples/backend/tsconfig.admin.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext" + }, + "include": ["src/admin"], + "exclude": ["**/*.spec.js"] +} diff --git a/examples/backend/tsconfig.json b/examples/backend/tsconfig.json new file mode 100644 index 0000000..05ae513 --- /dev/null +++ b/examples/backend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es2019", + "allowJs": true, + "esModuleInterop": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "declaration": true, + "sourceMap": false, + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": ".", + "jsx": "react-jsx", + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "checkJs": false + }, + "include": ["src/"], + "exclude": [ + "**/__tests__", + "**/__fixtures__", + "node_modules", + "build", + ".cache" + ] +} diff --git a/examples/backend/tsconfig.server.json b/examples/backend/tsconfig.server.json new file mode 100644 index 0000000..94a32ac --- /dev/null +++ b/examples/backend/tsconfig.server.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + /* Emit a single file with source maps instead of having a separate file. */ + "inlineSourceMap": true + }, + "exclude": ["src/admin", "**/*.spec.js"] +} diff --git a/examples/backend/tsconfig.spec.json b/examples/backend/tsconfig.spec.json new file mode 100644 index 0000000..6876481 --- /dev/null +++ b/examples/backend/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} From f77171a96f508e77780e44806bfef7f6c755db24 Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Fri, 26 Jan 2024 02:05:42 +0000 Subject: [PATCH 2/3] feat: add paystack integration --- examples/backend/.env.template | 3 + examples/backend/data/seed-onboarding.json | 141 --- examples/backend/data/seed.json | 886 +----------------- examples/backend/medusa-config.js | 13 + examples/backend/package.json | 1 + .../modules/home/components/hero/index.tsx | 10 +- 6 files changed, 55 insertions(+), 999 deletions(-) delete mode 100644 examples/backend/data/seed-onboarding.json diff --git a/examples/backend/.env.template b/examples/backend/.env.template index 68fe3ef..b145695 100644 --- a/examples/backend/.env.template +++ b/examples/backend/.env.template @@ -3,3 +3,6 @@ COOKIE_SECRET=something DATABASE_TYPE="postgres" REDIS_URL=redis://localhost:6379 + +# Your Paystack secret key. See – https://support.paystack.com/hc/en-us/articles/360009881600-Paystack-Test-Keys-Live-Keys-and-Webhooks +PAYSTACK_SECRET_KEY=sk_something diff --git a/examples/backend/data/seed-onboarding.json b/examples/backend/data/seed-onboarding.json deleted file mode 100644 index 4641c39..0000000 --- a/examples/backend/data/seed-onboarding.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "store": { - "currencies": [ - "eur", - "usd" - ] - }, - "users": [], - "regions": [ - { - "id": "test-region-eu", - "name": "EU", - "currency_code": "eur", - "tax_rate": 0, - "payment_providers": [ - "manual" - ], - "fulfillment_providers": [ - "manual" - ], - "countries": [ - "gb", - "de", - "dk", - "se", - "fr", - "es", - "it" - ] - }, - { - "id": "test-region-na", - "name": "NA", - "currency_code": "usd", - "tax_rate": 0, - "payment_providers": [ - "manual" - ], - "fulfillment_providers": [ - "manual" - ], - "countries": [ - "us", - "ca" - ] - } - ], - "shipping_options": [ - { - "name": "PostFake Standard", - "region_id": "test-region-eu", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "amount": 1000 - }, - { - "name": "PostFake Express", - "region_id": "test-region-eu", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "amount": 1500 - }, - { - "name": "PostFake Return", - "region_id": "test-region-eu", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "is_return": true, - "amount": 1000 - }, - { - "name": "I want to return it myself", - "region_id": "test-region-eu", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "is_return": true, - "amount": 0 - }, - { - "name": "FakeEx Standard", - "region_id": "test-region-na", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "amount": 800 - }, - { - "name": "FakeEx Express", - "region_id": "test-region-na", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "amount": 1200 - }, - { - "name": "FakeEx Return", - "region_id": "test-region-na", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "is_return": true, - "amount": 800 - }, - { - "name": "I want to return it myself", - "region_id": "test-region-na", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "is_return": true, - "amount": 0 - } - ], - "products": [], - "categories": [], - "publishable_api_keys": [ - { - "title": "Development" - } - ] -} \ No newline at end of file diff --git a/examples/backend/data/seed.json b/examples/backend/data/seed.json index b99f3a3..384a4af 100644 --- a/examples/backend/data/seed.json +++ b/examples/backend/data/seed.json @@ -1,9 +1,7 @@ { "store": { - "currencies": [ - "eur", - "usd" - ] + "currencies": ["ghs"], + "default_currency_code": "ghs" }, "users": [ { @@ -13,691 +11,44 @@ ], "regions": [ { - "id": "test-region-eu", - "name": "EU", - "currency_code": "eur", - "tax_rate": 0, - "payment_providers": [ - "manual" - ], - "fulfillment_providers": [ - "manual" - ], - "countries": [ - "gb", - "de", - "dk", - "se", - "fr", - "es", - "it" - ] - }, - { - "id": "test-region-na", - "name": "NA", - "currency_code": "usd", - "tax_rate": 0, - "payment_providers": [ - "manual" - ], - "fulfillment_providers": [ - "manual" - ], - "countries": [ - "us", - "ca" - ] + "id": "test-region-gh", + "name": "Ghana", + "currency_code": "ghs", + "tax_rate": 0.125, + "payment_providers": ["paystack", "manual"], + "fulfillment_providers": ["manual"], + "countries": ["gh"] } ], "shipping_options": [ { - "name": "PostFake Standard", - "region_id": "test-region-eu", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "amount": 1000 - }, - { - "name": "PostFake Express", - "region_id": "test-region-eu", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "amount": 1500 - }, - { - "name": "PostFake Return", - "region_id": "test-region-eu", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "is_return": true, - "amount": 1000 - }, - { - "name": "I want to return it myself", - "region_id": "test-region-eu", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "is_return": true, - "amount": 0 - }, - { - "name": "FakeEx Standard", - "region_id": "test-region-na", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "amount": 800 - }, - { - "name": "FakeEx Express", - "region_id": "test-region-na", - "provider_id": "manual", - "data": { - "id": "manual-fulfillment" - }, - "price_type": "flat_rate", - "amount": 1200 - }, - { - "name": "FakeEx Return", - "region_id": "test-region-na", + "name": "GhanaPost Standard", + "region_id": "test-region-gh", "provider_id": "manual", "data": { "id": "manual-fulfillment" }, "price_type": "flat_rate", - "is_return": true, - "amount": 800 + "amount": 4500 }, { - "name": "I want to return it myself", - "region_id": "test-region-na", + "name": "GhanaPost Return", + "region_id": "test-region-gh", "provider_id": "manual", "data": { "id": "manual-fulfillment" }, "price_type": "flat_rate", "is_return": true, - "amount": 0 + "amount": 4500 } ], "products": [ - { - "title": "Medusa T-Shirt", - "categories": [ - { - "id": "pcat_shirts" - } - ], - "subtitle": null, - "description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", - "handle": "t-shirt", - "is_giftcard": false, - "weight": 400, - "images": [ - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png" - ], - "options": [ - { - "title": "Size", - "values": [ - "S", - "M", - "L", - "XL" - ] - }, - { - "title": "Color", - "values": [ - "Black", - "White" - ] - } - ], - "variants": [ - { - "title": "S / Black", - "prices": [ - { - "currency_code": "eur", - "amount": 1950 - }, - { - "currency_code": "usd", - "amount": 2200 - } - ], - "options": [ - { - "value": "S" - }, - { - "value": "Black" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "S / White", - "prices": [ - { - "currency_code": "eur", - "amount": 1950 - }, - { - "currency_code": "usd", - "amount": 2200 - } - ], - "options": [ - { - "value": "S" - }, - { - "value": "White" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "M / Black", - "prices": [ - { - "currency_code": "eur", - "amount": 1950 - }, - { - "currency_code": "usd", - "amount": 2200 - } - ], - "options": [ - { - "value": "M" - }, - { - "value": "Black" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "M / White", - "prices": [ - { - "currency_code": "eur", - "amount": 1950 - }, - { - "currency_code": "usd", - "amount": 2200 - } - ], - "options": [ - { - "value": "M" - }, - { - "value": "White" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "L / Black", - "prices": [ - { - "currency_code": "eur", - "amount": 1950 - }, - { - "currency_code": "usd", - "amount": 2200 - } - ], - "options": [ - { - "value": "L" - }, - { - "value": "Black" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "L / White", - "prices": [ - { - "currency_code": "eur", - "amount": 1950 - }, - { - "currency_code": "usd", - "amount": 2200 - } - ], - "options": [ - { - "value": "L" - }, - { - "value": "White" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "XL / Black", - "prices": [ - { - "currency_code": "eur", - "amount": 1950 - }, - { - "currency_code": "usd", - "amount": 2200 - } - ], - "options": [ - { - "value": "XL" - }, - { - "value": "Black" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "XL / White", - "prices": [ - { - "currency_code": "eur", - "amount": 1950 - }, - { - "currency_code": "usd", - "amount": 2200 - } - ], - "options": [ - { - "value": "XL" - }, - { - "value": "White" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - } - ] - }, - { - "title": "Medusa Sweatshirt", - "categories": [ - { - "id": "pcat_shirts" - } - ], - "subtitle": null, - "description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", - "handle": "sweatshirt", - "is_giftcard": false, - "weight": 400, - "images": [ - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png" - ], - "options": [ - { - "title": "Size", - "values": [ - "S", - "M", - "L", - "XL" - ] - } - ], - "variants": [ - { - "title": "S", - "prices": [ - { - "currency_code": "eur", - "amount": 2950 - }, - { - "currency_code": "usd", - "amount": 3350 - } - ], - "options": [ - { - "value": "S" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "M", - "prices": [ - { - "currency_code": "eur", - "amount": 2950 - }, - { - "currency_code": "usd", - "amount": 3350 - } - ], - "options": [ - { - "value": "M" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "L", - "prices": [ - { - "currency_code": "eur", - "amount": 2950 - }, - { - "currency_code": "usd", - "amount": 3350 - } - ], - "options": [ - { - "value": "L" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "XL", - "prices": [ - { - "currency_code": "eur", - "amount": 2950 - }, - { - "currency_code": "usd", - "amount": 3350 - } - ], - "options": [ - { - "value": "XL" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - } - ] - }, - { - "title": "Medusa Sweatpants", - "categories": [ - { - "id": "pcat_pants" - } - ], - "subtitle": null, - "description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", - "handle": "sweatpants", - "is_giftcard": false, - "weight": 400, - "images": [ - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png" - ], - "options": [ - { - "title": "Size", - "values": [ - "S", - "M", - "L", - "XL" - ] - } - ], - "variants": [ - { - "title": "S", - "prices": [ - { - "currency_code": "eur", - "amount": 2950 - }, - { - "currency_code": "usd", - "amount": 3350 - } - ], - "options": [ - { - "value": "S" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "M", - "prices": [ - { - "currency_code": "eur", - "amount": 2950 - }, - { - "currency_code": "usd", - "amount": 3350 - } - ], - "options": [ - { - "value": "M" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "L", - "prices": [ - { - "currency_code": "eur", - "amount": 2950 - }, - { - "currency_code": "usd", - "amount": 3350 - } - ], - "options": [ - { - "value": "L" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "XL", - "prices": [ - { - "currency_code": "eur", - "amount": 2950 - }, - { - "currency_code": "usd", - "amount": 3350 - } - ], - "options": [ - { - "value": "XL" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - } - ] - }, - { - "title": "Medusa Shorts", - "categories": [ - { - "id": "pcat_merch" - } - ], - "subtitle": null, - "description": "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", - "handle": "shorts", - "is_giftcard": false, - "weight": 400, - "images": [ - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png" - ], - "options": [ - { - "title": "Size", - "values": [ - "S", - "M", - "L", - "XL" - ] - } - ], - "variants": [ - { - "title": "S", - "prices": [ - { - "currency_code": "eur", - "amount": 2500 - }, - { - "currency_code": "usd", - "amount": 2850 - } - ], - "options": [ - { - "value": "S" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "M", - "prices": [ - { - "currency_code": "eur", - "amount": 2500 - }, - { - "currency_code": "usd", - "amount": 2850 - } - ], - "options": [ - { - "value": "M" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "L", - "prices": [ - { - "currency_code": "eur", - "amount": 2500 - }, - { - "currency_code": "usd", - "amount": 2850 - } - ], - "options": [ - { - "value": "L" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "XL", - "prices": [ - { - "currency_code": "eur", - "amount": 2500 - }, - { - "currency_code": "usd", - "amount": 2850 - } - ], - "options": [ - { - "value": "XL" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - } - ] - }, { "title": "Medusa Hoodie", "categories": [ { "id": "pcat_merch" - }, - { - "id": "pcat_hidden_featured" } ], "subtitle": null, @@ -712,12 +63,7 @@ "options": [ { "title": "Size", - "values": [ - "S", - "M", - "L", - "XL" - ] + "values": ["S", "M", "L", "XL"] } ], "variants": [ @@ -725,12 +71,8 @@ "title": "S", "prices": [ { - "currency_code": "eur", - "amount": 3650 - }, - { - "currency_code": "usd", - "amount": 4150 + "currency_code": "ghs", + "amount": 12000 } ], "options": [ @@ -738,19 +80,15 @@ "value": "S" } ], - "inventory_quantity": 100, + "inventory_quantity": 1000000, "manage_inventory": true }, { "title": "M", "prices": [ { - "currency_code": "eur", - "amount": 3650 - }, - { - "currency_code": "usd", - "amount": 4150 + "currency_code": "ghs", + "amount": 12000 } ], "options": [ @@ -758,19 +96,15 @@ "value": "M" } ], - "inventory_quantity": 100, + "inventory_quantity": 1000000, "manage_inventory": true }, { "title": "L", "prices": [ { - "currency_code": "eur", - "amount": 3650 - }, - { - "currency_code": "usd", - "amount": 4150 + "currency_code": "ghs", + "amount": 12000 } ], "options": [ @@ -778,19 +112,15 @@ "value": "L" } ], - "inventory_quantity": 100, + "inventory_quantity": 1000000, "manage_inventory": true }, { "title": "XL", "prices": [ { - "currency_code": "eur", - "amount": 3650 - }, - { - "currency_code": "usd", - "amount": 4150 + "currency_code": "ghs", + "amount": 12000 } ], "options": [ @@ -798,120 +128,7 @@ "value": "XL" } ], - "inventory_quantity": 100, - "manage_inventory": true - } - ] - }, - { - "title": "Medusa Longsleeve", - "categories": [ - { - "id": "pcat_shirts" - }, - { - "id": "pcat_hidden_featured" - } - ], - "subtitle": null, - "description": "Reimagine the feeling of a classic longsleeve. With our cotton longsleeve, everyday essentials no longer have to be ordinary.", - "handle": "longsleeve", - "is_giftcard": false, - "weight": 400, - "images": [ - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-front.png", - "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-back.png" - ], - "options": [ - { - "title": "Size", - "values": [ - "S", - "M", - "L", - "XL" - ] - } - ], - "variants": [ - { - "title": "S", - "prices": [ - { - "currency_code": "eur", - "amount": 3650 - }, - { - "currency_code": "usd", - "amount": 4150 - } - ], - "options": [ - { - "value": "S" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "M", - "prices": [ - { - "currency_code": "eur", - "amount": 3650 - }, - { - "currency_code": "usd", - "amount": 4150 - } - ], - "options": [ - { - "value": "M" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "L", - "prices": [ - { - "currency_code": "eur", - "amount": 3650 - }, - { - "currency_code": "usd", - "amount": 4150 - } - ], - "options": [ - { - "value": "L" - } - ], - "inventory_quantity": 100, - "manage_inventory": true - }, - { - "title": "XL", - "prices": [ - { - "currency_code": "eur", - "amount": 3650 - }, - { - "currency_code": "usd", - "amount": 4150 - } - ], - "options": [ - { - "value": "XL" - } - ], - "inventory_quantity": 100, + "inventory_quantity": 1000000, "manage_inventory": true } ] @@ -921,9 +138,6 @@ "categories": [ { "id": "pcat_merch" - }, - { - "id": "pcat_hidden_featured" } ], "subtitle": null, @@ -937,9 +151,7 @@ "options": [ { "title": "Size", - "values": [ - "One Size" - ] + "values": ["One Size"] } ], "variants": [ @@ -947,12 +159,8 @@ "title": "One Size", "prices": [ { - "currency_code": "eur", - "amount": 1000 - }, - { - "currency_code": "usd", - "amount": 1200 + "currency_code": "ghs", + "amount": 5000 } ], "options": [ @@ -960,47 +168,19 @@ "value": "One Size" } ], - "inventory_quantity": 100, + "inventory_quantity": 1000000, "manage_inventory": true } ] } ], "categories": [ - { - "id": "pcat_pants", - "name": "Pants", - "rank": 0, - "category_children": [], - "handle": "pants" - }, - { - "id": "pcat_shirts", - "name": "Shirts", - "rank": 0, - "category_children": [], - "handle": "shirts" - }, { "id": "pcat_merch", "name": "Merch", "rank": 0, "category_children": [], "handle": "merch" - }, - { - "id": "pcat_hidden_carousel", - "name": "Hidden homepage carousel", - "rank": 0, - "category_children": [], - "handle": "hidden-homepage-carousel" - }, - { - "id": "pcat_hidden_featured", - "name": "Hidden homepage featured", - "rank": 0, - "category_children": [], - "handle": "hidden-homepage-featured-items" } ] -} \ No newline at end of file +} diff --git a/examples/backend/medusa-config.js b/examples/backend/medusa-config.js index d29c875..58256af 100644 --- a/examples/backend/medusa-config.js +++ b/examples/backend/medusa-config.js @@ -33,6 +33,12 @@ const DATABASE_URL = const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379"; +const PAYSTACK_SECRET_KEY = process.env.PAYSTACK_SECRET_KEY; + +if (!PAYSTACK_SECRET_KEY) { + throw new Error("Please provide a PAYSTACK_SECRET_KEY environment variable"); +} + const plugins = [ `medusa-fulfillment-manual`, `medusa-payment-manual`, @@ -52,6 +58,13 @@ const plugins = [ }, }, }, + { + resolve: "medusa-payment-paystack", + options: { + secret_key: PAYSTACK_SECRET_KEY, + debug: true, // Shows helpful debugging information + }, + }, ]; const modules = { diff --git a/examples/backend/package.json b/examples/backend/package.json index 2a8b5e9..29da53e 100644 --- a/examples/backend/package.json +++ b/examples/backend/package.json @@ -40,6 +40,7 @@ "medusa-fulfillment-manual": "^1.1.39", "medusa-interfaces": "^1.3.8", "medusa-payment-manual": "^1.0.24", + "medusa-payment-paystack": "^1.2.1", "medusa-payment-stripe": "^6.0.7", "prism-react-renderer": "^2.0.4", "typeorm": "^0.3.16" diff --git a/examples/storefront/src/modules/home/components/hero/index.tsx b/examples/storefront/src/modules/home/components/hero/index.tsx index 4d1e831..3425ce0 100644 --- a/examples/storefront/src/modules/home/components/hero/index.tsx +++ b/examples/storefront/src/modules/home/components/hero/index.tsx @@ -5,23 +5,23 @@ import { Github } from "@medusajs/icons" const Hero = () => { return (
-
+
- Ecommerce Starter Template + Ecommerce Starter Template with Paystack Integration Powered by Medusa and Next.js