@flamingo-stack/openframe-frontend-core 0.0.583 → 0.0.584-1967.5424.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/dist/{chunk-BTBW4L2W.cjs → chunk-33DHEEMP.cjs} +33 -33
- package/dist/{chunk-BTBW4L2W.cjs.map → chunk-33DHEEMP.cjs.map} +1 -1
- package/dist/{chunk-XZ3MKTJZ.js → chunk-3ELW4VML.js} +2 -2
- package/dist/{chunk-WTC6QQNX.cjs → chunk-3ZA2OYRE.cjs} +3 -3
- package/dist/{chunk-WTC6QQNX.cjs.map → chunk-3ZA2OYRE.cjs.map} +1 -1
- package/dist/{chunk-J3EEL6YC.js → chunk-4T5KOD3G.js} +4 -4
- package/dist/{chunk-HIHPWVKP.js → chunk-5CJKZHWO.js} +5 -5
- package/dist/{chunk-VWHEXQ64.js → chunk-6BJYPG2V.js} +2 -2
- package/dist/{chunk-WBTKXNKZ.cjs → chunk-7X5TCK4R.cjs} +4 -4
- package/dist/{chunk-WBTKXNKZ.cjs.map → chunk-7X5TCK4R.cjs.map} +1 -1
- package/dist/{chunk-IOP7BCRQ.js → chunk-EJKEDCTH.js} +5 -5
- package/dist/{chunk-OCXFFGFA.cjs → chunk-HCKEDTKJ.cjs} +10 -10
- package/dist/{chunk-OCXFFGFA.cjs.map → chunk-HCKEDTKJ.cjs.map} +1 -1
- package/dist/{chunk-EATPU5EF.cjs → chunk-IIT5HWLY.cjs} +3 -3
- package/dist/{chunk-EATPU5EF.cjs.map → chunk-IIT5HWLY.cjs.map} +1 -1
- package/dist/{chunk-RBDVHLT5.cjs → chunk-JAJQ5P2Q.cjs} +17 -17
- package/dist/{chunk-RBDVHLT5.cjs.map → chunk-JAJQ5P2Q.cjs.map} +1 -1
- package/dist/{chunk-YNLIXABT.cjs → chunk-JFKK3ZOA.cjs} +34 -34
- package/dist/{chunk-YNLIXABT.cjs.map → chunk-JFKK3ZOA.cjs.map} +1 -1
- package/dist/{chunk-PFKWJ6PJ.js → chunk-JVWRMLQZ.js} +18 -1
- package/dist/chunk-JVWRMLQZ.js.map +1 -0
- package/dist/{chunk-WMRXB6RS.js → chunk-KP6VJP36.js} +2 -2
- package/dist/{chunk-FCLM2RXI.js → chunk-LNKULR4V.js} +2 -2
- package/dist/{chunk-3TNM6SDP.cjs → chunk-MEJFHD5N.cjs} +13 -13
- package/dist/{chunk-3TNM6SDP.cjs.map → chunk-MEJFHD5N.cjs.map} +1 -1
- package/dist/{chunk-EZ5VU5WS.js → chunk-MYEG5ZDT.js} +2 -2
- package/dist/{chunk-KDWNMFX4.cjs → chunk-NVY2ZP7Z.cjs} +17 -17
- package/dist/{chunk-KDWNMFX4.cjs.map → chunk-NVY2ZP7Z.cjs.map} +1 -1
- package/dist/{chunk-TRD2YICZ.cjs → chunk-P2VRLSAP.cjs} +16 -16
- package/dist/{chunk-TRD2YICZ.cjs.map → chunk-P2VRLSAP.cjs.map} +1 -1
- package/dist/{chunk-6BI3AYBY.cjs → chunk-Q66AMADB.cjs} +4 -4
- package/dist/{chunk-6BI3AYBY.cjs.map → chunk-Q66AMADB.cjs.map} +1 -1
- package/dist/{chunk-M6ZUEBDK.cjs → chunk-QJR32JM2.cjs} +18 -1
- package/dist/chunk-QJR32JM2.cjs.map +1 -0
- package/dist/{chunk-PMA7DVQR.js → chunk-RHPCSFJH.js} +3 -3
- package/dist/{chunk-R5A3MGMY.js → chunk-RURZ6QLN.js} +2 -2
- package/dist/{chunk-L26MD32B.js → chunk-TLKHLGAB.js} +2 -2
- package/dist/{chunk-3RR67XC2.cjs → chunk-UCNEQKIT.cjs} +3 -3
- package/dist/{chunk-3RR67XC2.cjs.map → chunk-UCNEQKIT.cjs.map} +1 -1
- package/dist/{chunk-IJVWKLJK.cjs → chunk-VOAZU46B.cjs} +49 -49
- package/dist/{chunk-IJVWKLJK.cjs.map → chunk-VOAZU46B.cjs.map} +1 -1
- package/dist/{chunk-DROXHJVZ.js → chunk-VZSTL5VG.js} +2 -2
- package/dist/{chunk-AJLF6MBD.js → chunk-XPJKQ63S.js} +2 -2
- package/dist/{chunk-WHNC7C76.js → chunk-YFZDIEVC.js} +4 -4
- package/dist/{chunk-IFPGXSMZ.cjs → chunk-ZHUBNWEX.cjs} +3 -3
- package/dist/{chunk-IFPGXSMZ.cjs.map → chunk-ZHUBNWEX.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +29 -29
- package/dist/components/index.js +12 -12
- package/dist/components/meeting-scheduler/index.cjs +18 -18
- package/dist/components/meeting-scheduler/index.js +1 -1
- package/dist/components/navigation/index.cjs +5 -5
- package/dist/components/navigation/index.js +4 -4
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +6 -6
- package/dist/index.js +5 -5
- package/dist/utils/announcement-storage.d.ts +25 -16
- package/dist/utils/announcement-storage.d.ts.map +1 -1
- package/dist/utils/dismiss-cookie.d.ts +27 -8
- package/dist/utils/dismiss-cookie.d.ts.map +1 -1
- package/dist/utils/dismissal-storage.d.ts +2 -2
- package/dist/utils/dismissal-storage.d.ts.map +1 -1
- package/dist/utils/index.cjs +70 -53
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +70 -53
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/utils/__tests__/dismiss-cookie.test.ts +133 -0
- package/src/utils/announcement-storage.ts +25 -16
- package/src/utils/dismiss-cookie.ts +86 -9
- package/src/utils/dismissal-storage.ts +2 -2
- package/dist/chunk-M6ZUEBDK.cjs.map +0 -1
- package/dist/chunk-PFKWJ6PJ.js.map +0 -1
- /package/dist/{chunk-XZ3MKTJZ.js.map → chunk-3ELW4VML.js.map} +0 -0
- /package/dist/{chunk-J3EEL6YC.js.map → chunk-4T5KOD3G.js.map} +0 -0
- /package/dist/{chunk-HIHPWVKP.js.map → chunk-5CJKZHWO.js.map} +0 -0
- /package/dist/{chunk-VWHEXQ64.js.map → chunk-6BJYPG2V.js.map} +0 -0
- /package/dist/{chunk-IOP7BCRQ.js.map → chunk-EJKEDCTH.js.map} +0 -0
- /package/dist/{chunk-WMRXB6RS.js.map → chunk-KP6VJP36.js.map} +0 -0
- /package/dist/{chunk-FCLM2RXI.js.map → chunk-LNKULR4V.js.map} +0 -0
- /package/dist/{chunk-EZ5VU5WS.js.map → chunk-MYEG5ZDT.js.map} +0 -0
- /package/dist/{chunk-PMA7DVQR.js.map → chunk-RHPCSFJH.js.map} +0 -0
- /package/dist/{chunk-R5A3MGMY.js.map → chunk-RURZ6QLN.js.map} +0 -0
- /package/dist/{chunk-L26MD32B.js.map → chunk-TLKHLGAB.js.map} +0 -0
- /package/dist/{chunk-DROXHJVZ.js.map → chunk-VZSTL5VG.js.map} +0 -0
- /package/dist/{chunk-AJLF6MBD.js.map → chunk-XPJKQ63S.js.map} +0 -0
- /package/dist/{chunk-WHNC7C76.js.map → chunk-YFZDIEVC.js.map} +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier selection on both sides of the cookie-averse split, plus the match rule
|
|
3
|
+
* the two tiers feed.
|
|
4
|
+
*
|
|
5
|
+
* `document.URL` is stubbed rather than the document navigated: jsdom will not
|
|
6
|
+
* take a `tauri://` document URL, and the scheme is the only input the tier
|
|
7
|
+
* choice reads — so the stub sits exactly at the seam production reads.
|
|
8
|
+
*
|
|
9
|
+
* The round-trips run against the real stores rather than spies: reading a
|
|
10
|
+
* value back is the only proof a write actually stuck, which is the whole
|
|
11
|
+
* subject of this file.
|
|
12
|
+
*/
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
14
|
+
import { clearDismissCookie, isDismissedCookieValue, readDismissCookie, writeDismissCookie } from '../dismiss-cookie';
|
|
15
|
+
|
|
16
|
+
const KEY = 'openframe-walkthrough-video-dismissed';
|
|
17
|
+
const nativeUrlDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'URL');
|
|
18
|
+
|
|
19
|
+
function setDocumentUrl(url: string): void {
|
|
20
|
+
Object.defineProperty(document, 'URL', { value: url, writable: true, configurable: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Restore the native accessor rather than re-stubbing a hardcoded URL, which
|
|
25
|
+
// would silently diverge from jsdom's own document URL if the vitest
|
|
26
|
+
// environment is ever given an explicit one.
|
|
27
|
+
if (nativeUrlDescriptor) Object.defineProperty(document, 'URL', nativeUrlDescriptor);
|
|
28
|
+
clearDismissCookie(KEY);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('dismissal on an http(s) origin', () => {
|
|
32
|
+
beforeEach(() => setDocumentUrl('https://openframe.ai/dashboard'));
|
|
33
|
+
|
|
34
|
+
it('round-trips through the cookie', () => {
|
|
35
|
+
writeDismissCookie(KEY, 'video-7');
|
|
36
|
+
expect(document.cookie).toContain(`${KEY}=video-7`);
|
|
37
|
+
expect(readDismissCookie(KEY)).toBe('video-7');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('takes the cookie tier on plain http too', () => {
|
|
41
|
+
// Dev runs on `http://localhost`. Were only `https:` accepted, it would
|
|
42
|
+
// silently drop to the fallback tier, where SSR cannot see the dismissal.
|
|
43
|
+
setDocumentUrl('http://localhost:3000/dashboard');
|
|
44
|
+
writeDismissCookie(KEY, 'video-7');
|
|
45
|
+
expect(document.cookie).toContain(`${KEY}=video-7`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('leaves the fallback tier untouched', () => {
|
|
49
|
+
// Pins fallback-not-mirror; see `fallbackStore`.
|
|
50
|
+
writeDismissCookie(KEY, 'video-7');
|
|
51
|
+
expect(window.localStorage.getItem(KEY)).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('fails open on a malformed cookie value', () => {
|
|
55
|
+
// `decodeURIComponent` throws `URIError` on bad percent-encoding; the other
|
|
56
|
+
// half of the guarantee `readDismissCookie` documents.
|
|
57
|
+
document.cookie = `${KEY}=%E0%A4%A; path=/`;
|
|
58
|
+
expect(readDismissCookie(KEY)).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('clears the cookie tier', () => {
|
|
62
|
+
writeDismissCookie(KEY, 'video-7');
|
|
63
|
+
clearDismissCookie(KEY);
|
|
64
|
+
expect(document.cookie).not.toContain(KEY);
|
|
65
|
+
expect(readDismissCookie(KEY)).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('dismissal on a cookie-averse origin (tauri://, capacitor://)', () => {
|
|
70
|
+
beforeEach(() => setDocumentUrl('tauri://localhost/index.html'));
|
|
71
|
+
|
|
72
|
+
it('writes no cookie', () => {
|
|
73
|
+
writeDismissCookie(KEY, 'video-7');
|
|
74
|
+
expect(document.cookie).not.toContain(KEY);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('persists through localStorage and keeps the id-match rule', () => {
|
|
78
|
+
writeDismissCookie(KEY, 'video-7');
|
|
79
|
+
// jsdom keys its cookie jar off the REAL document URL, not the stub above,
|
|
80
|
+
// so a cookie-only implementation's write would still land here and serve
|
|
81
|
+
// the reads below. Emptying the jar first is what makes them prove the
|
|
82
|
+
// value came from the fallback tier; the assertion confirms the wipe took,
|
|
83
|
+
// since a path or domain mismatch would leave the cookie in place and
|
|
84
|
+
// hollow out both reads.
|
|
85
|
+
document.cookie = `${KEY}=; path=/; max-age=0`;
|
|
86
|
+
expect(document.cookie).not.toContain(KEY);
|
|
87
|
+
|
|
88
|
+
// The positive half is the discriminator — asserting only the `video-8`
|
|
89
|
+
// false would also pass against a fallback tier that stored nothing.
|
|
90
|
+
expect(isDismissedCookieValue(readDismissCookie(KEY), 'video-7')).toBe(true);
|
|
91
|
+
expect(isDismissedCookieValue(readDismissCookie(KEY), 'video-8')).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('fails open on a corrupted stored value', () => {
|
|
95
|
+
// Seeded through the real writer first, so this also pins the fallback key
|
|
96
|
+
// to the cookie name: read a different key and the pre-corruption value
|
|
97
|
+
// would still come back.
|
|
98
|
+
writeDismissCookie(KEY, 'video-7');
|
|
99
|
+
window.localStorage.setItem(KEY, 'not-json');
|
|
100
|
+
// Callers reveal the surface off this read, so an unreadable value must
|
|
101
|
+
// mean "not dismissed" rather than throwing and hiding it permanently.
|
|
102
|
+
expect(readDismissCookie(KEY)).toBeUndefined();
|
|
103
|
+
|
|
104
|
+
// Parses, but is not the string the tier stores — what `validate` is for.
|
|
105
|
+
window.localStorage.setItem(KEY, '123');
|
|
106
|
+
expect(readDismissCookie(KEY)).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('the match rule itself', () => {
|
|
111
|
+
it('treats "nothing stored, nothing active" as NOT dismissed', () => {
|
|
112
|
+
// Both sides undefined is the case the `!!id` guard exists for: a bare
|
|
113
|
+
// equality would call it dismissed and suppress a surface that has no
|
|
114
|
+
// active item to be dismissed against.
|
|
115
|
+
expect(isDismissedCookieValue(undefined, undefined)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('clearing across the tier split', () => {
|
|
120
|
+
it('clears the tier the current scheme does NOT select', () => {
|
|
121
|
+
// Written on the fallback tier, cleared from a document on the cookie tier
|
|
122
|
+
// — the case a same-origin `blob:`/`about:blank` child produces, and the
|
|
123
|
+
// one a tier-checking clear would leave stranded.
|
|
124
|
+
setDocumentUrl('tauri://localhost/index.html');
|
|
125
|
+
writeDismissCookie(KEY, 'video-7');
|
|
126
|
+
|
|
127
|
+
setDocumentUrl('https://openframe.ai/dashboard');
|
|
128
|
+
clearDismissCookie(KEY);
|
|
129
|
+
|
|
130
|
+
setDocumentUrl('tauri://localhost/index.html');
|
|
131
|
+
expect(readDismissCookie(KEY)).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Announcement dismissal store.
|
|
3
3
|
*
|
|
4
|
-
* The dismissal COOKIE is the
|
|
5
|
-
* reads it server-side to decide whether to SSR-seed the bar (no
|
|
6
|
-
* dismissed users), and the client reads it via
|
|
7
|
-
* One cookie per platform holding the LAST dismissed
|
|
8
|
-
* dismissal is an id-match, never mere cookie presence, so a
|
|
9
|
-
* always shows after an old one was dismissed, and
|
|
10
|
-
* announcement intentionally re-shows it.
|
|
4
|
+
* The dismissal COOKIE is the source of truth wherever cookies work: the hub's
|
|
5
|
+
* root layout reads it server-side to decide whether to SSR-seed the bar (no
|
|
6
|
+
* flash for dismissed users), and the client reads it via
|
|
7
|
+
* `isAnnouncementDismissed`. One cookie per platform holding the LAST dismissed
|
|
8
|
+
* announcement id — dismissal is an id-match, never mere cookie presence, so a
|
|
9
|
+
* new announcement always shows after an old one was dismissed, and
|
|
10
|
+
* re-activating an old announcement intentionally re-shows it.
|
|
11
|
+
*
|
|
12
|
+
* On a cookie-averse document (see `utils/dismiss-cookie.ts`) the primitive
|
|
13
|
+
* writes `<platform>-announcement-dismissed` to localStorage instead. Do not
|
|
14
|
+
* mistake it for the legacy key below and delete the write: it is the ONLY
|
|
15
|
+
* thing making a dismissal persist in a shell. `clearAnnouncementDismissals`' prefix sweep does match it, which
|
|
16
|
+
* is right — a full reset should take both.
|
|
11
17
|
*
|
|
12
18
|
* The legacy per-id localStorage key (`<platform>-announcement-<id>-dismissed`)
|
|
13
19
|
* is READ-ONLY legacy: consulted only when no cookie exists for the platform,
|
|
14
20
|
* purely as the migration trigger (the bar's mount effect backfills the
|
|
15
|
-
* cookie).
|
|
21
|
+
* cookie). Nothing writes it any more, so that store dies out.
|
|
16
22
|
*
|
|
17
23
|
* No `"use client"` directive on purpose — `announcementDismissCookieName` and
|
|
18
24
|
* `isDismissedCookieValue` are pure/isomorphic and imported by the hub's
|
|
@@ -36,18 +42,21 @@ export { isDismissedCookieValue } from './dismiss-cookie';
|
|
|
36
42
|
|
|
37
43
|
const legacyDismissKey = (platform: string, id: string) => `${platform}-announcement-${id}-dismissed`;
|
|
38
44
|
|
|
39
|
-
/** Persist a dismissal
|
|
40
|
-
*
|
|
45
|
+
/** Persist a dismissal (1 year). The SSR layout sees it on the next request —
|
|
46
|
+
* except in a native shell, where the primitive falls back to localStorage and
|
|
47
|
+
* there is no SSR layout to see it anyway. The LEGACY per-id key is still never
|
|
48
|
+
* written. */
|
|
41
49
|
export function dismissAnnouncement(platform: string, id: string): void {
|
|
42
50
|
if (typeof document === 'undefined') return;
|
|
43
51
|
writeDismissCookie(announcementDismissCookieName(platform), id);
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
/**
|
|
47
|
-
* Client-side dismissal check —
|
|
48
|
-
* consulted ONLY when
|
|
49
|
-
* storage, so callers must invoke it
|
|
50
|
-
* (a render-time read would desync hydration
|
|
55
|
+
* Client-side dismissal check — primary store first, with the legacy
|
|
56
|
+
* localStorage key consulted ONLY when that comes back empty. Reads browser
|
|
57
|
+
* storage, so callers must invoke it
|
|
58
|
+
* from effects, never during render (a render-time read would desync hydration
|
|
59
|
+
* from the SSR HTML).
|
|
51
60
|
*/
|
|
52
61
|
export function isAnnouncementDismissed(platform: string, id: string): boolean {
|
|
53
62
|
if (typeof document === 'undefined') return false;
|
|
@@ -60,8 +69,8 @@ export function isAnnouncementDismissed(platform: string, id: string): boolean {
|
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
/** Remove ALL dismissal state for a platform
|
|
64
|
-
*
|
|
72
|
+
/** Remove ALL dismissal state for a platform — test/story helper so callers
|
|
73
|
+
* never restate the key encoding. The header owns the tier inventory. */
|
|
65
74
|
export function clearAnnouncementDismissals(platform: string): void {
|
|
66
75
|
if (typeof document === 'undefined') return;
|
|
67
76
|
clearDismissCookie(announcementDismissCookieName(platform));
|
|
@@ -3,31 +3,92 @@
|
|
|
3
3
|
* dismissed", shared by every dismissible surface (announcement bar, floating
|
|
4
4
|
* walkthrough video, and anything added later).
|
|
5
5
|
*
|
|
6
|
-
* The dismissal rule is an ID MATCH, never mere cookie presence: the
|
|
7
|
-
*
|
|
6
|
+
* The dismissal rule is an ID MATCH, never mere cookie presence: the store
|
|
7
|
+
* holds the id of the LAST dismissed item, so publishing a NEW item re-shows
|
|
8
8
|
* the surface while the old dismissal still suppresses the old item.
|
|
9
9
|
*
|
|
10
|
+
* TWO TIERS. The cookie is primary wherever cookies work; on a cookie-averse
|
|
11
|
+
* document `localStorage` takes over under the same key. See `canUseCookies`
|
|
12
|
+
* for when that is, and `fallbackStore` for why it is a fallback and not a
|
|
13
|
+
* mirror.
|
|
14
|
+
*
|
|
10
15
|
* No `"use client"` on purpose — `isDismissedCookieValue` is pure and is used by
|
|
11
16
|
* server code (the hub's root layout reads the cookie during SSR to avoid a
|
|
12
17
|
* flash); the DOM-touching helpers guard on `typeof document`.
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
import { createLocalStorageAdapter } from './local-storage-adapter';
|
|
21
|
+
|
|
22
|
+
/** One year — the COOKIE tier's horizon. The fallback tier has no expiry, so a
|
|
23
|
+
* dismissal in a native shell is permanent; adding an expiry envelope there
|
|
24
|
+
* would be inventing a second encoding to answer a question nobody has asked. */
|
|
16
25
|
const DISMISS_COOKIE_MAX_AGE_SECONDS = 31_536_000;
|
|
17
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Whether `document.cookie` actually stores anything here.
|
|
29
|
+
*
|
|
30
|
+
* A Document whose URL scheme is not http(s) is COOKIE-AVERSE — the HTML spec's
|
|
31
|
+
* term for one where the `document.cookie` setter is a silent no-op and the
|
|
32
|
+
* getter returns ''. That is where the custom-scheme shells run: Tauri serves
|
|
33
|
+
* the bundle from `tauri://localhost` on macOS/Linux, Capacitor from
|
|
34
|
+
* `capacitor://localhost` on iOS. Every dismissal there was dropped on write
|
|
35
|
+
* and unreadable on the next mount, so the surface came back in every new
|
|
36
|
+
* window and on every launch. Tauri on Windows and Capacitor on Android serve
|
|
37
|
+
* http(s), were never affected, and stay on the cookie tier.
|
|
38
|
+
*
|
|
39
|
+
* The scheme is read directly rather than probed with a throwaway write: a lib
|
|
40
|
+
* whose consumers are consent-regulated marketing sites should not set a
|
|
41
|
+
* cookie, however briefly, to answer a question the URL already answers.
|
|
42
|
+
* Deliberately NOT covered: cookies blocked by user or enterprise policy on an
|
|
43
|
+
* http(s) origin — the surface reappears there exactly as it always has, which
|
|
44
|
+
* is what someone who blocked cookies asked for.
|
|
45
|
+
*
|
|
46
|
+
* Read off `document.URL`, not `window.location`: averseness is a property of
|
|
47
|
+
* the DOCUMENT's URL, and this file must stay usable wherever `document` is the
|
|
48
|
+
* only thing the other helpers here already require. `about:blank` and
|
|
49
|
+
* `about:srcdoc` are the one miss: they carry their creator's origin and are
|
|
50
|
+
* served its cookies, yet land on the fallback tier here. Accepted — nothing
|
|
51
|
+
* hosts this lib in one, and the cost is a re-show, not a broken surface.
|
|
52
|
+
*/
|
|
53
|
+
function canUseCookies(): boolean {
|
|
54
|
+
if (typeof document === 'undefined') return false;
|
|
55
|
+
return document.URL.startsWith('http:') || document.URL.startsWith('https:');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The fallback tier for cookie-averse documents, keyed identically to the
|
|
60
|
+
* cookie so a host needs no second key.
|
|
61
|
+
*
|
|
62
|
+
* A FALLBACK, never a mirror: cookies stay primary wherever they work, because
|
|
63
|
+
* they are the only tier a server can read — which is what lets the hub
|
|
64
|
+
* suppress a dismissed surface during SSR instead of flashing it — and writing
|
|
65
|
+
* both would mean clearing cookies no longer clears a dismissal.
|
|
66
|
+
*/
|
|
67
|
+
function fallbackStore(name: string) {
|
|
68
|
+
return createLocalStorageAdapter<string>({
|
|
69
|
+
key: name,
|
|
70
|
+
logTag: '[dismiss-cookie]',
|
|
71
|
+
validate: (parsed): parsed is string => typeof parsed === 'string',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
18
75
|
/** THE dismissal match rule. `undefined` id (nothing active) → not dismissed. */
|
|
19
76
|
export function isDismissedCookieValue(cookieValue: string | undefined, id: string | undefined): boolean {
|
|
20
77
|
return !!id && cookieValue === id;
|
|
21
78
|
}
|
|
22
79
|
|
|
23
80
|
/**
|
|
24
|
-
* Read a dismissal
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
81
|
+
* Read a dismissal. Returns `undefined` when absent OR unreadable — one
|
|
82
|
+
* guarantee with two causes: a cookie with bad percent-encoding, where
|
|
83
|
+
* `decodeURIComponent` throws `URIError`, and a fallback value that is not the
|
|
84
|
+
* JSON string it should be, where the adapter swallows the parse failure.
|
|
85
|
+
* Callers run this inside reveal paths where an uncaught throw would hide the
|
|
86
|
+
* surface permanently, so failing open — treated as not-dismissed — is always
|
|
87
|
+
* the safe side.
|
|
28
88
|
*/
|
|
29
89
|
export function readDismissCookie(name: string): string | undefined {
|
|
30
90
|
if (typeof document === 'undefined') return undefined;
|
|
91
|
+
if (!canUseCookies()) return fallbackStore(name).load() ?? undefined;
|
|
31
92
|
const match = document.cookie.split('; ').find(row => row.startsWith(`${name}=`));
|
|
32
93
|
if (!match) return undefined;
|
|
33
94
|
try {
|
|
@@ -37,14 +98,30 @@ export function readDismissCookie(name: string): string | undefined {
|
|
|
37
98
|
}
|
|
38
99
|
}
|
|
39
100
|
|
|
40
|
-
/** Persist a dismissal
|
|
101
|
+
/** Persist a dismissal. */
|
|
41
102
|
export function writeDismissCookie(name: string, id: string): void {
|
|
42
103
|
if (typeof document === 'undefined') return;
|
|
104
|
+
if (!canUseCookies()) {
|
|
105
|
+
fallbackStore(name).save(id);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
43
108
|
document.cookie = `${name}=${encodeURIComponent(id)}; path=/; max-age=${DISMISS_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;
|
|
44
109
|
}
|
|
45
110
|
|
|
46
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Clear a dismissal (test/story helper).
|
|
113
|
+
*
|
|
114
|
+
* BOTH tiers, unconditionally — the tier check governs writes, not removal.
|
|
115
|
+
* Clearing writes nothing and cannot resurrect a dismissal, so it carries none
|
|
116
|
+
* of the reasons the write path commits to one tier, and a clear that leaves
|
|
117
|
+
* the other tier holding a value is a lie.
|
|
118
|
+
*
|
|
119
|
+
* NOT for a shell that changes the scheme it serves under: that is an origin
|
|
120
|
+
* change, so the old tier moves to a storage bucket no code in the new origin
|
|
121
|
+
* can reach — `clear` included.
|
|
122
|
+
*/
|
|
47
123
|
export function clearDismissCookie(name: string): void {
|
|
48
124
|
if (typeof document === 'undefined') return;
|
|
125
|
+
fallbackStore(name).clear();
|
|
49
126
|
document.cookie = `${name}=; path=/; max-age=0`;
|
|
50
127
|
}
|
|
@@ -22,8 +22,8 @@ export function walkthroughDismissCookieName(platform: string): string {
|
|
|
22
22
|
return `${platform}-${WALKTHROUGH_VIDEO_DISMISS_KEY}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** Client-side dismissal check (id-match). Reads
|
|
26
|
-
* effect, never during render. */
|
|
25
|
+
/** Client-side dismissal check (id-match). Reads browser storage, so call it
|
|
26
|
+
* from an effect, never during render. */
|
|
27
27
|
export function isWalkthroughDismissed(key: string, id: string | undefined): boolean {
|
|
28
28
|
return isDismissedCookieValue(readDismissCookie(key), id);
|
|
29
29
|
}
|