Skip to content

Commit

Permalink
feat: order transfers (#411)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
VariableVic and fPolic authored Nov 27, 2024
1 parent 94453a4 commit 414a2a2
Show file tree
Hide file tree
Showing 11 changed files with 614 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Metadata } from "next"
import OrderOverview from "@modules/account/components/order-overview"
import { notFound } from "next/navigation"
import { listOrders } from "@lib/data/orders"
import Divider from "@modules/common/components/divider"
import TransferRequestForm from "@modules/account/components/transfer-request-form"

export const metadata: Metadata = {
title: "Orders",
Expand All @@ -27,6 +29,8 @@ export default async function Orders() {
</div>
<div>
<OrderOverview orders={orders} />
<Divider className="my-16" />
<TransferRequestForm />
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { acceptTransferRequest } from "@lib/data/orders"
import { Heading, Text } from "@medusajs/ui"
import TransferImage from "@modules/order/components/transfer-image"

export default async function TransferPage({
params,
}: {
params: { id: string; token: string }
}) {
const { id, token } = params

const { success, error } = await acceptTransferRequest(id, token)

return (
<div className="flex flex-col gap-y-4 items-start w-2/5 mx-auto mt-10 mb-20">
<TransferImage />
<div className="flex flex-col gap-y-6">
{success && (
<>
<Heading level="h1" className="text-xl text-zinc-900">
Order transfered!
</Heading>
<Text className="text-zinc-600">
Order {id} has been successfully transfered to the new owner.
</Text>
</>
)}
{!success && (
<>
<Text className="text-zinc-600">
There was an error accepting the transfer. Please try again.
</Text>
{error && (
<Text className="text-red-500">Error message: {error}</Text>
)}
</>
)}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { declineTransferRequest } from "@lib/data/orders"
import { Heading, Text } from "@medusajs/ui"
import TransferImage from "@modules/order/components/transfer-image"

export default async function TransferPage({
params,
}: {
params: { id: string; token: string }
}) {
const { id, token } = params

const { success, error } = await declineTransferRequest(id, token)

return (
<div className="flex flex-col gap-y-4 items-start w-2/5 mx-auto mt-10 mb-20">
<TransferImage />
<div className="flex flex-col gap-y-6">
{success && (
<>
<Heading level="h1" className="text-xl text-zinc-900">
Order transfer declined!
</Heading>
<Text className="text-zinc-600">
Transfer of order {id} has been successfully declined.
</Text>
</>
)}
{!success && (
<>
<Text className="text-zinc-600">
There was an error declining the transfer. Please try again.
</Text>
{error && (
<Text className="text-red-500">Error message: {error}</Text>
)}
</>
)}
</div>
</div>
)
}
38 changes: 38 additions & 0 deletions src/app/[countryCode]/(main)/order/[id]/transfer/[token]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Heading, Text } from "@medusajs/ui"
import TransferActions from "@modules/order/components/transfer-actions"
import TransferImage from "@modules/order/components/transfer-image"

export default async function TransferPage({
params,
}: {
params: { id: string; token: string }
}) {
const { id, token } = params

return (
<div className="flex flex-col gap-y-4 items-start w-2/5 mx-auto mt-10 mb-20">
<TransferImage />
<div className="flex flex-col gap-y-6">
<Heading level="h1" className="text-xl text-zinc-900">
Transfer request for order {id}
</Heading>
<Text className="text-zinc-600">
You've received a request to transfer ownership of your order ({id}).
If you agree to this request, you can approve the transfer by clicking
the button below.
</Text>
<div className="w-full h-px bg-zinc-200" />
<Text className="text-zinc-600">
If you accept, the new owner will take over all responsibilities and
permissions associated with this order.
</Text>
<Text className="text-zinc-600">
If you do not recognize this request or wish to retain ownership, no
further action is required.
</Text>
<div className="w-full h-px bg-zinc-200" />
<TransferActions id={id} token={token} />
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion src/lib/data/cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ export async function placeOrder() {
const countryCode =
cartRes.order.shipping_address?.country_code?.toLowerCase()
removeCartId()
redirect(`/${countryCode}/order/confirmed/${cartRes?.order.id}`)
redirect(`/${countryCode}/order/${cartRes?.order.id}/confirmed`)
}

return cartRes.cart
Expand Down
52 changes: 52 additions & 0 deletions src/lib/data/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { sdk } from "@lib/config"
import medusaError from "@lib/util/medusa-error"
import { cache } from "react"
import { getAuthHeaders } from "./cookies"
import { HttpTypes } from "@medusajs/types"

export const retrieveOrder = cache(async function (id: string) {
return sdk.store.order
Expand All @@ -25,3 +26,54 @@ export const listOrders = cache(async function (
.then(({ orders }) => orders)
.catch((err) => medusaError(err))
})

export const createTransferRequest = async (
state: {
success: boolean
error: string | null
order: HttpTypes.StoreOrder | null
},
formData: FormData
): Promise<{
success: boolean
error: string | null
order: HttpTypes.StoreOrder | null
}> => {
const id = formData.get("order_id") as string

if (!id) {
return { success: false, error: "Order ID is required", order: null }
}

const headers = getAuthHeaders()

return await sdk.store.order
.requestTransfer(
id,
{},
{
fields: "id, email",
},
headers
)
.then(({ order }) => ({ success: true, error: null, order }))
.catch((err) => ({ success: false, error: err.message, order: null }))
}

export const acceptTransferRequest = async (id: string, token: string) => {
const headers = getAuthHeaders()

return await sdk.store.order
.acceptTransfer(id, { token }, {}, headers)
.then(({ order }) => ({ success: true, error: null, order }))
.catch((err) => ({ success: false, error: err.message, order: null }))
}

export const declineTransferRequest = async (id: string, token: string) => {
const headers = getAuthHeaders()

return await sdk.store.order
.declineTransfer(id, { token }, {}, headers)
.then(({ order }) => ({ success: true, error: null, order }))
.catch((err) => ({ success: false, error: err.message, order: null }))
}
81 changes: 81 additions & 0 deletions src/modules/account/components/transfer-request-form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client"

import { useFormState } from "react-dom"
import { createTransferRequest } from "@lib/data/orders"
import { Text, Heading, Input, Button, IconButton, Toaster } from "@medusajs/ui"
import { SubmitButton } from "@modules/checkout/components/submit-button"
import { CheckCircleMiniSolid, XCircleSolid } from "@medusajs/icons"
import { useEffect, useState } from "react"

export default function TransferRequestForm() {
const [showSuccess, setShowSuccess] = useState(false)

const [state, formAction] = useFormState(createTransferRequest, {
success: false,
error: null,
order: null,
})

useEffect(() => {
if (state.success && state.order) {
setShowSuccess(true)
}
}, [state.success, state.order])

return (
<div className="flex flex-col gap-y-4 w-full">
<div className="grid sm:grid-cols-2 items-center gap-x-8 gap-y-4 w-full">
<div className="flex flex-col gap-y-1">
<Heading level="h3" className="text-lg text-neutral-950">
Order transfers
</Heading>
<Text className="text-base-regular text-neutral-500">
Can&apos;t find the order you are looking for?
<br /> Connect an order to your account.
</Text>
</div>
<form
action={formAction}
className="flex flex-col gap-y-1 sm:items-end"
>
<div className="flex flex-col gap-y-2 w-full">
<Input className="w-full" name="order_id" placeholder="Order ID" />
<SubmitButton
variant="secondary"
className="w-fit whitespace-nowrap self-end"
>
Request transfer
</SubmitButton>
</div>
</form>
</div>
{!state.success && state.error && (
<Text className="text-base-regular text-rose-500 text-right">
{state.error}
</Text>
)}
{showSuccess && (
<div className="flex justify-between p-4 bg-neutral-50 shadow-borders-base w-full self-stretch items-center">
<div className="flex gap-x-2 items-center">
<CheckCircleMiniSolid className="w-4 h-4 text-emerald-500" />
<div className="flex flex-col gap-y-1">
<Text className="text-medim-pl text-neutral-950">
Transfer for order {state.order?.id} requested
</Text>
<Text className="text-base-regular text-neutral-600">
Transfer request email sent to {state.order?.email}
</Text>
</div>
</div>
<IconButton
variant="transparent"
className="h-fit"
onClick={() => setShowSuccess(false)}
>
<XCircleSolid className="w-4 h-4 text-neutral-500" />
</IconButton>
</div>
)}
</div>
)
}
81 changes: 81 additions & 0 deletions src/modules/order/components/transfer-actions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client"

import { acceptTransferRequest, declineTransferRequest } from "@lib/data/orders"
import { Button, Text } from "@medusajs/ui"
import { useState } from "react"

type TransferStatus = "pending" | "success" | "error"

const TransferActions = ({ id, token }: { id: string; token: string }) => {
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [status, setStatus] = useState<{
accept: TransferStatus | null
decline: TransferStatus | null
} | null>({
accept: null,
decline: null,
})

const acceptTransfer = async () => {
setStatus({ accept: "pending", decline: null })
setErrorMessage(null)

const { success, error } = await acceptTransferRequest(id, token)

if (error) setErrorMessage(error)
setStatus({ accept: success ? "success" : "error", decline: null })
}

const declineTransfer = async () => {
setStatus({ accept: null, decline: "pending" })
setErrorMessage(null)

const { success, error } = await declineTransferRequest(id, token)

if (error) setErrorMessage(error)
setStatus({ accept: null, decline: success ? "success" : "error" })
}

return (
<div className="flex flex-col gap-y-4">
{status?.accept === "success" && (
<Text className="text-emerald-500">
Order transferred successfully!
</Text>
)}
{status?.decline === "success" && (
<Text className="text-emerald-500">
Order transfer declined successfully!
</Text>
)}
{status?.accept !== "success" && status?.decline !== "success" && (
<div className="flex gap-x-4">
<Button
size="large"
onClick={acceptTransfer}
isLoading={status?.accept === "pending"}
disabled={
status?.accept === "pending" || status?.decline === "pending"
}
>
Accept transfer
</Button>
<Button
size="large"
variant="secondary"
onClick={declineTransfer}
isLoading={status?.decline === "pending"}
disabled={
status?.accept === "pending" || status?.decline === "pending"
}
>
Decline transfer
</Button>
</div>
)}
{errorMessage && <Text className="text-red-500">{errorMessage}</Text>}
</div>
)
}

export default TransferActions
Loading

0 comments on commit 414a2a2

Please sign in to comment.