Skip to content

Commit 28e1c8d

Browse files
committed
Add code examples of POS subscriptions UI extension
1 parent 78318cc commit 28e1c8d

File tree

6 files changed

+239
-0
lines changed

6 files changed

+239
-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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export async function fetchSellingPlans(variantId) {
2+
const requestBody = {
3+
query: `#graphql
4+
query GetSellingPlans($variantId: ID!) {
5+
productVariant(id: $variantId) {
6+
sellingPlanGroups(first: 10) {
7+
nodes {
8+
name
9+
sellingPlans(first: 10) {
10+
nodes {
11+
id
12+
name
13+
category
14+
}
15+
}
16+
}
17+
}
18+
}
19+
}
20+
`,
21+
variables: {variantId: `gid://shopify/ProductVariant/${variantId}`},
22+
};
23+
24+
const res = await fetch('shopify:admin/api/graphql.json', {
25+
method: 'POST',
26+
body: JSON.stringify(requestBody),
27+
});
28+
return res.json();
29+
}
30+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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
16+
onClick={handleButtonPress}
17+
disabled={!hasSellingPlanGroups}
18+
variant="secondary"
19+
/>
20+
);
21+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
const sellingPlanItem = shopify.cart.current.value.lineItems.find(
12+
(lineItem) => lineItem.hasSellingPlanGroups === true,
13+
);
14+
15+
const [response, setResponse] = useState(undefined);
16+
17+
useEffect(() => {
18+
async function getSellingPlans() {
19+
setResponse(await fetchSellingPlans(sellingPlanItem?.variantId));
20+
}
21+
getSellingPlans();
22+
}, [sellingPlanItem]);
23+
24+
// [START modal.handle-click]
25+
const handleClick = (plan) => {
26+
shopify.cart.addLineItemSellingPlan({
27+
lineItemUuid: sellingPlanItem.uuid,
28+
// convert from GID to ID
29+
sellingPlanId: Number(plan.id.split('/').pop()),
30+
sellingPlanName: plan.name,
31+
});
32+
window.close();
33+
};
34+
// [END modal.handle-click]
35+
36+
// [START modal.display]
37+
return (
38+
<s-page heading="POS modal">
39+
<s-scroll-box>
40+
<s-box padding="small">
41+
{response?.data.productVariant.sellingPlanGroups.nodes.map(
42+
(group) => {
43+
return (
44+
<s-section key={`${group.name}-section`} heading={group.name}>
45+
{group.sellingPlans.nodes.map((plan) => {
46+
return (
47+
<s-clickable
48+
key={`${plan.name}-clickable`}
49+
onClick={() => {
50+
handleClick(plan);
51+
}}
52+
>
53+
<s-text key={`${plan.name}-text`}>{plan.name}</s-text>
54+
</s-clickable>
55+
);
56+
})}
57+
</s-section>
58+
);
59+
},
60+
)}
61+
</s-box>
62+
</s-scroll-box>
63+
</s-page>
64+
);
65+
// [END modal.display]
66+
}
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.filter(
15+
(lineItem) => lineItem.hasSellingPlanGroups === true,
16+
);
17+
18+
setSellingPlanEligible(sellingPlanEligibleLineItems.length > 0);
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)