Skip to main content

POS Extension

The POS (Point of Sale) extension enables retail staff to create and manage subscription orders directly from Shopify POS. This extension provides multiple UI components that integrate subscription functionality into the point of sale workflow, allowing in-person subscription sales.

Extension Configuration

extensions/pos-extension/shopify.extension.toml
api_version = "2025-10"

[[extensions]]
type = "ui_extension"
name = "pos-extension"
handle = "pos-extension"
description = "Subscriptions"

[[extensions.targeting]]
module = "./src/MenuItemModal.tsx"
target = "pos.cart.line-item-details.action.render"

[[extensions.targeting]]
module = "./src/MenuItem.tsx"
target = "pos.cart.line-item-details.action.menu-item.render"

[[extensions.targeting]]
module = "./src/Tile.tsx"
target = "pos.home.tile.render"

[[extensions.targeting]]
module = "./src/TileModal.tsx"
target = "pos.home.modal.render"

Extension Targets

The POS extension integrates at four key touchpoints:

Cart Line Item Menu

Target: pos.cart.line-item-details.action.menu-item.renderAdds a “Subscriptions” button to cart line item action menus

Line Item Modal

Target: pos.cart.line-item-details.action.renderFull modal for selecting subscription options on cart items

Home Tile

Target: pos.home.tile.renderDashboard tile on POS home screen for quick access

Home Modal

Target: pos.home.modal.renderModal opened from home tile for subscription management

Components

Home Tile

Displays subscription information on the POS home screen:
extensions/pos-extension/src/Tile.tsx
import {render} from 'preact';
import {useSellingPlanTile} from './hooks/useSellingPlanTile';
import I18nProvider from './components/I18nProvider';
import i18n from './i18n/config';

const TileComponent = () => {
  const {title, subtitle, enabled, disabledReason, onCartChange} =
    useSellingPlanTile(
      shopify.cart.current.value,
      i18n,
      shopify.session.currentSession.posVersion,
    );

  shopify.cart.current.subscribe((cart) => {
    onCartChange(cart);
  });

  return (
    <s-tile
      heading={title}
      subheading={subtitle}
      onClick={() => {
        shopify.action.presentModal();
      }}
      disabled={!enabled}
      data-testid="subscription-tile"
      data-disabled-reason={disabledReason}
    />
  );
};

export default async () => {
  render(
    <I18nProvider locale={shopify.locale}>
      <TileComponent />
    </I18nProvider>,
    document.body,
  );
};
Adds subscription action to cart line items:
extensions/pos-extension/src/MenuItem.tsx
import {render} from 'preact';
import I18nProvider from './components/I18nProvider';
import {MINIMUM_VERSION_SUPPORTED} from './constants/constants';
import {isVersionSupported} from './utils/versionComparison';

const ButtonComponent = () => {
  const handleButtonPress = () => {
    shopify.action.presentModal();
  };

  const versionSupported = isVersionSupported(
    shopify.session.currentSession.posVersion,
    MINIMUM_VERSION_SUPPORTED,
  );

  const hasSellingPlanGroups = shopify.cartLineItem?.hasSellingPlanGroups;
  const enabled = hasSellingPlanGroups && versionSupported;

  const disabledReason = !versionSupported
    ? 'version-unsupported'
    : !hasSellingPlanGroups
      ? 'feature-disabled'
      : null;

  return (
    <s-button
      onClick={handleButtonPress}
      disabled={!enabled}
      variant="secondary"
      data-testid="subscription-menu-item"
      data-disabled-reason={disabledReason}
    />
  );
};

export default async () => {
  render(
    <I18nProvider locale={shopify.locale}>,
      <ButtonComponent />
    </I18nProvider>,
    document.body,
  );
};

Version Support

The extension checks POS version compatibility:
extensions/pos-extension/src/constants/constants.ts
export const MINIMUM_VERSION_SUPPORTED = '10.14.0';
extensions/pos-extension/src/utils/versionComparison.ts
export function isVersionSupported(
  currentVersion: string,
  minimumVersion: string
): boolean {
  const current = currentVersion.split('.').map(Number);
  const minimum = minimumVersion.split('.').map(Number);

  for (let i = 0; i < 3; i++) {
    if (current[i] > minimum[i]) return true;
    if (current[i] < minimum[i]) return false;
  }
  return true;
}

Features

Subscription Selection

Staff can select subscription options for products in the cart:
1

Add Product to Cart

Staff adds a subscription-eligible product to the cart
2

Open Line Item Actions

Tap on the cart line item to open actions menu
3

Select Subscription

Choose “Subscriptions” from the menu
4

Choose Plan

Select delivery frequency and any discount options
5

Apply to Cart

Subscription is applied to the line item

Cart Integration

The extension monitors cart state:
shopify.cart.current.subscribe((cart) => {
  // Check if cart has subscription-eligible items
  const hasEligibleItems = cart.lineItems.some(
    item => item.hasSellingPlanGroups
  );
  
  // Update tile state
  updateTileStatus(hasEligibleItems);
});

Selling Plan Selection

Display available selling plans for a product:
interface SellingPlanGroup {
  id: string;
  name: string;
  sellingPlans: SellingPlan[];
}

interface SellingPlan {
  id: string;
  name: string;
  description: string;
  billingPolicy: {
    interval: 'DAY' | 'WEEK' | 'MONTH' | 'YEAR';
    intervalCount: number;
  };
  pricingPolicy: {
    adjustmentType: 'PERCENTAGE' | 'FIXED_AMOUNT' | 'PRICE';
    adjustmentValue: number;
  };
}

POS-Specific Components

The extension uses POS-specific Smart Grid components:
<s-tile
  heading="Subscriptions"
  subheading="Manage subscription items"
  onClick={handleClick}
  disabled={false}
/>
The line item modal allows selling plan selection:
extensions/pos-extension/src/MenuItemModal.tsx
import {render} from 'preact';
import {useState} from 'preact/hooks';

const MenuItemModal = () => {
  const [selectedPlan, setSelectedPlan] = useState(null);
  const cartLineItem = shopify.cartLineItem;
  const sellingPlanGroups = cartLineItem?.sellingPlanGroups || [];

  const handleSelectPlan = async (planId: string) => {
    setSelectedPlan(planId);
    
    // Apply selling plan to line item
    await shopify.cart.applySellingPlan({
      lineItemId: cartLineItem.id,
      sellingPlanId: planId,
    });
    
    shopify.action.dismissModal();
  };

  return (
    <s-modal title="Select Subscription">
      <s-list>
        {sellingPlanGroups.map(group => (
          <div key={group.id}>
            <s-heading>{group.name}</s-heading>
            {group.sellingPlans.map(plan => (
              <s-list-item
                key={plan.id}
                onClick={() => handleSelectPlan(plan.id)}
                selected={selectedPlan === plan.id}
              >
                <s-stack direction="vertical">
                  <s-text weight="bold">{plan.name}</s-text>
                  <s-text size="small">{plan.description}</s-text>
                </s-stack>
              </s-list-item>
            ))}
          </div>
        ))}
      </s-list>
    </s-modal>
  );
};

Tile Modal

The home screen tile opens a modal with subscription management:
extensions/pos-extension/src/TileModal.tsx
const TileModal = () => {
  const cart = shopify.cart.current.value;
  const subscriptionItems = cart.lineItems.filter(
    item => item.sellingPlanAllocation
  );

  return (
    <s-modal title="Subscription Items">
      {subscriptionItems.length === 0 ? (
        <s-text>No subscription items in cart</s-text>
      ) : (
        <s-list>
          {subscriptionItems.map(item => (
            <s-list-item key={item.id}>
              <s-stack direction="horizontal" spacing="base">
                <s-image src={item.image} size="small" />
                <s-stack direction="vertical">
                  <s-text weight="bold">{item.title}</s-text>
                  <s-text size="small">
                    {item.sellingPlanAllocation.sellingPlan.name}
                  </s-text>
                </s-stack>
              </s-stack>
            </s-list-item>
          ))}
        </s-list>
      )}
    </s-modal>
  );
};

Localization

Support multiple languages for POS staff:
extensions/pos-extension/locales/en.json
{
  "tile": {
    "title": "Subscriptions",
    "subtitle": "Manage subscription items",
    "noItems": "No subscription items"
  },
  "modal": {
    "title": "Select Subscription",
    "apply": "Apply",
    "cancel": "Cancel"
  },
  "menuItem": {
    "label": "Subscriptions"
  },
  "errors": {
    "versionUnsupported": "POS version not supported",
    "noPlansAvailable": "No subscription plans available"
  }
}

Error Handling

Handle common POS scenarios:
if (!versionSupported) {
  return (
    <s-banner tone="warning">
      This feature requires POS version {MINIMUM_VERSION_SUPPORTED} or higher.
      Current version: {currentVersion}
    </s-banner>
  );
}

if (!hasSellingPlanGroups) {
  return (
    <s-banner tone="info">
      This product doesn't have subscription options available.
    </s-banner>
  );
}

Testing

Test the POS extension on actual POS devices or the POS simulator:
npm run dev -- --pos

Test Scenarios

  1. Version Compatibility: Test on minimum and latest POS versions
  2. Cart States: Empty cart, cart with eligible/ineligible items
  3. Network Issues: Handle offline mode and API failures
  4. Multiple Selling Plans: Products with single and multiple plan groups
  5. Mixed Cart: Cart with subscription and one-time purchase items

Best Practices

Clear Labeling

Use clear, concise labels for subscription options visible on small screens

Quick Selection

Optimize for speed - retail staff need quick workflows

Visual Feedback

Show immediate confirmation when subscription is applied

Error Prevention

Disable options that aren’t available rather than showing errors

Offline Support

POS extensions should handle offline scenarios:
if (!navigator.onLine) {
  // Show cached subscription options
  const cachedPlans = localStorage.getItem('sellingPlans');
  if (cachedPlans) {
    return JSON.parse(cachedPlans);
  }
  
  // Queue for sync when online
  queueOfflineAction({
    type: 'APPLY_SELLING_PLAN',
    lineItemId,
    sellingPlanId,
  });
}

Customization Examples

Custom Tile Styling

const getTileAppearance = (cartState) => {
  const subscriptionCount = cartState.lineItems.filter(
    item => item.sellingPlanAllocation
  ).length;
  
  return {
    heading: 'Subscriptions',
    subheading: subscriptionCount > 0 
      ? `${subscriptionCount} subscription items`
      : 'Add subscription items',
    variant: subscriptionCount > 0 ? 'primary' : 'secondary',
  };
};

Plan Recommendations

const getRecommendedPlan = (sellingPlans: SellingPlan[]) => {
  // Recommend most frequent billing for retail
  return sellingPlans.sort((a, b) => {
    const aDays = intervalToDays(a.billingPolicy);
    const bDays = intervalToDays(b.billingPolicy);
    return aDays - bDays;
  })[0];
};

Admin Actions

Create selling plan groups available in POS

Buyer Subscriptions

Customer portal for managing POS-created subscriptions

Requirements

  • POS Version: 10.14.0 or higher
  • Hardware: Compatible POS devices (iPad, Android tablets)
  • Network: Internet connection for initial setup (offline support for operations)
  • Permissions: Staff must have cart modification permissions

Build docs developers (and LLMs) love