import type { Metadata } from 'next'; export const metadata: Metadata = { title: 'Funnel SDK Docs', description: 'User answers object, useFunnel runtime reference, and analytics SDK usage for this project.', }; const userObjectExample = `{ "stepChoices": { "step-1": "continue" }, "selectedPaywallPlanId": "secondary" }`; type MethodDoc = { id: string; name: string; signature: string; description: string; example: string; }; const methodDocs: MethodDoc[] = [ { id: 'active-step-id', name: 'activeStepId', signature: 'activeStepId: FunnelStepId', description: 'Current rendered step id in the runtime shell.', example: `const { activeStepId } = useFunnel(); if (activeStepId === 'paywall') { // Step-specific behavior }`, }, { id: 'answers', name: 'answers', signature: 'answers: FunnelUserAnswers', description: 'Typed answers object persisted across steps.', example: `const { answers } = useFunnel(); const selectedPlanId = answers.selectedPaywallPlanId || null;`, }, { id: 'set-answer', name: 'setAnswer', signature: "setAnswer(key: K, value: FunnelUserAnswers[K]): void", description: 'Typed way to write answer values.', example: `const { setAnswer } = useFunnel(); setAnswer('selectedPaywallPlanId', 'secondary');`, }, { id: 'get-answer', name: 'getAnswer', signature: 'getAnswer(key: K): FunnelUserAnswers[K] | undefined', description: 'Typed getter for a single answer key.', example: `const { getAnswer } = useFunnel(); const selectedPlanId = getAnswer('selectedPaywallPlanId');`, }, { id: 'go-next', name: 'goNext', signature: 'goNext(): void', description: 'Navigate to configured next step based on routing and current answers.', example: `const { goNext } = useFunnel(); `, }, { id: 'go-to-step', name: 'goToStep', signature: 'goToStep(stepId: string, outcome: FunnelNavigationOutcome): void', description: 'Imperative navigation to a specific step id.', example: `const { goToStep } = useFunnel(); goToStep('paywall', { type: 'complete' });`, }, { id: 'get-choice-targets', name: 'getChoiceTargets', signature: 'getChoiceTargets(stepId?: string): { yes: FunnelStepId; no: FunnelStepId } | null', description: 'Read yes/no branch targets from routing config.', example: `const { getChoiceTargets } = useFunnel(); const choiceTargets = getChoiceTargets();`, }, { id: 'go-choice', name: 'goChoice', signature: "goChoice(choice: 'yes' | 'no', stepId?: string): void", description: 'Record a binary choice and route using configured choice targets with fallback logic.', example: `const { goChoice } = useFunnel(); `, }, { id: 'user', name: 'user', signature: 'user: FunnelUser', description: 'Current SDK user profile and historical completed steps.', example: `const { user } = useFunnel(); const userEmail = user.email;`, }, { id: 'set-user', name: 'setUser', signature: 'setUser(user: FunnelUser): void', description: 'Replace user object (advanced/rare use only).', example: `const { user, setUser } = useFunnel(); setUser({ ...user, email: 'new-email@example.com' });`, }, { id: 'complete-step', name: 'completeStep', signature: 'completeStep(stepId: string, choices?: Record): void', description: 'Manually emit and persist a step completion record.', example: `const { completeStep } = useFunnel(); completeStep('email-capture');`, }, { id: 'complete-funnel', name: 'completeFunnel', signature: 'completeFunnel(stepId: string): void', description: 'Complete the active terminal step and emit the canonical funnel conversion once.', example: `const { completeFunnel } = useFunnel(); completeFunnel('subscription-started');`, }, { id: 'legacy-attributes', name: 'attributes / setAttribute', signature: 'attributes: FunnelUserAnswers, setAttribute(key: string, value: unknown): void (compat)', description: 'Compatibility aliases for older step implementations.', example: `const { attributes, setAttribute } = useFunnel(); setAttribute('legacyCustomKey', true); const value = attributes.legacyCustomKey;`, }, ]; const quickStartExample = `import { useFunnel } from '@funnelsgrove/runtime'; export function ExampleStep() { const { answers, setAnswer, goNext } = useFunnel(); return ( ); }`; type AnalyticsMethodDoc = { id: string; name: string; signature: string; description: string; example: string; }; type AnalyticsUseCaseDoc = { id: string; title: string; description: string; example: string; }; const analyticsQuickStartExample = `import { bootstrapPublicAnalyticsUser, publicAnalyticsSdk, } from '@funnelsgrove/analytics'; export async function initializeAnalytics() { await bootstrapPublicAnalyticsUser({ email: 'user@example.com', metadata: { source: 'landing' }, }); publicAnalyticsSdk.track({ eventType: 'funnel_opened', stepId: 'step-1', stepName: 'Welcome', }); }`; const analyticsUseCases: AnalyticsUseCaseDoc[] = [ { id: 'analytics-use-case-cta', title: 'Track CTA clicks and plan picks', description: 'Capture conversion intent (button clicks, plan id, variant id) without blocking UI.', example: `publicAnalyticsSdk.track({ eventType: 'cta_click', stepId: 'paywall', stepName: 'Paywall', payload: { cta: 'start_trial', planId: selectedPlanId, experimentVariant: 'v2', }, });`, }, { id: 'analytics-use-case-step-override', title: 'Complete custom interactions through FunnelFlow', description: 'FunnelFlow owns step timing and lifecycle events, including long-running custom interactions.', example: `const { goToStep } = useFunnel(); // ...custom async flow... goToStep('email-capture', { type: 'complete', selected: { uploadCount: files.length }, });`, }, { id: 'analytics-use-case-flush', title: 'Force flush before sensitive transitions', description: 'Flush before redirecting to external checkout/store links when you need best-effort delivery.', example: `await publicAnalyticsSdk.track({ eventType: 'checkout_redirect_clicked', stepId: 'paywall', metadata: { destination: 'stripe_checkout' }, }); await publicAnalyticsSdk.flush(); window.location.href = checkoutUrl;`, }, ]; const analyticsMethodDocs: AnalyticsMethodDoc[] = [ { id: 'analytics-bootstrap-public-user', name: 'bootstrapPublicAnalyticsUser', signature: 'bootstrapPublicAnalyticsUser(input?: PublicSdkIdentifyInput, client?: BootstrapPublicAnalyticsUserClient): Promise', description: 'Thin bootstrap helper that identifies the visitor, persists the funnel-scoped user_id, and returns the resolved id.', example: `const userId = await bootstrapPublicAnalyticsUser({ email: 'user@example.com', metadata: { source: 'landing' }, });`, }, { id: 'analytics-identify', name: 'identify', signature: 'identify(input?: PublicSdkIdentifyInput): Promise', description: 'Bootstraps or resolves the funnel-scoped user_id through /sdk/public/users/bootstrap and persists it locally.', example: `const userId = await publicAnalyticsSdk.identify({ user_id: existingIdFromCookie, email: userEmail, fullName: userName, });`, }, { id: 'analytics-track', name: 'track', signature: 'track(event: PublicSdkTrackEventInput): string | null', description: 'Queues custom events only. Returns null for empty or contract-reserved event names; use their owning FunnelFlow or named helper path.', example: `const eventId = publicAnalyticsSdk.track({ eventType: 'paywall_offer_seen', stepId: 'paywall', payload: { offerSetId: activeOfferSetId }, });`, }, { id: 'analytics-track-step-started', name: 'trackStepStarted', signature: 'trackStepStarted(input: PublicSdkStepStartInput): string | null', description: 'FunnelFlow-owned adapter method for canonical step_start events; step components must not call it.', example: `const { goNext } = useFunnel(); goNext(); // FunnelFlow records the current completion and starts the next visit.`, }, { id: 'analytics-track-step-completed', name: 'trackStepCompleted', signature: 'trackStepCompleted(input: PublicSdkStepEndInput): string | null', description: 'FunnelFlow-owned adapter method for canonical step_end events; step components must not call it.', example: `const { goToStep } = useFunnel(); goToStep('email-capture', { type: 'complete', selected: { painScore: 7 }, });`, }, { id: 'analytics-flush', name: 'flush', signature: 'flush(): Promise', description: 'Attempts to deliver queued events immediately. Returns number of events sent in this flush run.', example: `const sentCount = await publicAnalyticsSdk.flush(); console.log('analytics events sent', sentCount);`, }, { id: 'analytics-get-current-user-id', name: 'getCurrentUserId', signature: 'getCurrentUserId(): string', description: 'Returns the active funnel-scoped user_id used for analytics envelopes.', example: `const userId = publicAnalyticsSdk.getCurrentUserId();`, }, ]; export default function SdkDocsPage() { return (

Funnel SDK

Runtime + Analytics SDK Reference

Typed `useFunnel()` runtime reference plus analytics SDK event patterns and practical implementation examples.

User Object (Start Here)

This is the canonical runtime answer payload available as `useFunnel().answers`. Keep this aligned with `@funnelsgrove/runtime` and `docs/funnelsgrove/contracts/content-answers.md`.

              {userObjectExample}
            

Quick Start

Import `useFunnel()` in any step and write typed answer keys before moving forward.

              {quickStartExample}
            

Analytics SDK

Use `publicAnalyticsSdk` from `@funnelsgrove/analytics` for custom analytics events. It batches events and sends them through the first-party `/ingest` proxy using `navigator.sendBeacon`, with `fetch(keepalive)` fallback.

Standard step start/complete events are already emitted by `FunnelFlow`. Add custom events only for business-specific moments (CTA clicks, upload completed, checkout redirect, variant exposure).

Analytics Quick Start

Bootstrap the visitor once, then emit custom events from steps/components as needed.

              {analyticsQuickStartExample}
            

Analytics Use Cases

Reference implementations for the most common customization patterns.

{analyticsUseCases.map((useCase) => (

{useCase.title}

{useCase.description}

                {useCase.example}
              
))}

Analytics Methods

Public method reference for `bootstrapPublicAnalyticsUser` and `publicAnalyticsSdk`.

{analyticsMethodDocs.map((methodDoc) => (

analytics.{methodDoc.name}

{methodDoc.signature}

{methodDoc.description}

                {methodDoc.example}
              
))}

All SDK Methods

Reference for every field/function currently exposed by `@funnelsgrove/runtime`.

{methodDocs.map((methodDoc) => (

{methodDoc.name}

{methodDoc.signature}

{methodDoc.description}

                {methodDoc.example}
              
))}
{sdkDocsStyles ? : null}
); } const sdkDocsStyles = ` .sdk-docs { position: relative; min-height: 100svh; padding: 36px 24px 48px; background: linear-gradient(180deg, #f5f8ff 0%, #edf3ff 55%, #e6eefb 100%); color: #1f2740; font-family: var(--font-family-base); } .sdk-docs__backdrop { position: fixed; inset: 0; pointer-events: none; background: radial-gradient(circle at 10% 8%, rgba(63, 81, 181, 0.17), transparent 46%), radial-gradient(circle at 90% 14%, rgba(77, 181, 63, 0.15), transparent 44%); z-index: 0; } .sdk-docs__hero, .sdk-docs__layout { position: relative; z-index: 1; width: min(1180px, 100%); margin: 0 auto; } .sdk-docs__hero { border: 1px solid rgba(95, 114, 205, 0.26); border-radius: 22px; padding: 26px; background: linear-gradient(135deg, rgba(255, 255, 255, 0.93), rgba(240, 246, 255, 0.92)); box-shadow: 0 16px 38px rgba(31, 39, 64, 0.09); } .sdk-docs__eyebrow { margin: 0; color: var(--color-primary); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } .sdk-docs__title { margin: 10px 0 0; color: #1f2740; font-size: 40px; line-height: 1.08; font-weight: 800; } .sdk-docs__subtitle { margin: 14px 0 0; color: #475179; max-width: 760px; font-size: 17px; line-height: 1.45; } .sdk-docs__layout { margin-top: 20px; display: grid; grid-template-columns: 270px minmax(0, 1fr); gap: 20px; align-items: start; } .sdk-docs__sidebar { position: sticky; top: 20px; } .sdk-docs__nav { display: flex; flex-direction: column; gap: 6px; border: 1px solid rgba(95, 114, 205, 0.2); border-radius: 18px; background: rgba(255, 255, 255, 0.85); padding: 12px; box-shadow: 0 10px 24px rgba(31, 39, 64, 0.07); } .sdk-docs__nav a { border-radius: 10px; padding: 8px 10px; text-decoration: none; color: #3a4366; font-size: 13px; font-weight: 600; line-height: 1.25; } .sdk-docs__nav a:hover { background: rgba(63, 81, 181, 0.12); color: #25337f; } .sdk-docs__content { display: flex; flex-direction: column; gap: 16px; } .sdk-docs__section { border: 1px solid rgba(95, 114, 205, 0.2); border-radius: 18px; background: rgba(255, 255, 255, 0.88); padding: 20px; box-shadow: 0 10px 24px rgba(31, 39, 64, 0.07); } .sdk-docs__section h2, .sdk-docs__section h3 { margin: 0; color: #222f77; font-family: var(--font-family-display); } .sdk-docs__section h2 { font-size: 28px; line-height: 1.2; } .sdk-docs__section h3 { font-size: 24px; line-height: 1.2; } .sdk-docs__section p { margin: 10px 0 0; color: #495175; font-size: 15px; line-height: 1.48; } .sdk-docs__signature { margin-top: 8px; border: 1px dashed rgba(63, 81, 181, 0.35); border-radius: 10px; background: rgba(238, 243, 255, 0.9); padding: 10px 12px; color: #27316d; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 12px; line-height: 1.45; overflow-x: auto; } .sdk-docs pre { margin: 14px 0 0; border: 1px solid #d6defe; border-radius: 14px; background: #151a2e; color: #eaf0ff; padding: 14px; overflow-x: auto; } .sdk-docs code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 12px; line-height: 1.6; } .sdk-docs__method { scroll-margin-top: 20px; } @media (max-width: 1020px) { .sdk-docs__layout { grid-template-columns: 1fr; } .sdk-docs__sidebar { position: static; } .sdk-docs__nav { max-height: 260px; overflow-y: auto; } } @media (max-width: 640px) { .sdk-docs { padding: 20px 14px 30px; } .sdk-docs__hero { padding: 18px; } .sdk-docs__title { font-size: 30px; } .sdk-docs__subtitle { font-size: 15px; } .sdk-docs__section { padding: 16px; } .sdk-docs__section h2 { font-size: 24px; } .sdk-docs__section h3 { font-size: 20px; } } `;