Skip to main content
userTourKit
API Reference

@tour-kit/license API

API reference for @tour-kit/license: LicenseProvider, LicenseGate, ProGate, useLicense, useIsPro, useLicenseGate, validateLicenseKey, and related types

domidex01Published

Complete API reference for the license package. Polar.sh-based key validation, domain activation, and React gating components used by Tour Kit Pro packages.

For installation and environment setup see Licensing. Free packages (@tour-kit/core, @tour-kit/react, @tour-kit/hints) never need this package.


Providers

LicenseProvider

Root provider that validates the key against Polar, maintains license state, and exposes it via context. Place once at the top of your app, above any pro components.

import { LicenseProvider } from '@tour-kit/license';

<LicenseProvider
  licenseKey={process.env.NEXT_PUBLIC_TOURKIT_LICENSE_KEY!}
  organizationId={process.env.NEXT_PUBLIC_POLAR_ORG_ID}
  onValidate={(state) => console.log('License:', state.status)}
  onError={(err) => console.error(err)}
>
  {children}
</LicenseProvider>
PropTypeDescription
licenseKeystringPolar license key (TOURKIT-...)
organizationIdstringPolar organization id; required for validation against the API
childrenReactNodeApp tree
onValidate(state: LicenseState) => voidCalled after each successful validation
onError(error: Error) => voidCalled on validation failure

Components

LicenseGate

Soft gate used internally by every Tour Kit Pro package. Renders children unconditionally; on non-localhost hosts without a valid license, layers a single small badge (LicenseWatermark) and a dev-only console warning over the top. Tolerates a missing LicenseProvider so Pro packages stay rendered while a customer evaluates them.

import { LicenseGate } from '@tour-kit/license';

<LicenseGate
  require="pro"
  fallback={<UpgradePrompt />}
  loading={<Skeleton />}
>
  <AdoptionDashboard />
</LicenseGate>
PropTypeDescription
require'pro'Minimum tier required
childrenReactNodeAlways rendered. On unlicensed non-localhost hosts, the badge layers on top
fallbackReactNodeOnly used when a LicenseProvider is mounted and the state is gated. Replaces children + badge with this node
loadingReactNodeRendered while validation is in progress (only when a LicenseProvider is mounted)

Behavior matrix:

HostLicenseProvider mountedlicenseKeyLicenseRendered
localhost / 127.0.0.1 / *.localyesnon-emptydev bypassChildren only, no badge
localhost / 127.0.0.1 / *.localyesempty / blankunlicensedChildren + badge + dev warning
localhost / 127.0.0.1 / *.localnon/an/aChildren only, no badge (no key to inspect)
Other hostyesnon-emptyvalid proChildren only, no badge
Other hostyesnon-emptyinvalid/expired/revokedChildren + badge + dev warning
Other hostyesnon-emptyerror + fresh cacheChildren only, no badge
Other hostyesempty / blankunlicensedChildren + badge + dev warning
Other hostnon/an/aChildren + badge + dev warning. No throw

ProGate

Hard gate that replaces children with a branded placeholder when unlicensed. Tour Kit's own Pro packages no longer use this internally — they use LicenseGate so evaluation hosts can render the real UI. ProGate remains exported for downstream packages that want a hard placeholder.

import { ProGate } from '@tour-kit/license';

<ProGate package="@your-org/private-pro-pkg">
  <YourProComponent />
</ProGate>
PropTypeDescription
packagestringnpm package name shown in the placeholder + console warning
childrenReactNodeRendered when licensed; replaced by branded placeholder otherwise

Dev environments (localhost, 127.0.0.1, *.local) render children when no LicenseProvider is mounted, or when a LicenseProvider with a non-empty licenseKey is mounted. With a LicenseProvider and an empty/blank licenseKey, the hard placeholder is shown on localhost too.

LicenseWatermark

Small Tour Kit · Unlicensed · Buy license badge rendered into a portal at the bottom-right of the viewport. Layered by LicenseGate automatically when unlicensed on a non-localhost host. Multiple mounted instances coalesce into a single visible badge via a singleton ownership transfer, so 8 mounted Pro providers still produce one badge. Inline-styled with pointer-events: none on the wrapper and pointer-events: auto on the link, so the badge never blocks app clicks outside its own link. Clicking the badge opens pricing with UTM params and emits unlicensed_badge_clicked via window.gtag or window.dataLayer when present.

import { LicenseWatermark } from '@tour-kit/license';

{isUnlicensed && <LicenseWatermark />}

No props.

LicenseWarning

Console-only warning component. Renders nothing visible; calls console.warn once on mount when the license is invalid. Use as a low-friction notification in dev/preview builds.

import { LicenseWarning } from '@tour-kit/license';

<LicenseWarning />
PropTypeDescription
messagestringOverride the default warning message
pricingUrlstringURL surfaced in the warning
dismissiblebooleanWhether the user can dismiss (visual variants only)
onDismiss() => voidDismissal callback
classNamestringClass name override

Hooks

useLicense

Returns the full license context value. Throws if no LicenseProvider is mounted above.

import { useLicense } from '@tour-kit/license';

const { state, refresh, isGated, isLoading, gracePeriodActive } = useLicense();
ReturnTypeDescription
stateLicenseStateFull license state object
refresh() => Promise<void>Force re-validation against Polar
isGatedbooleanWhether pro components should be blocked
isLoadingbooleanWhether validation is in progress
gracePeriodActivebooleanTrue when running on a fresh-but-stale cache after a network failure

useIsPro

Boolean shortcut. Equivalent to useLicense().state.tier === 'pro' && state.status === 'valid'.

import { useIsPro } from '@tour-kit/license';

const isPro = useIsPro();

Returns boolean.

useLicenseGate

Reads the precomputed gating decision from context. Use this inside pro components instead of recomputing logic on every render.

import { useLicenseGate } from '@tour-kit/license';

const { isGated, isLoading } = useLicenseGate();
if (isGated) return <UnlicensedPlaceholder />;
if (isLoading) return null;
ReturnTypeDescription
isGatedbooleanTrue if the component should be blocked
isLoadingbooleanTrue while validation is in progress (avoid flash)

Provider context takes precedence over the hostname check. When a LicenseProvider is mounted, the result follows the provider's derived signals:

  • licenseKey is empty or blank → gated on every host (including localhost)
  • Dev host + non-empty licenseKey → not gated (dev bypass)
  • status: 'loading' → not gated, isLoading=true (avoid flash)
  • status: 'valid' + pro + render key → not gated
  • status: 'error' + fresh cache → not gated (grace period)
  • status: 'error' + no cache → gated
  • status: 'invalid' | 'expired' | 'revoked' → gated

When no LicenseProvider is in the tree, the hook falls back to the hostname check: dev hosts stay quiet (no key to inspect), every other host is gated.


Utilities

validateLicenseKey

Headless validation against the Polar API. Use in CI scripts or server-side checks.

import { validateLicenseKey } from '@tour-kit/license';

const state = await validateLicenseKey({ key: 'TOURKIT-...', organizationId: '...' });
if (state.status !== 'valid') throw new Error('License invalid');
ParamTypeDescription
configLicenseConfig{ key, organizationId }

Returns Promise<LicenseState>.

getCurrentDomain

Reads window.location.hostname. Returns null on the server.

import { getCurrentDomain } from '@tour-kit/license';

const domain = getCurrentDomain(); // 'app.example.com' | null

Returns string | null.

isDevEnvironment

Checks whether the current hostname is localhost, 127.0.0.1, or matches *.local.

import { isDevEnvironment } from '@tour-kit/license';

if (isDevEnvironment()) {
  console.log('Skipping license check in dev');
}

Returns boolean.


Types

LicenseTier

type LicenseTier = 'free' | 'pro';

LicenseState

Single source of truth for validity. Never derive validity from tier alone — a pro tier with status: 'expired' is invalid.

type LicenseState = {
  status: 'valid' | 'invalid' | 'expired' | 'revoked' | 'loading' | 'error';
  tier: LicenseTier;
  activations: number;
  maxActivations: number;
  domain: string | null;
  expiresAt: string | null;
  validatedAt: number;
  renderKey: string | undefined;
};

renderKey is set only when status === 'valid'. It is the anti-bypass mechanism consumed by <LicenseGate>.

LicenseCache

Shape stored in localStorage. keyHash is set when the cache is written with a license key; readers compare it against the current key's hash and invalidate on mismatch.

type LicenseCache = {
  state: LicenseState;
  cachedAt: number;
  domain: string;
  keyHash?: string;
};

Cache keys are domain-scoped: tourkit:license:{domain}. TTL is 72 hours.

LicenseActivation

type LicenseActivation = {
  id: string;
  licenseKeyId: string;
  label: string;
  createdAt: string;
  modifiedAt: string | null;
};

LicenseError

type LicenseError =
  | 'invalid_key'
  | 'network_error'
  | 'parse_error'
  | 'activation_limit_reached'
  | 'domain_mismatch';

LicenseConfig

type LicenseConfig = {
  key: string;
  organizationId: string;
};

LicenseContextValue

type LicenseContextValue = {
  state: LicenseState;
  refresh: () => Promise<void>;
  isGated: boolean;
  isLoading: boolean;
  gracePeriodActive: boolean;
};

LicenseGateResult

type LicenseGateResult = {
  isGated: boolean;
  isLoading: boolean;
};

LicenseProviderProps

type LicenseProviderProps = {
  licenseKey: string;
  organizationId?: string;
  children: React.ReactNode;
  onValidate?: (state: LicenseState) => void;
  onError?: (error: Error) => void;
};

LicenseGateProps

type LicenseGateProps = {
  require: 'pro';
  children: React.ReactNode;
  fallback?: React.ReactNode;
  loading?: React.ReactNode;
};

ProGateProps

type ProGateProps = {
  package: string;
  children: React.ReactNode;
};

LicenseWarningProps

type LicenseWarningProps = {
  message?: string;
  pricingUrl?: string;
  dismissible?: boolean;
  onDismiss?: () => void;
  className?: string;
};

Trial Types

Trial mode lets gated packages run for a limited time before requiring activation.

interface TrialConfig {
  durationDays: number;
  startsAt?: Date;             // defaults to first activation
  /** Optional grace window after expiry before gating closes */
  graceDays?: number;
  /** Storage namespace; defaults to '@tour-kit/license:trial' */
  storageKey?: string;
}

interface TrialContextValue {
  isTrialing: boolean;
  daysRemaining: number;
  expiresAt: Date | null;
  isExpired: boolean;
  start: () => void;
  end: () => void;
}

interface TrialBadgeRenderProps {
  daysRemaining: number;
  isExpired: boolean;
  expiresAt: Date | null;
}

interface TrialBadgeProps {
  className?: string;
  pricingUrl?: string;
  /** Render-prop override for custom badge UI */
  children?: (state: TrialBadgeRenderProps) => React.ReactNode;
}

See Trial guide.

Debug Components

interface LicenseDebugPanelProps {
  /** Position the floating panel; defaults to 'bottom-right' */
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
  className?: string;
}

interface LicenseTestModeProps {
  /** Force a tier without making network calls (dev only) */
  forceTier?: 'free' | 'pro';
  children: React.ReactNode;
}

Polar Response Types

Raw Polar API response shapes (after camelCase transform). Subject to upstream Polar SDK changes.

type PolarValidateResponse = {
  id: string;
  organizationId: string;
  status: 'granted' | 'revoked' | 'disabled';
  key: string;
  limitActivations: number | null;
  usage: number;
  validations: number;
  lastValidatedAt: string;
  expiresAt: string | null;
  activation: {
    id: string;
    licenseKeyId: string;
    label: string;
    meta: Record<string, unknown>;
    createdAt: string;
    modifiedAt: string | null;
  } | null;
};

type PolarActivateResponse = {
  id: string;
  licenseKeyId: string;
  label: string;
  meta: Record<string, unknown>;
  createdAt: string;
  modifiedAt: string | null;
  licenseKey: {
    id: string;
    organizationId: string;
    status: 'granted' | 'revoked' | 'disabled';
    limitActivations: number | null;
    usage: number;
    limitUsage: number | null;
    validations: number;
    lastValidatedAt: string;
    expiresAt: string | null;
  };
};

Internal Exports

These are exported for advanced use; most consumers should not need them.

ExportTypeNotes
LicenseContextContext<LicenseContextValue | null>Raw React context. Prefer useLicense()
LicenseRenderContextContext<string | undefined>Internal render-key context used by <LicenseGate> for anti-bypass

See also

Free & open source

Ship onboarding, not config.

npm i @tour-kit/core is MIT and free. The Pro packages work unlicensed too — a one-time $99 license removes the production watermark when you ship.

MIT-licensed — no signup, no credit card. Pay once, only when you ship.