commerce icon indicating copy to clipboard operation
commerce copied to clipboard

Code for Cart Discount

Open osseonews opened this issue 3 months ago • 0 comments

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.

  1. 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}
`;
  1. 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);
}
  1. 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';
  }
}
  1. 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>
  );
}

osseonews avatar Mar 31 '24 22:03 osseonews