diff --git a/app/account/page.tsx b/app/account/page.tsx index 1ec80cc1d..fe2482c40 100644 --- a/app/account/page.tsx +++ b/app/account/page.tsx @@ -61,7 +61,7 @@ export default async function AccountPage() { View Order {order.normalizedId} - + diff --git a/components/form/file-input/actions.ts b/components/form/file-input/actions.ts index cf71d22ec..55581992d 100644 --- a/components/form/file-input/actions.ts +++ b/components/form/file-input/actions.ts @@ -1,9 +1,28 @@ 'use server'; import { createFile, stageUploadFile, uploadFile } from 'lib/shopify'; -import { UploadInput } from 'lib/shopify/types'; +import { StagedUploadsCreatePayload, UploadInput } from 'lib/shopify/types'; -export const createStagedUploadFiles = async (params: UploadInput) => { +const prepareFilePayload = ({ + stagedFileUpload, + file +}: { + stagedFileUpload: StagedUploadsCreatePayload; + file: File; +}) => { + const formData = new FormData(); + + const url = stagedFileUpload.url; + + stagedFileUpload.parameters.forEach(({ name, value }) => { + formData.append(name, value); + }); + + formData.append('file', file); + return { url, formData }; +}; + +const createStagedUploadFiles = async (params: UploadInput) => { try { const stagedTargets = await stageUploadFile(params); if (!stagedTargets || stageUploadFile.length === 0) return null; @@ -14,25 +33,54 @@ export const createStagedUploadFiles = async (params: UploadInput) => { } }; -export const onUploadFile = async ({ +const onUploadFile = async ({ url, formData, fileName, - resourceUrl + resourceUrl, + contentType = 'FILE' }: { url: string; formData: FormData; fileName: string; resourceUrl: string; + contentType?: 'FILE' | 'IMAGE'; }) => { try { await uploadFile({ url, formData }); - await createFile({ + return await createFile({ alt: fileName, - contentType: 'FILE', + contentType, originalSource: resourceUrl }); } catch (error) { console.log(error); } }; + +export const handleUploadFile = async ({ file }: { file: File }) => { + if (!file) return; + try { + const stagedTarget = await createStagedUploadFiles({ + filename: file.name, + fileSize: String(file.size), + httpMethod: 'POST', + resource: 'FILE', + mimeType: file.type + }); + + if (stagedTarget) { + const data = prepareFilePayload({ file, stagedFileUpload: stagedTarget }); + + const result = await onUploadFile({ + ...data, + fileName: file.name, + resourceUrl: stagedTarget.resourceUrl + }); + + return result?.[0]?.id; + } + } catch (error) { + console.log(error); + } +}; diff --git a/components/form/file-input/utils.ts b/components/form/file-input/utils.ts deleted file mode 100644 index f359ce458..000000000 --- a/components/form/file-input/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { StagedUploadsCreatePayload } from 'lib/shopify/types'; -import { createStagedUploadFiles, onUploadFile } from './actions'; - -export const prepareFilePayload = ({ - stagedFileUpload, - file -}: { - stagedFileUpload: StagedUploadsCreatePayload; - file: File; -}) => { - const formData = new FormData(); - - const url = stagedFileUpload.url; - - stagedFileUpload.parameters.forEach(({ name, value }) => { - formData.append(name, value); - }); - - formData.append('file', file); - return { url, formData }; -}; - -export const handleUploadFile = async ({ file }: { file: File }) => { - if (!file) return; - const stagedTarget = await createStagedUploadFiles({ - filename: file.name, - fileSize: String(file.size), - httpMethod: 'POST', - resource: 'FILE', - mimeType: file.type - }); - - if (stagedTarget) { - const data = prepareFilePayload({ file, stagedFileUpload: stagedTarget }); - - await onUploadFile({ - ...data, - fileName: file.name, - resourceUrl: stagedTarget.resourceUrl - }); - } -}; diff --git a/components/orders/actions.ts b/components/orders/actions.ts new file mode 100644 index 000000000..3b392d1a0 --- /dev/null +++ b/components/orders/actions.ts @@ -0,0 +1,44 @@ +'use server'; + +import { handleUploadFile } from 'components/form/file-input/actions'; +import { updateOrderMetafields } from 'lib/shopify'; +import { revalidatePath } from 'next/cache'; + +export const activateWarranty = async (orderId: string, formData: FormData) => { + let odometerFileId = null; + let installationFileId = null; + const odometerFile = formData.get('warranty_activation_odometer'); + const installationFile = formData.get('warranty_activation_installation'); + if (odometerFile) { + odometerFileId = await handleUploadFile({ file: odometerFile as File }); + } + + if (installationFile) { + installationFileId = await handleUploadFile({ file: installationFile as File }); + } + + const rawFormData = [ + { key: 'warranty_activation_odometer', value: odometerFileId, type: 'file_reference' }, + { key: 'warranty_activation_installation', value: installationFileId, type: 'file_reference' }, + { + key: 'warranty_activation_mileage', + value: formData.get('warranty_activation_mileage') as string | null, + type: 'number_integer' + }, + { + key: 'warranty_activation_vin', + value: formData.get('warranty_activation_vin') as string | null, + type: 'single_line_text_field' + } + ]; + + try { + await updateOrderMetafields({ + orderId, + metafields: rawFormData + }); + revalidatePath('/account'); + } catch (error) { + console.log(error); + } +}; diff --git a/components/orders/activate-warranty-modal.tsx b/components/orders/activate-warranty-modal.tsx index 9ac74abfc..ab7356d19 100644 --- a/components/orders/activate-warranty-modal.tsx +++ b/components/orders/activate-warranty-modal.tsx @@ -1,15 +1,36 @@ 'use client'; -import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'; +import { Button, Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'; +import clsx from 'clsx'; import FileInput from 'components/form/file-input'; import Input from 'components/form/input'; +import LoadingDots from 'components/loading-dots'; +import { FormEventHandler, useRef, useTransition } from 'react'; +import { activateWarranty } from './actions'; type ActivateWarrantyModalProps = { isOpen: boolean; onClose: () => void; + orderId: string; }; -function ActivateWarrantyModal({ onClose, isOpen }: ActivateWarrantyModalProps) { +function ActivateWarrantyModal({ onClose, isOpen, orderId }: ActivateWarrantyModalProps) { + const [pending, startTransition] = useTransition(); + const formRef = useRef(null); + + const handleSubmit: FormEventHandler = (event) => { + event.preventDefault(); + const form = formRef.current; + if (!form) return; + const formData = new FormData(form); + + startTransition(async () => { + await activateWarranty(orderId, formData); + form.reset(); + onClose(); + }); + }; + return ( Activate Warranty -
+
- - - - + + + + +
+
+ +
-
- - -
diff --git a/components/orders/activate-warranty.tsx b/components/orders/activate-warranty.tsx index 8ba7c7c9c..7777cd52e 100644 --- a/components/orders/activate-warranty.tsx +++ b/components/orders/activate-warranty.tsx @@ -1,13 +1,14 @@ 'use client'; +import { Order } from 'lib/shopify/types'; import { useState } from 'react'; import ActivateWarrantyModal from './activate-warranty-modal'; type ActivateWarrantyModalProps = { - orderId: string; + order: Order; }; -const ActivateWarranty = ({ orderId }: ActivateWarrantyModalProps) => { +const ActivateWarranty = ({ order }: ActivateWarrantyModalProps) => { const [isOpen, setIsOpen] = useState(false); return ( <> @@ -17,7 +18,7 @@ const ActivateWarranty = ({ orderId }: ActivateWarrantyModalProps) => { > Activate Warranty - setIsOpen(false)} /> + setIsOpen(false)} orderId={order.id} /> ); }; diff --git a/components/orders/mobile-order-actions.tsx b/components/orders/mobile-order-actions.tsx index 4cfc02f1c..5fcc3acd4 100644 --- a/components/orders/mobile-order-actions.tsx +++ b/components/orders/mobile-order-actions.tsx @@ -55,7 +55,7 @@ const MobileOrderActions = ({ order }: { order: Order }) => { - setIsOpen(false)} /> + setIsOpen(false)} orderId={order.id} /> ); }; diff --git a/lib/constants.ts b/lib/constants.ts index 21b728f12..c3c8d0a82 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -35,7 +35,7 @@ export const TAGS = { export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden'; export const DEFAULT_OPTION = 'Default Title'; export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2024-04/graphql.json'; -export const SHOPIFY_GRAPHQL_CUSTOMER_API_ENDPOINT = '/account/customer/api/2024-07/graphql'; +export const SHOPIFY_GRAPHQL_CUSTOMER_API_ENDPOINT = '/account/customer/api/2024-04/graphql'; export const SHOPIFY_GRAPHQL_ADMIN_ADMIN_API_ENDPOINT = '/admin/api/2024-04/graphql.json'; export const CORE_WAIVER = 'core-waiver'; @@ -60,3 +60,10 @@ export const ADD_ON_PRODUCT_TYPES = { addOn: 'Add On', coreCharge: 'Core Charge' }; + +export const WARRANTY_FIELDS = [ + 'warranty_activation_odometer', + 'warranty_activation_installation', + 'warranty_activation_vin', + 'warranty_activation_mileage' +]; diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index 6fd5c12cd..dbd69e2fb 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -11,6 +11,7 @@ import { SHOPIFY_GRAPHQL_CUSTOMER_API_ENDPOINT, TAGS, VARIANT_METAFIELD_PREFIX, + WARRANTY_FIELDS, YEAR_FILTER_ID } from 'lib/constants'; import { isShopifyError } from 'lib/type-guards'; @@ -26,6 +27,7 @@ import { setCartAttributesMutation } from './mutations/cart'; import { createFileMutation, createStageUploads } from './mutations/file'; +import { updateOrderMetafieldsMutation } from './mutations/order'; import { getCartQuery } from './queries/cart'; import { getCollectionProductsQuery, @@ -57,6 +59,7 @@ import { Image, LineItem, Menu, + Metafield, Metaobject, Money, Order, @@ -97,9 +100,11 @@ import { ShopifySetCartAttributesOperation, ShopifyStagedUploadOperation, ShopifyUpdateCartOperation, + ShopifyUpdateOrderMetafieldsOperation, Transaction, TransmissionType, - UploadInput + UploadInput, + WarrantyStatus } from './types'; const domain = process.env.SHOPIFY_STORE_DOMAIN @@ -592,6 +597,7 @@ function reshapeOrder(shopifyOrder: ShopifyOrder): Order { totalPrice: reshapeMoney(item.totalPrice) })) || []; + console.log(shopifyOrder); const order: Order = { id: shopifyOrder.id, normalizedId: shopifyOrder.id.replace('gid://shopify/Order/', ''), @@ -1072,5 +1078,45 @@ export const createFile = async (params: FileCreateInput) => { variables: { files: [params] } }); - return res.body.data; + return res.body.data.fileCreate.files; +}; + +export const updateOrderMetafields = async ({ + orderId, + metafields +}: { + orderId: string; + metafields: { key: string; value: string | undefined | null; type: string }[]; +}) => { + const validMetafields = ( + metafields.filter((field) => Boolean(field)) as Array> + ).map((field) => ({ + ...field, + namespace: 'custom' + })); + + const shouldSetWarrantyStatusToActivated = WARRANTY_FIELDS.every((field) => + validMetafields.find(({ key }) => key === field) + ); + + const response = await adminFetch({ + query: updateOrderMetafieldsMutation, + variables: { + input: { + metafields: shouldSetWarrantyStatusToActivated + ? validMetafields.concat([ + { + key: 'warranty_status', + value: WarrantyStatus.Activated, + namespace: 'custom', + type: 'single_line_text_field' + } + ]) + : validMetafields, + id: orderId + } + } + }); + + return response.body.data.orderUpdate.order.id; }; diff --git a/lib/shopify/mutations/file.ts b/lib/shopify/mutations/file.ts index c9944c6ef..75cab4a36 100644 --- a/lib/shopify/mutations/file.ts +++ b/lib/shopify/mutations/file.ts @@ -18,9 +18,7 @@ export const createFileMutation = /* GraphQL */ ` fileCreate(files: $files) { files { fileStatus - ... on MediaImage { - id - } + id } userErrors { field diff --git a/lib/shopify/mutations/order.ts b/lib/shopify/mutations/order.ts new file mode 100644 index 000000000..02df6a108 --- /dev/null +++ b/lib/shopify/mutations/order.ts @@ -0,0 +1,13 @@ +export const updateOrderMetafieldsMutation = /* GraphQL */ ` + mutation updateOrderMetafields($input: OrderInput!) { + orderUpdate(input: $input) { + order { + id + } + userErrors { + message + field + } + } + } +`; diff --git a/lib/shopify/types.ts b/lib/shopify/types.ts index fdded6e1c..4965f259f 100644 --- a/lib/shopify/types.ts +++ b/lib/shopify/types.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ export type Maybe = T | null; export type Connection = { @@ -825,3 +826,33 @@ export type ShopifyCreateFileOperation = { }; variables: { files: FileCreateInput[] }; }; + +export type Metafield = { + namespace: string; + value: string; + key: string; + type: string; +}; + +export type ShopifyUpdateOrderMetafieldsOperation = { + data: { + orderUpdate: { + order: { + id: string; + }; + userErrors: { field: string; message: string }[]; + }; + }; + variables: { + input: { + metafields: Metafield[]; + id: string; + }; + }; +}; + +export enum WarrantyStatus { + Activated = 'Activated', + NotActivated = 'Not Activated', + LimitedActivated = 'Limited Activation' +} diff --git a/tsconfig.json b/tsconfig.json index 6cd05f98a..4e93cf11a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,