@buoy-gg/shared-ui 7.0.22 → 7.0.24

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 (154) hide show
  1. package/lib/commonjs/JsModal.js +39 -16
  2. package/lib/commonjs/dataViewer/JsonStringNotice.js +59 -0
  3. package/lib/commonjs/dataViewer/ThemedSplitView.js +20 -1
  4. package/lib/commonjs/dataViewer/TimestampDeltaBar.js +96 -0
  5. package/lib/commonjs/dataViewer/index.js +52 -0
  6. package/lib/commonjs/dataViewer/jsonStringValue.js +94 -0
  7. package/lib/commonjs/dataViewer/timestampDiff.js +146 -0
  8. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  9. package/lib/commonjs/index.js +50 -1
  10. package/lib/commonjs/license/FeatureGate.js +43 -10
  11. package/lib/commonjs/license/FreeAccountPrompt.js +181 -0
  12. package/lib/commonjs/license/index.js +57 -42
  13. package/lib/commonjs/license/licenseModule.js +99 -0
  14. package/lib/commonjs/license/tierLimits.js +165 -0
  15. package/lib/commonjs/ui/backgrounds/BackgroundSwitcher.js +180 -0
  16. package/lib/commonjs/ui/backgrounds/README.md +115 -0
  17. package/lib/commonjs/ui/backgrounds/ToolBackground.js +74 -0
  18. package/lib/commonjs/ui/backgrounds/backgroundStore.js +116 -0
  19. package/lib/commonjs/ui/backgrounds/index.js +70 -0
  20. package/lib/commonjs/ui/backgrounds/primitives.js +333 -0
  21. package/lib/commonjs/ui/backgrounds/registry.js +112 -0
  22. package/lib/commonjs/ui/backgrounds/rng.js +72 -0
  23. package/lib/commonjs/ui/backgrounds/types.js +1 -0
  24. package/lib/commonjs/ui/backgrounds/variants/deepField.js +351 -0
  25. package/lib/commonjs/ui/backgrounds/variants/lattice.js +579 -0
  26. package/lib/commonjs/ui/backgrounds/variants/starfield.js +210 -0
  27. package/lib/commonjs/ui/index.js +15 -3
  28. package/lib/module/JsModal.js +39 -16
  29. package/lib/module/dataViewer/JsonStringNotice.js +54 -0
  30. package/lib/module/dataViewer/ThemedSplitView.js +20 -1
  31. package/lib/module/dataViewer/TimestampDeltaBar.js +91 -0
  32. package/lib/module/dataViewer/index.js +4 -0
  33. package/lib/module/dataViewer/jsonStringValue.js +89 -0
  34. package/lib/module/dataViewer/timestampDiff.js +139 -0
  35. package/lib/module/hooks/safe-area-impl.js +1 -1
  36. package/lib/module/index.js +1 -1
  37. package/lib/module/license/FeatureGate.js +43 -10
  38. package/lib/module/license/FreeAccountPrompt.js +176 -0
  39. package/lib/module/license/index.js +5 -40
  40. package/lib/module/license/licenseModule.js +92 -0
  41. package/lib/module/license/tierLimits.js +158 -0
  42. package/lib/module/ui/backgrounds/BackgroundSwitcher.js +176 -0
  43. package/lib/module/ui/backgrounds/README.md +115 -0
  44. package/lib/module/ui/backgrounds/ToolBackground.js +70 -0
  45. package/lib/module/ui/backgrounds/backgroundStore.js +111 -0
  46. package/lib/module/ui/backgrounds/index.js +9 -0
  47. package/lib/module/ui/backgrounds/primitives.js +328 -0
  48. package/lib/module/ui/backgrounds/registry.js +105 -0
  49. package/lib/module/ui/backgrounds/rng.js +65 -0
  50. package/lib/module/ui/backgrounds/types.js +1 -0
  51. package/lib/module/ui/backgrounds/variants/deepField.js +347 -0
  52. package/lib/module/ui/backgrounds/variants/lattice.js +575 -0
  53. package/lib/module/ui/backgrounds/variants/starfield.js +206 -0
  54. package/lib/module/ui/index.js +3 -0
  55. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/dataViewer/JsonStringNotice.d.ts +6 -0
  57. package/lib/typescript/commonjs/dataViewer/JsonStringNotice.d.ts.map +1 -0
  58. package/lib/typescript/commonjs/dataViewer/ThemedSplitView.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/dataViewer/TimestampDeltaBar.d.ts +7 -0
  60. package/lib/typescript/commonjs/dataViewer/TimestampDeltaBar.d.ts.map +1 -0
  61. package/lib/typescript/commonjs/dataViewer/index.d.ts +6 -0
  62. package/lib/typescript/commonjs/dataViewer/index.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/dataViewer/jsonStringValue.d.ts +25 -0
  64. package/lib/typescript/commonjs/dataViewer/jsonStringValue.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/dataViewer/timestampDiff.d.ts +55 -0
  66. package/lib/typescript/commonjs/dataViewer/timestampDiff.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  68. package/lib/typescript/commonjs/index.d.ts +2 -2
  69. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/license/FeatureGate.d.ts +19 -3
  71. package/lib/typescript/commonjs/license/FeatureGate.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts +23 -0
  73. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/license/index.d.ts +6 -15
  75. package/lib/typescript/commonjs/license/index.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/license/licenseModule.d.ts +55 -0
  77. package/lib/typescript/commonjs/license/licenseModule.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/license/tierLimits.d.ts +143 -0
  79. package/lib/typescript/commonjs/license/tierLimits.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts +13 -0
  81. package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/ui/backgrounds/ToolBackground.d.ts +30 -0
  83. package/lib/typescript/commonjs/ui/backgrounds/ToolBackground.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts +43 -0
  85. package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts +7 -0
  87. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts +106 -0
  89. package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts +28 -0
  91. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts.map +1 -0
  92. package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts +27 -0
  93. package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts.map +1 -0
  94. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts +14 -0
  95. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts.map +1 -0
  96. package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts +65 -0
  97. package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts +33 -0
  99. package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts +12 -0
  101. package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  103. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  104. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  105. package/lib/typescript/module/dataViewer/JsonStringNotice.d.ts +6 -0
  106. package/lib/typescript/module/dataViewer/JsonStringNotice.d.ts.map +1 -0
  107. package/lib/typescript/module/dataViewer/ThemedSplitView.d.ts.map +1 -1
  108. package/lib/typescript/module/dataViewer/TimestampDeltaBar.d.ts +7 -0
  109. package/lib/typescript/module/dataViewer/TimestampDeltaBar.d.ts.map +1 -0
  110. package/lib/typescript/module/dataViewer/index.d.ts +6 -0
  111. package/lib/typescript/module/dataViewer/index.d.ts.map +1 -1
  112. package/lib/typescript/module/dataViewer/jsonStringValue.d.ts +25 -0
  113. package/lib/typescript/module/dataViewer/jsonStringValue.d.ts.map +1 -0
  114. package/lib/typescript/module/dataViewer/timestampDiff.d.ts +55 -0
  115. package/lib/typescript/module/dataViewer/timestampDiff.d.ts.map +1 -0
  116. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  117. package/lib/typescript/module/index.d.ts +2 -2
  118. package/lib/typescript/module/index.d.ts.map +1 -1
  119. package/lib/typescript/module/license/FeatureGate.d.ts +19 -3
  120. package/lib/typescript/module/license/FeatureGate.d.ts.map +1 -1
  121. package/lib/typescript/module/license/FreeAccountPrompt.d.ts +23 -0
  122. package/lib/typescript/module/license/FreeAccountPrompt.d.ts.map +1 -0
  123. package/lib/typescript/module/license/index.d.ts +6 -15
  124. package/lib/typescript/module/license/index.d.ts.map +1 -1
  125. package/lib/typescript/module/license/licenseModule.d.ts +55 -0
  126. package/lib/typescript/module/license/licenseModule.d.ts.map +1 -0
  127. package/lib/typescript/module/license/tierLimits.d.ts +143 -0
  128. package/lib/typescript/module/license/tierLimits.d.ts.map +1 -0
  129. package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts +13 -0
  130. package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -0
  131. package/lib/typescript/module/ui/backgrounds/ToolBackground.d.ts +30 -0
  132. package/lib/typescript/module/ui/backgrounds/ToolBackground.d.ts.map +1 -0
  133. package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts +43 -0
  134. package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts.map +1 -0
  135. package/lib/typescript/module/ui/backgrounds/index.d.ts +7 -0
  136. package/lib/typescript/module/ui/backgrounds/index.d.ts.map +1 -0
  137. package/lib/typescript/module/ui/backgrounds/primitives.d.ts +106 -0
  138. package/lib/typescript/module/ui/backgrounds/primitives.d.ts.map +1 -0
  139. package/lib/typescript/module/ui/backgrounds/registry.d.ts +28 -0
  140. package/lib/typescript/module/ui/backgrounds/registry.d.ts.map +1 -0
  141. package/lib/typescript/module/ui/backgrounds/rng.d.ts +27 -0
  142. package/lib/typescript/module/ui/backgrounds/rng.d.ts.map +1 -0
  143. package/lib/typescript/module/ui/backgrounds/types.d.ts +14 -0
  144. package/lib/typescript/module/ui/backgrounds/types.d.ts.map +1 -0
  145. package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts +65 -0
  146. package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts.map +1 -0
  147. package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts +33 -0
  148. package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts.map +1 -0
  149. package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts +12 -0
  150. package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts.map +1 -0
  151. package/lib/typescript/module/ui/index.d.ts +1 -0
  152. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  153. package/package.json +4 -4
  154. package/scripts/detect-safe-area.js +0 -20
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Timestamp detection for diff views.
5
+ *
6
+ * `1787085897165 → 1787012175875` is unreadable. It is also the single most
7
+ * common diff in a devtool: `lastSyncTimestamp`, `expiresAt`, `updatedAt`,
8
+ * every cache stamp. This turns that pair into "20h 29m earlier" plus both
9
+ * instants spelled out, WITHOUT hiding the raw values — a devtool that
10
+ * silently reinterprets what is stored is worse than one that says nothing.
11
+ *
12
+ * DETECTION IS DELIBERATELY NARROW, because the failure mode is confidently
13
+ * mislabelling an id or a counter as a date:
14
+ * - Epoch milliseconds only in [2001-09-09, 2100), i.e. 13 digits.
15
+ * - Epoch seconds only in the same window, i.e. 10 digits.
16
+ * The two ranges cannot overlap, so a value is never ambiguous.
17
+ * - ISO 8601 strings that both match the shape AND parse.
18
+ * - BOTH sides must be the same kind. An ISO string against an epoch number
19
+ * is a representation change, not a time delta, and reporting a gap there
20
+ * would bury the more interesting fact.
21
+ */
22
+
23
+ /** Units we recognize. A pair must agree on one of them. */
24
+
25
+ /** 2001-09-09 — below this, a 13-digit-ish number is not a plausible stamp. */
26
+ const MS_MIN = 1_000_000_000_000;
27
+ /** 2100-01-01. */
28
+ const MS_MAX = 4_102_444_800_000;
29
+ const S_MIN = 1_000_000_000;
30
+ const S_MAX = 4_102_444_800;
31
+
32
+ /** Date, or date-time with optional seconds/fraction/zone. */
33
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
34
+ function fromNumber(n) {
35
+ if (!Number.isInteger(n)) return null;
36
+ if (n >= MS_MIN && n < MS_MAX) return {
37
+ kind: "epoch-ms",
38
+ epochMs: n
39
+ };
40
+ if (n >= S_MIN && n < S_MAX) return {
41
+ kind: "epoch-s",
42
+ epochMs: n * 1000
43
+ };
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Read one value as an instant, or null when it isn't confidently a time.
49
+ * Numeric strings count: storage backends hand back everything as strings.
50
+ */
51
+ export function detectTimestamp(value) {
52
+ if (value instanceof Date) {
53
+ const ms = value.getTime();
54
+ return Number.isFinite(ms) ? {
55
+ kind: "iso",
56
+ epochMs: ms
57
+ } : null;
58
+ }
59
+ if (typeof value === "number") return fromNumber(value);
60
+ if (typeof value !== "string") return null;
61
+ const trimmed = value.trim();
62
+ if (trimmed === "") return null;
63
+ if (/^\d+$/.test(trimmed)) return fromNumber(Number(trimmed));
64
+ if (!ISO_RE.test(trimmed)) return null;
65
+ const parsed = Date.parse(trimmed);
66
+ return Number.isFinite(parsed) ? {
67
+ kind: "iso",
68
+ epochMs: parsed
69
+ } : null;
70
+ }
71
+ function two(n) {
72
+ return n < 10 ? `0${n}` : String(n);
73
+ }
74
+
75
+ /** Local wall-clock, fixed format — a devtool wants it sortable, not chatty. */
76
+ export function formatInstant(epochMs) {
77
+ const d = new Date(epochMs);
78
+ return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())} ` + `${two(d.getHours())}:${two(d.getMinutes())}:${two(d.getSeconds())}`;
79
+ }
80
+
81
+ /** Coarsest two units that still carry the magnitude: "3d 4h", "5m 30s". */
82
+ export function formatDuration(ms) {
83
+ const abs = Math.abs(ms);
84
+ if (abs < 1000) return `${abs}ms`;
85
+ const seconds = Math.floor(abs / 1000);
86
+ if (seconds < 60) return `${seconds}s`;
87
+ const minutes = Math.floor(seconds / 60);
88
+ const restSeconds = seconds % 60;
89
+ if (minutes < 60) return restSeconds ? `${minutes}m ${restSeconds}s` : `${minutes}m`;
90
+ const hours = Math.floor(minutes / 60);
91
+ const restMinutes = minutes % 60;
92
+ if (hours < 24) return restMinutes ? `${hours}h ${restMinutes}m` : `${hours}h`;
93
+ const days = Math.floor(hours / 24);
94
+ const restHours = hours % 24;
95
+ return restHours ? `${days}d ${restHours}h` : `${days}d`;
96
+ }
97
+ function pair(oldValue, newValue, path) {
98
+ const before = detectTimestamp(oldValue);
99
+ const after = detectTimestamp(newValue);
100
+ if (!before || !after || before.kind !== after.kind) return null;
101
+ const deltaMs = after.epochMs - before.epochMs;
102
+ return {
103
+ path,
104
+ kind: before.kind,
105
+ deltaMs,
106
+ deltaLabel: deltaMs === 0 ? "same instant" : `${formatDuration(deltaMs)} ${deltaMs < 0 ? "earlier" : "later"}`,
107
+ fromLabel: formatInstant(before.epochMs),
108
+ toLabel: formatInstant(after.epochMs)
109
+ };
110
+ }
111
+ function isPlainObject(value) {
112
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
113
+ }
114
+
115
+ /** Beyond a handful the bar stops being a summary and becomes the diff. */
116
+ const MAX_DELTAS = 6;
117
+
118
+ /**
119
+ * Every timestamp pair worth explaining in a diff of these two values.
120
+ *
121
+ * Scalars give at most one entry. Plain objects are inspected ONE level deep
122
+ * — enough for the `{ updatedAt, expiresAt }` shape that dominates, without
123
+ * walking a whole document to hunt for dates.
124
+ */
125
+ export function describeTimestampDeltas(oldValue, newValue) {
126
+ const scalar = pair(oldValue, newValue);
127
+ if (scalar) return [scalar];
128
+ if (!isPlainObject(oldValue) || !isPlainObject(newValue)) return [];
129
+ const out = [];
130
+ for (const key of Object.keys(oldValue)) {
131
+ if (out.length >= MAX_DELTAS) break;
132
+ if (!(key in newValue)) continue;
133
+ // Unchanged stamps are noise: the point is explaining what MOVED.
134
+ if (Object.is(oldValue[key], newValue[key])) continue;
135
+ const entry = pair(oldValue[key], newValue[key], key);
136
+ if (entry) out.push(entry);
137
+ }
138
+ return out;
139
+ }
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Auto-generated safe area implementation
5
5
  * Detected: none
6
- * Generated at: 2026-08-18T00:32:52.026Z
6
+ * Generated at: 2026-08-19T20:50:51.001Z
7
7
  *
8
8
  * DO NOT EDIT - This file is generated by scripts/detect-safe-area.js
9
9
  *
@@ -82,4 +82,4 @@ export { HintsProvider, useHintsDisabled, useHintsContext } from "./context/inde
82
82
  export * from "./history/index.js";
83
83
 
84
84
  // License/Feature gating exports
85
- export { FeatureGate, UpgradePrompt, useFeatureGate, ProFeatureBanner, UpgradeModal, LicenseEntryModal, ProBadge, ProUpgradeModal, getUseIsPro, getUseLicense } from "./license/index.js";
85
+ export { FeatureGate, UpgradePrompt, useFeatureGate, ProFeatureBanner, UpgradeModal, LicenseEntryModal, ProBadge, ProUpgradeModal, FreeAccountPrompt, LOGIN_COMMAND, getUseIsPro, getUseLicense, getUseTier, TIER_LIMITS, useLimit, resolveEffectiveTier, freeKeyRaises } from "./license/index.js";
@@ -12,6 +12,9 @@ import { gameUIColors, buoyColors } from "../ui/gameUI/constants/gameUIColors.js
12
12
  import { LockIcon, X, Zap, Shield, Clock, Check } from "../icons/lucide-icons.js";
13
13
  import { LicenseEntryModal } from "./LicenseEntryModal.js";
14
14
  import { openPricing, isWeb } from "./openPricing.js";
15
+ import { FreeAccountPrompt } from "./FreeAccountPrompt.js";
16
+ import { getUseTier } from "./licenseModule.js";
17
+ import { freeKeyRaises } from "./tierLimits.js";
15
18
  import { useIsPro, useLicense, useProAccess } from "@buoy-gg/license";
16
19
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
17
20
  // Weekend Pass branding — a distinct violet so it never reads as the gold PRO.
@@ -49,14 +52,19 @@ export const ProBadge = ({
49
52
  export const ProUpgradeModal = ({
50
53
  visible,
51
54
  onClose,
52
- featureName = "This feature"
55
+ featureName = "This feature",
56
+ limitKey
53
57
  }) => {
54
58
  // Simple wrapper around UpgradeModal with minimal description and no benefits
55
59
  return /*#__PURE__*/_jsx(UpgradeModal, {
56
60
  visible: visible,
57
61
  onClose: onClose,
58
- featureName: featureName,
59
- description: `${featureName} requires a Pro subscription.`
62
+ featureName: featureName
63
+ // Only meaningful when this upsell is about a LIMIT; without it the
64
+ // description below (which promises Pro) is the accurate message.
65
+ ,
66
+ limitKey: limitKey,
67
+ description: limitKey ? undefined : `${featureName} requires a Pro subscription.`
60
68
  });
61
69
  };
62
70
  /**
@@ -66,9 +74,13 @@ export const ProUpgradeModal = ({
66
74
  export const UpgradePrompt = ({
67
75
  featureName,
68
76
  onUpgradePress,
69
- compact = false
77
+ compact = false,
78
+ limitKey
70
79
  }) => {
71
80
  const [showLicenseModal, setShowLicenseModal] = useState(false);
81
+ // Only offer the free key when it would actually raise this limit — see
82
+ // UpgradeModalProps.limitKey.
83
+ const isAnonymous = getUseTier()() === "anonymous" && limitKey != null && freeKeyRaises(limitKey);
72
84
  const handlePress = useCallback(() => {
73
85
  if (onUpgradePress) {
74
86
  onUpgradePress();
@@ -96,7 +108,11 @@ export const UpgradePrompt = ({
96
108
  style: styles.compactText,
97
109
  children: "PRO"
98
110
  })]
99
- }), /*#__PURE__*/_jsx(LicenseEntryModal, {
111
+ }), isAnonymous ? /*#__PURE__*/_jsx(FreeAccountPrompt, {
112
+ visible: showLicenseModal,
113
+ onClose: handleCloseModal,
114
+ featureName: featureName
115
+ }) : /*#__PURE__*/_jsx(LicenseEntryModal, {
100
116
  visible: showLicenseModal,
101
117
  onClose: handleCloseModal
102
118
  })]
@@ -113,23 +129,27 @@ export const UpgradePrompt = ({
113
129
  })
114
130
  }), /*#__PURE__*/_jsx(Text, {
115
131
  style: styles.title,
116
- children: "Pro Feature"
132
+ children: isAnonymous ? "Get a free key" : "Pro Feature"
117
133
  }), featureName && /*#__PURE__*/_jsx(Text, {
118
134
  style: styles.featureName,
119
135
  children: featureName
120
136
  }), /*#__PURE__*/_jsx(Text, {
121
137
  style: styles.description,
122
- children: "Upgrade to React Buoy Pro to unlock this feature and more."
138
+ children: isAnonymous ? "Buoy works without an account, with a small slice of each tool. A free key opens it up." : "Upgrade to React Buoy Pro to unlock this feature and more."
123
139
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
124
140
  style: styles.upgradeButton,
125
141
  onPress: handlePress,
126
142
  activeOpacity: 0.8,
127
143
  children: /*#__PURE__*/_jsx(Text, {
128
144
  style: styles.upgradeButtonText,
129
- children: "Upgrade to Pro"
145
+ children: isAnonymous ? "Get a free key" : "Upgrade to Pro"
130
146
  })
131
147
  })]
132
- }), /*#__PURE__*/_jsx(LicenseEntryModal, {
148
+ }), isAnonymous ? /*#__PURE__*/_jsx(FreeAccountPrompt, {
149
+ visible: showLicenseModal,
150
+ onClose: handleCloseModal,
151
+ featureName: featureName
152
+ }) : /*#__PURE__*/_jsx(LicenseEntryModal, {
133
153
  visible: showLicenseModal,
134
154
  onClose: handleCloseModal
135
155
  })]
@@ -308,8 +328,14 @@ export const UpgradeModal = ({
308
328
  onClose,
309
329
  featureName = "This Feature",
310
330
  description,
311
- benefits
331
+ benefits,
332
+ limitKey
312
333
  }) => {
334
+ // Anonymous users get a different ask — but ONLY when a free key would
335
+ // actually fix what they just hit. Everything else here is Pro-exclusive,
336
+ // where a free key changes nothing and offering one would be dishonest.
337
+ const tier = getUseTier()();
338
+ const offerFreeKey = tier === "anonymous" && limitKey != null && freeKeyRaises(limitKey);
313
339
  const hasBenefits = benefits && benefits.length > 0;
314
340
  const [showLicenseModal, setShowLicenseModal] = useState(false);
315
341
  const license = useLicense();
@@ -332,6 +358,13 @@ export const UpgradeModal = ({
332
358
  onClose();
333
359
  }, [onClose]);
334
360
  if (!visible) return null;
361
+ if (offerFreeKey) {
362
+ return /*#__PURE__*/_jsx(FreeAccountPrompt, {
363
+ visible: visible,
364
+ onClose: onClose,
365
+ featureName: featureName === "This Feature" ? undefined : featureName
366
+ });
367
+ }
335
368
  return /*#__PURE__*/_jsxs(_Fragment, {
336
369
  children: [/*#__PURE__*/_jsx(Modal, {
337
370
  visible: visible && !showLicenseModal,
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The upsell an ANONYMOUS user sees — someone with no key at all.
5
+ *
6
+ * Deliberately not the Pro modal. Their next step is free and takes about
7
+ * thirty seconds, so asking them for money would be asking the wrong question
8
+ * at the wrong moment, and would suppress exactly the signup this exists to
9
+ * produce. Pro is still offered, as a secondary action.
10
+ *
11
+ * The command is shown inline and copyable rather than linked, because there is
12
+ * no grace period: someone who just watched their limits shrink needs the fix
13
+ * in front of them, not a link to a page that has a link to a command.
14
+ */
15
+
16
+ import React, { useCallback, useEffect, useRef, useState } from "react";
17
+ import { Modal, StyleSheet, Text, TouchableOpacity } from "react-native";
18
+ import { buoyColors } from "../ui/gameUI/constants/gameUIColors.js";
19
+ import { Check, Copy, X } from "../icons/lucide-icons.js";
20
+ import { copyToClipboard } from "../clipboard/index.js";
21
+ import { openPricing } from "./openPricing.js";
22
+
23
+ /** The one thing a reader has to be able to act on. */
24
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
+ export const LOGIN_COMMAND = "npx buoy login";
26
+ export const FreeAccountPrompt = ({
27
+ visible,
28
+ onClose,
29
+ featureName
30
+ }) => {
31
+ const [copied, setCopied] = useState(false);
32
+ const resetTimer = useRef(null);
33
+ useEffect(() => () => {
34
+ if (resetTimer.current) clearTimeout(resetTimer.current);
35
+ }, []);
36
+ const handleCopy = useCallback(() => {
37
+ void copyToClipboard(LOGIN_COMMAND).then(ok => {
38
+ if (!ok) return;
39
+ setCopied(true);
40
+ if (resetTimer.current) clearTimeout(resetTimer.current);
41
+ resetTimer.current = setTimeout(() => setCopied(false), 1500);
42
+ });
43
+ }, []);
44
+ if (!visible) return null;
45
+ return /*#__PURE__*/_jsx(Modal, {
46
+ visible: visible,
47
+ transparent: true,
48
+ animationType: "fade",
49
+ onRequestClose: onClose,
50
+ children: /*#__PURE__*/_jsx(TouchableOpacity, {
51
+ style: styles.overlay,
52
+ onPress: onClose,
53
+ activeOpacity: 1,
54
+ children: /*#__PURE__*/_jsxs(TouchableOpacity, {
55
+ style: styles.card,
56
+ activeOpacity: 1,
57
+ onPress: () => {},
58
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
59
+ style: styles.close,
60
+ onPress: onClose,
61
+ activeOpacity: 0.7,
62
+ children: /*#__PURE__*/_jsx(X, {
63
+ size: 18,
64
+ color: buoyColors.textMuted
65
+ })
66
+ }), /*#__PURE__*/_jsx(Text, {
67
+ style: styles.title,
68
+ children: featureName ? `More ${featureName}` : "See more"
69
+ }), /*#__PURE__*/_jsx(Text, {
70
+ style: styles.subtitle,
71
+ children: "Buoy runs without an account \u2014 you just get a small slice of each tool. A free key opens it up."
72
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
73
+ style: styles.command,
74
+ onPress: handleCopy,
75
+ activeOpacity: 0.7,
76
+ accessibilityLabel: `Copy ${LOGIN_COMMAND}`,
77
+ children: [/*#__PURE__*/_jsx(Text, {
78
+ style: styles.commandText,
79
+ children: LOGIN_COMMAND
80
+ }), copied ? /*#__PURE__*/_jsx(Check, {
81
+ size: 14,
82
+ color: buoyColors.success
83
+ }) : /*#__PURE__*/_jsx(Copy, {
84
+ size: 14,
85
+ color: buoyColors.textMuted
86
+ })]
87
+ }), /*#__PURE__*/_jsx(Text, {
88
+ style: styles.reassure,
89
+ children: "Free forever. No card. About thirty seconds."
90
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
91
+ style: styles.secondary,
92
+ onPress: () => openPricing(),
93
+ activeOpacity: 0.8,
94
+ children: /*#__PURE__*/_jsx(Text, {
95
+ style: styles.secondaryText,
96
+ children: "Or see Pro"
97
+ })
98
+ })]
99
+ })
100
+ })
101
+ });
102
+ };
103
+ const styles = StyleSheet.create({
104
+ overlay: {
105
+ flex: 1,
106
+ backgroundColor: "rgba(0, 0, 0, 0.75)",
107
+ alignItems: "center",
108
+ justifyContent: "center",
109
+ padding: 20
110
+ },
111
+ card: {
112
+ backgroundColor: buoyColors.card,
113
+ borderRadius: 16,
114
+ borderWidth: 1,
115
+ borderColor: buoyColors.primary + "60",
116
+ padding: 24,
117
+ maxWidth: 340,
118
+ width: "100%",
119
+ position: "relative"
120
+ },
121
+ close: {
122
+ position: "absolute",
123
+ top: 12,
124
+ right: 12,
125
+ padding: 6,
126
+ zIndex: 1
127
+ },
128
+ title: {
129
+ fontSize: 20,
130
+ fontWeight: "700",
131
+ color: buoyColors.text,
132
+ textAlign: "center",
133
+ marginBottom: 8
134
+ },
135
+ subtitle: {
136
+ fontSize: 13,
137
+ lineHeight: 19,
138
+ color: buoyColors.textMuted,
139
+ textAlign: "center",
140
+ marginBottom: 20
141
+ },
142
+ command: {
143
+ flexDirection: "row",
144
+ alignItems: "center",
145
+ justifyContent: "space-between",
146
+ backgroundColor: buoyColors.base,
147
+ borderRadius: 8,
148
+ borderWidth: 1,
149
+ borderColor: buoyColors.border,
150
+ paddingHorizontal: 14,
151
+ paddingVertical: 12,
152
+ gap: 10
153
+ },
154
+ commandText: {
155
+ fontSize: 14,
156
+ fontWeight: "600",
157
+ color: buoyColors.text,
158
+ fontFamily: "monospace"
159
+ },
160
+ reassure: {
161
+ fontSize: 12,
162
+ color: buoyColors.textMuted,
163
+ textAlign: "center",
164
+ marginTop: 10
165
+ },
166
+ secondary: {
167
+ marginTop: 18,
168
+ paddingVertical: 10,
169
+ alignItems: "center"
170
+ },
171
+ secondaryText: {
172
+ fontSize: 13,
173
+ fontWeight: "600",
174
+ color: buoyColors.textMuted
175
+ }
176
+ });
@@ -2,43 +2,8 @@
2
2
 
3
3
  export { FeatureGate, UpgradePrompt, useFeatureGate, ProFeatureBanner, UpgradeModal, ProBadge, ProUpgradeModal } from "./FeatureGate.js";
4
4
  export { LicenseEntryModal } from "./LicenseEntryModal.js";
5
- // Centralized lazy loading for license hooks to avoid duplication across packages
6
- // This is the SINGLE source of truth - all packages should import from here
7
- let _useIsPro = null;
8
- let _useLicense = null;
9
- let _licenseLoadAttempted = false;
10
- function loadLicenseModuleCentral() {
11
- if (_licenseLoadAttempted) return;
12
- _licenseLoadAttempted = true;
13
- try {
14
- const mod = require("@buoy-gg/license");
15
- if (mod) {
16
- _useIsPro = mod.useIsPro ?? null;
17
- _useLicense = mod.useLicense ?? null;
18
- }
19
- } catch {
20
- // License package not available - will use fallback
21
- }
22
- }
23
-
24
- /**
25
- * Get the useIsPro hook from @buoy-gg/license
26
- * Falls back to () => false if license package is not available
27
- *
28
- * IMPORTANT: Use this instead of doing your own lazy loading in other packages
29
- */
30
- export function getUseIsPro() {
31
- loadLicenseModuleCentral();
32
- return _useIsPro ?? (() => false);
33
- }
34
-
35
- /**
36
- * Get the useLicense hook from @buoy-gg/license
37
- * Returns null if license package is not available
38
- *
39
- * IMPORTANT: Use this instead of doing your own lazy loading in other packages
40
- */
41
- export function getUseLicense() {
42
- loadLicenseModuleCentral();
43
- return _useLicense;
44
- }
5
+ export { FreeAccountPrompt, LOGIN_COMMAND } from "./FreeAccountPrompt.js";
6
+ // Lazy license-module access lives in ./licenseModule so tierLimits can use it
7
+ // without an import cycle. Re-exported here to keep the existing surface.
8
+ export { getUseIsPro, getUseLicense, getUseTier } from "./licenseModule.js";
9
+ export { TIER_LIMITS, useLimit, resolveEffectiveTier, freeKeyRaises } from "./tierLimits.js";
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Centralized lazy loading of `@buoy-gg/license`.
5
+ *
6
+ * This is the SINGLE place any package resolves license hooks — tools must not
7
+ * `require("@buoy-gg/license")` themselves. Lives in its own module (rather
8
+ * than `./index`) so `tierLimits` can consume it without an import cycle.
9
+ */
10
+
11
+ /** Access tier. Mirrors BuoyTier in @buoy-gg/license. */
12
+
13
+ let _useIsPro = null;
14
+ let _useLicense = null;
15
+ let _useTier = null;
16
+ let _anonymousTierEnabled = null;
17
+ let _licenseLoadAttempted = false;
18
+ function loadLicenseModuleCentral() {
19
+ if (_licenseLoadAttempted) return;
20
+ _licenseLoadAttempted = true;
21
+ try {
22
+ const mod = require("@buoy-gg/license");
23
+ if (mod) {
24
+ _useIsPro = mod.useIsPro ?? null;
25
+ _useLicense = mod.useLicense ?? null;
26
+ _useTier = mod.useTier ?? null;
27
+ _anonymousTierEnabled = typeof mod.ANONYMOUS_TIER_ENABLED === "boolean" ? mod.ANONYMOUS_TIER_ENABLED : null;
28
+ }
29
+ } catch {
30
+ // License package not available - will use fallback
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get the useIsPro hook from @buoy-gg/license.
36
+ * Falls back to () => false if the license package is not available.
37
+ *
38
+ * IMPORTANT: Use this instead of doing your own lazy loading in other packages
39
+ */
40
+ export function getUseIsPro() {
41
+ loadLicenseModuleCentral();
42
+ return _useIsPro ?? (() => false);
43
+ }
44
+
45
+ /**
46
+ * Get the useLicense hook from @buoy-gg/license.
47
+ * Returns null if the license package is not available.
48
+ *
49
+ * IMPORTANT: Use this instead of doing your own lazy loading in other packages
50
+ */
51
+ export function getUseLicense() {
52
+ loadLicenseModuleCentral();
53
+ return _useLicense;
54
+ }
55
+
56
+ /**
57
+ * Get the useTier hook from @buoy-gg/license.
58
+ *
59
+ * Falls back to `"free"` — NOT `"anonymous"` — when the license package can't
60
+ * be resolved. That deliberately matches the existing `getUseIsPro()` fallback,
61
+ * which yields free-tier behavior today, and it picks the safer failure:
62
+ *
63
+ * - falling back to "anonymous" would let one bundler quirk silently cut every
64
+ * legitimate user's limits by ~80%;
65
+ * - falling back to "free" costs no revenue, because Pro still requires a key
66
+ * that a live license server validated — this path can never grant Pro.
67
+ *
68
+ * Reaching it at all requires `@buoy-gg/license` to be missing, and it is a
69
+ * hard dependency of `@buoy-gg/core`.
70
+ */
71
+ export function getUseTier() {
72
+ loadLicenseModuleCentral();
73
+ return _useTier ?? (() => "free");
74
+ }
75
+
76
+ /**
77
+ * Whether the anonymous tier is switched on.
78
+ *
79
+ * The switch itself lives in @buoy-gg/license (see its config.ts) because the
80
+ * Weekend Pass change has to flip at the same moment as the reduced limits, and
81
+ * those live in different packages. Read through the same lazy loader as the
82
+ * hooks so a missing license package degrades gracefully rather than throwing
83
+ * at module load.
84
+ *
85
+ * Falls back to `false` — the anonymous tier off, everyone on free limits —
86
+ * which matches the `getUseTier()` fallback of "free". A build that cannot
87
+ * resolve the license package must never take limits AWAY from someone.
88
+ */
89
+ export function isAnonymousTierEnabled() {
90
+ loadLicenseModuleCentral();
91
+ return _anonymousTierEnabled ?? false;
92
+ }