diff --git a/app/components/OrderCard.tsx b/app/components/OrderCard.tsx index 8fc528ec25..18eb96d596 100644 --- a/app/components/OrderCard.tsx +++ b/app/components/OrderCard.tsx @@ -8,14 +8,14 @@ import { statusMessage } from "~/lib/utils"; export function OrderCard({ order }: { order: Order }) { if (!order?.id) return null; - const legacyOrderId = order!.id!.split("/").pop()!.split("?")[0]; + const [legacyOrderId, key] = order!.id!.split("/").pop()!.split("?"); const lineItems = flattenConnection(order?.lineItems); return (
  • {lineItems[0].variant?.image && (
    diff --git a/app/data/index.ts b/app/data/index.ts index 410115c8ef..f1c79bba81 100644 --- a/app/data/index.ts +++ b/app/data/index.ts @@ -13,6 +13,7 @@ import type { Blog, PageConnection, Shop, + Order, Localization, CustomerAccessTokenCreatePayload, Customer, @@ -1413,6 +1414,149 @@ const CUSTOMER_QUERY = `#graphql } `; +const CUSTOMER_ORDER_QUERY = `#graphql + fragment Money on MoneyV2 { + amount + currencyCode + } + fragment AddressFull on MailingAddress { + address1 + address2 + city + company + country + countryCodeV2 + firstName + formatted + id + lastName + name + phone + province + provinceCode + zip + } + fragment DiscountApplication on DiscountApplication { + value { + ... on MoneyV2 { + amount + currencyCode + } + ... on PricingPercentageValue { + percentage + } + } + } + fragment Image on Image { + altText + height + src: url(transform: {crop: CENTER, maxHeight: 96, maxWidth: 96, scale: 2}) + id + width + } + fragment ProductVariant on ProductVariant { + id + image { + ...Image + } + priceV2 { + ...Money + } + product { + handle + } + sku + title + } + fragment LineItemFull on OrderLineItem { + title + quantity + discountAllocations { + allocatedAmount { + ...Money + } + discountApplication { + ...DiscountApplication + } + } + originalTotalPrice { + ...Money + } + discountedTotalPrice { + ...Money + } + variant { + ...ProductVariant + } + } + + query CustomerOrder( + $country: CountryCode + $language: LanguageCode + $orderId: ID! + ) @inContext(country: $country, language: $language) { + node(id: $orderId) { + ... on Order { + id + name + orderNumber + processedAt + fulfillmentStatus + totalTaxV2 { + ...Money + } + totalPriceV2 { + ...Money + } + subtotalPriceV2 { + ...Money + } + shippingAddress { + ...AddressFull + } + discountApplications(first: 100) { + nodes { + ...DiscountApplication + } + } + lineItems(first: 100) { + nodes { + ...LineItemFull + } + } + } + } + } +`; + +export async function getCustomerOrder({ + orderId, + params, +}: { + orderId: string; + params: Params; +}) : Promise { + const { language, country } = getLocalizationFromLang(params.lang); + + const { data, errors } = await getStorefrontData<{ + node: Order; + }>({ + query: CUSTOMER_ORDER_QUERY, + variables: { + country, + language, + orderId + }, + }); + + if (errors) { + const errorMessages = errors.map(error => error.message).join('\n') + throw new Error(errorMessages) + } + + return data?.node; +} + export async function getCustomer({ request, context, @@ -1426,7 +1570,7 @@ export async function getCustomer({ }) { const { language, country } = getLocalizationFromLang(params.lang); - const { data } = await getStorefrontData<{ + const { data, errors } = await getStorefrontData<{ customer: Customer; }>({ query: CUSTOMER_QUERY, @@ -1437,11 +1581,16 @@ export async function getCustomer({ }, }); + if (errors) { + const errorMessages = errors.map(error => error.message).join('\n') + throw new Error(errorMessages) + } + /** * If the customer failed to load, we assume their access token is invalid. */ if (!data || !data.customer) { - throw logout(request, context); + throw await logout(request, context) } return data.customer; diff --git a/app/hooks/useCountries.tsx b/app/hooks/useCountries.tsx index 949fff9e49..1d08daf4ed 100644 --- a/app/hooks/useCountries.tsx +++ b/app/hooks/useCountries.tsx @@ -10,7 +10,7 @@ export function useCountries(): Array | undefined { if (rootData?.countries?._data) { return rootData?.countries?._data; } - // return rootData?.countries?._data + throw rootData?.countries } diff --git a/app/routes/account.orders.$id.tsx b/app/routes/account.orders.$id.tsx new file mode 100644 index 0000000000..13eb906194 --- /dev/null +++ b/app/routes/account.orders.$id.tsx @@ -0,0 +1,320 @@ +import invariant from "tiny-invariant"; +import clsx from "clsx"; +import { type LoaderArgs, type MetaFunction, redirect, json } from "@remix-run/cloudflare"; +import { Link, useLoaderData } from "@remix-run/react"; +import { Money, Image, flattenConnection } from "@shopify/hydrogen-ui-alpha"; +import {statusMessage} from '~/lib/utils'; +import type { + DiscountApplication, + DiscountApplicationConnection, + OrderLineItem, +} from '@shopify/hydrogen/storefront-api-types'; +import { + Heading, + PageHeader, + Text, +} from "~/components"; +import { getCustomerOrder } from "~/data"; +import { getSession } from "~/lib/session.server"; + +export const meta: MetaFunction = ({data}) => ({ + title: `Order ${data?.order?.name}`, +}); + +export async function loader({request, context, params}: LoaderArgs) { + if (!params.id) { + return redirect('/account') + } + + const queryParams = new URL(request.url).searchParams + const orderToken = queryParams.get('key') + + invariant(orderToken, "Order token is required") + + const session = await getSession(request, context); + const customerAccessToken = await session.get("customerAccessToken"); + + if (!customerAccessToken) { + return redirect("/account/login"); + } + + const orderId = `gid://shopify/Order/${params.id}?key=${orderToken}`; + + const order = await getCustomerOrder({ params, orderId }) + + if (!order) { + throw new Response('Order not found', { status: 404 }) + } + + const lineItems: Array = flattenConnection(order.lineItems!); + + const discountApplications = flattenConnection( + order.discountApplications as DiscountApplicationConnection, + ); + + const firstDiscount = discountApplications[0]?.value; + + const discountValue = + firstDiscount?.__typename === 'MoneyV2' && firstDiscount; + + const discountPercentage = + firstDiscount?.__typename === 'PricingPercentageValue' && + firstDiscount?.percentage; + + return json({ + order, + lineItems, + discountValue, + discountPercentage, + }) +} + +export default function OrderRoute() { + const {order, lineItems, discountValue, discountPercentage} = useLoaderData(); + return ( +
    + + + Return to Account Overview + + +
    +
    + + Order No. {order.name} + + + Placed on {new Date(order.processedAt!).toDateString()} + +
    + + + + + + + + + + + {lineItems.map((lineItem: OrderLineItem) => ( + + + + + + + ))} + + + {((discountValue && discountValue.amount) || + discountPercentage) && ( + + + + + + )} + + + + + + + + + + + + + + + + +
    + Product + + Price + + Quantity + + Total +
    +
    + + {lineItem?.variant?.image && ( +
    + {lineItem.variant.image.altText!} +
    + )} + +
    + {lineItem.title} + + {lineItem.variant!.title} + +
    +
    +
    Product
    +
    + + {lineItem.title} + + + {lineItem.variant!.title} + +
    +
    Price
    +
    + + + +
    +
    Quantity
    +
    + + Qty: {lineItem.quantity} + +
    +
    +
    +
    + + + {lineItem.quantity} + + + + +
    + Discounts + + Discounts + + {discountPercentage ? ( + + -{discountPercentage}% OFF + + ) : ( + discountValue && + )} +
    + Subtotal + + Subtotal + + +
    + Tax + + Tax + + +
    + Total + + Total + + +
    +
    + + Shipping Address + + {order?.shippingAddress ? ( +
      +
    • + + {order.shippingAddress.firstName && + order.shippingAddress.firstName + ' '} + {order.shippingAddress.lastName} + +
    • + {order?.shippingAddress?.formatted ? ( + order.shippingAddress.formatted.map((line) => ( +
    • + {line} +
    • + )) + ) : ( + <> + )} +
    + ) : ( +

    No shipping address defined

    + )} + + Status + +
    + + {statusMessage(order.fulfillmentStatus!)} + +
    +
    +
    +
    +
    +
    + ) +}