@fanvue/builder-sdk 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -1
- package/dist/bridge/index.d.ts +2 -0
- package/dist/bridge/index.js +3 -0
- package/dist/bridge-CGVtI3hr.js +336 -0
- package/dist/bridge-CGVtI3hr.js.map +1 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core-BqdYUMrJ.js +1653 -0
- package/dist/core-BqdYUMrJ.js.map +1 -0
- package/dist/index-BRDYLBlc.d.ts +223 -0
- package/dist/index-BRDYLBlc.d.ts.map +1 -0
- package/dist/{index-CweVyIKX.d.ts → index-C4ewLil3.d.ts} +2 -2
- package/dist/{index-CweVyIKX.d.ts.map → index-C4ewLil3.d.ts.map} +1 -1
- package/dist/index-ClbZoV_Z.d.ts +420 -0
- package/dist/index-ClbZoV_Z.d.ts.map +1 -0
- package/dist/{index-pS9wR5yg.d.ts → index-Dr-mZ0qP.d.ts} +500 -901
- package/dist/index-Dr-mZ0qP.d.ts.map +1 -0
- package/dist/nextjs/embedded-app/index.d.ts +2 -2
- package/dist/nextjs/embedded-app/index.js +3 -2
- package/dist/nextjs/embedded-app/index.js.map +1 -1
- package/dist/nextjs/off-platform/index.d.ts +2 -2
- package/dist/nextjs/off-platform/index.js +3 -2
- package/dist/nextjs/off-platform/index.js.map +1 -1
- package/dist/{nextjs-CESI_EiU.js → nextjs-B5Tqgt_n.js} +2 -2
- package/dist/{nextjs-CESI_EiU.js.map → nextjs-B5Tqgt_n.js.map} +1 -1
- package/dist/react/index.d.ts +37 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +61 -2
- package/dist/react/index.js.map +1 -1
- package/dist/{core-CvVOMyqr.js → schemas-DbyHF7Xi.js} +261 -1662
- package/dist/schemas-DbyHF7Xi.js.map +1 -0
- package/package.json +20 -16
- package/dist/core-CvVOMyqr.js.map +0 -1
- package/dist/index-pS9wR5yg.d.ts.map +0 -1
package/dist/react/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as AnalyticsProperties } from "../index-BRDYLBlc.js";
|
|
2
|
+
import { c as FanvueTheme } from "../index-ClbZoV_Z.js";
|
|
2
3
|
import { ReactNode } from "react";
|
|
3
4
|
|
|
4
5
|
//#region src/react/auth-context.d.ts
|
|
@@ -92,5 +93,39 @@ interface UseEmbeddedAuthResult {
|
|
|
92
93
|
*/
|
|
93
94
|
declare function useEmbeddedAuth(opts?: UseEmbeddedAuthOptions): UseEmbeddedAuthResult;
|
|
94
95
|
//#endregion
|
|
95
|
-
|
|
96
|
+
//#region src/react/use-fanvue-analytics.d.ts
|
|
97
|
+
/**
|
|
98
|
+
* The value returned by {@link useFanvueAnalytics}.
|
|
99
|
+
*
|
|
100
|
+
* @property track - Fires an event through the host page's Amplitude client.
|
|
101
|
+
* Fire-and-forget: it never throws and no-ops when the app is not embedded in
|
|
102
|
+
* Fanvue or was not granted analytics.
|
|
103
|
+
* @property isEnabled - Whether events are actually reaching Fanvue. `false`
|
|
104
|
+
* until the handshake completes, and permanently `false` outside Fanvue.
|
|
105
|
+
*/
|
|
106
|
+
interface UseFanvueAnalyticsResult {
|
|
107
|
+
track: (eventName: string, properties?: AnalyticsProperties) => void;
|
|
108
|
+
isEnabled: boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Fires analytics events through the Fanvue page hosting this app.
|
|
112
|
+
*
|
|
113
|
+
* Events land in Fanvue's Amplitude project namespaced as `embedded_app_<eventName>`,
|
|
114
|
+
* attributed to the app and stitched into the viewer's session — no identity
|
|
115
|
+
* data crosses the frame boundary. Outside Fanvue (local dev, previews, a
|
|
116
|
+
* standalone deployment) `track` silently does nothing, so app code can call it
|
|
117
|
+
* unconditionally.
|
|
118
|
+
*
|
|
119
|
+
* @returns A stable {@link UseFanvueAnalyticsResult.track} and whether events
|
|
120
|
+
* are currently reaching Fanvue.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* function CreateCourseButton() {
|
|
124
|
+
* const { track } = useFanvueAnalytics();
|
|
125
|
+
* return <button onClick={() => track('course_created', { chapters: 4 })}>Create</button>;
|
|
126
|
+
* }
|
|
127
|
+
*/
|
|
128
|
+
declare function useFanvueAnalytics(): UseFanvueAnalyticsResult;
|
|
129
|
+
//#endregion
|
|
130
|
+
export { type AuthContextValue, AuthProvider, type EmbeddedAuthStatus, type FanvueTheme, type UseEmbeddedAuthOptions, type UseEmbeddedAuthResult, type UseFanvueAnalyticsResult, useAuth, useEmbeddedAuth, useFanvueAnalytics };
|
|
96
131
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/react/auth-context.tsx","../../src/react/use-embedded-auth.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/react/auth-context.tsx","../../src/react/use-embedded-auth.ts","../../src/react/use-fanvue-analytics.ts"],"mappings":";;;;;;;;;;;UAuDiB,gBAAA;EACf,GAAA;EACA,eAAA;EACA,MAAA,GAAS,GAAA;EACT,QAAA;EACA,SAAA,GAAY,KAAA,EAAO,WAAA,GAAc,GAAA,EAAK,IAAA,GAAO,WAAA,KAAgB,OAAA,CAAQ,QAAA;AAAA;;;;;;;iBAWvD,YAAA,CAAa,KAAA;EAAS,QAAA,EAAU,SAAA;AAAA,IAAc,SAAA;;;;;;;;iBA6D9C,OAAA,CAAA,GAAW,gBAAA;;;;;;;AA7E3B;;;;;KCpCY,kBAAA;;;;;;;UAQK,sBAAA;EACf,YAAA;AAAA;;;;;;;;;;;UAae,qBAAA;EACf,MAAA,EAAQ,kBAAA;EACR,KAAA;EACA,KAAA,EAAO,WAAA;AAAA;;;;;;ADwFT;;;;;;;;ACjHA;;;;;AAQA;;;;iBA0CgB,eAAA,CAAgB,IAAA,GAAO,sBAAA,GAAyB,qBAAA;;;;;;;ADdhE;;;;;UEhCiB,wBAAA;EACf,KAAA,GAAQ,SAAA,UAAmB,UAAA,GAAa,mBAAA;EACxC,SAAA;AAAA;;;;;;;;;;;;;;;;;AF8CF;;iBEHgB,kBAAA,CAAA,GAAsB,wBAAA"}
|
package/dist/react/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import
|
|
2
|
+
import "../schemas-DbyHF7Xi.js";
|
|
3
|
+
import { C as HEADER_UPDATED_SESSION, S as BEARER_PREFIX, i as getThemeFromUrl, r as getSessionTokenFromUrl } from "../core-BqdYUMrJ.js";
|
|
4
|
+
import { t as connectFanvueBridge } from "../bridge-CGVtI3hr.js";
|
|
3
5
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
6
|
import { jsx } from "react/jsx-runtime";
|
|
5
7
|
//#region src/react/auth-context.tsx
|
|
@@ -187,6 +189,63 @@ function useEmbeddedAuth(opts) {
|
|
|
187
189
|
};
|
|
188
190
|
}
|
|
189
191
|
//#endregion
|
|
190
|
-
|
|
192
|
+
//#region src/react/use-fanvue-analytics.ts
|
|
193
|
+
/**
|
|
194
|
+
* The connection is shared by every caller: the handshake costs a round trip
|
|
195
|
+
* and the host issues one port per frame, so a page with several components
|
|
196
|
+
* tracking events must not open several bridges.
|
|
197
|
+
*/
|
|
198
|
+
let sharedBridge = null;
|
|
199
|
+
/** Connects on first use, then returns the same connection to every caller. */
|
|
200
|
+
function getSharedBridge() {
|
|
201
|
+
sharedBridge ??= connectFanvueBridge().then((result) => {
|
|
202
|
+
if (result.isErr() && result.error.code === "TIMEOUT") sharedBridge = null;
|
|
203
|
+
return result;
|
|
204
|
+
});
|
|
205
|
+
return sharedBridge;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Fires analytics events through the Fanvue page hosting this app.
|
|
209
|
+
*
|
|
210
|
+
* Events land in Fanvue's Amplitude project namespaced as `embedded_app_<eventName>`,
|
|
211
|
+
* attributed to the app and stitched into the viewer's session — no identity
|
|
212
|
+
* data crosses the frame boundary. Outside Fanvue (local dev, previews, a
|
|
213
|
+
* standalone deployment) `track` silently does nothing, so app code can call it
|
|
214
|
+
* unconditionally.
|
|
215
|
+
*
|
|
216
|
+
* @returns A stable {@link UseFanvueAnalyticsResult.track} and whether events
|
|
217
|
+
* are currently reaching Fanvue.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* function CreateCourseButton() {
|
|
221
|
+
* const { track } = useFanvueAnalytics();
|
|
222
|
+
* return <button onClick={() => track('course_created', { chapters: 4 })}>Create</button>;
|
|
223
|
+
* }
|
|
224
|
+
*/
|
|
225
|
+
function useFanvueAnalytics() {
|
|
226
|
+
const [isEnabled, setIsEnabled] = useState(false);
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
let active = true;
|
|
229
|
+
getSharedBridge().then((result) => {
|
|
230
|
+
if (!active) return;
|
|
231
|
+
setIsEnabled(result.isOk() && result.value.has("analytics"));
|
|
232
|
+
});
|
|
233
|
+
return () => {
|
|
234
|
+
active = false;
|
|
235
|
+
};
|
|
236
|
+
}, []);
|
|
237
|
+
return {
|
|
238
|
+
track: useCallback((eventName, properties) => {
|
|
239
|
+
getSharedBridge().then(async (result) => {
|
|
240
|
+
if (result.isErr()) return;
|
|
241
|
+
const tracked = await result.value.analytics.track(eventName, properties);
|
|
242
|
+
if (tracked.isErr() && tracked.error.code === "invalid_payload" && process.env["NODE_ENV"] !== "production") console.warn(`[fanvue/analytics] dropped "${eventName}": ${tracked.error.message}`);
|
|
243
|
+
});
|
|
244
|
+
}, []),
|
|
245
|
+
isEnabled
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
export { AuthProvider, useAuth, useEmbeddedAuth, useFanvueAnalytics };
|
|
191
250
|
|
|
192
251
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/react/auth-context.tsx","../../src/react/use-embedded-auth.ts"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { BEARER_PREFIX, HEADER_UPDATED_SESSION } from '../core/index.js';\n\nconst STORAGE_KEY = 'fanvue:jwt';\n\n/**\n * Retrieve the JWT from session storage, or `null` if unavailable.\n *\n * Exported for sibling hooks that need a synchronous storage check inside\n * effects (which can run before this provider's mount effect has loaded the\n * stored JWT into state). Not part of the public package API.\n */\nexport function getStoredJwt(): string | null {\n try {\n return sessionStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\n/** Persist the JWT into session storage. */\nfunction setStoredJwt(jwt: string): void {\n try {\n sessionStorage.setItem(STORAGE_KEY, jwt);\n } catch {\n // sessionStorage may be unavailable (e.g. sandboxed iframe)\n }\n}\n\n/** Remove the JWT from session storage. */\nfunction removeStoredJwt(): void {\n try {\n sessionStorage.removeItem(STORAGE_KEY);\n } catch {\n // sessionStorage may be unavailable\n }\n}\n\n/**\n * The value exposed by {@link AuthProvider} via React context.\n *\n * Includes the current JWT, authentication status, and helpers for\n * managing credentials and making authenticated requests.\n */\nexport interface AuthContextValue {\n jwt: string | null;\n isAuthenticated: boolean;\n setJwt: (jwt: string) => void;\n clearJwt: () => void;\n authFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n}\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\n/**\n * Provides authentication state to the React component tree.\n *\n * Wrap your application (or the authenticated section) with this provider\n * so that descendant components can access auth helpers via {@link useAuth}.\n */\nexport function AuthProvider(props: { children: ReactNode }): ReactNode {\n const { children } = props;\n // Start at null so the first client render matches the server-rendered HTML\n // (the server can't see sessionStorage); load any stored JWT after mount.\n // Reading storage in the useState initializer causes hydration mismatches\n // (React error #418) whenever a JWT is already stored.\n const [jwt, setJwtState] = useState<string | null>(null);\n\n useEffect(() => {\n const stored = getStoredJwt();\n if (stored !== null) {\n setJwtState(stored);\n }\n }, []);\n\n const setJwt = useCallback((token: string): void => {\n setStoredJwt(token);\n setJwtState(token);\n }, []);\n\n const clearJwt = useCallback((): void => {\n removeStoredJwt();\n setJwtState(null);\n }, []);\n\n const authFetch = useCallback(\n async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const currentJwt = getStoredJwt();\n const headers = new Headers(init?.headers);\n if (currentJwt) {\n headers.set('Authorization', `${BEARER_PREFIX}${currentJwt}`);\n }\n\n const response = await fetch(input, { ...init, headers });\n\n const updatedToken = response.headers.get(HEADER_UPDATED_SESSION);\n if (updatedToken) {\n setStoredJwt(updatedToken);\n setJwtState(updatedToken);\n }\n\n return response;\n },\n [],\n );\n\n const value = useMemo<AuthContextValue>(\n () => ({ jwt, isAuthenticated: jwt != null, setJwt, clearJwt, authFetch }),\n [jwt, setJwt, clearJwt, authFetch],\n );\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n/**\n * Access the current authentication context.\n *\n * Must be called from a component that is a descendant of {@link AuthProvider}.\n *\n * @throws {Error} If called outside of an `AuthProvider`.\n */\nexport function useAuth(): AuthContextValue {\n const ctx = useContext(AuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within an <AuthProvider>');\n }\n return ctx;\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { getSessionTokenFromUrl, getThemeFromUrl, type FanvueTheme } from '../core/index.js';\n\nimport { useAuth, getStoredJwt } from './auth-context.js';\n\nconst DEFAULT_EXCHANGE_PATH = '/api/fanvue/session';\n\n/**\n * The state of the embedded session exchange.\n *\n * - `idle` — no session token in the URL (the page was not opened from a\n * Fanvue embedded surface) and no existing session.\n * - `exchanging` — the session token is being exchanged with the backend.\n * - `authenticated` — a session JWT is available; `authFetch` will attach it.\n * - `error` — the exchange failed; see `error` for the cause.\n */\nexport type EmbeddedAuthStatus = 'idle' | 'exchanging' | 'authenticated' | 'error';\n\n/**\n * Options for {@link useEmbeddedAuth}.\n *\n * @property exchangePath - The path of the session-exchange route created by\n * `createSessionExchangeHandler`. Defaults to `/api/fanvue/session`.\n */\nexport interface UseEmbeddedAuthOptions {\n exchangePath?: string;\n}\n\n/**\n * The value returned by {@link useEmbeddedAuth}.\n *\n * @property status - The current state of the embedded session exchange.\n * @property error - The error code when `status` is `error` (e.g.\n * `consent_required`, `invalid_session_token`), otherwise `null`.\n * @property theme - The creator's active colour scheme (`'light'` or `'dark'`)\n * read from the iframe URL, or `null` when absent (e.g. the app was opened\n * outside Fanvue). Use it to theme-match Fanvue.\n */\nexport interface UseEmbeddedAuthResult {\n status: EmbeddedAuthStatus;\n error: string | null;\n theme: FanvueTheme | null;\n}\n\n/**\n * Completes embedded authentication when the app is opened inside Fanvue.\n *\n * On mount, reads the short-lived session token that Fanvue appends to the\n * iframe URL (`?token=`), exchanges it with your backend session route\n * (created by `createSessionExchangeHandler`), and stores the returned\n * session JWT in the auth context — after which `authFetch` automatically\n * authenticates your API calls.\n *\n * Must be used inside an `AuthProvider`.\n *\n * @param opts - Optional overrides (e.g. a custom `exchangePath`).\n * @returns The exchange {@link EmbeddedAuthStatus} and error code, if any.\n *\n * @example\n * function EmbeddedHome() {\n * const { status, error, theme } = useEmbeddedAuth();\n * if (status === 'exchanging') return <p>Connecting…</p>;\n * if (status === 'error') return <p>Auth failed: {error}</p>;\n * return <Dashboard theme={theme ?? 'light'} />;\n * }\n */\nexport function useEmbeddedAuth(opts?: UseEmbeddedAuthOptions): UseEmbeddedAuthResult {\n const { isAuthenticated, setJwt } = useAuth();\n const [status, setStatus] = useState<EmbeddedAuthStatus>(\n isAuthenticated ? 'authenticated' : 'idle',\n );\n const [error, setError] = useState<string | null>(null);\n // Start at null so the first client render matches the server-rendered HTML\n // (the server can't read window.location); read the theme after mount.\n const [theme, setTheme] = useState<FanvueTheme | null>(null);\n const startedRef = useRef(false);\n const exchangePath = opts?.exchangePath ?? DEFAULT_EXCHANGE_PATH;\n\n useEffect(() => {\n // Guard against React Strict Mode double-invocation: the session token is\n // single-use, so the exchange must only run once.\n if (startedRef.current) return;\n startedRef.current = true;\n\n // The theme is read from the URL independently of the (single-use) token\n // exchange, so capture it before any of the auth-status early returns.\n setTheme(getThemeFromUrl(window.location.href));\n\n // Check storage directly as well as context state: this effect runs\n // before the provider's mount effect has loaded any stored JWT, and the\n // session token in the URL is single-use — a re-exchange would fail.\n if (isAuthenticated || getStoredJwt() !== null) {\n setStatus('authenticated');\n return;\n }\n\n const token = getSessionTokenFromUrl(window.location.href);\n if (!token) {\n setStatus('idle');\n return;\n }\n\n setStatus('exchanging');\n\n void (async (): Promise<void> => {\n let response: Response;\n try {\n response = await fetch(exchangePath, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token }),\n });\n } catch {\n setError('network_error');\n setStatus('error');\n return;\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = null;\n }\n\n if (!response.ok) {\n const errorCode =\n body !== null && typeof body === 'object' && 'error' in body\n ? String((body as { error: unknown }).error)\n : 'exchange_failed';\n setError(errorCode);\n setStatus('error');\n return;\n }\n\n const jwt =\n body !== null && typeof body === 'object' && 'jwt' in body\n ? (body as { jwt: unknown }).jwt\n : null;\n if (typeof jwt !== 'string' || jwt.length === 0) {\n setError('exchange_failed');\n setStatus('error');\n return;\n }\n\n setJwt(jwt);\n setStatus('authenticated');\n })();\n }, [exchangePath, isAuthenticated, setJwt]);\n\n return { status, error, theme };\n}\n"],"mappings":";;;;;AAcA,MAAM,cAAc;;;;;;;;AASpB,SAAgB,eAA8B;AAC5C,KAAI;AACF,SAAO,eAAe,QAAQ,YAAY;SACpC;AACN,SAAO;;;;AAKX,SAAS,aAAa,KAAmB;AACvC,KAAI;AACF,iBAAe,QAAQ,aAAa,IAAI;SAClC;;;AAMV,SAAS,kBAAwB;AAC/B,KAAI;AACF,iBAAe,WAAW,YAAY;SAChC;;AAmBV,MAAM,cAAc,cAAuC,KAAK;;;;;;;AAQhE,SAAgB,aAAa,OAA2C;CACtE,MAAM,EAAE,aAAa;CAKrB,MAAM,CAAC,KAAK,eAAe,SAAwB,KAAK;AAExD,iBAAgB;EACd,MAAM,SAAS,cAAc;AAC7B,MAAI,WAAW,KACb,aAAY,OAAO;IAEpB,EAAE,CAAC;CAEN,MAAM,SAAS,aAAa,UAAwB;AAClD,eAAa,MAAM;AACnB,cAAY,MAAM;IACjB,EAAE,CAAC;CAEN,MAAM,WAAW,kBAAwB;AACvC,mBAAiB;AACjB,cAAY,KAAK;IAChB,EAAE,CAAC;CAEN,MAAM,YAAY,YAChB,OAAO,OAA0B,SAA0C;EACzE,MAAM,aAAa,cAAc;EACjC,MAAM,UAAU,IAAI,QAAQ,MAAM,QAAQ;AAC1C,MAAI,WACF,SAAQ,IAAI,iBAAiB,GAAG,gBAAgB,aAAa;EAG/D,MAAM,WAAW,MAAM,MAAM,OAAO;GAAE,GAAG;GAAM;GAAS,CAAC;EAEzD,MAAM,eAAe,SAAS,QAAQ,IAAI,uBAAuB;AACjE,MAAI,cAAc;AAChB,gBAAa,aAAa;AAC1B,eAAY,aAAa;;AAG3B,SAAO;IAET,EAAE,CACH;CAED,MAAM,QAAQ,eACL;EAAE;EAAK,iBAAiB,OAAO;EAAM;EAAQ;EAAU;EAAW,GACzE;EAAC;EAAK;EAAQ;EAAU;EAAU,CACnC;AAED,QAAO,oBAAC,YAAY,UAAb;EAA6B;EAAQ;EAAgC,CAAA;;;;;;;;;AAU9E,SAAgB,UAA4B;CAC1C,MAAM,MAAM,WAAW,YAAY;AACnC,KAAI,CAAC,IACH,OAAM,IAAI,MAAM,gDAAgD;AAElE,QAAO;;;;ACjIT,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;AA6D9B,SAAgB,gBAAgB,MAAsD;CACpF,MAAM,EAAE,iBAAiB,WAAW,SAAS;CAC7C,MAAM,CAAC,QAAQ,aAAa,SAC1B,kBAAkB,kBAAkB,OACrC;CACD,MAAM,CAAC,OAAO,YAAY,SAAwB,KAAK;CAGvD,MAAM,CAAC,OAAO,YAAY,SAA6B,KAAK;CAC5D,MAAM,aAAa,OAAO,MAAM;CAChC,MAAM,eAAe,MAAM,gBAAgB;AAE3C,iBAAgB;AAGd,MAAI,WAAW,QAAS;AACxB,aAAW,UAAU;AAIrB,WAAS,gBAAgB,OAAO,SAAS,KAAK,CAAC;AAK/C,MAAI,mBAAmB,cAAc,KAAK,MAAM;AAC9C,aAAU,gBAAgB;AAC1B;;EAGF,MAAM,QAAQ,uBAAuB,OAAO,SAAS,KAAK;AAC1D,MAAI,CAAC,OAAO;AACV,aAAU,OAAO;AACjB;;AAGF,YAAU,aAAa;AAEvB,GAAM,YAA2B;GAC/B,IAAI;AACJ,OAAI;AACF,eAAW,MAAM,MAAM,cAAc;KACnC,QAAQ;KACR,SAAS,EAAE,gBAAgB,oBAAoB;KAC/C,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;KAChC,CAAC;WACI;AACN,aAAS,gBAAgB;AACzB,cAAU,QAAQ;AAClB;;GAGF,IAAI;AACJ,OAAI;AACF,WAAO,MAAM,SAAS,MAAM;WACtB;AACN,WAAO;;AAGT,OAAI,CAAC,SAAS,IAAI;AAKhB,aAHE,SAAS,QAAQ,OAAO,SAAS,YAAY,WAAW,OACpD,OAAQ,KAA4B,MAAM,GAC1C,kBACa;AACnB,cAAU,QAAQ;AAClB;;GAGF,MAAM,MACJ,SAAS,QAAQ,OAAO,SAAS,YAAY,SAAS,OACjD,KAA0B,MAC3B;AACN,OAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAC/C,aAAS,kBAAkB;AAC3B,cAAU,QAAQ;AAClB;;AAGF,UAAO,IAAI;AACX,aAAU,gBAAgB;MACxB;IACH;EAAC;EAAc;EAAiB;EAAO,CAAC;AAE3C,QAAO;EAAE;EAAQ;EAAO;EAAO"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/react/auth-context.tsx","../../src/react/use-embedded-auth.ts","../../src/react/use-fanvue-analytics.ts"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { BEARER_PREFIX, HEADER_UPDATED_SESSION } from '../core/index.js';\n\nconst STORAGE_KEY = 'fanvue:jwt';\n\n/**\n * Retrieve the JWT from session storage, or `null` if unavailable.\n *\n * Exported for sibling hooks that need a synchronous storage check inside\n * effects (which can run before this provider's mount effect has loaded the\n * stored JWT into state). Not part of the public package API.\n */\nexport function getStoredJwt(): string | null {\n try {\n return sessionStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\n/** Persist the JWT into session storage. */\nfunction setStoredJwt(jwt: string): void {\n try {\n sessionStorage.setItem(STORAGE_KEY, jwt);\n } catch {\n // sessionStorage may be unavailable (e.g. sandboxed iframe)\n }\n}\n\n/** Remove the JWT from session storage. */\nfunction removeStoredJwt(): void {\n try {\n sessionStorage.removeItem(STORAGE_KEY);\n } catch {\n // sessionStorage may be unavailable\n }\n}\n\n/**\n * The value exposed by {@link AuthProvider} via React context.\n *\n * Includes the current JWT, authentication status, and helpers for\n * managing credentials and making authenticated requests.\n */\nexport interface AuthContextValue {\n jwt: string | null;\n isAuthenticated: boolean;\n setJwt: (jwt: string) => void;\n clearJwt: () => void;\n authFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n}\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\n/**\n * Provides authentication state to the React component tree.\n *\n * Wrap your application (or the authenticated section) with this provider\n * so that descendant components can access auth helpers via {@link useAuth}.\n */\nexport function AuthProvider(props: { children: ReactNode }): ReactNode {\n const { children } = props;\n // Start at null so the first client render matches the server-rendered HTML\n // (the server can't see sessionStorage); load any stored JWT after mount.\n // Reading storage in the useState initializer causes hydration mismatches\n // (React error #418) whenever a JWT is already stored.\n const [jwt, setJwtState] = useState<string | null>(null);\n\n useEffect(() => {\n const stored = getStoredJwt();\n if (stored !== null) {\n setJwtState(stored);\n }\n }, []);\n\n const setJwt = useCallback((token: string): void => {\n setStoredJwt(token);\n setJwtState(token);\n }, []);\n\n const clearJwt = useCallback((): void => {\n removeStoredJwt();\n setJwtState(null);\n }, []);\n\n const authFetch = useCallback(\n async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const currentJwt = getStoredJwt();\n const headers = new Headers(init?.headers);\n if (currentJwt) {\n headers.set('Authorization', `${BEARER_PREFIX}${currentJwt}`);\n }\n\n const response = await fetch(input, { ...init, headers });\n\n const updatedToken = response.headers.get(HEADER_UPDATED_SESSION);\n if (updatedToken) {\n setStoredJwt(updatedToken);\n setJwtState(updatedToken);\n }\n\n return response;\n },\n [],\n );\n\n const value = useMemo<AuthContextValue>(\n () => ({ jwt, isAuthenticated: jwt != null, setJwt, clearJwt, authFetch }),\n [jwt, setJwt, clearJwt, authFetch],\n );\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n/**\n * Access the current authentication context.\n *\n * Must be called from a component that is a descendant of {@link AuthProvider}.\n *\n * @throws {Error} If called outside of an `AuthProvider`.\n */\nexport function useAuth(): AuthContextValue {\n const ctx = useContext(AuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within an <AuthProvider>');\n }\n return ctx;\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { getSessionTokenFromUrl, getThemeFromUrl, type FanvueTheme } from '../core/index.js';\n\nimport { useAuth, getStoredJwt } from './auth-context.js';\n\nconst DEFAULT_EXCHANGE_PATH = '/api/fanvue/session';\n\n/**\n * The state of the embedded session exchange.\n *\n * - `idle` — no session token in the URL (the page was not opened from a\n * Fanvue embedded surface) and no existing session.\n * - `exchanging` — the session token is being exchanged with the backend.\n * - `authenticated` — a session JWT is available; `authFetch` will attach it.\n * - `error` — the exchange failed; see `error` for the cause.\n */\nexport type EmbeddedAuthStatus = 'idle' | 'exchanging' | 'authenticated' | 'error';\n\n/**\n * Options for {@link useEmbeddedAuth}.\n *\n * @property exchangePath - The path of the session-exchange route created by\n * `createSessionExchangeHandler`. Defaults to `/api/fanvue/session`.\n */\nexport interface UseEmbeddedAuthOptions {\n exchangePath?: string;\n}\n\n/**\n * The value returned by {@link useEmbeddedAuth}.\n *\n * @property status - The current state of the embedded session exchange.\n * @property error - The error code when `status` is `error` (e.g.\n * `consent_required`, `invalid_session_token`), otherwise `null`.\n * @property theme - The creator's active colour scheme (`'light'` or `'dark'`)\n * read from the iframe URL, or `null` when absent (e.g. the app was opened\n * outside Fanvue). Use it to theme-match Fanvue.\n */\nexport interface UseEmbeddedAuthResult {\n status: EmbeddedAuthStatus;\n error: string | null;\n theme: FanvueTheme | null;\n}\n\n/**\n * Completes embedded authentication when the app is opened inside Fanvue.\n *\n * On mount, reads the short-lived session token that Fanvue appends to the\n * iframe URL (`?token=`), exchanges it with your backend session route\n * (created by `createSessionExchangeHandler`), and stores the returned\n * session JWT in the auth context — after which `authFetch` automatically\n * authenticates your API calls.\n *\n * Must be used inside an `AuthProvider`.\n *\n * @param opts - Optional overrides (e.g. a custom `exchangePath`).\n * @returns The exchange {@link EmbeddedAuthStatus} and error code, if any.\n *\n * @example\n * function EmbeddedHome() {\n * const { status, error, theme } = useEmbeddedAuth();\n * if (status === 'exchanging') return <p>Connecting…</p>;\n * if (status === 'error') return <p>Auth failed: {error}</p>;\n * return <Dashboard theme={theme ?? 'light'} />;\n * }\n */\nexport function useEmbeddedAuth(opts?: UseEmbeddedAuthOptions): UseEmbeddedAuthResult {\n const { isAuthenticated, setJwt } = useAuth();\n const [status, setStatus] = useState<EmbeddedAuthStatus>(\n isAuthenticated ? 'authenticated' : 'idle',\n );\n const [error, setError] = useState<string | null>(null);\n // Start at null so the first client render matches the server-rendered HTML\n // (the server can't read window.location); read the theme after mount.\n const [theme, setTheme] = useState<FanvueTheme | null>(null);\n const startedRef = useRef(false);\n const exchangePath = opts?.exchangePath ?? DEFAULT_EXCHANGE_PATH;\n\n useEffect(() => {\n // Guard against React Strict Mode double-invocation: the session token is\n // single-use, so the exchange must only run once.\n if (startedRef.current) return;\n startedRef.current = true;\n\n // The theme is read from the URL independently of the (single-use) token\n // exchange, so capture it before any of the auth-status early returns.\n setTheme(getThemeFromUrl(window.location.href));\n\n // Check storage directly as well as context state: this effect runs\n // before the provider's mount effect has loaded any stored JWT, and the\n // session token in the URL is single-use — a re-exchange would fail.\n if (isAuthenticated || getStoredJwt() !== null) {\n setStatus('authenticated');\n return;\n }\n\n const token = getSessionTokenFromUrl(window.location.href);\n if (!token) {\n setStatus('idle');\n return;\n }\n\n setStatus('exchanging');\n\n void (async (): Promise<void> => {\n let response: Response;\n try {\n response = await fetch(exchangePath, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token }),\n });\n } catch {\n setError('network_error');\n setStatus('error');\n return;\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = null;\n }\n\n if (!response.ok) {\n const errorCode =\n body !== null && typeof body === 'object' && 'error' in body\n ? String((body as { error: unknown }).error)\n : 'exchange_failed';\n setError(errorCode);\n setStatus('error');\n return;\n }\n\n const jwt =\n body !== null && typeof body === 'object' && 'jwt' in body\n ? (body as { jwt: unknown }).jwt\n : null;\n if (typeof jwt !== 'string' || jwt.length === 0) {\n setError('exchange_failed');\n setStatus('error');\n return;\n }\n\n setJwt(jwt);\n setStatus('authenticated');\n })();\n }, [exchangePath, isAuthenticated, setJwt]);\n\n return { status, error, theme };\n}\n","'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport {\n ANALYTICS_CAPABILITY,\n connectFanvueBridge,\n type AnalyticsProperties,\n type BridgeConnectError,\n type FanvueBridge,\n} from '../bridge/index.js';\n\nimport type { Result } from 'neverthrow';\n\n/**\n * The value returned by {@link useFanvueAnalytics}.\n *\n * @property track - Fires an event through the host page's Amplitude client.\n * Fire-and-forget: it never throws and no-ops when the app is not embedded in\n * Fanvue or was not granted analytics.\n * @property isEnabled - Whether events are actually reaching Fanvue. `false`\n * until the handshake completes, and permanently `false` outside Fanvue.\n */\nexport interface UseFanvueAnalyticsResult {\n track: (eventName: string, properties?: AnalyticsProperties) => void;\n isEnabled: boolean;\n}\n\n/**\n * The connection is shared by every caller: the handshake costs a round trip\n * and the host issues one port per frame, so a page with several components\n * tracking events must not open several bridges.\n */\nlet sharedBridge: Promise<Result<FanvueBridge, BridgeConnectError>> | null = null;\n\n/** Connects on first use, then returns the same connection to every caller. */\nfunction getSharedBridge(): Promise<Result<FanvueBridge, BridgeConnectError>> {\n sharedBridge ??= connectFanvueBridge().then((result) => {\n // A timeout is not final — the host may simply not have been ready yet —\n // so forget it and let a later track or mount handshake again. A\n // connected bridge and NOT_EMBEDDED stay cached: neither can change\n // without a reload.\n if (result.isErr() && result.error.code === 'TIMEOUT') {\n sharedBridge = null;\n }\n return result;\n });\n return sharedBridge;\n}\n\n/**\n * Fires analytics events through the Fanvue page hosting this app.\n *\n * Events land in Fanvue's Amplitude project namespaced as `embedded_app_<eventName>`,\n * attributed to the app and stitched into the viewer's session — no identity\n * data crosses the frame boundary. Outside Fanvue (local dev, previews, a\n * standalone deployment) `track` silently does nothing, so app code can call it\n * unconditionally.\n *\n * @returns A stable {@link UseFanvueAnalyticsResult.track} and whether events\n * are currently reaching Fanvue.\n *\n * @example\n * function CreateCourseButton() {\n * const { track } = useFanvueAnalytics();\n * return <button onClick={() => track('course_created', { chapters: 4 })}>Create</button>;\n * }\n */\nexport function useFanvueAnalytics(): UseFanvueAnalyticsResult {\n const [isEnabled, setIsEnabled] = useState(false);\n\n useEffect(() => {\n let active = true;\n void getSharedBridge().then((result) => {\n if (!active) return;\n setIsEnabled(result.isOk() && result.value.has(ANALYTICS_CAPABILITY));\n });\n return (): void => {\n active = false;\n };\n }, []);\n\n const track = useCallback((eventName: string, properties?: AnalyticsProperties): void => {\n void getSharedBridge().then(async (result) => {\n if (result.isErr()) return;\n const tracked = await result.value.analytics.track(eventName, properties);\n // Surface a typo'd event name or property in development rather than\n // silently swallowing the event; production stays fire-and-forget.\n if (\n tracked.isErr() &&\n tracked.error.code === 'invalid_payload' &&\n process.env['NODE_ENV'] !== 'production'\n ) {\n console.warn(`[fanvue/analytics] dropped \"${eventName}\": ${tracked.error.message}`);\n }\n });\n }, []);\n\n return { track, isEnabled };\n}\n"],"mappings":"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"}
|