Diese Anleitung richtet sich an Entwickler. Du brauchst Zugriff auf den Code deines Themes. Für eine Loyalty-Seite ohne Code nutze den Shopify Loyalty Hub.
Diese Anleitung baut eine vollständige Loyalty-Seite mit dem Storefront SDK: Punkte und Stufenfortschritt, Belohnungen, die der Kunde einlösen kann, Rabattcodes, Sammelmöglichkeiten und die Wallet-Karte, dazu eine Anmeldeansicht für Besucher. Du besitzt das gesamte HTML und CSS und kannst das Design frei ändern.
Bevor du anfängst ¶
- Das App-Embed SDK ist unter Shopify Admin → Online Store → Themes → Customize → App embeds aktiviert.
- Dein Loyalty-Programm hat in JeriCommerce mindestens eine Belohnung und eine Sammelmöglichkeit, damit die Seite etwas anzuzeigen hat.
Die Seite zu deinem Theme hinzufügen ¶
Die Section erstellen
Gehe zu Shopify Admin → Online Store → Themes → Edit code. Klicke in sections/ auf Add a new section, nenne sie jeri-loyalty und füge das Template unten ein.
Ein Seitentemplate erstellen
Füge in templates/ ein JSON-Template hinzu, das die Section verwendet:
{
"sections": {
"loyalty": {
"type": "jeri-loyalty"
}
},
"order": ["loyalty"]
}
Du kannst die Section im Theme-Editor auch mit Add section zu einem bestehenden Template hinzufügen.
Die Seite erstellen
Gehe zu Shopify Admin → Online Store → Pages → Add page, gib ihr einen Titel wie "Rewards", wähle das Template page.loyalty und speichere.
Als Besucher und als Kunde prüfen
Öffne die Seite abgemeldet: Du siehst die öffentlichen Belohnungen und einen Anmelde-Button. Melde dich mit einem Testkunden an: Du siehst seine Punkte, seinen Stufenfortschritt und seine Codes und kannst eine Belohnung einlösen.
Das Template ¶
- Besucher oder Kunde:
auth.getSession()entscheidet, welche Teile angezeigt werden. Besucher sehen die öffentlichen Belohnungen und einen Button, derauth.login()aufruft. - Erstes Laden: Die Seite liest die Sammelmöglichkeiten, danach parallel den Kunden, die Belohnungen, die Codes und die Wallet-Links. Der Wallet-Bereich erscheint nur, wenn
wallet.getLinks()Links liefert. - Einlösen:
rewards.claim()liefert den Code, der sofort angezeigt wird. Das SDK liest dann Kunde, Belohnungen und Codes erneut und löstcustomer:updated,rewards:updatedunddiscounts:updatedaus, die den Kontostand, die Belohnungs-Buttons und die Liste der Codes aktualisieren. - Sammelmöglichkeiten: Sammelmöglichkeiten mit Button rufen
earnings.execute()mitaction: "flow"auf und öffnen ihren Link, falls vorhanden. Der Button wird wieder aktiv, solangeremainingExecutionsgrößer als 0 ist. Sammelmöglichkeiten, die nur einen Link öffnen, zeigen ihn als Button. Follow us-Links öffnen die Social-Media-Seite und zeichnen den Klick mitaction: "click"auf. Sammelmöglichkeiten für Empfehlungen und für das Vervollständigen des Profils bekommen diesen Button nicht, weil sie nie so ausgeführt werden. - Fehler: Jeder Aufruf ist abgesichert, damit der Kunde eine kurze Meldung sieht. Das Template zeigt eine eigene Meldung für
NOT_ENOUGH_POINTS. Wenn das SDK oder die Sitzung des Kunden nicht geladen werden kann, zeigt die Seite eine Meldung, statt leer zu bleiben.
Das Template löst Points as cash-Belohnungen mit den Mindestpunkten ein. Damit Kunden selbst wählen können, füge zwischen minPoints und maxPoints ein Zahlenfeld hinzu und übergib seinen Wert als 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 %}
Ideen zum Erweitern ¶
Alle diese Funktionen findest du in der SDK-Referenz:
- Punkteverlauf mit
history.list(). - Empfehlungslink mit
referrals.getLink(). - Stufenliste und Vorteile mit
tiers.list()und demcontentjeder Stufe.contentist HTML, bereinige es also, bevor du es einfügst. - Aktive Boosts mit
boosts.list({ scope: "public" }). - Das Widget öffnen auf einer Seite mit
widget.open({ page: "rewards" }).
Mit einem KI-Assistenten bauen ¶
Die SDK-Dokumentation ist so geschrieben, dass ein KI-Coding-Assistent daraus eine Seite bauen kann. Gib dem Assistenten diese zwei Anleitungen, Storefront SDK und diese Seite, oder den vollständigen Text der Website unter /llms-full.txt. Beschreibe dann, was du möchtest, zum Beispiel:
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().
Prüfe das Ergebnis als Besucher und als Kunde, bevor du die Seite veröffentlichst.