import { B as BeacioErrorCode } from '../error-taxonomy-CWrJx3aZ.mjs'; /** * Where the user is in the irreducibly-manual iOS-26 setup funnel, derived purely * from the in-page markers the content script / injected polyfill set: * - 'not-installed' → no markers; the app is not installed * - 'installed-inactive' → installed, but the Safari extension toggle is off * - 'active' → the polyfill is live on this page * * The shared shape consumed by detect.ts, the react-sdk ExtensionDetector, and * the headless API. (The per-site 'denied' refinement is NOT a marker state — it * is derived separately in initBeacio from navigator.bluetooth.getAvailability().) */ type ExtensionInstallState = 'not-installed' | 'installed-inactive' | 'active'; /** * SB-SDK-12 (AC4): the canonical id-form App Store URL for the public beacio app. * The id form survives the public App Store rename — pinning the banner CTA here * means no banner code path can hardcode a NAME slug (`/app//id…`) that * would 404 or mislead if Apple's slug differs from "beacio". The slug-form URL is * a SEPARATE concern owned by the CDN/website surfaces; the SDK side uses the id * form only. */ declare const APP_STORE_URL = "https://apps.apple.com/app/id6761301368"; /** * Synchronously read the current install state from the in-page markers. Pure, * zero-DOM, side-effect-free — the one accessor detect.ts, the react-sdk * ExtensionDetector, and the banner all share. A vanilla-JS partner calls this * to decide whether to render its own "Enable Bluetooth in Safari" card. */ declare function getInstallState(): ExtensionInstallState; /** True once the content script has flagged the extension active on this page. */ declare function isExtensionActive(): boolean; /** * SB-SDK-12 (AC3): the headless detector. Resolves the CURRENT install state * immediately when it is already 'active' (markers set); otherwise it waits for * the in-page extension to announce itself via the canonical EXTENSION_READY * handshake ('beacio:extension:ready') — the seam the react-sdk ExtensionDetector * and the in-page polyfill already speak — and resolves 'active' when it fires. * Falls back to a final marker read after `timeoutMs`. Injects no DOM. * * This lets a vanilla-JS partner await activation without polling and without any * beacio chrome: `const state = await observeInstallState();`. */ declare function observeInstallState(timeoutMs?: number): Promise; /** * SB-PRD-08 (AC5): the two suppression windows, written to the SAME DISMISS_KEY. * * - LONG (`DEFAULT_DISMISS_DAYS`) is the EXPLICIT "Don't show again" — the user * deliberately opted out, so honour it for a fortnight. * - SHORT (`SHORT_DISMISS_DAYS`) is a soft "Not now" / backdrop tap: the user is * interested-but-not-ready, not opted out. * * Why the long default is 14 and NOT longer in a hardware-companion context: a * Storz & Bickel device is a considered EUR300-700 purchase whose owner returns * over days/weeks while they actually receive and set up the hardware. The old * behaviour applied this 14-day silence to EVERY dismiss gesture, so one reflexive * "Not now" churned a warm lead. We keep 14 ONLY for the explicit opt-out and make * the incidental dismiss a single day, so the passive on-load banner re-appears on * the next session while a force-show recovery path (banner.ts) always lets a * dismissed user re-open setup immediately. 14 stays the LONG default (configurable * via dismissDays) because an explicit opt-out should not nag the next day either. */ declare const DEFAULT_DISMISS_DAYS = 14; declare const SHORT_DISMISS_DAYS = 1; /** True while a prior dismissal is still inside its suppression window. */ declare function isDismissed(): boolean; /** * Suppress the prompt for `days` (default {@link DEFAULT_DISMISS_DAYS} = 14) — the * LONG, explicit "Don't show again" window. Named `dismiss` on the headless * surface; banner.ts calls it directly. */ declare function dismiss(days?: number): void; /** * SB-PRD-08 (AC1): the SHORT, soft-dismissal primitive — a "Not now" / backdrop * tap suppresses the passive banner for {@link SHORT_DISMISS_DAYS} (1 day) only, * not the full fortnight, so an interested-but-not-ready user is not silenced for * two weeks. Writes the SAME DISMISS_KEY window as {@link dismiss}, just shorter. */ declare function dismissShort(): void; /** * GH #448: the PURE "escape hatch" builder for the iOS Home Screen web-app dead * end. `x-safari-` is the WebKit scheme that hands a URL to Safari proper, so * a page trapped in a standalone web app (where Safari extensions never load) can * offer a single tap back into a real Safari tab * (project_safari_only_links — the same `x-safari-https://` form the website uses). * * Only http(s) is mapped; anything else (about:, file:, javascript:, a non-URL, or * the empty off-DOM page URL) returns the EMPTY-STRING sentinel rather than * undefined, so the caller's field stays required and a renderer can branch on * `=== ''` instead of an optionality check. Side-effect-free. * * The NORMALISED `url.href` is what gets the prefix, never the raw argument: this * is a public headless-API entry point taking an arbitrary string, and `new URL()` * happily parses forms WebKit's scheme handler would choke on — * 'https:beacio.com', 'HTTPS://beacio.com/', or an input with surrounding * whitespace would otherwise produce a malformed `x-safari-…` deep link. */ declare function buildOpenInSafariUrl(pageUrl: string): string; /** * Persist (and best-effort copy) the originating page so the return link survives * the round trip into Settings and back. The return link is the * `https://link.beacio.com/return?url=` form built by * {@link buildReturnLink}. Injects no DOM — a partner surfaces the link in its OWN * card. */ declare function saveReturnContext(): void; /** * The originating page saved by {@link saveReturnContext}, as a VISIBLE, tappable * affordance — never relying on a silent clipboard write. Returns the current * href as a sensible fallback when nothing was saved. */ declare function getReturnContext(): { url: string; returnLink: string; }; /** * Platform detection utilities for Beacio */ declare function isIOSSafari(): boolean; /** * GH #448: is this page running as an iOS Home Screen web app ("Add to Home * Screen" with "Open as Web App" ON)? Safari extensions NEVER load in that * standalone context, so beacio is inert there even when the app is installed and * enabled — and because the standalone UA drops the `Safari/` token, * {@link isIOSSafari} is false and every caller would otherwise report the generic * "unsupported browser" / "not installed" dead end instead of the real diagnosis * ("open this page in Safari, or re-add it with Open as Web App off"). * * Two signals, either of which is sufficient: the classic WebKit * `navigator.standalone` boolean, and the `(display-mode: standalone)` media query * (which also covers third-party-browser Home Screen web apps on iOS 16.4+, where * extensions are likewise unavailable — the diagnosis holds). This is a * DETERMINISTIC platform API, not a heuristic like the Private-Browsing probe. * * `nav` and `win` are REQUIRED parameters: the caller owns the * `typeof window !== 'undefined'` guard. The `typeof win.matchMedia` test is not an * optionality check on our own API — it is a capability probe for environments * (jsdom, older WebKit) that do not implement matchMedia at all. */ declare function isIOSHomeScreenWebApp(nav: Navigator, win: Window): boolean; declare function getExtensionInstallState(): Promise; declare function isExtensionInstalled(): Promise; /** * @beacio/detect#i18n — SB-SDK-07 * * The shared localized-string seam for the two user-facing surfaces in * @beacio/detect: the install banner (banner.ts) and the branded error * presenter (error-presenter.ts). The install banner is the SINGLE end-user * onboarding screen that replaces S&B's Bluefy alert, and it was hardcoded * English — only `text`/`buttonText` were overridable. S&B is HQ'd in Bayreuth * and its German users would see English at the make-or-break moment. This * module centralises every visible token into a typed string pack, ships a * built-in German (`de`) pack alongside the English (`en`) default, and exposes * one PURE selector with a documented policy. * * Selection policy (resolveStrings): * 1. an explicit BCP-47 `lang` ALWAYS wins (prefix-matched: 'de', 'de-DE', * 'de-AT' all select the German pack); * 2. else the runtime's `navigator.language` is prefix-matched the same way * (so a German-locale iPhone gets German with zero config); * 3. else English. * A caller-supplied partial `strings` object then deep-merges OVER the selected * pack, so an operator can override one field (e.g. just `buttonText`) without * restating the whole pack, in any language. * * Design constraints (mirroring banner.ts / error-presenter.ts): * - The per-code packs are typed against the ONE `BeacioErrorCode` union in * `../error-taxonomy` (a TYPE-only import — erased at build time). The ONE * runtime import from that leaf is `IOS_GRANT_WORDING_CANONICAL` (R-52), * already bundled into detect via `error-presenter.ts` — zero new weight. * A code added to the taxonomy * fails THIS file's compile until both packs cover it; that is stronger than * the hand-maintained test Record it replaced, and it fails in src. * - All copy uses neutral install-path framing only — no "App Store approved / * cleared / reviewed" language (feedback_no_app_store_status_claims). * - `{operator}` is the ONLY interpolation token; banner.ts substitutes the * resolved operator name into it (see fill()). * * SDK has zero external consumers, so adding the `lang`/`strings` seam is a free, * non-breaking change (project_sdk_no_consumers). */ /** Funnel-state lead copy: a title + body shown at the top of the bottom sheet. */ interface StateCopy { title: string; body: string; } /** A single setup step: the imperative label the user taps + its one-line "why". */ interface SetupStepCopy { label: string; why: string; } /** A branded error card's headline + body. */ interface ErrorCopy { title: string; body: string; } /** The error-presenter half of the pack — shared by presentError. */ interface ErrorStrings { /** Dismiss button label on the error card. */ dismiss: string; /** Retry affordance label (retriable errors only). */ retry: string; /** Per-code branded headline. EVERY BeacioErrorCode is present (parity-guarded). */ titles: Record; /** Per-code branded body. EVERY BeacioErrorCode is present (parity-guarded). */ messages: Record; /** Fallback copy for an unrecognised error (bare string / unknown DOMException). */ generic: ErrorCopy; } /** * The complete visible-string surface of @beacio/detect. The built-in `en`/`de` * packs both define EXACTLY these keys (i18n.test.ts pins the parity), so a new * English-only string cannot silently bypass localization. */ interface LocaleStrings { /** Primary CTA label on the not-installed sheet + the lightweight bar. */ buttonText: string; /** Sheet soft-dismiss ("Not now") label — short suppression (SB-PRD-08). */ dismiss: string; /** * SB-PRD-08: the EXPLICIT "Don't show again" opt-out label — the long-suppression * control, distinct from the soft "Not now" {@link dismiss} above. */ dontShowAgain: string; /** Per-funnel-state lead copy (the 'active' state renders the toast instead). */ states: { 'not-installed': StateCopy; 'installed-inactive': StateCopy; denied: StateCopy; /** * SB-SDK-17: Private Browsing dead end. iOS Safari disables web extensions in * Private Browsing (no per-extension opt-in), so beacio is inert and the app * may already be installed — the recovery is to reopen the page in a normal * tab, NOT to install anything. A distinct hint (no install CTA, no steps). */ 'private-browsing': StateCopy; /** * GH #448: the Home Screen web-app dead end. A page opened from a Home Screen * icon added with "Open as Web App" ON runs standalone, and Safari extensions * NEVER load there — so beacio is inert even when the app IS installed (the * standalone UA even drops the `Safari/` token, so the page looks like an * unsupported browser). The recovery is Safari itself, NOT another install, so * this is a distinct hint: no install CTA, no steps, and the * {@link openInSafari} escape hatch below instead. */ 'home-screen-web-app': StateCopy; }; /** The ordered first-run step list (install → … → return). */ steps: SetupStepCopy[]; /** Visible "Return to {operator}" CTA label. */ returnCta: string; /** Sub-line explaining the link was also copied to the clipboard. */ clipboardHint: string; /** GH #448: label of the `x-safari-` CTA on the Home Screen web-app sheet. */ openInSafari: string; /** * GH #448: lead-in for the copy/paste fallback line, rendered immediately before * the page address. The `x-safari-` scheme can silently no-op inside a web app, * so the address itself is always VISIBLE and the sheet never dead-ends. */ openInSafariFallback: string; /** "Reload page to re-check" control label. */ reload: string; /** "How does setup work?"
summary. */ howSummary: string; /** "How does setup work?"
body (ends with the linked guide phrase). */ howBody: string; /** Linked phrase inside howBody that points at the setup guide. */ howLink: string; /** "Privacy: No data collected"
summary. */ privacySummary: string; /** "Privacy: No data collected"
body. */ privacyBody: string; /** "Still stuck? Open the setup guide" affordance. */ stillStuck: string; /** Lightweight bar banner heading ("Enable Bluetooth"). */ barTitle: string; /** Lightweight bar banner body text. */ barText: string; /** Once-only success toast text ("beacio is ready — tap Connect …"). */ readyToast: string; /** Error-presenter strings (shared with presentError). */ error: ErrorStrings; } /** * English (default) pack — the byte-identical source of today's rendered copy. * `{operator}` is substituted by banner.ts with the resolved operator name. */ declare const EN_STRINGS: LocaleStrings; /** * German (`de`) pack. Mirrors EN_STRINGS key-for-key (i18n.test.ts pins the * parity, so this pack can never fall behind a new English string). Native, * neutral install-path German; iOS-26 Settings paths use the localized Settings * labels (Apps → Safari → Erweiterungen) a German iPhone actually shows. The * stylized brand word "beacio" stays lowercase mid-sentence, matching the * English copy and the app's lowercase display name. */ declare const DE_STRINGS: LocaleStrings; /** A recursively-optional view of a type, for partial `strings` overrides. */ type DeepPartial = T extends (infer U)[] ? U[] : T extends object ? { [K in keyof T]?: DeepPartial; } : T; /** Options shared by the banner + error presenter for selecting localized copy. */ interface ResolveStringsOptions { /** Explicit BCP-47 language tag. Always wins when its primary subtag is known. */ lang?: string; /** Partial overrides deep-merged over the selected pack (any field, any depth). */ strings?: DeepPartial; } /** * PURE locale selector implementing the SB-SDK-07 policy: * explicit `lang` (prefix-matched) > navigator.language (prefix-matched) > English, * then a partial `strings` override deep-merged over the selected pack. * * Pure + side-effect-free: it reads navigator.language only when no explicit * `lang` is given, and never mutates the built-in packs. An unknown subtag falls * through to English (never throws). Returns a fresh object when an override is * supplied, else the shared pack reference (so identity checks against EN/DE_STRINGS * hold for the no-override path the tests assert). */ declare function resolveStrings(options?: ResolveStringsOptions): LocaleStrings; /** * Install prompt UI for Beacio * * Two modes: * 1. Bottom sheet (default) — iOS-native feel, shown on requestDevice() trigger * 2. Banner — lightweight top/bottom bar for passive prompting * * Features: * - Clipboard context saving for return-to-web-app flow * - 14-day dismissal frequency capping * - Configurable install/onboarding redirect * - Dark mode support via prefers-color-scheme */ /** * Where the user is in the irreducibly-manual iOS-26 setup funnel, so the sheet * can render the SPECIFIC remaining step instead of restarting the whole flow: * - 'not-installed' → app not installed; full install→enable→grant walkthrough * - 'installed-inactive' → installed but the Safari extension toggle is off * - 'denied' → enabled but per-origin access not granted on THIS site * - 'private-browsing' → Private Browsing disables extensions; reopen in a normal tab * - 'home-screen-web-app' → running standalone from the Home Screen, where Safari * extensions never load; open the page in Safari * - 'active' → ready; render the once-only success toast * Mirrors ExtensionInstallState ('active' | 'installed-inactive' | 'not-installed') * plus the in-page refinements only the page flow can distinguish: the per-site * 'denied' grant, the SB-SDK-17 'private-browsing' dead end and the GH #448 * Home Screen web-app dead end. */ type BannerState = 'not-installed' | 'installed-inactive' | 'denied' | 'private-browsing' | 'home-screen-web-app' | 'active'; interface BannerOptions { /** 'sheet' (default) for iOS bottom sheet, 'banner' for lightweight bar. The 'home-screen-web-app' state always renders the sheet (the bar has no state-specific copy and would show the install CTA). */ mode?: 'sheet' | 'banner'; position?: 'top' | 'bottom'; style?: Record; /** Preferred install or onboarding URL to open when the user taps the CTA */ startOnboardingUrl?: string; /** Legacy install destination option; still supported for compatibility */ appStoreUrl?: string; /** Operator/app name shown in the prompt (e.g. "FitTracker") */ operatorName?: string; /** API key for campaign tracking */ apiKey?: string; /** * Days to suppress the PASSIVE on-load banner after the EXPLICIT "Don't show * again" opt-out (default: 14). SB-PRD-08: the soft "Not now"/backdrop tap uses * a separate, short (1-day) window and is NOT governed by this option, so one * reflexive dismiss no longer silences guidance for a fortnight. */ dismissDays?: number; /** * SB-PRD-08 (AC3): ignore the active dismissal cooldown and render anyway. The * passive on-load banner leaves this false so a dismissed user is not nagged; * a USER-INITIATED recovery gesture (e.g. tapping Connect, or a "Set up * Bluetooth"/"Can't connect?" affordance) passes `forceShow: true` to re-open * the activation flow without the integrator having to clear localStorage. */ forceShow?: boolean; /** * Funnel position. Lets initBeacio render state-specific guidance (and, on * 'active', the once-only "ready" toast) without restarting setup. Defaults to * 'not-installed' for the legacy "show the full walkthrough" call site. */ state?: BannerState; /** * Setup destination shown behind the "still stuck?" affordance and the * "How does setup work?" disclosure. Defaults to the canonical /setup page; * an operator (e.g. Storz & Bickel) can point it at their own branded help. */ setupUrl?: string; /** * SB-SDK-07: BCP-47 UI language (e.g. 'de'). When set, its primary subtag * selects the built-in pack; when omitted, the language is derived from * navigator.language, else English. Always wins over navigator.language. */ lang?: string; /** * SB-SDK-07: partial copy overrides deep-merged over the selected language * pack — override one field (e.g. `buttonText`) without restating the rest. */ strings?: DeepPartial; /** * SB-SDK-11 (tier-2 co-brand): partner accent colour applied to the sheet/bar * chrome (icon tile, step bullets, primary CTA, disclosure links). Routed * through a `--bc-accent` CSS variable so every accent rule switches to * var(--bc-accent); when omitted the variable defaults to the beacio Apple-blue * (#007aff) and the prompt renders exactly as before. Any CSS colour token. */ accentColor?: string; /** * SB-SDK-11: partner logo, restricted to a URL (no raw SVG markup) so it can * never inject script. Validated with `new URL()` against the page origin and * accepted ONLY when the resolved protocol is http(s); a `javascript:`/`data:`/ * `ftp:` value is dropped and the default beacio chrome icon is kept. Rendered * as an in place of the inline beacio . */ brandLogoUrl?: string; /** * SB-SDK-11: the specific device being connected (e.g. "VOLCANO HYBRID"). When * set it is interpolated into the `{device}` token of any copy that carries it, * so a co-brand sheet can read "Connect your VOLCANO HYBRID in Safari". */ deviceName?: string; /** * SB-SDK-11: a one-shot override for the sheet's lead body copy. Wins over the * resolved language pack's state body (HTML-escaped via esc(), like all copy). * For finer-grained per-field overrides use the SB-SDK-07 `strings` seam. */ body?: string; /** * SB-SDK-11: override for the privacy reassurance body (the medical-market * trust line). HTML-escaped. Defaults to the resolved pack's privacyBody. */ privacyBody?: string; } interface SetupStep { /** Imperative step label the user taps. */ label: string; /** One-line "why this is required", shown under the label. */ why: string; } /** * The real sequence a first-run owner actually taps on a physical iPhone, each * grant with its own "why" so no system prompt is a surprise. Ordering and count * are the contract: install → open app → enable extension → allow website access * (the aA gesture) → allow Bluetooth on first scan → return. * * SB-SDK-07: this is the ENGLISH step list, now sourced from EN_STRINGS.steps so * the exported constant (mirrored by the react-sdk InstallationWizard) and the * localized pack never drift. Localized rendering reads the resolved pack's * steps; the per-state filtering below is by INDEX into this canonical order, so * it is language-independent (German labels do not match the old English regex). */ declare const SETUP_STEPS: readonly SetupStep[]; declare function showInstallBanner(options?: BannerOptions): HTMLElement | null; declare function removeInstallBanner(): void; /** * @beacio/detect#presentError — SB-SDK-05 * * A drop-in, framework-free branded ERROR presenter. The polished branded surface * already exists for the INSTALL prompt (banner.ts); this is its sibling for the * FAILURE path. S&B (and any vanilla-JS site) uses raw `navigator.bluetooth` * across hundreds of call sites and will not rewrite them — so the worst surface, * a blocking, stack-leaking `window.alert()`, is converted into a non-blocking, * dismissible, recovery-oriented card with a ~1-line edit: * * catch (error) { beacioDetect.presentError(error); } * * Design constraints (mirroring banner.ts): * - Errors are consumed STRUCTURALLY — anything carrying a `.code` / `.message` * / `.suggestion` / `.isRetriable` is understood — so a host page can hand us * a BeacioError-shaped object without owning the class. The CLASSIFICATION of * a raw DOMException, the code union and the retriable set come from * `../error-taxonomy`, the leaf module the SDK's own `BeacioError.from` uses: * one classifier, so the card and the caller's `switch` can never disagree. * (Until 2026-08-12 they were hand-copied twins, justified by an * "@beacio/core is an OPTIONAL peer of @beacio/detect" rule that no longer * exists — there is no `packages/detect`, `./detect` is a subpath export of * core, and these modules already import `../events` / `../urls`. The twins * had diverged on eleven inputs by the time they were collapsed.) * - The card NEVER leaks a stack trace, internal codes, WebKit jargon, or a * competitor name. The friendly body comes from the per-code copy table, NOT * the raw error string. * - Identical errors fired in a short window are coalesced to ONE card (defends * against the backgrounded alert-storm). * - All user-visible strings are overridable via a copy/locale object * (PresentErrorOptions.strings) — the i18n seam SB-SDK-07 converges on; the * `lang` field selects a built-in pack (German shipped), and `strings` * deep-merges over it. English defaults apply when neither is supplied (no * regression). The per-code copy + dismiss/retry come from the SAME shared * i18n module the install banner uses (./i18n), so a localized card and a * localized banner never drift. */ /** * Per-code friendly headline + body. Plain-English, recovery-oriented, no internal * codes, no jargon, no competitor names. This is the body shown to the user — the * raw error string (which may carry a stack or a competitor name) is NEVER shown. * * SB-SDK-07: there is no local copy table. The presenter's English * source-of-truth and the shared i18n pack are ONE table (EN_STRINGS.error), so * they cannot drift; localized rendering reads the RESOLVED pack (which may be * German). `isCodedError` therefore anchors code membership on * `EN_STRINGS.error.titles` directly. Completeness needs no test: the packs are * typed `Record` against the ONE union, so a new code * fails i18n.ts's compile before any test runs. */ /** * Options for {@link presentError}. Parity with BannerOptions where it overlaps * (operatorName, style), plus the retry affordance + the copy/locale seam. */ interface PresentErrorOptions { /** Operator/app name shown in the card (e.g. "STORZ & BICKEL"). */ operatorName?: string; /** * SB-SDK-07: BCP-47 UI language (e.g. 'de'). Selects the built-in pack for the * per-code title/body + dismiss/retry labels; omitted ⇒ derived from * navigator.language, else English. A per-call `strings` (and the explicit * dismissText/retryText) still override the selected pack. Always wins over * navigator.language. */ lang?: string; /** Retry button label override (takes precedence over strings.retry). */ retryText?: string; /** Dismiss button label override (takes precedence over strings.dismiss). */ dismissText?: string; /** * Invoked when the user taps the retry affordance (retriable errors only), so a * caller can re-run its connect()/operation. The card is dismissed first. */ onRetry?: () => void; /** Extra inline styles merged onto the card container. */ style?: Record; /** * Copy/locale overrides for every user-visible string (SB-SDK-07 seam), * deep-merged over the selected language pack by the SAME `resolveStrings` * the install banner uses — so the seam also covers the per-code `titles` and * the `generic` fallback, not just `messages`/`dismiss`/`retry`. Every field is * optional; an omitted field keeps the pack's value, so a caller that passes * nothing is byte-identical to today. */ strings?: DeepPartial; } /** * Present a branded, non-blocking, dismissible error card. Replaces a blocking * `window.alert(error.toString() + error.stack)` with a recovery-oriented surface. * * @param errorOrMessage A BeacioError, a raw DOMException/Error, or a string. * @param options Operator name, copy/locale overrides, and an onRetry handler. * @returns The card element, or null when the error is coalesced (a card for an * identical error is already on screen) so callers can no-op safely. */ declare function presentError(errorOrMessage: unknown, options?: PresentErrorOptions): HTMLElement | null; /** * Analytics event reporter and API key validator. * Fire-and-forget — analytics must never throw or block. */ declare function reportEvent(apiKey: string, event: string, _data?: { [key: string]: string | number | boolean | null; }): void; declare function validateApiKey(apiKey: string): Promise<{ operatorId: string; appName: string | null; plan: string; } | null>; /** * @beacio/detect * * Detects iOS Safari, checks if the Beacio extension is installed, * and shows an install banner if not. No-op on all other platforms. * * Your existing Web Bluetooth code works unchanged — this package only * handles the "extension not installed" case on iOS Safari. */ interface BeacioOptions { /** Optional API key for campaign tracking */ key?: string; /** Operator/app name shown in the prompt (e.g. "FitTracker") */ operatorName?: string; /** * SB-SDK-07: BCP-47 UI language (e.g. 'de') for the install banner. Threaded * to showInstallBanner so the zero-config initBeacio path is localizable; * omitted ⇒ the banner derives the language from navigator.language, else * English. A `banner.lang` (below) overrides this for the banner specifically. */ lang?: string; /** Install banner configuration, or false to disable */ banner?: { /** 'sheet' (default) for iOS bottom sheet, 'banner' for lightweight bar */ mode?: 'sheet' | 'banner'; position?: 'top' | 'bottom'; text?: string; buttonText?: string; style?: Record; startOnboardingUrl?: string; appStoreUrl?: string; /** Days to suppress after the explicit "Don't show again" opt-out (default: 14) */ dismissDays?: number; /** * SB-PRD-08 (AC3): ignore the dismissal cooldown and show anyway. Set this * on a user-initiated recovery call (e.g. re-invoking initBeacio from a * Connect / "Can't connect?" gesture) so a previously-dismissed user can * re-open setup without clearing localStorage. */ forceShow?: boolean; /** SB-SDK-07: BCP-47 language override for the banner (wins over the top-level `lang`). */ lang?: string; /** SB-SDK-11 (tier-2 co-brand): partner accent colour for the prompt chrome. */ accentColor?: string; /** SB-SDK-11: partner logo URL (http(s) only; validated). Replaces the beacio glyph. */ brandLogoUrl?: string; /** SB-SDK-11: the connected device's display name (e.g. "VOLCANO HYBRID"). */ deviceName?: string; /** SB-SDK-11: one-shot lead body copy override (HTML-escaped). */ body?: string; /** SB-SDK-11: privacy reassurance body override (HTML-escaped). */ privacyBody?: string; } | false; /** Called when the extension is detected and ready */ onReady?: () => void; /** Called when the extension is installed but Safari still needs activation/allow access */ onInstalledInactive?: () => void; /** Called when the extension is NOT installed */ onNotInstalled?: () => void; } /** * Initialize Beacio detection. * * On iOS Safari: checks if the extension is installed, dispatches events, * and optionally shows an install banner. * * On an iOS Home Screen web app (GH #448): renders the "open this page in Safari" * diagnosis instead — the page is NOT iOS Safari by the UA test, yet it runs on an * iOS device one tap away from a browser where beacio does work. * * On all other platforms: no-op (returns immediately). */ declare function initBeacio(options: BeacioOptions): Promise; /** * Where a first-run owner is in the irreducibly-manual iOS-26 setup funnel, as * DATA a partner renders itself. The union is CLOSED to exactly initBeacio's seven * routing outcomes (a discriminated union + exhaustive switch, not scattered * undefined checks): * - 'unsupported' → not iOS Safari; Web Bluetooth via beacio is unavailable. * - 'home-screen-web-app' → running standalone from the Home Screen, where Safari * extensions never load; the fix is opening it in Safari. * - 'not-installed' → app not installed; `installUrl` is the id-form App Store link. * - 'installed-inactive' → installed but the Safari extension toggle is off; `setupUrl` guides. * - 'denied' → enabled, but per-origin access not granted on THIS site; `setupUrl` guides. * - 'private-browsing' → Private Browsing disables extensions; the fix is a normal tab. * - 'ready' → the polyfill is live and this origin is granted; nothing to prompt. * * Required fields, sentinels over optionals (owner's API rule): each variant * carries only the render-ready URLs its OWN prompt needs, all required — no `?`. * `returnLink` is the tappable "return to your page" affordance * (`https://link.beacio.com/return?url=…`) computed purely, with no side effect. * 'home-screen-web-app' carries no `returnLink`: the /setup round-trip does not * apply there (the recovery is Safari itself), so it carries the current `pageUrl` * — the copy/paste fallback — plus the `x-safari-` deep link built from it. * * GH #448 twin ruling: the DOM twin AGREES — the banner's footer splits on the same * question this union does. The /setup round-trip affordances (`#bc-return`, the * "link also copied" hint and `saveReturnContext()`'s clipboard write) render only * for the states whose recovery comes BACK to this page; the 'home-screen-web-app' * sheet suppresses all three, because its recovery LEAVES for Safari and it prints * the page address for the user to copy — a second, different "paste this into * Safari" URL would contradict it, and the return link would clobber the clipboard. * So "no `returnLink` in the data" and "no `#bc-return` in the DOM" are one ruling, * not a divergence. The other footer chrome (still-stuck / reload / dismiss) is * state-independent and unchanged. */ type OnboardingState = { kind: 'unsupported'; } | { kind: 'home-screen-web-app'; pageUrl: string; openInSafariUrl: string; } | { kind: 'not-installed'; installUrl: string; returnLink: string; } | { kind: 'installed-inactive'; setupUrl: string; returnLink: string; } | { kind: 'denied'; setupUrl: string; returnLink: string; } | { kind: 'private-browsing'; returnLink: string; } | { kind: 'ready'; }; /** * The REQUIRED config for {@link resolveOnboardingState} — no optional args. * `apiKey` threads the App Store campaign token (ct/mt) onto the install deep link * exactly as the banner's install button does; `operatorName` threads the operator * identity onto the guided /setup deep link so it can render "Return to ". * Pass empty-string sentinels when a field is not in play. */ interface OnboardingConfig { operatorName: string; apiKey: string; } /** * Resolve the current tier-3 onboarding funnel position WITHOUT rendering any * beacio chrome. This is the headless projection of initBeacio's routing: the same * isIOSSafari early-return (widened by the GH #448 Home Screen web-app guard), the * same active → (denied?) → ready split, and the same "standalone, then Private * Browsing, then a marker-suppressed denied" precedence for the non-active states — * but it returns the position as data for a partner to render, dispatching NO * events and injecting NO DOM. */ declare function resolveOnboardingState(config: OnboardingConfig): Promise; export { APP_STORE_URL, type BannerOptions, type BannerState, BeacioErrorCode, type BeacioOptions, DEFAULT_DISMISS_DAYS, DE_STRINGS, type DeepPartial, EN_STRINGS, type ErrorCopy, type ErrorStrings, type ExtensionInstallState, type LocaleStrings, type OnboardingConfig, type OnboardingState, type PresentErrorOptions, type ResolveStringsOptions, SETUP_STEPS, SHORT_DISMISS_DAYS, type SetupStep, type SetupStepCopy, type StateCopy, buildOpenInSafariUrl, dismiss, dismissShort, getExtensionInstallState, getInstallState, getReturnContext, initBeacio, isDismissed, isExtensionActive, isExtensionInstalled, isIOSHomeScreenWebApp, isIOSSafari, observeInstallState, presentError, removeInstallBanner, reportEvent, resolveOnboardingState, resolveStrings, saveReturnContext, showInstallBanner, validateApiKey };