Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: order transfers #411

Merged
merged 10 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"dependencies": {
"@headlessui/react": "^1.6.1",
"@hookform/error-message": "^2.0.0",
"@medusajs/js-sdk": "2.0.0",
"@medusajs/js-sdk": "2.0.5-snapshot-20241124153140",
"@medusajs/ui": "2.0.0",
"@meilisearch/instant-meilisearch": "^0.7.1",
"@paypal/paypal-js": "^5.0.6",
Expand Down
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-3/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-3/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
fPolic marked this conversation as resolved.
Show resolved Hide resolved

return (
<div className="flex flex-col gap-y-4 items-start w-3/5 mx-auto mt-10 mb-20">
fPolic marked this conversation as resolved.
Show resolved Hide resolved
<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 gap-x-2">
<Input name="order_id" placeholder="Order ID" />
fPolic marked this conversation as resolved.
Show resolved Hide resolved
<SubmitButton
variant="secondary"
className="w-fit whitespace-nowrap"
>
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
Loading