chore(docs): added eslint to lint documentation code blocks (#2920)
* docs: added rule for code length * chore: fixes based on vale errors * changed to use eslint * fixes using eslint * added github action for documentation eslint * changed allowed max-length * fixed incorrect heading level * removed comment
This commit is contained in:
@@ -97,7 +97,7 @@ Finally, in `medusa-config.js` add the following item into the `plugins` arr
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
//...
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-plugin-algolia`,
|
||||
options: {
|
||||
@@ -123,7 +123,7 @@ const plugins = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
```
|
||||
|
||||
The `searchableAttributes` are the attributes in a product that are searchable, and `attributesToRetrieve` are the attributes to retrieve for each product result. You’re free to make changes to these attributes as you see fit, but these are the recommended attributes.
|
||||
@@ -207,7 +207,7 @@ Finally, change the code in `src/lib/search-client.ts` to the following:
|
||||
```jsx title=src/lib/search-client.ts
|
||||
import algoliasearch from "algoliasearch/lite"
|
||||
|
||||
const appId = process.env.NEXT_PUBLIC_SEARCH_APP_ID || "" // You should add this to your environment variables
|
||||
const appId = process.env.NEXT_PUBLIC_SEARCH_APP_ID || ""
|
||||
|
||||
const apiKey = process.env.NEXT_PUBLIC_SEARCH_API_KEY || "test_key"
|
||||
|
||||
@@ -227,9 +227,7 @@ To make sure the Next.js storefront properly displays the products in the search
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Add to Gatsby and React-Based Storefronts
|
||||
### Add to Gatsby and React-Based Storefronts
|
||||
|
||||
This section covers adding the search UI to React-based storefronts. It uses the Gatsby storefront as an example, but you can use the same steps on any React-based framework.
|
||||
|
||||
@@ -269,11 +267,11 @@ import {
|
||||
Hits,
|
||||
InstantSearch,
|
||||
SearchBox,
|
||||
connectStateResults
|
||||
connectStateResults,
|
||||
} from "react-instantsearch-dom"
|
||||
|
||||
import React from "react"
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import algoliasearch from "algoliasearch/lite"
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
process.env.GATSBY_ALGOLIA_APP_ID,
|
||||
@@ -281,19 +279,28 @@ const searchClient = algoliasearch(
|
||||
)
|
||||
|
||||
const Search = () => {
|
||||
const Results = connectStateResults(({ searchState, searchResults, children }) =>
|
||||
searchState && searchState.query && searchResults && searchResults.nbHits !== 0 ? (
|
||||
<div className="absolute top-full w-full p-2 bg-gray-200 shadow-md">
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)
|
||||
);
|
||||
const Results = connectStateResults(
|
||||
({ searchState, searchResults, children }) => {
|
||||
return (
|
||||
searchState && searchState.query &&
|
||||
searchResults && searchResults.nbHits !== 0 ?
|
||||
(
|
||||
<div
|
||||
className="absolute top-full w-full p-2 bg-gray-200 shadow-md">
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<InstantSearch indexName={process.env.GATSBY_SEARCH_INDEX_NAME} searchClient={searchClient}>
|
||||
<InstantSearch
|
||||
indexName={process.env.GATSBY_SEARCH_INDEX_NAME}
|
||||
searchClient={searchClient}>
|
||||
<SearchBox submit={null} reset={null} />
|
||||
<Results>
|
||||
<Hits hitComponent={Hit} />
|
||||
@@ -313,7 +320,7 @@ const Hit = ({ hit }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Search;
|
||||
export default Search
|
||||
```
|
||||
|
||||
This file uses the dependencies you installed to show the search results. It also initializes Algolia using the environment variables you added.
|
||||
@@ -333,10 +340,12 @@ import Search from "./search"
|
||||
And add the `Search` component in the returned JSX before `RegionPopover`:
|
||||
|
||||
```jsx title=src/components/header/index.jsx
|
||||
//...
|
||||
<Search />
|
||||
<RegionPopover regions={mockData.regions} />
|
||||
//...
|
||||
// ...
|
||||
<div className="...">
|
||||
<Search />
|
||||
<RegionPopover regions={mockData.regions} />
|
||||
</div>
|
||||
// ...
|
||||
```
|
||||
|
||||
If you run your Gatsby storefront while the Medusa server is running, you should find a search bar in the header of the page. Try entering a query to search through the products in your store.
|
||||
|
||||
@@ -29,32 +29,32 @@ Here’s an example of a migration created in a new file `contentful-migrations/
|
||||
```jsx title=contentful-migrations/rich-text.js
|
||||
#! /usr/bin/env node
|
||||
|
||||
require("dotenv").config();
|
||||
require("dotenv").config()
|
||||
|
||||
const { runMigration } = require("contentful-migration");
|
||||
const { runMigration } = require("contentful-migration")
|
||||
|
||||
const options = {
|
||||
spaceId: process.env.CONTENTFUL_SPACE_ID,
|
||||
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
|
||||
environment: process.env.CONTENTFUL_ENVIRONMENT,
|
||||
yes: true,
|
||||
};
|
||||
}
|
||||
|
||||
const migration = async () => {
|
||||
await runMigration({
|
||||
...options,
|
||||
migrationFunction: function (migration, context) {
|
||||
|
||||
//create Rich Text content model
|
||||
// create Rich Text content model
|
||||
const richText = migration
|
||||
.createContentType("richText")
|
||||
.name("Rich Text")
|
||||
.displayField("title");
|
||||
.displayField("title")
|
||||
|
||||
richText.createField("title").name("Title (Internal)").type("Symbol");
|
||||
richText.createField("body").name("Body").type("RichText");
|
||||
richText.createField("title").name("Title (Internal)").type("Symbol")
|
||||
richText.createField("body").name("Body").type("RichText")
|
||||
|
||||
//edit Page content model
|
||||
// edit Page content model
|
||||
const page = migration.editContentType("page")
|
||||
|
||||
page.editField("contentModules").items({
|
||||
@@ -67,7 +67,7 @@ const migration = async () => {
|
||||
],
|
||||
})
|
||||
|
||||
//edit Product content model
|
||||
// edit Product content model
|
||||
const product = migration.editContentType("product")
|
||||
|
||||
product
|
||||
@@ -83,11 +83,11 @@ const migration = async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
migration();
|
||||
migration()
|
||||
```
|
||||
|
||||
This example creates a new content model Rich Text that has two fields: title and body. It also edits the Page content model to allow using Rich Text content models on the page.
|
||||
@@ -97,7 +97,19 @@ In addition, it edits the Product content model by adding a new field `contentMo
|
||||
You can also add other types of content models the `contentModules` should accept. For example, to accept `tileSection` add it to the `linkContentType` option:
|
||||
|
||||
```jsx
|
||||
linkContentType: ["tileSection", "richText"],
|
||||
product
|
||||
.createField("contentModules")
|
||||
.name("Content Modules")
|
||||
.type("Array")
|
||||
.items({
|
||||
type: "Link",
|
||||
linkType: "Entry",
|
||||
validations: [
|
||||
{
|
||||
linkContentType: ["richText", "tileSection"],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Run a Contentful Migration
|
||||
@@ -165,10 +177,10 @@ import { renderRichText } from "gatsby-source-contentful/rich-text"
|
||||
const RichText = ({ data }) => {
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: '870px',
|
||||
margin: '0 auto',
|
||||
paddingTop: '20px',
|
||||
paddingBottom: '20px'
|
||||
maxWidth: "870px",
|
||||
margin: "0 auto",
|
||||
paddingTop: "20px",
|
||||
paddingBottom: "20px",
|
||||
}}>
|
||||
{data.body ? renderRichText(data.body) : ""}
|
||||
</div>
|
||||
@@ -194,7 +206,7 @@ Then, in the returned JSX add a new case to the switch statement:
|
||||
|
||||
```jsx title=src/pages/{ContentfulPage.slug}.js
|
||||
switch (cm.internal.type) {
|
||||
//...
|
||||
// ...
|
||||
case "ContentfulRichText":
|
||||
return <RichText key={cm.id} data={cm} />
|
||||
default:
|
||||
|
||||
@@ -123,14 +123,14 @@ Then, in `medusa-config.js` in the exported object, comment out or remove the SQ
|
||||
```jsx title=medusa-config.js
|
||||
module.exports = {
|
||||
projectConfig: {
|
||||
//...
|
||||
// ...
|
||||
database_url: DATABASE_URL,
|
||||
database_type: "postgres",
|
||||
//REMOVE OR COMMENT OUT THE BELOW:
|
||||
// REMOVE OR COMMENT OUT THE BELOW:
|
||||
// database_database: "./medusa-db.sql",
|
||||
// database_type: "sqlite",
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Migrate Content Types to Contentful
|
||||
|
||||
@@ -55,22 +55,22 @@ Finally, in `medusa-config.js`, add the Klarna plugin to the `plugins` array wit
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
//other plugins...
|
||||
// other plugins...
|
||||
{
|
||||
resolve: `medusa-payment-klarnal`,
|
||||
options: {
|
||||
backend_url: process.env.KLARNA_BACKEND_URL
|
||||
backend_url: process.env.KLARNA_BACKEND_URL,
|
||||
url: process.env.KLARNA_URL,
|
||||
user: process.env.KLARNA_USER,
|
||||
password: process.env.KLARNA_PASSWORD,
|
||||
merchant_urls: {
|
||||
terms: process.env.KLARNA_TERMS_URL,
|
||||
checkout: process.env.KLARNA_CHECKOUT_URL,
|
||||
confirmation: process.env.KLARNA_CONFIRMATION_URL
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
confirmation: process.env.KLARNA_CONFIRMATION_URL,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -59,15 +59,15 @@ Open `medusa-config.js` and add the new plugin into the `plugins` array:
|
||||
|
||||
```js title=medusa-config.js
|
||||
const plugins = [
|
||||
...,
|
||||
// ...,
|
||||
{
|
||||
resolve: `medusa-plugin-mailchimp`,
|
||||
options: {
|
||||
api_key: process.env.MAILCHIMP_API_KEY,
|
||||
newsletter_list_id: process.env.MAILCHIMP_NEWSLETTER_LIST_ID
|
||||
}
|
||||
}
|
||||
];
|
||||
newsletter_list_id: process.env.MAILCHIMP_NEWSLETTER_LIST_ID,
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
@@ -147,35 +147,37 @@ npm install axios
|
||||
Then, in the component you want to add the subscription form add the following code:
|
||||
|
||||
```jsx
|
||||
import axios from 'axios'
|
||||
import { useState } from "react";
|
||||
import axios from "axios"
|
||||
import { useState } from "react"
|
||||
|
||||
export default function NewsletterForm() {
|
||||
const [email, setEmail] = useState("")
|
||||
|
||||
function subscribe(e) {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
if (!email) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
axios.post('http://localhost:9000/mailchimp/subscribe', {
|
||||
email
|
||||
axios.post("http://localhost:9000/mailchimp/subscribe", {
|
||||
email,
|
||||
})
|
||||
.then((e) => {
|
||||
alert("Subscribed sucessfully!")
|
||||
setEmail("")
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
alert("An error occurred");
|
||||
console.error(e)
|
||||
alert("An error occurred")
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={subscribe}>
|
||||
<h2>Sign Up for our newsletter</h2>
|
||||
<input type="email" name="email" id="email" placeholder="example@gmail.com"
|
||||
<input
|
||||
type="email" name="email" id="email"
|
||||
placeholder="example@gmail.com"
|
||||
value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
|
||||
@@ -57,11 +57,12 @@ Finally, in `medusa-config.js` add the following item into the `plugins` array:
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
//...
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-plugin-meilisearch`,
|
||||
options: {
|
||||
// config object passed when creating an instance of the MeiliSearch client
|
||||
// config object passed when creating an instance
|
||||
// of the MeiliSearch client
|
||||
config: {
|
||||
host: process.env.MEILISEARCH_HOST,
|
||||
apiKey: process.env.MEILISEARCH_API_KEY,
|
||||
@@ -71,12 +72,18 @@ const plugins = [
|
||||
products: {
|
||||
// MeiliSearch's setting options to be set on a particular index
|
||||
searchableAttributes: ["title", "description", "variant_sku"],
|
||||
displayedAttributes: ["title", "description", "variant_sku", "thumbnail", "handle"],
|
||||
displayedAttributes: [
|
||||
"title",
|
||||
"description",
|
||||
"variant_sku",
|
||||
"thumbnail",
|
||||
"handle",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
```
|
||||
|
||||
You can change the `searchableAttributes` and `displayedAttributes` as you see fit. However, the attributes included are the recommended attributes.
|
||||
@@ -220,7 +227,7 @@ import {
|
||||
Hits,
|
||||
InstantSearch,
|
||||
SearchBox,
|
||||
connectStateResults
|
||||
connectStateResults,
|
||||
} from "react-instantsearch-dom"
|
||||
|
||||
import React from "react"
|
||||
@@ -232,19 +239,28 @@ const searchClient = instantMeiliSearch(
|
||||
)
|
||||
|
||||
const Search = () => {
|
||||
const Results = connectStateResults(({ searchState, searchResults, children }) =>
|
||||
searchState && searchState.query && searchResults && searchResults.nbHits !== 0 ? (
|
||||
<div className="absolute top-full w-full p-2 bg-gray-200 shadow-md">
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)
|
||||
);
|
||||
const Results = connectStateResults(
|
||||
({ searchState, searchResults, children }) => {
|
||||
return (
|
||||
searchState && searchState.query &&
|
||||
searchResults && searchResults.nbHits !== 0 ?
|
||||
(
|
||||
<div
|
||||
className="absolute top-full w-full p-2 bg-gray-200 shadow-md">
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<InstantSearch indexName={process.env.GATSBY_SEARCH_INDEX_NAME} searchClient={searchClient}>
|
||||
<InstantSearch
|
||||
indexName={process.env.GATSBY_SEARCH_INDEX_NAME}
|
||||
searchClient={searchClient}>
|
||||
<SearchBox submit={null} reset={null} />
|
||||
<Results>
|
||||
<Hits hitComponent={Hit} />
|
||||
@@ -264,7 +280,7 @@ const Hit = ({ hit }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Search;
|
||||
export default Search
|
||||
```
|
||||
|
||||
This file uses the dependencies you installed to show the search results. It also initializes MeiliSearch using the environment variables you added.
|
||||
@@ -284,10 +300,12 @@ import Search from "./search"
|
||||
And add the `Search` component in the returned JSX before `RegionPopover`:
|
||||
|
||||
```jsx title=src/components/header/index.jsx
|
||||
//...
|
||||
<Search />
|
||||
<RegionPopover regions={mockData.regions} />
|
||||
//...
|
||||
// ...
|
||||
<div className="...">
|
||||
<Search />
|
||||
<RegionPopover regions={mockData.regions} />
|
||||
</div>
|
||||
// ...
|
||||
```
|
||||
|
||||
If you run your Gatsby storefront while the Medusa server and the MeiliSearch instance are running, you should find a search bar in the header of the page. Try entering a query to search through the products in your store.
|
||||
|
||||
@@ -104,7 +104,9 @@ Where `<ENDPOINT>` is the URL of your MinIO server, `<BUCKET>` is the name of th
|
||||
Finally, configure your `medusa-config.js` to include the plugin with the required options:
|
||||
|
||||
```js title=medusa-config.js
|
||||
{
|
||||
const plugins = [
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-file-minio`,
|
||||
options: {
|
||||
endpoint: process.env.MINIO_ENDPOINT,
|
||||
@@ -112,7 +114,8 @@ Finally, configure your `medusa-config.js` to include the plugin with the requ
|
||||
access_key_id: process.env.MINIO_ACCESS_KEY,
|
||||
secret_access_key: process.env.MINIO_SECRET_KEY,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
:::caution
|
||||
@@ -156,13 +159,16 @@ MINIO_PRIVATE_BUCKET=exports
|
||||
Then, add a new option to the plugin’s options in `medusa-config.js`:
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
{
|
||||
const plugins = [
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-file-minio`,
|
||||
options: {
|
||||
//...
|
||||
private_bucket: process.env.MINIO_PRIVATE_BUCKET
|
||||
// ...
|
||||
private_bucket: process.env.MINIO_PRIVATE_BUCKET,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### Use Different Secret and Access Keys
|
||||
@@ -181,14 +187,17 @@ Where `<YOUR_PRIVATE_ACCESS_KEY>` and `<YOUR_PRIVATE_SECRET_KEY>` are the access
|
||||
Then, add two new options to the plugin’s options in `medusa-config.js`:
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
{
|
||||
const plugins = [
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-file-minio`,
|
||||
options: {
|
||||
//...
|
||||
// ...
|
||||
private_access_key_id: process.env.MINIO_PRIVATE_ACCESS_KEY,
|
||||
private_secret_access_key: process.env.MINIO_PRIVATE_SECRET_KEY
|
||||
private_secret_access_key: process.env.MINIO_PRIVATE_SECRET_KEY,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
@@ -204,13 +213,13 @@ In `next.config.js` add the following option in the exported object:
|
||||
```jsx title=next.config.js
|
||||
const { withStoreConfig } = require("./store-config")
|
||||
|
||||
//...
|
||||
// ...
|
||||
|
||||
module.exports = withStoreConfig({
|
||||
//...
|
||||
// ...
|
||||
images: {
|
||||
domains: [
|
||||
//...
|
||||
// ...
|
||||
"127.0.0.1",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -57,17 +57,17 @@ Then, in `medusa-config.js`, add the PayPal plugin to the `plugins` array with t
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
//other plugins...
|
||||
// other plugins...
|
||||
{
|
||||
resolve: `medusa-payment-paypal`,
|
||||
options: {
|
||||
sandbox: process.env.PAYPAL_SANDBOX,
|
||||
client_id: process.env.PAYPAL_CLIENT_ID,
|
||||
client_secret: process.env.PAYPAL_CLIENT_SECRET,
|
||||
auth_webhook_id: process.env.PAYPAL_AUTH_WEBHOOK_ID
|
||||
}
|
||||
}
|
||||
];
|
||||
auth_webhook_id: process.env.PAYPAL_AUTH_WEBHOOK_ID,
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
That’s all you need to install PayPal on your Medusa server!
|
||||
@@ -158,12 +158,15 @@ npm install @paypal/react-paypal-js
|
||||
Next, create a new file `src/components/payment/paypal-payment/index.jsx` with the following content:
|
||||
|
||||
```jsx title=src/components/payment/paypal-payment/index.jsx
|
||||
import { PayPalButtons, PayPalScriptProvider } from "@paypal/react-paypal-js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import {
|
||||
PayPalButtons,
|
||||
PayPalScriptProvider,
|
||||
} from "@paypal/react-paypal-js"
|
||||
import React, { useMemo, useState } from "react"
|
||||
|
||||
import { navigate } from "gatsby"
|
||||
import { useCart } from "../../../hooks/use-cart"
|
||||
import { useMedusa } from "../../../hooks/use-medusa";
|
||||
import { useMedusa } from "../../../hooks/use-medusa"
|
||||
|
||||
const paypalClientId = process.env.GATSBY_PAYPAL_CLIENT_ID || ""
|
||||
|
||||
@@ -179,7 +182,7 @@ const PaypalPayment = () => {
|
||||
|
||||
const paypalSession = useMemo(() => {
|
||||
if (cart.payment_sessions) {
|
||||
return cart.payment_sessions.find(s => s.provider_id === "paypal")
|
||||
return cart.payment_sessions.find((s) => s.provider_id === "paypal")
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -200,10 +203,10 @@ const PaypalPayment = () => {
|
||||
await client.carts.updatePaymentSession(cart.id, "paypal", {
|
||||
data: {
|
||||
data: {
|
||||
...authorizationOrder
|
||||
}
|
||||
}
|
||||
});
|
||||
...authorizationOrder,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const order = await completeCart(cart.id)
|
||||
|
||||
@@ -218,10 +221,12 @@ const PaypalPayment = () => {
|
||||
|
||||
const handlePayment = (data, actions) => {
|
||||
actions.order.authorize().then((authorization) => {
|
||||
if (authorization.status !== 'COMPLETED') {
|
||||
setErrorMessage(`An error occurred, status: ${authorization.status}`);
|
||||
setProcessing(false);
|
||||
return;
|
||||
if (authorization.status !== "COMPLETED") {
|
||||
setErrorMessage(
|
||||
`An error occurred, status: ${authorization.status}`
|
||||
)
|
||||
setProcessing(false)
|
||||
return
|
||||
}
|
||||
|
||||
completeOrder(authorization)
|
||||
@@ -232,7 +237,7 @@ const PaypalPayment = () => {
|
||||
<PayPalScriptProvider options={{
|
||||
"client-id": paypalClientId,
|
||||
"currency": cart.region.currency_code.toUpperCase(),
|
||||
"intent": "authorize"
|
||||
"intent": "authorize",
|
||||
}}>
|
||||
{errorMessage && (
|
||||
<span className="text-rose-500 mt-4">{errorMessage}</span>
|
||||
@@ -246,7 +251,7 @@ const PaypalPayment = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default PaypalPayment;
|
||||
export default PaypalPayment
|
||||
```
|
||||
|
||||
Here’s briefly what this code snippet does:
|
||||
@@ -264,9 +269,11 @@ In `src/components/payment/index.js` you’ll find in the return statement a swi
|
||||
```jsx title=src/components/payment/index.js
|
||||
switch (ps.provider_id) {
|
||||
case "stripe":
|
||||
//...
|
||||
// ...
|
||||
break
|
||||
case "manual":
|
||||
//...
|
||||
// ...
|
||||
break
|
||||
case "paypal":
|
||||
return <PaypalPayment />
|
||||
default:
|
||||
@@ -303,28 +310,33 @@ Then, add the Client ID as an environment variable based on the framework you’
|
||||
Next, create the file that will hold the PayPal component with the following content:
|
||||
|
||||
```jsx
|
||||
import { PayPalButtons, PayPalScriptProvider } from "@paypal/react-paypal-js";
|
||||
import {
|
||||
PayPalButtons,
|
||||
PayPalScriptProvider,
|
||||
} from "@paypal/react-paypal-js"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import Medusa from "@medusajs/medusa-js"
|
||||
|
||||
function Paypal() {
|
||||
const client = new Medusa();
|
||||
const client = new Medusa()
|
||||
const [errorMessage, setErrorMessage] = useState(undefined)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const cart //TODO retrieve the cart here
|
||||
const cart = "..." // TODO retrieve the cart here
|
||||
|
||||
const handlePayment = (data, actions) => {
|
||||
actions.order.authorize().then(async (authorization) => {
|
||||
if (authorization.status !== 'COMPLETED') {
|
||||
setErrorMessage(`An error occurred, status: ${authorization.status}`);
|
||||
setProcessing(false);
|
||||
return;
|
||||
if (authorization.status !== "COMPLETED") {
|
||||
setErrorMessage(
|
||||
`An error occurred, status: ${authorization.status}`
|
||||
)
|
||||
setProcessing(false)
|
||||
return
|
||||
}
|
||||
|
||||
const response = await client.carts.setPaymentSession(cart.id, {
|
||||
"provider_id": "paypal"
|
||||
});
|
||||
"provider_id": "paypal",
|
||||
})
|
||||
|
||||
if (!response.cart) {
|
||||
setProcessing(false)
|
||||
@@ -334,30 +346,30 @@ function Paypal() {
|
||||
await client.carts.updatePaymentSession(cart.id, "paypal", {
|
||||
data: {
|
||||
data: {
|
||||
...authorization
|
||||
}
|
||||
}
|
||||
});
|
||||
...authorization,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const {data} = await client.carts.complete(cart.id)
|
||||
const { data } = await client.carts.complete(cart.id)
|
||||
|
||||
if (!data || data.object !== "order") {
|
||||
setProcessing(false)
|
||||
return
|
||||
}
|
||||
|
||||
//order successful
|
||||
// order successful
|
||||
alert("success")
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{marginTop: "10px", marginLeft: "10px"}}>
|
||||
<div style={{ marginTop: "10px", marginLeft: "10px" }}>
|
||||
{cart !== undefined && (
|
||||
<PayPalScriptProvider options={{
|
||||
"client-id": <CLIENT_ID>,
|
||||
"client-id": "<CLIENT_ID>",
|
||||
"currency": "EUR",
|
||||
"intent": "authorize"
|
||||
"intent": "authorize",
|
||||
}}>
|
||||
{errorMessage && (
|
||||
<span className="text-rose-500 mt-4">{errorMessage}</span>
|
||||
@@ -370,10 +382,10 @@ function Paypal() {
|
||||
</PayPalScriptProvider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Paypal;
|
||||
export default Paypal
|
||||
```
|
||||
|
||||
Here’s briefly what this code snippet does:
|
||||
|
||||
@@ -118,7 +118,7 @@ Finally, in `medusa-config.js`, add to the `plugins` array the following new ite
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
//...
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-file-s3`,
|
||||
options: {
|
||||
@@ -129,7 +129,7 @@ const plugins = [
|
||||
secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
```
|
||||
|
||||
### Add AWS Configurations
|
||||
@@ -140,7 +140,7 @@ For example, you can pass the `region`, `access_key_id` and `secret_access_key`
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
//...
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-file-s3`,
|
||||
options: {
|
||||
@@ -150,10 +150,10 @@ const plugins = [
|
||||
region: process.env.S3_REGION,
|
||||
access_key_id: process.env.S3_ACCESS_KEY_ID,
|
||||
secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
```
|
||||
|
||||
Make sure to define `S3_REGION`, `S3_ACCESS_KEY_ID`, and `S3_SECRET_ACCESS_KEY` in your environment variables first.
|
||||
@@ -198,14 +198,14 @@ In `next.config.js` add the following option in the exported object:
|
||||
```jsx title=next.config.js
|
||||
const { withStoreConfig } = require("./store-config")
|
||||
|
||||
//...
|
||||
// ...
|
||||
|
||||
module.exports = withStoreConfig({
|
||||
//...
|
||||
// ...
|
||||
images: {
|
||||
domains: [
|
||||
//...
|
||||
"<BUCKET_NAME>.s3.amazonaws.com"
|
||||
// ...
|
||||
"<BUCKET_NAME>.s3.amazonaws.com",
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -110,14 +110,14 @@ Finally, in `medusa-config.js`, add the following new item to the `plugins` arra
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
//...
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-plugin-segment`,
|
||||
options: {
|
||||
write_key: process.env.SEGMENT_WRITE_KEY,
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
@@ -155,24 +155,24 @@ For example, you can add the following subscriber to listen to the `customer.cre
|
||||
```jsx title=src/subscribers/customer.ts
|
||||
class CustomerSubscriber {
|
||||
constructor({ segmentService, eventBusService }) {
|
||||
this.segmentService = segmentService;
|
||||
this.segmentService = segmentService
|
||||
|
||||
eventBusService.subscribe("customer.created", this.handleCustomer);
|
||||
eventBusService.subscribe("customer.created", this.handleCustomer)
|
||||
}
|
||||
|
||||
handleCustomer = async (data) => {
|
||||
const customerData = data;
|
||||
delete customerData['password_hash'];
|
||||
const customerData = data
|
||||
delete customerData["password_hash"]
|
||||
|
||||
this.segmentService.track({
|
||||
event: 'Customer Created',
|
||||
event: "Customer Created",
|
||||
userId: data.id,
|
||||
properties: customerData
|
||||
properties: customerData,
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomerSubscriber;
|
||||
export default CustomerSubscriber
|
||||
```
|
||||
|
||||
You resolve the `SegmentService` using dependency injection. Then, when the `customer.created` event is triggered, you use the `track` method available in the `SegmentService` to send tracking data to Segment.
|
||||
|
||||
@@ -3928,7 +3928,7 @@ Finally, in your `medusa-config.js` file, add the SendGrid plugin into the array
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
...,
|
||||
// ...,
|
||||
{
|
||||
resolve: `medusa-plugin-sendgrid`,
|
||||
options: {
|
||||
@@ -3937,12 +3937,13 @@ const plugins = [
|
||||
order_placed_template: process.env.SENDGRID_ORDER_PLACED_ID,
|
||||
localization: {
|
||||
"de-DE": { // locale key
|
||||
order_placed_template: process.env.SENDGRID_ORDER_PLACED_ID_LOCALIZED,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
order_placed_template:
|
||||
process.env.SENDGRID_ORDER_PLACED_ID_LOCALIZED,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
The `api_key` and `from` options are required. Then, use the key of each template you create (from the [reference](#template-reference)) as the option name with the template ID as the value.
|
||||
|
||||
@@ -83,16 +83,16 @@ After that, open `medusa-config.js` and add the new plugin with its configuratio
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
...,
|
||||
// ...,
|
||||
{
|
||||
resolve: `medusa-plugin-slack-notification`,
|
||||
options: {
|
||||
show_discount_code: false,
|
||||
slack_url: `<WEBHOOK_URL>`,
|
||||
admin_orders_url: `http://localhost:7001/a/orders`
|
||||
}
|
||||
}
|
||||
];
|
||||
admin_orders_url: `http://localhost:7001/a/orders`,
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
- Make sure to change `<WEBHOOK_URL>` with the Webhook URL you copied after creating the Slack app.
|
||||
|
||||
@@ -105,7 +105,7 @@ Finally, in `medusa-config.js` add a new item to the `plugins` array:
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
//...
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-file-spaces`,
|
||||
options: {
|
||||
@@ -116,7 +116,7 @@ const plugins = [
|
||||
secret_access_key: process.env.SPACE_SECRET_ACCESS_KEY,
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
```
|
||||
|
||||
:::caution
|
||||
@@ -158,14 +158,14 @@ In `next.config.js` add the following option in the exported object:
|
||||
```jsx title=next.config.js
|
||||
const { withStoreConfig } = require("./store-config")
|
||||
|
||||
//...
|
||||
// ...
|
||||
|
||||
module.exports = withStoreConfig({
|
||||
//...
|
||||
// ...
|
||||
images: {
|
||||
domains: [
|
||||
//...
|
||||
"<YOUR_SPACE_DOMAIN>"
|
||||
// ...
|
||||
"<YOUR_SPACE_DOMAIN>",
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -94,10 +94,10 @@ Once the command is done executing, change to the newly created `medusa-server`
|
||||
module.exports = {
|
||||
projectConfig: {
|
||||
redis_url: REDIS_URL,
|
||||
//...
|
||||
}
|
||||
//...
|
||||
};
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
This uses the default Redis configurations. If you want to learn more about configuring Redis, [check out this documentation](../usage/configurations.md#redis).
|
||||
@@ -140,18 +140,18 @@ Finally, open `medusa-config.js` and add the following new item to the `plugins`
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
//...
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-plugin-strapi`,
|
||||
options: {
|
||||
strapi_medusa_user: process.env.STRAPI_USER,
|
||||
strapi_medusa_password: process.env.STRAPI_PASSWORD,
|
||||
strapi_url: process.env.STRAPI_URL, //optional
|
||||
strapi_port: process.env.STRAPI_PORT, //optional
|
||||
strapi_protocol: process.env.STRAPI_PROTOCOL //optional
|
||||
}
|
||||
}
|
||||
];
|
||||
strapi_url: process.env.STRAPI_URL, // optional
|
||||
strapi_port: process.env.STRAPI_PORT, // optional
|
||||
strapi_protocol: process.env.STRAPI_PROTOCOL, // optional
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -48,7 +48,7 @@ In `medusa-config.js` add the following at the end of the `plugins` array:
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
...,
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-payment-stripe`,
|
||||
options: {
|
||||
@@ -56,7 +56,7 @@ const plugins = [
|
||||
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
```
|
||||
|
||||
:::note
|
||||
@@ -75,7 +75,7 @@ You’ll first retrieve the API key. You can find it by choosing API Keys from t
|
||||
|
||||
Next, you need to add the key to your environment variables. In your Medusa server, create `.env` if it doesn’t already exist and add the Stripe key:
|
||||
|
||||
```jsx
|
||||
```bash
|
||||
STRIPE_API_KEY=sk_...
|
||||
```
|
||||
|
||||
@@ -95,7 +95,7 @@ Then, you can add a description. You must select at least one event to listen to
|
||||
|
||||
After the Webhook is created, you’ll see "Signing secret" in the Webhook details. Click on "Reveal" to reveal the secret key. Copy that key and in your Medusa server add the Webhook secret environment variable:
|
||||
|
||||
```jsx
|
||||
```bash
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
```
|
||||
|
||||
@@ -195,30 +195,30 @@ In this section, you’ll initialize Stripe without Medusa’s checkout workflow
|
||||
Create a container component that will hold the payment card component:
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
import { useState } from "react"
|
||||
|
||||
import {Elements} from '@stripe/react-stripe-js';
|
||||
import Form from './Form';
|
||||
import {loadStripe} from '@stripe/stripe-js';
|
||||
import { Elements } from "@stripe/react-stripe-js"
|
||||
import Form from "./Form"
|
||||
import { loadStripe } from "@stripe/stripe-js"
|
||||
|
||||
const stripePromise = loadStripe('pk_...');
|
||||
const stripePromise = loadStripe("pk_...")
|
||||
|
||||
export default function Container() {
|
||||
const [clientSecret, setClientSecret] = useState()
|
||||
|
||||
//TODO set clientSecret
|
||||
// TODO set clientSecret
|
||||
|
||||
return (
|
||||
<div>
|
||||
{clientSecret && (
|
||||
<Elements stripe={stripePromise} options={{
|
||||
clientSecret
|
||||
clientSecret,
|
||||
}}>
|
||||
<Form clientSecret={clientSecret} cartId={cartId} />
|
||||
</Elements>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
@@ -237,15 +237,19 @@ Once the clientSecret is set, the `Elements` Stripe component will wrap a `Form`
|
||||
Create a new file for the `Form` component with the following content:
|
||||
|
||||
```jsx
|
||||
import {CardElement, useElements, useStripe} from '@stripe/react-stripe-js';
|
||||
import {
|
||||
CardElement,
|
||||
useElements,
|
||||
useStripe,
|
||||
} from "@stripe/react-stripe-js"
|
||||
|
||||
export default function Form({clientSecret, cartId}) {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
export default function Form({ clientSecret, cartId }) {
|
||||
const stripe = useStripe()
|
||||
const elements = useElements()
|
||||
|
||||
async function handlePayment(e) {
|
||||
e.preventDefault()
|
||||
//TODO handle payment
|
||||
// TODO handle payment
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -253,7 +257,7 @@ export default function Form({clientSecret, cartId}) {
|
||||
<CardElement />
|
||||
<button onClick={handlePayment}>Submit</button>
|
||||
</form>
|
||||
);
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
@@ -267,8 +271,8 @@ You’ll now implement the workflow explained earlier. You’ll use Medusa’s J
|
||||
import Medusa from "@medusajs/medusa-js"
|
||||
|
||||
export default function Container() {
|
||||
const client = new Medusa();
|
||||
...
|
||||
const client = new Medusa()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -282,19 +286,21 @@ Then, in the place of the `//TODO` inside the `Container` element, initialize th
|
||||
|
||||
```jsx
|
||||
client.carts.createPaymentSessions(cart.id)
|
||||
.then(({cart}) => {
|
||||
//check if stripe is selected
|
||||
const isStripeAvailable = cart.payment_sessions?.some((session) => session.provider_id === 'stripe');
|
||||
.then(({ cart }) => {
|
||||
// check if stripe is selected
|
||||
const isStripeAvailable = cart.payment_sessions?.some((session) => (
|
||||
session.provider_id === "stripe"
|
||||
))
|
||||
if (!isStripeAvailable) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
//select stripe payment session
|
||||
// select stripe payment session
|
||||
client.carts.setPaymentSession(cart.id, {
|
||||
provider_id: 'stripe'
|
||||
}).then(({cart}) => {
|
||||
setClientSecret(cart.payment_session.data.client_secret);
|
||||
});
|
||||
provider_id: "stripe",
|
||||
}).then(({ cart }) => {
|
||||
setClientSecret(cart.payment_session.data.client_secret)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
@@ -314,8 +320,8 @@ As you’ll use Medusa’s client again make sure to import it and initialize it
|
||||
import Medusa from "@medusajs/medusa-js"
|
||||
|
||||
export default function Form() {
|
||||
const client = new Medusa();
|
||||
...
|
||||
const client = new Medusa()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -335,12 +341,12 @@ return stripe.confirmCardPayment(clientSecret, {
|
||||
line1,
|
||||
line2,
|
||||
postal_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}).then(({ error, paymentIntent }) => {
|
||||
//TODO handle errors
|
||||
client.carts.complete(cartId).then(resp => console.log(resp))
|
||||
// TODO handle errors
|
||||
client.carts.complete(cartId).then((resp) => console.log(resp))
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -52,16 +52,16 @@ Finally, add the plugin and its options in the `medusa-config.js` file to the `p
|
||||
|
||||
```jsx title=medusa-config.js
|
||||
const plugins = [
|
||||
...,
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-plugin-twilio-sms`,
|
||||
options: {
|
||||
account_sid: process.env.TWILIO_SMS_ACCOUNT_SID,
|
||||
auth_token: process.env.TWILIO_SMS_AUTH_TOKEN,
|
||||
from_number: process.env.TWILIO_SMS_FROM_NUMBER
|
||||
}
|
||||
}
|
||||
];
|
||||
from_number: process.env.TWILIO_SMS_FROM_NUMBER,
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
@@ -83,27 +83,27 @@ Create the file `src/services/sms.js` in your Medusa server with the following c
|
||||
```jsx title=src/services/sms.js
|
||||
class SmsSubscriber {
|
||||
constructor({ twilioSmsService, orderService, eventBusService }) {
|
||||
this.twilioSmsService_ = twilioSmsService;
|
||||
this.orderService = orderService;
|
||||
this.twilioSmsService_ = twilioSmsService
|
||||
this.orderService = orderService
|
||||
|
||||
eventBusService.subscribe("order.placed", this.sendSMS);
|
||||
eventBusService.subscribe("order.placed", this.sendSMS)
|
||||
}
|
||||
|
||||
sendSMS = async (data) => {
|
||||
const order = await this.orderService.retrieve(data.id, {
|
||||
relations: ['shipping_address']
|
||||
});
|
||||
relations: ["shipping_address"],
|
||||
})
|
||||
|
||||
if (order.shipping_address.phone) {
|
||||
this.twilioSmsService_.sendSms({
|
||||
to: order.shipping_address.phone,
|
||||
body: 'We have received your order #' + data.id,
|
||||
body: "We have received your order #" + data.id,
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default SmsSubscriber;
|
||||
export default SmsSubscriber
|
||||
```
|
||||
|
||||
In the `constructor`, you resolve the `twilioSmsService` and `orderService` using dependency injection to use it later in the `sendSMS` method.
|
||||
|
||||
Reference in New Issue
Block a user