Back to blog

Shopify's "Buy Again" Problem - Preserving Line Item Properties

5 min
Wojciech Kałużny
Wojciech Kałużny

For merchants who sell customizable products, the "Buy Again" button in Shopify's new Customer Account pages presents a significant challenge. When a customer reorders, any custom line item properties—like engravings, monograms, or special add-ons—are lost. This limitation, widely discussed in a Shopify Community thread, forces customers to re-enter their customizations or leaves merchants clueless as to the order specifics.

We've recently encountered the problem with one of our clients, here are all of the solutions to that issue.

The Core Problem

When a customer clicks the default "Buy Again" button, Shopify creates a new cart with the same product variants and quantities. However, it completely ignores the properties attached to the original line items. As one community member put it, this makes the feature "fairly useless for products that required custom information."

"Buy Again" button in customer accounts screens works by utilizing cart permalinks which can be constructed with different properties. However, when it comes to line item properties, we can only add them to the first item in the cart.

Simplest solution - disable Buy again.

If the "Buy Again" feature is breaking a core part of your store's operations, the most straightforward and immediate solution is often the simplest: disable it.

A partially implemented or broken feature can cause more significant problems—like incorrect orders and customer support issues—than removing the feature altogether. For merchants facing this issue, Shopify has provided the option to hide the "Buy again" button.

Buy again button option
Buy again button option

To disable buy again button go to your store dashboard navigate to Settings > Checkout, click on Customize then in editor go to Settings and Scroll down to find Buy again button option

This serves as a crucial stopgap, preventing a negative customer experience while you work on a more permanent, custom solution.

Our goal was to create a new "Buy Again with Properties" button that would appear on the order status page for relevant orders. The development process was iterative, exploring several of Shopify's APIs to find the most effective approach.

Creating an alternative with Shopify UI Extensions

Initial Idea: Backend API Routes

Inspired by the community discussion, our first approach involved the customer account extension calling a custom backend API route within our app. This route would then use the Admin API to create a new cart with the correct properties and return a checkoutUrl. This works, but it adds an extra layer of complexity by involving a server-side component for a task that could potentially be handled on the frontend.

Next, we explored Cart Permalinks. This method allows you to construct a URL that, when clicked, directs the user to a pre-filled cart. Line item properties can be included by Base64 URL-encoding a JSON object.

While this method removes the need for a backend call, it has drawbacks:

  • Limited to only 1 custom item: This method only allows for a single custom item to have added priorities.
  • URL Length Limits: Complex products with many properties could result in URLs that are too long for some browsers.
  • Encoding Complexity: Managing the Base64 encoding and URL formatting can be cumbersome.
  • Limited Error Handling: It's more difficult to manage errors gracefully if the cart creation fails.

The Winning Solution: Direct Storefront API from the Extension

The most elegant solution came from leveraging the tools provided directly within the Customer Account UI extension framework.

The Storefront API is directly accessible from the extension using the query function provided by the useApi() hook. This allows us to create a new cart with all the necessary properties and get a checkoutUrl in return, all within the frontend extension.

The Final Implementation

Here is a breakdown of the final, working code.

1. Scaffold a new UI Extension

Using shopify's cli scaffold a new extension.

shopify app generate extension

Select Customer Account UI as extension type, name it and select the scaffolding (I recommend Typescript react). The generated extension configuration file should give what you need.

name = "buy-again-with-properties"
type = "ui_extension"

[[extensions.targeting]]
module = "./src/OrderStatusBlock.tsx"
target = "customer-account.order-status.block.render"

[extensions.capabilities]
api_access = true
network_access = true # Required for the query function

The target we want is customer-account.order-status.block.render, but you can also consider adding it in payments placement.

2. Develop UI Extension (OrderStatusBlock.tsx)

The component uses hooks provided by Shopify to access order data and execute GraphQL mutations.

  • useCartLines(): This hook is the correct way to get the line items from the current order, including their custom attributes.
  • useApi(): This provides the query function to call the Storefront API, and navigation object to redirect once the cart is created
import {
  reactExtension,
  useCartLines,
  Button,
  BlockStack,
  useApi,
} from "@shopify/ui-extensions-react/customer-account";
import { useState } from "react";

export default reactExtension(
  "customer-account.order-status.block.render",
  () => <Extension />,
);

const CART_CREATE_MUTATION = `
  mutation cartCreate($input: CartInput!) {
    cartCreate(input: $input) {
      cart {
        id
        checkoutUrl
      }
      userErrors {
        field
        message
      }
    }
  }
`;

function Extension() {
  const lines = useCartLines();
  const { query, navigation } = useApi();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleBuyAgain = async () => {
    if (!lines?.length) return;

    setIsLoading(true);
    setError(null);

    try {
      // Prepare cart lines with attributes
      const cartLines = lines.map((line: any) => ({
        merchandiseId: line.merchandise.id,
        quantity: line.quantity,
        attributes: line.attributes || [],
      }));

      // Create cart using Storefront API
      const result: any = await query(CART_CREATE_MUTATION, {
        variables: {
          input: {
            lines: cartLines,
          },
        },
      });

      if (result.data?.cartCreate?.cart?.checkoutUrl) {
        // Redirect to checkout
        navigation.navigate(result.data.cartCreate.cart.checkoutUrl);
      } else if (result.data?.cartCreate?.userErrors?.length > 0) {
        setError(result.data.cartCreate.userErrors[0].message);
        console.error(result.data.cartCreate.userErrors[0].message);
      } else {
        setError("Failed to create cart");
      }
    } catch (err) {
      console.error("Error creating cart:", err);
      setError("Failed to create cart. Please try again.");
    } finally {
      setIsLoading(false);
    }
  };

  // Hide extension for empty orders
  if (!lines?.length) return null;

  return (
    <BlockStack spacing="base">
      <Button
        kind="secondary"
        loading={isLoading}
        onPress={handleBuyAgain}
        disabled={isLoading}
      >
        Buy again
      </Button>
    </BlockStack>
  );
}

Why This Approach Works Best

  1. Security and Reliability: It uses Shopify's authenticated Storefront API, which is more secure and reliable than constructing URLs manually.
  2. No window Object: The solution avoids using the window object for navigation (e.g., window.open()), which is not permitted in Customer Account UI extensions. Instead, it uses the to prop on the Button component for navigation.
  3. Superior User Experience: The component provides clear loading states and quick redirect. Once the cart is created, the button cleverly transforms into a direct link to the checkout, providing a clean two-step process for the user.
  4. No Backend Needed: The entire logic is self-contained within the frontend extension, simplifying the architecture of the Shopify app and reducing maintenance.
  5. What can be improved? The drawback is that we can't redirect to the storefront in this example, just the checkout. However generated cart in this case is a 1-click experience - shipping details are already provided for the customer!

Conclusion

Solving the "Buy Again" problem for customizable products is crucial for a positive customer experience and for your sanity.

If you need a custom Shopify app fine-tuned for your needs, reach out to us today.

By leveraging the power of Customer Account UI Extensions and the direct Storefront API access they provide, we can build a seamless, reliable, and user-friendly solution that correctly preserves all line item properties.

This approach not only fixes the immediate issue but also aligns with Shopify's modern development practices, ensuring a robust and future-proof implementation.

More articles

Step-by-Step Guide to Shopify Funnel Tracking

Tim Davidson

How to Analyze Shopify Funnel Drop-Offs

Tim Davidson

What's a Good Shopify Conversion Rate? 2025 Benchmarks

Tim Davidson