Skip to content

Commit 414a2a2

Browse files
VariableVicfPolic
andauthored
feat: order transfers (#411)
* feat: transfer form ui * feat: confirmation * feat: transfer pages * fix: made transfer page standalone * chore: revert lock changes * fix: NL * fix: lock from main * fix: spacing * feat: wider order input --------- Co-authored-by: fPolic <[email protected]>
1 parent 94453a4 commit 414a2a2

File tree

11 files changed

+614
-1
lines changed

11 files changed

+614
-1
lines changed

src/app/[countryCode]/(main)/account/@dashboard/orders/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Metadata } from "next"
33
import OrderOverview from "@modules/account/components/order-overview"
44
import { notFound } from "next/navigation"
55
import { listOrders } from "@lib/data/orders"
6+
import Divider from "@modules/common/components/divider"
7+
import TransferRequestForm from "@modules/account/components/transfer-request-form"
68

79
export const metadata: Metadata = {
810
title: "Orders",
@@ -27,6 +29,8 @@ export default async function Orders() {
2729
</div>
2830
<div>
2931
<OrderOverview orders={orders} />
32+
<Divider className="my-16" />
33+
<TransferRequestForm />
3034
</div>
3135
</div>
3236
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { acceptTransferRequest } from "@lib/data/orders"
2+
import { Heading, Text } from "@medusajs/ui"
3+
import TransferImage from "@modules/order/components/transfer-image"
4+
5+
export default async function TransferPage({
6+
params,
7+
}: {
8+
params: { id: string; token: string }
9+
}) {
10+
const { id, token } = params
11+
12+
const { success, error } = await acceptTransferRequest(id, token)
13+
14+
return (
15+
<div className="flex flex-col gap-y-4 items-start w-2/5 mx-auto mt-10 mb-20">
16+
<TransferImage />
17+
<div className="flex flex-col gap-y-6">
18+
{success && (
19+
<>
20+
<Heading level="h1" className="text-xl text-zinc-900">
21+
Order transfered!
22+
</Heading>
23+
<Text className="text-zinc-600">
24+
Order {id} has been successfully transfered to the new owner.
25+
</Text>
26+
</>
27+
)}
28+
{!success && (
29+
<>
30+
<Text className="text-zinc-600">
31+
There was an error accepting the transfer. Please try again.
32+
</Text>
33+
{error && (
34+
<Text className="text-red-500">Error message: {error}</Text>
35+
)}
36+
</>
37+
)}
38+
</div>
39+
</div>
40+
)
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { declineTransferRequest } from "@lib/data/orders"
2+
import { Heading, Text } from "@medusajs/ui"
3+
import TransferImage from "@modules/order/components/transfer-image"
4+
5+
export default async function TransferPage({
6+
params,
7+
}: {
8+
params: { id: string; token: string }
9+
}) {
10+
const { id, token } = params
11+
12+
const { success, error } = await declineTransferRequest(id, token)
13+
14+
return (
15+
<div className="flex flex-col gap-y-4 items-start w-2/5 mx-auto mt-10 mb-20">
16+
<TransferImage />
17+
<div className="flex flex-col gap-y-6">
18+
{success && (
19+
<>
20+
<Heading level="h1" className="text-xl text-zinc-900">
21+
Order transfer declined!
22+
</Heading>
23+
<Text className="text-zinc-600">
24+
Transfer of order {id} has been successfully declined.
25+
</Text>
26+
</>
27+
)}
28+
{!success && (
29+
<>
30+
<Text className="text-zinc-600">
31+
There was an error declining the transfer. Please try again.
32+
</Text>
33+
{error && (
34+
<Text className="text-red-500">Error message: {error}</Text>
35+
)}
36+
</>
37+
)}
38+
</div>
39+
</div>
40+
)
41+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Heading, Text } from "@medusajs/ui"
2+
import TransferActions from "@modules/order/components/transfer-actions"
3+
import TransferImage from "@modules/order/components/transfer-image"
4+
5+
export default async function TransferPage({
6+
params,
7+
}: {
8+
params: { id: string; token: string }
9+
}) {
10+
const { id, token } = params
11+
12+
return (
13+
<div className="flex flex-col gap-y-4 items-start w-2/5 mx-auto mt-10 mb-20">
14+
<TransferImage />
15+
<div className="flex flex-col gap-y-6">
16+
<Heading level="h1" className="text-xl text-zinc-900">
17+
Transfer request for order {id}
18+
</Heading>
19+
<Text className="text-zinc-600">
20+
You've received a request to transfer ownership of your order ({id}).
21+
If you agree to this request, you can approve the transfer by clicking
22+
the button below.
23+
</Text>
24+
<div className="w-full h-px bg-zinc-200" />
25+
<Text className="text-zinc-600">
26+
If you accept, the new owner will take over all responsibilities and
27+
permissions associated with this order.
28+
</Text>
29+
<Text className="text-zinc-600">
30+
If you do not recognize this request or wish to retain ownership, no
31+
further action is required.
32+
</Text>
33+
<div className="w-full h-px bg-zinc-200" />
34+
<TransferActions id={id} token={token} />
35+
</div>
36+
</div>
37+
)
38+
}

src/lib/data/cart.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ export async function placeOrder() {
371371
const countryCode =
372372
cartRes.order.shipping_address?.country_code?.toLowerCase()
373373
removeCartId()
374-
redirect(`/${countryCode}/order/confirmed/${cartRes?.order.id}`)
374+
redirect(`/${countryCode}/order/${cartRes?.order.id}/confirmed`)
375375
}
376376

377377
return cartRes.cart

src/lib/data/orders.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { sdk } from "@lib/config"
44
import medusaError from "@lib/util/medusa-error"
55
import { cache } from "react"
66
import { getAuthHeaders } from "./cookies"
7+
import { HttpTypes } from "@medusajs/types"
78

89
export const retrieveOrder = cache(async function (id: string) {
910
return sdk.store.order
@@ -25,3 +26,54 @@ export const listOrders = cache(async function (
2526
.then(({ orders }) => orders)
2627
.catch((err) => medusaError(err))
2728
})
29+
30+
export const createTransferRequest = async (
31+
state: {
32+
success: boolean
33+
error: string | null
34+
order: HttpTypes.StoreOrder | null
35+
},
36+
formData: FormData
37+
): Promise<{
38+
success: boolean
39+
error: string | null
40+
order: HttpTypes.StoreOrder | null
41+
}> => {
42+
const id = formData.get("order_id") as string
43+
44+
if (!id) {
45+
return { success: false, error: "Order ID is required", order: null }
46+
}
47+
48+
const headers = getAuthHeaders()
49+
50+
return await sdk.store.order
51+
.requestTransfer(
52+
id,
53+
{},
54+
{
55+
fields: "id, email",
56+
},
57+
headers
58+
)
59+
.then(({ order }) => ({ success: true, error: null, order }))
60+
.catch((err) => ({ success: false, error: err.message, order: null }))
61+
}
62+
63+
export const acceptTransferRequest = async (id: string, token: string) => {
64+
const headers = getAuthHeaders()
65+
66+
return await sdk.store.order
67+
.acceptTransfer(id, { token }, {}, headers)
68+
.then(({ order }) => ({ success: true, error: null, order }))
69+
.catch((err) => ({ success: false, error: err.message, order: null }))
70+
}
71+
72+
export const declineTransferRequest = async (id: string, token: string) => {
73+
const headers = getAuthHeaders()
74+
75+
return await sdk.store.order
76+
.declineTransfer(id, { token }, {}, headers)
77+
.then(({ order }) => ({ success: true, error: null, order }))
78+
.catch((err) => ({ success: false, error: err.message, order: null }))
79+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"use client"
2+
3+
import { useFormState } from "react-dom"
4+
import { createTransferRequest } from "@lib/data/orders"
5+
import { Text, Heading, Input, Button, IconButton, Toaster } from "@medusajs/ui"
6+
import { SubmitButton } from "@modules/checkout/components/submit-button"
7+
import { CheckCircleMiniSolid, XCircleSolid } from "@medusajs/icons"
8+
import { useEffect, useState } from "react"
9+
10+
export default function TransferRequestForm() {
11+
const [showSuccess, setShowSuccess] = useState(false)
12+
13+
const [state, formAction] = useFormState(createTransferRequest, {
14+
success: false,
15+
error: null,
16+
order: null,
17+
})
18+
19+
useEffect(() => {
20+
if (state.success && state.order) {
21+
setShowSuccess(true)
22+
}
23+
}, [state.success, state.order])
24+
25+
return (
26+
<div className="flex flex-col gap-y-4 w-full">
27+
<div className="grid sm:grid-cols-2 items-center gap-x-8 gap-y-4 w-full">
28+
<div className="flex flex-col gap-y-1">
29+
<Heading level="h3" className="text-lg text-neutral-950">
30+
Order transfers
31+
</Heading>
32+
<Text className="text-base-regular text-neutral-500">
33+
Can&apos;t find the order you are looking for?
34+
<br /> Connect an order to your account.
35+
</Text>
36+
</div>
37+
<form
38+
action={formAction}
39+
className="flex flex-col gap-y-1 sm:items-end"
40+
>
41+
<div className="flex flex-col gap-y-2 w-full">
42+
<Input className="w-full" name="order_id" placeholder="Order ID" />
43+
<SubmitButton
44+
variant="secondary"
45+
className="w-fit whitespace-nowrap self-end"
46+
>
47+
Request transfer
48+
</SubmitButton>
49+
</div>
50+
</form>
51+
</div>
52+
{!state.success && state.error && (
53+
<Text className="text-base-regular text-rose-500 text-right">
54+
{state.error}
55+
</Text>
56+
)}
57+
{showSuccess && (
58+
<div className="flex justify-between p-4 bg-neutral-50 shadow-borders-base w-full self-stretch items-center">
59+
<div className="flex gap-x-2 items-center">
60+
<CheckCircleMiniSolid className="w-4 h-4 text-emerald-500" />
61+
<div className="flex flex-col gap-y-1">
62+
<Text className="text-medim-pl text-neutral-950">
63+
Transfer for order {state.order?.id} requested
64+
</Text>
65+
<Text className="text-base-regular text-neutral-600">
66+
Transfer request email sent to {state.order?.email}
67+
</Text>
68+
</div>
69+
</div>
70+
<IconButton
71+
variant="transparent"
72+
className="h-fit"
73+
onClick={() => setShowSuccess(false)}
74+
>
75+
<XCircleSolid className="w-4 h-4 text-neutral-500" />
76+
</IconButton>
77+
</div>
78+
)}
79+
</div>
80+
)
81+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"use client"
2+
3+
import { acceptTransferRequest, declineTransferRequest } from "@lib/data/orders"
4+
import { Button, Text } from "@medusajs/ui"
5+
import { useState } from "react"
6+
7+
type TransferStatus = "pending" | "success" | "error"
8+
9+
const TransferActions = ({ id, token }: { id: string; token: string }) => {
10+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
11+
const [status, setStatus] = useState<{
12+
accept: TransferStatus | null
13+
decline: TransferStatus | null
14+
} | null>({
15+
accept: null,
16+
decline: null,
17+
})
18+
19+
const acceptTransfer = async () => {
20+
setStatus({ accept: "pending", decline: null })
21+
setErrorMessage(null)
22+
23+
const { success, error } = await acceptTransferRequest(id, token)
24+
25+
if (error) setErrorMessage(error)
26+
setStatus({ accept: success ? "success" : "error", decline: null })
27+
}
28+
29+
const declineTransfer = async () => {
30+
setStatus({ accept: null, decline: "pending" })
31+
setErrorMessage(null)
32+
33+
const { success, error } = await declineTransferRequest(id, token)
34+
35+
if (error) setErrorMessage(error)
36+
setStatus({ accept: null, decline: success ? "success" : "error" })
37+
}
38+
39+
return (
40+
<div className="flex flex-col gap-y-4">
41+
{status?.accept === "success" && (
42+
<Text className="text-emerald-500">
43+
Order transferred successfully!
44+
</Text>
45+
)}
46+
{status?.decline === "success" && (
47+
<Text className="text-emerald-500">
48+
Order transfer declined successfully!
49+
</Text>
50+
)}
51+
{status?.accept !== "success" && status?.decline !== "success" && (
52+
<div className="flex gap-x-4">
53+
<Button
54+
size="large"
55+
onClick={acceptTransfer}
56+
isLoading={status?.accept === "pending"}
57+
disabled={
58+
status?.accept === "pending" || status?.decline === "pending"
59+
}
60+
>
61+
Accept transfer
62+
</Button>
63+
<Button
64+
size="large"
65+
variant="secondary"
66+
onClick={declineTransfer}
67+
isLoading={status?.decline === "pending"}
68+
disabled={
69+
status?.accept === "pending" || status?.decline === "pending"
70+
}
71+
>
72+
Decline transfer
73+
</Button>
74+
</div>
75+
)}
76+
{errorMessage && <Text className="text-red-500">{errorMessage}</Text>}
77+
</div>
78+
)
79+
}
80+
81+
export default TransferActions

0 commit comments

Comments
 (0)