Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {render} from 'preact';
import {useState, useEffect} from 'preact/hooks';

// [START modal.fetch]
async function fetchSellingPlans(variantId) {
const requestBody = {
query: `#graphql
query GetSellingPlans($variantId: ID!) {
productVariant(id: $variantId) {
sellingPlanGroups(first: 10) {
nodes {
name
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();
}
// [END modal.fetch]

export default function extension() {
render(<Modal />, document.body);
}

function Modal() {
// For this example, we'll just use the first selling plan item
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])

// [START modal.handle-click]
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()
}
// [END modal.handle-click]

return (
<s-page heading='POS modal'>
<s-scroll-box>
<s-box padding="small">
{response?.data.productVariant.sellingPlanGroups.nodes.map(group => {
return (
<s-section key={`${group.name}-section`} heading={group.name}>
{group.sellingPlans.nodes.map(plan => {
return (
<s-clickable key={`${plan.name}-clickable`} onClick={() => {
handleClick(plan)
}}>
<s-text key={`${plan.name}-text`}>{plan.name}</s-text>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious: What does the key prop do in this context?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we're looping through the selling plans here, it's good practice to include a unique key to tell these components apart.

</s-clickable>
)
})}
</s-section>
)
})}
</s-box>
</s-scroll-box>
</s-page>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {render} from 'preact';
import {useState} from 'preact/hooks';

export default function extension() {
render(<Tile />, document.body);
}

function Tile() {
const [sellingPlanEligible, setSellingPlanEligible] = useState(false);

// [START tile.subscribe]
useEffect(() => {
const unsubscribe = shopify.cart.current.subscribe(cart => {
const sellingPlanEligibleLineItems = cart.lineItems.filter(lineItem => lineItem.hasSellingPlanGroups === true)

setSellingPlanEligible(sellingPlanEligibleLineItems.length > 0)
})
return unsubscribe
}, [])
// [END tile.subscribe]

return (
<s-tile
heading={"Subscriptions"}
subheading={sellingPlanEligible
? "Subscriptions available"
: "Subscriptions not available"
}
// [START tile.disabled]
disabled={!sellingPlanEligible}
// [END tile.disabled]
onClick={() => shopify.action.presentModal()}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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"