├── .changeset ├── README.md └── config.json ├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── backend │ ├── .env.template │ ├── .env.test │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── .yarnrc.yml │ ├── README.md │ ├── instrumentation.ts │ ├── integration-tests │ │ └── http │ │ │ ├── README.md │ │ │ └── health.spec.ts │ ├── jest.config.js │ ├── medusa-config.ts │ ├── package.json │ ├── src │ │ ├── admin │ │ │ ├── README.md │ │ │ └── tsconfig.json │ │ ├── api │ │ │ ├── README.md │ │ │ ├── admin │ │ │ │ └── custom │ │ │ │ │ └── route.ts │ │ │ └── store │ │ │ │ └── custom │ │ │ │ └── route.ts │ │ ├── jobs │ │ │ └── README.md │ │ ├── links │ │ │ └── README.md │ │ ├── modules │ │ │ └── README.md │ │ ├── scripts │ │ │ ├── README.md │ │ │ └── seed.ts │ │ ├── subscribers │ │ │ └── README.md │ │ └── workflows │ │ │ └── README.md │ ├── tsconfig.json │ └── yarn.lock └── storefront │ ├── .eslintrc.js │ ├── .github │ └── ISSUE_TEMPLATE │ │ └── bug-report.yml │ ├── .gitignore │ ├── .prettierrc │ ├── .yarnrc.yml │ ├── LICENSE │ ├── README.md │ ├── check-env-variables.js │ ├── next-env.d.ts │ ├── next-sitemap.js │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── favicon.ico │ ├── src │ ├── app │ │ ├── [countryCode] │ │ │ ├── (checkout) │ │ │ │ ├── checkout │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── not-found.tsx │ │ │ └── (main) │ │ │ │ ├── account │ │ │ │ ├── @dashboard │ │ │ │ │ ├── addresses │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── orders │ │ │ │ │ │ ├── details │ │ │ │ │ │ │ └── [id] │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── profile │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── @login │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── loading.tsx │ │ │ │ ├── cart │ │ │ │ ├── loading.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── categories │ │ │ │ └── [...category] │ │ │ │ │ └── page.tsx │ │ │ │ ├── collections │ │ │ │ └── [handle] │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ ├── order │ │ │ │ └── [id] │ │ │ │ │ ├── confirmed │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ │ └── transfer │ │ │ │ │ └── [token] │ │ │ │ │ ├── accept │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── decline │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── products │ │ │ │ └── [handle] │ │ │ │ │ └── page.tsx │ │ │ │ └── store │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ ├── opengraph-image.jpg │ │ └── twitter-image.jpg │ ├── lib │ │ ├── config.ts │ │ ├── constants.tsx │ │ ├── context │ │ │ └── modal-context.tsx │ │ ├── data │ │ │ ├── cart.ts │ │ │ ├── categories.ts │ │ │ ├── collections.ts │ │ │ ├── cookies.ts │ │ │ ├── customer.ts │ │ │ ├── fulfillment.ts │ │ │ ├── onboarding.ts │ │ │ ├── orders.ts │ │ │ ├── payment.ts │ │ │ ├── products.ts │ │ │ └── regions.ts │ │ ├── hooks │ │ │ ├── use-in-view.tsx │ │ │ └── use-toggle-state.tsx │ │ └── util │ │ │ ├── compare-addresses.ts │ │ │ ├── env.ts │ │ │ ├── get-precentage-diff.ts │ │ │ ├── get-product-price.ts │ │ │ ├── isEmpty.ts │ │ │ ├── medusa-error.ts │ │ │ ├── money.ts │ │ │ ├── repeat.ts │ │ │ └── sort-products.ts │ ├── middleware.ts │ ├── modules │ │ ├── account │ │ │ ├── components │ │ │ │ ├── account-info │ │ │ │ │ └── index.tsx │ │ │ │ ├── account-nav │ │ │ │ │ └── index.tsx │ │ │ │ ├── address-book │ │ │ │ │ └── index.tsx │ │ │ │ ├── address-card │ │ │ │ │ ├── add-address.tsx │ │ │ │ │ └── edit-address-modal.tsx │ │ │ │ ├── login │ │ │ │ │ └── index.tsx │ │ │ │ ├── order-card │ │ │ │ │ └── index.tsx │ │ │ │ ├── order-overview │ │ │ │ │ └── index.tsx │ │ │ │ ├── overview │ │ │ │ │ └── index.tsx │ │ │ │ ├── profile-billing-address │ │ │ │ │ └── index.tsx │ │ │ │ ├── profile-email │ │ │ │ │ └── index.tsx │ │ │ │ ├── profile-name │ │ │ │ │ └── index.tsx │ │ │ │ ├── profile-password │ │ │ │ │ └── index.tsx │ │ │ │ ├── profile-phone │ │ │ │ │ └── index.tsx │ │ │ │ ├── register │ │ │ │ │ └── index.tsx │ │ │ │ └── transfer-request-form │ │ │ │ │ └── index.tsx │ │ │ └── templates │ │ │ │ ├── account-layout.tsx │ │ │ │ └── login-template.tsx │ │ ├── cart │ │ │ ├── components │ │ │ │ ├── cart-item-select │ │ │ │ │ └── index.tsx │ │ │ │ ├── empty-cart-message │ │ │ │ │ └── index.tsx │ │ │ │ ├── item │ │ │ │ │ └── index.tsx │ │ │ │ └── sign-in-prompt │ │ │ │ │ └── index.tsx │ │ │ └── templates │ │ │ │ ├── index.tsx │ │ │ │ ├── items.tsx │ │ │ │ ├── preview.tsx │ │ │ │ └── summary.tsx │ │ ├── categories │ │ │ └── templates │ │ │ │ └── index.tsx │ │ ├── checkout │ │ │ ├── components │ │ │ │ ├── address-select │ │ │ │ │ └── index.tsx │ │ │ │ ├── addresses │ │ │ │ │ └── index.tsx │ │ │ │ ├── billing_address │ │ │ │ │ └── index.tsx │ │ │ │ ├── country-select │ │ │ │ │ └── index.tsx │ │ │ │ ├── discount-code │ │ │ │ │ └── index.tsx │ │ │ │ ├── error-message │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-button │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-container │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-test │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-wrapper │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── stripe-wrapper.tsx │ │ │ │ ├── payment │ │ │ │ │ └── index.tsx │ │ │ │ ├── review │ │ │ │ │ └── index.tsx │ │ │ │ ├── shipping-address │ │ │ │ │ └── index.tsx │ │ │ │ ├── shipping │ │ │ │ │ └── index.tsx │ │ │ │ └── submit-button │ │ │ │ │ └── index.tsx │ │ │ └── templates │ │ │ │ ├── checkout-form │ │ │ │ └── index.tsx │ │ │ │ └── checkout-summary │ │ │ │ └── index.tsx │ │ ├── collections │ │ │ └── templates │ │ │ │ └── index.tsx │ │ ├── common │ │ │ ├── components │ │ │ │ ├── cart-totals │ │ │ │ │ └── index.tsx │ │ │ │ ├── checkbox │ │ │ │ │ └── index.tsx │ │ │ │ ├── delete-button │ │ │ │ │ └── index.tsx │ │ │ │ ├── divider │ │ │ │ │ └── index.tsx │ │ │ │ ├── filter-radio-group │ │ │ │ │ └── index.tsx │ │ │ │ ├── input │ │ │ │ │ └── index.tsx │ │ │ │ ├── interactive-link │ │ │ │ │ └── index.tsx │ │ │ │ ├── line-item-options │ │ │ │ │ └── index.tsx │ │ │ │ ├── line-item-price │ │ │ │ │ └── index.tsx │ │ │ │ ├── line-item-unit-price │ │ │ │ │ └── index.tsx │ │ │ │ ├── localized-client-link │ │ │ │ │ └── index.tsx │ │ │ │ ├── modal │ │ │ │ │ └── index.tsx │ │ │ │ ├── native-select │ │ │ │ │ └── index.tsx │ │ │ │ └── radio │ │ │ │ │ └── index.tsx │ │ │ └── icons │ │ │ │ ├── back.tsx │ │ │ │ ├── bancontact.tsx │ │ │ │ ├── chevron-down.tsx │ │ │ │ ├── eye-off.tsx │ │ │ │ ├── eye.tsx │ │ │ │ ├── fast-delivery.tsx │ │ │ │ ├── ideal.tsx │ │ │ │ ├── map-pin.tsx │ │ │ │ ├── medusa.tsx │ │ │ │ ├── nextjs.tsx │ │ │ │ ├── package.tsx │ │ │ │ ├── paypal.tsx │ │ │ │ ├── paystack.tsx │ │ │ │ ├── placeholder-image.tsx │ │ │ │ ├── refresh.tsx │ │ │ │ ├── spinner.tsx │ │ │ │ ├── trash.tsx │ │ │ │ ├── user.tsx │ │ │ │ └── x.tsx │ │ ├── home │ │ │ └── components │ │ │ │ ├── featured-products │ │ │ │ ├── index.tsx │ │ │ │ └── product-rail │ │ │ │ │ └── index.tsx │ │ │ │ └── hero │ │ │ │ └── index.tsx │ │ ├── layout │ │ │ ├── components │ │ │ │ ├── cart-button │ │ │ │ │ └── index.tsx │ │ │ │ ├── cart-dropdown │ │ │ │ │ └── index.tsx │ │ │ │ ├── cart-mismatch-banner │ │ │ │ │ └── index.tsx │ │ │ │ ├── country-select │ │ │ │ │ └── index.tsx │ │ │ │ ├── medusa-cta │ │ │ │ │ └── index.tsx │ │ │ │ └── side-menu │ │ │ │ │ └── index.tsx │ │ │ └── templates │ │ │ │ ├── footer │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── nav │ │ │ │ └── index.tsx │ │ ├── order │ │ │ ├── components │ │ │ │ ├── help │ │ │ │ │ └── index.tsx │ │ │ │ ├── item │ │ │ │ │ └── index.tsx │ │ │ │ ├── items │ │ │ │ │ └── index.tsx │ │ │ │ ├── onboarding-cta │ │ │ │ │ └── index.tsx │ │ │ │ ├── order-details │ │ │ │ │ └── index.tsx │ │ │ │ ├── order-summary │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-details │ │ │ │ │ └── index.tsx │ │ │ │ ├── shipping-details │ │ │ │ │ └── index.tsx │ │ │ │ ├── transfer-actions │ │ │ │ │ └── index.tsx │ │ │ │ └── transfer-image │ │ │ │ │ └── index.tsx │ │ │ └── templates │ │ │ │ ├── order-completed-template.tsx │ │ │ │ └── order-details-template.tsx │ │ ├── products │ │ │ ├── components │ │ │ │ ├── image-gallery │ │ │ │ │ └── index.tsx │ │ │ │ ├── product-actions │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── mobile-actions.tsx │ │ │ │ │ └── option-select.tsx │ │ │ │ ├── product-onboarding-cta │ │ │ │ │ └── index.tsx │ │ │ │ ├── product-preview │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── price.tsx │ │ │ │ ├── product-price │ │ │ │ │ └── index.tsx │ │ │ │ ├── product-tabs │ │ │ │ │ ├── accordion.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── related-products │ │ │ │ │ └── index.tsx │ │ │ │ └── thumbnail │ │ │ │ │ └── index.tsx │ │ │ └── templates │ │ │ │ ├── index.tsx │ │ │ │ ├── product-actions-wrapper │ │ │ │ └── index.tsx │ │ │ │ └── product-info │ │ │ │ └── index.tsx │ │ ├── shipping │ │ │ └── components │ │ │ │ └── free-shipping-price-nudge │ │ │ │ └── index.tsx │ │ ├── skeletons │ │ │ ├── components │ │ │ │ ├── skeleton-button │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-cart-item │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-cart-totals │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-code-form │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-line-item │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-order-confirmed-header │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-order-information │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-order-items │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-order-summary │ │ │ │ │ └── index.tsx │ │ │ │ └── skeleton-product-preview │ │ │ │ │ └── index.tsx │ │ │ └── templates │ │ │ │ ├── skeleton-cart-page │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-order-confirmed │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-product-grid │ │ │ │ └── index.tsx │ │ │ │ └── skeleton-related-products │ │ │ │ └── index.tsx │ │ └── store │ │ │ ├── components │ │ │ ├── pagination │ │ │ │ └── index.tsx │ │ │ └── refinement-list │ │ │ │ ├── index.tsx │ │ │ │ └── sort-products │ │ │ │ └── index.tsx │ │ │ └── templates │ │ │ ├── index.tsx │ │ │ └── paginated-products.tsx │ ├── styles │ │ └── globals.css │ └── types │ │ ├── global.ts │ │ └── icon.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── yarn.lock ├── package.json ├── packages └── plugin │ ├── .eslintrc.yml │ ├── .gitignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── index.ts │ ├── lib │ │ ├── __mocks__ │ │ │ └── paystack.ts │ │ └── paystack.ts │ ├── services │ │ ├── __tests__ │ │ │ └── paystack-payment-processor.ts │ │ └── paystack-payment-processor.ts │ └── utils │ │ └── currencyCode.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/packages/plugin" 10 | versioning-strategy: increase 11 | schedule: 12 | interval: "weekly" 13 | 14 | - package-ecosystem: "npm" 15 | directory: "/examples/backend" 16 | versioning-strategy: increase 17 | schedule: 18 | interval: "daily" 19 | allow: 20 | - dependency-name: "medusa-payment-paystack" # Keep plugin dep up to date 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Build & Lint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | plugin: 11 | name: Build & Lint Plugin 12 | runs-on: ubuntu-latest 13 | defaults: 14 | run: 15 | working-directory: packages/plugin 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup PNPM 21 | uses: pnpm/action-setup@v2 22 | with: 23 | version: 9 24 | 25 | - name: Setup Node.js 20 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 20 29 | cache: "pnpm" 30 | 31 | - name: Install Dependencies 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: Build 35 | run: pnpm run build 36 | env: 37 | NODE_ENV: production 38 | 39 | - name: Lint 40 | run: pnpm run lint 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup PNPM 19 | uses: pnpm/action-setup@v2 20 | with: 21 | version: 9 22 | 23 | - name: Setup Node.js 20 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 20 27 | cache: "pnpm" 28 | 29 | - name: Install Dependencies 30 | run: pnpm install --frozen-lockfile 31 | 32 | - name: Build 33 | run: pnpm run build 34 | 35 | - name: Create Release Pull Request 36 | uses: changesets/action@v1 37 | with: 38 | publish: pnpm release 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | name: Run test suites 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup PNPM 18 | uses: pnpm/action-setup@v2 19 | with: 20 | version: 9 21 | 22 | - name: Setup Node.js 20 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 20 26 | cache: "pnpm" 27 | 28 | - name: Install Dependencies 29 | run: pnpm install --frozen-lockfile 30 | 31 | - name: Test 32 | run: pnpm run test 33 | 34 | - name: Check dependencies 35 | run: pnpm run check-deps 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_Store 4 | turbo-build.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Andrew Glago, Femi Akinyemi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/backend/.env.template: -------------------------------------------------------------------------------- 1 | STORE_CORS=http://localhost:8000,https://docs.medusajs.com 2 | ADMIN_CORS=http://localhost:5173,http://localhost:9000,https://docs.medusajs.com 3 | AUTH_CORS=http://localhost:5173,http://localhost:9000,https://docs.medusajs.com 4 | REDIS_URL=redis://localhost:6379 5 | JWT_SECRET=supersecret 6 | COOKIE_SECRET=supersecret 7 | DATABASE_URL= 8 | DB_NAME=medusa-v2 9 | POSTGRES_URL= 10 | PAYSTACK_SECRET_KEY= -------------------------------------------------------------------------------- /examples/backend/.env.test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a11rew/medusa-payment-paystack/00fbb696b866e52fad679fbe1ae241513a850020/examples/backend/.env.test -------------------------------------------------------------------------------- /examples/backend/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | .env 3 | .DS_Store 4 | /uploads 5 | /node_modules 6 | yarn-error.log 7 | 8 | .idea 9 | 10 | coverage 11 | 12 | !src/** 13 | 14 | ./tsconfig.tsbuildinfo 15 | medusa-db.sql 16 | build 17 | .cache 18 | 19 | .yarn/* 20 | !.yarn/patches 21 | !.yarn/plugins 22 | !.yarn/releases 23 | !.yarn/sdks 24 | !.yarn/versions 25 | 26 | .medusa -------------------------------------------------------------------------------- /examples/backend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /examples/backend/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules -------------------------------------------------------------------------------- /examples/backend/instrumentation.ts: -------------------------------------------------------------------------------- 1 | // Uncomment this file to enable instrumentation and observability using OpenTelemetry 2 | // Refer to the docs for installation instructions: https://docs.medusajs.com/learn/debugging-and-testing/instrumentation 3 | 4 | // import { registerOtel } from "@medusajs/medusa" 5 | // // If using an exporter other than Zipkin, require it here. 6 | // import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" 7 | 8 | // // If using an exporter other than Zipkin, initialize it here. 9 | // const exporter = new ZipkinExporter({ 10 | // serviceName: 'my-medusa-project', 11 | // }) 12 | 13 | // export function register() { 14 | // registerOtel({ 15 | // serviceName: 'medusajs', 16 | // // pass exporter 17 | // exporter, 18 | // instrument: { 19 | // http: true, 20 | // workflows: true, 21 | // query: true 22 | // }, 23 | // }) 24 | // } -------------------------------------------------------------------------------- /examples/backend/integration-tests/http/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | The `medusa-test-utils` package provides utility functions to create integration tests for your API routes and workflows. 4 | 5 | For example: 6 | 7 | ```ts 8 | import { medusaIntegrationTestRunner } from "medusa-test-utils" 9 | 10 | medusaIntegrationTestRunner({ 11 | testSuite: ({ api, getContainer }) => { 12 | describe("Custom endpoints", () => { 13 | describe("GET /store/custom", () => { 14 | it("returns correct message", async () => { 15 | const response = await api.get( 16 | `/store/custom` 17 | ) 18 | 19 | expect(response.status).toEqual(200) 20 | expect(response.data).toHaveProperty("message") 21 | expect(response.data.message).toEqual("Hello, World!") 22 | }) 23 | }) 24 | }) 25 | } 26 | }) 27 | ``` 28 | 29 | Learn more in [this documentation](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests). -------------------------------------------------------------------------------- /examples/backend/integration-tests/http/health.spec.ts: -------------------------------------------------------------------------------- 1 | import { medusaIntegrationTestRunner } from "@medusajs/test-utils" 2 | jest.setTimeout(60 * 1000) 3 | 4 | medusaIntegrationTestRunner({ 5 | inApp: true, 6 | env: {}, 7 | testSuite: ({ api }) => { 8 | describe("Ping", () => { 9 | it("ping the server health endpoint", async () => { 10 | const response = await api.get('/health') 11 | expect(response.status).toEqual(200) 12 | }) 13 | }) 14 | }, 15 | }) -------------------------------------------------------------------------------- /examples/backend/jest.config.js: -------------------------------------------------------------------------------- 1 | const { loadEnv } = require("@medusajs/utils"); 2 | loadEnv("test", process.cwd()); 3 | 4 | module.exports = { 5 | transform: { 6 | "^.+\\.[jt]s$": [ 7 | "@swc/jest", 8 | { 9 | jsc: { 10 | parser: { syntax: "typescript", decorators: true }, 11 | }, 12 | }, 13 | ], 14 | }, 15 | testEnvironment: "node", 16 | moduleFileExtensions: ["js", "ts", "json"], 17 | modulePathIgnorePatterns: ["dist/", "/.medusa/"], 18 | }; 19 | 20 | if (process.env.TEST_TYPE === "integration:http") { 21 | module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"]; 22 | } else if (process.env.TEST_TYPE === "integration:modules") { 23 | module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"]; 24 | } else if (process.env.TEST_TYPE === "unit") { 25 | module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"]; 26 | } 27 | -------------------------------------------------------------------------------- /examples/backend/medusa-config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv, defineConfig } from '@medusajs/framework/utils' 2 | 3 | loadEnv(process.env.NODE_ENV || 'development', process.cwd()) 4 | 5 | const paystackSecretKey = process.env.PAYSTACK_SECRET_KEY; 6 | 7 | if (!paystackSecretKey) { 8 | throw new Error('PAYSTACK_SECRET_KEY is required'); 9 | } 10 | 11 | module.exports = defineConfig({ 12 | projectConfig: { 13 | databaseUrl: process.env.DATABASE_URL, 14 | http: { 15 | storeCors: process.env.STORE_CORS!, 16 | adminCors: process.env.ADMIN_CORS!, 17 | authCors: process.env.AUTH_CORS!, 18 | jwtSecret: process.env.JWT_SECRET || "supersecret", 19 | cookieSecret: process.env.COOKIE_SECRET || "supersecret", 20 | }, 21 | }, 22 | modules: [ 23 | // other modules 24 | { 25 | resolve: "@medusajs/medusa/payment", 26 | options: { 27 | providers: [ 28 | // other payment providers like stripe, paypal etc 29 | { 30 | resolve: "medusa-payment-paystack", 31 | options: { 32 | secret_key: paystackSecretKey, 33 | debug: true, 34 | } satisfies import("medusa-payment-paystack").PluginOptions, 35 | }, 36 | ], 37 | }, 38 | }, 39 | ], 40 | }); 41 | -------------------------------------------------------------------------------- /examples/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medusa-payment-paystack-example-backend", 3 | "version": "0.0.1", 4 | "description": "A starter for Medusa projects.", 5 | "author": "Medusa (https://medusajs.com)", 6 | "license": "MIT", 7 | "keywords": [ 8 | "sqlite", 9 | "postgres", 10 | "typescript", 11 | "ecommerce", 12 | "headless", 13 | "medusa" 14 | ], 15 | "scripts": { 16 | "build": "medusa build", 17 | "seed": "medusa exec ./src/scripts/seed.ts", 18 | "start": "medusa start", 19 | "dev": "medusa develop", 20 | "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", 21 | "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", 22 | "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" 23 | }, 24 | "dependencies": { 25 | "@medusajs/admin-sdk": "2.1.3", 26 | "@medusajs/cli": "2.1.3", 27 | "@medusajs/framework": "2.1.3", 28 | "@medusajs/medusa": "2.1.3", 29 | "@mikro-orm/core": "5.9.7", 30 | "@mikro-orm/knex": "5.9.7", 31 | "@mikro-orm/migrations": "5.9.7", 32 | "@mikro-orm/postgresql": "5.9.7", 33 | "awilix": "^8.0.1", 34 | "medusa-payment-paystack": "^2.0.0", 35 | "pg": "^8.13.0" 36 | }, 37 | "devDependencies": { 38 | "@medusajs/test-utils": "2.1.3", 39 | "@mikro-orm/cli": "5.9.7", 40 | "@swc/core": "1.5.7", 41 | "@swc/jest": "^0.2.36", 42 | "@types/jest": "^29.5.13", 43 | "@types/node": "^20.0.0", 44 | "@types/react": "^18.3.2", 45 | "@types/react-dom": "^18.2.25", 46 | "jest": "^29.7.0", 47 | "prop-types": "^15.8.1", 48 | "react": "^18.2.0", 49 | "react-dom": "^18.2.0", 50 | "ts-node": "^10.9.2", 51 | "typescript": "^5.6.2", 52 | "vite": "^5.2.11" 53 | }, 54 | "engines": { 55 | "node": ">=20" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/backend/src/admin/README.md: -------------------------------------------------------------------------------- 1 | # Admin Customizations 2 | 3 | You can extend the Medusa Admin to add widgets and new pages. Your customizations interact with API routes to provide merchants with custom functionalities. 4 | 5 | ## Example: Create a Widget 6 | 7 | A widget is a React component that can be injected into an existing page in the admin dashboard. 8 | 9 | For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: 10 | 11 | ```tsx title="src/admin/widgets/product-widget.tsx" 12 | import { defineWidgetConfig } from "@medusajs/admin-sdk" 13 | 14 | // The widget 15 | const ProductWidget = () => { 16 | return ( 17 |
18 |

Product Widget

19 |
20 | ) 21 | } 22 | 23 | // The widget's configurations 24 | export const config = defineWidgetConfig({ 25 | zone: "product.details.after", 26 | }) 27 | 28 | export default ProductWidget 29 | ``` 30 | 31 | This inserts a widget with the text “Product Widget” at the end of a product’s details page. -------------------------------------------------------------------------------- /examples/backend/src/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["."] 24 | } -------------------------------------------------------------------------------- /examples/backend/src/api/admin/custom/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"; 2 | 3 | export async function GET( 4 | req: MedusaRequest, 5 | res: MedusaResponse 6 | ): Promise { 7 | res.sendStatus(200); 8 | } 9 | -------------------------------------------------------------------------------- /examples/backend/src/api/store/custom/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"; 2 | 3 | export async function GET( 4 | req: MedusaRequest, 5 | res: MedusaResponse 6 | ): Promise { 7 | res.sendStatus(200); 8 | } 9 | -------------------------------------------------------------------------------- /examples/backend/src/jobs/README.md: -------------------------------------------------------------------------------- 1 | # Custom scheduled jobs 2 | 3 | A scheduled job is a function executed at a specified interval of time in the background of your Medusa application. 4 | 5 | A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. 6 | 7 | For example, create the file `src/jobs/hello-world.ts` with the following content: 8 | 9 | ```ts 10 | import { 11 | IProductModuleService, 12 | MedusaContainer 13 | } from "@medusajs/framework/types"; 14 | import { Modules } from "@medusajs/framework/utils"; 15 | 16 | export default async function myCustomJob(container: MedusaContainer) { 17 | const productService: IProductModuleService = container.resolve(Modules.PRODUCT) 18 | 19 | const products = await productService.listAndCountProducts(); 20 | 21 | // Do something with the products 22 | } 23 | 24 | export const config = { 25 | name: "daily-product-report", 26 | schedule: "0 0 * * *", // Every day at midnight 27 | }; 28 | ``` 29 | 30 | A scheduled job file must export: 31 | 32 | - The function to be executed whenever it’s time to run the scheduled job. 33 | - A configuration object defining the job. It has three properties: 34 | - `name`: a unique name for the job. 35 | - `schedule`: a [cron expression](https://crontab.guru/). 36 | - `numberOfExecutions`: an optional integer, specifying how many times the job will execute before being removed 37 | 38 | The `handler` is a function that accepts one parameter, `container`, which is a `MedusaContainer` instance used to resolve services. 39 | -------------------------------------------------------------------------------- /examples/backend/src/links/README.md: -------------------------------------------------------------------------------- 1 | # Module Links 2 | 3 | A module link forms an association between two data models of different modules, while maintaining module isolation. 4 | 5 | For example: 6 | 7 | ```ts 8 | import HelloModule from "../modules/hello" 9 | import ProductModule from "@medusajs/medusa/product" 10 | import { defineLink } from "@medusajs/framework/utils" 11 | 12 | export default defineLink( 13 | ProductModule.linkable.product, 14 | HelloModule.linkable.myCustom 15 | ) 16 | ``` 17 | 18 | This defines a link between the Product Module's `product` data model and the Hello Module (custom module)'s `myCustom` data model. 19 | 20 | Learn more about links in [this documentation](https://docs.medusajs.com/learn/advanced-development/module-links) -------------------------------------------------------------------------------- /examples/backend/src/scripts/README.md: -------------------------------------------------------------------------------- 1 | # Custom CLI Script 2 | 3 | A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run as a CLI tool. 4 | 5 | ## How to Create a Custom CLI Script? 6 | 7 | To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function. 8 | 9 | For example, create the file `src/scripts/my-script.ts` with the following content: 10 | 11 | ```ts title="src/scripts/my-script.ts" 12 | import { 13 | ExecArgs, 14 | IProductModuleService 15 | } from "@medusajs/framework/types" 16 | import { Modules } from "@medusajs/framework/utils" 17 | 18 | export default async function myScript ({ 19 | container 20 | }: ExecArgs) { 21 | const productModuleService: IProductModuleService = 22 | container.resolve(Modules.PRODUCT) 23 | 24 | const [, count] = await productModuleService.listAndCountProducts() 25 | 26 | console.log(`You have ${count} product(s)`) 27 | } 28 | ``` 29 | 30 | The function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application. 31 | 32 | --- 33 | 34 | ## How to Run Custom CLI Script? 35 | 36 | To run the custom CLI script, run the `exec` command: 37 | 38 | ```bash 39 | npx medusa exec ./src/scripts/my-script.ts 40 | ``` 41 | 42 | --- 43 | 44 | ## Custom CLI Script Arguments 45 | 46 | Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property. 47 | 48 | For example: 49 | 50 | ```ts 51 | import { ExecArgs } from "@medusajs/framework/types" 52 | 53 | export default async function myScript ({ 54 | args 55 | }: ExecArgs) { 56 | console.log(`The arguments you passed: ${args}`) 57 | } 58 | ``` 59 | 60 | Then, pass the arguments in the `exec` command after the file path: 61 | 62 | ```bash 63 | npx medusa exec ./src/scripts/my-script.ts arg1 arg2 64 | ``` -------------------------------------------------------------------------------- /examples/backend/src/subscribers/README.md: -------------------------------------------------------------------------------- 1 | # Custom subscribers 2 | 3 | Subscribers handle events emitted in the Medusa application. 4 | 5 | The subscriber is created in a TypeScript or JavaScript file under the `src/subscribers` directory. 6 | 7 | For example, create the file `src/subscribers/product-created.ts` with the following content: 8 | 9 | ```ts 10 | import { 11 | type SubscriberConfig, 12 | } from "@medusajs/framework" 13 | 14 | // subscriber function 15 | export default async function productCreateHandler() { 16 | console.log("A product was created") 17 | } 18 | 19 | // subscriber config 20 | export const config: SubscriberConfig = { 21 | event: "product.created", 22 | } 23 | ``` 24 | 25 | A subscriber file must export: 26 | 27 | - The subscriber function that is an asynchronous function executed whenever the associated event is triggered. 28 | - A configuration object defining the event this subscriber is listening to. 29 | 30 | ## Subscriber Parameters 31 | 32 | A subscriber receives an object having the following properties: 33 | 34 | - `event`: An object holding the event's details. It has a `data` property, which is the event's data payload. 35 | - `container`: The Medusa container. Use it to resolve modules' main services and other registered resources. 36 | 37 | ```ts 38 | import type { 39 | SubscriberArgs, 40 | SubscriberConfig, 41 | } from "@medusajs/framework" 42 | import { IProductModuleService } from "@medusajs/framework/types" 43 | import { Modules } from "@medusajs/framework/utils" 44 | 45 | export default async function productCreateHandler({ 46 | event: { data }, 47 | container, 48 | }: SubscriberArgs<{ id: string }>) { 49 | const productId = data.id 50 | 51 | const productModuleService: IProductModuleService = 52 | container.resolve(Modules.PRODUCT) 53 | 54 | const product = await productModuleService.retrieveProduct(productId) 55 | 56 | console.log(`The product ${product.title} was created`) 57 | } 58 | 59 | export const config: SubscriberConfig = { 60 | event: "product.created", 61 | } 62 | ``` -------------------------------------------------------------------------------- /examples/backend/src/workflows/README.md: -------------------------------------------------------------------------------- 1 | # Custom Workflows 2 | 3 | A workflow is a series of queries and actions that complete a task. 4 | 5 | The workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. 6 | 7 | For example: 8 | 9 | ```ts 10 | import { 11 | createStep, 12 | createWorkflow, 13 | WorkflowResponse, 14 | StepResponse, 15 | } from "@medusajs/framework/workflows-sdk" 16 | 17 | const step1 = createStep("step-1", async () => { 18 | return new StepResponse(`Hello from step one!`) 19 | }) 20 | 21 | type WorkflowInput = { 22 | name: string 23 | } 24 | 25 | const step2 = createStep( 26 | "step-2", 27 | async ({ name }: WorkflowInput) => { 28 | return new StepResponse(`Hello ${name} from step two!`) 29 | } 30 | ) 31 | 32 | type WorkflowOutput = { 33 | message1: string 34 | message2: string 35 | } 36 | 37 | const helloWorldWorkflow = createWorkflow( 38 | "hello-world", 39 | (input: WorkflowInput) => { 40 | const greeting1 = step1() 41 | const greeting2 = step2(input) 42 | 43 | return new WorkflowResponse({ 44 | message1: greeting1, 45 | message2: greeting2 46 | }) 47 | } 48 | ) 49 | 50 | export default helloWorldWorkflow 51 | ``` 52 | 53 | ## Execute Workflow 54 | 55 | You can execute the workflow from other resources, such as API routes, scheduled jobs, or subscribers. 56 | 57 | For example, to execute the workflow in an API route: 58 | 59 | ```ts 60 | import type { 61 | MedusaRequest, 62 | MedusaResponse, 63 | } from "@medusajs/framework" 64 | import myWorkflow from "../../../workflows/hello-world" 65 | 66 | export async function GET( 67 | req: MedusaRequest, 68 | res: MedusaResponse 69 | ) { 70 | const { result } = await myWorkflow(req.scope) 71 | .run({ 72 | input: { 73 | name: req.query.name as string, 74 | }, 75 | }) 76 | 77 | res.send(result) 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /examples/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "esModuleInterop": true, 5 | "module": "Node16", 6 | "moduleResolution": "Node16", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "skipLibCheck": true, 10 | "skipDefaultLibCheck": true, 11 | "declaration": false, 12 | "sourceMap": false, 13 | "inlineSourceMap": true, 14 | "outDir": "./.medusa/server", 15 | "rootDir": "./", 16 | "baseUrl": ".", 17 | "jsx": "react-jsx", 18 | "forceConsistentCasingInFileNames": true, 19 | "resolveJsonModule": true, 20 | "checkJs": false, 21 | "strictNullChecks": true 22 | }, 23 | "ts-node": { 24 | "swc": true 25 | }, 26 | "include": [ 27 | "**/*", 28 | ".medusa/types/*" 29 | ], 30 | "exclude": [ 31 | "node_modules", 32 | ".medusa/server", 33 | ".medusa/admin", 34 | ".cache" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/storefront/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next/core-web-vitals"] 3 | }; -------------------------------------------------------------------------------- /examples/storefront/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # IDEs 4 | .idea 5 | .vscode 6 | 7 | # dependencies 8 | /node_modules 9 | /.pnp 10 | .pnp.js 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | .pnpm-debug.log* 31 | 32 | # local env files 33 | .env 34 | .env.local 35 | .env.development.local 36 | .env.test.local 37 | .env.production.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | node_modules 45 | 46 | .yarn 47 | .swc 48 | dump.rdb 49 | /test-results/ 50 | /playwright-report/ 51 | /blob-report/ 52 | /playwright/.cache/ 53 | /test-results/ 54 | /playwright-report/ 55 | /blob-report/ 56 | /playwright/.cache/ 57 | /playwright/.auth 58 | -------------------------------------------------------------------------------- /examples/storefront/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": false, 4 | "endOfLine": "auto", 5 | "singleQuote": false, 6 | "tabWidth": 2, 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /examples/storefront/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | -------------------------------------------------------------------------------- /examples/storefront/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Medusa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/storefront/check-env-variables.js: -------------------------------------------------------------------------------- 1 | const c = require("ansi-colors") 2 | 3 | const requiredEnvs = [ 4 | { 5 | key: "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", 6 | // TODO: we need a good doc to point this to 7 | description: 8 | "Learn how to create a publishable key: https://docs.medusajs.com/v2/resources/storefront-development/publishable-api-keys", 9 | }, 10 | ] 11 | 12 | function checkEnvVariables() { 13 | const missingEnvs = requiredEnvs.filter(function (env) { 14 | return !process.env[env.key] 15 | }) 16 | 17 | if (missingEnvs.length > 0) { 18 | console.error( 19 | c.red.bold("\n🚫 Error: Missing required environment variables\n") 20 | ) 21 | 22 | missingEnvs.forEach(function (env) { 23 | console.error(c.yellow(` ${c.bold(env.key)}`)) 24 | if (env.description) { 25 | console.error(c.dim(` ${env.description}\n`)) 26 | } 27 | }) 28 | 29 | console.error( 30 | c.yellow( 31 | "\nPlease set these variables in your .env file or environment before starting the application.\n" 32 | ) 33 | ) 34 | 35 | process.exit(1) 36 | } 37 | } 38 | 39 | module.exports = checkEnvVariables 40 | -------------------------------------------------------------------------------- /examples/storefront/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/storefront/next-sitemap.js: -------------------------------------------------------------------------------- 1 | const excludedPaths = ["/checkout", "/account/*"] 2 | 3 | module.exports = { 4 | siteUrl: process.env.NEXT_PUBLIC_VERCEL_URL, 5 | generateRobotsTxt: true, 6 | exclude: excludedPaths + ["/[sitemap]"], 7 | robotsTxtOptions: { 8 | policies: [ 9 | { 10 | userAgent: "*", 11 | allow: "/", 12 | }, 13 | { 14 | userAgent: "*", 15 | disallow: excludedPaths, 16 | }, 17 | ], 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /examples/storefront/next.config.js: -------------------------------------------------------------------------------- 1 | const checkEnvVariables = require("./check-env-variables") 2 | 3 | checkEnvVariables() 4 | 5 | /** 6 | * @type {import('next').NextConfig} 7 | */ 8 | const nextConfig = { 9 | reactStrictMode: true, 10 | logging: { 11 | fetches: { 12 | fullUrl: true, 13 | }, 14 | }, 15 | eslint: { 16 | ignoreDuringBuilds: true, 17 | }, 18 | typescript: { 19 | ignoreBuildErrors: true, 20 | }, 21 | images: { 22 | remotePatterns: [ 23 | { 24 | protocol: "http", 25 | hostname: "localhost", 26 | }, 27 | { 28 | protocol: "https", 29 | hostname: "medusa-public-images.s3.eu-west-1.amazonaws.com", 30 | }, 31 | { 32 | protocol: "https", 33 | hostname: "medusa-server-testing.s3.amazonaws.com", 34 | }, 35 | { 36 | protocol: "https", 37 | hostname: "medusa-server-testing.s3.us-east-1.amazonaws.com", 38 | }, 39 | ], 40 | }, 41 | } 42 | 43 | module.exports = nextConfig 44 | -------------------------------------------------------------------------------- /examples/storefront/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medusa-payment-paystack-example-storefront", 3 | "version": "1.0.3", 4 | "private": true, 5 | "author": "Kasper Fabricius Kristensen & Victor Gerbrands (https://www.medusajs.com)", 6 | "description": "Next.js Starter to be used with Medusa V2", 7 | "keywords": [ 8 | "medusa-storefront" 9 | ], 10 | "scripts": { 11 | "dev": "next dev --turbopack -p 8000", 12 | "build": "next build", 13 | "start": "next start -p 8000", 14 | "lint": "next lint", 15 | "analyze": "ANALYZE=true next build" 16 | }, 17 | "dependencies": { 18 | "@headlessui/react": "^2.2.0", 19 | "@medusajs/js-sdk": "latest", 20 | "@medusajs/ui": "latest", 21 | "@radix-ui/react-accordion": "^1.2.1", 22 | "@stripe/react-stripe-js": "^1.7.2", 23 | "@stripe/stripe-js": "^1.29.0", 24 | "lodash": "^4.17.21", 25 | "next": "15.0.3", 26 | "paystack-inline-ts": "^1.0.3", 27 | "pg": "^8.11.3", 28 | "qs": "^6.12.1", 29 | "react": "19.0.0-rc-66855b96-20241106", 30 | "react-country-flag": "^3.1.0", 31 | "react-dom": "19.0.0-rc-66855b96-20241106", 32 | "server-only": "^0.0.1", 33 | "tailwindcss-radix": "^2.8.0", 34 | "webpack": "^5" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.17.5", 38 | "@medusajs/types": "latest", 39 | "@medusajs/ui-preset": "latest", 40 | "@types/lodash": "^4.14.195", 41 | "@types/node": "17.0.21", 42 | "@types/pg": "^8.11.0", 43 | "@types/react": "19", 44 | "@types/react-dom": "19", 45 | "@types/react-instantsearch-dom": "^6.12.3", 46 | "ansi-colors": "^4.1.3", 47 | "autoprefixer": "^10.4.2", 48 | "babel-loader": "^8.2.3", 49 | "eslint": "8.10.0", 50 | "eslint-config-next": "15.0.3", 51 | "postcss": "^8.4.8", 52 | "prettier": "^2.8.8", 53 | "tailwindcss": "^3.0.23", 54 | "typescript": "^5.3.2" 55 | }, 56 | "packageManager": "yarn@3.2.3", 57 | "resolutions": { 58 | "@types/react": "npm:types-react@19.0.0-rc.1", 59 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" 60 | }, 61 | "overrides": { 62 | "react": "19.0.0-rc-66855b96-20241106", 63 | "react-dom": "19.0.0-rc-66855b96-20241106" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/storefront/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/storefront/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a11rew/medusa-payment-paystack/00fbb696b866e52fad679fbe1ae241513a850020/examples/storefront/public/favicon.ico -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(checkout)/checkout/page.tsx: -------------------------------------------------------------------------------- 1 | import { retrieveCart } from "@lib/data/cart" 2 | import { retrieveCustomer } from "@lib/data/customer" 3 | import PaymentWrapper from "@modules/checkout/components/payment-wrapper" 4 | import CheckoutForm from "@modules/checkout/templates/checkout-form" 5 | import CheckoutSummary from "@modules/checkout/templates/checkout-summary" 6 | import { Metadata } from "next" 7 | import { notFound } from "next/navigation" 8 | 9 | export const metadata: Metadata = { 10 | title: "Checkout", 11 | } 12 | 13 | export default async function Checkout() { 14 | const cart = await retrieveCart() 15 | 16 | if (!cart) { 17 | return notFound() 18 | } 19 | 20 | const customer = await retrieveCustomer() 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(checkout)/layout.tsx: -------------------------------------------------------------------------------- 1 | import LocalizedClientLink from "@modules/common/components/localized-client-link" 2 | import ChevronDown from "@modules/common/icons/chevron-down" 3 | import MedusaCTA from "@modules/layout/components/medusa-cta" 4 | 5 | export default function CheckoutLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode 9 | }) { 10 | return ( 11 |
12 |
13 | 36 |
37 |
{children}
38 |
39 | 40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(checkout)/not-found.tsx: -------------------------------------------------------------------------------- 1 | import InteractiveLink from "@modules/common/components/interactive-link" 2 | import { Metadata } from "next" 3 | 4 | export const metadata: Metadata = { 5 | title: "404", 6 | description: "Something went wrong", 7 | } 8 | 9 | export default async function NotFound() { 10 | return ( 11 |
12 |

Page not found

13 |

14 | The page you tried to access does not exist. 15 |

16 | Go to frontpage 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/account/@dashboard/addresses/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import { notFound } from "next/navigation" 3 | 4 | import AddressBook from "@modules/account/components/address-book" 5 | 6 | import { getRegion } from "@lib/data/regions" 7 | import { retrieveCustomer } from "@lib/data/customer" 8 | 9 | export const metadata: Metadata = { 10 | title: "Addresses", 11 | description: "View your addresses", 12 | } 13 | 14 | export default async function Addresses(props: { 15 | params: Promise<{ countryCode: string }> 16 | }) { 17 | const params = await props.params 18 | const { countryCode } = params 19 | const customer = await retrieveCustomer() 20 | const region = await getRegion(countryCode) 21 | 22 | if (!customer || !region) { 23 | notFound() 24 | } 25 | 26 | return ( 27 |
28 |
29 |

Shipping Addresses

30 |

31 | View and update your shipping addresses, you can add as many as you 32 | like. Saving your addresses will make them available during checkout. 33 |

34 |
35 | 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/account/@dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | import Spinner from "@modules/common/icons/spinner" 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/account/@dashboard/orders/details/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { retrieveOrder } from "@lib/data/orders" 2 | import OrderDetailsTemplate from "@modules/order/templates/order-details-template" 3 | import { Metadata } from "next" 4 | import { notFound } from "next/navigation" 5 | 6 | type Props = { 7 | params: Promise<{ id: string }> 8 | } 9 | 10 | export async function generateMetadata(props: Props): Promise { 11 | const params = await props.params 12 | const order = await retrieveOrder(params.id).catch(() => null) 13 | 14 | if (!order) { 15 | notFound() 16 | } 17 | 18 | return { 19 | title: `Order #${order.display_id}`, 20 | description: `View your order`, 21 | } 22 | } 23 | 24 | export default async function OrderDetailPage(props: Props) { 25 | const params = await props.params 26 | const order = await retrieveOrder(params.id).catch(() => null) 27 | 28 | if (!order) { 29 | notFound() 30 | } 31 | 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/account/@dashboard/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | import OrderOverview from "@modules/account/components/order-overview" 4 | import { notFound } from "next/navigation" 5 | import { listOrders } from "@lib/data/orders" 6 | import Divider from "@modules/common/components/divider" 7 | import TransferRequestForm from "@modules/account/components/transfer-request-form" 8 | 9 | export const metadata: Metadata = { 10 | title: "Orders", 11 | description: "Overview of your previous orders.", 12 | } 13 | 14 | export default async function Orders() { 15 | const orders = await listOrders() 16 | 17 | if (!orders) { 18 | notFound() 19 | } 20 | 21 | return ( 22 |
23 |
24 |

Orders

25 |

26 | View your previous orders and their status. You can also create 27 | returns or exchanges for your orders if needed. 28 |

29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/account/@dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | import Overview from "@modules/account/components/overview" 4 | import { notFound } from "next/navigation" 5 | import { retrieveCustomer } from "@lib/data/customer" 6 | import { listOrders } from "@lib/data/orders" 7 | 8 | export const metadata: Metadata = { 9 | title: "Account", 10 | description: "Overview of your account activity.", 11 | } 12 | 13 | export default async function OverviewTemplate() { 14 | const customer = await retrieveCustomer().catch(() => null) 15 | const orders = (await listOrders().catch(() => null)) || null 16 | 17 | if (!customer) { 18 | notFound() 19 | } 20 | 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | import ProfilePhone from "@modules/account//components/profile-phone" 4 | import ProfileBillingAddress from "@modules/account/components/profile-billing-address" 5 | import ProfileEmail from "@modules/account/components/profile-email" 6 | import ProfileName from "@modules/account/components/profile-name" 7 | import ProfilePassword from "@modules/account/components/profile-password" 8 | 9 | import { notFound } from "next/navigation" 10 | import { listRegions } from "@lib/data/regions" 11 | import { retrieveCustomer } from "@lib/data/customer" 12 | 13 | export const metadata: Metadata = { 14 | title: "Profile", 15 | description: "View and edit your Medusa Store profile.", 16 | } 17 | 18 | export default async function Profile() { 19 | const customer = await retrieveCustomer() 20 | const regions = await listRegions() 21 | 22 | if (!customer || !regions) { 23 | notFound() 24 | } 25 | 26 | return ( 27 |
28 |
29 |

Profile

30 |

31 | View and update your profile information, including your name, email, 32 | and phone number. You can also update your billing address, or change 33 | your password. 34 |

35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | {/* 44 | */} 45 | 46 |
47 |
48 | ) 49 | } 50 | 51 | const Divider = () => { 52 | return
53 | } 54 | ;`` 55 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/account/@login/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | import LoginTemplate from "@modules/account/templates/login-template" 4 | 5 | export const metadata: Metadata = { 6 | title: "Sign in", 7 | description: "Sign in to your Medusa Store account.", 8 | } 9 | 10 | export default function Login() { 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/account/layout.tsx: -------------------------------------------------------------------------------- 1 | import { retrieveCustomer } from "@lib/data/customer" 2 | import { Toaster } from "@medusajs/ui" 3 | import AccountLayout from "@modules/account/templates/account-layout" 4 | 5 | export default async function AccountPageLayout({ 6 | dashboard, 7 | login, 8 | }: { 9 | dashboard?: React.ReactNode 10 | login?: React.ReactNode 11 | }) { 12 | const customer = await retrieveCustomer().catch(() => null) 13 | 14 | return ( 15 | 16 | {customer ? dashboard : login} 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/account/loading.tsx: -------------------------------------------------------------------------------- 1 | import Spinner from "@modules/common/icons/spinner" 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/cart/loading.tsx: -------------------------------------------------------------------------------- 1 | import SkeletonCartPage from "@modules/skeletons/templates/skeleton-cart-page" 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/cart/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | import InteractiveLink from "@modules/common/components/interactive-link" 4 | 5 | export const metadata: Metadata = { 6 | title: "404", 7 | description: "Something went wrong", 8 | } 9 | 10 | export default function NotFound() { 11 | return ( 12 |
13 |

Page not found

14 |

15 | The cart you tried to access does not exist. Clear your cookies and try 16 | again. 17 |

18 | Go to frontpage 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/cart/page.tsx: -------------------------------------------------------------------------------- 1 | import { retrieveCart } from "@lib/data/cart" 2 | import { retrieveCustomer } from "@lib/data/customer" 3 | import CartTemplate from "@modules/cart/templates" 4 | import { Metadata } from "next" 5 | import { notFound } from "next/navigation" 6 | 7 | export const metadata: Metadata = { 8 | title: "Cart", 9 | description: "View your cart", 10 | } 11 | 12 | export default async function Cart() { 13 | const cart = await retrieveCart() 14 | const customer = await retrieveCustomer() 15 | 16 | if (!cart) { 17 | return notFound() 18 | } 19 | 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /examples/storefront/src/app/[countryCode]/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | import { listCartOptions, retrieveCart } from "@lib/data/cart" 4 | import { retrieveCustomer } from "@lib/data/customer" 5 | import { getBaseURL } from "@lib/util/env" 6 | import { StoreCartShippingOption } from "@medusajs/types" 7 | import CartMismatchBanner from "@modules/layout/components/cart-mismatch-banner" 8 | import Footer from "@modules/layout/templates/footer" 9 | import Nav from "@modules/layout/templates/nav" 10 | import FreeShippingPriceNudge from "@modules/shipping/components/free-shipping-price-nudge" 11 | 12 | export const metadata: Metadata = { 13 | metadataBase: new URL(getBaseURL()), 14 | } 15 | 16 | export default async function PageLayout(props: { children: React.ReactNode }) { 17 | const customer = await retrieveCustomer() 18 | const cart = await retrieveCart() 19 | let shippingOptions: StoreCartShippingOption[] = [] 20 | 21 | if (cart) { 22 | const { shipping_options } = await listCartOptions() 23 | 24 | shippingOptions = shipping_options 25 | } 26 | 27 | return ( 28 | <> 29 |