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(dashboard,core-flows,js-sdk,types,link-modules,payment): ability to copy payment link #8630

Merged
merged 9 commits into from
Aug 20, 2024
13 changes: 13 additions & 0 deletions integration-tests/http/__tests__/claims/claims.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,19 @@ medusaIntegrationTestRunner({
message:
"Active payment collections were found. Complete existing ones or delete them before proceeding.",
})

const deleted = (
await api.delete(
`/admin/payment-collections/${paymentCollection.id}`,
adminHeaders
)
).data

expect(deleted).toEqual({
id: expect.any(String),
object: "payment-collection",
deleted: true,
})
})
})

Expand Down
1 change: 1 addition & 0 deletions packages/admin-next/dashboard/src/hooks/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./inventory"
export * from "./invites"
export * from "./notification"
export * from "./orders"
export * from "./payment-collections"
export * from "./payments"
export * from "./price-lists"
export * from "./product-types"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import { useMutation, UseMutationOptions } from "@tanstack/react-query"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { ordersQueryKeys } from "./orders"

const PAYMENT_COLLECTION_QUERY_KEY = "payment-collection" as const
export const paymentCollectionQueryKeys = queryKeysFactory(
PAYMENT_COLLECTION_QUERY_KEY
)

export const useCreatePaymentCollection = (
options?: UseMutationOptions<
HttpTypes.AdminPaymentCollectionResponse,
Error,
HttpTypes.AdminCreatePaymentCollection
>
) => {
return useMutation({
mutationFn: (payload) => sdk.admin.paymentCollection.create(payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.details(),
})

queryClient.invalidateQueries({
queryKey: ordersQueryKeys.lists(),
})

options?.onSuccess?.(data, variables, context)
},
...options,
})
}

export const useDeletePaymentCollection = (
options?: Omit<
UseMutationOptions<
HttpTypes.AdminDeletePaymentCollectionResponse,
FetchError,
string
>,
"mutationFn"
>
) => {
return useMutation({
mutationFn: (id: string) => sdk.admin.paymentCollection.delete(id),
onSuccess: async (data, variables, context) => {
await queryClient.invalidateQueries({
queryKey: ordersQueryKeys.all,
})

await queryClient.invalidateQueries({
queryKey: paymentCollectionQueryKeys.all,
})

options?.onSuccess?.(data, variables, context)
},
...options,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { sdk } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"

const REFUND_REASON_QUERY_KEY = "refund-reason" as const
export const paymentQueryKeys = queryKeysFactory(REFUND_REASON_QUERY_KEY)
export const refundReasonQueryKeys = queryKeysFactory(REFUND_REASON_QUERY_KEY)

export const useRefundReasons = (
query?: HttpTypes.RefundReasonFilters,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,9 @@
"capturePaymentSuccess": "Payment of {{amount}} successfully captured",
"createRefund": "Create Refund",
"refundPaymentSuccess": "Refund of amount {{amount}} successful",
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}"
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}",
"refundAmount": "Refund {{ amount }}",
"paymentLink": "Copy payment link for {{ amount }}"
},

"edits": {
Expand Down
9 changes: 9 additions & 0 deletions packages/admin-next/dashboard/src/lib/payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ export const getTotalCaptured = (
(paymentCollection.refunded_amount as number))
return acc
}, 0)

export const getTotalPending = (paymentCollections: AdminPaymentCollection[]) =>
paymentCollections.reduce((acc, paymentCollection) => {
acc +=
(paymentCollection.amount as number) -
(paymentCollection.captured_amount as number)

return acc
}, 0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { CheckCircleSolid, SquareTwoStack } from "@medusajs/icons"
import { AdminOrder } from "@medusajs/types"
import { Button, toast, Tooltip } from "@medusajs/ui"
import copy from "copy-to-clipboard"
import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import {
useCreatePaymentCollection,
useDeletePaymentCollection,
} from "../../../../../hooks/api"
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"

export const MEDUSA_BACKEND_URL = __STOREFRONT_URL__ ?? "http://localhost:8000"

type CopyPaymentLinkProps = {
order: AdminOrder
}

/**
* This component is based on the `button` element and supports all of its props
*/
const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
({ order }: CopyPaymentLinkProps, ref) => {
const [isCreating, setIsCreating] = useState(false)
const [url, setUrl] = useState("")
const [done, setDone] = useState(false)
const [open, setOpen] = useState(false)
const [text, setText] = useState("CopyPaymentLink")
const { t } = useTranslation()
const { mutateAsync: createPaymentCollection } =
useCreatePaymentCollection()

const { mutateAsync: deletePaymentCollection } =
useDeletePaymentCollection()

const copyToClipboard = async (
e:
| React.MouseEvent<HTMLElement, MouseEvent>
| React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
e.stopPropagation()

if (!url?.length) {
const activePaymentCollection = order.payment_collections.find(
(pc) =>
pc.status === "not_paid" &&
pc.amount === order.summary?.pending_difference
)

if (!activePaymentCollection) {
setIsCreating(true)

const paymentCollectionsToDelete = order.payment_collections.filter(
(pc) => pc.status === "not_paid"
)

const promises = paymentCollectionsToDelete.map((paymentCollection) =>
deletePaymentCollection(paymentCollection.id)
)

await Promise.all(promises)

await createPaymentCollection(
{ order_id: order.id },
{
onSuccess: (data) => {
setUrl(
`${MEDUSA_BACKEND_URL}/payment-collection/${data.payment_collection.id}`
)
},
onError: (err) => {
toast.error(err.message)
},
onSettled: () => setIsCreating(false),
}
)
} else {
setUrl(
`${MEDUSA_BACKEND_URL}/payment-collection/${activePaymentCollection.id}`
)
}
}

setDone(true)
copy(url)

setTimeout(() => {
setDone(false)
}, 2000)
}

React.useEffect(() => {
if (done) {
setText("Copied")
return
}

setTimeout(() => {
setText("CopyPaymentLink")
}, 500)
}, [done])

return (
<Tooltip content={text} open={done || open} onOpenChange={setOpen}>
<Button
ref={ref}
variant="secondary"
size="small"
aria-label="CopyPaymentLink code snippet"
onClick={copyToClipboard}
isLoading={isCreating}
>
{done ? (
<CheckCircleSolid className="inline" />
) : (
<SquareTwoStack className="inline" />
)}
{t("orders.payment.paymentLink", {
amount: getStylizedAmount(
order?.summary?.pending_difference,
order?.currency_code
),
})}
</Button>
</Tooltip>
)
}
)
CopyPaymentLink.displayName = "CopyPaymentLink"

export { CopyPaymentLink }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./copy-payment-link"
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
getStylizedAmount,
} from "../../../../../lib/money-amount-helpers"
import { getOrderPaymentStatus } from "../../../../../lib/order-helpers"
import { getTotalCaptured } from "../../../../../lib/payment"
import { getTotalCaptured, getTotalPending } from "../../../../../lib/payment"

type OrderPaymentSectionProps = {
order: HttpTypes.AdminOrder
Expand Down Expand Up @@ -321,15 +321,34 @@ const Total = ({
currencyCode: string
}) => {
const { t } = useTranslation()
const totalPending = getTotalPending(paymentCollections)

return (
<div className="flex items-center justify-between px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("orders.payment.totalPaidByCustomer")}
</Text>
<Text size="small" weight="plus" leading="compact">
{getStylizedAmount(getTotalCaptured(paymentCollections), currencyCode)}
</Text>
<div>
<div className="flex items-center justify-between px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("orders.payment.totalPaidByCustomer")}
</Text>

<Text size="small" weight="plus" leading="compact">
{getStylizedAmount(
getTotalCaptured(paymentCollections),
currencyCode
)}
</Text>
</div>

{totalPending > 0 && (
<div className="flex items-center justify-between px-6 py-4">
<Text size="small" weight="plus" leading="compact">
Total pending
</Text>

<Text size="small" weight="plus" leading="compact">
{getStylizedAmount(totalPending, currencyCode)}
</Text>
</div>
)}
</div>
)
}
Loading
Loading