commerce
commerce copied to clipboard
Code for Cart Discount
We developed the code to apply a discount in the cart. I would normally put in a pull request for this type of thing, but our code now has so many conflicts, it would be too complicated. I figured I'd just post the basic code here and hopefully it will help people who are looking to get this functionality. Adapt to your base code, obviously.
- Add a Mutation in mutations/cart.ts
export const CART_DISCOUNT_CODE_UPDATE_MUTATION = /* GraphQL */ `
mutation cartDiscountCodesUpdate ($cartId: ID!
$discountCodes: [String!]) {
cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) {
cart {
...cart
}
}
}
${cartFragment}
`;
- Add a Function in lib/shopify/index where all the functions are:
export async function updateDiscounts(cartId: string, discounts: string[]): Promise<Cart> {
const res = await shopifyFetch<ShopifyDiscountCartOperation>({
query: CART_DISCOUNT_CODE_UPDATE_MUTATION,
variables: {
cartId,
discountCodes: discounts
},
cache: 'no-store'
});
return reshapeCart(res.body.data.cartDiscountCodesUpdate.cart);
}
- Add an Action in cart/actions.ts
export async function applyDiscount(
prevState: any,
formData: FormData
) {
//console.log ("Form Data", formData)
const cartId = cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
}
const schema = z.object({
discountCode: z.string().min(1),
});
const parse = schema.safeParse({
discountCode: formData.get("discountCode"),
});
if (!parse.success) {
return "Error applying discount. Discount code required.";
}
const data = parse.data;
let discountCodes = []; // Create a new empty array - actually this array should be the current array of discount codes, but as we only allow one code now, we create an empty array
discountCodes.push(data.discountCode); // Push the string into the array
// Ensure the discount codes are unique - this is not really necessary now, because we are only using one code
const uniqueCodes = discountCodes.filter((value, index, array) => {
return array.indexOf(value) === index;
});
try {
await updateDiscounts(cartId, uniqueCodes);
//close cart and have tooltip for c
revalidateTag(TAGS.cart);
} catch (e) {
return 'Error applying discount';
}
}
export async function removeDiscount(
prevState: any,
payload: {
discount: string;
discounts: string[];
}
) {
//console.log ("payload", payload)
const cartId = cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
}
const code = payload?.discount
const codes = payload?.discounts ?? [] //the entire array of discounts
if (!code) {
return "Error removing discount. Discount code required.";
}
let discountCodes = codes;
//remove the code from the array and return the array
let newCodes = discountCodes.filter(item => item !== code);
try {
await updateDiscounts(cartId, newCodes);
//close cart and have tooltip for c
revalidateTag(TAGS.cart);
} catch (e) {
return 'Error applying discount';
}
}
- Add a Form Component in your Modal for the cart that is something like this (we use Shadcn, so many our components come from there)
function SubmitButton(props: Props) {
const { pending } = useFormStatus();
const buttonClasses =
'mt-3 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-9 rounded-md px-3';
const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60';
const message = props?.message;
return (
<>
{message && (
<Alert className="my-5" variant="destructive">
<ExclamationTriangleIcon className="mr-2 h-4 w-4" />
<AlertTitle>{message}</AlertTitle>
</Alert>
)}
{pending ? "" : <input type="text" name="discountCode" placeholder="Discount code" required />}
<button
onClick={(e: React.FormEvent<HTMLButtonElement>) => {
if (pending) e.preventDefault();
}}
aria-label="Add Discount Code"
aria-disabled={pending}
className={clsx(buttonClasses, {
'hover:opacity-90': true,
[disabledClasses]: pending
})}
>
<div className="w-full">
{pending ? <LoadingDots className="bg-secondary text-white mb-3" /> : null}
</div>
{pending ? '' : 'Apply Discount'}
</button>
</>
);
}
export function ApplyCartDiscount({ cart }: { cart: Cart }) {
const [message, formAction] = useFormState(applyDiscount, null);
//console.log('Cart Discount Codes', cart?.discountCodes);
//filter the discount codes to remove those where applicablce is false and then jsut get the code in array
//if we have a discount code we don't show the form
const filteredCodes =
cart?.discountCodes && cart?.discountCodes.length
? cart?.discountCodes.filter((code) => code.applicable).map((code) => code.code)
: [];
return (
<>
{filteredCodes && filteredCodes.length > 0 ? (
<div>
{filteredCodes.map((code, index) => (
<Badge key={index} variant="secondary">
<TagIcon className="mr-2 inline-block h-5 w-5" />
{code}
<DeleteDiscountButton item={code} items={filteredCodes}/>
</Badge>
))}
</div>
) : (
<form action={formAction}>
<SubmitButton message={message} />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
</form>
)}
</>
);
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
onClick={(e: React.FormEvent<HTMLButtonElement>) => {
if (pending) e.preventDefault();
}}
aria-label="Remove Discount"
aria-disabled={pending}
className={clsx(
'ease flex',
{
'cursor-not-allowed px-0': pending
}
)}
>
{pending ? (
<LoadingDots className="bg-primary" />
) : (
<XCircleIcon className="mx-5 text-primary hover:border-neutral-800 hover:opacity-80 h-5 w-5" />
)}
</button>
);
}
export function DeleteDiscountButton({ item,items}: { item: string, items:string[]}) {
const [message, formAction] = useFormState(removeDiscount, null);
const payload = {
discount: item,
discounts: items,
};
const actionWithVariant = formAction.bind(null, payload);
return (
<form action={actionWithVariant}>
<SubmitButton />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
</form>
);
}