Skip to content

Commit abbe1be

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

File tree

5 files changed

+204
-0
lines changed

5 files changed

+204
-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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {render} from 'preact';
2+
3+
function MenuItem() {
4+
const handleButtonPress = () => {
5+
shopify.action.presentModal();
6+
};
7+
8+
const hasSellingPlanGroups = shopify.cartLineItem?.hasSellingPlanGroups;
9+
10+
return (
11+
<s-button
12+
onClick={handleButtonPress}
13+
disabled={!hasSellingPlanGroups}
14+
variant="secondary"
15+
/>
16+
);
17+
}
18+
19+
export default function extension() {
20+
render(<MenuItem />, document.body);
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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {render} from 'preact';
2+
import {useState} 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(lineItem => lineItem.hasSellingPlanGroups === true)
15+
16+
setSellingPlanEligible(sellingPlanEligibleLineItems.length > 0)
17+
})
18+
return unsubscribe
19+
}, [])
20+
// [END tile.subscribe]
21+
22+
return (
23+
<s-tile
24+
heading={"Subscriptions"}
25+
subheading={sellingPlanEligible
26+
? "Subscriptions available"
27+
: "Subscriptions not available"
28+
}
29+
// [START tile.disabled]
30+
disabled={!sellingPlanEligible}
31+
// [END tile.disabled]
32+
onClick={() => shopify.action.presentModal()}
33+
/>
34+
);
35+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
api_version = "2025-10"
2+
3+
[[extensions]]
4+
type = "ui_extension"
5+
name = "Subscription Tutorial"
6+
handle = "subscription-tutorial"
7+
description = "POS UI extension subscription tutorial"
8+
9+
[[extensions.targeting]]
10+
module = "./src/Tile.jsx"
11+
target = "pos.home.tile.render"
12+
13+
[[extensions.targeting]]
14+
module = "./src/Modal.jsx"
15+
target = "pos.home.modal.render"
16+
17+
# [START toml.optional_targets]
18+
# Optional additional targets for line item details screen
19+
[[extensions.targeting]]
20+
module = "./src/Action.tsx"
21+
target = "pos.cart.line-item-details.action.render"
22+
23+
[[extensions.targeting]]
24+
module = "./src/MenuItem.tsx"
25+
target = "pos.cart.line-item-details.action.menu-item.render"
26+
# [END toml.optional_targets]

0 commit comments

Comments
 (0)