|
1 | 1 | --- |
| 2 | +sidebar_label: "Checkout Step 2: Set Address" |
2 | 3 | tags: |
3 | 4 | - cart |
4 | 5 | - storefront |
|
7 | 8 | import { CodeTabs, CodeTab } from "docs-ui" |
8 | 9 |
|
9 | 10 | export const metadata = { |
10 | | - title: `Checkout Step 2: Enter Address`, |
| 11 | + title: `Checkout Step 2: Set Shipping and Billing Addresses`, |
11 | 12 | } |
12 | 13 |
|
13 | 14 | # {metadata.title} |
14 | 15 |
|
15 | | -The second step of the checkout flow is to ask the customer for their address. |
| 16 | +The second step of the checkout flow is to ask the customer for their address. A cart has shipping and billing addresses that customers need to set. |
16 | 17 |
|
17 | | -{/* TODO add how to list addresses of logged in customer. */} |
| 18 | +You can either show a form to enter the address, or, if the customer is logged in, allow them to pick an address from their account. |
18 | 19 |
|
19 | | -A cart has shipping and billing addesses. Use the [Update Cart API route]() to update the cart's addresses. |
| 20 | +This guide shows you how to implement both approaches. You can choose either or combine them, based on your use case. |
| 21 | + |
| 22 | +## Approach One: Address Form |
| 23 | + |
| 24 | +The first approach to setting the cart's shipping and billing addresses is to show a form to the customer to enter their address details. To update the cart's address, use the [Update Cart API route](!api!/store#carts_postcartsid) to update the cart's addresses. |
20 | 25 |
|
21 | 26 | For example: |
22 | 27 |
|
@@ -210,5 +215,191 @@ In the example above: |
210 | 215 |
|
211 | 216 | - The same address is used for shipping and billing for simplicity. You can provide the option to enter both addresses instead. |
212 | 217 | - You send the address to the Update Cart API route under the `shipping_address` and `billing_address` request body parameters. |
213 | | -- The updated cart object is retuned in the response. |
| 218 | +- The updated cart object is returned in the response. |
214 | 219 | - **React example:** in the address, the chosen country must be in the cart's region. So, only the countries part of the cart's region are shown. |
| 220 | + |
| 221 | +--- |
| 222 | + |
| 223 | +## Approach Two: Select Customer Address |
| 224 | + |
| 225 | +The second approach to setting the cart's shipping and billing addresses is to allow the logged-in customer to select an address they added previously to their account. To retrieve the customer's addresses, use the [List Customer Addresses API route](!api!/store#customers_getcustomersmeaddresses). Then, once the customer selects an address, use the [Update Cart API route](!api!/store#carts_postcartsid) to update the cart's addresses. |
| 226 | + |
| 227 | +<Note title="Good to Know"> |
| 228 | + |
| 229 | +A customer's address and a cart's address are represented by different data models in the Medusa application, as they're managed by the [Customer Module](../../../commerce-modules/customer/page.mdx) and the [Cart Module](../../../commerce-modules/cart/page.mdx), respectively. So, addresses that the customer used previously during checkout aren't automatically saved to their account. You need to save the customer's address using the [Create Customer Address API route](!api!/store#customers_postcustomersmeaddresses). |
| 230 | + |
| 231 | +</Note> |
| 232 | + |
| 233 | +For example: |
| 234 | + |
| 235 | +<CodeTabs group="store-request"> |
| 236 | + <CodeTab label="Fetch API" value="fetch"> |
| 237 | + |
| 238 | +export const fetch2Highlights = [ |
| 239 | + ["1", "cartId", "Assuming the cart's ID is stored in the database."], |
| 240 | + ["3", "retrieveCustomerAddresses", "Retrieve the customer's addresses."], |
| 241 | + ["18", "updateCartAddress", "Update the cart's address with the selected customer address."], |
| 242 | + ["19", "address", "Map the customer address to the expected cart address."], |
| 243 | + ["39", "shipping_address", "Pass the selected address as a shipping address."], |
| 244 | + ["40", "billing_address", "Pass the selected address as a billing address."], |
| 245 | +] |
| 246 | + |
| 247 | +```ts highlights={fetch2Highlights} |
| 248 | +const cartId = localStorage.getItem("cart_id") |
| 249 | + |
| 250 | +const retrieveCustomerAddresses = () => { |
| 251 | + fetch("http://localhost:9000/store/customers/me/addresses", { |
| 252 | + credentials: "include", |
| 253 | + headers: { |
| 254 | + "Content-Type": "application/json", |
| 255 | + "x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp", |
| 256 | + }, |
| 257 | + }) |
| 258 | + .then((res) => res.json()) |
| 259 | + .then(({ addresses }) => { |
| 260 | + // use addresses... |
| 261 | + console.log(addresses) |
| 262 | + }) |
| 263 | +} |
| 264 | + |
| 265 | +const updateCartAddress = (customerAddress: Record<string, unknown>) => { |
| 266 | + const address = { |
| 267 | + first_name: customerAddress.first_name || "", |
| 268 | + last_name: customerAddress.last_name || "", |
| 269 | + address_1: customerAddress.address_1 || "", |
| 270 | + company: customerAddress.company || "", |
| 271 | + postal_code: customerAddress.postal_code || "", |
| 272 | + city: customerAddress.city || "", |
| 273 | + country_code: customerAddress.country_code || cart.region?.countries?.[0].iso_2, |
| 274 | + province: customerAddress.province || "", |
| 275 | + phone: customerAddress.phone || "" |
| 276 | + } |
| 277 | + |
| 278 | + fetch(`http://localhost:9000/store/carts/${cart.id}`, { |
| 279 | + credentials: "include", |
| 280 | + method: "POST", |
| 281 | + headers: { |
| 282 | + "Content-Type": "application/json", |
| 283 | + "x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp", |
| 284 | + }, |
| 285 | + body: JSON.stringify({ |
| 286 | + shipping_address: address, |
| 287 | + billing_address: address |
| 288 | + }) |
| 289 | + }) |
| 290 | + .then((res) => res.json()) |
| 291 | + .then(({ cart: updatedCart }) => { |
| 292 | + // use cart... |
| 293 | + console.log(cart) |
| 294 | + }) |
| 295 | +} |
| 296 | +``` |
| 297 | + |
| 298 | + </CodeTab> |
| 299 | + <CodeTab label="React" value="react"> |
| 300 | + |
| 301 | +export const react2Highlights = [ |
| 302 | + ["4", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."], |
| 303 | + ["5", "useCustomer", "The `useCustomer` hook was defined in the Customer React Context documentation."], |
| 304 | + ["11", "selectedAddress", "Store the ID of the address that the customer selects."], |
| 305 | + ["20", "updateAddress", "Update the cart's shipping and billing addresses based on the selected address."], |
| 306 | + ["31", "address", "Map the customer address to the expected cart address."], |
| 307 | + ["51", "shipping_address", "Pass the selected address as a shipping address."], |
| 308 | + ["52", "billing_address", "Pass the selected address as a billing address."], |
| 309 | + ["66", "select", "Show a dropdown to select the customer's address."], |
| 310 | +] |
| 311 | + |
| 312 | +```tsx highlights={react2Highlights} |
| 313 | +"use client" // include with Next.js 13+ |
| 314 | + |
| 315 | +import { useEffect, useState } from "react"; |
| 316 | +import { useCart } from "../../../providers/cart"; |
| 317 | +import { useCustomer } from "../../../providers/customer"; |
| 318 | + |
| 319 | +export default function CheckoutAddressStep () { |
| 320 | + const { cart, setCart } = useCart() |
| 321 | + const { customer } = useCustomer() |
| 322 | + const [loading, setLoading] = useState(false) |
| 323 | + const [selectedAddress, setSelectedAddress] = useState(customer?.addresses[0]?.id || "") |
| 324 | + |
| 325 | + useEffect(() => { |
| 326 | + if (!customer) { |
| 327 | + // TODO you can redirect here to another page or component that shows the address form |
| 328 | + } |
| 329 | + setSelectedAddress(customer?.addresses[0]?.id || "") |
| 330 | + }, [customer]) |
| 331 | + |
| 332 | + const updateAddress = ( |
| 333 | + e: React.MouseEvent<HTMLButtonElement, MouseEvent> |
| 334 | + ) => { |
| 335 | + e.preventDefault() |
| 336 | + |
| 337 | + const customerAddress = customer?.addresses.find((address) => address.id === selectedAddress) |
| 338 | + if (!cart || !customerAddress) { |
| 339 | + return |
| 340 | + } |
| 341 | + setLoading(true) |
| 342 | + |
| 343 | + const address = { |
| 344 | + first_name: customerAddress.first_name || "", |
| 345 | + last_name: customerAddress.last_name || "", |
| 346 | + address_1: customerAddress.address_1 || "", |
| 347 | + company: customerAddress.company || "", |
| 348 | + postal_code: customerAddress.postal_code || "", |
| 349 | + city: customerAddress.city || "", |
| 350 | + country_code: customerAddress.country_code || cart.region?.countries?.[0].iso_2, |
| 351 | + province: customerAddress.province || "", |
| 352 | + phone: customerAddress.phone || "" |
| 353 | + } |
| 354 | + |
| 355 | + fetch(`http://localhost:9000/store/carts/${cart.id}`, { |
| 356 | + credentials: "include", |
| 357 | + method: "POST", |
| 358 | + headers: { |
| 359 | + "Content-Type": "application/json", |
| 360 | + "x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp", |
| 361 | + }, |
| 362 | + body: JSON.stringify({ |
| 363 | + shipping_address: address, |
| 364 | + billing_address: address |
| 365 | + }) |
| 366 | + }) |
| 367 | + .then((res) => res.json()) |
| 368 | + .then(({ cart: updatedCart }) => { |
| 369 | + setCart(updatedCart) |
| 370 | + }) |
| 371 | + .finally(() => setLoading(false)) |
| 372 | + } |
| 373 | + |
| 374 | + return ( |
| 375 | + <form> |
| 376 | + {!cart && <span>Loading...</span>} |
| 377 | + {!customer?.addresses.length && <span>Customer doesn't have addresses</span>} |
| 378 | + <select value={selectedAddress} onChange={(e) => setSelectedAddress(e.target.value)}> |
| 379 | + {customer?.addresses.map((address) => ( |
| 380 | + <option value={address.id} key={address.id}>{address.country_code}</option> |
| 381 | + ))} |
| 382 | + </select> |
| 383 | + <button |
| 384 | + disabled={!cart || loading || !selectedAddress} |
| 385 | + onClick={updateAddress} |
| 386 | + > |
| 387 | + Save |
| 388 | + </button> |
| 389 | + </form> |
| 390 | + ) |
| 391 | +} |
| 392 | +``` |
| 393 | + |
| 394 | + </CodeTab> |
| 395 | +</CodeTabs> |
| 396 | + |
| 397 | +In the example above, you retrieve the customer's addresses and, when the customer selects an address, you update the cart's shipping and billing addresses with the selected address. |
| 398 | + |
| 399 | +In the React example, you use the [Customer React Context](../../customers/context/page.mdx) to retrieve the logged-in customer, who has a list of addresses. You show a dropdown to select the address, and when the customer selects an address, you send a request to update the cart's addresses. |
| 400 | + |
| 401 | +<Note title="Tip"> |
| 402 | + |
| 403 | +For both examples, you send a request as an authenticated customer using the cookie session. Learn about other options to send an authenticated request in [this guide](../../customers/login/page.mdx). |
| 404 | + |
| 405 | +</Note> |
0 commit comments