Skip to content

Commit 064de12

Browse files
authored
Add code examples of POS subscriptions UI extension (#3552)
Resolves shop/issues-retail#18596 # Add subscription example for Point of Sale UI extensions ## Background This PR adds example code for a Point of Sale UI extension that demonstrates how to implement subscription functionality. Related PR to view the code refs being used: [https://app.graphite.com/github/pr/Shopify/shopify-dev/64603/[POS-UI-Extension]-Build-subscription-UI-extension](https://app.graphite.com/github/pr/Shopify/shopify-dev/64603/%5BPOS-UI-Extension%5D-Build-subscription-UI-extension) ## Solution Added example files that demonstrate a complete subscription implementation for Point of Sale UI extensions: - `Action.jsx` - Implements an action component that fetches and displays selling plans for a cart line item - `FetchSellingPlans.js` - Contains the GraphQL query to fetch selling plans for a product variant - `MenuItem.jsx` - Creates a menu item button that is enabled/disabled based on selling plan availability - `Modal.jsx` - Implements a modal that displays available subscription options and handles selection - `Tile.jsx` - Creates a tile component that subscribes to cart updates and enables/disables based on subscription eligibility - `shopify.extension.toml` - Configuration file defining the extension targets including home tile, modal, and line item actions The example demonstrates how to fetch selling plans via GraphQL, manage subscription state, and implement the UI components needed for a complete subscription flow in Point of Sale. ## 🎩 https://app.graphite.com/github/pr/Shopify/shopify-dev/64603 ## Checklist - [x] I have 🎩'd these changes - [x] I have updated relevant documentation
2 parents 59c27fd + b2dbe64 commit 064de12

File tree

6 files changed

+237
-0
lines changed

6 files changed

+237
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {render} from 'preact';
2+
import {useState, useEffect} from 'preact/hooks';
3+
import {fetchSellingPlans} from './FetchSellingPlans';
4+
5+
export default function extension() {
6+
render(<Action />, document.body);
7+
}
8+
9+
function Action() {
10+
const [response, setResponse] = useState(undefined);
11+
12+
useEffect(() => {
13+
async function getSellingPlans() {
14+
setResponse(await fetchSellingPlans(shopify.cartLineItem?.variantId));
15+
}
16+
getSellingPlans();
17+
}, [shopify.cartLineItem]);
18+
19+
const handleClick = (plan) => {
20+
shopify.cart.addLineItemSellingPlan({
21+
lineItemUuid: shopify.cartLineItem.uuid,
22+
sellingPlanId: Number(plan.id.split('/').pop()),
23+
sellingPlanName: plan.name,
24+
});
25+
window.close();
26+
};
27+
28+
return (
29+
<s-page heading="Subscriptions">
30+
<s-scroll-box>
31+
<s-box padding="small">
32+
{response?.data.productVariant.sellingPlanGroups.nodes.map(
33+
(group) => {
34+
return (
35+
<s-section key={`${group.name}-section`} heading={group.name}>
36+
{group.sellingPlans.nodes.map((plan) => {
37+
return (
38+
<s-clickable
39+
key={`${plan.name}-clickable`}
40+
onClick={() => {
41+
handleClick(plan);
42+
}}
43+
>
44+
<s-text key={`${plan.name}-text`}>{plan.name}</s-text>
45+
</s-clickable>
46+
);
47+
})}
48+
</s-section>
49+
);
50+
},
51+
)}
52+
</s-box>
53+
</s-scroll-box>
54+
</s-page>
55+
);
56+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export async function fetchSellingPlans(variantId) {
2+
const requestBody = {
3+
query: `#graphql
4+
query GetSellingPlans($variantId: ID!) {
5+
productVariant(id: $variantId) {
6+
# Note: For production use, implement pagination to fetch all sellingPlanGroups and sellingPlans as needed.
7+
sellingPlanGroups(first: 10) {
8+
nodes {
9+
name
10+
# Handle pagination (see comment above)
11+
sellingPlans(first: 10) {
12+
nodes {
13+
id
14+
name
15+
category
16+
}
17+
}
18+
}
19+
}
20+
}
21+
}
22+
`,
23+
variables: {variantId: `gid://shopify/ProductVariant/${variantId}`},
24+
};
25+
26+
const res = await fetch('shopify:admin/api/graphql.json', {
27+
method: 'POST',
28+
body: JSON.stringify(requestBody),
29+
});
30+
return res.json();
31+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {render} from 'preact';
2+
3+
export default function extension() {
4+
render(<MenuItem />, document.body);
5+
}
6+
7+
function MenuItem() {
8+
const handleButtonPress = () => {
9+
shopify.action.presentModal();
10+
};
11+
12+
const hasSellingPlanGroups = shopify.cartLineItem?.hasSellingPlanGroups;
13+
14+
return (
15+
<s-button onClick={handleButtonPress} disabled={!hasSellingPlanGroups} />
16+
);
17+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {render} from 'preact';
2+
import {useState, useEffect} from 'preact/hooks';
3+
import {fetchSellingPlans} from './FetchSellingPlans';
4+
5+
export default function extension() {
6+
render(<Modal />, document.body);
7+
}
8+
9+
function Modal() {
10+
// For this example, we'll just use the first selling plan item
11+
// Your app should handle displaying multiple line items with selling plan groups.
12+
const sellingPlanItem = shopify.cart.current.value.lineItems.find(
13+
(lineItem) => lineItem.hasSellingPlanGroups === true,
14+
);
15+
16+
const [response, setResponse] = useState(undefined);
17+
18+
useEffect(() => {
19+
async function getSellingPlans() {
20+
setResponse(await fetchSellingPlans(sellingPlanItem?.variantId));
21+
}
22+
getSellingPlans();
23+
}, [sellingPlanItem]);
24+
25+
// [START modal.handle-click]
26+
const handleClick = (plan) => {
27+
shopify.cart.addLineItemSellingPlan({
28+
lineItemUuid: sellingPlanItem.uuid,
29+
// convert from GID to ID
30+
sellingPlanId: Number(plan.id.split('/').pop()),
31+
sellingPlanName: plan.name,
32+
});
33+
window.close();
34+
};
35+
// [END modal.handle-click]
36+
37+
// [START modal.display]
38+
return (
39+
<s-page heading="POS subscription modal">
40+
<s-scroll-box>
41+
<s-box padding="small">
42+
{response?.data.productVariant.sellingPlanGroups.nodes.map(
43+
(group) => {
44+
return (
45+
<s-section key={`${group.name}-section`} heading={group.name}>
46+
{group.sellingPlans.nodes.map((plan) => {
47+
return (
48+
<s-clickable
49+
key={`${plan.name}-clickable`}
50+
onClick={() => {
51+
handleClick(plan);
52+
}}
53+
>
54+
<s-text key={`${plan.name}-text`}>{plan.name}</s-text>
55+
</s-clickable>
56+
);
57+
})}
58+
</s-section>
59+
);
60+
},
61+
)}
62+
</s-box>
63+
</s-scroll-box>
64+
</s-page>
65+
);
66+
// [END modal.display]
67+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {render} from 'preact';
2+
import {useState, useEffect} from 'preact/hooks';
3+
4+
export default function extension() {
5+
render(<Tile />, document.body);
6+
}
7+
8+
function Tile() {
9+
const [sellingPlanEligible, setSellingPlanEligible] = useState(false);
10+
11+
// [START tile.subscribe]
12+
useEffect(() => {
13+
const unsubscribe = shopify.cart.current.subscribe((cart) => {
14+
const sellingPlanEligibleLineItems = cart.lineItems.find(
15+
(lineItem) => lineItem.hasSellingPlanGroups === true,
16+
);
17+
18+
setSellingPlanEligible(sellingPlanEligibleLineItems !== undefined);
19+
});
20+
return unsubscribe;
21+
}, []);
22+
// [END tile.subscribe]
23+
24+
return (
25+
<s-tile
26+
heading={'Subscriptions'}
27+
subheading={
28+
sellingPlanEligible
29+
? 'Subscriptions available'
30+
: 'Subscriptions not available'
31+
}
32+
// [START tile.disabled]
33+
disabled={!sellingPlanEligible}
34+
// [END tile.disabled]
35+
onClick={() => shopify.action.presentModal()}
36+
/>
37+
);
38+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
api_version = "2025-10"
2+
3+
# [START toml.description]
4+
[[extensions]]
5+
type = "ui_extension"
6+
name = "Subscription Tutorial"
7+
handle = "subscription-tutorial"
8+
description = "POS UI extension subscription tutorial"
9+
# [END toml.description]
10+
11+
[[extensions.targeting]]
12+
module = "./src/Tile.jsx"
13+
target = "pos.home.tile.render"
14+
15+
[[extensions.targeting]]
16+
module = "./src/Modal.jsx"
17+
target = "pos.home.modal.render"
18+
19+
# [START toml.optional_targets]
20+
# Optional additional targets for line item details screen
21+
[[extensions.targeting]]
22+
module = "./src/Action.jsx"
23+
target = "pos.cart.line-item-details.action.render"
24+
25+
[[extensions.targeting]]
26+
module = "./src/MenuItem.jsx"
27+
target = "pos.cart.line-item-details.action.menu-item.render"
28+
# [END toml.optional_targets]

0 commit comments

Comments
 (0)