Skip to content

Advanced Embed Patterns

This page covers advanced embed integration patterns for developers who need more control over the FanFest embed behavior.

Custom Login Callback

The hostLoginFn callback lets you replace the embed's built-in login with your own authentication flow. When a user clicks "Login" inside the embed, the SDK calls your function instead of showing the FanFest login form.

javascript
FanFestSDK.init({
  clientId: "your-web-app-id",
  hostLoginFn: () => {
    // Example: redirect to your Auth0 login
    auth0Client.loginWithRedirect({
      redirect_uri: window.location.origin + "/auth/callback",
    });
  },
});

Requires SSO integration

Custom login callbacks only work when you have SSO configured. Without SSO, the embed uses FanFest's built-in authentication and the hostLoginFn is ignored.

How it works:

  1. The user clicks a login trigger inside the embed
  2. The embed sends a PostMessage to the SDK
  3. The SDK calls your hostLoginFn
  4. Your function handles the authentication (redirect, modal, etc.)
  5. After successful auth, the SDK's silent authentication syncs the user state back to the embed

Custom Rewards Callback

The hostRewardsFn callback lets you direct users to your own rewards experience instead of the embed's built-in rewards view.

javascript
FanFestSDK.init({
  clientId: "your-web-app-id",
  hostRewardsFn: () => {
    // Example: open a rewards modal on your site
    document.getElementById("rewards-modal").showModal();
  },
});

Requires SSO integration

Custom rewards callbacks only work when you have SSO configured. Without SSO, the embed handles rewards display internally and the hostRewardsFn is ignored.

Embed Lifecycle

The embed communicates with the host page through a PostMessage bridge. Here is the sequence of events during initialization:

Host Page                    SDK                         Embed Iframe
    |                         |                              |
    |-- init(options) ------->|                              |
    |                         |-- Create iframe ------------>|
    |                         |-- MutationObserver setup     |
    |                         |                              |
    |                         |<-- EmbedReady ---------------|
    |                         |-- Authentication ----------->|
    |                         |                              |
    |                         |<-- AuthenticationFinished ---|
    |                         |                              |

Key lifecycle events:

  1. SDK Initializationinit() validates options, fetches web app config, and creates the embed iframe
  2. Embed Ready — The iframe content loads and signals readiness via embed-ready PostMessage
  3. Authentication — The SDK sends the current auth state to the embed
  4. Authentication Finished — The embed confirms authentication is complete and reports the user state

The PostMessage bridge validates message origins against the configured appOrigin for security. Messages from unknown origins are silently ignored.

When FanFest is embedded, share and referral URLs point back to the host page instead of fanfest.vip. The SDK preserves the host page's path, query string, and hash, and stores the FanFest route — relative to the embedded channel — in the ff_r query parameter. The SDK already knows the channel it's embedding, so the channel slug is omitted to keep the URL compact.

Example:

text
https://partner.example/fanfest?existing=1&ff_r=%2Fln%2Fabc#host-section

When that URL loads, the SDK reads ff_r, re-prepends the configured channel, and boots the iframe directly into the matching FanFest route. If the route is invalid or external, the SDK ignores it and loads the configured channel root.

The legacy fanfest_route parameter is still read for backwards compatibility with links emitted by older SDK versions; values that point at another channel are rejected.

Multiple Embeds on One Page

The SDK supports multiple embed containers on the same page. Each <div id="fanfest-embed"> element gets its own iframe.

html
<!-- Main content area -->
<div id="fanfest-embed" style="width: 100%; height: 600px;"></div>

<!-- Sidebar widget -->
<div id="fanfest-embed" style="width: 300px; height: 400px;"></div>

Same channel

All embed instances on a page share the same clientId and display the same channel. The SDK does not currently support showing different channels in different embed containers on the same page.

Behavior notes:

  • All embed instances share the same authentication state — logging in through one embed authenticates all of them
  • The SDK tracks each container independently and creates a separate iframe for each
  • Removing a container from the DOM causes the SDK to automatically clean up that embed instance
  • Dynamic containers are supported — add a <div id="fanfest-embed"> at any time and the SDK's MutationObserver picks it up

Auto-Resize Embed

The enableAutoHeightEmbed option makes the iframe height automatically follow the content inside the embed, so you do not need to set a fixed height on the container.

javascript
FanFestSDK.init({
  clientId: "your-web-app-id",
  enableAutoHeightEmbed: true,
});
html
<!-- No fixed height — the iframe grows to fit content -->
<div id="fanfest-embed" style="width: 100%;"></div>

How it works:

  1. After creating the embed iframe, the SDK lazy-loads the @open-iframe-resizer/core parent script
  2. The parent script sends an iframe-child-init postMessage to the embed
  3. The frontend detects embed mode and lazy-loads the child script, which attaches a ResizeObserver to document.documentElement
  4. When the embed content height changes (navigation, expanded sections, etc.), the child sends an iframe-resized postMessage with the new height
  5. The parent script sets iframe.style.height to the reported value

Both scripts are lazy-loaded at runtime — this feature adds zero bytes to the SDK or frontend initial bundles.

Container requirements:

  • Do not set a fixed height on the container — it will override the auto-resize
  • Use min-height if you want a floor (e.g., to prevent a collapsed state before the first resize message):
html
<div id="fanfest-embed" style="width: 100%; min-height: 300px;"></div>

Cleanup: When the embed container is removed from the DOM, the SDK automatically calls unsubscribe() on the resize handle. No manual cleanup is required.

Browser compatibility: Chrome 64+, Safari 13.1+, Firefox 69+, Opera 51+.

Content Security Policy: If your host page uses a CSP, the auto-resize feature requires no additional directives — the library script is loaded from the same origin as the SDK bundle, and the postMessage exchange uses the existing allow-same-origin iframe sandbox permission.

Responsive Embed Sizing

The embed iframe fills 100% of its container's width and height. To make the embed responsive, style the container:

Fixed Height

html
<div id="fanfest-embed" style="width: 100%; height: 600px;"></div>

Viewport-Relative Height

html
<div id="fanfest-embed" style="width: 100%; height: 80vh;"></div>

Full-Page Embed

css
#fanfest-embed {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: 1000;
}

Responsive with Aspect Ratio

css
#fanfest-embed {
  width: 100%;
  aspect-ratio: 16 / 9;
}

Mobile-Responsive Layout

css
#fanfest-embed {
  width: 100%;
  height: 600px;
}

@media (max-width: 768px) {
  #fanfest-embed {
    height: 100vh;
    position: fixed;
    top: 0;
    left: 0;
  }
}

Action Mapping with registerActionMapping

The registerActionMapping method maps custom action names (used in your tracking code) to channel action IDs (configured in the FanFest dashboard). This is required for event tracking to correctly associate tracked events with leaderboard actions and point rewards.

Why Action Mapping Matters

When you track an event like FanFestSDK.trackEvent({ action: "purchase" }), the SDK needs to know which channel action ID this corresponds to. Action mappings create that link between your application's event names and FanFest's internal action system.

Three Input Formats

The SDK accepts three formats for registering action mappings. All three are validated at runtime and produce the same result internally.

Format 1: Single Object

Register one mapping at a time:

javascript
FanFestSDK.registerActionMapping({
  action: "purchase",
  channelActionId: "ca_abc123",
});

Format 2: Array of Objects

Register multiple mappings in a single call:

javascript
FanFestSDK.registerActionMapping([
  { action: "purchase", channelActionId: "ca_abc123" },
  { action: "page_view", channelActionId: "ca_def456" },
  { action: "share", channelActionId: "ca_ghi789" },
]);

Register multiple mappings using a key-value record where keys are action names and values are channel action IDs:

javascript
FanFestSDK.registerActionMapping({
  purchase: "ca_abc123",
  page_view: "ca_def456",
  share: "ca_ghi789",
  signup: "ca_jkl012",
  referral: "ca_mno345",
});

Recommended format

The bulk record format is the most concise and readable, especially when registering many mappings. We recommend this format for production use.

When to Register Mappings

Register action mappings after init() completes but before tracking any events:

javascript
await FanFestSDK.init({ clientId: "your-web-app-id" });

// Register all action mappings up front
FanFestSDK.registerActionMapping({
  purchase: "ca_abc123",
  page_view: "ca_def456",
  share: "ca_ghi789",
});

// Now tracking works correctly
FanFestSDK.trackEvent({ action: "purchase" });

Finding Channel Action IDs

Channel action IDs are created in the FanFest dashboard under your channel's leaderboard settings. Each action you create in the dashboard gets a unique ID that you use in your action mappings. See the Leaderboards guide for details on creating channel actions.

Validation Errors

If you pass an invalid payload to registerActionMapping, the SDK logs an error to the console but does not throw. This prevents a misconfigured mapping from breaking your application.

javascript
// This logs an error — values must be strings
FanFestSDK.registerActionMapping({
  purchase: 12345, // Error: expected string
});

Initialization Guard

The SDK prevents double initialization. If you call init() more than once, the second call is ignored and a warning is logged:

javascript
await FanFestSDK.init({ clientId: "your-web-app-id" });
await FanFestSDK.init({ clientId: "other-id" }); // Ignored, logs warning

This is safe to call in application code that might run multiple times (e.g., in a React useEffect or Vue onMounted hook).

Host-Relayed Navigator Events

SDK 1.9.0+

These features require SDK version 1.9.0 or later on the host page. Earlier versions don't run the navigator handler — the embed still works but share/copy/OAuth from inside the iframe behave as if there's no host. Update your <script src="..."> to point at 1.9.0/embed-sdk.js (or latest/embed-sdk.js).

When the FanFest experience runs inside your iframe, several browser APIs scope to the iframe's origin instead of yours:

APIWithout the relayWith the relay
window.locationReturns the iframe URL (fanfest.vip/...)Returns your host page URL
navigator.share"Share from fanfest.vip" sheet"Share from your-domain.com"
navigator.clipboardPermission-denied on SafariWrites from your origin
window.location.assignNavigates the iframe onlyNavigates the host page
OAuth popupSevered window.opener on Safari + iframeWorks (popup is host-sibling)

The SDK transparently relays these calls through the host page so the embed feels like a native part of your site. Nothing on your side is required — just keep the SDK version current. Existing host integrations continue to work unchanged.

What this fixes

  • Share / copy links point at your-domain.com/path?fanfestEvent=show/... instead of fanfest.vip/.... Users who paste the link land back on your site, not on FanFest's standalone domain.
  • Native share sheet shows your site's title and origin.
  • Clipboard write works on Safari (Clipboard API permission is scoped to the top-level origin; iframe writes were rejected).
  • Sign-in with Google works on Safari + iframe. Without the relay, Safari's COOP severs window.opener and the auth-success postMessage from the OAuth popup never lands.

What you need to do

Nothing — the relay is automatic once SDK 1.9.0+ is loaded on your host page. The events are listed below for reference if you want to inspect or proxy them in your own code.

Protocol overview

The SDK installs a message listener on window and responds to a small set of ff-<event>:ask messages from the iframe:

EventPurposeIframe-side fallback if missing
ff-locationReturns host window.locationIframe window.location (legacy behaviour)
ff-shareCalls navigator.share on the hostnavigator.share from inside the iframe
ff-clipboardCalls navigator.clipboard.writeText on the hostnavigator.clipboard from inside the iframe
ff-navigateCalls window.location.assign on the hostIframe-only navigation
ff-oauthOpens the provider login popup from host contextNone — Safari + iframe sign-in is broken without this

All asks carry a requestId and a protocol version (v: 1); responses echo both. The SDK pins targetOrigin to the iframe's URL origin on replies and rejects asks from event.source windows that aren't tracked iframes.

If you embed multiple FanFest channels on the same page, the SDK matches each ask to the originating iframe and replies only to that one. There's no cross-talk between embeds.

Security Model

The embed uses several security mechanisms:

  • Origin validation — PostMessage events are only accepted from the configured appOrigin. Replies on the host-relayed navigator protocol are pinned to each iframe's src origin.
  • Iframe sandbox — The embed iframe runs with a restricted sandbox: allow-same-origin, allow-scripts, allow-forms, allow-modals, allow-popups
  • Function bindinghostLoginFn and hostRewardsFn are bound to the window object to prevent exposing your function scope to the embed
  • Schema validation — All incoming PostMessage data is validated against a strict schema before processing
  • Protocol versioning — Navigator events carry a v field; messages with an unknown version are dropped, so a partial deploy fails closed

Next Steps

Released under the MIT License.