@bravely-studios/account-web 0.3.4

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.
Files changed (59) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +262 -0
  3. package/dist/ActivationStateMachine.d.ts +50 -0
  4. package/dist/ActivationStateMachine.d.ts.map +1 -0
  5. package/dist/ActivationStateMachine.js +141 -0
  6. package/dist/ActivationStateMachine.js.map +1 -0
  7. package/dist/BravelyAccountManager.d.ts +156 -0
  8. package/dist/BravelyAccountManager.d.ts.map +1 -0
  9. package/dist/BravelyAccountManager.js +621 -0
  10. package/dist/BravelyAccountManager.js.map +1 -0
  11. package/dist/EntitlementCache.d.ts +50 -0
  12. package/dist/EntitlementCache.d.ts.map +1 -0
  13. package/dist/EntitlementCache.js +116 -0
  14. package/dist/EntitlementCache.js.map +1 -0
  15. package/dist/components/ActivationLadder.d.ts +78 -0
  16. package/dist/components/ActivationLadder.d.ts.map +1 -0
  17. package/dist/components/ActivationLadder.js +145 -0
  18. package/dist/components/ActivationLadder.js.map +1 -0
  19. package/dist/components/CrossAppCard.d.ts +48 -0
  20. package/dist/components/CrossAppCard.d.ts.map +1 -0
  21. package/dist/components/CrossAppCard.js +45 -0
  22. package/dist/components/CrossAppCard.js.map +1 -0
  23. package/dist/deprecation.d.ts +19 -0
  24. package/dist/deprecation.d.ts.map +1 -0
  25. package/dist/deprecation.js +83 -0
  26. package/dist/deprecation.js.map +1 -0
  27. package/dist/displayName.d.ts +15 -0
  28. package/dist/displayName.d.ts.map +1 -0
  29. package/dist/displayName.js +41 -0
  30. package/dist/displayName.js.map +1 -0
  31. package/dist/dpop.d.ts +30 -0
  32. package/dist/dpop.d.ts.map +1 -0
  33. package/dist/dpop.js +87 -0
  34. package/dist/dpop.js.map +1 -0
  35. package/dist/hooks/useActivationLaneFromUrl.d.ts +54 -0
  36. package/dist/hooks/useActivationLaneFromUrl.d.ts.map +1 -0
  37. package/dist/hooks/useActivationLaneFromUrl.js +105 -0
  38. package/dist/hooks/useActivationLaneFromUrl.js.map +1 -0
  39. package/dist/hooks/useFreshLaunchRestoration.d.ts +62 -0
  40. package/dist/hooks/useFreshLaunchRestoration.d.ts.map +1 -0
  41. package/dist/hooks/useFreshLaunchRestoration.js +135 -0
  42. package/dist/hooks/useFreshLaunchRestoration.js.map +1 -0
  43. package/dist/index.d.ts +16 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +15 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/oauth.d.ts +50 -0
  48. package/dist/oauth.d.ts.map +1 -0
  49. package/dist/oauth.js +107 -0
  50. package/dist/oauth.js.map +1 -0
  51. package/dist/storage.d.ts +48 -0
  52. package/dist/storage.d.ts.map +1 -0
  53. package/dist/storage.js +153 -0
  54. package/dist/storage.js.map +1 -0
  55. package/dist/types.d.ts +172 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +7 -0
  58. package/dist/types.js.map +1 -0
  59. package/package.json +61 -0
@@ -0,0 +1,50 @@
1
+ import type { CachedEntitlements, Entitlement } from "./types.js";
2
+ import type { Storage } from "./storage.js";
3
+ declare const TTL_SECONDS: number;
4
+ export interface EntitlementCacheConfig {
5
+ storage: Storage;
6
+ /** TTL override (seconds). Defaults to 72h. */
7
+ ttlSeconds?: number;
8
+ /** Logger override; defaults to console.info. Used for `offline_entitlement_served` marker. */
9
+ log?: (event: string, ctx: Record<string, unknown>) => void;
10
+ }
11
+ export interface CacheRead {
12
+ entitlements: Entitlement[];
13
+ cachedAt: Date;
14
+ ttlSeconds: number;
15
+ /** True when the read came from the cache (rather than a fresh fetch). */
16
+ fromCache: true;
17
+ }
18
+ export declare class EntitlementCache {
19
+ private readonly storage;
20
+ private readonly ttl;
21
+ private readonly log;
22
+ /** In-memory mirror so synchronous reads after `update` are immediate. */
23
+ private memory;
24
+ /** Track whether we've hydrated from the persistent store yet. */
25
+ private hydrated;
26
+ constructor(config: EntitlementCacheConfig);
27
+ /** Hydrate from persistent storage on first call; safe to call multiple times. */
28
+ hydrate(): Promise<void>;
29
+ /**
30
+ * Returns the cached entitlements iff fresh (within TTL). Emits the
31
+ * `offline_entitlement_served` marker. Returns null if stale or empty.
32
+ */
33
+ getCached(): CacheRead | null;
34
+ /** Update the cache from a fresh server response. */
35
+ update(args: {
36
+ entitlements: Entitlement[];
37
+ dpopJktThumbprint?: string | null;
38
+ signature?: string | null;
39
+ }): Promise<void>;
40
+ /** Drop the cache (sign-out, account switch, etc.). */
41
+ invalidate(): Promise<void>;
42
+ /** True iff the cache is older than the TTL (or empty). */
43
+ isStale(): boolean;
44
+ /** Age of the cached row in seconds. Returns Infinity if empty. */
45
+ ageSeconds(): number;
46
+ /** Raw cache row (for debugging / inspection). */
47
+ raw(): CachedEntitlements | null;
48
+ }
49
+ export { TTL_SECONDS as ENTITLEMENT_CACHE_TTL_SECONDS };
50
+ //# sourceMappingURL=EntitlementCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntitlementCache.d.ts","sourceRoot":"","sources":["../src/EntitlementCache.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,QAAA,MAAM,WAAW,QAAe,CAAC;AAEjC,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+FAA+F;IAC/F,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC7D;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,QAAQ,EAAE,IAAI,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAwD;IAC5E,0EAA0E;IAC1E,OAAO,CAAC,MAAM,CAAmC;IACjD,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,sBAAsB;IAM1C,kFAAkF;IAC5E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB9B;;;OAGG;IACH,SAAS,IAAI,SAAS,GAAG,IAAI;IAiB7B,qDAAqD;IAC/C,MAAM,CAAC,IAAI,EAAE;QACjB,YAAY,EAAE,WAAW,EAAE,CAAC;QAC5B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAClC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjB,uDAAuD;IACjD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAKjC,2DAA2D;IAC3D,OAAO,IAAI,OAAO;IAQlB,mEAAmE;IACnE,UAAU,IAAI,MAAM;IAOpB,kDAAkD;IAClD,GAAG,IAAI,kBAAkB,GAAG,IAAI;CAGjC;AAED,OAAO,EAAE,WAAW,IAAI,6BAA6B,EAAE,CAAC"}
@@ -0,0 +1,116 @@
1
+ // Track E3 — 72-hour signed entitlement cache.
2
+ //
3
+ // Why: reduces network calls and lets the app serve Pro to a returning user
4
+ // who is briefly offline (plane, transit, ISP hiccup) without forcing a
5
+ // dead-end "Couldn't reach servers" message. Matches the
6
+ // `entitlement_cached_valid` state in the canonical activation state machine.
7
+ //
8
+ // Gate 1 vs Gate 2:
9
+ // - Gate 1: cache row signature is `null`. The cache is best-effort offline
10
+ // fallback only. If a host page corrupts the localStorage row the only
11
+ // downside is "user has to re-verify online."
12
+ // - Gate 2: server returns a signed entitlement payload (HMAC over
13
+ // `entitlements + ba_id + dpopJktThumbprint`). Cache stores the signature
14
+ // and validates on read, refusing tampered rows.
15
+ //
16
+ // TTL is 72h. After that `isStale()` returns true, `getCached()` returns null,
17
+ // and the caller MUST re-fetch.
18
+ const CACHE_KEY = "entitlement_cache";
19
+ const TTL_SECONDS = 72 * 60 * 60; // 72 hours
20
+ export class EntitlementCache {
21
+ storage;
22
+ ttl;
23
+ log;
24
+ /** In-memory mirror so synchronous reads after `update` are immediate. */
25
+ memory = null;
26
+ /** Track whether we've hydrated from the persistent store yet. */
27
+ hydrated = false;
28
+ constructor(config) {
29
+ this.storage = config.storage;
30
+ this.ttl = config.ttlSeconds ?? TTL_SECONDS;
31
+ this.log = config.log ?? ((event, ctx) => console.info(`[bravely-account-web] ${event}`, ctx));
32
+ }
33
+ /** Hydrate from persistent storage on first call; safe to call multiple times. */
34
+ async hydrate() {
35
+ if (this.hydrated)
36
+ return;
37
+ const raw = await this.storage.get(CACHE_KEY);
38
+ if (raw) {
39
+ try {
40
+ const parsed = JSON.parse(raw);
41
+ // Validate shape minimally so a malformed row doesn't crash.
42
+ if (Array.isArray(parsed.entitlements) && typeof parsed.cachedAt === "string") {
43
+ this.memory = parsed;
44
+ }
45
+ }
46
+ catch {
47
+ // Corrupt row — drop it.
48
+ await this.storage.remove(CACHE_KEY);
49
+ }
50
+ }
51
+ this.hydrated = true;
52
+ }
53
+ /**
54
+ * Returns the cached entitlements iff fresh (within TTL). Emits the
55
+ * `offline_entitlement_served` marker. Returns null if stale or empty.
56
+ */
57
+ getCached() {
58
+ if (!this.memory)
59
+ return null;
60
+ if (this.isStale())
61
+ return null;
62
+ const cachedAt = new Date(this.memory.cachedAt);
63
+ this.log("offline_entitlement_served", {
64
+ cachedAt: this.memory.cachedAt,
65
+ entitlementCount: this.memory.entitlements.length,
66
+ dpopJktThumbprint: this.memory.dpopJktThumbprint,
67
+ });
68
+ return {
69
+ entitlements: this.memory.entitlements,
70
+ cachedAt,
71
+ ttlSeconds: this.ttl,
72
+ fromCache: true,
73
+ };
74
+ }
75
+ /** Update the cache from a fresh server response. */
76
+ async update(args) {
77
+ const row = {
78
+ entitlements: args.entitlements,
79
+ cachedAt: new Date().toISOString(),
80
+ dpopJktThumbprint: args.dpopJktThumbprint ?? null,
81
+ signature: args.signature ?? null,
82
+ };
83
+ this.memory = row;
84
+ await this.storage.set(CACHE_KEY, JSON.stringify(row));
85
+ }
86
+ /** Drop the cache (sign-out, account switch, etc.). */
87
+ async invalidate() {
88
+ this.memory = null;
89
+ await this.storage.remove(CACHE_KEY);
90
+ }
91
+ /** True iff the cache is older than the TTL (or empty). */
92
+ isStale() {
93
+ if (!this.memory)
94
+ return true;
95
+ const cachedAt = Date.parse(this.memory.cachedAt);
96
+ if (Number.isNaN(cachedAt))
97
+ return true;
98
+ const ageSeconds = (Date.now() - cachedAt) / 1000;
99
+ return ageSeconds > this.ttl;
100
+ }
101
+ /** Age of the cached row in seconds. Returns Infinity if empty. */
102
+ ageSeconds() {
103
+ if (!this.memory)
104
+ return Infinity;
105
+ const cachedAt = Date.parse(this.memory.cachedAt);
106
+ if (Number.isNaN(cachedAt))
107
+ return Infinity;
108
+ return (Date.now() - cachedAt) / 1000;
109
+ }
110
+ /** Raw cache row (for debugging / inspection). */
111
+ raw() {
112
+ return this.memory ? { ...this.memory } : null;
113
+ }
114
+ }
115
+ export { TTL_SECONDS as ENTITLEMENT_CACHE_TTL_SECONDS };
116
+ //# sourceMappingURL=EntitlementCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntitlementCache.js","sourceRoot":"","sources":["../src/EntitlementCache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import { type ReactElement } from "react";
2
+ import type { ActivationState } from "../types.js";
3
+ export interface ActivationLadderPhase {
4
+ /** Phase index (0..3). */
5
+ index: 0 | 1 | 2 | 3;
6
+ /** Inclusive lower bound, ms since state entered. */
7
+ fromMs: number;
8
+ /** Exclusive upper bound, ms since state entered. `null` = open-ended. */
9
+ toMs: number | null;
10
+ /** Final rendered copy (already interpolated). */
11
+ text: string;
12
+ /** True iff this is the terminal phase that surfaces Retry + Contact CTAs. */
13
+ showRecoveryActions: boolean;
14
+ }
15
+ /**
16
+ * Phase fixtures used by `<ActivationLadder>`. Templates mirror
17
+ * `activation-state-machine.json` byte-for-byte — DO NOT edit by hand
18
+ * outside a router/spec change.
19
+ */
20
+ export declare const ACTIVATION_LADDER_PHASE_TEMPLATES: readonly {
21
+ fromMs: number;
22
+ toMs: number | null;
23
+ template: string;
24
+ showRecoveryActions: boolean;
25
+ }[];
26
+ /**
27
+ * Resolve the active ladder phase for a given elapsedMs.
28
+ * Phases are 0-indexed.
29
+ */
30
+ export declare function ladderPhaseForElapsed(elapsedMs: number): 0 | 1 | 2 | 3;
31
+ /**
32
+ * Interpolate one ladder phase template with the supplied `appName` and
33
+ * `orderId`. When `orderId` is null/undefined, the order-bearing phrase
34
+ * is rewritten to drop the `(order #…)` chunk (router decision #3 plan B).
35
+ */
36
+ export declare function renderLadderCopy(template: string, appName: string, orderId: string | null | undefined): string;
37
+ /**
38
+ * Build the full set of resolved phases for a given run. Useful for
39
+ * snapshot testing and for host pages that want to render the ladder
40
+ * with all phases visible at once.
41
+ */
42
+ export declare function buildResolvedLadder(appName: string, orderId: string | null | undefined): ActivationLadderPhase[];
43
+ export interface ActivationLadderProps {
44
+ /** Current activation state (from `manager.getActivationState()`). */
45
+ state: ActivationState;
46
+ /**
47
+ * Override the elapsed ms. If omitted, the component computes elapsed
48
+ * from `state.enteredAt` and ticks every second.
49
+ */
50
+ elapsedMs?: number;
51
+ /** Paddle order id; rendered into phase 3 + 4 copy when present. */
52
+ orderId?: string | null;
53
+ /** App slug — drives the `displayNameFor()` interpolation. */
54
+ appSlug: string;
55
+ /**
56
+ * Override the display name. Defaults to `displayNameFor(appSlug)`.
57
+ * Useful for utilities with slug variants (e.g. `printscreenly-web`
58
+ * still uses appSlug `printscreenly`).
59
+ */
60
+ appName?: string;
61
+ /** Phase-4 retry callback. */
62
+ onRetry?: () => void;
63
+ /** Phase-4 contact-support callback. */
64
+ onContactSupport?: () => void;
65
+ /** Optional class name applied to the outer container. */
66
+ className?: string;
67
+ /** Localized "Retry" button label. Defaults to "Try again". */
68
+ retryLabel?: string;
69
+ /** Localized "Contact support" button label. */
70
+ contactSupportLabel?: string;
71
+ }
72
+ /**
73
+ * Render the M3 activation ladder. Renders nothing unless the manager is
74
+ * in `post_checkout_activation`. Host pages decide where to mount it
75
+ * (modal, banner, inline panel).
76
+ */
77
+ export declare function ActivationLadder(props: ActivationLadderProps): ReactElement | null;
78
+ //# sourceMappingURL=ActivationLadder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActivationLadder.d.ts","sourceRoot":"","sources":["../../src/components/ActivationLadder.tsx"],"names":[],"mappings":"AA6BA,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,MAAM,WAAW,qBAAqB;IACpC,0BAA0B;IAC1B,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACrB,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,EAAE,SAAS;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,OAAO,CAAC;CAC9B,EA0BS,CAAC;AAEX;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAKtE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACjC,MAAM,CAeR;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACjC,qBAAqB,EAAE,CAQzB;AAED,MAAM,WAAW,qBAAqB;IACpC,sEAAsE;IACtE,KAAK,EAAE,eAAe,CAAC;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,YAAY,GAAG,IAAI,CAwElF"}
@@ -0,0 +1,145 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // `<ActivationLadder>` — M3 post-checkout activation surface.
3
+ //
4
+ // Renders the 4-phase ladder defined by `post_checkout_activation` in
5
+ // `bravely-commerce-router/docs/activation-state-machine.json` (v1.0.0).
6
+ // Copy is mirrored byte-for-byte from that file. The drift-test in
7
+ // `__tests__/ActivationLadder.test.tsx` enforces the exact strings.
8
+ //
9
+ // Inputs:
10
+ // - `state`: the current `ActivationState` from the manager. The ladder
11
+ // only renders when `state.name === "post_checkout_activation"`. In any
12
+ // other state it renders nothing (the host page should hide the surface).
13
+ // - `elapsedMs`: ms since the state was entered. Host pages either pass
14
+ // a tick from a `setInterval`, or rely on `<ActivationLadder>` to
15
+ // compute it from `state.enteredAt` (the default fallback).
16
+ // - `orderId`: the Paddle order #, surfaced from `?order=<id>` on the
17
+ // post-checkout return URL (or BAS exchange). Phase 60-120s and 120s+
18
+ // interpolate it; if unset, the phrase shifts to drop the `(order #…)`.
19
+ // - `appSlug`: the current app's canonical slug. Drives the
20
+ // `displayNameFor()` lookup; copy stays product-side per CLAUDE.md.
21
+ // - `onRetry` / `onContactSupport`: invoked when the user clicks the
22
+ // phase-4 CTAs. Host pages typically reset the manager state machine
23
+ // (`force("post_checkout_activation")` for retry) or open a mailto/
24
+ // intercom thread for support.
25
+ //
26
+ // CLAUDE.md `feedback_no_etas` rule: ELAPSED time is the only number the
27
+ // surface ever shows. No predicted ETA. Anti-pattern #4 ("Taking longer
28
+ // than usual") is banned — the drift-test asserts the substring is absent
29
+ // from every phase.
30
+ import { useEffect, useState } from "react";
31
+ import { displayNameFor } from "../displayName.js";
32
+ /**
33
+ * Phase fixtures used by `<ActivationLadder>`. Templates mirror
34
+ * `activation-state-machine.json` byte-for-byte — DO NOT edit by hand
35
+ * outside a router/spec change.
36
+ */
37
+ export const ACTIVATION_LADDER_PHASE_TEMPLATES = [
38
+ {
39
+ fromMs: 0,
40
+ toMs: 15_000,
41
+ template: "Activating [AppName] Pro…",
42
+ showRecoveryActions: false,
43
+ },
44
+ {
45
+ fromMs: 15_000,
46
+ toMs: 60_000,
47
+ template: "Just a moment, finalizing your purchase.",
48
+ showRecoveryActions: false,
49
+ },
50
+ {
51
+ fromMs: 60_000,
52
+ toMs: 120_000,
53
+ template: "Your purchase is confirmed (order #[OrderId]). We're applying it now.",
54
+ showRecoveryActions: false,
55
+ },
56
+ {
57
+ fromMs: 120_000,
58
+ toMs: null,
59
+ template: "Something went sideways on our end — your purchase is safe (order #[OrderId]).",
60
+ showRecoveryActions: true,
61
+ },
62
+ ];
63
+ /**
64
+ * Resolve the active ladder phase for a given elapsedMs.
65
+ * Phases are 0-indexed.
66
+ */
67
+ export function ladderPhaseForElapsed(elapsedMs) {
68
+ if (elapsedMs < 15_000)
69
+ return 0;
70
+ if (elapsedMs < 60_000)
71
+ return 1;
72
+ if (elapsedMs < 120_000)
73
+ return 2;
74
+ return 3;
75
+ }
76
+ /**
77
+ * Interpolate one ladder phase template with the supplied `appName` and
78
+ * `orderId`. When `orderId` is null/undefined, the order-bearing phrase
79
+ * is rewritten to drop the `(order #…)` chunk (router decision #3 plan B).
80
+ */
81
+ export function renderLadderCopy(template, appName, orderId) {
82
+ let out = template.replace(/\[AppName\]/g, appName);
83
+ if (orderId && orderId.length > 0) {
84
+ out = out.replace(/\[OrderId\]/g, orderId);
85
+ }
86
+ else {
87
+ // Strip the parenthetical " (order #[OrderId])" entirely when no ID is
88
+ // known. Per plan doc decision #3, option B — render the 60-120s and
89
+ // 120s+ phases without the order-# anchor instead of waiting for the
90
+ // BAS-side router change.
91
+ out = out
92
+ .replace(/ \(order #\[OrderId\]\)\./g, ".")
93
+ .replace(/ \(order #\[OrderId\]\)/g, "")
94
+ .replace(/\[OrderId\]/g, "unknown");
95
+ }
96
+ return out;
97
+ }
98
+ /**
99
+ * Build the full set of resolved phases for a given run. Useful for
100
+ * snapshot testing and for host pages that want to render the ladder
101
+ * with all phases visible at once.
102
+ */
103
+ export function buildResolvedLadder(appName, orderId) {
104
+ return ACTIVATION_LADDER_PHASE_TEMPLATES.map((p, i) => ({
105
+ index: i,
106
+ fromMs: p.fromMs,
107
+ toMs: p.toMs,
108
+ text: renderLadderCopy(p.template, appName, orderId),
109
+ showRecoveryActions: p.showRecoveryActions,
110
+ }));
111
+ }
112
+ /**
113
+ * Render the M3 activation ladder. Renders nothing unless the manager is
114
+ * in `post_checkout_activation`. Host pages decide where to mount it
115
+ * (modal, banner, inline panel).
116
+ */
117
+ export function ActivationLadder(props) {
118
+ const { state, elapsedMs: elapsedOverride, orderId, appSlug, appName: appNameOverride, onRetry, onContactSupport, className, retryLabel = "Try again", contactSupportLabel = "Contact support", } = props;
119
+ // The component auto-ticks if no elapsedMs override is supplied. We use
120
+ // `Date.now() - state.enteredAt.getTime()` as the source of truth.
121
+ const [tickElapsed, setTickElapsed] = useState(() => elapsedOverride !== undefined
122
+ ? elapsedOverride
123
+ : Math.max(0, Date.now() - state.enteredAt.getTime()));
124
+ useEffect(() => {
125
+ if (elapsedOverride !== undefined) {
126
+ setTickElapsed(elapsedOverride);
127
+ return;
128
+ }
129
+ // Auto-tick every 1s. Phase boundaries are 15s / 60s / 120s so a 1s
130
+ // tick is comfortably finer-grained than the UI needs.
131
+ setTickElapsed(Math.max(0, Date.now() - state.enteredAt.getTime()));
132
+ const id = setInterval(() => {
133
+ setTickElapsed(Math.max(0, Date.now() - state.enteredAt.getTime()));
134
+ }, 1000);
135
+ return () => clearInterval(id);
136
+ }, [state.enteredAt, elapsedOverride]);
137
+ if (state.name !== "post_checkout_activation")
138
+ return null;
139
+ const appName = appNameOverride ?? displayNameFor(appSlug);
140
+ const phaseIndex = ladderPhaseForElapsed(tickElapsed);
141
+ const phaseFixture = ACTIVATION_LADDER_PHASE_TEMPLATES[phaseIndex];
142
+ const text = renderLadderCopy(phaseFixture.template, appName, orderId);
143
+ return (_jsxs("div", { className: className, "data-bravely-component": "activation-ladder", "data-phase": phaseIndex, role: "status", "aria-live": "polite", children: [_jsx("p", { "data-bravely-slot": "text", children: text }), phaseFixture.showRecoveryActions ? (_jsxs("div", { "data-bravely-slot": "recovery-actions", children: [onRetry ? (_jsx("button", { type: "button", onClick: onRetry, "data-bravely-action": "retry", children: retryLabel })) : null, onContactSupport ? (_jsx("button", { type: "button", onClick: onContactSupport, "data-bravely-action": "contact-support", children: contactSupportLabel })) : null] })) : null] }));
144
+ }
145
+ //# sourceMappingURL=ActivationLadder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActivationLadder.js","sourceRoot":"","sources":["../../src/components/ActivationLadder.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { ReactElement } from "react";
2
+ import type { Entitlement } from "../types.js";
3
+ /**
4
+ * Return the entitlements that count as "other Bravely Pro apps" for the
5
+ * cross-app card on `currentAppSlug`. Excludes the current app's own
6
+ * `<slug>_pro` entitlement. Includes `bravely_premium` only if the user
7
+ * does NOT have `<currentApp>_pro` (otherwise the bundle double-counts the
8
+ * current app).
9
+ */
10
+ export declare function filterCrossAppEntitlements(entitlements: Entitlement[], currentAppSlug: string): Entitlement[];
11
+ /**
12
+ * Count how many distinct apps the user owns across the account, excluding
13
+ * the current app. `bravely_premium` (the bundle entitlement) counts as
14
+ * one cross-app token — the journey doc treats it as a single "unifying
15
+ * signal" rather than enumerating every bundled app.
16
+ */
17
+ export declare function countCrossAppEntitlements(entitlements: Entitlement[], currentAppSlug: string): number;
18
+ /**
19
+ * Render the locked cross-app copy: "You own [N] Bravely Pro app[s] on
20
+ * this account." with grammatically correct pluralization.
21
+ */
22
+ export declare function renderCrossAppCopy(count: number): string;
23
+ export interface CrossAppCardProps {
24
+ /** Full set of active entitlements from `manager.getEntitlements()`. */
25
+ entitlements: Entitlement[];
26
+ /** This utility's slug; the card excludes its own entitlement from the count. */
27
+ currentAppSlug: string;
28
+ /**
29
+ * When true, render a Dismiss button that calls `onDismiss`. Per the
30
+ * 5-decisions list in `cux-m2-m4-rollout-plan.md` the journey doc default
31
+ * is persistent (non-dismissible). Wave B-D can override per-variant.
32
+ */
33
+ dismissible?: boolean;
34
+ /** Invoked when the dismiss button is clicked (only when `dismissible`). */
35
+ onDismiss?: () => void;
36
+ /** Render variant: full card or compact paywall footer chip. */
37
+ variant?: "card" | "footer-chip";
38
+ /** Outer class name. */
39
+ className?: string;
40
+ /** Localized dismiss label. */
41
+ dismissLabel?: string;
42
+ }
43
+ /**
44
+ * Render the M4 cross-app card. Returns null when the user has zero
45
+ * cross-app entitlements (the card has nothing to say).
46
+ */
47
+ export declare function CrossAppCard(props: CrossAppCardProps): ReactElement | null;
48
+ //# sourceMappingURL=CrossAppCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CrossAppCard.d.ts","sourceRoot":"","sources":["../../src/components/CrossAppCard.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,WAAW,EAAE,EAC3B,cAAc,EAAE,MAAM,GACrB,WAAW,EAAE,CAGf;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,WAAW,EAAE,EAC3B,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC;IACjC,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,GAAG,IAAI,CAoC1E"}
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { displayNameFor } from "../displayName.js";
3
+ /**
4
+ * Return the entitlements that count as "other Bravely Pro apps" for the
5
+ * cross-app card on `currentAppSlug`. Excludes the current app's own
6
+ * `<slug>_pro` entitlement. Includes `bravely_premium` only if the user
7
+ * does NOT have `<currentApp>_pro` (otherwise the bundle double-counts the
8
+ * current app).
9
+ */
10
+ export function filterCrossAppEntitlements(entitlements, currentAppSlug) {
11
+ const own = `${currentAppSlug}_pro`;
12
+ return entitlements.filter((e) => e.lookup_key !== own);
13
+ }
14
+ /**
15
+ * Count how many distinct apps the user owns across the account, excluding
16
+ * the current app. `bravely_premium` (the bundle entitlement) counts as
17
+ * one cross-app token — the journey doc treats it as a single "unifying
18
+ * signal" rather than enumerating every bundled app.
19
+ */
20
+ export function countCrossAppEntitlements(entitlements, currentAppSlug) {
21
+ return filterCrossAppEntitlements(entitlements, currentAppSlug).length;
22
+ }
23
+ /**
24
+ * Render the locked cross-app copy: "You own [N] Bravely Pro app[s] on
25
+ * this account." with grammatically correct pluralization.
26
+ */
27
+ export function renderCrossAppCopy(count) {
28
+ if (count <= 0)
29
+ return "";
30
+ const noun = count === 1 ? "app" : "apps";
31
+ return `You own ${count} Bravely Pro ${noun} on this account.`;
32
+ }
33
+ /**
34
+ * Render the M4 cross-app card. Returns null when the user has zero
35
+ * cross-app entitlements (the card has nothing to say).
36
+ */
37
+ export function CrossAppCard(props) {
38
+ const { entitlements, currentAppSlug, dismissible = false, onDismiss, variant = "card", className, dismissLabel = "Dismiss", } = props;
39
+ const count = countCrossAppEntitlements(entitlements, currentAppSlug);
40
+ if (count <= 0)
41
+ return null;
42
+ const text = renderCrossAppCopy(count);
43
+ return (_jsxs("aside", { className: className, "data-bravely-component": "cross-app-card", "data-variant": variant, "data-count": count, "data-current-app": currentAppSlug, "data-current-app-name": displayNameFor(currentAppSlug), children: [_jsx("p", { "data-bravely-slot": "text", children: text }), dismissible && onDismiss ? (_jsx("button", { type: "button", onClick: onDismiss, "data-bravely-action": "dismiss", children: dismissLabel })) : null] }));
44
+ }
45
+ //# sourceMappingURL=CrossAppCard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CrossAppCard.js","sourceRoot":"","sources":["../../src/components/CrossAppCard.tsx"],"names":[],"mappings":";AAkBA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,YAA2B,EAC3B,cAAsB;IAEtB,MAAM,GAAG,GAAG,GAAG,cAAc,MAAM,CAAC;IACpC,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CACvC,YAA2B,EAC3B,cAAsB;IAEtB,OAAO,0BAA0B,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC,MAAM,CAAC;AACzE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC1B,MAAM,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;IAC1C,OAAO,WAAW,KAAK,gBAAgB,IAAI,mBAAmB,CAAC;AACjE,CAAC;AAuBD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,EACJ,YAAY,EACZ,cAAc,EACd,WAAW,GAAG,KAAK,EACnB,SAAS,EACT,OAAO,GAAG,MAAM,EAChB,SAAS,EACT,YAAY,GAAG,SAAS,GACzB,GAAG,KAAK,CAAC;IAEV,MAAM,KAAK,GAAG,yBAAyB,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IACtE,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAE5B,MAAM,IAAI,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACvC,OAAO,CACL,iBACE,SAAS,EAAE,SAAS,4BACG,gBAAgB,kBACzB,OAAO,gBACT,KAAK,sBACC,cAAc,2BACT,cAAc,CAAC,cAAc,CAAC,aAErD,iCAAqB,MAAM,YAAE,IAAI,GAAK,EACrC,WAAW,IAAI,SAAS,CAAC,CAAC,CAAC,CAC1B,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,SAAS,yBACE,SAAS,YAE5B,YAAY,GACN,CACV,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAC;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { DeprecationDirective } from "./types.js";
2
+ /** Parse one `Bravely-Deprecation` header value into directives. */
3
+ export declare function parseDeprecation(header: string | null): DeprecationDirective[];
4
+ /** Thrown when the server returns HTTP 426 (kill-switch). */
5
+ export declare class BravelyClientKilledError extends Error {
6
+ readonly replacement?: string;
7
+ readonly reason?: string;
8
+ constructor(message: string, opts?: {
9
+ replacement?: string;
10
+ reason?: string;
11
+ });
12
+ }
13
+ /** Thrown when the OAuth token endpoint returns an RFC 6749 error. */
14
+ export declare class OAuthError extends Error {
15
+ readonly error: string;
16
+ readonly errorDescription?: string;
17
+ constructor(error: string, description?: string);
18
+ }
19
+ //# sourceMappingURL=deprecation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deprecation.d.ts","sourceRoot":"","sources":["../src/deprecation.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,oBAAoB,EAAE,CAW9E;AAmCD,6DAA6D;AAC7D,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBACb,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO;CAMlF;AAED,sEAAsE;AACtE,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;gBACvB,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;CAMhD"}
@@ -0,0 +1,83 @@
1
+ // `Bravely-Deprecation` response-header parser. Server publishes this on any
2
+ // response to flag the endpoint or the calling client as deprecated. Two
3
+ // shapes (Track E9):
4
+ //
5
+ // Bravely-Deprecation: endpoint; expires=2026-08-11; replacement=/api/v2/...; reason=demolition
6
+ // Bravely-Deprecation: client; min-version=0.3.0; current=0.2.22; reason=force-upgrade
7
+ //
8
+ // Multiple values can be comma-separated. Browsers can read it via
9
+ // `Access-Control-Expose-Headers`.
10
+ /** Parse one `Bravely-Deprecation` header value into directives. */
11
+ export function parseDeprecation(header) {
12
+ if (!header)
13
+ return [];
14
+ // Multiple directives are comma-separated at the top level. We tolerate
15
+ // missing whitespace and stray semicolons inside values (none should
16
+ // contain commas).
17
+ const out = [];
18
+ for (const raw of header.split(",")) {
19
+ const directive = parseOne(raw.trim());
20
+ if (directive)
21
+ out.push(directive);
22
+ }
23
+ return out;
24
+ }
25
+ function parseOne(raw) {
26
+ if (!raw)
27
+ return null;
28
+ const parts = raw.split(";").map((p) => p.trim()).filter(Boolean);
29
+ if (parts.length === 0)
30
+ return null;
31
+ const kindToken = parts.shift();
32
+ const params = {};
33
+ for (const p of parts) {
34
+ const idx = p.indexOf("=");
35
+ if (idx === -1)
36
+ continue;
37
+ const k = p.slice(0, idx).trim();
38
+ const v = p.slice(idx + 1).trim();
39
+ if (k && v)
40
+ params[k] = v;
41
+ }
42
+ if (kindToken === "endpoint") {
43
+ return {
44
+ kind: "endpoint",
45
+ endpoint: params.endpoint,
46
+ expires: params.expires,
47
+ replacement: params.replacement,
48
+ reason: params.reason,
49
+ };
50
+ }
51
+ if (kindToken === "client") {
52
+ return {
53
+ kind: "client",
54
+ minVersion: params["min-version"] ?? params.min_version,
55
+ current: params.current,
56
+ reason: params.reason,
57
+ };
58
+ }
59
+ return null;
60
+ }
61
+ /** Thrown when the server returns HTTP 426 (kill-switch). */
62
+ export class BravelyClientKilledError extends Error {
63
+ replacement;
64
+ reason;
65
+ constructor(message, opts = {}) {
66
+ super(message);
67
+ this.name = "BravelyClientKilledError";
68
+ this.replacement = opts.replacement;
69
+ this.reason = opts.reason;
70
+ }
71
+ }
72
+ /** Thrown when the OAuth token endpoint returns an RFC 6749 error. */
73
+ export class OAuthError extends Error {
74
+ error;
75
+ errorDescription;
76
+ constructor(error, description) {
77
+ super(description ?? error);
78
+ this.name = "OAuthError";
79
+ this.error = error;
80
+ this.errorDescription = description;
81
+ }
82
+ }
83
+ //# sourceMappingURL=deprecation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deprecation.js","sourceRoot":"","sources":["../src/deprecation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { AppSlug } from "./types.js";
2
+ /**
3
+ * Canonical slug → display name map. Sourced from the H1 of each
4
+ * `<app>-free-vs-pro.md`. Branding owners: keep this in lockstep with the
5
+ * marketing pages on `bravely.dev/<slug>` and the Settings strings inside
6
+ * each utility.
7
+ */
8
+ export declare const DISPLAY_NAMES: Record<AppSlug, string>;
9
+ /**
10
+ * Returns the display name for a slug. Unknown slugs fall back to a
11
+ * naive Title-case rendering so host pages never crash on an unrecognized
12
+ * slug (e.g. a new utility added before this lib version ships).
13
+ */
14
+ export declare function displayNameFor(slug: string): string;
15
+ //# sourceMappingURL=displayName.d.ts.map