<!-- Site without a backend: load the script once, place the element. -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>
<aditus-shop
public-key="pk_your_site"
cart-scope="tab"
></aditus-shop>
有后端
您的服务器使用 mint secret 创建会话,并决定购买者是谁。将返回的令牌传给元素。提供购买者邮箱时,默认的 cart-scope "shared" 会在所有标签页中保持同一个购物车。
server.js
// 1. On your server: create a session. The mint secret never leaves the server.const res = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
"Content-Type": "application/json",
},
// email comes from your own login session, never from the browser request.
body: JSON.stringify({ publicKey: "pk_your_site", email: user.email }),
});
const { sessionToken } = await res.json();
index.html
<!-- 2. In your page: pass the token from your server to the element. -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>
<aditus-shop
public-key="pk_your_site"
session-token="SESSION_TOKEN_FROM_YOUR_SERVER"
></aditus-shop>
将 pk_your_site 替换为您在 ADITUS 管理后台中的可发布 key。developers.aditus.com 是脚本和 API 的生产地址。两种方式在下文中都有详细说明。
微前端背后的技术
微前端将完整的购买流程(活动、商品、购物车、注册、支付、完成)渲染到您页面的 DOM 中。它使用您页面的布局,您的 CSS 可以为其每个部分设置样式。
<!-- 1. Include the bundle hosted by ADITUS (once per page). -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>
<!-- 2. Explicit tab isolation for anonymous sessions created automatically in the browser (not the default).
For shared: session-token from your backend; anonymous ones also need a server-side sharedCartKey. -->
<aditus-shop
public-key="pk_live_yourkey"
api-base="https://developers.aditus.com/api"
cart-scope="tab"
culture="en-GB"
></aditus-shop>
<!-- 3. Optional: consume journey events as plain DOM events. -->
<script>
document.querySelector("aditus-shop").addEventListener("cart:update", (e) => {
console.log("Items in cart:", e.detail.itemCount);
});
</script>
微前端特意是仅限客户端的。它的内容是实时的并且受会话限制:可用性、价格和用户购物车仅在请求时经过身份验证的会话中存在,因此您的服务器没有任何有意义的内容可以预渲染。 mount() 创建一个新的客户端 React 根 (createRoot);容器内服务器渲染的标记未水化。这使得 SSR 集成变得简单且可预测:在服务器上渲染占位符,安装在客户端上,并保留空间,这样就不会发生任何跳跃。
ticketshop-section.tsx
// Next.js (App Router): the micro-frontend is deliberately client-only."use client";
import { useEffect, useRef } from"react";
import { mount, type ShopHandle } from"@aditus/shop-embed";
exportfunction TicketshopSection({ sessionToken }: { sessionToken: string }) {
const el = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!el.current) return;
const handle: ShopHandle = mount(el.current, {
publicKey: "pk_your_site",
sessionToken, // issued server-side, passed to the client component as a prop
cartScope: "shared", // the default in frameworks too; trusted server identity
});
return () => handle(); // unmount on route change
}, [sessionToken]);
// Server and client render the SAME placeholder and so avoid a hydration mismatch.// min-height reserves the space so the page does not jump on mount (CLS).return <div id="aditus-shop" ref={el} style={{ minHeight: 560 }} />;
}
SSR 应用有一个天然的优势:呈现页面的服务器运行时也可以使用key创建会话,服务器到服务器,与下面的创建流程完全相同(Next.js:路由处理程序或服务器组件;Nuxt:服务器路由)。将生成的不透明token作为 prop 或有效负载传递给客户端组件。该秘密永远不会出现在客户端代码中,并且token保留在内存中。
// Load the recommended centrally hosted aditus-shop.js first.// sessionToken comes from your trusted backend's server-side session, not a browser-created one.// Identified user: backend authenticates user and derives email/externalUserId.// Anonymous visitor: backend omits email and MUST derive sharedCartKey// from its own trusted visitor session. Neither identity nor key is browser input.const slot = document.getElementById("ticketshop-slot");
if (!slot) throw new Error("Missing mount container");
const config = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site",
sessionToken,
cartScope: "shared",
};
const handle = window.AditusShop.mount(slot, config);
// Full replacement config retains the chosen scope:
handle.update({ ...config, culture: "en-GB" });
// Do not change cartScope on this handle. Route teardown: handle();
微前端需要一个会话
无token
无会话token:微前端保持不活动状态
如果没有会话token,微前端会呈现中立的通知,并且根本不会启动任何 API 调用。无法填充购物车、无法注册、无法结账。在商店激活之前,会话是强制性的,但它不必与已知用户绑定:访客会话足以浏览和填充购物车,并且可以在稍后结帐时建立真正的用户(请参阅下面的可选用户流程)。
mount.ts
import { mount } from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
// Without a sessionToken the micro-frontend stays inactive: it only shows a// neutral notice and starts no API calls: no cart, no registration,// no checkout. Only a session activates the shop.
mount(el, {
publicKey: "pk_your_site",
// sessionToken missing -> the shop is not activated.
});
import {
mount,
type ShopHandle,
type AditusShopConfig,
} from"@aditus/shop-embed";
// getElementById can return null. Check it properly in TypeScript instead of using "!".const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
// AditusShopConfig types every option and gives you autocompletion.const config: AditusShopConfig = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site", // required as soon as a session is involved
sessionToken, // issued by your backend, keep it in memory only
cartScope: "shared", // default; requires a trusted server identity
};
const handle: ShopHandle = mount(el, config);
// The micro-frontend sends the X-Aditus-Session header with every shop call.// On logout, revoke the session on the server and remount/update.
// Server side: use the same session route as above, only the body varies.// You build user and your backend's login/session route yourself.// eventSlug sets the event. startMode also works without a slug (event// selection first, then the mode); only assortment needs the slug// (otherwise 400 start_option_requires_event_slug).// 1) Article start (default): the event's article selectionconst bodyArticles = { publicKey, email: user.email, eventSlug: "ExperienceDaysv52024" };
// 2) Assortment start: only one assortment (id or exact name, case-insensitive)const bodyAssortment = {
publicKey, email: user.email,
eventSlug: "ExperienceDaysv52024",
assortment: "Pressetickets",
};
// 3) Voucher start: "Redeem voucher / register ticket" firstconst bodyVoucher = {
publicKey, email: user.email,
eventSlug: "ExperienceDaysv52024",
startMode: "voucher" as const,
};
const res = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`, "Content-Type": "application/json" },
body: JSON.stringify(bodyVoucher),
});
const { sessionToken }: { sessionToken: string } = await res.json();
voucher-hooks.css
/* Voucher form: the same tokens and role hooks as the rest of the journey */
.aditus-shop__voucher-panel { /* panel around hints + form (general panel hook) */ }
.aditus-shop__voucher-hints { /* the two hint lines */ }
.aditus-shop__voucher-form { /* form */ }
.aditus-shop__voucher-input { /* code field (also .aditus-shop__field-input) */ }
.aditus-shop__voucher-redeem { /* "Redeem" button (data-variant="primary") */ }
.aditus-shop__field-error { /* rejection reason, role="alert" */ }
/* Event home page and event actions: structured expert hooks */
.aditus-shop__home { /* home page; root data-step="home" */ }
.aditus-shop__home-info { /* event information */ }
.aditus-shop__home-info-head { /* heading and logo */ }
.aditus-shop__home-info-name { /* event name */ }
.aditus-shop__home-info-date { /* date */ }
.aditus-shop__home-info-venue { /* venue */ }
.aditus-shop__home-info-desc { /* description */ }
.aditus-shop__home-actions[data-variant="tiles"] { /* tiles, alternatively compact */ }
.aditus-shop__home-action[data-kind="articles"] { /* buy tickets; also home / voucher / assortment */ }
.aditus-shop__home-action-icon { /* icon */ }
.aditus-shop__home-action-body { /* text group */ }
.aditus-shop__home-action-title { /* label */ }
.aditus-shop__home-action-desc { /* optional description */ }
.aditus-shop__home-action-chevron { /* arrow */ }
/* Phase hook: [data-step="voucher"] on the root */
// Voucher links point to your site: https://your-site.com/tickets?code=ED24-ADITUS// The route, request and authentication below are examples for your own website.// The parameter name is up to you: here "code".// Option A (browser): the micro-frontend reads the parameter itself.import { mount } from"@aditus/shop-embed";
mount(document.getElementById("aditus-shop")!, {
publicKey: "pk_your_site",
sessionToken, // session with eventSlug (article or voucher start)
voucherCodeParam: "code", // read ?code=… once, remove it from the address bar, redeem it
});
// Web Component: <aditus-shop public-key="pk_your_site" voucher-code-param="code"></aditus-shop>// Option B (server): your backend hands the code over when it creates the session.// (Express example; the code never goes into the session response, only into the session context.)
app.get("/tickets", async (req, res) => {
const code = typeof req.query.code === "string" ? req.query.code : undefined;
const r = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`, "Content-Type": "application/json" },
body: JSON.stringify({
publicKey: "pk_your_site",
email: req.user?.email,
eventSlug: "ExperienceDaysv52024",
startMode: "voucher", // or omit: article start, the code lands in the first cart
voucherCode: code, // undefined = normal start without a code
}),
});
const { sessionToken }: { sessionToken: string } = await r.json();
// The page additionally mounts with voucherCodeParam: "code" so that ?code= disappears// from the address bar (the session code wins, it is redeemed only once).
res.render("tickets", { sessionToken });
});
此模式要求 voucherCode、固定的 eventSlug、已识别的用户(email,非匿名)以及 Idempotency-Key 请求头。仅可在带有 mint secret 的后端调用,不能由浏览器 POST /shop/embed-session 调用。仅支持代金券/票码,不支持 PromotionCode。代码在购物车中预留,门票尚未注册或签发;仍须完成注册和结算,后续检查可能失败。
prepare-voucher.ts
// Server side of your website: one voucher attempt, no additional check call.// You implement the authentication and storage of this attempt yourself.// attemptKey is a random id your backend stores durably per logical redemption// attempt (e.g. crypto.randomUUID()). On network errors, resend the same id// and the SAME body; never generate a new one before a retry.asyncfunction prepareVoucherSession(
user: { email: string },
code: string,
attemptKey: string,
) {
const response = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
"Content-Type": "application/json",
"Idempotency-Key": attemptKey,
},
body: JSON.stringify({
publicKey: "pk_your_site",
email: user.email, // only from your authenticated server session
eventSlug: "ExperienceDaysv52024",
startMode: "voucher", // or "articles"
voucherCode: code,
redeemVoucherOnMint: true,
}),
});
const result: {
sessionToken?: string; expiresAt?: number; voucherRedeemed?: boolean;
error?: string; reason?: string; upstreamStatus?: number;
} = await response.json();
if (response.ok && result.sessionToken && result.voucherRedeemed === true) {
// Only now pass it to the browser and mount the embed.return { sessionToken: result.sessionToken, expiresAt: result.expiresAt };
}
if (response.status === 422 && result.error === "voucher_rejected") {
// Your own UI: handle the business rejection using reason, do not open the embed.// Translate only documented reason values into your own texts.return { rejected: result.reason };
}
if (response.status === 409 && result.error === "voucher_preparation_uncertain") {
// Uncertain stays uncertain; a repeated POST never turns it into a success.// If the Shop API cannot provide the cart id, manual clarification is needed.
throw new Error("Redemption uncertain; clarify with ADITUS, do not book again.");
}
if (response.status === 410 && result.error === "voucher_preparation_expired") {
throw new Error("Prepared session no longer valid; no blind new session.");
}
// 400 = invalid request/key, 409 = conflict/attempt in progress,// 502/503 voucher_upstream_error = infrastructure, not a business error.
throw new Error(`Session creation not completed (${response.status})`);
}
import {
mount,
type ShopHandle,
type AditusShopConfig,
} from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
// A guest session token is enough to mount: created server-to-server as// usual, just without the email field (no placeholder address needed). The// visitor may browse and fill the cart before a specific user is known.const config: AditusShopConfig = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site",
sessionToken: guestSessionToken,
// Called once as soon as the visitor leaves the cart towards registration.// This is where you authenticate the user (login/SSO) and create a new,// user-bound session server-to-server.// Return the fresh token -> the micro-frontend adopts it and rebuilds the// cart under the identified user (articles are kept). Return null to// cancel -> the visitor stays in the cart, without an error.
onUserRequired: async () => {
// Calls only your backend, with no identity in the payload. Your backend reads// the signed-in user from its own session (cookie/JWT) and creates the session// server-side for exactly this identity. Never pass the email from the browser,// otherwise an attacker could insert someone else's address here and reach// that person's personal data through the prefill.// host.loginAndMintSession(): Promise<string | null> is your own function.// Implement login/SSO + authenticated backend call + server-side session creation on your website.// On cancel the function returns null; on failure it throws explicitly.return host.loginAndMintSession();
},
};
const handle: ShopHandle = mount(el, config);
// Server side of your website. The secret key stays on the server and never reaches the browser.// You build user and the login/session route between the browser and your server yourself; ADITUS does not provide them.// Your backend has already authenticated the user itself (login/SSO/session).// Important: user comes from this server session, never from the browser's// request body. Otherwise an attacker could slip in someone else's email.const res = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: {
// Your client's mint secret (generated in the ADITUS admin), server-side only.
Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
publicKey: "pk_your_site", // binds the session to your key (required)
email: user.email, // who the micro-frontend acts as (omit = anonymous guest session)
externalUserId: user.id, // optional metadata (your user id)// sharedCartKey: visitor.stableRef, // required for anonymous shared carts (default):// opaque reference from your trusted server session, never browser input
ttlSeconds: 1200, // optional: lifetime (default 20 min, max 24 h)
eventSlug: "ExperienceDaysv52024", // optional: start directly in this event (otherwise the event overview)// startMode: "voucher", // optional: start with the voucher/ticket code form (without eventSlug after the event selection)// assortment: "Pressetickets", // optional (with eventSlug): only this assortment (id or name)// voucherCode: req.query.code, // optional: redeem a code from a voucher link in the embed// redeemVoucherOnMint: true, // optional: reserve it here instead (requires email, eventSlug and Idempotency-Key)
}),
});
// Choose the response type freely and pass sessionToken to the browser (inline or via fetch).const { sessionToken }: { sessionToken: string; expiresAt: number } = await res.json();
2. 使用token挂载
page.ts
import {
mount,
type ShopHandle,
type AditusShopConfig,
} from"@aditus/shop-embed";
// getElementById can return null. Check it properly in TypeScript instead of using "!".const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
// AditusShopConfig types every option and gives you autocompletion.const config: AditusShopConfig = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site", // required as soon as a session is involved
sessionToken, // issued by your backend, keep it in memory only
cartScope: "shared", // default; requires a trusted server identity
};
const handle: ShopHandle = mount(el, config);
// The micro-frontend sends the X-Aditus-Session header with every shop call.// On logout, revoke the session on the server and remount/update.
3.注销时撤销
server.ts
// Server side, e.g. when your user logs out.// You build the logout route between the browser and your server yourself; only your backend calls ADITUS.await fetch(`https://developers.aditus.com/api/shop/session/${sessionToken}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}` },
});
// { revoked: true }
import {
mount,
type ShopHandle,
type CompletedOrder,
} from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
const handle: ShopHandle = mount(el, {
publicKey: "pk_your_site",
sessionToken,
// Called exactly once as soon as the order is placed.// The micro-frontend has already resolved the order for you. You get// the resolved order, not just an id.
onComplete: (order: CompletedOrder) => {
order.id; // order GUID for further processing (server, reconciliation)
order.number; // order number for display
order.status; // order status
order.buyer; // buyer (name / email), if present
order.tickets; // [{ name, links: [{ kind: "pdf" | "apple" | "google", url }] }]// -> store order.id, show a confirmation, trigger DOI / newsletter ...
},
// Called on every unrecoverable error in the flow.
onError: (err: Error) => {
console.error("Shop error:", err.message);
},
});
微前端可以向您的分析报告购物渠道,但前提是您要求这样做。它不捆绑任何 gtag、GTM 和 GA SDK,不加载任何内容,不设置 cookie,也不自行发送任何内容。您添加一个可选回调 onEvent,并接收已在 Google 的 GA4 增强型电子商务架构中形成的类型化事件。省略 onEvent 则不会发生任何变化。您提供哪种工具以及您是否同意发送,完全取决于您。
mount.ts
import {
mount,
type ShopHandle,
type ShopAnalyticsEvent,
} from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
const handle: ShopHandle = mount(el, {
publicKey: "pk_your_site",
sessionToken,
// Optional. If you leave out onEvent, nothing changes: the micro-frontend// sends nothing, loads no gtag/GTM/GA SDK and sets no cookies.// Tool, consent and mapping stay entirely with you. This example uses GA4 via gtag.
onEvent: (event: ShopAnalyticsEvent) => {
// Consent is yours: only send once the user has agreed.
if (!hasAnalyticsConsent()) return;
// The events already follow the GA4 schema (name + items/value/currency ...),// so pass them to gtag 1:1.const { name, ...params } = event;
window.gtag?.("event", name, params);
},
});
import {
mount,
type ShopHandle,
type ShopCartUpdate,
} from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
const handle: ShopHandle = mount(el, {
publicKey: "pk_your_site",
sessionToken,
});
// Optional. Without on() the micro-frontend works completely normally.// The bus is pure observation; no event is required.// on() returns the unsubscribe function; alternatively handle.off(name, fn).const offCart = handle.on("cart:update", (cart: ShopCartUpdate) => {
// On every cart change: count, total, line items (GA4 item shape).
updateMiniCartBadge(cart.itemCount); // e.g. a badge in the page header
});
handle.on("checkout:complete", ({ order }) => {
// The order is placed. You receive the same resolved order as in onComplete.
showConfirmationBanner(order.number);
});
handle.on("analytics", (event) => {
// Mirror of the GA4 funnel (identical payloads to config.onEvent).
console.debug("Funnel:", event.name);
});
// Unsubscribe selectively later; the rest stays active:
offCart();
import type { ShopHandle } from"@aditus/shop-embed";
// Optional: the browser's back gesture goes one step back in the shop.// The micro-frontend itself never writes to the browser history. Your page// keeps its own history entry while the shop is past its entry step.exportfunction connectBrowserBack(shop: ShopHandle): () => void {
let inside = false; // after the entry step and before the completion pagelet entry = false; // our history entry is on topconst offPhase = shop.on("phase:change", ({ phase, startPhase }) => {
inside = phase !== startPhase && phase !== "done";
if (inside && !entry) {
history.pushState({ aditusShop: true }, "");
entry = true;
} else if (!inside && entry) {
entry = false;
history.back(); // entry no longer needed
}
});
const onPopState = () => {
if (!entry) return; // normal navigation of your page
entry = false;
// false: the shop's back button is missing or locked (e.g. payment in progress).
if (!shop.back() && inside) {
history.pushState({ aditusShop: true }, "");
entry = true;
}
};
window.addEventListener("popstate", onPopState);
return () => {
offPhase();
window.removeEventListener("popstate", onPopState);
};
}
// Your own mini cart: back to the cart step, otherwise scroll to the shop.exportfunction showCart(shop: ShopHandle, container: HTMLElement): void {
if (!shop.goTo("cart")) container.scrollIntoView({ behavior: "smooth" });
}