Upgrade to subscription block

Overview

Loop's native Checkout Upgrade block lets shoppers convert a one-time purchase into a subscription directly inside Shopify Checkout, with a dropdown to pick the delivery frequency. This works out of the box for most merchants. Learn more

However, some brands want deeper UI control in checkout, for example:

  • Layout: Showing each delivery frequency as an individual button / card / radio button instead of a dropdown to make the choices more discoverable.
  • Badges: Adding badges like "Popular" or "Best value" on specific plans
  • Business logic: Conditionally showing or ordering frequencies based on brand-specific logic
  • Styling: Change styles of button, text and other components

Because Shopify's Checkout UI Extensibility restricts custom CSS on first-party app blocks, the right way to achieve this is for the brand's development team (or agency) to build a custom Shopify Checkout UI extension that uses Loop's selling plan data and applies the shopper's choice to the cart line. This guide walks you through how to do that.


How it works?

At a high level, your custom Checkout UI extension will:

  • Fetch the eligible selling plans for each product in the cart from the Shopify Storefront API, including Loop's checkout availability metafields that drive selling plan visibility on Checkout blocks.
  • Filter and sort the plans using those metafields — only show plans flagged as available on checkout, in the order Loop has configured.
  • Render your custom UI (buttons, radios, cards) using Shopify's Checkout UI extension components.
  • Apply the selected selling plan to the relevant cart line using applyCartLinesChange.

Loop continues to manage the entire subscription lifecycle downstream - contract creation, recurring orders, dunning, the customer portal, and so on. Your extension only owns the frequency selection UI at checkout.


Prerequisites

Before you start, make sure:

  • Store is on Loop Pro plan to access Checkout upgrade feature.
  • Store is on Shopify Plus plan (required for most Checkout UI Extensibility targets).
  • Loop's native Checkout Upgrade block is disabled for the targets where your custom extension will render. Otherwise, both UIs will appear simultaneously.
  • Selling plans are created and configured in the Loop admin - not directly in Shopify. Loop writes the checkout availability metafields your extension will read.
  • Familiarity with Shopify Checkout UI Extensions. Learn more

Step 1: Fetch eligible selling plans

For each product in the cart, query the Shopify Storefront API to fetch its selling plans along with the Loop metafields that govern checkout behavior.

You can refer the following GraphQL query (specifically line 101-109)

query ProductDetails(
  $id: ID!
  $metafieldPrepaidKey: String!
  $metafieldCheckoutUpgradeKey: String!
  $metafieldDefaultSellingPlanKey: String!
  $metafieldSellingPlanGroupPositionKey: String!
) {
  product(id: $id) {
    sellingPlanGroups(first: 10) {
      edges {
        node {
          sellingPlans(first: 10) {
            edges {
              node {
                id
                name
                deliveryPolicy {
                  ... on SellingPlanRecurringDeliveryPolicy {
                    __typename
                    interval
                    intervalCount
                  }
                }
                isDefaultSellingPlan: metafield(key: $metafieldDefaultSellingPlanKey) {
                  value
                }
                sellingPlanGroupPosition: metafield(key: $metafieldSellingPlanGroupPositionKey) {
                  value
                }
                priceAdjustments {
                  adjustmentValue {
                    ... on SellingPlanFixedAmountPriceAdjustment {
                      __typename
                      adjustmentAmount {
                        amount
                        currencyCode
                      }
                    }
                    ... on SellingPlanFixedPriceAdjustment {
                      __typename
                      price {
                        amount
                        currencyCode
                      }
                    }
                    ... on SellingPlanPercentagePriceAdjustment {
                      __typename
                      adjustmentPercentage
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    variants(first: 50) {
      edges {
        node {
          id
          price {
            amount
            currencyCode
          }
          sellingPlanAllocations(first: 50) {
            edges {
              node {
                sellingPlan {
                  id
                  name
                  deliveryPolicy {
                    ... on SellingPlanRecurringDeliveryPolicy {
                      __typename
                      interval
                      intervalCount
                    }
                  }
                  priceAdjustments {
                    adjustmentValue {
                      ... on SellingPlanFixedAmountPriceAdjustment {
                        __typename
                        adjustmentAmount {
                          amount
                          currencyCode
                        }
                      }
                      ... on SellingPlanFixedPriceAdjustment {
                        __typename
                        price {
                          amount
                          currencyCode
                        }
                      }
                      ... on SellingPlanPercentagePriceAdjustment {
                        __typename
                        adjustmentPercentage
                      }
                    }
                  }
                  isSpgAvailableOnCheckout: metafield(key: $metafieldCheckoutUpgradeKey) {
                    value
                  }
                  isDefaultSellingPlan: metafield(key: $metafieldDefaultSellingPlanKey) {
                    value
                  }
                  sellingPlanGroupPosition: metafield(key: $metafieldSellingPlanGroupPositionKey) {
                    value
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Metafields keys for GraphQL query

Loop stores its checkout configuration on selling-plan metafields. Pass the following keys as query variables to get relevant data:

VariableWhat it controls
metafieldCheckoutUpgradeKeyWhether the selling plan should be shown on checkout (isSpgAvailableOnCheckout)
metafieldDefaultSellingPlanKeyWhether this plan is the default / popular option
metafieldSellingPlanGroupPositionKeyThe display order of each plan

Note: These keys are consistent across all Loop merchants but must be passed accurately for the query to return values.


Step 2: Filter, sort, and identify the default plan

Once you have the response, on the variant's sellingPlanAllocations:

  1. Filter plans where isSpgAvailableOnCheckout.value === "true". This will give you a list of all the selling plans marked as available on Checkout. Manage availability of selling plans by visiting Loop admin > Acquire > Selling plans > Specific selling plan > Availability > Channels.
  2. Sort the filtered plans by the numeric value of sellingPlanGroupPosition. This is the order the merchant has configured in Loop.
  3. Identify the default plan by finding the plan where isDefaultSellingPlan.value === "true". Use this to pre-select that frequency on render and maybe display a "Recommended" badge.

The final output is a clean, ordered list of selling plans ready to render.


Step 3: Render your custom UI

Use Shopify's Checkout UI Extension components to render each selling plan as a button, radio, or card. The choice of components is yours - Loop has no requirement here, as long as the shopper can pick exactly one frequency per cart line (or choose one-time purchase).

Refer to the Shopify Checkout UI Extensions component for the available components and styling primitives.


Step 4: Apply the selected selling plan to the cart line

When the shopper picks a purchase option, specific frequency or one-time, apply it to the relevant cart line using the applyCartLinesChange hook from @shopify/ui-extensions-react/checkout.

You can refer below snippet

await applyCartLinesChange({
  type: "updateCartLine",
  id: line.id,
  sellingPlanId: plan.id,
  attributes: [
    ...line.attributes,
  ],
});

where:

  • line.id is the ID of the cart line being upgraded to a subscription
  • plan.id is the selling plan ID of the frequency the shopper selected (from Step 2)
  • attributes preserves any existing line attributes — important if Loop or the merchant has already attached metadata to the line

For a one-time purchase selection, omit sellingPlanId (or pass null) to remove any previously applied selling plan from the line.

Once applyCartLinesChange resolves successfully, Shopify will recalculate the cart with the subscription pricing, and the order once placed will flow into Loop as a subscription contract automatically. No additional API call to Loop is required.


Best practices

  • Disable Loop's native Checkout Upgrade block on the same target before deploying your extension. Running both will render a duplicate UI to the shopper.
  • Handle the "no eligible plans" case gracefully - if a product has no selling plans flagged as available on checkout, make sure not to render an empty card.
  • Cap the number of buttons rendered. If a product has more than 4-5 frequencies, consider falling back to a dropdown or a "Show more" pattern to avoid an unusable layout.
  • Test with multiple cart-line scenarios, specifically mixed carts (subscription + one-time), products with different selling plan groups, and prepaid plans.
  • Re-run the query when the cart changes. A shopper adding a new product to the cart should trigger a fresh fetch for that product.
  • Respect Loop's selling plan IDs as the source of truth. Do not create or modify selling plans directly in Shopify - always manage them from the Loop admin so the checkout availability and other metafields stay in sync.

Need help?

If you run into issues - incorrect metafield values, missing selling plans on the response, or unexpected behavior after applyCartLinesChange - reach out to your Loop CSM or write to [email protected] with:

  • The store's myshopify.com URL
  • The product ID and variant ID being tested
  • The full request and response from the GraphQL query
  • A screenshot of your extension and the Loop admin selling plan configuration