Pouvons-nous utiliser Google Analytics 4 pour comprendre quelles pages et sections de la documentation sont consultées ? Rien n'est chargé ni envoyé avant votre accord. Détails dans notre politique de confidentialité. Politique de confidentialité
Le micro-frontend affiche la billetterie ADITUS directement dans votre page, sans iframe. Les sites sans backend chargent le script et définissent public-key avec cart-scope="tab". Les sites avec backend créent la session sur leur serveur et transmettent le token de session ; votre serveur détermine alors qui est l'acheteur. Sans session, le micro-frontend reste inactif.
Il existe deux façons d'obtenir une boutique fonctionnelle. Les deux utilisent le même script et le même élément ; elles diffèrent par l'entité qui crée la session.
Sans backend
L'élément crée lui-même une session navigateur pour votre publicKey, limitée aux domaines autorisés. Définissez cart-scope="tab" : sans identité serveur, la valeur par défaut "shared" aboutit à une erreur explicite. Chaque onglet a alors son propre panier. La session est anonyme : les visiteurs parcourent l'offre et remplissent le panier. Avant l'inscription, element.config.onUserRequired doit renvoyer un jeton de session lié à un utilisateur, émis par un serveur ; sinon la boutique s'arrête au panier avec un message.
index.html
<!-- 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>
Avec backend
Votre serveur crée la session avec le mint secret et détermine qui est l'acheteur. Transmettez le token retourné à l'élément. Avec l'e-mail de l'acheteur, la valeur par défaut cart-scope "shared" conserve un seul panier dans tous les onglets.
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>
Remplacez pk_your_site par votre clé publique issue de l'admin ADITUS. developers.aditus.com est l'adresse de production du script et de l'API. Les deux façons sont détaillées plus bas.
La technologie derrière le micro-frontend
Le micro-frontend affiche tout le processus d'achat (événements, articles, panier, inscription, paiement, finalisation) dans le DOM de votre page. Il utilise la mise en page de votre page, et votre CSS peut styliser chacune de ses parties.
React 19, compatible avec React 18
L'ensemble du parcours s'exécute comme un flux React typé. Il est construit et testé avec React 19 et n'utilise que les fonctionnalités de React 18 (useState, useEffect, useMemo, etc.) ; le paquet source fonctionne donc avec React 18 ou plus récent (peerDependency react >=18). En dehors de React, il a une dépendance runtime : libphonenumber-js pour la validation des numéros de téléphone. Pas de bibliothèque UI, de framework de data-fetching ni de toolkit d'état. Le script hébergé par ADITUS contient déjà React : votre page n'a pas besoin de son propre React.
Light DOM au lieu d'iframe
Pas d'iframe, pas de shadow DOM. La boutique s'affiche directement dans le DOM de votre page, s'insère dans votre mise en page et est responsive et accessible. Polices et couleurs proviennent des tokens --aditus-* ; la police par défaut est system-ui. Définissez --aditus-font pour utiliser la police de votre page.
Agnostique au framework : un seul mount(), partout
Un seul appel (mount(container, config)) attache la boutique à n'importe quel élément du DOM. React, Vue, Angular ou HTML brut : l'adaptateur est indépendant du framework et fonctionne partout de la même façon.
Le style vous appartient
Chaque élément porte un hook CSS stable (classes BEM aditus-shop__* plus data-state). Vous le stylisez avec votre propre CSS ou définissez couleurs de marque, rayons et espacements avec quelques design tokens --aditus-*. Les styles de base optionnels se trouvent dans CSS @layer aditus-shop : vos règles hors layer l'emportent toujours. Cela vaut aussi pour les règles génériques de votre site comme button {}, input {} ou a {} d'un thème WordPress ou Bootstrap : elles modifient aussi la boutique. Limitez ces règles à vos propres zones de page si la boutique ne doit pas les reprendre.
Multilingue, commutable en direct
DE/EN sont intégrés. Un changement de langue est poussé directement dans l'instance en cours via handle.update() sans réinitialiser le parcours de l'utilisateur. Le panier et la progression sont préservés.
Sécurisé par conception
Sans session valide, la boutique reste inactive et n'envoie aucune requête boutique. La session provient soit de votre serveur (créée avec le mint secret, qui n'atteint jamais le navigateur), soit, pour les sites sans backend, de la session navigateur limitée au domaine de votre publicKey. Tous les appels passent par l'API ADITUS, qui ajoute les identifiants de la boutique côté serveur.
Styliser en trois niveaux
Utilisez le niveau le plus général qui convient : les tokens de design établissent la marque, les hooks BEM sémantiques généraux rendent les rôles récurrents cohérents et les hooks existants propres aux composants sont réservés aux ajustements experts.
Hooks sémantiques généraux
.aditus-shop__section, .aditus-shop__panel, .aditus-shop__list, .aditus-shop__list-item, .aditus-shop__actions et .aditus-shop__action. Ces hooks additifs décrivent des rôles tandis que chaque élément conserve son hook spécialisé. L’analyse URL et capture du Styling Studio propose des règles exactement à ce niveau (bordure vs ombre, graisse des boutons, espacement des sections), jamais sur des hooks spécifiques aux composants.
Actions et exceptions
Lorsqu'il est implémenté, data-variant="primary" ou data-variant="secondary" documente le rôle d'une action. Ne déduisez pas les rôles sémantiques de l'imbrication du DOM : reg-form n'est pas un panel documenté. Ciblez son hook spécialisé uniquement si ce formulaire doit volontairement déroger.
Les prérequis dépendent du mode de livraison. Le script hébergé par ADITUS ne nécessite qu'un navigateur récent ; le paquet source nécessite en plus React 18+ dans votre application. Les deux nécessitent une session pour un publicKey dont la liste blanche contient votre domaine.
Ce qui est requis
Paquet source uniquement : React 18+ et react-dom 18+ dans le bundle de votre application (peerDependency react >=18) ; mount() utilise createRoot de react-dom/client. Le script hébergé apporte son propre React.
Un navigateur avec un DOM (côté client). L'adaptateur monte dans un véritable élément du DOM (light DOM). Les applications SSR conviennent tant que mount() s'exécute côté client.
Un navigateur evergreen actuel est requis. L’adaptateur utilise des API web standard (fetch, AbortController, ResizeObserver, CSS @layer), baseline depuis ~2022. Pas d'IE.
Une session pour votre publicKey. Les sites sans backend définissent public-key avec cart-scope="tab" ; l'élément crée alors lui-même sa session navigateur via /shop/embed-session, limité au domaine. Les sites avec backend créent la session sur leur serveur avec le mint secret et transmettent le token. Sans session, le micro-frontend reste inactif. Votre domaine doit figurer sur la liste blanche du publicKey.
Ce qui n'est pas requis
Aucun framework particulier. mount(container, config) fonctionne avec React, Vue, Angular ou du HTML brut.
Pas d'iframe, pas de shadow DOM. Il se rend nativement dans votre page.
Aucun CSS particulier sur votre site. Les styles de base se trouvent dans @layer aditus-shop avec une spécificité d'une seule classe : toute règle hors layer de votre site l'emporte. Cela vaut aussi pour les règles d'éléments génériques comme button, input ou a : elles atteignent aussi les contrôles de la boutique ; limitez-les à votre propre contenu si elles ne doivent pas s'y appliquer. Box-sizing, padding et espacements sont en outre protégés contre un reset universel comme * { margin:0; padding:0 }. baseStyles: false désactive les styles de base.
Aucune autre bibliothèque sur votre site. Le paquet source apporte, en plus des peerDependencies React, une dépendance runtime (libphonenumber-js pour valider les numéros de téléphone) ; le script hébergé contient tout.
Aucune configuration de build pour le script hébergé ; le paquet source nécessite seulement un import ESM.
Votre site est de confiance : votre serveur authentifie l'utilisateur avant de générer une session identifiée. Le micro-frontend s'affiche volontairement dans le Light DOM de votre page, sans isolation iframe. Les scripts de votre page peuvent accéder au DOM et au stockage du navigateur ; sécurisez donc votre site et ses scripts tiers. L'autorisation d'origine par clé publique n'est pas une authentification de l'utilisateur. Les données de paiement sont saisies uniquement sur la page externe du prestataire de paiement.
Sécurité des paiements, PCI & CSP
La préparation de l'achat s'affiche dans votre page. Le paiement lui-même a lieu sur la page du prestataire de paiement : aucune donnée de paiement ne transite par votre page.
Aucune donnée de paiement dans votre page
La préparation complète de l'achat (événement, billets, extras, panier, inscription) se rend nativement dans votre page. Dès que l'utilisateur lance un paiement par redirection, le flux sensible quitte entièrement votre page : une navigation top-level normale confie la main à la page de paiement externe dédiée du prestataire, et le retour reprend le parcours et finalise la commande. Les données de carte ou bancaires ne sont saisies que là. Le micro-frontend ne rend, ne transporte ni ne stocke jamais d'identifiants de paiement dans votre page.
Périmètre PCI minimal
Comme aucune donnée de carte bancaire ou bancaire n'est jamais traitée, transportée ni stockée dans votre site, votre environnement reste dans le périmètre PCI-DSS minimal du modèle de redirection classique (typiquement SAQ A ; la classification contraignante est toujours établie par votre acquéreur ou QSA). Les moyens de paiement sans redirection, comme la facture, se finalisent côté serveur, sans aucune donnée de paiement dans le navigateur.
Content Security Policy
Si votre site définit une Content Security Policy, ajoutez : script-src pour l'origine ADITUS (script hébergé), connect-src pour l'origine de l'API ADITUS (chaque appel boutique passe par elle), img-src pour l'origine de l'API ADITUS (drapeaux, logos des moyens de paiement, badges wallet) et les hôtes de vos images d'événements et d'articles, ainsi que style-src 'unsafe-inline', car les styles de base sont injectés sous forme d'élément <style> (inutile avec baseStyles: false et votre propre feuille de style). Aucune entrée script-src ou frame-src pour les prestataires de paiement n'est nécessaire, car l'étape de paiement est une redirection de premier niveau.
Comparaison avec l'iframe et le shadow DOM
Iframe
Une iframe est un document séparé : les styles de votre page ne s'y appliquent pas, sa hauteur doit être synchronisée et elle a son propre ordre de focus. Le micro-frontend affiche au contraire la préparation de l'achat en HTML dans le DOM de votre page. Seul le paiement quitte votre page, par une redirection de premier niveau vers la page du prestataire de paiement.
Shadow DOM
De nombreux web components s'encapsulent dans un shadow DOM. Les styles de votre page ne les atteignent alors que via des custom properties et ::part. Le micro-frontend utilise au contraire la cascade CSS normale : ses styles de base se trouvent dans @layer aditus-shop, donc chaque règle hors layer de votre page l'emporte sans spécificité plus élevée ni !important.
AccessibilitéDocument séparé avec son propre ordre de focus et sa propre structure.Les références ARIA par ID (aria-labelledby, aria-describedby) ne traversent pas la frontière du shadow DOM.HTML dans le DOM de votre page : un seul ordre de focus et une seule structure avec votre propre balisage.
Style & brandingRigide : pas d'héritage CSS, personnalisation uniquement via les API postMessage.Les styles globaux n'atteignent pas l'intérieur ; le theming se fait uniquement via des propriétés et des parts transmis.Votre CSS hors layer l'emporte sur les styles de base dans @layer ; ceux-ci peuvent être surchargés ou désactivés.
Bundle & performanceCharge généralement une seconde application complète, runtime inclus, dans le frame.Embarque souvent son propre runtime et des dépendances en double.Script hébergé : un bundle incluant React. Paquet source : utilise le React de votre application plus une dépendance runtime (libphonenumber-js).
Sécurité des paiements & périmètre PCIFortement isolé, mais au prix de l'UX, du design et de la réactivité.Dépend du widget : les champs de carte dans le widget font partie de votre page.Sélection dans votre page, paiement sur la page du prestataire de paiement.
Notre recommandation : JavaScript directement depuis l’URL ADITUS
Chargez le bundle JavaScript directement depuis ADITUS. C’est notre méthode recommandée pour tous les sites, y compris React et les autres frameworks, et pas seulement les sites HTML ou CMS. ADITUS améliore la logique interne de la boutique de manière centralisée. Les améliorations compatibles atteignent votre intégration sans téléchargement d’un nouveau paquet, compilation ou déploiement de votre site.
L’URL v1 fournit des versions compatibles avec des interfaces stables. Une nouvelle version est chargée lors d’une visite ultérieure, après expiration ou revalidation du cache HTTP, actuellement d’une heure. Cela ne se produit pas nécessairement à chaque rechargement. Un paiement déjà ouvert conserve son code.
Alternative : gérer vous-même la diffusion et les mises à jour
Choisissez le paquet source, l’installation du tarball via npm ou une copie JavaScript auto-hébergée uniquement si vous souhaitez gérer vous-même la diffusion. ADITUS ne peut pas mettre à jour votre copie. Vous devez surveiller les versions, les télécharger ou les installer, les tester, recompiler si nécessaire et les déployer. Le paquet n’est pas publié sur le registre npm public.
Alternative : paquet source (distribué par vous)
Vous pouvez aussi télécharger le paquet source depuis votre instance ADITUS et l'intégrer à votre application (Vite, webpack, Next.js ou Nuxt). Vous gérez alors sa distribution : chaque mise à jour exige réinstallation, build et déploiement. Il n'est pas sur le npm public.
Ce que contient la livraison
L'adaptateur complet sous forme de source TypeScript lisible, avec des déclarations de types pour chaque option de config, callback et événement. React et react-dom restent des peerDependencies. Votre page les fournit, donc rien n'entre en collision dans votre bundle et il n'y a pas de second React.
Obtenir le paquet
Le paquet est identique pour chaque site et se télécharge directement depuis cette instance, sans identifiants requis : aditus-shop-embed.tgz. Installez-le depuis cette URL (npm install accepte les URL de tarball, chemin /api/embed/v1/aditus-shop-embed.tgz) ou décompressez-le dans votre dépôt. L'onboarding fournit votre publicKey et la liste des domaines autorisés ; sans publicKey, le micro-frontend reste inactif. Contact : page de contact. Utilisez ce paquet uniquement si vous distribuez et mettez à jour votre propre build.
Restez à jour : la piste v1 reçoit des mises à jour compatibles sur place. Vérifiez régulièrement manifest.json (avant chaque release de votre application, idéalement via une tâche planifiée) et comparez package.sourceHash avec aditus.sourceHash du package.json installé. S'ils diffèrent, réinstallez le tarball et relancez build et tests ; hooks, tokens et événements restent stables au sein de v1. Le bundle Web Component se met à jour de lui-même via le cache HTTP d'une heure.
Intégration par script via l’URL ADITUS
Le bundle précompilé à https://developers.aditus.com/api/embed/v1/aditus-shop.js fonctionne sur tout site, y compris les applications avec build, WordPress, Typo3 et HTML brut. Une balise script enregistre <aditus-shop>, React inclus : ni npm ni build de votre côté. Cette URL publique et versionnée ne demande aucun identifiant ; la boutique exige toujours une publicKey autorisée et une session. Un paiement en cours n'est jamais remplacé à chaud ; le cache du navigateur peut retarder le nouveau bundle d'une heure au maximum.
index.html
<!-- 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>
La même URL JS hébergée enregistre automatiquement <aditus-shop> ET expose window.AditusShop : mount, themeToCss, clearJourney, mintEmbedSession et onEmbedSessionRenewed, avec les signatures du paquet source. Votre site n'a pas besoin de React. Le mount hébergé, element.config et le paquet source acceptent la même AditusShopConfig, y compris thèmes, eventTheme: false, articleContent et callbacks. Seuls les attributs HTML seuls ne transmettent pas de fonctions ou d'objets. Le paquet source reste adapté aux bundlers et au partage de votre runtime React.
mount(container, config) est le même mount source : sans sessionToken, il reste inactif et ne génère pas de session. Définissez apiBase explicitement avec l'URL complète /api/shop. Après votre connexion asynchrone, le backend génère la session serveur à serveur et transmet uniquement le token. host.renewSession et host.revokeSession sont des fonctions/routes à implémenter vous-même dans votre backend authentifié, pas des endpoints navigateur ADITUS prêts à utiliser. Aucun secret dans le navigateur. onUserRequired reste un callback asynchrone de connexion et de création de session côté serveur, renvoyant un token ou null, pour chaque variante JavaScript.
index.html / your own login
<div id="ticketshop-slot"></div>
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js"></script>
<script>
// Called after your async login and server-side session creation, with the returned sessionToken.
// host.renewSession(staleToken): Promise<string | null>
// host.revokeSession(token): Promise<void>
// These are HOST-SPECIFIC backend functions, not built-in ADITUS routes.
// Your backend authenticates the user; mint secrets never enter the browser.
window.mountKnownSessionShop = function (sessionToken, host) {
if (!sessionToken) throw new Error("A sessionToken from your backend is required");
const slot = document.getElementById("ticketshop-slot");
if (!slot) throw new Error("Missing mount container");
let disposed = false;
let config = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site",
sessionToken,
cartScope: "shared", // default; trusted S2S identity required; immutable after mount
culture: "en-GB",
refreshSession: async (staleToken) => {
const token = await host.renewSession(staleToken);
if (disposed) return null;
if (token) config = { ...config, sessionToken: token };
return token; // Adapter swaps token and retries the failed request once.
},
onError: (error) => console.error("Ticketshop:", error.message),
};
const handle = window.AditusShop.mount(slot, config);
return {
updateCulture(culture) {
if (disposed) return;
config = { ...config, culture };
handle.update(config); // full replacement, not a patch
},
async logout() {
disposed = true;
handle();
window.AditusShop.clearJourney(); // local resume pointer only
await host.revokeSession(config.sessionToken); // HOST-SPECIFIC S2S revoke
},
teardown() {
disposed = true;
handle(); // SPA route cleanup, not server-side revocation
},
};
};
// Your site supplies the real token and implementations:
// const shop = window.mountKnownSessionShop(sessionToken, host);
// Logout: await shop.logout(); route change: shop.teardown();
</script>
JS simple : session navigateur et renouvellement
Pour une publicKey autorisée, mintEmbedSession(publicKey, apiRoot = "/api", { fresh?, staleToken? }) renvoie Promise<string | null> sans secret. Les helpers et l'attribut api-base utilisent la racine /api ; config.apiBase utilise la base shop /api/shop. onEmbedSessionRenewed(publicKey, apiRoot, listener) transmet un token et renvoie une fonction de désabonnement. L'exemple gère échec initial, expiration, renouvellement proactif et nettoyage. Le désabonnement ne supprime que cet écouteur, pas les caches partagés, timers ou sessions serveur ; les helpers conservent la continuité via sessionStorage.
index.html / browser session
<div id="ticketshop-slot"></div>
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js"></script>
<script>
window.mountBrowserSessionShop = async function () {
const { mount, mintEmbedSession, onEmbedSessionRenewed } = window.AditusShop;
const publicKey = "pk_your_site"; // your whitelisted key, not a secret
const apiRoot = "https://developers.aditus.com/api"; // no /shop for helpers
const slot = document.getElementById("ticketshop-slot");
if (!slot) throw new Error("Missing mount container");
const sessionToken = await mintEmbedSession(publicKey, apiRoot);
if (!sessionToken) throw new Error("Browser session failed: check key/domain");
let disposed = false;
let config = {
apiBase: apiRoot + "/shop", // mount uses the shop base
publicKey,
sessionToken,
cartScope: "tab", // explicit isolation opt-in: browser-created sessions cannot safely share
culture: "en-GB",
refreshSession: async (staleToken) => {
const token = await mintEmbedSession(publicKey, apiRoot, {
fresh: true, staleToken,
});
if (disposed) return null;
if (token) config = { ...config, sessionToken: token };
return token;
},
onError: (error) => console.error("Ticketshop:", error.message),
};
const handle = mount(slot, config);
const offRenewed = onEmbedSessionRenewed(publicKey, apiRoot, (token) => {
if (disposed) return;
config = { ...config, sessionToken: token };
handle.update(config); // preserve callbacks and all other options
});
return function teardown() {
disposed = true;
offRenewed();
handle();
};
};
// const teardown = await window.mountBrowserSessionShop();
// Start now; your SPA calls window.teardownTicketshop() on route change.
window.mountBrowserSessionShop().then((teardown) => {
window.teardownTicketshop = teardown;
}).catch((error) => {
document.getElementById("ticketshop-slot").textContent = error.message;
});
</script>
Attributs et config JavaScript complète
element.config accepte toute AditusShopConfig, fonctions incluses. Les clés définies priment sur les attributs, même false ; les clés absentes/undefined laissent les attributs actifs. Une affectation remplace tous les overrides, pas un patch : element.config = {} les retire. L'affectation avant l'upgrade est prise en charge. api-base est la racine /api, config.apiBase finit par /api/shop. themeToCss(theme, selector = ".aditus-shop") renvoie du CSS de tokens à la place d'un objet thème.
index.html / element.config
<aditus-shop id="shop" public-key="pk_your_site"
api-base="https://developers.aditus.com/api" cart-scope="tab" culture="de-DE"></aditus-shop>
<script>
const element = document.getElementById("shop");
// Supported before upgrade by the following hosted script, too.
element.config = {
apiBase: "https://developers.aditus.com/api/shop",
culture: "en-GB", // overrides the HTML culture attribute
cartScope: "tab", // explicit isolation opt-in for this anonymous browser-created example
theme: { accent: "#c2410c", fontSize: "16px" },
eventTheme: false,
onComplete: (order) => console.log("Order completed:", order),
onError: (error) => console.error("Ticketshop:", error.message),
};
// No sessionToken: effective publicKey enables browser session creation.
// Optional refresh by your site: refreshSession: (staleToken) => host.renewSession(staleToken)
// host.renewSession is HOST-SPECIFIC; your refresh always takes precedence.
// Later: element.config = {}; removes overrides, attributes apply again.
// Cleanup: element.remove();
</script>
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>
AttributSignification
cart-scopeAditusShopConfig.cartScope?: 'tab' | 'shared' vaut 'shared' par défaut, même si omis. 'tab' est un choix explicite pour isoler les parcours. window.AditusShop.mount() hébergé centralement, element.config et mount() du paquet source/framework acceptent cette même option ; en HTML, utilisez cart-scope="tab" ou cart-scope="shared". Choisissez avant le montage. La portée est immuable pour l'instance de checkout montée : handle.update() refuse une modification au lieu de migrer un panier actif. Conservez-la dans les configurations de remplacement complètes.
public-keyVotre clé publique (la liste blanche de domaines s'applique). Sans session-token, l'élément crée lui-même sa session navigateur. Les sites sans backend utilisent ce mode et définissent en plus cart-scope="tab" ; avec la valeur par défaut "shared", la session navigateur aboutit à une erreur explicite, faute d'identité serveur de confiance.
session-tokenUn token de session créé par votre backend avec le mint secret (voir ci-dessous). S'il est défini, l'élément ne crée jamais de session lui-même ; votre page gère le cycle de vie de la session.
api-baseURL de base de l'API ADITUS. Par défaut, l'origine depuis laquelle le script du bundle a été chargé. Normalement vous ne le définissez jamais.
cultureCulture/locale de la boutique transmise pour la sélection des ressources ADITUS ; les valeurs valides comme it-IT sont transmises. Les textes de ressources voucher et upsell restent tels que fournis par ADITUS ; le reste de l'interface utilise des ressources allemandes/anglaises avec fallback. Cela ne garantit pas une interface entièrement en italien. Une modification relocalise sur place sans réinitialiser le parcours.
article-layout / article-group-layout / event-layout / article-select-mode / show-headerLes options de présentation à plat, mêmes valeurs que la config de mount() (cards, grid, quantity, …). Le point d'entrée (sélecteur d'événement vs. un événement épinglé) n'est pas un attribut : il est fixé côté serveur lors de la génération de la session.
include-shop-assortmentsMélange optionnel des assortiments ; includeShopAssortments dans mount(). Par défaut false : seul l'assortiment par défaut est chargé. Avec true, ajoute les assortiments showInShop=true. Un assortiment de départ explicite reste prioritaire. Sans attribut, le défaut client s'applique ; false explicite le remplace.
survey-columnsNombre de colonnes (1–4) des options de réponse des questions RadioButtonList/CheckBoxList. Cela correspond à surveyColumns dans la config mount(). Sans attribut, l'affichage reste sur une colonne (ou suit le défaut client côté serveur) ; les écrans étroits reviennent à une colonne.
hide-house-numberMasque le champ du numéro de rue dans le bloc d'adresse des questionnaires, pour chaque variante de pays ; cela correspond à hideHouseNumber dans la config mount(). La rue et le numéro sont alors saisis ensemble dans un champ « Rue et numéro » qui reprend la largeur libérée ; les suggestions d'adresse y inscrivent la rue et le numéro dans l'ordre usuel du pays (p. ex. "12 Rue Cler" en France), un numéro prérempli est vidé et la réponse séparée du numéro est toujours envoyée sous forme d'espace invisible de largeur nulle (U+200B), sans erreur de validation, même si le questionnaire la rend obligatoire. Le formulaire d'adresse de facturation suit la même règle. Désactivé par défaut ; sans attribut, le défaut client s'applique, "false" explicite le remplace.
show-jump-buttonBouton de saut à l'étape de personnalisation ; correspond à showJumpButton dans la config mount(). Activé par défaut : après un envoi échoué, une petite pastille reste en bas du widget, indique le nombre d'informations manquantes et mène à la suivante ; une fois tout rempli, elle ramène au bouton sans jamais l'envoyer. Elle est masquée tant que ce bouton ou la cible est visible et pendant la saisie. "false" la désactive ; sans attribut, le défaut client s'applique.
voucher-code-paramNom d'un paramètre de requête de l'URL de votre page contenant un code de bon/billet (p. ex. "code" pour des liens comme …/tickets?code=ED24). Cela correspond à voucherCodeParam dans la config mount(). L'élément le lit une fois au montage, le retire de la barre d'adresse et échange le code automatiquement (formulaire de bon ou premier panier). Uniquement pour les sessions fixées sur un événement.
base-stylesRéglé sur "false", le skin neutre par défaut disparaît et vous stylez chaque hook vous-même.
autoScroll / articleIconVisible / classPrefixPas d'attribut : définissez ces options via element.config (mêmes noms que dans la config mount()). autoScroll (true par défaut) ramène le début de la boutique dans la vue après un changement d'étape ; mettez false si votre page gère le défilement. articleIconVisible (false par défaut) affiche une image ou un monogramme par article dans la présentation en liste. classPrefix ("aditus-shop" par défaut) change le préfixe de tous les hooks CSS.
Session & auto-réparation
Un sessionToken effectif (config ou attributs) désactive la création de session dans le navigateur. Avec une publicKey effective sans token, l'élément génère toujours sa session. Le refreshSession fourni par votre site prime toujours sur le refresh intégré. Sinon les sessions générées utilisent le helper partagé pour refresh et renouvellement proactif. Le Light DOM reste inchangé : sans iframe ni shadow DOM.
Événements & versionnement
Les DOM CustomEvents remontent depuis l'élément, payload dans event.detail : cart:update, phase:change, checkout:complete et analytics, identiques à handle.on(). Les callbacks de config sont aussi disponibles.
Réinitialiser le parcours délibérément
element.clearJourney() et window.AditusShop.clearJourney() retirent seulement le pointeur local de reprise de cette page. Appelez avant de réinsérer ou remonter pour repartir à zéro. Aucun panier serveur ni session n'est supprimé ; la révocation à la déconnexion appartient au backend.
Mises à jour sans remontage
Les configs mount/update sont des remplacements complets, pas des patches : gardez la config et passez { ...config, culture } à handle.update(). element.config remplace aussi tous les overrides ; les attributs non masqués mettent à jour sur place. La présentation préserve le parcours ; l'identité rétablit la session. handle() démonte ; retirer l'élément nettoie ses écouteurs embed et renouvellement. Retirez vous-même vos écouteurs DOM sur un élément conservé.
Applications SSR : Next.js & Nuxt
Le micro-frontend est délibérément client-only. Son contenu est en direct et lié à la session : disponibilités, prix et panier de l'utilisateur n'existent que pour une session authentifiée au moment de la requête, il n'y a donc rien de pertinent que votre serveur puisse pré-rendre. mount() crée une nouvelle racine React côté client (createRoot) ; le balisage rendu côté serveur à l'intérieur du conteneur n'est pas hydraté. Cela rend l'intégration SSR simple et prévisible : rendez un placeholder sur le serveur, montez sur le client et réservez l'espace pour que rien ne saute.
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 }} />;
}
Éviter le décalage de mise en page (CLS)
Donnez au conteneur de mount une min-height correspondant approximativement à la première vue de la boutique, et rendez-y éventuellement votre propre skeleton. Serveur et client produisent le même placeholder, il n'y a donc pas de hydration mismatch. L'adaptateur se rend immédiatement après le mount et remplace le placeholder en un seul paint. N'essayez pas hydrateRoot sur le conteneur : il n'y a aucun arbre de boutique rendu côté serveur auquel se rattacher.
La session dans une application SSR
Les applications SSR ont un avantage naturel : le runtime serveur qui rend la page peut aussi générer la session serveur à serveur avec le secret, exactement comme le flux de création de session ci-dessous (Next.js : Route Handler ou Server Component ; Nuxt : route serveur). Transmettez le token opaque obtenu au composant client sous forme de prop ou de payload. Le secret n'apparaît jamais dans le code client, et le token reste en mémoire.
Spécificités Next.js
Marquez le composant de mount avec "use client" et montez dans useEffect (voir snippet). Avec le Pages Router, next/dynamic avec ssr: false obtient le même résultat. Renvoyez le handle depuis l'effet comme cleanup afin que les changements de route côté client démontent proprement. Un changement de culture sur l'instance en cours passe par handle.update() sans réinitialiser le parcours ; un nouveau token de session remonte (comme dans le snippet). Le panier y survit aussi, car le micro-frontend restaure le parcours au mount.
Spécificités Nuxt / Vue
Enveloppez la cible de mount dans <ClientOnly> ou montez dans onMounted. L'adaptateur a besoin d'un véritable élément du DOM dans le navigateur. N'oubliez pas le prérequis du bundle : react et react-dom 18+ doivent être installés comme dépendances de votre application Nuxt ; la boutique se rend dans son propre conteneur et n'interfère pas avec le DOM virtuel de Vue.
Portée du panier, confiance et stockage
cartScope détermine si les onglets d'un visiteur partagent un même panier. La valeur par défaut est 'shared' ; 'tab' sépare volontairement les onglets. Choisissez-le avant le montage ; il ne peut plus changer ensuite.
Aspect'shared' (par défaut)'tab'
PanierUn panier pour tous les onglets ayant la même origine, la même publicKey, la même identité et le même événementChaque onglet a son propre panier
SessionCréée par votre backend : utilisateur identifié, ou anonyme avec un sharedCartKey issu de votre propre session serveur. Ne reprenez jamais l'identité ni la clé depuis le navigateur.Toute session, y compris une session créée dans le navigateur
Plusieurs ongletsUn onglet modifie ; les autres sont en lecture seule et proposent « Continuer ici ». Un onglet ouvert ou rechargé reprend automatiquement la main, jamais pendant le paiement.Indépendants
Si la condition manqueErreur explicite, même si cartScope est omis ; aucun basculement silencieux vers 'tab'. Les pages sans backend définissent donc 'tab'.–
Configuration: cartScope dans mount() et element.config, cart-scope en HTML. handle.update() refuse toute modification ; conservez donc la valeur dans chaque config de remplacement. Attributs · Champs de session
Stockage: Les brouillons de formulaire sont stockés par onglet dans sessionStorage et expirent 2 heures après la dernière modification. Les tokens ne vont jamais dans localStorage. Les commandes et inscriptions confirmées ne sont pas concernées.
Déconnexion: Lors d'une déconnexion ou d'un changement d'utilisateur, votre backend révoque l'ancienne session ; mettez ensuite le token à jour ou démontez l'embed et appelez clearJourney(). Un nouvel utilisateur n'hérite jamais du panier du précédent.
Sécurité: L'embed s'affiche directement dans votre page, sans iframe. Les scripts de votre page, y compris tiers, peuvent y accéder ; n'intégrez donc que des scripts de confiance. Les données de paiement sont saisies uniquement chez le prestataire de paiement.
shared-cart.js
// 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();
Le micro-frontend a besoin d'une session
Sans token
Sans token de session : le micro-frontend reste inactif
Sans token de session, le micro-frontend affiche un avis neutre et ne lance aucun appel d'API. Aucun panier ne peut être rempli, aucune inscription, aucun paiement. Une session est obligatoire avant que la boutique s'active, mais elle n'a pas à être liée à un utilisateur connu : une session invité suffit pour naviguer et remplir le panier, et l'utilisateur réel peut être défini plus tard au moment du paiement (voir le flux utilisateur optionnel ci-dessous).
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.
});
Utilisateur de votre site
Token de session pour un utilisateur précis
Votre backend génère un token de courte durée lié à un seul utilisateur. Le micro-frontend agit alors comme cet utilisateur : son panier, ses données. Le token est opaque, expirant, mono-utilisateur et lié à votre publicKey, et votre secret n'atteint jamais le navigateur.
mount.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.
Au rechargement, un pointeur de parcours valide permet de demander le panier serveur sous la même portée autorisée et identité de confiance. Ce n'est pas une autorisation ni une garantie de reprise après expiration, effacement du stockage ou changement d'utilisateur. Le choix explicite tab ne fusionne pas les parcours des onglets ; shared par défaut maintient le verrou de checkout avec un seul écrivain.
Le retour de paiement utilise aditus-return (un token opaque, court et lié au serveur) avec aditus-pay. Il peut revenir dans le même onglet ou un nouveau, mais le serveur exige un client/une origine autorisés et l'identité de confiance correspondante. Les paramètres de retour ne prouvent pas un achat. Le serveur vérifie le paiement et finalise la tentative liée au token ; un résultat inconnu reste bloqué pour clarification, sans relance aveugle d'un nouvel achat. Les retours expirés ou non autorisés sont des erreurs explicites, pas une permission de créer une commande de remplacement.
L'attribution d'un prestataire prépare seulement le paiement : le récapitulatif reste modifiable. Le verrou commence lors de la redirection effective, après les consentements requis. Un paramètre de retour cancel/error ne prouve pas un échec : le backend revérifie le panier ; Completed avec order.id confirme la finalisation. Une nouvelle tentative exige la suppression réussie et confirmée de l'attribution pour ce même panier ; les résultats ambigus restent bloqués et peuvent être revérifiés. Cela ne garantit ni remboursement ni absence de règlement tardif.
Paiement par redirection : ce que votre page doit faire pour la session
Servez l'URL de retour configurée avec l'embed monté. Préservez aditus-return et aditus-pay lors du routage ou des redirections de connexion nécessaires ; aditus-pay=success n'est jamais une autorisation. Fournissez une session valide pour la même identité de confiance et publicKey. Conservez cartScope et ne supprimez pas le parcours ni ne lancez un second checkout pour contourner un retour en attente, expiré ou incertain.
Vous préférez un redémarrage délibéré avec un véritable bouton « recommencer » ? Le paquet exporte clearJourney() : appelez-le avant de monter (ou remonter), et l'enregistrement de parcours persisté localement pour la page courante est supprimé, de sorte que le prochain mount démarre à neuf au point d'entrée. Seul le pointeur de reprise local est retiré ; tout état de panier côté serveur reste intact.
Point d'entrée : accueil, articles ou bon
Le point de départ est décidé par votre serveur lors de la création de la session, jamais par le navigateur. Sans eventSlug s'affiche l'aperçu des événements. Avec eventSlug, startMode choisit les articles (défaut), l'accueil ou le formulaire de bon ; assortment limite les articles.
La sélection d'articles et la suite après un bon suivent le parcours du magasin d'origine fondé sur les recommandations. Sans offre upsell utilisable ni vente complémentaire, on passe directement à la personnalisation des billets via les contrôles habituels de connexion/session ; avec une vente complémentaire, on affiche le panier. Une offre upsell utilisable ouvre d'abord son dialogue. Ignorer mène à la personnalisation sans vente complémentaire, ou au panier avec elle. Une mise à niveau confirmée mène au panier ; fermer le dialogue conserve la sélection. Le parcours n'enregistre jamais l'inscription en arrière-plan.
Les assortiments visibles peuvent avoir leur propre accès direct. Si un assortiment est fixé, l'accueil ne montre que son action billets, ni la liste complète ni l'action bon.
startMode: "home" ouvre l'accueil de l'événement. On y choisit Acheter des billets ou Échanger un bon ; l'ouverture seule ne crée aucun panier et n'échange aucun code. Acheter des billets ouvre la liste d'articles (limitée par assortment, si défini) ; Échanger un bon ouvre le formulaire de code. Sans eventSlug, la liste des événements apparaît d'abord, puis l'accueil de l'événement choisi. Un panier de bon préparé ou un parcours à reprendre reste prioritaire.
Par défaut
Démarrage sur les articles
eventSlug seul : le micro-frontend ouvre la sélection d'articles de cet événement avec tous les assortiments visibles en boutique.
assortment
Un seul assortiment
assortment limite le démarrage sur les articles à un assortiment de l'événement : son identifiant ou son nom exact (insensible à la casse). Les assortiments masqués sont autorisés, ce qui permet p. ex. de proposer des billets presse gratuits sur une page dédiée. Un sélecteur sans correspondance affiche une erreur explicite au lieu de tout lister en silence.
startMode: "voucher"
Échanger un bon / enregistrer un billet
Le parcours s'ouvre sur un formulaire de code au lieu de la liste d'articles. Un code valide réserve son billet. Après une redemption réussie, la configuration ADITUS de l'événement configuration.redirectAfterRegistration détermine si le visiteur reste sur l'écran de succès ou est automatiquement redirigé. La suite suit le même parcours fondé sur les recommandations que la sélection d'articles, avec les contrôles habituels de connexion/session avant la personnalisation. Retour conserve la réservation. « Ajouter d'autres vouchers » rouvre la saisie du code sur l'écran voucher pour le même panier ; « Sélectionner des billets » ouvre la sélection d'articles. Un code refusé reste dans le formulaire avec son motif. Un billet entièrement couvert ignore le paiement.
Un code promotionnel ouvre la sélection aux prix du panier. L'avis reste dans le pied à côté du champ toujours utilisable : plusieurs promotions s'appliquent au même panier, un bon peut être échangé avec les quantités choisies et chaque promotion peut être retirée séparément.
Où le définir
Génération serveur ou fiche client
Les générations serveur-à-serveur passent startMode / assortment dans le corps à côté d'eventSlug. Les sessions générées par le navigateur (clé publique seule) prennent les trois valeurs dans la fiche client configurée par votre contact ADITUS ; une valeur dans le corps y est ignorée. Seul assortment exige eventSlug (400 start_option_requires_event_slug) ; startMode fonctionne aussi avec le choix de l'événement ; des valeurs inconnues donnent 400 invalid_start_mode / invalid_assortment. GET /shop/session-context renvoie startMode et assortment de la session.
mint-entry.ts
// 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 */
Liens de bon : échanger un code depuis l'URL
Les bons sont souvent envoyés sous forme de liens vers votre page avec le code en paramètre de requête, p. ex. …/tickets?code=ED24-ADITUS. Le micro-frontend peut reprendre ce code et l'échanger sans que le visiteur ne tape quoi que ce soit. Deux voies de transmission existent ; toutes deux aboutissent au même appel d'échange et se comportent ensuite de façon identique.
voucherCodeParam: "code"
A) Dans le navigateur : le micro-frontend lit l'URL
Nommez le paramètre dans la config mount() (voucherCodeParam) ou sur l'élément (voucher-code-param). Au montage, l'adaptateur le lit une fois, le retire de la barre d'adresse via history.replaceState (pas de rechargement, le reste de l'URL est conservé) et échange le code. Un rechargement, un favori ou une URL transférée n'échange donc jamais deux fois. Adapté aux pages statiques et à la session générée dans le navigateur.
voucherCode dans le corps de génération
B) Côté serveur : transmettre le code avec la session
Votre backend lit le paramètre dans la requête entrante et le place dans le corps de génération sous voucherCode (eventSlug facultatif). Le code fait partie de la session et n'atteint le micro-frontend que via le GET /shop/session-context authentifié, qui ne le fournit qu'une seule fois puis l'efface ; la réponse de génération renvoie le code accepté pour vérification. Définissez aussi voucherCodeParam pour que le paramètre disparaisse également de la barre d'adresse. Adapté lorsque votre page est de toute façon rendue côté serveur ou que vous voulez valider, journaliser ou limiter vous-même les liens entrants. Si les deux voies livrent un code, celui du serveur l'emporte.
Ce qui arrive au code
Même comportement sur les deux voies
Le code est échangé une seule fois et uniquement dans un nouveau parcours (un parcours repris après rechargement ou retour de paiement l'ignore). Avec startMode "voucher", le formulaire de code se comporte comme décrit sous « Échanger un bon / enregistrer un billet ». Avec le démarrage sur les articles, le code est appliqué au premier panier tandis que la vue, le défilement et les quantités restent en place. Continuer ajoute ces quantités à ce même panier. Le micro-frontend ne transmet jamais le code à onEvent. Sans eventSlug, il est échangé après le choix de l'événement.
voucher-link.ts
// 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 });
});
Option : réserver le bon lors de la génération côté serveur
Si votre site veut traiter le refus avant d'ouvrir le micro-frontend, mettez redeemVoucherOnMint: true dans le POST /api/shop/session de confiance. Contrairement à la transmission normale du code, le serveur crée un panier pour l'utilisateur authentifié et y réserve réellement le bon avant de délivrer la session : ce n'est pas une requête de vérification supplémentaire. En cas de succès, ne transmettez que sessionToken au navigateur. Le micro-frontend reprend le même panier sans le recréer ni échanger le code deux fois, en mode voucher ou articles. Sans l'option, la transmission normale du voucherCode s'applique.
Ce mode exige voucherCode, un eventSlug fixé, un utilisateur identifié (email ; pas anonyme) et l'en-tête Idempotency-Key. Il est réservé au backend utilisant le secret de génération, pas à POST /shop/embed-session dans le navigateur. Seuls les codes de bon/billet sont pris en charge, pas PromotionCode. Le code est réservé dans un panier, mais le billet n'est pas encore inscrit ni délivré. L'inscription et la finalisation restent nécessaires ; des vérifications ultérieures peuvent encore échouer.
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})`);
}
Le refus métier est 422 voucher_rejected avec un reason assaini : affichez-le sur votre site, sans monter le micro-frontend sans token. Une configuration/clé incorrecte vaut 400 ; 409 idempotency_conflict signifie une clé réutilisée avec d'autres données. Une opération en cours/incertaine, 502/503 ou un délai réseau n'indique pas un bon invalide. Conservez une clé aléatoire Idempotency-Key par tentative logique côté serveur et réessayez uniquement avec la même clé et exactement les mêmes données ; une réservation peut déjà exister. Ne journalisez ni code, ni email, ni erreurs brutes, ni token.
Une génération réussie peut laisser un panier réservé même si la page n'est jamais ouverte. Le nettoyage des paniers connus après refus/révocation est tenté au mieux ; un balayage au démarrage puis toutes les 60 secondes retente jusqu'à 25 dossiers par passage. La même clé et les mêmes données rejouent le résultat d'une tentative terminée encore valide. Si le token initial a expiré, a été révoqué, achevé, supprimé ou transféré, la réponse est 410 voucher_preparation_expired : ne recréez pas la réservation sans vérification. Un successeur autorisé peut prolonger/transférer la préparation au premier accès au panier. Supprimer l'URL n'annule pas le panier.
409 idempotency_conflict signifie que les données ont changé ; voucher_preparation_pending renvoie Retry-After: 2 secondes et voucher_preparation_uncertain reste incertain même lors d'un nouvel essai (pas de succès automatique ultérieur). 410 voucher_preparation_expired interdit la reprise de la préparation initiale. 502/503 voucher_upstream_error est une erreur upstream. Si un panier a été créé sans retour de son identifiant, l'API disponible ne permet ni rapprochement ni libération automatique : l'état incertain est conservé et aucun second panier n'est créé lors du retry. Contactez ADITUS pour vérification manuelle avant toute nouvelle tentative. En cas d'échec de session-context, le micro-frontend affiche une erreur permettant de réessayer plutôt que de créer un panier.
Essayez les trois variantes en direct dans le Testeur d'intégration : générez une session avec eventSlug, choisissez startMode ou un assortiment, montez et observez le journal des requêtes. Le système de démonstration accepte le bon de test ED24-ADITUS sur ExperienceDaysv52024 (chaque échange réserve le billet environ 20 minutes ; revenir en arrière le libère). Ouvrir le Testeur d'intégration
Optionnel : naviguer anonymement, se connecter au paiement
Vous n'êtes pas obligé de savoir dès le départ qui est l'utilisateur. Montez avec une session invité et le micro-frontend laisse un visiteur anonyme parcourir les événements, choisir des articles et remplir le panier. Ce n'est que lorsqu'il dépasse le panier (vers l'inscription et le paiement) qu'il vous demande de définir l'utilisateur réel, via le callback onUserRequired. Le reste du flux continue ensuite avec le panier préservé.
1
Monter avec une session invité
Un token de session reste nécessaire pour activer la boutique, mais ce peut être une session invité : générez-le simplement sans le champ email, aucune adresse fictive n'est nécessaire. Le visiteur navigue et remplit le panier sous cette session.
2
Quitter le panier déclenche onUserRequired
Dès que le visiteur dépasse le panier, le micro-frontend appelle votre callback une seule fois et affiche un état d'attente neutre. Authentifiez l'utilisateur (login/SSO), puis générez une nouvelle session liée à l'utilisateur exactement comme la première : serveur à serveur avec votre secret. La définition ultérieure de l'utilisateur passe elle aussi par cette création de session de serveur à serveur. Le secret n'atteint jamais le navigateur et l'utilisateur n'est jamais défini dans le code client. Votre callback ne fait que relayer le token opaque obtenu. Point crucial : votre backend déduit cette identité de SA propre session authentifiée (cookie/JWT) ; il ne doit jamais accepter l'e-mail ou l'identifiant utilisateur depuis le navigateur, sinon un visiteur malveillant pourrait demander la session d'autrui et lire ses données personnelles via le préremplissage.
3
Renvoyez le nouveau token. Le panier est conservé
Renvoyez le nouveau token de session et le micro-frontend l'adopte et lie l'utilisateur identifié au panier existant. Le panier, ses articles et d'éventuels créneaux horaires restent inchangés. Il passe ensuite à l'inscription. Renvoyez null pour annuler. Le visiteur reste simplement sur le panier, sans erreur.
4
Omettre onUserRequired pour un utilisateur fixe
Si vous omettez le callback, rien ne change par rapport au flux classique : montez avec une session liée à un utilisateur et le micro-frontend agit comme cet utilisateur dès la toute première étape.
page.ts
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);
L'authentification se fait sur votre site
ADITUS n'authentifie pas vos utilisateurs finaux. Votre site est la seule source de vérité quant à qui est l'utilisateur. Vous exploitez votre propre login, SSO ou session. ADITUS ne fait confiance qu'à un token de session que votre backend génère avec une clé secrète. La chaîne de confiance est la suivante : votre auth prouve l'identité, votre token généré avec le secret la garantit, le micro-frontend la présente.
1
Votre site authentifie l'utilisateur
Login, SSO et session membre sont entièrement les vôtres. ADITUS n'intervient pas ici.
2
Votre backend génère une session
Serveur à serveur avec la clé secrète. Vous transmettez en tant que qui le micro-frontend doit agir (email) et le liez à votre publicKey.
3
La page monte le micro-frontend
Le token opaque est transmis au navigateur et passé comme sessionToken à mount(). Le secret reste sur votre serveur.
4
Chaque appel porte la session
Le micro-frontend envoie X-Aditus-Session à chaque requête ; l'API shop résout l'utilisateur du panier à partir de la session.
5
Pas de token → le micro-frontend reste inactif
Sans session, le micro-frontend ne s'active jamais : il affiche un avis neutre et n'effectue aucun appel. Ni le panier ni le paiement ne sont disponibles. Générez une session pour l'activer.
1. Générer sur votre backend
server.ts
// 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. Monter avec le 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. Révoquer à la déconnexion
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 }
La commande finalisée revient à votre page
Lorsqu'un achat de billets se termine, vous n'avez pas besoin d'une requête de commande distincte. Comme le micro-frontend s'exécute nativement dans votre page (light DOM, pas une iframe), il renvoie la commande finalisée directement à votre code via le callback onComplete. Il s’agit d’un appel JS direct, sans postMessage. Le micro-frontend a déjà résolu la commande pour vous, si bien que vous recevez le numéro de commande et les liens de billets, et pas seulement un identifiant.
mount.ts
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);
},
});
Ce que vous recevez
onComplete se déclenche une fois par paiement réussi avec la commande résolue : id (le GUID de la commande, votre clé pour la suite du traitement), number (le numéro de commande à afficher), status, buyer (nom & e-mail lorsqu'ils sont présents) et tickets. Chaque billet porte des liens typés de kind “pdf”, “apple” ou “google”. onError se déclenche à toute erreur irrécupérable du flux.
Moyens de paiement avec redirection
Les moyens qui restent sur la page (facture, prépaiement) déclenchent onComplete immédiatement. Un moyen qui envoie le navigateur vers une page de paiement externe (p. ex. Saferpay) est pris en charge de bout en bout par le micro-frontend : il déduit les URL de retour de la page où il s'exécute, envoie l'utilisateur à l'extérieur, et au retour finalise la commande et déclenche onComplete. Cet aller-retour complet ne nécessite aucun code de votre côté. Ce n'est que si le retour doit aboutir sur une autre page que vous surchargez redirectUrls.successUrl / cancelUrl / errorUrl.
Événements GA4 / e-commerce (optionnel)
Le micro-frontend peut remonter le tunnel d'achat vers votre analytique, mais seulement si vous le lui demandez. Il n'embarque ni gtag, ni GTM, ni SDK GA, ne charge rien, ne pose aucun cookie et n'envoie rien de lui-même. Vous ajoutez un callback optionnel, onEvent, et recevez des événements typés déjà formatés selon le schéma GA4 Enhanced Ecommerce de Google. Omettez onEvent et rien ne change. Quel outil vous alimentez, et si vous avez le consentement pour envoyer, reste entièrement de votre côté.
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);
},
});
Déjà au format GA4
Chaque événement porte un nom d'événement GA4 plus ses paramètres standard : items (avec item_id, item_name, price, quantity, item_category et, le cas échéant, discount et coupon), value et currency ; add_payment_info ajoute payment_type et purchase ajoute transaction_id. Extrayez name par déstructuration et transmettez le reste directement à gtag("event", name, params). Aucun remappage n’est nécessaire.
Le consentement reste le vôtre
onEvent n'est qu'une fonction JavaScript normale de votre page que nous appelons directement. Le micro-frontend lui-même n'envoie aucune analytique où que ce soit. Rien ne quitte la page tant que votre handler ne l'envoie pas, si bien que votre gestion du consentement garde le plein contrôle : conditionnez la transmission au consentement, routez vers le dataLayer de GTM au lieu de gtag, regroupez ou écartez entièrement les événements. Toute erreur que votre handler lève est interceptée et isolée, donc elle ne casse jamais le flux utilisateur. Gardez le handler léger (un push gtag/dataLayer) puisqu'il s'exécute inline.
ÉvénementSe déclenche quand
view_item_listL'utilisateur voit la liste d'articles (se déclenche une fois par ensemble affiché).
add_to_cartUn article ou un add-on est ajouté au panier.
remove_from_cartUne ligne du panier ou un add-on est retiré.
view_cartLa vue du panier est affichée (une fois par panier).
begin_checkoutL'utilisateur quitte le panier vers l'inscription / le paiement.
add_payment_infoUn mode de paiement est choisi (contient payment_type).
purchaseLa commande est passée (contient transaction_id, value, items).
Bus d'événements : handle.on / handle.off (optionnel)
Outre les callbacks de config, le handle renvoyé par mount() porte un petit bus d'événements entièrement optionnel. Abonnez-vous avec handle.on(event, listener) (il renvoie la fonction de désabonnement correspondante) et détachez avec handle.off(event, listener). Si vous n'appelez jamais on(), rien ne change : le bus n'ajoute aucune dépendance, n'envoie rien et ne coûte rien. C'est de la pure observation pour votre page (un badge mini-panier, une bannière de confirmation, votre propre logging) et il complète les callbacks au lieu de les remplacer.
mount.ts
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();
ÉvénementSe déclenche quand
cart:updateLe panier change : créé, article ajouté ou retiré, ou vidé. Contient cartId, itemCount, value, currency et les lignes sous forme d'items au format GA4.
phase:changeL'étape visible a changé. L’événement contient phase (events · home · articles · voucher · cart · registration · legitimation · buyer · payment · summary · done, même vocabulaire que data-step), previous (null pour l'étape d'entrée) et startPhase, l'étape d'entrée de la session. Votre page pilote ainsi ses propres boutons Retour / Recommencer sans lire les hooks CSS.
checkout:completeLa commande est passée. Émis juste après config.onComplete avec le même objet de commande résolu, avant que le micro-frontend n’affiche sa page de confirmation. Appelez preventDefault() de manière synchrone pour afficher votre propre page de confirmation (voir « Votre propre page de confirmation »).
analyticsMiroir du funnel GA4 ci-dessus : chaque payload onEvent est aussi émis sur le bus, sans modification.
Les listeners survivent à update()
Les abonnements vivent sur le handle, pas sur un rendu : handle.update({ ... }) re-rend le micro-frontend mais garde chaque listener attaché. Le démontage via handle() détache automatiquement tous les listeners. Vous n'avez besoin de off() (ou de la fonction de désabonnement renvoyée) que si vous voulez cesser d'écouter alors que la boutique continue de tourner.
Isolé, jamais bloquant
Un listener qui lève une erreur est intercepté et isolé. Il ne casse jamais le flux utilisateur et n'affecte jamais les autres listeners. L'événement “analytics” reflète le tunnel GA4 avec des payloads identiques à config.onEvent, si bien que vous pouvez consommer le tunnel via le bus, le callback ou les deux ; la même règle de consentement s'applique : rien ne quitte la page tant que votre code ne l'envoie pas.
Votre propre page de confirmation
checkout:complete est émis avant que le micro-frontend n’affiche sa propre page de confirmation. Si votre page affiche sa propre confirmation, appelez preventDefault() de manière synchrone dans le listener : event.preventDefault() sur l’élément <aditus-shop>, preventDefault() du payload avec handle.on(), ou completion.preventDefault() du second argument de config.onComplete. Le micro-frontend n’affiche alors aucune page de confirmation, ne fait pas défiler la page et n’affiche plus d’étape ; votre page prend le relais. La commande est passée dans tous les cas. L’appel doit avoir lieu dans le listener lui-même, pas après un await. Sans lui, le micro-frontend affiche sa page de confirmation comme avant. Cela vaut aussi pour le retour depuis une page de paiement externe.
Terminez ensuite la session. La commande est finalisée : votre serveur peut donc révoquer la session dans la foulée avec DELETE /api/shop/session/:token et votre clé secrète (voir « Révoquer une session »). La commande passée et les liens des billets dans l’objet de commande restent valides, car les liens ne dépendent pas de la session. Pour un nouvel achat, générez une nouvelle session et montez à nouveau le micro-frontend. Le micro-frontend ne termine pas la session lui-même, car la révocation nécessite votre clé secrète. Ne conservez la session que si votre page continue délibérément à vendre dans cette session.
Commandes : goTo et back
En plus d'observer, votre page peut envoyer deux requêtes ciblées au Micro Frontend. Toutes deux renvoient true si la boutique a changé d'étape, sinon false ; elles ne remontent jamais le composant et ne perdent jamais le parcours. goTo("cart") affiche l'étape panier pour votre propre mini-panier ; elle n'est acceptée qu'à l'étape d'enregistrement, tant que rien n'a encore été écrit. back() revient d'une étape, exactement comme le bouton Retour de la boutique. Elle renvoie false partout où ce bouton est absent ou désactivé : à l'étape d'entrée (startPhase), sur la page de confirmation et pendant un paiement ou une autre modification en cours. Les deux existent sur le handle de mount et sur <aditus-shop> (element.back(), element.goTo("cart")).
browser-back.ts
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" });
}
Le Micro Frontend n'écrit jamais lui-même dans l'historique du navigateur : les URL et le routeur appartiennent à votre site. L'exemple conserve exactement une entrée d'historique supplémentaire tant que la boutique a dépassé l'étape d'entrée ; le geste Retour du navigateur (y compris le balayage sur iPhone) recule d'abord d'une étape dans la boutique et ne quitte la page qu'à partir de l'étape d'entrée. Si votre site utilise un routeur côté client (React Router, Next.js, Vue Router), créez cette entrée via le routeur plutôt qu'avec history.pushState, afin qu'il ne la traite pas comme une navigation étrangère.
publicKey vs. sessionToken
Gardez les deux distincts. Ils répondent à des questions différentes et sont appariés, non interchangeables.
publicKey: confiance d'origine
Publiable. Elle est livrée dans votre code client, donc ce n'est pas un secret et elle n'accorde rien à elle seule. Le backend applique une liste blanche de domaines par clé : une clé ne fonctionne que depuis ses domaines enregistrés. Elle répond à « quel est ce site ? » Seule une option explicite, délibérément non sécurisée, définie par l'opérateur pour un client précis peut lever cette liste blanche ; le statut actif, l'obligation de session et les secrets restent alors en vigueur.
sessionToken: identité
Généré avec le secret sur votre backend, opaque et de courte durée. Il répond à « qui est l'utilisateur ? » Une session est liée au publicKey pour lequel elle a été générée : une requête de session doit aussi envoyer cette clé, et une non-concordance est rejetée (403).
Règles de sécurité
La clé secrète ne vit que sur votre serveur, jamais dans le code client, les bundles ou les fichiers env livrés au navigateur.
Ne stockez pas vous-même le token dans localStorage ou dans des cookies ; gardez-le en mémoire et renouvelez-le en créant une nouvelle session. Pour les rechargements et l'aller-retour de paiement, le micro-frontend le conserve lui-même dans le sessionStorage de l'onglet.
Les tokens sont de courte durée (20 minutes par défaut, 24 heures max) et mono-utilisateur.
Révoquez la session à la déconnexion : un token révoqué est refusé immédiatement (401). Jusqu'à son expiration ou sa révocation, un token valide est un bearer token. Traitez-le comme tel.
Associez toujours la session à son publicKey. Une clé manquante ou non concordante est rejetée (403).
Une session invalide ou expirée renvoie 401. Il n'y a aucun repli sur un autre utilisateur.
Ce qu'un token de session peut faire
Un token de session est un bearer token. Voici la portée exacte que l'API ADITUS applique. Utilisez ces informations pour votre modèle de menaces et votre revue de sécurité.
Token et durée de vie
Aléatoire 256 bits, opaque, ne contient aucune donnée et ne peut pas être décodé. 20 minutes par défaut, configurable de 1 minute à 24 heures lors de la création. Une session n'est jamais prolongée. Renouvelez-la en en créant une nouvelle.
Liaison
Liée à la publicKey pour laquelle elle a été créée. Chaque requête de session doit envoyer exactement cette clé, et l'origine de la requête est vérifiée par rapport à la liste blanche de domaines de la clé. Une autre clé ou une origine étrangère est rejetée (403).
Session anonyme
Créée sans utilisateur : peut parcourir les événements et créer et modifier son propre panier. L'inscription, les créneaux, l'attribution du paiement et le checkout sont refusés côté serveur (403) jusqu'à ce que votre page la remplace par une session identifiée.
Session identifiée
En plus : inscription, créneaux, attribution du paiement et checkout, toujours limités aux paniers que possède cette session. Avec protectUserData, les données acheteur enregistrées pour l'utilisateur sont protégées : une écriture qui les modifierait est refusée (403).
Propriété du panier
Un panier est lié à son propriétaire par un hash à clé (HMAC): pour une session anonyme le token lui-même, pour une session identifiée l'identité utilisateur créée par votre backend, par publicKey. Les sessions d'autres utilisateurs n'y ont pas accès et les écritures sur un panier sont sérialisées.
Données personnelles
Le contexte de session renvoie des indicateurs, des identifiants et l'identifiant utilisateur créé par votre propre backend. Votre site connaît déjà toutes ces informations. Les données acheteur ne sont accessibles que via les routes panier et inscription, soumises au contrôle de propriété.
Limites de débit
Valeurs par défaut : création et révocation 300 par minute par client et par IP, autocomplétion d'adresse, d'entreprise et de ville 60 par minute par session et 3000 par minute par client toutes sessions confondues. Chaque limite est un budget propre à votre client, les autres clients ne l'affectent jamais. ADITUS peut définir des limites individuelles par client sur demande.
Journal d'audit
Les requêtes vers l'API shop sont enregistrées et conservées 90 jours : client, route normalisée, statut, hôte d'origine, hash abrégé du token de session et identifiant de panier ou de commande. Jamais le token lui-même, ni e-mail, ni adresse IP.
Révocation
DELETE /api/shop/session/:token invalide la session immédiatement. Aucun cache entre les deux ; la toute prochaine requête avec ce token reçoit un 401. Appelez-le à la déconnexion.
Référence des endpoints & réponses
POST/api/shop/sessiongénérer avec le secret Bearer
DELETE/api/shop/session/:tokenrévoquer avec le secret Bearer
Les deux appels proviennent de votre backend : il s'authentifie avec le mint secret de votre client (généré dans la console d'administration ADITUS) comme token Bearer. Une session par connexion est le schéma habituel ; le navigateur ne voit que le token obtenu. Les sites sans backend n'utilisent pas ces endpoints : l'élément crée lui-même sa session navigateur via POST /api/shop/embed-session, limité au domaine (public-key avec cart-scope="tab").
Vous pouvez essayer ce flux exact avec vos propres identifiants dans le testeur d'intégration. Générez une session, montez la boutique et suivez le parcours.
Générer une session : requête
En-têteValeurSignification
AuthorizationBearer <mint-secret>Le secret de mint de votre client, généré dans la console d'administration ADITUS. Uniquement côté serveur. Il ne doit jamais être livré à un navigateur ou à un bundle applicatif.
Content-Typeapplication/jsonLe corps est un objet JSON.
Idempotency-Keystring · 8–200 visible ASCII chars · required with redeemVoucherOnMintObligatoire si redeemVoucherOnMint est true : 8–200 caractères ASCII visibles. Clé stable pour une tentative d'échange ; conservez-la côté serveur et réutilisez-la avec une requête identique lors des nouvelles tentatives. Une autre requête avec la même clé renvoie 409 idempotency_conflict. Ne transmettez ni le secret ni la clé depuis le navigateur.
Champ du bodyTypeSignification
sharedCartKeystring · facultatifLes sessions identifiées créées serveur à serveur partagent normalement via l'identité de confiance. Pour les paniers anonymes partagés, votre backend DOIT créer une session avec sharedCartKey : une référence visiteur opaque et stable issue de sa propre session visiteur de confiance, pas un e-mail. Ce champ est réservé à la création de session côté serveur, ni configuration navigateur ni attribut HTML. N'acceptez jamais d'e-mail, d'identité ou de sharedCartKey arbitraire du navigateur. Une session créée par le navigateur ne permet pas à elle seule un partage sûr ; shared sans identité de confiance renvoie une erreur explicite de non-prise en charge, même si la portée est omise. L'isolation explicite 'tab' n'exige pas sharedCartKey ; aucun repli silencieux.
publicKeystring · obligatoireLa clé publique pour laquelle la session est générée. Ce doit être la même clé avec laquelle le micro-frontend monte. Chaque appel boutique ultérieur est vérifié par rapport à elle (403 en cas de divergence). Le secret Bearer doit appartenir au client de cette clé.
emailstring · optionalL'utilisateur boutique en tant que lequel la session agit. Prenez-le de votre session serveur authentifiée (cookie de login/SSO ou JWT), jamais de la requête navigateur, sinon un visiteur pourrait obtenir la session d'autrui. S'il est omis, une session anonyme est générée : le visiteur peut naviguer et remplir le panier, mais le micro-frontend bloque l'étape après le panier jusqu'à ce que votre callback onUserRequired fournisse une session liée à un utilisateur (voir le flux utilisateur optionnel). Un e-mail présent mais mal formé reste rejeté (400).
externalUserIdstring · facultatifVotre référence utilisateur stable issue de votre session serveur authentifiée. Conservée avec la session et utilisée pour la portée d'identité si présente ; ne la modifiez pas au renouvellement ni ne l'acceptez d'une saisie navigateur arbitraire. Ce n'est pas une authentification en soi.
ttlSecondsnumber · optionalDurée de vie de la session en secondes. Par défaut 20 minutes, plafonnée à 24 heures. À l'expiration, les appels boutique renvoient 401 session_expired. Émettez alors un nouveau token ; avec refreshSession dans la config, le micro-frontend le demande automatiquement.
eventSlugstring · optionalFixe le point d'entrée du parcours côté serveur : défini, le micro-frontend démarre directement sur la sélection d'articles de cet événement ; omis, il démarre sur l'aperçu des événements. Comme il fait partie de la session générée, le navigateur ne peut pas le manipuler. Des caractères invalides renvoient 400 invalid_event_slug ; pour un slug sans événement en direct (404 event_not_found) ou un événement fermé / hors vente (409 event_not_sellable), la génération échoue volontairement. Aucune session n'est créée et aucun retour à l'aperçu des événements n'a lieu.
startMode "voucher" · optionalVue d'entrée du parcours. "articles" (par défaut) ouvre la sélection d'articles ; "home" ouvre la page d'accueil de l'événement avec ses points d'entrée (billets, voucher, assortiments publiés) ; "voucher" ouvre d'abord le formulaire de code voucher/billet. Le formulaire de bon se comporte comme décrit sous « Échanger un bon / enregistrer un billet ». Sans eventSlug, le choix de l'événement vient d'abord, puis le mode s'applique à l'événement choisi.
assortmentstring · optionalRestreint le démarrage sur les articles à un assortiment de l'événement fixé (nécessite eventSlug ; sans effet avec startMode "voucher"). Indiquez l'identifiant de l'assortiment ou son nom exact (insensible à la casse) ; les assortiments masqués sont autorisés. Un sélecteur sans correspondance affiche une erreur explicite dans le micro-frontend au lieu de revenir silencieusement à la liste complète. 200 caractères max., sinon 400 invalid_assortment.
voucherCodestring · optionalCode de bon/billet transmis avec la session (les deux modes). Par défaut, le micro-frontend l'échange sur un nouveau parcours, après le choix de l'événement s'il n'est pas fixé ; un refus s'affiche dans le formulaire ou dans un avis du premier panier. Avec redeemVoucherOnMint: true, il est réservé côté serveur avant la réponse pour un événement fixé et un utilisateur identifié. 1–100 caractères sans espaces ni délimiteurs d'URL, sinon 400 invalid_voucher_code.
redeemVoucherOnMintboolean · optional (default false)À true uniquement pour une génération serveur à serveur avec voucherCode, eventSlug fixé, email identifié et Idempotency-Key. Le serveur crée un panier lié à l'utilisateur et réserve réellement le bon avant de délivrer la session. Le micro-frontend reprend ce panier sans le recréer ni échanger le code une deuxième fois ; les deux modes fonctionnent. Codes de bon/billet uniquement, pas PromotionCode. Sans l'option, la transmission normale du voucherCode s'applique. Inscription et finalisation restent nécessaires.
Générer une session : réponse (200)
ChampTypeSignification
sessionTokenstringToken opaque (sess_…). Transmettez-le au navigateur et passez-le à mount() comme sessionToken ; le micro-frontend l'envoie comme X-Aditus-Session à chaque appel boutique. Il ne contient aucune donnée utilisateur et ne peut pas être décodé.
expiresAtnumberExpiration sous forme d'horodatage Unix en millisecondes. Purement informatif pour votre propre planification. Le micro-frontend réagit lui-même au 401.
voucherRedeemedboolean · only with redeemVoucherOnMinttrue si la génération avec préparation du bon réussit. La session possède déjà un panier et une réservation, repris par le micro-frontend. L'inscription du billet n'est pas encore terminée.
Révoquer une session
DELETE /api/shop/session/:token avec le même secret Bearer. Le secret doit appartenir au client pour lequel la session a été générée. Appelez-le à la déconnexion afin que le token meure avec votre propre session. Réponse : { "revoked": true } (ou false si la session avait déjà expiré). La révocation est idempotente et peut être envoyée en fire-and-forget.
Lectures serveur à serveur
Un backend qui pilote le micro-frontend (coque de kiosque, boutique sur site) a généralement besoin de données du tenant avant de générer une session : quels événements existent et sont en vente, quelles langues parle le shop, quelles listes de référence et quel branding s'appliquent. Quatre endpoints de lecture y répondent avec les mêmes identifiants que la génération (secret Bearer + en-tête X-Aditus-Public-Key, même limite de débit, Cache-Control: no-store). Ce sont des projections filtrées de la Shop API, jamais un relais brut ; ?language= (ou Accept-Language, de par défaut) choisit la langue des noms. Erreurs : 400 invalid_public_key, 401 unauthorized, 503 shop_api_not_configured, 502 upstream_error.
EndpointRenvoie
GET /api/shop/events?language=deTous les événements du tenant : id, slug, name, year, start/end, location, saleState (open · registration_only · closed), les indicateurs bruts, showInOverview, languages, URL d'icône/logo. Le slug pour la génération vient d'ici.
GET /api/shop/configurationDonnées du tenant : mandator, les noms des listes de référence (civilités, pays, titres, consentement cookies) et websiteMetricTracking. Les identifiants des fournisseurs de connexion ne sont volontairement jamais inclus.
GET /api/shop/languagessystemLanguage plus la liste des langues du shop ({ code, name }). Ce sont les valeurs acceptées par l'attribut culture du micro-frontend.
GET /api/shop/branding?images=ONSITE_SHOP_LOGO,…Le thème Shop du tenant : styles en table plate clé → valeur, les images du thème avec leurs URL et (pour jusqu'à 40 clés d'image que vous nommez (A–Z, 0–9, _ , -)) si la clé existe, son URL publique et son type de contenu. Les URL d'images ne nécessitent aucune authentification. Ajoutez &eventSlug=<slug> ou &event=<GUID> pour limiter le thème et les clés à un événement (surcharges au niveau événement, clés propres à l'événement comme ONSITE_SHOP_HEADER / ONSITE_SHOP_ATTRACT_*) ; la réponse nomme alors l'événement résolu. Slug inconnu → 404 event_not_found, pas un GUID → 400 invalid_event, Shop API injoignable → 503 event_unavailable.
Les erreurs sont explicites
StatutCodeSignification
503session_not_configuredminting is disabled for this client: no mint secret has been generated in the admin console yet.
401unauthorizedthe mint secret is wrong or missing. Check that the secret belongs to the client of exactly this public key (rotated secrets invalidate the old one immediately).
400invalid_public_keythe public key is malformed or not registered. It must look like pk_… and belong to a configured client.
400invalid_emailthe shop user email is missing or not a valid address.
400invalid_tokenthe session token in the revoke call is malformed. Pass exactly the sessionToken returned by the mint call.
429rate_limitedtoo many mint/revoke calls from your IP. Wait a minute and try again.
403public_key_invalidthe X-Aditus-Public-Key header is malformed. It must look like pk_… exactly as issued during onboarding.
403public_key_unknownthe X-Aditus-Public-Key is well-formed but not registered (or deactivated). Check for typos and that the key's client is active.
403public_key_origin_unresolvedthe request carried a publicKey but no usable Origin/Referer header, so the domain whitelist cannot be checked. Browsers send Origin automatically; server-side calls must not use the publicKey header.
403public_key_domain_not_allowedthe request's origin domain is not on this publicKey's whitelist. Add the domain during onboarding (or in the admin console) before going live on it.
401invalid_sessionthe shop call carried a malformed X-Aditus-Session token. There is no fallback to another user. Pass exactly the sessionToken (sess_…) returned by the mint call.
401session_expiredthe X-Aditus-Session token is unknown, revoked or expired. There is no fallback to another user. Mint a fresh session server-to-server.
403session_requires_public_keya session was sent without its X-Aditus-Public-Key header (origin pinning is mandatory once a session is in play).
403session_key_mismatchthe X-Aditus-Public-Key does not match the key the session was minted for.
403anonymous_sessionthe session was minted WITHOUT an email (anonymous) and only allows browsing and the cart. Registration, payment and checkout require a user-bound session. Create one via onUserRequired.
Maintenant, faites-le vôtre
L'identité est établie. Passons maintenant au branding du micro-frontend. L'outil de styling génère une config de thème prête à coller et explique chaque paramètre, avec un aperçu en direct.
Vous travaillez avec un agent de programmation ? Transmettez-lui le guide d’intégration lisible par machine du Micro Frontend. Copiez l’URI canonique ci-dessous et collez-la dans votre agent.