@pythiasport/betting-widget
v1.1.5
Published
TypeScript SDK for embedding Pythia betting widget
Maintainers
Readme
@pythiasport/betting-widget
A modern, framework-agnostic TypeScript SDK for embedding the Pythia betting widget into any web application.
Features
- ✅ TypeScript-first with full type safety
- 🎯 Framework-agnostic - works with React, Vue, Angular, or vanilla JS
- 🎨 Theme support with dark/light modes
- 📡 Event-driven architecture using EventEmitter pattern
- 🔗 Deep-link navigation with type-safe route builders
- 💾 Optional route restoration across widget re-initialization
- 🔒 Type-safe APIs with comprehensive JSDoc documentation
- 📦 Multiple build formats - ESM, CommonJS, and UMD/IIFE
- 🎪 Zero dependencies in runtime
Installation
npm install @pythiasport/betting-widgetyarn add @pythiasport/betting-widgetpnpm add @pythiasport/betting-widgetQuick Start
1. Add container to your HTML
<div id="betting-widget-container"></div>2. Initialize the SDK
import { PythiaSDK } from '@pythiasport/betting-widget';
const sdk = new PythiaSDK();
// Listen to events
sdk.on('betslip:open', () => {
console.log('Betslip opened!');
});
sdk.on('ready', () => {
console.log('Widget is ready!');
});
// Initialize with configuration
sdk.init({
tenantId: 'your-tenant-id',
jwtToken: 'user-jwt-token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '',
theme: 'dark',
style: 'width: 100%; height: 100%; border: none;',
clientIdForWidgets: 'your-client-id-for-widgets',
onLoginRequest: () => {
// Handle login request
console.log('User needs to login');
}
});Framework Integration
React
import { useEffect, useRef } from 'react';
import { PythiaSDK } from '@pythiasport/betting-widget';
function BettingWidget() {
const sdkRef = useRef<PythiaSDK | null>(null);
useEffect(() => {
const sdk = new PythiaSDK();
sdkRef.current = sdk;
sdk.on('betslip:open', () => {
console.log('Betslip opened');
});
sdk.init({
tenantId: 'your-tenant-id',
jwtToken: 'user-token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '',
theme: 'dark',
clientIdForWidgets: 'your-client-id-for-widgets'
});
return () => {
sdk.destroy();
};
}, []);
const handleThemeToggle = () => {
sdkRef.current?.setTheme('light');
};
return (
<div>
<button onClick={handleThemeToggle}>Toggle Theme</button>
<div id="betting-widget-container" />
</div>
);
}Vue 3
<template>
<div>
<button @click="toggleTheme">Toggle Theme</button>
<div id="betting-widget-container"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { PythiaSDK } from '@pythiasport/betting-widget';
const sdk = ref<PythiaSDK | null>(null);
onMounted(() => {
const instance = new PythiaSDK();
sdk.value = instance;
instance.on('betslip:open', () => {
console.log('Betslip opened');
});
instance.init({
tenantId: 'your-tenant-id',
jwtToken: 'user-token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '',
theme: 'dark',
clientIdForWidgets: 'your-client-id-for-widgets'
});
});
onUnmounted(() => {
sdk.value?.destroy();
});
const toggleTheme = () => {
sdk.value?.setTheme('light');
};
</script>Angular
import { Component, OnInit, OnDestroy } from '@angular/core';
import { PythiaSDK } from '@pythiasport/betting-widget';
@Component({
selector: 'app-betting-widget',
template: `
<div>
<button (click)="toggleTheme()">Toggle Theme</button>
<div id="betting-widget-container"></div>
</div>
`
})
export class BettingWidgetComponent implements OnInit, OnDestroy {
private sdk: PythiaSDK | null = null;
ngOnInit() {
this.sdk = new PythiaSDK();
this.sdk.on('betslip:open', () => {
console.log('Betslip opened');
});
this.sdk.init({
tenantId: 'your-tenant-id',
jwtToken: 'user-token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '',
theme: 'dark',
clientIdForWidgets: 'your-client-id-for-widgets'
});
}
ngOnDestroy() {
this.sdk?.destroy();
}
toggleTheme() {
this.sdk?.setTheme('light');
}
}Vanilla JavaScript (UMD)
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/@pythiasport/betting-widget@latest/dist/index.global.js"></script>
</head>
<body>
<div id="betting-widget-container"></div>
<script>
const sdk = new PythiaSDK.PythiaSDK();
sdk.on('betslip:open', function() {
console.log('Betslip opened');
});
sdk.init({
tenantId: 'your-tenant-id',
jwtToken: 'user-token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '$',
theme: 'dark',
clientIdForWidgets: 'your-client-id-for-widgets'
});
</script>
</body>
</html>API Reference
Constructor
const sdk = new PythiaSDK();Methods
init(options: PythiaSDKOptions): void
Initializes the SDK with configuration options.
Parameters:
tenantId(string, required) - Unique identifier for the tenantjwtToken(string, required) - JWT token for authenticationuserId(string, required) - User identifiercurrencyCode(string, required) - ISO currency code (e.g., 'USD', 'EUR')currencySymbol(string, required) - Currency symbol (e.g., '$', '€')style(string, optional) - CSS styles for the iframetheme('light' | 'dark', optional) - Initial theme (default is 'dark')bettingAppUrl(string, optional) - URL of the betting applicationinitialRoute(string, optional) - Initial relative iframe route (e.g./horses/full-race?race=2715350)restoreRoute(boolean, optional) - Store iframe route changes and restore the last route on the next initialization (default isfalse)routeStorageKey(string, optional) - Custom storage key used by route restorationrouteStorage(Storage, optional) - Storage implementation used by route restoration (default issessionStorage)oddsFormat('decimal' | 'fractional' | 'moneyline', optional) - a parameter that defines which price format should be used (default is 'decimal').language(optional) - the interface language, specified as a country/language code (e.g. 'en', 'de'; default is 'en').timeFormat('24h' | '12h', optional) - the time display format (default is '24h').userLocation('string', optional) - The user’s location in country code format is used to display only the streams available for that specific location. If this parameter is not provided, all streams will be displayed, but not all of them will be playable. (e.g. 'US', 'DE').clientIdForWidgets('string', optional) - Client ID required for ARM Standalone Components to function.onLoginRequest(function, optional) - Callback when login is requested.onRouteChange(function, optional) - Callback invoked when the iframe route changesonScrollToPosition(function, optional) - Callback used to handle iframe scroll requests in a custom parent scroll container
updateUserInfo(userInfo: UserInfo): void
Updates user authentication information after login/logout.
sdk.updateUserInfo({
jwtToken: 'new-jwt-token',
userId: 'user-123',
currencyCode: 'EUR',
currencySymbol: '€'
});It can be used to change the price type and the time format.
sdk.updateUserInfo({
timeFormat: '12h',
oddsFormat: 'moneyline'
});setTheme(theme: 'light' | 'dark'): void
Changes the widget theme.
sdk.setTheme('dark');setLanguage(language: string): void
Changes the widget language.
sdk.setLanguage('en');notifyScrollChange(scrollY: number, scrollX?: number): void
Manually notifies the iframe of scroll position changes.
sdk.notifyScrollChange(window.scrollY);notifyParentHeight(height: number): void
Manually notifies the iframe of the parent viewport height. This is useful when the widget is rendered inside a custom scroll container.
sdk.notifyParentHeight(scrollContainer.clientHeight);notifyIframePosition(position: number): void
Manually notifies the iframe of its absolute top position in the parent page or custom scroll container.
sdk.notifyIframePosition(iframeOffset);navigateToRoute(route: string, options?: RouteNavigationOptions): boolean
Navigates the iframe to a safe relative route. Returns false for absolute, protocol-relative, or otherwise invalid routes.
sdk.navigateToRoute('/horses/full-race?race=2715350');Deep-link route helpers
Build a route without navigating:
sdk.buildSportRoute('horses');
sdk.buildCompetitionGroupRoute({ sport: 'horses', competitionGroupId: 1 });
sdk.buildFullRaceRoute({ sport: 'horses', competitionGroupId: 1, competitionId: 219 });
sdk.buildRaceRoute({ sport: 'horses', raceId: 2715350 });Build and open a route:
sdk.openSport('horses');
sdk.openCompetitionGroup({ sport: 'horses', competitionGroupId: 1 });
sdk.openFullRace({ sport: 'horses', competitionGroupId: 1, competitionId: 219 });
sdk.openRace({ sport: 'horses', raceId: 2715350 });reload(): void
Reloads the managed iframe without changing its current URL.
sdk.reload();getStoredRoute(): string | null
Returns the route currently stored for route restoration.
const storedRoute = sdk.getStoredRoute();clearStoredRoute(): void
Removes the stored route.
sdk.clearStoredRoute();setBetslipButtonOffset(offset: BetslipButtonOffset): void
Sets the offset for betslip floating button positioning.
sdk.setBetslipButtonOffset({
top: 80, // pixels from top
bottom: 20 // pixels from bottom
});destroy(): void
Destroys the SDK instance and cleans up all resources.
sdk.destroy();getIsInitialized(): boolean
Checks if the SDK is initialized.
if (sdk.getIsInitialized()) {
// SDK is ready
}Events
The SDK uses an EventEmitter pattern for all events. Use on(), once(), and off() methods.
Event Types
betslip:open- Emitted when the betslip is openedbetslip:close- Emitted when the betslip is closedlogin:request- Emitted when the iframe requests loginscroll:up- Emitted when the iframe requests scroll to topscroll:to- Emitted when the iframe requests scroll to a specific parent-page positionheight:change- Emitted when iframe height changes (receives height string)ready- Emitted when the iframe is loaded and readyroute:change- Emitted when the iframe reports its current routeroute:navigate- Emitted when the SDK starts route navigation
Event Methods
// Listen to an event
sdk.on('betslip:open', () => {
console.log('Betslip opened');
});
// Listen once
sdk.once('ready', () => {
console.log('Widget ready - this will only fire once');
});
// Remove listener
const handler = () => console.log('Betslip closed');
sdk.on('betslip:close', handler);
sdk.off('betslip:close', handler);
// Remove all listeners for an event
sdk.removeAllListeners('betslip:open');
// Remove all listeners
sdk.removeAllListeners();
// Get listener count
const count = sdk.listenerCount('betslip:open');Deep Links and Route Restore
The SDK can open a specific Racing screen when the widget is initialized, navigate an existing iframe, build operator links, and restore the last iframe route.
A deep-link route is the path used inside the iframe. It is not automatically a public URL on the operator's domain. If the operator needs a shareable public URL, the operator application should store the iframe route in its own URL and pass it back to the SDK.
Supported routes
| Sport | Sport home | Full race |
|-------|------------|-----------|
| Horse Racing | /horses | /horses/full-race |
| Greyhound Racing | /greyhounds | /greyhounds/full-race |
SDK routes must:
- be relative;
- start with
/; - not start with
//; - not include an external origin.
# Valid
/horses/full-race?race=2715350
# Invalid
https://arm-ui.example.com/horses/full-race?race=2715350
//arm-ui.example.com/horsesThis rule applies to SDK navigation values. Backoffice banner buttons use absolute URLs because the banner form validates them as URLs; their behavior is described under Banner links inside the iframe.
Deep-link parameters
| Parameter | Description |
|-----------|-------------|
| competitionGroupId | Positive competition group ID. In the current Racing UI this normally represents a country |
| competitionId | Positive competition/meeting ID |
| competitionStartTime | Competition start time in ISO 8601 format with a timezone |
| race | Positive event/race ID |
| scrollToTarget | Requests parent-page scrolling after a sport-home target is resolved. Supported values are 1 and true |
Use the exact competition start time returned by the backend. The recommended format is:
2026-08-13T21:00:00.000ZAn explicit UTC offset is also supported:
2026-08-13T23:00:00+02:00The timestamp is parsed with its timezone and matched to the browser-local today, tomorrow, or day after tomorrow tab. Values without a timezone are not supported.
Builder methods use URLSearchParams, so generated timestamps are URL-encoded. For example, : may appear as %3A. This is expected and represents the same value.
Each query parameter must be provided only once. Duplicate parameters are not supported and their behavior is not guaranteed.
Open a route on initialization
Use initialRoute to open a deep link when the iframe is first created:
const sdk = new PythiaSDK();
sdk.init({
tenantId: 'tenant-123',
jwtToken: 'token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '$',
bettingAppUrl: 'https://arm-ui.example.com',
initialRoute: '/horses/full-race?race=2715350'
});initialRoute accepts the same safe relative routes as navigateToRoute().
Navigate an existing iframe
Use navigateToRoute() when a route is already available:
const didNavigate = sdk.navigateToRoute(
'/horses/full-race?race=2715350'
);
if (!didNavigate) {
console.error('Invalid iframe route');
}The method returns true when the route is accepted and false when it is invalid. Navigation updates the iframe src, so the iframe reloads.
Build and open operator links
Prefer SDK builders over manually concatenating query parameters. Builders validate numeric IDs, encode parameter values, and use the supported parameter names.
Sport home
const route = sdk.buildSportRoute('horses');
// /horses
sdk.openSport('horses');Competition group on sport home
const route = sdk.buildCompetitionGroupRoute({
sport: 'horses',
competitionGroupId: 1,
competitionStartTime: '2026-08-13T21:00:00.000Z'
});
sdk.openCompetitionGroup({
sport: 'horses',
competitionGroupId: 1,
competitionStartTime: '2026-08-13T21:00:00.000Z'
});Equivalent human-readable route:
/horses?competitionGroupId=1&competitionStartTime=2026-08-13T21:00:00.000Z&scrollToTarget=1buildCompetitionGroupRoute() and openCompetitionGroup() add scrollToTarget=1 by default. Disable it when parent-page scrolling is not required:
sdk.openCompetitionGroup({
sport: 'horses',
competitionGroupId: 1,
competitionStartTime: '2026-08-13T21:00:00.000Z',
scrollToTarget: false
});Competition in Full Race
const route = sdk.buildFullRaceRoute({
sport: 'horses',
competitionGroupId: 1,
competitionId: 219,
competitionStartTime: '2026-08-13T21:00:00.000Z'
});
sdk.openFullRace({
sport: 'horses',
competitionGroupId: 1,
competitionId: 219,
competitionStartTime: '2026-08-13T21:00:00.000Z'
});Equivalent human-readable route:
/horses/full-race?competitionGroupId=1&competitionId=219&competitionStartTime=2026-08-13T21:00:00.000ZThis is the recommended format for a competition deep link because it identifies the date, group, and competition explicitly.
Specific race
const route = sdk.buildRaceRoute({
sport: 'horses',
raceId: 2715350
});
// /horses/full-race?race=2715350
const didOpen = sdk.openRace({
sport: 'horses',
raceId: 2715350
});buildRaceRoute() returns null, and openRace() returns false, when raceId is invalid.
Do not add competitionGroupId, competitionId, competitionStartTime, or sportId to a race link. The iframe looks up the event by raceId and resolves the actual sport, date, group, and competition from the backend. The sport segment in the supplied route is used as the fallback destination if the race cannot be resolved.
Connect a link on the operator page
The operator can build the iframe route once and open it from any UI control:
const route = sdk.buildFullRaceRoute({
sport: 'horses',
competitionGroupId: 1,
competitionId: 219,
competitionStartTime: '2026-08-13T21:00:00.000Z'
});
document.querySelector('#open-race')?.addEventListener('click', () => {
sdk.navigateToRoute(route);
});For a shareable operator URL, encode the iframe route in the operator's own URL:
const operatorUrl = new URL('/racing', window.location.origin);
operatorUrl.searchParams.set('iframeRoute', route);Read it back when the operator page initializes the SDK:
const iframeRoute = new URL(window.location.href).searchParams.get('iframeRoute');
sdk.init({
tenantId: 'tenant-123',
jwtToken: 'token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '$',
bettingAppUrl: 'https://arm-ui.example.com',
initialRoute: iframeRoute ?? undefined
});Restore the last iframe route
Route restoration is opt-in and disabled by default:
sdk.init({
tenantId: 'tenant-123',
jwtToken: 'token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '$',
bettingAppUrl: 'https://arm-ui.example.com',
restoreRoute: true
});When restoreRoute is enabled, the SDK:
- Receives route changes from the iframe.
- Stores the current safe relative route.
- Uses that route the next time the SDK is initialized.
The route selection priority during init() is:
- A valid
initialRoute. - The stored route when
restoreRouteis enabled. - The base
bettingAppUrl.
Do not always pass a static initialRoute: '/horses' when route restoration is expected. A valid initialRoute always takes priority over the stored route.
An optional campaign route can be combined with restoration:
sdk.init({
// Other required options...
initialRoute: campaignRoute || undefined,
restoreRoute: true
});The campaign route wins when present. Otherwise, the SDK restores the last stored route.
Route storage
The default storage is sessionStorage, so the route survives a page reload in the same tab but is normally removed when the tab is closed.
The default key is scoped by tenant and base iframe URL:
pythia:last-route:<tenantId>:<bettingAppUrl>Use a custom key when multiple widget placements share the same tenant and iframe URL:
sdk.init({
// Other required options...
restoreRoute: true,
routeStorageKey: 'sportsbook:homepage:racing-route'
});Use localStorage when the route must survive closing the tab:
sdk.init({
// Other required options...
restoreRoute: true,
routeStorage: window.localStorage,
routeStorageKey: 'sportsbook:racing:last-route'
});Storage failures, including restricted embedded or private-browsing contexts, do not break SDK initialization. Route restoration is simply unavailable for that session.
Read or clear the route after initialization:
const storedRoute = sdk.getStoredRoute();
sdk.clearStoredRoute();Clearing the stored route is recommended on logout or when the operator intentionally resets the Racing experience.
navigateToRoute(route, { store: false }) skips the immediate storage write performed by navigation. If restoreRoute is enabled, the resolved route subsequently reported by the iframe is still stored.
Route events
sdk.on('route:navigate', route => {
console.log('Navigation started by the SDK:', route);
});
sdk.on('route:change', route => {
console.log('Current iframe route:', route);
});route:navigateis emitted when the SDK accepts a route throughnavigateToRoute()or anopen...()method.route:changeis emitted when the iframe reports its current route, including internal user navigation, normalized route parameters, redirects, and fallback routes.
The same route-change notification is available as an initialization callback:
sdk.init({
// Other required options...
onRouteChange(route) {
console.log('Iframe route changed:', route);
}
});onRouteChange and the route:change event work even when restoreRoute is disabled. Storage writes only occur when restoration is enabled.
Scroll to a deep-link target
A sport-home competition group link adds scrollToTarget=1 by default. After the group is resolved, the iframe requests that the parent page scroll to the matching competition or first meeting and then removes scrollToTarget from the route.
By default, the SDK handles this request with window.scrollTo().
Use onScrollToPosition when the iframe is inside a custom scroll container:
sdk.init({
// Other required options...
onScrollToPosition(_top, context) {
scrollContainer.scrollTo({
top: iframeOffset + context.iframeContentTop,
behavior: context.behavior
});
}
});For custom containers or fullscreen layouts, keep the iframe informed about the parent viewport:
sdk.notifyParentHeight(scrollContainer.clientHeight);
sdk.notifyIframePosition(iframeOffset);
sdk.notifyScrollChange(scrollContainer.scrollTop);The scroll:to event is also emitted with the absolute parent-page target:
sdk.on('scroll:to', top => {
console.log('Requested scroll target:', top);
});Resolution and fallback behavior
| Route or condition | Behavior |
|--------------------|----------|
| /horses/full-race | Opens the default date and selects the first available group, competition, and race |
| Full Race with only competitionStartTime | Selects the matching date tab and opens its default Full Race state |
| Full Race with only competitionGroupId | Selects the group on the default date and opens its first available competition/race |
| Full Race with group and date | Selects the date and group, then opens the first available competition/race |
| Full Race with only competitionId | Searches the default group on the current and other available date tabs |
| Invalid group in Full Race | Redirects to the corresponding sport home |
| Invalid competition | Redirects to the corresponding sport home |
| Valid race | Looks up the event and resolves its actual sport, date, group, and competition |
| Invalid race | Redirects to /horses or /greyhounds, based on the supplied route |
| Sport home with a date | Selects the matching date and displays the default group/meetings |
| Sport home with group and date | Selects the date and group and optionally requests parent scrolling |
| Invalid group on sport home | Removes invalid target parameters and displays the default state for the selected date |
| Invalid or unavailable date | Removes competitionStartTime and continues with the default date |
| Unknown route | Redirects to /horses |
The iframe currently supports only today, tomorrow, and day after tomorrow. A timestamp outside those tabs falls back to the default date. A race whose actual start date is outside the available tabs cannot be opened and falls back to the appropriate sport home.
Banner links inside the iframe
The Backoffice banner Button URL must be an absolute URL. For internal Racing navigation, its origin must match the iframe URL configured through bettingAppUrl:
https://arm-ui.example.com/horses/full-race?race=2715350When the protocol, hostname, and port match the running iframe and the path is a supported Horse Racing or Greyhound Racing route, the iframe strips the origin and navigates through its router. The path, query parameters, and hash are preserved, and the iframe is not reloaded.
A URL with another origin, including a URL for a different environment, opens in a new browser tab. A same-origin URL outside the supported Racing routes is also treated as an external destination:
https://example.com/promoBanner URLs and SDK routes are different inputs. Do not pass the absolute banner URL to initialRoute, navigateToRoute, or an SDK route helper; those APIs continue to require a relative route such as /horses/full-race?race=2715350.
Backward compatibility
All new initialization options are optional. Existing integrations that do not enable deep links or route restoration continue to use the base iframe URL as before.
Older SDK versions do not expose the new route builders, navigation methods, route events, or restoration storage. They can continue the basic iframe integration, but restoreRoute and parent-page scrolling through scrollToTarget require the updated SDK.
TypeScript Support
The SDK is written in TypeScript and provides full type definitions:
import { PythiaSDK, PythiaSDKOptions, UserInfo, ThemeType } from '@pythiasport/betting-widget';
const options: PythiaSDKOptions = {
tenantId: 'tenant-123',
jwtToken: 'token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '',
theme: 'dark',
clientIdForWidgets: 'your-client-id-for-widgets'
};
const sdk = new PythiaSDK();
sdk.init(options);Deep-link and route-navigation types are also exported:
import type {
RacingSport,
RouteNavigationOptions,
CompetitionGroupDeepLinkOptions,
FullRaceDeepLinkOptions,
RaceDeepLinkOptions,
ScrollToPositionContext
} from '@pythiasport/betting-widget';Legacy Version
For projects that require compatibility with older browsers, a legacy (umd) version of the SDK is available. To use it you can include it via CDN:
<script src="https://unpkg.com/@pythiasport/betting-widget@latest/dist/legacy.global.js"></script>
<script>
window.pythiaSDK.init({
tenantId: 'your-tenant-id',
jwtToken: 'user-token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '$',
theme: 'dark',
onLoginRequest: () => {
// Handle login request
console.log('User needs to login');
}
})
</script>Deprecated: The legacy API is maintained for backward compatibility. It does not expose the modern deep-link helpers, runtime route navigation, route events, or route-storage controls documented above. Use the modern
PythiaSDKAPI for new integrations.
Migration Guide
This guide helps you migrate from the original JavaScript SDK to the new TypeScript version.
Two Options
You have two options when upgrading to the new SDK:
- Use the Legacy API - Zero code changes, backward compatible
- Migrate to Modern API - Better features, type safety, and developer experience
Option 1: Legacy API (Zero Changes) ✨
What You Need to Do
Simply replace your script tag with the new legacy build:
Before:
<script src="path/to/old-pythia-sdk.js"></script>After:
<script src="https://unpkg.com/@pythiasport/betting-widget@latest/dist/legacy.global.js"></script>That's it! Your existing code will work without any changes:
// All your existing code works exactly as before
window.pythiaSDK.onBetslipOpen = function() {
console.log('Betslip opened');
};
window.pythiaSDK.init({
tenantId: 'your-tenant-id',
jwtToken: 'token',
userId: 'user-123',
currencyCode: 'USD',
currencySymbol: '$',
theme: 'dark'
});
window.pythiaSDK.setTheme('light');
window.pythiaSDK.updateUserInfo({ /* ... */ });What's Different (Behind the Scenes)
- ✅ Modern TypeScript implementation
- ✅ Better performance and optimization
- ✅ Improved error handling
- ✅ Automatic setup of internal listeners
- ⚠️ Some internal methods log deprecation warnings (but still work)
Deprecation Warnings
You might see console warnings for these methods (they still work, but are no longer necessary):
insertIframe()- Auto-handled on init()sendAuthDataToIframe()- Auto-sent on init()listenToIframeEvents()- Auto-setup on init()sendInitialTheme()- Auto-sent on init()syncParentScroll()- Auto-setup on init()syncParentHeight()- Auto-setup on init()sendIframeTopOffset()- Auto-sent on init()
You can safely remove calls to these methods if you want to clean up your code.
Option 2: Migrate to Modern API 🚀
Benefits
- ✅ Multiple event listeners per event
- ✅ Type safety with TypeScript
- ✅ Better IDE autocomplete
- ✅ One-time listeners with
once() - ✅ Cleaner event management
- ✅ Tree-shakeable imports
- ✅ Modern module system
Step-by-Step Migration
Step 1: Install via NPM
npm install @pythiasport/betting-widgetStep 2: Import the SDK
Before (Global Script):
<script src="path/to/pythia-sdk.js"></script>After (ES Module):
import { PythiaSDK } from '@pythiasport/betting-widget';Step 3: Create Instance
Before:
// Used global window.pythiaSDK
window.pythiaSDK.init({ /* ... */ });After:
// Create your own instance
const sdk = new PythiaSDK();
sdk.init({ /* ... */ });Step 4: Replace Callback Properties with Events
Before:
window.pythiaSDK.onBetslipOpen = function() {
console.log('Opened');
};
window.pythiaSDK.onBetslipClose = function() {
console.log('Closed');
};After:
sdk.on('betslip:open', () => {
console.log('Opened');
});
sdk.on('betslip:close', () => {
console.log('Closed');
});Step 5: Update Method Calls
Most methods stay the same, just called on your instance:
Before:
window.pythiaSDK.setTheme('light');
window.pythiaSDK.updateUserInfo({ /* ... */ });
window.pythiaSDK.setBetslipButtonOffset({ top: 80 });
window.pythiaSDK.scrollChanged(window.scrollY);After:
sdk.setTheme('light');
sdk.updateUserInfo({ /* ... */ });
sdk.setBetslipButtonOffset({ top: 80 });
sdk.notifyScrollChange(window.scrollY); // Renamed for clarityStep 6: Remove Unnecessary Method Calls
These methods are no longer needed (handled automatically):
Before:
window.pythiaSDK.init({ /* ... */ });
window.pythiaSDK.insertIframe(); // Remove this
window.pythiaSDK.sendAuthDataToIframe(); // Remove this
window.pythiaSDK.listenToIframeEvents(); // Remove this
window.pythiaSDK.sendInitialTheme(); // Remove thisAfter:
sdk.init({ /* ... */ });
// That's it! Everything else is automaticStep 7: Add Cleanup (Recommended)
Before:
// No cleanup mechanismAfter:
// Clean up when done (e.g., component unmount)
sdk.destroy();Complete Example: Before & After
Before (Legacy JavaScript)
<!DOCTYPE html>
<html>
<head>
<script src="pythia-sdk.js"></script>
</head>
<body>
<div id="betting-widget-container"></div>
<script>
window.pythiaSDK.onBetslipOpen = function() {
console.log('Betslip opened');
};
window.pythiaSDK.onBetslipClose = function() {
console.log('Betslip closed');
};
window.pythiaSDK.init({
tenantId: 'tenant-123',
jwtToken: 'token-abc',
userId: 'user-456',
currencyCode: 'USD',
currencySymbol: '$',
theme: 'dark',
onLoginRequest: function() {
alert('Please login');
}
});
// Later...
window.pythiaSDK.setTheme('light');
window.pythiaSDK.scrollChanged(window.scrollY);
</script>
</body>
</html>After (Modern TypeScript)
import { PythiaSDK } from '@pythiasport/betting-widget';
// Create instance
const sdk = new PythiaSDK();
// Set up event listeners
sdk.on('betslip:open', () => {
console.log('Betslip opened');
});
sdk.on('betslip:close', () => {
console.log('Betslip closed');
});
// Initialize
sdk.init({
tenantId: 'tenant-123',
jwtToken: 'token-abc',
userId: 'user-456',
currencyCode: 'USD',
currencySymbol: '$',
theme: 'dark',
onLoginRequest: () => {
alert('Please login');
}
});
// Later...
sdk.setTheme('light');
sdk.notifyScrollChange(window.scrollY);
// Clean up when done
// sdk.destroy();Framework-Specific Examples
React
import { useEffect, useRef } from 'react';
import { PythiaSDK } from '@pythiasport/betting-widget';
function BettingWidget() {
const sdkRef = useRef<PythiaSDK | null>(null);
useEffect(() => {
const sdk = new PythiaSDK();
sdkRef.current = sdk;
// Events
sdk.on('betslip:open', () => console.log('Opened'));
sdk.on('betslip:close', () => console.log('Closed'));
// Initialize
sdk.init({
tenantId: 'tenant-123',
jwtToken: 'token-abc',
userId: 'user-456',
currencyCode: 'USD',
currencySymbol: '$',
theme: 'dark',
});
// Cleanup
return () => {
sdk.destroy();
};
}, []);
return <div id="betting-widget-container" />;
}Vue 3
<template>
<div id="betting-widget-container"></div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { PythiaSDK } from '@pythiasport/betting-widget';
const sdk = ref<PythiaSDK | null>(null);
onMounted(() => {
const instance = new PythiaSDK();
sdk.value = instance;
// Events
instance.on('betslip:open', () => console.log('Opened'));
instance.on('betslip:close', () => console.log('Closed'));
// Initialize
instance.init({
tenantId: 'tenant-123',
jwtToken: 'token-abc',
userId: 'user-456',
currencyCode: 'USD',
currencySymbol: '$',
theme: 'dark',
});
});
onUnmounted(() => {
sdk.value?.destroy();
});
</script>API Changes Summary
| Legacy API | Modern API | Notes |
|------------|------------|-------|
| window.pythiaSDK.onBetslipOpen = fn | sdk.on('betslip:open', fn) | Event-based |
| window.pythiaSDK.onBetslipClose = fn | sdk.on('betslip:close', fn) | Event-based |
| window.pythiaSDK.init(opts) | sdk.init(opts) | Same |
| window.pythiaSDK.setTheme(theme) | sdk.setTheme(theme) | Same |
| window.pythiaSDK.setLanguage(language) | sdk.setLanguage(language) | Same |
| window.pythiaSDK.updateUserInfo(info) | sdk.updateUserInfo(info) | Same |
| window.pythiaSDK.setBetslipButtonOffset(o) | sdk.setBetslipButtonOffset(o) | Same |
| window.pythiaSDK.scrollChanged(y, x) | sdk.notifyScrollChange(y, x) | Renamed |
| window.pythiaSDK.getIframe() | Internal | Not exposed |
| window.pythiaSDK.insertIframe() | Auto-handled | Not needed |
| window.pythiaSDK.config | Internal | Not exposed |
| window.pythiaSDK.$iframe | Internal | Not exposed |
| - | sdk.destroy() | New method |
| - | sdk.on(event, fn) | New method |
| - | sdk.once(event, fn) | New method |
| - | sdk.off(event, fn) | New method |
| - | sdk.navigateToRoute(route) | Modern API only |
| - | sdk.openSport(sport) | Modern API only |
| - | sdk.openCompetitionGroup(options) | Modern API only |
| - | sdk.openFullRace(options) | Modern API only |
| - | sdk.openRace(options) | Modern API only |
| - | sdk.getStoredRoute() | Modern API only |
| - | sdk.clearStoredRoute() | Modern API only |
Event Names
| Legacy Callback | Modern Event | Description |
|----------------|--------------|-------------|
| onBetslipOpen | 'betslip:open' | Betslip opened |
| onBetslipClose | 'betslip:close' | Betslip closed |
| onLoginRequest callback | 'login:request' | Login requested |
| - | 'scroll:up' | Scroll to top requested |
| - | 'scroll:to' | Scroll to deep-link target requested |
| - | 'height:change' | Iframe height changed |
| - | 'ready' | Widget loaded |
| - | 'route:change' | Iframe route changed |
| - | 'route:navigate' | SDK route navigation started |
TypeScript Support
Add types to your project:
import type {
PythiaSDKOptions,
UserInfo,
ThemeType,
BetslipButtonOffset
} from '@pythiasport/betting-widget';
const options: PythiaSDKOptions = {
tenantId: 'tenant-123',
jwtToken: 'token',
userId: 'user-456',
currencyCode: 'USD',
currencySymbol: '$',
theme: 'dark'
};Testing Your Migration
- Keep the old implementation running in parallel for testing
- Test all events - make sure they fire as expected
- Test theme switching - verify dark/light mode works
- Test user updates - ensure auth updates work
- Test scroll sync - verify scroll position updates
- Test cleanup - ensure
destroy()works properly
Rollback Plan
If you need to rollback, you can always switch back to the legacy API:
<!-- Rollback: use legacy build -->
<script src="https://unpkg.com/@pythiasport/betting-widget@latest/dist/legacy.global.js"></script>Your original code will work immediately.
Recommended Migration Path
- Week 1-2: Install new SDK, use legacy API (zero changes)
- Week 3-4: Migrate one component/page to modern API
- Week 5-6: Gradually migrate remaining components
- Week 7+: Complete migration, remove legacy API
This gradual approach minimizes risk and allows thorough testing.
Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
License
MIT
