diff --git a/examples/commerce-essentials/README.md b/examples/commerce-essentials/README.md index 242ce4cf..74fae9cf 100644 --- a/examples/commerce-essentials/README.md +++ b/examples/commerce-essentials/README.md @@ -1,7 +1,4 @@ # Commerce Essentials Elastic Path storefront starter - -This project was generated with [Composable CLI](https://www.npmjs.com/package/composable-cli). - This storefront accelerates the development of a direct-to-consumer ecommerce experience using Elastic Path's modular products. ## Tech Stack diff --git a/examples/commerce-essentials/package.json b/examples/commerce-essentials/package.json index 4714a238..5f32a971 100644 --- a/examples/commerce-essentials/package.json +++ b/examples/commerce-essentials/package.json @@ -58,6 +58,7 @@ "react-toastify": "^9.1.3", "schema-dts": "^1.1.2", "server-only": "^0.0.1", + "short-unique-id": "^5.2.0", "tailwind-clip-path": "^1.0.0", "tailwind-merge": "^2.0.0", "zod": "^3.22.4", @@ -96,4 +97,4 @@ "vite": "^4.2.1", "vitest": "^0.34.5" } -} \ No newline at end of file +} diff --git a/examples/commerce-essentials/src/app/(checkout)/checkout/OrderConfirmation.tsx b/examples/commerce-essentials/src/app/(checkout)/checkout/OrderConfirmation.tsx index 0391da30..1281c2ff 100644 --- a/examples/commerce-essentials/src/app/(checkout)/checkout/OrderConfirmation.tsx +++ b/examples/commerce-essentials/src/app/(checkout)/checkout/OrderConfirmation.tsx @@ -8,6 +8,8 @@ import * as React from "react"; import { Separator } from "../../../components/separator/Separator"; import { CheckoutFooter } from "./CheckoutFooter"; import { Button } from "../../../components/button/Button"; +import { Resource } from "@elasticpath/js-sdk"; +import { OrderWithShortNumber } from "../../../lib/types/order-with-short-number"; export function OrderConfirmation() { const { confirmationData } = useCheckout(); @@ -15,8 +17,7 @@ export function OrderConfirmation() { if (!confirmationData) { return null; } - - const { order } = confirmationData; + const order = confirmationData.order as Resource; const customerName = ( order.data.contact?.name ?? @@ -24,8 +25,9 @@ export function OrderConfirmation() { "" ).split(" ")[0]; - const { shipping_address, id: orderId } = order.data; - + const { shipping_address, id: orderId} = order.data; + const orderIdentifier = order.data.order_number ? order.data.order_number : orderId; + return (
@@ -51,7 +53,7 @@ export function OrderConfirmation() {
- Order #{orderId} is confirmed. + Order #{orderIdentifier} is confirmed. {/* Shipping */}
@@ -74,7 +76,7 @@ export function OrderConfirmation() {

Need to make changes?

- Email us or call. Remember to reference order #{orderId} + Email us or call. Remember to reference order #{orderIdentifier}

diff --git a/examples/commerce-essentials/src/app/(checkout)/checkout/actions.ts b/examples/commerce-essentials/src/app/(checkout)/checkout/actions.ts new file mode 100644 index 00000000..13d17e1b --- /dev/null +++ b/examples/commerce-essentials/src/app/(checkout)/checkout/actions.ts @@ -0,0 +1,39 @@ +"use server"; + +import ShortUniqueId from "short-unique-id"; +import { getServerSideCredentialsClient } from "../../../lib/epcc-server-side-credentials-client"; +import { Order, OrderFilter, OrderInclude, OrdersEndpoint, OrderSort, QueryableResource } from "@elasticpath/js-sdk"; + +interface OrderFilterWithShortOrder extends OrderFilter { + eq?: OrderFilter['eq'] & { + order_number: string; + }; +} + +interface OrdersEndpointWithShortOrder extends QueryableResource { + endpoint: 'orders' +} +const shortIdGen = new ShortUniqueId(); + +export async function getShortOrderNumber():Promise { + return generateShortOrder(); +} + +async function generateShortOrder(previousShortOrders: string[]=[]):Promise { + const shortId = shortIdGen.rnd(); + if (await found(shortId)){ + previousShortOrders.push(shortId); + return previousShortOrders.length<3?generateShortOrder(previousShortOrders):undefined; + } + return shortId; + } + + async function found(shortId: string) { + const client=getServerSideCredentialsClient(); + const orderEndpoint=client.Orders as OrdersEndpointWithShortOrder; + orderEndpoint.Filter({eq:{order_number: shortId}}); + const previousShortOrders=await orderEndpoint.All(); + if(previousShortOrders.data.length>0) + return true; + return false; + } \ No newline at end of file diff --git a/examples/commerce-essentials/src/app/(checkout)/checkout/usePaymentComplete.tsx b/examples/commerce-essentials/src/app/(checkout)/checkout/usePaymentComplete.tsx index 2c555d2e..7867010d 100644 --- a/examples/commerce-essentials/src/app/(checkout)/checkout/usePaymentComplete.tsx +++ b/examples/commerce-essentials/src/app/(checkout)/checkout/usePaymentComplete.tsx @@ -1,10 +1,11 @@ +import { getShortOrderNumber } from "./actions"; import { useAddCustomItemToCart, - useCheckout as useCheckoutCart, - useCheckoutWithAccount, useOrderConfirm, usePayments, } from "@elasticpath/react-shopper-hooks"; + +import { useCheckout as useCheckoutCart, useCheckoutWithAccount } from "../../../hooks/use-checkout"; import { useMutation, UseMutationOptions } from "@tanstack/react-query"; import { CheckoutForm } from "../../../components/checkout/form-schema/checkout-form-schema"; import { useShippingMethod } from "./useShippingMethod"; @@ -16,6 +17,7 @@ import { } from "@elasticpath/js-sdk"; import { useElements, useStripe } from "@stripe/react-stripe-js"; + export type UsePaymentCompleteProps = { cartId: string | undefined; accountToken?: string; @@ -25,6 +27,8 @@ export type UsePaymentCompleteReq = { data: CheckoutForm; }; + + export function usePaymentComplete( { cartId, accountToken }: UsePaymentCompleteProps, options?: UseMutationOptions< @@ -49,7 +53,8 @@ export function usePaymentComplete( const stripe = useStripe(); const elements = useElements(); - const staticDeliveryMethods=useShippingMethod(cartId)?.data; + const staticDeliveryMethods = useShippingMethod(cartId)?.data; + const paymentComplete = useMutation({ mutationFn: async ({ data }) => { const { @@ -65,13 +70,14 @@ export function usePaymentComplete( billingAddress: billingAddress && !sameAsShipping ? billingAddress : shippingAddress, shippingAddress: shippingAddress, + shortOrder: await getShortOrderNumber() }; /** * The handling of shipping options is not production ready. * You must implement your own based on your business needs. */ - const shippingAmount =staticDeliveryMethods?.find((method) => method.value === shippingMethod)?.amount ?? 0; + const shippingAmount = staticDeliveryMethods?.find((method) => method.value === shippingMethod)?.amount ?? 0; /** * Using a cart custom_item to represent shipping for demo purposes. @@ -94,20 +100,20 @@ export function usePaymentComplete( */ const createdOrder = await ("guest" in data ? mutateConvertToOrder({ - customer: { - email: data.guest.email, - name: customerName, - }, - ...checkoutProps, - }) + customer: { + email: data.guest.email, + name: customerName, + }, + ...checkoutProps, + }) : mutateConvertToOrderAsAccount({ - contact: { - name: data.account.name, - email: data.account.email, - }, - token: accountToken ?? "", - ...checkoutProps, - })); + contact: { + name: data.account.name, + email: data.account.email, + }, + token: accountToken ?? "", + ...checkoutProps, + })); /** * 2. Start payment against the order @@ -171,3 +177,5 @@ export function usePaymentComplete( ...paymentComplete, }; } + + diff --git a/examples/commerce-essentials/src/app/(store)/account/orders/OrderItem.tsx b/examples/commerce-essentials/src/app/(store)/account/orders/OrderItem.tsx index 52ff1f43..71f5d764 100644 --- a/examples/commerce-essentials/src/app/(store)/account/orders/OrderItem.tsx +++ b/examples/commerce-essentials/src/app/(store)/account/orders/OrderItem.tsx @@ -6,7 +6,7 @@ import { formatIsoDateString } from "../../../../lib/format-iso-date-string"; export type OrderItemProps = { children?: ReactNode; - order: Order; + order: Order & {order_number?:string}; orderItems: OrderItemType[]; imageUrl?: string; }; @@ -26,7 +26,7 @@ export function OrderItem({ children, order, orderItems }: OrderItemProps) {
- Order # {order.external_ref ?? order.id} + Order # {order.order_number?? order.id}

diff --git a/examples/commerce-essentials/src/app/(store)/account/orders/[orderId]/page.tsx b/examples/commerce-essentials/src/app/(store)/account/orders/[orderId]/page.tsx index 8b604f2a..bb424325 100644 --- a/examples/commerce-essentials/src/app/(store)/account/orders/[orderId]/page.tsx +++ b/examples/commerce-essentials/src/app/(store)/account/orders/[orderId]/page.tsx @@ -9,6 +9,7 @@ import { RelationshipToMany, ResourcePage, PcmProduct, + ResourceIncluded, } from "@elasticpath/js-sdk"; import { getSelectedAccount, @@ -43,9 +44,9 @@ export default async function Order({ const selectedAccount = getSelectedAccount(accountMemberCookie); const client = getServerSideImplicitClient(); + + let result: ResourceIncluded | undefined = undefined; - let result: Awaited> | undefined = - undefined; try { result = await client.request.send( `/orders/${params.orderId}?include=items`, @@ -100,7 +101,7 @@ export default async function Order({
-

Order # {shopperOrder.raw.id}

+

Order # {result?.data.order_number?? shopperOrder.raw.id}

diff --git a/examples/commerce-essentials/src/hooks/use-checkout.ts b/examples/commerce-essentials/src/hooks/use-checkout.ts new file mode 100644 index 00000000..7a650c01 --- /dev/null +++ b/examples/commerce-essentials/src/hooks/use-checkout.ts @@ -0,0 +1,127 @@ +"use client" + +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "@elasticpath/react-shopper-hooks" +import { + Address, + CartAdditionalHeaders, + CheckoutCustomer, + CheckoutCustomerObject, + ElasticPath, + Order, + Resource, +} from "@elasticpath/js-sdk" + +export type UseCheckoutReq = { + customer: string | CheckoutCustomer | CheckoutCustomerObject + billingAddress: Partial
+ shippingAddress?: Partial
+ additionalHeaders?: CartAdditionalHeaders, + shortOrder?: string +} + +export const useCheckout = ( + cartId: string, + options?: UseMutationOptions, Error, UseCheckoutReq>, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ + customer, + billingAddress, + shippingAddress, + additionalHeaders, + shortOrder + }: UseCheckoutReq) => { + return shortOrder ? checkoutWithShortOrder(client, cartId, shortOrder, billingAddress, shippingAddress, customer, undefined, additionalHeaders as { [key: string]: string }) : + checkoutAsGuest(client, cartId, customer, billingAddress, shippingAddress, additionalHeaders) + }, + ...options, + }) +} + +export type UseCheckoutWithAccountReq = { + contact: string | CheckoutCustomer | CheckoutCustomerObject + billingAddress: Partial
+ token: string + shippingAddress?: Partial
+ additionalHeaders?: CartAdditionalHeaders + shortOrder?: string +} + +export const useCheckoutWithAccount = ( + cartId: string, + options?: UseMutationOptions< + Resource, + Error, + UseCheckoutWithAccountReq + >, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ + contact, + billingAddress, + shippingAddress, + token, + additionalHeaders, + shortOrder + }: UseCheckoutWithAccountReq) => { + if (token && additionalHeaders) { + additionalHeaders["EP-Account-Management-Authentication-Token"] = token; + } else if (token) { + additionalHeaders = { "EP-Account-Management-Authentication-Token": token }; + } + return shortOrder ? checkoutWithShortOrder(client, cartId, shortOrder, billingAddress, shippingAddress, undefined, contact, additionalHeaders as { [key: string]: string }) : + checkoutWithContact(client, cartId, contact, billingAddress, shippingAddress, token, additionalHeaders) + }, + ...options, + }) +} +function checkoutAsGuest(client: ElasticPath, cartId: string, customer: string | CheckoutCustomer | CheckoutCustomerObject, billingAddress: Partial
, shippingAddress: Partial
| undefined, additionalHeaders: CartAdditionalHeaders | undefined): Resource | PromiseLike> { + return client + .Cart(cartId) + .Checkout(customer, billingAddress, shippingAddress, additionalHeaders) +} + +function checkoutWithContact(client: ElasticPath, cartId: string, contact: string | CheckoutCustomer | CheckoutCustomerObject, billingAddress: Partial
, shippingAddress: Partial
| undefined, token: string, additionalHeaders: CartAdditionalHeaders | undefined): Resource | PromiseLike> { + return client.Cart(cartId).CheckoutWithAccountManagementToken( + contact, + billingAddress, + shippingAddress, + token, + // @ts-ignore TODO: Fix type definition in js-sdk + additionalHeaders + ) +} + +function checkoutWithShortOrder(client: ElasticPath, + cartId: string, + shortOrderId: string, + billingAddress: Partial
, + shippingAddress: Partial
| undefined, + customer?: string | CheckoutCustomer | CheckoutCustomerObject, + contact?: string | CheckoutCustomer | CheckoutCustomerObject, + additionalHeaders?: { [key: string]: string }) { + + const body: { + customer?: string | CheckoutCustomer | CheckoutCustomerObject, + contact?:string | CheckoutCustomer | CheckoutCustomerObject, + order_number: string, + billing_address: Partial
, + shipping_address?: Partial
+ + } = { + order_number: shortOrderId, + billing_address: billingAddress, + shipping_address: shippingAddress + } + body.customer = customer; + body.contact = contact; + + + return client.request.send(`/carts/${cartId}/checkout`, "POST", body, undefined, client, undefined, "v2", + additionalHeaders, + ); +} + diff --git a/examples/commerce-essentials/src/lib/types/order-with-short-number.ts b/examples/commerce-essentials/src/lib/types/order-with-short-number.ts new file mode 100644 index 00000000..0dfffd85 --- /dev/null +++ b/examples/commerce-essentials/src/lib/types/order-with-short-number.ts @@ -0,0 +1,4 @@ +import { Order } from "@elasticpath/js-sdk"; +export interface OrderWithShortNumber extends Order { + order_number?: string; +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 125a6e84..2529113e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -457,6 +457,9 @@ importers: server-only: specifier: ^0.0.1 version: 0.0.1 + short-unique-id: + specifier: ^5.2.0 + version: 5.2.0 tailwind-clip-path: specifier: ^1.0.0 version: 1.0.0 @@ -27725,6 +27728,11 @@ packages: interpret: 1.4.0 rechoir: 0.6.2 + /short-unique-id@5.2.0: + resolution: {integrity: sha512-cMGfwNyfDZ/nzJ2k2M+ClthBIh//GlZl1JEf47Uoa9XR11bz8Pa2T2wQO4bVrRdH48LrIDWJahQziKo3MjhsWg==} + hasBin: true + dev: false + /should-equal@2.0.0: resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==} dependencies: