From 4c9a146db31377d511c9ee2d3979c9420ee011ae Mon Sep 17 00:00:00 2001 From: mateo-agilo Date: Fri, 6 Feb 2026 15:40:15 +0100 Subject: [PATCH 01/71] feat: Implement return management features including return creation and tracking - Add SidebarNav component with a link to "My returns" - Create OrderItem component for displaying individual order items - Implement RefundMethodSelect for selecting refund methods - Add ReturnProductItem for managing return items with quantity and reason selection - Create ReturnReasonSelect for selecting return reasons - Implement ReturnShippingOptions for choosing return shipping methods - Add ReturnStatusTracker to display the status of returns - Create ReturnSummary to summarize return details and refund amounts - Implement ReturnCreationTemplate for managing return requests - Add ReturnTrackingTemplate for tracking return status and details --- .../account/my-orders/[orderId]/page.tsx | 96 ++--- .../my-orders/[orderId]/return/page.tsx | 38 ++ .../(main)/account/my-orders/page.tsx | 54 ++- .../account/returns/[returnId]/page.tsx | 78 ++++ .../(main)/account/returns/page.tsx | 116 ++++++ .../(main)/return-policy/page.tsx | 116 ++++++ .../modules/account/components/SidebarNav.tsx | 13 +- .../order/components/item/OrderItem.tsx | 83 +++++ .../returns/components/RefundMethodSelect.tsx | 70 ++++ .../returns/components/ReturnProductItem.tsx | 176 +++++++++ .../returns/components/ReturnReasonSelect.tsx | 66 ++++ .../components/ReturnShippingOptions.tsx | 82 +++++ .../components/ReturnStatusTracker.tsx | 66 ++++ .../returns/components/ReturnSummary.tsx | 105 ++++++ .../templates/ReturnCreationTemplate.tsx | 343 ++++++++++++++++++ .../templates/ReturnTrackingTemplate.tsx | 169 +++++++++ 16 files changed, 1595 insertions(+), 76 deletions(-) create mode 100644 storefront/src/app/[countryCode]/(main)/account/my-orders/[orderId]/return/page.tsx create mode 100644 storefront/src/app/[countryCode]/(main)/account/returns/[returnId]/page.tsx create mode 100644 storefront/src/app/[countryCode]/(main)/account/returns/page.tsx create mode 100644 storefront/src/app/[countryCode]/(main)/return-policy/page.tsx create mode 100644 storefront/src/modules/order/components/item/OrderItem.tsx create mode 100644 storefront/src/modules/returns/components/RefundMethodSelect.tsx create mode 100644 storefront/src/modules/returns/components/ReturnProductItem.tsx create mode 100644 storefront/src/modules/returns/components/ReturnReasonSelect.tsx create mode 100644 storefront/src/modules/returns/components/ReturnShippingOptions.tsx create mode 100644 storefront/src/modules/returns/components/ReturnStatusTracker.tsx create mode 100644 storefront/src/modules/returns/components/ReturnSummary.tsx create mode 100644 storefront/src/modules/returns/templates/ReturnCreationTemplate.tsx create mode 100644 storefront/src/modules/returns/templates/ReturnTrackingTemplate.tsx diff --git a/storefront/src/app/[countryCode]/(main)/account/my-orders/[orderId]/page.tsx b/storefront/src/app/[countryCode]/(main)/account/my-orders/[orderId]/page.tsx index 0a4afcae..c74d4f6d 100644 --- a/storefront/src/app/[countryCode]/(main)/account/my-orders/[orderId]/page.tsx +++ b/storefront/src/app/[countryCode]/(main)/account/my-orders/[orderId]/page.tsx @@ -1,20 +1,19 @@ import * as React from "react" import { Metadata } from "next" -import Image from "next/image" import { HttpTypes } from "@medusajs/types" -import { convertToLocale } from "@lib/util/money" import { retrieveOrder } from "@lib/data/orders" import { OrderTotals } from "@modules/order/components/OrderTotals" +import { OrderItem } from "@modules/order/components/item/OrderItem" import { UiTag } from "@/components/ui/Tag" import { UiTagList, UiTagListDivider } from "@/components/ui/TagList" import { Icon } from "@/components/Icon" -import { LocalizedLink } from "@/components/LocalizedLink" +import { ButtonLink } from "@/components/Button" import { getCustomer } from "@lib/data/customer" import { redirect } from "next/navigation" export const metadata: Metadata = { - title: "Account - Order", + title: "Account - Order Details", description: "Check your order history", } @@ -101,7 +100,7 @@ export default async function AccountOrderPage({ return ( <> -

+

Order: {order.display_id}

@@ -197,61 +196,19 @@ export default async function AccountOrderPage({
{order.items?.map((item) => ( -
- {item.thumbnail && ( - - {item.title} - - )} -
-

- - {item.product_title} - -

-
-
- {item.variant?.options?.map((option) => ( -

- - {option.option?.title}: - - {option.value} -

- ))} -
-
-
-

- - Quantity: - - {item.quantity} -

-
-
-

- {convertToLocale({ - currency_code: order.currency_code, - amount: item.total, - })} -

-
-
-
-
-
+ /> ))}
@@ -263,6 +220,29 @@ export default async function AccountOrderPage({
+
+
+
+
+ +

Returns

+
+

+ Returns are available for the first 14 days after receiving your + items. +

+
+ {order.fulfillment_status === "delivered" && ( + + Start Return + + )} +
+
) diff --git a/storefront/src/app/[countryCode]/(main)/account/my-orders/[orderId]/return/page.tsx b/storefront/src/app/[countryCode]/(main)/account/my-orders/[orderId]/return/page.tsx new file mode 100644 index 00000000..f02afb64 --- /dev/null +++ b/storefront/src/app/[countryCode]/(main)/account/my-orders/[orderId]/return/page.tsx @@ -0,0 +1,38 @@ +import { Metadata } from "next" +import { redirect } from "next/navigation" + +import { getCustomer } from "@lib/data/customer" +import { retrieveOrder } from "@lib/data/orders" +import { ReturnCreationTemplate } from "@modules/returns/templates/ReturnCreationTemplate" + +export const metadata: Metadata = { + title: "Account - Return Items", + description: "Request a return for your order", +} + +type Props = { + params: Promise<{ orderId: string }> +} + +export default async function ReturnPage({ params }: Props) { + const customer = await getCustomer().catch(() => null) + + if (!customer) { + redirect("/") + } + + const { orderId } = await params + const order = await retrieveOrder(orderId) + + if (!order) { + redirect("/account/my-orders") + } + + const isEligible = order.fulfillment_status === "delivered" + + if (!isEligible) { + redirect(`/account/my-orders/${orderId}`) + } + + return +} diff --git a/storefront/src/app/[countryCode]/(main)/account/my-orders/page.tsx b/storefront/src/app/[countryCode]/(main)/account/my-orders/page.tsx index cfda16a0..07d9ce53 100644 --- a/storefront/src/app/[countryCode]/(main)/account/my-orders/page.tsx +++ b/storefront/src/app/[countryCode]/(main)/account/my-orders/page.tsx @@ -144,22 +144,44 @@ export default async function AccountMyOrdersPage({ searchParams }: PageProps) {
- - Check details - - - Check details - +
+ {order.fulfillment_status === "delivered" && ( + <> + + Return + + + Return + + + )} + + Check details + + + Check details + +
))} diff --git a/storefront/src/app/[countryCode]/(main)/account/returns/[returnId]/page.tsx b/storefront/src/app/[countryCode]/(main)/account/returns/[returnId]/page.tsx new file mode 100644 index 00000000..94c8554a --- /dev/null +++ b/storefront/src/app/[countryCode]/(main)/account/returns/[returnId]/page.tsx @@ -0,0 +1,78 @@ +import { Metadata } from "next" +import { redirect } from "next/navigation" + +import { getCustomer } from "@lib/data/customer" +import { ReturnTrackingTemplate } from "@modules/returns/templates/ReturnTrackingTemplate" + +export const metadata: Metadata = { + title: "Account - Return Details", + description: "View your return request details", +} + +type Props = { + params: Promise<{ returnId: string }> +} + +// Mock function to get return data +// In a real app, this would fetch from the API +async function getReturnData(returnId: string) { + // Mock data for demo + return { + id: returnId, + orderId: "order_123", + orderDisplayId: 1234, + status: "approved" as const, + createdAt: "2026-01-28T10:00:00Z", + items: [ + { + id: "item_1", + title: "Classic T-Shirt - Black / M", + productTitle: "Classic T-Shirt", + product_title: "Classic T-Shirt", + product_handle: "classic-t-shirt", + thumbnail: + "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + quantity: 1, + unitPrice: 2900, + reason: "size_issue" as const, + variant: { + options: [ + { + id: "opt_1", + option: { title: "Color" }, + value: "Black", + }, + { + id: "opt_2", + option: { title: "Size" }, + value: "M", + }, + ], + }, + }, + ], + totalRefund: 2900, + currencyCode: "eur", + refundMethod: "original_payment" as const, + shippingMethod: "prepaid_label", + trackingNumber: undefined, + estimatedRefundDate: "2026-02-10", + } +} + +export default async function AccountReturnPage({ params }: Props) { + const customer = await getCustomer().catch(() => null) + + if (!customer) { + redirect("/") + } + + const { returnId } = await params + const returnData = await getReturnData(returnId) + + if (!returnData) { + redirect("/account/my-orders") + } + + return +} diff --git a/storefront/src/app/[countryCode]/(main)/account/returns/page.tsx b/storefront/src/app/[countryCode]/(main)/account/returns/page.tsx new file mode 100644 index 00000000..f6ed3ed0 --- /dev/null +++ b/storefront/src/app/[countryCode]/(main)/account/returns/page.tsx @@ -0,0 +1,116 @@ +import { Metadata } from "next" +import { redirect } from "next/navigation" + +import { getCustomer } from "@lib/data/customer" +import { LocalizedLink } from "@/components/LocalizedLink" +import { ButtonLink } from "@/components/Button" +import { + ReturnStatusTracker, + ReturnStatus, +} from "@modules/returns/components/ReturnStatusTracker" +import { convertToLocale } from "@lib/util/money" + +export const metadata: Metadata = { + title: "Account - Returns", + description: "View all your return requests", +} + +// Mock function to get returns data +// In a real app, this would fetch from the API +async function getReturns() { + // Mock data for demo + return [ + { + id: "ret_01h4ghqt9j7z8xr2n3k5m6v7w8", + orderDisplayId: 1234, + status: "shipped" as ReturnStatus, + createdAt: "2026-01-28T10:00:00Z", + totalRefund: 5900, + currencyCode: "eur", + itemsCount: 2, + thumbnail: null, + }, + { + id: "ret_02h4ghqt9j7z8xr2n3k5m6v7w9", + orderDisplayId: 1180, + status: "refunded" as ReturnStatus, + createdAt: "2026-01-15T10:00:00Z", + totalRefund: 12900, + currencyCode: "eur", + itemsCount: 1, + thumbnail: null, + }, + ] +} + +export default async function AccountReturnsPage() { + const customer = await getCustomer().catch(() => null) + + if (!customer) { + redirect("/") + } + + const returns = await getReturns() + + return ( + <> +

My returns

+ + {returns.length > 0 ? ( +
+ {returns.map((returnItem) => ( +
+
+
+
+ + Return: # + {returnItem.id.slice(-8).toUpperCase()} + +

+ Order #{returnItem.orderDisplayId} +

+
+
+

+ {convertToLocale({ + currency_code: returnItem.currencyCode, + amount: returnItem.totalRefund, + })} +

+

+ {returnItem.itemsCount} item + {returnItem.itemsCount > 1 ? "s" : ""} +

+
+
+ +
+
+ +
+ + View Details + +
+
+
+ ))} +
+ ) : ( +

You don't have any returns yet

+ )} + + ) +} diff --git a/storefront/src/app/[countryCode]/(main)/return-policy/page.tsx b/storefront/src/app/[countryCode]/(main)/return-policy/page.tsx new file mode 100644 index 00000000..ff10c3ed --- /dev/null +++ b/storefront/src/app/[countryCode]/(main)/return-policy/page.tsx @@ -0,0 +1,116 @@ +import { Metadata } from "next" +import { Icon } from "@/components/Icon" + +export const metadata: Metadata = { + title: "Return Policy", + description: "Learn about our return and refund policy", +} + +export default function ReturnPolicyPage() { + return ( +
+

Return Policy

+ +
+
+

Return Window

+

+ You have 30 days from the date of delivery to return most items for + a full refund. Items must be in their original condition with all + tags attached. +

+
+ +
+

Eligibility

+

+ To be eligible for a return, your item must meet the following + criteria: +

+
    +
  • Unworn and unwashed
  • +
  • In original packaging with all tags attached
  • +
  • Not marked as final sale or non-returnable
  • +
  • Returned within 30 days of delivery
  • +
+
+ +
+

Non-Returnable Items

+

+ The following items cannot be returned: +

+
    +
  • Intimate apparel and swimwear
  • +
  • Personalized or custom-made items
  • +
  • Items marked as final sale
  • +
  • Gift cards
  • +
+
+ +
+

Refund Process

+

+ Once we receive and inspect your return, we will process your refund + within 5-10 business days. Refunds will be credited to your original + payment method. +

+
+
+ +

+ Store Credit Option: Choose store credit for + your refund and receive a 10% bonus on your refund amount! +

+
+
+
+ +
+

How to Start a Return

+
    +
  1. Log in to your account and go to My Orders
  2. +
  3. Select the order containing the item(s) you wish to return
  4. +
  5. Click "Start Return" and select the items
  6. +
  7. Choose your refund method and shipping option
  8. +
  9. Print your prepaid return label
  10. +
  11. + Pack your items securely and drop off at any carrier location +
  12. +
+
+ +
+

Return Shipping

+

+ We offer free return shipping on all orders. Simply use the prepaid + return label provided when you initiate your return. +

+
+ +
+

Exchanges

+

+ We currently do not offer direct exchanges. To get a different size + or color, please return your item for a refund and place a new + order. +

+
+ +
+

Questions?

+

+ If you have any questions about our return policy, please contact + our customer support team at{" "} + + support@example.com + +

+
+
+
+ ) +} diff --git a/storefront/src/modules/account/components/SidebarNav.tsx b/storefront/src/modules/account/components/SidebarNav.tsx index a28fd1ba..330c2895 100644 --- a/storefront/src/modules/account/components/SidebarNav.tsx +++ b/storefront/src/modules/account/components/SidebarNav.tsx @@ -17,7 +17,7 @@ export const SidebarNav: React.FC = () => { @@ -26,12 +26,21 @@ export const SidebarNav: React.FC = () => { My orders + + My returns + ) } diff --git a/storefront/src/modules/order/components/item/OrderItem.tsx b/storefront/src/modules/order/components/item/OrderItem.tsx new file mode 100644 index 00000000..c840df2d --- /dev/null +++ b/storefront/src/modules/order/components/item/OrderItem.tsx @@ -0,0 +1,83 @@ +import * as React from "react" +import Image from "next/image" +import { LocalizedLink } from "@/components/LocalizedLink" +import { convertToLocale } from "@lib/util/money" + +type OrderItemProps = { + id: string + thumbnail: string | null + product_handle: string | null + product_title: string | null + title: string + quantity: number + currencyCode: string + amount: number + variant?: { + options?: Array<{ + id: string + option?: { title?: string } | null + value: string + }> | null + } + className?: string +} + +export const OrderItem: React.FC = ({ + thumbnail, + product_handle, + product_title, + title, + quantity, + currencyCode, + amount, + variant, + className, +}) => { + return ( +
+ {thumbnail && ( + + {title} + + )} +
+

+ + {product_title} + +

+
+
+ {variant?.options?.map((option) => ( +

+ + {option.option?.title}: + + {option.value} +

+ ))} +
+
+
+

+ Quantity: + {quantity} +

+
+
+

+ {convertToLocale({ + currency_code: currencyCode, + amount: amount, + })} +

+
+
+
+
+
+ ) +} diff --git a/storefront/src/modules/returns/components/RefundMethodSelect.tsx b/storefront/src/modules/returns/components/RefundMethodSelect.tsx new file mode 100644 index 00000000..50c07877 --- /dev/null +++ b/storefront/src/modules/returns/components/RefundMethodSelect.tsx @@ -0,0 +1,70 @@ +"use client" + +import * as React from "react" +import { twMerge } from "tailwind-merge" +import { + UiRadioGroup, + UiRadio, + UiRadioBox, + UiRadioLabel, +} from "@/components/ui/Radio" +import { Icon } from "@/components/Icon" + +export type RefundMethod = "original_payment" | "store_credit" + +type RefundMethodSelectProps = { + value: RefundMethod + onChange: (value: RefundMethod) => void + storeCreditBonus?: number + className?: string +} + +export const RefundMethodSelect: React.FC = ({ + value, + onChange, + storeCreditBonus = 10, + className, +}) => { + return ( + onChange(newValue as RefundMethod)} + className={twMerge("flex flex-col gap-3", className)} + > + + +
+ +
+ + Original payment method + + + Refund to the card/account used for purchase + +
+
+
+ + + +
+ +
+ + Store credit + {storeCreditBonus > 0 && ( + + +{storeCreditBonus}% bonus + + )} + + + Get credit for future purchases + +
+
+
+
+ ) +} diff --git a/storefront/src/modules/returns/components/ReturnProductItem.tsx b/storefront/src/modules/returns/components/ReturnProductItem.tsx new file mode 100644 index 00000000..8f0ff581 --- /dev/null +++ b/storefront/src/modules/returns/components/ReturnProductItem.tsx @@ -0,0 +1,176 @@ +"use client" + +import * as React from "react" +import Image from "next/image" +import { twMerge } from "tailwind-merge" +import { + UiCheckbox, + UiCheckboxBox, + UiCheckboxIcon, +} from "@/components/ui/Checkbox" +import { NumberField } from "@/components/NumberField" +import { + ReturnReasonSelect, + ReturnReasonId, +} from "@modules/returns/components/ReturnReasonSelect" +import { convertToLocale } from "@lib/util/money" + +export type ReturnProductItemData = { + id: string + title: string + productTitle: string + thumbnail: string | null + quantity: number + unitPrice: number + currencyCode: string + variant?: { + options?: Array<{ + id: string + option?: { title?: string } + value: string + }> + } +} + +type ReturnProductItemProps = { + item: ReturnProductItemData + isSelected: boolean + onSelectionChange: (selected: boolean) => void + returnQuantity: number + onQuantityChange: (quantity: number) => void + returnReason?: ReturnReasonId + onReasonChange: (reason: ReturnReasonId) => void + otherReasonText?: string + onOtherReasonTextChange?: (text: string) => void + className?: string +} + +export const ReturnProductItem: React.FC = ({ + item, + isSelected, + onSelectionChange, + returnQuantity, + onQuantityChange, + returnReason, + onReasonChange, + otherReasonText, + onOtherReasonTextChange, + className, +}) => { + return ( +
+
+ {/* Checkbox */} + + + + + + + {/* Product Image */} + {item.thumbnail && ( +
+ {item.title} +
+ )} + + {/* Product Info */} +
+
+

{item.productTitle}

+ {item.variant?.options?.map((option) => ( +

+ {option.option?.title}: + {option.value} +

+ ))} +
+ +
+

+ Original quantity: {item.quantity} +

+

+ {convertToLocale({ + currency_code: item.currencyCode, + amount: item.unitPrice, + })} +

+
+
+
+ + {/* Return Options - Only shown when selected */} + {isSelected && ( +
+ {/* Quantity Selector */} +
+ + +
+ + {/* Reason Selector */} +
+ + +
+ + {/* Other Reason Text */} + {returnReason === "other" && onOtherReasonTextChange && ( +
+ +