npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pythiasport/betting-widget

v1.1.5

Published

TypeScript SDK for embedding Pythia betting widget

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-widget
yarn add @pythiasport/betting-widget
pnpm add @pythiasport/betting-widget

Quick 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 tenant
  • jwtToken (string, required) - JWT token for authentication
  • userId (string, required) - User identifier
  • currencyCode (string, required) - ISO currency code (e.g., 'USD', 'EUR')
  • currencySymbol (string, required) - Currency symbol (e.g., '$', '€')
  • style (string, optional) - CSS styles for the iframe
  • theme ('light' | 'dark', optional) - Initial theme (default is 'dark')
  • bettingAppUrl (string, optional) - URL of the betting application
  • initialRoute (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 is false)
  • routeStorageKey (string, optional) - Custom storage key used by route restoration
  • routeStorage (Storage, optional) - Storage implementation used by route restoration (default is sessionStorage)
  • 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 changes
  • onScrollToPosition (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 opened
  • betslip:close - Emitted when the betslip is closed
  • login:request - Emitted when the iframe requests login
  • scroll:up - Emitted when the iframe requests scroll to top
  • scroll:to - Emitted when the iframe requests scroll to a specific parent-page position
  • height:change - Emitted when iframe height changes (receives height string)
  • ready - Emitted when the iframe is loaded and ready
  • route:change - Emitted when the iframe reports its current route
  • route: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/horses

This 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.000Z

An explicit UTC offset is also supported:

2026-08-13T23:00:00+02:00

The 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=1

buildCompetitionGroupRoute() 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.000Z

This 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:

  1. Receives route changes from the iframe.
  2. Stores the current safe relative route.
  3. Uses that route the next time the SDK is initialized.

The route selection priority during init() is:

  1. A valid initialRoute.
  2. The stored route when restoreRoute is enabled.
  3. 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:navigate is emitted when the SDK accepts a route through navigateToRoute() or an open...() method.
  • route:change is 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=2715350

When 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/promo

Banner 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 PythiaSDK API 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:

  1. Use the Legacy API - Zero code changes, backward compatible
  2. 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-widget
Step 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 clarity
Step 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 this

After:

sdk.init({ /* ... */ });
// That's it! Everything else is automatic
Step 7: Add Cleanup (Recommended)

Before:

// No cleanup mechanism

After:

// 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

  1. Keep the old implementation running in parallel for testing
  2. Test all events - make sure they fire as expected
  3. Test theme switching - verify dark/light mode works
  4. Test user updates - ensure auth updates work
  5. Test scroll sync - verify scroll position updates
  6. 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

  1. Week 1-2: Install new SDK, use legacy API (zero changes)
  2. Week 3-4: Migrate one component/page to modern API
  3. Week 5-6: Gradually migrate remaining components
  4. 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