@tour-kit/license API
API reference for @tour-kit/license: LicenseProvider, LicenseGate, ProGate, useLicense, useIsPro, useLicenseGate, validateLicenseKey, and related types
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>| Prop | Type | Description |
|---|---|---|
licenseKey | string | Polar license key (TOURKIT-...) |
organizationId | string | Polar organization id; required for validation against the API |
children | ReactNode | App tree |
onValidate | (state: LicenseState) => void | Called after each successful validation |
onError | (error: Error) => void | Called 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>| Prop | Type | Description |
|---|---|---|
require | 'pro' | Minimum tier required |
children | ReactNode | Always rendered. On unlicensed non-localhost hosts, the badge layers on top |
fallback | ReactNode | Only used when a LicenseProvider is mounted and the state is gated. Replaces children + badge with this node |
loading | ReactNode | Rendered while validation is in progress (only when a LicenseProvider is mounted) |
Behavior matrix:
| Host | LicenseProvider mounted | licenseKey | License | Rendered |
|---|---|---|---|---|
localhost / 127.0.0.1 / *.local | yes | non-empty | dev bypass | Children only, no badge |
localhost / 127.0.0.1 / *.local | yes | empty / blank | unlicensed | Children + badge + dev warning |
localhost / 127.0.0.1 / *.local | no | n/a | n/a | Children only, no badge (no key to inspect) |
| Other host | yes | non-empty | valid pro | Children only, no badge |
| Other host | yes | non-empty | invalid/expired/revoked | Children + badge + dev warning |
| Other host | yes | non-empty | error + fresh cache | Children only, no badge |
| Other host | yes | empty / blank | unlicensed | Children + badge + dev warning |
| Other host | no | n/a | n/a | Children + 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>| Prop | Type | Description |
|---|---|---|
package | string | npm package name shown in the placeholder + console warning |
children | ReactNode | Rendered 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 />| Prop | Type | Description |
|---|---|---|
message | string | Override the default warning message |
pricingUrl | string | URL surfaced in the warning |
dismissible | boolean | Whether the user can dismiss (visual variants only) |
onDismiss | () => void | Dismissal callback |
className | string | Class 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();| Return | Type | Description |
|---|---|---|
state | LicenseState | Full license state object |
refresh | () => Promise<void> | Force re-validation against Polar |
isGated | boolean | Whether pro components should be blocked |
isLoading | boolean | Whether validation is in progress |
gracePeriodActive | boolean | True 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;| Return | Type | Description |
|---|---|---|
isGated | boolean | True if the component should be blocked |
isLoading | boolean | True 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:
licenseKeyis 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 gatedstatus: 'error'+ fresh cache → not gated (grace period)status: 'error'+ no cache → gatedstatus: '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');| Param | Type | Description |
|---|---|---|
config | LicenseConfig | { 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' | nullReturns 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.
| Export | Type | Notes |
|---|---|---|
LicenseContext | Context<LicenseContextValue | null> | Raw React context. Prefer useLicense() |
LicenseRenderContext | Context<string | undefined> | Internal render-key context used by <LicenseGate> for anti-bypass |
See also
- Licensing overview — Polar setup, pricing tiers, and environment variables.
- Trial mode — evaluate Pro packages locally without a key.
- API reference index — browse other package references.
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.