Questa guida è pensata per gli sviluppatori. Richiede l'accesso al codice del tema. Per una pagina fedeltà che configuri senza codice, usa la Shopify Loyalty Hub.
Questa guida costruisce una pagina fedeltà completa con lo Storefront SDK: punti e avanzamento del livello, premi che il cliente può riscattare, codici sconto, modi per guadagnare e carta wallet, più una vista di accesso per i visitatori. Tutto l'HTML e il CSS sono tuoi, quindi puoi cambiare il design liberamente.
Prima di iniziare ¶
- L'app embed SDK è attivo in Shopify Admin > Online Store > Themes > Customize > App embeds.
- Il tuo programma fedeltà ha almeno un premio e un modo per guadagnare in JeriCommerce, così la pagina ha qualcosa da mostrare.
Aggiungi la pagina al tema ¶
Crea la sezione
Vai su Shopify Admin > Online Store > Themes > Edit code. In sections/ fai clic su Add a new section, chiamala jeri-loyalty e incolla il template qui sotto.
Crea un template di pagina
In templates/ aggiungi un template JSON che usa la sezione:
{
"sections": {
"loyalty": {
"type": "jeri-loyalty"
}
},
"order": ["loyalty"]
}
Puoi anche aggiungere la sezione a un template esistente dall'editor del tema con Add section.
Crea la pagina
Vai su Shopify Admin > Online Store > Pages > Add page, dai un titolo come "Rewards", scegli il template page.loyalty e salva.
Verifica come visitatore e come cliente
Apri la pagina senza aver effettuato l'accesso: vedi i premi pubblici e un pulsante di accesso. Accedi con un cliente di prova: vedi i suoi punti, l'avanzamento del livello e i codici, e puoi riscattare un premio.
Il template ¶
- Visitatore o cliente:
auth.getSession()decide quali parti mostrare. I visitatori vedono i premi pubblici e un pulsante che chiamaauth.login(). - Primo caricamento: la pagina legge i modi per guadagnare, poi in parallelo cliente, premi, codici e link wallet. La sezione wallet compare solo quando
wallet.getLinks()restituisce dei link. - Riscatto:
rewards.claim()si risolve con il codice, mostrato subito. L'SDK poi rilegge cliente, premi e codici e inviacustomer:updated,rewards:updatedediscounts:updated, che aggiornano il saldo, i pulsanti dei premi e l'elenco dei codici. - Modi per guadagnare: gli earning con un pulsante chiamano
earnings.execute()conaction: "flow"e aprono il loro link, se ne hanno uno. Il pulsante si riattiva finchéremainingExecutionsè maggiore di 0. Gli earning che aprono solo un link lo mostrano come pulsante. I link Follow us aprono la pagina social e registrano il clic conaction: "click". Gli earning di referral e di completamento del profilo non hanno quel pulsante, perché non vengono mai eseguiti così. - Errori: ogni chiamata è racchiusa in modo che il cliente veda un breve messaggio. Il template mostra un messaggio specifico per
NOT_ENOUGH_POINTS. Se l'SDK o la sessione del cliente non si caricano, la pagina mostra un messaggio invece di restare vuota.
Il template riscatta i premi Points as cash con il minimo di punti. Per lasciare scegliere ai clienti, aggiungi un campo numerico tra minPoints e maxPoints e passa il suo valore come pointsToRedeem.
sections/jeri-loyalty.liquid
<section class="jl" data-jl>
<p class="jl-notice" data-jl-notice hidden></p>
<header class="jl-hero">
<h1>{{ section.settings.title }}</h1>
<div data-jl-guest hidden>
<p>Join our rewards club to earn points on every order.</p>
<button class="jl-button" type="button" data-jl-login>Sign in or join</button>
</div>
<div data-jl-member hidden>
<p class="jl-balance"><strong data-jl-balance>0</strong> points</p>
<p data-jl-tier></p>
<div class="jl-progress"><span data-jl-progress></span></div>
</div>
</header>
<h2>Rewards</h2>
<div class="jl-grid" data-jl-rewards></div>
<p class="jl-code" data-jl-claimed hidden>Your code: <strong data-jl-claimed-code></strong></p>
<div data-jl-member hidden>
<h2>Your discount codes</h2>
<ul class="jl-list" data-jl-codes></ul>
</div>
<h2>Ways to earn</h2>
<div class="jl-grid" data-jl-earnings></div>
<div data-jl-wallet hidden>
<h2>Your loyalty card</h2>
<a class="jl-button" data-jl-wallet-ios href="#">Add to Apple Wallet</a>
<a class="jl-button" data-jl-wallet-android href="#">Add to Google Wallet</a>
</div>
</section>
<style>
.jl { max-width: 960px; margin: 0 auto; padding: 32px 16px; }
.jl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; }
.jl-card { border: 1px solid #e5e5e5; border-radius: 12px; padding: 16px; }
.jl-button { display: inline-block; padding: 10px 16px; border: 0; border-radius: 8px; background: #111; color: #fff; cursor: pointer; text-decoration: none; }
.jl-button[disabled] { opacity: 0.5; cursor: default; }
.jl-progress { height: 8px; border-radius: 4px; background: #eee; overflow: hidden; }
.jl-progress span { display: block; height: 100%; background: #111; }
.jl-notice { padding: 12px; border-radius: 8px; background: #fdecea; }
.jl-list { list-style: none; padding: 0; }
[hidden] { display: none !important; }
</style>
<script type="module">
const root = document.querySelector("[data-jl]");
const $ = selector => root.querySelector(selector);
const $ = selector => [...root.querySelectorAll(selector)];
const notice = message => {
$("[data-jl-notice]").textContent = message;
$("[data-jl-notice]").hidden = !message;
};
const card = (title, text) => {
const element = document.createElement("article");
element.className = "jl-card";
element.innerHTML = "<h3></h3><p></p>";
element.querySelector("h3").textContent = title;
element.querySelector("p").textContent = text ?? "";
return element;
};
const button = (label, onClick) => {
const element = document.createElement("button");
element.type = "button";
element.className = "jl-button";
element.textContent = label;
element.addEventListener("click", () => onClick(element));
return element;
};
const errorText = error =>
error?.code === "api_error" && error.serverCode === "NOT_ENOUGH_POINTS"
? "You don't have enough points for this reward."
: "Something went wrong. Please try again.";
const socialUrl = (network, username) => {
const name = username.trim().replace(/^@/, "");
if (/^https?:\/\//.test(name)) return name;
// Same profile URLs as JeriCommerce. Accounts saved from the admin already are full links.
const urls = {
facebook: `https://www.facebook.com/${name}`,
instagram: `https://www.instagram.com/${name}`,
tiktok: `https://www.tiktok.com/@${name}`,
x: `https://www.x.com/${name}`,
youtube: `https://www.youtube.com/@${name}`,
spotify: `https://open.spotify.com/user/${name}`,
whatsapp: `https://www.whatsapp.com/channel/${name}`,
};
return urls[network] ?? null;
};
// These earnings have their own actions, so they never run with action: "flow".
const NOT_EXECUTABLE = ["FOLLOW_US", "MEMBER_GET_MEMBER", "COMPLETE_USER_PROFILE"];
async function start() {
const jeri = await window.JeriCommerce.ready();
const { authenticated } = await jeri.auth.getSession();
$(authenticated ? "[data-jl-member]" : "[data-jl-guest]").forEach(element => (element.hidden = false));
$("[data-jl-login]").addEventListener("click", () => jeri.auth.login());
const renderCustomer = async data => {
if (!data) return;
$("[data-jl-balance]").textContent = data.customer.loyaltyData.balance;
const progress = await jeri.customer.getTierProgress();
$("[data-jl-tier]").textContent = progress.nextTier
? `${progress.currentTier?.name ?? ""} · ${progress.pointsToNextTier} points to ${progress.nextTier.name}`
: progress.currentTier?.name ?? "";
$("[data-jl-progress]").style.width = `${progress.progressValue}%`;
};
const renderRewards = rewards =>
$("[data-jl-rewards]").replaceChildren(
...rewards.map(reward => {
const dynamic = reward.type === "dynamic";
const cost = dynamic ? `From ${reward.minPoints ?? 1} points` : `${reward.cost ?? reward.redeemCost} points`;
const element = card(reward.title, cost);
if (!authenticated) return element;
element.append(
button(reward.redeemable ? "Claim" : reward.nonRedeemableReason === "NOT_ENOUGH_BALANCE" ? "Not enough points" : "Not available", async claim => {
claim.disabled = true;
try {
const input = { rewardId: reward.id };
if (dynamic) input.pointsToRedeem = reward.minPoints ?? 1;
const claimed = await jeri.rewards.claim(input);
$("[data-jl-claimed-code]").textContent = claimed.code;
$("[data-jl-claimed]").hidden = false;
notice("");
} catch (error) {
notice(errorText(error));
claim.disabled = false;
}
}),
);
element.querySelector("button").disabled = !reward.redeemable;
return element;
}),
);
const renderCodes = ({ discounts }) =>
$("[data-jl-codes]").replaceChildren(
...discounts.data.map(code => {
const item = document.createElement("li");
item.textContent = code.value;
return item;
}),
);
const renderEarnings = earnings =>
$("[data-jl-earnings]").replaceChildren(
...earnings.flatMap(definition => {
const earning = definition.programEngagementFlows?.[0];
if (!earning?.active) return [];
const element = card(earning.title, earning.description);
if (authenticated && earning.properties.executeOnClick && !NOT_EXECUTABLE.includes(definition.name)) {
element.append(
button(earning.buttonText || "Earn points", async run => {
run.disabled = true;
if (earning.url) window.open(earning.url, "_blank", "noopener");
try {
const result = await jeri.earnings.execute({ earningId: earning.id, action: "flow" });
run.textContent = `+${result.pointsAwarded} points`;
run.disabled = result.remainingExecutions === 0;
} catch (error) {
notice(errorText(error));
run.disabled = false;
}
}),
);
} else if (earning.url && !NOT_EXECUTABLE.includes(definition.name)) {
const link = document.createElement("a");
link.className = "jl-button";
link.href = earning.url;
link.target = "_blank";
link.rel = "noopener";
link.textContent = earning.buttonText || "Learn more";
element.append(link);
}
if (authenticated && definition.name === "FOLLOW_US") {
for (const [network, account] of Object.entries(earning.properties.followUs ?? {})) {
const url = account?.username ? socialUrl(network, account.username) : null;
if (!url) continue;
element.append(
button(network, () => {
window.open(url, "_blank", "noopener");
jeri.earnings.execute({ earningId: earning.id, action: "click", linkId: network }).catch(() => {});
}),
);
}
}
return [element];
}),
);
renderEarnings(await jeri.earnings.list());
if (authenticated) {
const [customer, rewards, discounts, wallet] = await Promise.all([
jeri.customer.get(),
jeri.rewards.list({ scope: "customer" }),
jeri.discounts.list({ page: 1, limit: 20 }),
jeri.wallet.getLinks(),
]);
await renderCustomer(customer);
renderRewards(rewards);
renderCodes({ discounts });
if (wallet) {
$("[data-jl-wallet]").hidden = false;
$("[data-jl-wallet-ios]").href = wallet.ios;
$("[data-jl-wallet-android]").href = wallet.android;
}
// After a claim or an earning, the SDK reads these again and fires the events.
jeri.on("customer:updated", renderCustomer);
jeri.on("rewards:updated", ({ scope, rewards }) => scope === "customer" && renderRewards(rewards));
jeri.on("discounts:updated", renderCodes);
} else {
renderRewards(await jeri.rewards.list({ scope: "public" }));
}
}
if (!window.JeriCommerce) {
notice("Loyalty is not available right now. Turn on the SDK app embed in your theme.");
} else {
start().catch(error => notice(errorText(error)));
}
</script>
{% schema %}
{
"name": "JeriCommerce loyalty",
"settings": [
{ "type": "text", "id": "title", "label": "Title", "default": "Rewards club" }
],
"presets": [{ "name": "JeriCommerce loyalty" }]
}
{% endschema %}
Idee per estenderlo ¶
Tutto questo è nel riferimento SDK:
- Cronologia punti con
history.list(). - Link referral con
referrals.getLink(). - Elenco livelli e vantaggi con
tiers.list()e ilcontentdi ogni livello.contentè HTML, quindi sanificalo prima di inserirlo. - Boost attivi con
boosts.list({ scope: "public" }). - Apri il widget in una pagina con
widget.open({ page: "rewards" }).
Crealo con un assistente AI ¶
La documentazione dell'SDK è scritta in modo che un assistente AI di programmazione possa costruire una pagina a partire da essa. Dai all'assistente queste due guide, Storefront SDK e questa pagina, oppure il testo completo del sito in /llms-full.txt. Poi descrivi cosa vuoi, ad esempio:
Build a Shopify section called jeri-loyalty for my theme using the JeriCommerce
Storefront SDK (window.JeriCommerce). Follow the JeriCommerce SDK guides I attached.
Match the look of https://www.my-store.com: fonts, colors and buttons.
Show the customer's points and tier progress, the rewards with a claim button,
their discount codes and the ways to earn. Visitors see the rewards and a sign-in button.
Only use SDK methods and fields listed in the reference, handle errors by their code,
and update the page from the SDK events instead of calling refresh().
Verifica il risultato come visitatore e come cliente prima di pubblicare la pagina.