--- title: Build a subscription UI extension description: >- Learn how to develop a scaffolded POS UI extension into a simple subscription UI extension. source_url: html: >- https://shopify.dev/docs/apps/build/pos/build-subscription-extension?extension=polaris md: >- https://shopify.dev/docs/apps/build/pos/build-subscription-extension.md?extension=polaris --- # Build a subscription UI extension This tutorial shows you how to build a [POS UI extension](https://shopify.dev/docs/api/pos-ui-extensions/latest) that enables subscription [selling plans](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/selling-plans) for products in the cart. You'll create an extension that allows merchants to offer recurring purchases directly from [Shopify POS](https://apps.shopify.com/shopify-pos), turning one-time buyers into subscribers. ## What you'll learn In this tutorial, you'll learn how to do the following tasks: * Query the [GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql/latest) for subscription selling plan data from within Shopify POS. * Build a tile that shows subscription-eligible items. * Create a modal interface for selecting subscription options. * Display pricing information and savings for subscriptions. * Use the [Cart API](https://shopify.dev/docs/api/pos-ui-extensions/latest/apis/cart-api) to apply and remove selling plans from line items. ![POS subscription extension](https://shopify.dev/assets/assets/apps/pos/pos-ui-extensions-subscription-example-BJmnekYn.gif) ## Requirements [Create a Partner account](https://www.shopify.com/partners) [Create a development store](https://shopify.dev/docs/apps/tools/development-stores#create-a-development-store-to-test-your-app) [Scaffold an app](https://shopify.dev/docs/apps/build/scaffold-app) Scaffold an app using Shopify CLI. This tutorial is compatible with the [extension-only template](https://shopify.dev/docs/apps/structure/app-extensions/extension-only-apps). Set up a development store with: * Shopify [POS sales channel installed](https://help.shopify.com/en/manual/online-sales-channels/manage#add-an-online-sales-channel-to-your-shopify-admin). * Products with subscription [selling plans configured](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/selling-plans/build-a-selling-plan), published to the Shopify POS sales channel. * [Shopify Payments](https://help.shopify.com/en/manual/payments/shopify-payments) enabled. * See [additional store configuration requirements](https://help.shopify.com/en/manual/sell-in-person/shopify-pos/payment-management/subscriptions#requirements-for-selling-subscription-products-on-shopify-pos). [Generate a POS UI extension](https://shopify.dev/docs/apps/build/pos/getting-started) Generate a POS UI extension (minimum version: [2025-10](https://shopify.dev/docs/api/pos-ui-extensions/2025-10)) using the Shopify CLI. When prompted for a type of extension, select `POS smart grid` to generate `Tile.jsx` and `Modal.jsx` files. ## Project Polaris ### Update the tile based on cart content In `Tile.jsx`, subscribe to cart changes and update the tile state based on whether the cart contains items eligible for selling plans. [POS UI extensions version 2025-10](https://shopify.dev/docs/api/pos-ui-extensions/2025-10) introduced properties to indicate whether a product has selling plan groups and the currently applied selling plan. ```ts interface LineItem { uuid: string; productId: number; hasSellingPlanGroups?: boolean; // Product has available selling plans. sellingPlan?: SellingPlan; // Currently applied selling plan. } ``` Important These new fields and methods are available only if you're using: * **POS UI extensions version 2025-10**: Older APIs don't expose this field. * **Shopify POS 10.13+**: Using selling plan-related APIs and fields in older versions results in blocked checkouts. We recommend using the [Session API](https://shopify.dev/docs/api/pos-ui-extensions/2025-10/apis/session-api) (`shopify.session.currentSession.posVersion`) to handle the UI differently if your extension is running in an unsupported version. Subscribe to cart changes to detect when line items eligible for selling plans are added to the cart. ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/Tile.jsx ```jsx import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; export default function extension() { render(, document.body); } function Tile() { const [sellingPlanEligible, setSellingPlanEligible] = useState(false); useEffect(() => { const unsubscribe = shopify.cart.current.subscribe((cart) => { const sellingPlanEligibleLineItems = cart.lineItems.find( (lineItem) => lineItem.hasSellingPlanGroups === true, ); setSellingPlanEligible(sellingPlanEligibleLineItems !== undefined); }); return unsubscribe; }, []); return ( shopify.action.presentModal()} /> ); } ``` Enable or disable the tile based on the number of line items eligible for selling plans. ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/Tile.jsx ```jsx import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; export default function extension() { render(, document.body); } function Tile() { const [sellingPlanEligible, setSellingPlanEligible] = useState(false); useEffect(() => { const unsubscribe = shopify.cart.current.subscribe((cart) => { const sellingPlanEligibleLineItems = cart.lineItems.find( (lineItem) => lineItem.hasSellingPlanGroups === true, ); setSellingPlanEligible(sellingPlanEligibleLineItems !== undefined); }); return unsubscribe; }, []); return ( shopify.action.presentModal()} /> ); } ``` ### Fetch selling plans using Direct Access API You can make GraphQL Admin API requests directly from your extension using [Direct Access API](https://shopify.dev/docs/api/pos-ui-extensions/latest#direct-api-access) to retrieve selling plans for products in the cart. Create a function that fetches selling plans for a given variant. Info * You'll need the `read_products` access scope in your `shopify.app.toml` file to query variant data. * This example shows the basic implementation. Your production app should handle error scenarios and pagination. Warning * Shopify POS currently only supports subscription selling plans, so you must filter your selling plans by `category === 'SUBSCRIPTION'`. * Shopify POS currently doesn't support subscription-only products. ### Display selling plans In `Modal.jsx`, display the available selling plans for products in the cart, allowing merchants to select subscription options. Navigate to `Modal.jsx` and add code to display selling plans for a given variant. ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/Modal.jsx ```jsx import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; import {fetchSellingPlans} from './FetchSellingPlans'; export default function extension() { render(, document.body); } function Modal() { // For this example, we'll just use the first selling plan item // Your app should handle displaying multiple line items with selling plan groups. const sellingPlanItem = shopify.cart.current.value.lineItems.find( (lineItem) => lineItem.hasSellingPlanGroups === true, ); const [response, setResponse] = useState(undefined); useEffect(() => { async function getSellingPlans() { setResponse(await fetchSellingPlans(sellingPlanItem?.variantId)); } getSellingPlans(); }, [sellingPlanItem]); const handleClick = (plan) => { shopify.cart.addLineItemSellingPlan({ lineItemUuid: sellingPlanItem.uuid, // convert from GID to ID sellingPlanId: Number(plan.id.split('/').pop()), sellingPlanName: plan.name, }); window.close(); }; return ( {response?.data.productVariant.sellingPlanGroups.nodes.map( (group) => { return ( {group.sellingPlans.nodes.map((plan) => { return ( { handleClick(plan); }} > {plan.name} ); })} ); }, )} ); } ``` For simplicity, this example uses only the first selling plan item in the cart. Your production app should allow selling plan selection for all eligible items in the cart. ### Assign a selling plan to a line item In this step, you'll use the Cart API fields introduced in [POS UI extensions version 2025-10](https://shopify.dev/docs/api/pos-ui-extensions/2025-10) to apply a selling plan to a line item when a merchant selects a subscription option. Create a function that assigns a selling plan to the item. ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/Modal.jsx ```jsx import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; import {fetchSellingPlans} from './FetchSellingPlans'; export default function extension() { render(, document.body); } function Modal() { // For this example, we'll just use the first selling plan item // Your app should handle displaying multiple line items with selling plan groups. const sellingPlanItem = shopify.cart.current.value.lineItems.find( (lineItem) => lineItem.hasSellingPlanGroups === true, ); const [response, setResponse] = useState(undefined); useEffect(() => { async function getSellingPlans() { setResponse(await fetchSellingPlans(sellingPlanItem?.variantId)); } getSellingPlans(); }, [sellingPlanItem]); const handleClick = (plan) => { shopify.cart.addLineItemSellingPlan({ lineItemUuid: sellingPlanItem.uuid, // convert from GID to ID sellingPlanId: Number(plan.id.split('/').pop()), sellingPlanName: plan.name, }); window.close(); }; return ( {response?.data.productVariant.sellingPlanGroups.nodes.map( (group) => { return ( {group.sellingPlans.nodes.map((plan) => { return ( { handleClick(plan); }} > {plan.name} ); })} ); }, )} ); } ``` The Cart API offers two methods for selling plan management: ```ts // Apply a selling plan to a line item. shopify.cart.addLineItemSellingPlan({ lineItemUuid: 'line-item-uuid', sellingPlanId: 123456, sellingPlanName: 'Monthly Subscription - 10% off' }); // Remove a selling plan from a line item. shopify.cart.removeLineItemSellingPlan('line-item-uuid'); ``` This example handles only selling plan selection. To remove a selling plan, use the `removeLineItemSellingPlan` method. Refer to [`addLineItemSellingPlan`](https://shopify.dev/docs/api/pos-ui-extensions/2025-10/apis/cart-api#cartapi-propertydetail-addlineitemsellingplan) and [`removeLineItemSellingPlan`](https://shopify.dev/docs/api/pos-ui-extensions/2025-10/apis/cart-api#cartapi-propertydetail-removelineitemsellingplan). ## (Optional) Add to other action targets You can extend the subscription functionality to other action targets in your extension. This example uses the line item details action menu item and its corresponding action. Create new files for the line item details action menu item and action: `MenuItem.jsx` and `Action.jsx`. Add new [extension targets](https://shopify.dev/docs/apps/build/app-extensions/configure-app-extensions#common-properties) to your extension configuration TOML file and reference the new files as modules. ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/shopify.extension.toml ```toml api_version = "2025-10" [[extensions]] type = "ui_extension" name = "Subscription Tutorial" handle = "subscription-tutorial" description = "POS UI extension subscription tutorial" [[extensions.targeting]] module = "./src/Tile.jsx" target = "pos.home.tile.render" [[extensions.targeting]] module = "./src/Modal.jsx" target = "pos.home.modal.render" # Optional additional targets for line item details screen [[extensions.targeting]] module = "./src/Action.jsx" target = "pos.cart.line-item-details.action.render" [[extensions.targeting]] module = "./src/MenuItem.jsx" target = "pos.cart.line-item-details.action.menu-item.render" ``` In `MenuItem.jsx`, enable the line item details action menu item when the cart line item is eligible for selling plans. In `Action.jsx`, fetch selling plans for the item, display them, and handle selling plan selection. ### Add a description for your extension In Shopify POS, a description helps merchants discover and add your extension. This description is used as the label for your extension in Shopify POS. When you generate a POS UI extension from Shopify CLI, the extension description defaults to the name of the extension. You can update the description in the generated TOML file (`shopify.extension.toml`). ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/shopify.extension.toml ```toml api_version = "2025-10" [[extensions]] type = "ui_extension" name = "Subscription Tutorial" handle = "subscription-tutorial" description = "POS UI extension subscription tutorial" [[extensions.targeting]] module = "./src/Tile.jsx" target = "pos.home.tile.render" [[extensions.targeting]] module = "./src/Modal.jsx" target = "pos.home.modal.render" # Optional additional targets for line item details screen [[extensions.targeting]] module = "./src/Action.jsx" target = "pos.cart.line-item-details.action.render" [[extensions.targeting]] module = "./src/MenuItem.jsx" target = "pos.cart.line-item-details.action.menu-item.render" ``` ## Test and deploy Test your extension with real selling plan data in your development store. Verify the following: * Products with subscription selling plans appear correctly. * Selling plans can be applied and removed from line items. * Edge cases are handled properly (for example, products without selling plans, or API errors). For more information, refer to [Deploy app extensions](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions). Troubleshooting ##### Selling plans not appearing If selling plan items aren't showing up, complete the following checks: * Verify that products have active selling plan groups. * Verify that products have been published to the Shopify POS sales channel. * Confirm that the variant ID is correct in your query. ##### Can't apply selling plan to cart Common causes: * The selling plan ID might be incorrect. * The product might not be eligible for the selected plan. Check the browser console for specific error messages. ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/Tile.jsx ```jsx import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; export default function extension() { render(, document.body); } function Tile() { const [sellingPlanEligible, setSellingPlanEligible] = useState(false); useEffect(() => { const unsubscribe = shopify.cart.current.subscribe((cart) => { const sellingPlanEligibleLineItems = cart.lineItems.find( (lineItem) => lineItem.hasSellingPlanGroups === true, ); setSellingPlanEligible(sellingPlanEligibleLineItems !== undefined); }); return unsubscribe; }, []); return ( shopify.action.presentModal()} /> ); } ``` ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/FetchSellingPlans.js ```javascript export async function fetchSellingPlans(variantId) { const requestBody = { query: `#graphql query GetSellingPlans($variantId: ID!) { productVariant(id: $variantId) { # Note: For production use, implement pagination to fetch all sellingPlanGroups and sellingPlans as needed. sellingPlanGroups(first: 10) { nodes { name # Handle pagination (see comment above) sellingPlans(first: 10) { nodes { id name category } } } } } } `, variables: {variantId: `gid://shopify/ProductVariant/${variantId}`}, }; const res = await fetch('shopify:admin/api/graphql.json', { method: 'POST', body: JSON.stringify(requestBody), }); return res.json(); } ``` ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/Modal.jsx ```jsx import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; import {fetchSellingPlans} from './FetchSellingPlans'; export default function extension() { render(, document.body); } function Modal() { // For this example, we'll just use the first selling plan item // Your app should handle displaying multiple line items with selling plan groups. const sellingPlanItem = shopify.cart.current.value.lineItems.find( (lineItem) => lineItem.hasSellingPlanGroups === true, ); const [response, setResponse] = useState(undefined); useEffect(() => { async function getSellingPlans() { setResponse(await fetchSellingPlans(sellingPlanItem?.variantId)); } getSellingPlans(); }, [sellingPlanItem]); const handleClick = (plan) => { shopify.cart.addLineItemSellingPlan({ lineItemUuid: sellingPlanItem.uuid, // convert from GID to ID sellingPlanId: Number(plan.id.split('/').pop()), sellingPlanName: plan.name, }); window.close(); }; return ( {response?.data.productVariant.sellingPlanGroups.nodes.map( (group) => { return ( {group.sellingPlans.nodes.map((plan) => { return ( { handleClick(plan); }} > {plan.name} ); })} ); }, )} ); } ``` ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/shopify.extension.toml ```toml api_version = "2025-10" [[extensions]] type = "ui_extension" name = "Subscription Tutorial" handle = "subscription-tutorial" description = "POS UI extension subscription tutorial" [[extensions.targeting]] module = "./src/Tile.jsx" target = "pos.home.tile.render" [[extensions.targeting]] module = "./src/Modal.jsx" target = "pos.home.modal.render" # Optional additional targets for line item details screen [[extensions.targeting]] module = "./src/Action.jsx" target = "pos.cart.line-item-details.action.render" [[extensions.targeting]] module = "./src/MenuItem.jsx" target = "pos.cart.line-item-details.action.menu-item.render" ``` ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/MenuItem.jsx ```jsx import {render} from 'preact'; export default function extension() { render(, document.body); } function MenuItem() { const handleButtonPress = () => { shopify.action.presentModal(); }; const hasSellingPlanGroups = shopify.cartLineItem?.hasSellingPlanGroups; return ( ); } ``` ## /packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/subscription-example/Action.jsx ```jsx import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; import {fetchSellingPlans} from './FetchSellingPlans'; export default function extension() { render(, document.body); } function Action() { const [response, setResponse] = useState(undefined); useEffect(() => { async function getSellingPlans() { setResponse(await fetchSellingPlans(shopify.cartLineItem?.variantId)); } getSellingPlans(); }, [shopify.cartLineItem]); const handleClick = (plan) => { shopify.cart.addLineItemSellingPlan({ lineItemUuid: shopify.cartLineItem.uuid, sellingPlanId: Number(plan.id.split('/').pop()), sellingPlanName: plan.name, }); window.close(); }; return ( {response?.data.productVariant.sellingPlanGroups.nodes.map( (group) => { return ( {group.sellingPlans.nodes.map((plan) => { return ( { handleClick(plan); }} > {plan.name} ); })} ); }, )} ); } ``` ## Learn more [Cart API documentation\ \ Explore the complete reference for cart operations.](https://shopify.dev/docs/api/pos-ui-extensions/apis/cart-api)[Selling plans guide\ \ Learn about subscription selling plans.](https://shopify.dev/docs/apps/selling-strategies/subscriptions/selling-plans)[POS UI extensions reference\ \ Explore the complete targets, APIs, and component reference for POS.](https://shopify.dev/docs/api/pos-ui-extensions)[GraphQL Admin API\ \ Query selling plan data.](https://shopify.dev/docs/api/admin-graphql)