@buoy-gg/shared-ui 3.0.2 → 4.0.1
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/lib/commonjs/hooks/safe-area-impl.js +1 -1
- package/lib/commonjs/icons/lucide-icons.js +28 -2
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/license/FeatureGate.js +60 -11
- package/lib/commonjs/license/LicenseEntryModal.js +12 -2
- package/lib/commonjs/license/openPricing.js +36 -0
- package/lib/commonjs/storage/devToolsStorageKeys.js +1 -0
- package/lib/commonjs/stores/ignoredPatternsStore.js +229 -0
- package/lib/commonjs/stores/index.js +26 -1
- package/lib/commonjs/ui/components/CompactRow.js +14 -4
- package/lib/commonjs/ui/components/ExpandedInfoRow.js +13 -3
- package/lib/commonjs/utils/index.js +6 -0
- package/lib/commonjs/utils/safeExpoRouter.js +59 -4
- package/lib/module/hooks/safe-area-impl.js +1 -1
- package/lib/module/icons/lucide-icons.js +25 -0
- package/lib/module/index.js +1 -1
- package/lib/module/license/FeatureGate.js +61 -12
- package/lib/module/license/LicenseEntryModal.js +13 -3
- package/lib/module/license/openPricing.js +31 -0
- package/lib/module/storage/devToolsStorageKeys.js +1 -0
- package/lib/module/stores/ignoredPatternsStore.js +223 -0
- package/lib/module/stores/index.js +2 -1
- package/lib/module/ui/components/CompactRow.js +14 -4
- package/lib/module/ui/components/ExpandedInfoRow.js +13 -3
- package/lib/module/utils/index.js +1 -1
- package/lib/module/utils/safeExpoRouter.js +58 -4
- package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/commonjs/icons/lucide-icons.d.ts +1 -0
- package/lib/typescript/commonjs/icons/lucide-icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/license/FeatureGate.d.ts +14 -1
- package/lib/typescript/commonjs/license/FeatureGate.d.ts.map +1 -1
- package/lib/typescript/commonjs/license/LicenseEntryModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/license/openPricing.d.ts +14 -0
- package/lib/typescript/commonjs/license/openPricing.d.ts.map +1 -0
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +1 -0
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/stores/ignoredPatternsStore.d.ts +84 -0
- package/lib/typescript/commonjs/stores/ignoredPatternsStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/stores/index.d.ts +1 -0
- package/lib/typescript/commonjs/stores/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +3 -1
- package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts +3 -1
- package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/index.d.ts +1 -1
- package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts +9 -0
- package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts.map +1 -1
- package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/module/icons/lucide-icons.d.ts +1 -0
- package/lib/typescript/module/icons/lucide-icons.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/license/FeatureGate.d.ts +14 -1
- package/lib/typescript/module/license/FeatureGate.d.ts.map +1 -1
- package/lib/typescript/module/license/LicenseEntryModal.d.ts.map +1 -1
- package/lib/typescript/module/license/openPricing.d.ts +14 -0
- package/lib/typescript/module/license/openPricing.d.ts.map +1 -0
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +1 -0
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/module/stores/ignoredPatternsStore.d.ts +84 -0
- package/lib/typescript/module/stores/ignoredPatternsStore.d.ts.map +1 -0
- package/lib/typescript/module/stores/index.d.ts +1 -0
- package/lib/typescript/module/stores/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/CompactRow.d.ts +3 -1
- package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts +3 -1
- package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts.map +1 -1
- package/lib/typescript/module/utils/index.d.ts +1 -1
- package/lib/typescript/module/utils/index.d.ts.map +1 -1
- package/lib/typescript/module/utils/safeExpoRouter.d.ts +9 -0
- package/lib/typescript/module/utils/safeExpoRouter.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.getSafeCurrentPathname = getSafeCurrentPathname;
|
|
6
7
|
exports.getSafeRouter = getSafeRouter;
|
|
7
8
|
exports.isExpoRouterAvailable = isExpoRouterAvailable;
|
|
8
9
|
exports.useSafeGlobalSearchParams = useSafeGlobalSearchParams;
|
|
@@ -31,14 +32,27 @@ let checkedAvailability = false;
|
|
|
31
32
|
* where native modules moved to the Expo Modules API.
|
|
32
33
|
*/
|
|
33
34
|
function hasExpoNativeRuntime() {
|
|
35
|
+
// Modern Expo (SDK 50+, both Old and New Architecture): expo-modules-core
|
|
36
|
+
// installs a global `expo` object — the Expo Modules API runtime — via JSI
|
|
37
|
+
// when the native runtime is actually present. This is the most reliable
|
|
38
|
+
// signal and, crucially, the ONLY one of these checks that fires on SDK 54+ /
|
|
39
|
+
// New Architecture dev builds, where the legacy NativeModules entries below
|
|
40
|
+
// are no longer registered (ExpoLinking is gone, NativeUnimoduleProxy isn't
|
|
41
|
+
// on the bridge). It stays absent on bare RN CLI, so there's no false
|
|
42
|
+
// positive there. Without this, navigation-dependent tools (route sitemap,
|
|
43
|
+
// remote navigate, perf-monitor automation) silently no-op on modern builds.
|
|
44
|
+
const expoGlobal = globalThis.expo;
|
|
45
|
+
if (expoGlobal != null && (expoGlobal.modules != null || expoGlobal.EventEmitter != null || expoGlobal.NativeModule != null)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
// Check legacy NativeModules bridge (Expo SDK < 54)
|
|
35
50
|
if (_reactNative.NativeModules.ExpoLinking) {
|
|
36
51
|
return true;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
// Check for Expo Modules Core native bridge (
|
|
40
|
-
|
|
41
|
-
if (_reactNative.NativeModules.NativeUnimoduleProxy) {
|
|
54
|
+
// Check for Expo Modules Core native bridge (older SDKs / Old Architecture)
|
|
55
|
+
if (_reactNative.NativeModules.NativeUnimoduleProxy || _reactNative.NativeModules.ExpoModulesCore) {
|
|
42
56
|
return true;
|
|
43
57
|
}
|
|
44
58
|
|
|
@@ -47,7 +61,7 @@ function hasExpoNativeRuntime() {
|
|
|
47
61
|
return true;
|
|
48
62
|
}
|
|
49
63
|
|
|
50
|
-
// No Expo native runtime detected.
|
|
64
|
+
// No Expo native runtime detected (e.g. bare RN CLI).
|
|
51
65
|
// NOTE: We intentionally do NOT try require("expo-modules-core") here because
|
|
52
66
|
// on RN CLI it may be bundled as a transitive dep but the native modules
|
|
53
67
|
// (requireOptionalNativeModule etc.) are undefined, causing crashes.
|
|
@@ -161,6 +175,47 @@ function getSafeRouter() {
|
|
|
161
175
|
}
|
|
162
176
|
}
|
|
163
177
|
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Imperative current-route getter
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
// expo-router doesn't publicly re-export its imperative store, so we reach it
|
|
183
|
+
// via its build path. Cached + best-effort: if a future expo-router moves this
|
|
184
|
+
// path the require throws and the imperative fallback simply disables (callers
|
|
185
|
+
// degrade to route-events history). expo-router's store exposes getRouteInfo(),
|
|
186
|
+
// which returns the live pathname even before any navigation is recorded.
|
|
187
|
+
let routerStore;
|
|
188
|
+
function getRouterStore() {
|
|
189
|
+
if (routerStore !== undefined) return routerStore;
|
|
190
|
+
try {
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
192
|
+
routerStore = require("expo-router/build/global-state/router-store")?.store ?? null;
|
|
193
|
+
} catch {
|
|
194
|
+
routerStore = null;
|
|
195
|
+
}
|
|
196
|
+
return routerStore;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The current route pathname, read imperatively from expo-router's store.
|
|
201
|
+
*
|
|
202
|
+
* Unlike a route-events history lookup, this works even before any navigation
|
|
203
|
+
* has been recorded — e.g. right after a cold start while the app sits on its
|
|
204
|
+
* initial route. Returns null when expo-router isn't available or the store
|
|
205
|
+
* can't be reached.
|
|
206
|
+
*/
|
|
207
|
+
function getSafeCurrentPathname() {
|
|
208
|
+
if (!checkExpoRouterAvailability()) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const pathname = getRouterStore()?.getRouteInfo?.()?.pathname;
|
|
213
|
+
return typeof pathname === "string" && pathname.length > 0 ? pathname : null;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
164
219
|
// ============================================================================
|
|
165
220
|
// Availability check
|
|
166
221
|
// ============================================================================
|
|
@@ -505,6 +505,31 @@ export const Eye = ({
|
|
|
505
505
|
strokeWidth: strokeWidth
|
|
506
506
|
})]
|
|
507
507
|
});
|
|
508
|
+
export const Pin = ({
|
|
509
|
+
size = 24,
|
|
510
|
+
color = "currentColor",
|
|
511
|
+
strokeWidth = 2,
|
|
512
|
+
...props
|
|
513
|
+
}) => /*#__PURE__*/_jsxs(Svg, {
|
|
514
|
+
width: size,
|
|
515
|
+
height: size,
|
|
516
|
+
viewBox: "0 0 24 24",
|
|
517
|
+
...props,
|
|
518
|
+
children: [/*#__PURE__*/_jsx(Circle, {
|
|
519
|
+
cx: 12,
|
|
520
|
+
cy: 9,
|
|
521
|
+
r: 5,
|
|
522
|
+
stroke: color,
|
|
523
|
+
strokeWidth: strokeWidth
|
|
524
|
+
}), /*#__PURE__*/_jsx(Line, {
|
|
525
|
+
x1: 12,
|
|
526
|
+
y1: 14,
|
|
527
|
+
x2: 12,
|
|
528
|
+
y2: 21,
|
|
529
|
+
stroke: color,
|
|
530
|
+
strokeWidth: strokeWidth
|
|
531
|
+
})]
|
|
532
|
+
});
|
|
508
533
|
export const EyeOff = ({
|
|
509
534
|
size = 24,
|
|
510
535
|
color = "currentColor",
|
package/lib/module/index.js
CHANGED
|
@@ -29,7 +29,7 @@ Subscribable,
|
|
|
29
29
|
// Subscriber count notifier for cross-package notifications
|
|
30
30
|
subscriberCountNotifier, subscribeToSubscriberCountChanges, notifySubscriberCountChange,
|
|
31
31
|
// Safe expo-router wrappers (falls back to no-ops on RN CLI)
|
|
32
|
-
useSafeRouter, useSafePathname, useSafeSegments, useSafeGlobalSearchParams, getSafeRouter, isExpoRouterAvailable } from "./utils/index.js";
|
|
32
|
+
useSafeRouter, useSafePathname, useSafeSegments, useSafeGlobalSearchParams, getSafeRouter, getSafeCurrentPathname, isExpoRouterAvailable } from "./utils/index.js";
|
|
33
33
|
|
|
34
34
|
// Also export formatting utils
|
|
35
35
|
export * from "./utils/formatting/index.js";
|
|
@@ -7,24 +7,40 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React, { useState, useCallback } from "react";
|
|
10
|
-
import { View, Text, StyleSheet, TouchableOpacity,
|
|
10
|
+
import { View, Text, StyleSheet, TouchableOpacity, Modal } from "react-native";
|
|
11
11
|
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
|
-
import {
|
|
14
|
+
import { openPricing, isWeb } from "./openPricing.js";
|
|
15
|
+
import { useIsPro, useLicense, useProAccess } from "@buoy-gg/license";
|
|
15
16
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
17
|
+
// Weekend Pass branding — a distinct violet so it never reads as the gold PRO.
|
|
18
|
+
export const WEEKEND_PASS_LABEL = "WEEKEND PASS";
|
|
19
|
+
const WEEKEND_VIOLET = "#BF5AF2";
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
|
-
* Simple Pro badge for marking premium features
|
|
22
|
+
* Simple Pro badge for marking premium features.
|
|
23
|
+
*
|
|
24
|
+
* Reason-aware: when the free Weekend Pass is the active unlock (a free user on
|
|
25
|
+
* a weekend), it shows a violet "WEEKEND PASS" badge instead of the gold "PRO"
|
|
26
|
+
* so people know Pro is free this weekend. A real license — or a free user on a
|
|
27
|
+
* weekday seeing the badge as a Pro-feature marker — shows "PRO".
|
|
18
28
|
*/
|
|
19
29
|
export const ProBadge = ({
|
|
20
30
|
style
|
|
21
|
-
}) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
}) => {
|
|
32
|
+
const {
|
|
33
|
+
reason
|
|
34
|
+
} = useProAccess();
|
|
35
|
+
const weekend = reason === "weekend";
|
|
36
|
+
return /*#__PURE__*/_jsx(View, {
|
|
37
|
+
style: [styles.proBadge, weekend && styles.weekendBadge, style],
|
|
38
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
39
|
+
style: [styles.proBadgeText, weekend && styles.weekendBadgeText],
|
|
40
|
+
children: weekend ? WEEKEND_PASS_LABEL : "PRO"
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
};
|
|
28
44
|
|
|
29
45
|
/**
|
|
30
46
|
* Simple Pro upgrade modal - minimal modal with just upgrade button
|
|
@@ -56,6 +72,10 @@ export const UpgradePrompt = ({
|
|
|
56
72
|
const handlePress = useCallback(() => {
|
|
57
73
|
if (onUpgradePress) {
|
|
58
74
|
onUpgradePress();
|
|
75
|
+
} else if (isWeb) {
|
|
76
|
+
// Desktop dashboard: buy on the website, not via the mobile Buoy.init()
|
|
77
|
+
// code path. (Already have a key? Enter it from the toolbar Upgrade button.)
|
|
78
|
+
openPricing();
|
|
59
79
|
} else {
|
|
60
80
|
setShowLicenseModal(true);
|
|
61
81
|
}
|
|
@@ -181,13 +201,21 @@ export const FeatureGate = ({
|
|
|
181
201
|
* Returns { hasAccess, isPro, showUpgrade }
|
|
182
202
|
*/
|
|
183
203
|
export function useFeatureGate() {
|
|
184
|
-
const
|
|
204
|
+
const {
|
|
205
|
+
isPro,
|
|
206
|
+
isLicensed,
|
|
207
|
+
isWeekendFree,
|
|
208
|
+
reason
|
|
209
|
+
} = useProAccess();
|
|
185
210
|
const showUpgrade = () => {
|
|
186
|
-
|
|
211
|
+
openPricing("https://buoy.gg/pro");
|
|
187
212
|
};
|
|
188
213
|
return {
|
|
189
214
|
hasAccess: isPro,
|
|
190
215
|
isPro,
|
|
216
|
+
isLicensed,
|
|
217
|
+
isWeekendFree,
|
|
218
|
+
reason,
|
|
191
219
|
showUpgrade
|
|
192
220
|
};
|
|
193
221
|
}
|
|
@@ -205,6 +233,12 @@ export const ProFeatureBanner = ({
|
|
|
205
233
|
const [showLicenseModal, setShowLicenseModal] = useState(false);
|
|
206
234
|
const license = useLicense();
|
|
207
235
|
const handleUpgrade = useCallback(() => {
|
|
236
|
+
// Desktop dashboard: open pricing in the browser instead of the mobile
|
|
237
|
+
// license-entry modal.
|
|
238
|
+
if (isWeb) {
|
|
239
|
+
openPricing();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
208
242
|
setShowLicenseModal(true);
|
|
209
243
|
}, []);
|
|
210
244
|
const handleCloseModal = useCallback(() => {
|
|
@@ -280,6 +314,12 @@ export const UpgradeModal = ({
|
|
|
280
314
|
const [showLicenseModal, setShowLicenseModal] = useState(false);
|
|
281
315
|
const license = useLicense();
|
|
282
316
|
const handleUpgrade = useCallback(() => {
|
|
317
|
+
// Desktop dashboard: send users straight to the pricing page in their
|
|
318
|
+
// browser instead of the mobile Buoy.init() license-entry modal.
|
|
319
|
+
if (isWeb) {
|
|
320
|
+
openPricing();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
283
323
|
setShowLicenseModal(true);
|
|
284
324
|
}, []);
|
|
285
325
|
const handleCloseLicenseModal = useCallback(() => {
|
|
@@ -414,6 +454,15 @@ const styles = StyleSheet.create({
|
|
|
414
454
|
color: buoyColors.primary,
|
|
415
455
|
letterSpacing: 0.5
|
|
416
456
|
},
|
|
457
|
+
// Weekend Pass variant — violet instead of the gold/primary PRO.
|
|
458
|
+
weekendBadge: {
|
|
459
|
+
backgroundColor: WEEKEND_VIOLET + "1A",
|
|
460
|
+
borderColor: WEEKEND_VIOLET + "55"
|
|
461
|
+
},
|
|
462
|
+
weekendBadgeText: {
|
|
463
|
+
color: WEEKEND_VIOLET,
|
|
464
|
+
letterSpacing: 0.4
|
|
465
|
+
},
|
|
417
466
|
container: {
|
|
418
467
|
padding: 20,
|
|
419
468
|
alignItems: "center",
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import React, { useCallback } from "react";
|
|
12
|
-
import { View, Text, TouchableOpacity, StyleSheet,
|
|
12
|
+
import { View, Text, TouchableOpacity, StyleSheet, Modal, Platform } from "react-native";
|
|
13
13
|
import { absoluteFill } from "../utils/absoluteFill.js";
|
|
14
14
|
import { macOSColors } from "../ui/gameUI/constants/macOSDesignSystemColors.js";
|
|
15
|
+
import { openPricing, isWeb } from "./openPricing.js";
|
|
15
16
|
import { X, Key, Link, FileCode, Copy } from "../icons/lucide-icons.js";
|
|
16
17
|
|
|
17
18
|
// For clipboard functionality
|
|
@@ -37,7 +38,7 @@ export const LicenseEntryModal = ({
|
|
|
37
38
|
}) => {
|
|
38
39
|
const [copied, setCopied] = React.useState(false);
|
|
39
40
|
const handleGetLicense = useCallback(() => {
|
|
40
|
-
|
|
41
|
+
openPricing(purchaseUrl);
|
|
41
42
|
}, [purchaseUrl]);
|
|
42
43
|
const handleCopyCode = useCallback(() => {
|
|
43
44
|
if (Clipboard?.setString) {
|
|
@@ -103,7 +104,16 @@ export const LicenseEntryModal = ({
|
|
|
103
104
|
}), /*#__PURE__*/_jsx(View, {
|
|
104
105
|
style: styles.dividerLine
|
|
105
106
|
})]
|
|
106
|
-
}), /*#__PURE__*/
|
|
107
|
+
}), isWeb ? /*#__PURE__*/_jsx(View, {
|
|
108
|
+
style: styles.instructionsContainer,
|
|
109
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
110
|
+
style: styles.instructionsNote,
|
|
111
|
+
children: "Click the Upgrade button in the top toolbar and paste your license key. It also unlocks automatically when a connected device is running Buoy Pro."
|
|
112
|
+
})
|
|
113
|
+
}) :
|
|
114
|
+
/*#__PURE__*/
|
|
115
|
+
/* Option 2: Instructions */
|
|
116
|
+
_jsxs(View, {
|
|
107
117
|
style: styles.instructionsContainer,
|
|
108
118
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
109
119
|
style: styles.instructionsHeader,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Linking, Platform } from "react-native";
|
|
4
|
+
export const PRICING_URL = "https://buoy.gg/pricing";
|
|
5
|
+
|
|
6
|
+
/** True when running on the desktop dashboard / web (react-native-web). */
|
|
7
|
+
export const isWeb = Platform.OS === "web";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Open an upgrade/pricing URL.
|
|
11
|
+
*
|
|
12
|
+
* On the desktop dashboard (web) the upgrade flow is NOT the mobile
|
|
13
|
+
* `Buoy.init()` code path — the user buys on the website (or enters a key in
|
|
14
|
+
* the toolbar). Route the link through the Electron shell bridge so it lands in
|
|
15
|
+
* the user's DEFAULT browser instead of a bare in-app window; fall back to
|
|
16
|
+
* window.open for a plain browser tab. On native, use React Native Linking.
|
|
17
|
+
*/
|
|
18
|
+
export function openPricing(url = PRICING_URL) {
|
|
19
|
+
// Access the browser globals via globalThis so this file doesn't depend on
|
|
20
|
+
// the DOM lib (the shared package targets React Native).
|
|
21
|
+
const win = globalThis.window;
|
|
22
|
+
if (isWeb && win) {
|
|
23
|
+
if (win.buoyShell?.openExternal) {
|
|
24
|
+
win.buoyShell.openExternal(url);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
win.open?.(url, "_blank", "noopener");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
Linking.openURL(url);
|
|
31
|
+
}
|
|
@@ -91,6 +91,7 @@ export const devToolsStorageKeys = {
|
|
|
91
91
|
filters: () => `${devToolsStorageKeys.storage.root()}_filters`,
|
|
92
92
|
eventFilters: () => `${devToolsStorageKeys.storage.root()}_event_filters`,
|
|
93
93
|
keyFilters: () => `${devToolsStorageKeys.storage.root()}_key_filters`,
|
|
94
|
+
pinnedKeys: () => `${devToolsStorageKeys.storage.root()}_pinned_keys`,
|
|
94
95
|
preferences: () => `${devToolsStorageKeys.storage.root()}_preferences`,
|
|
95
96
|
activeTab: () => `${devToolsStorageKeys.storage.root()}_active_tab`,
|
|
96
97
|
isMonitoring: () => `${devToolsStorageKeys.storage.root()}_is_monitoring`,
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared "ignored patterns" store for network-style URL filtering.
|
|
5
|
+
*
|
|
6
|
+
* This is the SINGLE source of truth for the domains/URL patterns that the
|
|
7
|
+
* Network tool (and now the Events tool) hide from their lists. Both tools read
|
|
8
|
+
* and mutate this one singleton, so an ignore toggled in either place is shared
|
|
9
|
+
* everywhere and persisted to the same storage key.
|
|
10
|
+
*
|
|
11
|
+
* Lives in @buoy-gg/shared-ui (a hard dependency of both @buoy-gg/network and
|
|
12
|
+
* @buoy-gg/events) so the filter logic can be shared without @buoy-gg/events
|
|
13
|
+
* having to take a hard dependency on @buoy-gg/network (which is optional).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useMemo, useState } from "react";
|
|
17
|
+
import { persistentStorage } from "../utils/persistentStorage.js";
|
|
18
|
+
import { devToolsStorageKeys } from "../storage/devToolsStorageKeys.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* How an ignored-pattern entry should be compared against captured URLs.
|
|
22
|
+
*
|
|
23
|
+
* - `contains`: case-insensitive substring match on the full URL (legacy behavior).
|
|
24
|
+
* - `exact`: smart equality — if pattern starts with `/`, matches URL.pathname;
|
|
25
|
+
* if it starts with `http://`/`https://`, matches origin+pathname; otherwise
|
|
26
|
+
* matches URL.host.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** A single exclude pattern with its match mode. */
|
|
30
|
+
|
|
31
|
+
/** Patterns hidden by default — Buoy's own license API traffic. */
|
|
32
|
+
const DEFAULT_PATTERNS = [{
|
|
33
|
+
value: "api.keygen.sh",
|
|
34
|
+
mode: "contains"
|
|
35
|
+
}];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns true when `url` matches the ignored `pattern` according to its mode.
|
|
39
|
+
*
|
|
40
|
+
* `contains` → case-insensitive substring on the full URL (legacy behavior).
|
|
41
|
+
* `exact` → smart equality: pattern starting with `/` compares URL.pathname,
|
|
42
|
+
* full URLs compare origin+pathname (ignoring query/hash), bare
|
|
43
|
+
* values compare URL.host. Falls back to literal equality if URL
|
|
44
|
+
* parsing fails (e.g. relative URLs).
|
|
45
|
+
*/
|
|
46
|
+
export function urlMatchesIgnoredPattern(url, pattern) {
|
|
47
|
+
const lowerUrl = url.toLowerCase();
|
|
48
|
+
const lowerValue = pattern.value.toLowerCase();
|
|
49
|
+
if (pattern.mode === "contains") {
|
|
50
|
+
return lowerUrl.includes(lowerValue);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const parsed = new URL(url);
|
|
54
|
+
if (lowerValue.startsWith("/")) {
|
|
55
|
+
return parsed.pathname.toLowerCase() === lowerValue;
|
|
56
|
+
}
|
|
57
|
+
if (lowerValue.startsWith("http://") || lowerValue.startsWith("https://")) {
|
|
58
|
+
return `${parsed.origin}${parsed.pathname}`.toLowerCase() === lowerValue;
|
|
59
|
+
}
|
|
60
|
+
return parsed.host.toLowerCase() === lowerValue;
|
|
61
|
+
} catch {
|
|
62
|
+
return lowerUrl === lowerValue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Convenience: does `url` match ANY of the ignored `patterns`? */
|
|
67
|
+
export function isUrlIgnored(url, patterns) {
|
|
68
|
+
if (!url || patterns.length === 0) return false;
|
|
69
|
+
return patterns.some(pattern => urlMatchesIgnoredPattern(url, pattern));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Tolerate the legacy `string[]` persisted format and stray junk. */
|
|
73
|
+
function migratePatterns(raw) {
|
|
74
|
+
if (!Array.isArray(raw)) return [];
|
|
75
|
+
const migrated = [];
|
|
76
|
+
for (const entry of raw) {
|
|
77
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
78
|
+
migrated.push({
|
|
79
|
+
value: entry,
|
|
80
|
+
mode: "contains"
|
|
81
|
+
});
|
|
82
|
+
} else if (entry && typeof entry === "object" && typeof entry.value === "string") {
|
|
83
|
+
const e = entry;
|
|
84
|
+
migrated.push({
|
|
85
|
+
value: e.value,
|
|
86
|
+
mode: e.mode === "exact" ? "exact" : "contains"
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return migrated;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Always keep the default patterns present (e.g. Buoy's license API). */
|
|
94
|
+
function ensureDefaults(patterns) {
|
|
95
|
+
const result = [...patterns];
|
|
96
|
+
for (const def of DEFAULT_PATTERNS) {
|
|
97
|
+
if (!result.some(p => p.value === def.value)) {
|
|
98
|
+
result.push({
|
|
99
|
+
...def
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Singleton store. One instance per JS runtime, so every consumer (Network tool,
|
|
107
|
+
* Events tool, on mobile or on the desktop dashboard) shares the same patterns.
|
|
108
|
+
*/
|
|
109
|
+
class IgnoredPatternsStore {
|
|
110
|
+
patterns = ensureDefaults(DEFAULT_PATTERNS.map(p => ({
|
|
111
|
+
...p
|
|
112
|
+
})));
|
|
113
|
+
listeners = new Set();
|
|
114
|
+
loaded = false;
|
|
115
|
+
loadPromise = null;
|
|
116
|
+
/** Set once a mutation happens so a slow initial load can't clobber it. */
|
|
117
|
+
dirty = false;
|
|
118
|
+
|
|
119
|
+
/** Current patterns (stable reference until a mutation occurs). */
|
|
120
|
+
getPatterns() {
|
|
121
|
+
return this.patterns;
|
|
122
|
+
}
|
|
123
|
+
subscribe(listener) {
|
|
124
|
+
this.listeners.add(listener);
|
|
125
|
+
// Lazily hydrate from storage the first time anyone cares.
|
|
126
|
+
void this.ensureLoaded();
|
|
127
|
+
return () => {
|
|
128
|
+
this.listeners.delete(listener);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
emit() {
|
|
132
|
+
this.listeners.forEach(l => l());
|
|
133
|
+
}
|
|
134
|
+
ensureLoaded() {
|
|
135
|
+
if (this.loaded) return Promise.resolve();
|
|
136
|
+
if (this.loadPromise) return this.loadPromise;
|
|
137
|
+
this.loadPromise = (async () => {
|
|
138
|
+
try {
|
|
139
|
+
const stored = await persistentStorage.getItem(devToolsStorageKeys.network.ignoredDomains());
|
|
140
|
+
// A mutation may have raced this read — never overwrite user intent.
|
|
141
|
+
if (stored && !this.dirty) {
|
|
142
|
+
const migrated = ensureDefaults(migratePatterns(JSON.parse(stored)));
|
|
143
|
+
this.patterns = migrated;
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Silently fall back to defaults.
|
|
147
|
+
} finally {
|
|
148
|
+
this.loaded = true;
|
|
149
|
+
this.emit();
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
return this.loadPromise;
|
|
153
|
+
}
|
|
154
|
+
persist() {
|
|
155
|
+
persistentStorage.setItem(devToolsStorageKeys.network.ignoredDomains(), JSON.stringify(this.patterns)).catch(() => {
|
|
156
|
+
// Silently fail — patterns remain in memory.
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
commit(next) {
|
|
160
|
+
this.dirty = true;
|
|
161
|
+
this.patterns = next;
|
|
162
|
+
this.emit();
|
|
163
|
+
this.persist();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Add a pattern (no-op if its value already exists). */
|
|
167
|
+
add(pattern) {
|
|
168
|
+
const value = pattern.value.trim();
|
|
169
|
+
if (!value) return;
|
|
170
|
+
if (this.patterns.some(p => p.value === value)) return;
|
|
171
|
+
this.commit([...this.patterns, {
|
|
172
|
+
value,
|
|
173
|
+
mode: pattern.mode
|
|
174
|
+
}]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Remove a pattern by value. */
|
|
178
|
+
remove(value) {
|
|
179
|
+
if (!this.patterns.some(p => p.value === value)) return;
|
|
180
|
+
this.commit(this.patterns.filter(p => p.value !== value));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Add as `contains` if absent, otherwise remove (used by detail-view chips). */
|
|
184
|
+
toggle(value) {
|
|
185
|
+
const trimmed = value.trim();
|
|
186
|
+
if (!trimmed) return;
|
|
187
|
+
this.commit(this.patterns.some(p => p.value === trimmed) ? this.patterns.filter(p => p.value !== trimmed) : [...this.patterns, {
|
|
188
|
+
value: trimmed,
|
|
189
|
+
mode: "contains"
|
|
190
|
+
}]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Flip an existing pattern between `contains` and `exact`. */
|
|
194
|
+
toggleMode(value) {
|
|
195
|
+
if (!this.patterns.some(p => p.value === value)) return;
|
|
196
|
+
this.commit(this.patterns.map(p => p.value === value ? {
|
|
197
|
+
...p,
|
|
198
|
+
mode: p.mode === "contains" ? "exact" : "contains"
|
|
199
|
+
} : p));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export const ignoredPatternsStore = new IgnoredPatternsStore();
|
|
203
|
+
/**
|
|
204
|
+
* Subscribe to the shared ignored-patterns store. All consumers in a runtime
|
|
205
|
+
* see the same patterns and the same mutations.
|
|
206
|
+
*/
|
|
207
|
+
export function useIgnoredPatterns() {
|
|
208
|
+
const [patterns, setPatterns] = useState(() => ignoredPatternsStore.getPatterns());
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
return ignoredPatternsStore.subscribe(() => {
|
|
211
|
+
setPatterns(ignoredPatternsStore.getPatterns());
|
|
212
|
+
});
|
|
213
|
+
}, []);
|
|
214
|
+
const values = useMemo(() => new Set(patterns.map(p => p.value)), [patterns]);
|
|
215
|
+
return {
|
|
216
|
+
patterns,
|
|
217
|
+
values,
|
|
218
|
+
add: pattern => ignoredPatternsStore.add(pattern),
|
|
219
|
+
remove: value => ignoredPatternsStore.remove(value),
|
|
220
|
+
toggle: value => ignoredPatternsStore.toggle(value),
|
|
221
|
+
toggleMode: value => ignoredPatternsStore.toggleMode(value)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
* Shared store utilities and base classes
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export { BaseEventStore } from "./BaseEventStore.js";
|
|
7
|
+
export { BaseEventStore } from "./BaseEventStore.js";
|
|
8
|
+
export { ignoredPatternsStore, useIgnoredPatterns, urlMatchesIgnoredPattern, isUrlIgnored } from "./ignoredPatternsStore.js";
|
|
@@ -10,6 +10,7 @@ export function CompactRow({
|
|
|
10
10
|
statusSublabel,
|
|
11
11
|
primaryText,
|
|
12
12
|
secondaryText,
|
|
13
|
+
secondaryAccessory,
|
|
13
14
|
expandedContent,
|
|
14
15
|
isExpanded,
|
|
15
16
|
badgeText,
|
|
@@ -61,10 +62,13 @@ export function CompactRow({
|
|
|
61
62
|
style: styles.queryHash,
|
|
62
63
|
numberOfLines: isExpanded ? undefined : 2,
|
|
63
64
|
children: primaryText
|
|
64
|
-
}), !isExpanded && secondaryText ? /*#__PURE__*/
|
|
65
|
-
style: styles.
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
}), !isExpanded && (secondaryText || secondaryAccessory) ? /*#__PURE__*/_jsxs(View, {
|
|
66
|
+
style: styles.secondaryRow,
|
|
67
|
+
children: [secondaryText ? /*#__PURE__*/_jsx(Text, {
|
|
68
|
+
style: styles.secondaryText,
|
|
69
|
+
numberOfLines: 1,
|
|
70
|
+
children: secondaryText
|
|
71
|
+
}) : null, secondaryAccessory]
|
|
68
72
|
}) : null]
|
|
69
73
|
}), /*#__PURE__*/_jsxs(View, {
|
|
70
74
|
style: styles.rightSection,
|
|
@@ -185,6 +189,12 @@ const styles = StyleSheet.create({
|
|
|
185
189
|
color: buoyColors.text,
|
|
186
190
|
lineHeight: 16
|
|
187
191
|
},
|
|
192
|
+
secondaryRow: {
|
|
193
|
+
flexDirection: "row",
|
|
194
|
+
alignItems: "center",
|
|
195
|
+
gap: 6,
|
|
196
|
+
marginTop: 1
|
|
197
|
+
},
|
|
188
198
|
secondaryText: {
|
|
189
199
|
fontSize: 10,
|
|
190
200
|
color: buoyColors.textMuted,
|
|
@@ -39,10 +39,12 @@ export function ExpandedInfoRow({
|
|
|
39
39
|
export function PillBadge({
|
|
40
40
|
color,
|
|
41
41
|
children,
|
|
42
|
-
icon
|
|
42
|
+
icon,
|
|
43
|
+
size = "md"
|
|
43
44
|
}) {
|
|
45
|
+
const isSm = size === "sm";
|
|
44
46
|
return /*#__PURE__*/_jsxs(View, {
|
|
45
|
-
style: [styles.pill, {
|
|
47
|
+
style: [styles.pill, isSm && styles.pillSm, {
|
|
46
48
|
backgroundColor: color + "20",
|
|
47
49
|
borderColor: color + "40"
|
|
48
50
|
}],
|
|
@@ -50,7 +52,7 @@ export function PillBadge({
|
|
|
50
52
|
style: styles.pillIcon,
|
|
51
53
|
children: icon
|
|
52
54
|
}), /*#__PURE__*/_jsx(Text, {
|
|
53
|
-
style: [styles.pillText, {
|
|
55
|
+
style: [styles.pillText, isSm && styles.pillTextSm, {
|
|
54
56
|
color
|
|
55
57
|
}],
|
|
56
58
|
children: children
|
|
@@ -78,6 +80,10 @@ const styles = StyleSheet.create({
|
|
|
78
80
|
borderRadius: 999,
|
|
79
81
|
borderWidth: 1
|
|
80
82
|
},
|
|
83
|
+
pillSm: {
|
|
84
|
+
paddingHorizontal: 5,
|
|
85
|
+
paddingVertical: 1
|
|
86
|
+
},
|
|
81
87
|
pillIcon: {
|
|
82
88
|
marginRight: 4
|
|
83
89
|
},
|
|
@@ -86,5 +92,9 @@ const styles = StyleSheet.create({
|
|
|
86
92
|
fontWeight: "700",
|
|
87
93
|
fontFamily: "monospace",
|
|
88
94
|
letterSpacing: 0.5
|
|
95
|
+
},
|
|
96
|
+
pillTextSm: {
|
|
97
|
+
fontSize: 9,
|
|
98
|
+
letterSpacing: 0.3
|
|
89
99
|
}
|
|
90
100
|
});
|
|
@@ -10,4 +10,4 @@ export { parseValue, formatValue, getTypeColor, truncateText, flattenObject, for
|
|
|
10
10
|
export { loadOptionalModule, getCachedOptionalModule } from "./loadOptionalModule.js";
|
|
11
11
|
export { Subscribable } from "./subscribable.js";
|
|
12
12
|
export { subscriberCountNotifier, subscribeToSubscriberCountChanges, notifySubscriberCountChange } from "./subscriberCountNotifier.js";
|
|
13
|
-
export { useSafeRouter, useSafePathname, useSafeSegments, useSafeGlobalSearchParams, getSafeRouter, isExpoRouterAvailable } from "./safeExpoRouter.js";
|
|
13
|
+
export { useSafeRouter, useSafePathname, useSafeSegments, useSafeGlobalSearchParams, getSafeRouter, getSafeCurrentPathname, isExpoRouterAvailable } from "./safeExpoRouter.js";
|