@akin-travel/sdk-embed
v0.1.0
Published
Framework-agnostic embeddable Akin loyalty + auth widget. Drop-in Web Components (<akin-login>, <akin-loyalty-card>, <akin-tier-progress>) over @akin-travel/sdk-core — integrate on Vue, Angular, Svelte, plain HTML, or any server-rendered stack with a sing
Maintainers
Readme
@akin-travel/sdk-embed
Framework-agnostic embeddable Akin loyalty + auth widgets — drop-in
Web Components
built on @akin-travel/sdk-core.
This is Phase 2 of the multi-platform Partner SDK
(epic akin-travel/issues#2042):
the zero-framework reach. Partners on Vue, Angular, Svelte, plain HTML, or
any server-rendered stack integrate loyalty + auth with a single <script> tag
and custom elements — no per-framework SDK, no build step.
Looking for React? Use
@akin-travel/partner-sdk(providers, hooks, headless components). The embed widget is the answer for everything else.
Quick start (the <script> drop-in)
<script
src="https://cdn.jsdelivr.net/npm/@akin-travel/sdk-embed/dist/akin-embed.global.js"
data-partner-id="YOUR_PARTNER_ID"
data-api-key="YOUR_PARTNER_API_KEY"
data-environment="production"
data-firebase-api-key="…"
data-firebase-auth-domain="…"
data-firebase-project-id="…"
data-firebase-app-id="…"
data-verify-callback-url="https://your-site.example/loyalty"></script>
<akin-login></akin-login>
<akin-loyalty-card></akin-loyalty-card>
<akin-tier-progress
data-tiers='[{"displayName":"Silver","points":1000},{"displayName":"Gold","points":5000}]'></akin-tier-progress>The script auto-initialises from its own data-* attributes and registers the
custom elements. A member signs in via <akin-login> (magic link, passkey, or
Google), and <akin-loyalty-card> / <akin-tier-progress> render their tier and
points. Opt out of auto-init with data-auto-init="false" and call
AkinEmbed.init({...}) yourself.
The elements
| Tag | Renders |
|---|---|
| <akin-login> | Email magic-link form, plus passkey + Google buttons when available; signed-in summary with sign-out |
| <akin-loyalty-card> | The member's AKIN points + spendable balance + tier badge |
| <akin-tier-progress> | Current tier and a progress bar toward the next (data-tiers supplies the ladder) |
| <akin-book-search> | A booking search bar: your properties, check-in and check-out dates, and guests. Submit opens your AKIN booking site |
Each element is a thin view over a shared controller, so they stay in sync — sign
in once and every widget on the page updates. <akin-book-search> is the
exception: it needs no sign-in and no init() (see below)
Booking search (<akin-book-search>)
<akin-book-search partner="your-slug"> puts a search bar for your own booking
site on any page you own. It lists your properties, takes check-in and check-out
dates and guests, and on Search sends the guest — in the top window, never an
iframe — to your booking site:
https://book-<your-slug>.akintravel.com/<locale>/book?checkIn=2030-01-10&checkOut=2030-01-13&adults=2&children=0&rooms=1&property=<property-slug><locale> is the language the bar shows its copy in (see locale below), so the
guest stays in that language on the booking site
The element never takes payment details. Everything after the search happens on the booking site, so your page stays out of card-data scope
It needs no partner id, API key or Firebase config: the script defines it as soon as it loads, and the property list is a public query
| Attribute | Required | Description |
|---|---|---|
| partner | ✓ | Your AKIN partner slug — the <your-slug> in book-<your-slug>.akintravel.com |
| environment | | production (default), staging, or development. staging sends guests to staging-book-<your-slug>.akintravel.com |
| api-url | | Override the GraphQL endpoint (defaults per environment) |
| data-theme | | Per-element theme override (JSON), as for every element |
| locale | | The language of the bar's labels, placeholders and messages, and of the booking site it opens: en, es, pt, fr or de. A region suffix is dropped (es-CO is es). Without the attribute the bar uses the page's <html lang>, and follows it when the page changes it. Any other language shows English |
Before the widget goes live, AKIN adds the partner site's origin (apex + www, for
example https://your-site.example and https://www.your-site.example) to the API
CORS_ORIGINS (infrastructure/env/prod.yaml and staging.yaml). Until then the
browser refuses the property list. AKIN also switches on your booking site
Not on npm yet. Neither @akin-travel/sdk-embed nor @akin-travel/sdk-core has
been published, so the jsdelivr URL below returns 404 until a maintainer makes the
first publish by hand (npm trusted publishing needs an existing package — see
.github/workflows/sdk-release.yml). That first publish is a prerequisite for the
snippet
WordPress page (copy-paste)
In the block editor, add a Custom HTML block where the bar should appear and
paste this (replace your-slug). The element works in a classic-editor page or a
theme template too
<script src="https://cdn.jsdelivr.net/npm/@akin-travel/sdk-embed/dist/akin-embed.global.js" defer></script>
<akin-book-search partner="your-slug"
style="--akin-primary: #1f4d3a; --akin-radius: 0.5rem; max-width: 48rem;"></akin-book-search>Put the <script> tag on the page once, even with several bars. On a page that
also carries the loyalty elements, keep the one <script> with its data-* config
WordPress saves a <script> tag only for a user who may post unfiltered HTML — an
administrator or editor on a single site, a super admin on a multisite network. If
the script disappears when you save, ask one of them to add the block, or load the
script from your theme's header instead
Event
Before it navigates the element emits a composed, cancelable akin:book-search
event with detail.url (the booking-site URL) and detail.values. A listener that
only records the search needs nothing else — the element navigates afterwards:
document.querySelector('akin-book-search').addEventListener('akin:book-search', (e) => {
analytics.track('booking_search', e.detail.values);
});preventDefault() stops the navigation. The guest then stays on your page until
your own code sends them to e.detail.url — for example after an async analytics
call resolves:
bar.addEventListener('akin:book-search', async (e) => {
e.preventDefault();
await analytics.track('booking_search', e.detail.values);
window.top.location.href = e.detail.url;
});Events
The elements emit composed CustomEvents that cross the Shadow DOM boundary, so
the host page can react:
document.querySelector('akin-login')
.addEventListener('akin:authenticated', (e) => console.log(e.detail.member));akin:magic-link-sent, akin:authenticated, akin:signed-out, akin:error.
Style isolation
Every widget renders inside an open Shadow DOM, so host-page CSS can't reach in and the widget's own CSS can't leak out — the isolation acceptance criterion.
Theming
CSS custom properties inherit through the Shadow DOM boundary, so the widgets honour the partner theme-token contract used by white-label automatically:
- Set the namespaced
--akin-*tokens (highest precedence), or - expose the white-label/shadcn tokens (
--primary,--background,--foreground,--secondary,--muted,--border,--radius) — the widgets fall back to them, then to neutral defaults.
:root {
--akin-primary: oklch(0.62 0.19 28); /* OKLCH, hex, rgb — passed through verbatim */
--akin-radius: 0.75rem;
}Per-widget override with data-theme (JSON) or programmatically:
<akin-loyalty-card data-theme='{"primary":"#0f766e","radius":"1rem"}'></akin-loyalty-card>npm / bundler usage (Vue, Angular, Svelte build pipelines)
npm install @akin-travel/sdk-embed @akin-travel/sdk-coreimport { createAkinEmbed } from '@akin-travel/sdk-embed';
const akin = createAkinEmbed({
partnerId: 'YOUR_PARTNER_ID',
apiKey: 'YOUR_PARTNER_API_KEY',
environment: 'production',
firebaseApiKey: '…',
firebaseAuthDomain: '…',
firebaseProjectId: '…',
firebaseAppId: '…',
verifyCallbackUrl: 'https://your-site.example/loyalty',
});Then use the elements in your templates — they're standard custom elements:
- Vue: works out of the box; tell Vue they're custom elements via
compilerOptions.isCustomElement = (tag) => tag.startsWith('akin-'). - Angular: add
CUSTOM_ELEMENTS_SCHEMAto the module/component. - Svelte: use the tags directly in markup.
Using only <akin-book-search>? It needs no createAkinEmbed call — define it
with registerElements(INIT_FREE_ELEMENT_TAGS) from the same import
import { registerElements, INIT_FREE_ELEMENT_TAGS } from '@akin-travel/sdk-embed';
registerElements(INIT_FREE_ELEMENT_TAGS);<akin-login />
<akin-loyalty-card />Builds
| Output | For |
|---|---|
| dist/akin-embed.global.js (IIFE, window.AkinEmbed) | The CDN <script> drop-in — fully self-contained (inlines firebase/auth). unpkg/jsdelivr point here |
| dist/index.js (ESM) + dist/index.d.ts | npm consumers who bundle it themselves; runtime deps stay external |
npm run build ends with a size check: scripts/check-bundle-size.mjs fails the
build when dist/akin-embed.global.js outgrows its gzip budget. Every visitor to a
partner page downloads that file, so a raised budget is a decision recorded in the
commit, never a way to get a red build green
Demo
demo/index.html is a no-framework page wiring all three
widgets, theming, and a Shadow-DOM-isolation probe. Build, then open it:
npm run build --workspace=@akin-travel/sdk-embed
# open packages/sdk-embed/demo/index.htmlPublishing
Published by the shared SDK release pipeline: the
@akin-travel/* scope is auto-discovered, built via build:sdk, and published
idempotently. The CDN entry point is the unpkg/jsdelivr field above.
