@duffcloudservices/cms 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -2
- package/dist/chunk-DAYLLSEE.js +3 -0
- package/dist/{chunk-KCWMS7P4.js.map → chunk-DAYLLSEE.js.map} +1 -1
- package/dist/{chunk-UPAMLKOQ.js → chunk-F3EIWEZD.js} +158 -11
- package/dist/chunk-F3EIWEZD.js.map +1 -0
- package/dist/index.d.ts +74 -6
- package/dist/index.js +88 -6
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +91 -3
- package/dist/plugins/index.js +350 -82
- package/dist/plugins/index.js.map +1 -1
- package/dist/seo/index.d.ts +132 -226
- package/dist/seo/index.js +2 -2
- package/dist/spliceHeadHtml-CsBEucGy.d.ts +254 -0
- package/dist/{vitepressTransform-DeEzgGWU.d.ts → vitepressTransform-DfmABXmK.d.ts} +53 -2
- package/package.json +14 -2
- package/src/components/DcsCallButton.test.ts +126 -0
- package/src/components/DcsCallButton.vue +185 -0
- package/src/components/DcsReviewShowcase.vue +13 -2
- package/src/components/LiteMediaEmbed.test.ts +229 -0
- package/src/components/LiteMediaEmbed.vue +399 -0
- package/src/composables/useMediaCarousel.ts +6 -1
- package/src/composables/useReviewContent.test.ts +86 -0
- package/src/composables/useReviewContent.ts +34 -2
- package/src/composables/useSiteVisitorSession.test.ts +120 -0
- package/src/composables/useSiteVisitorSession.ts +160 -0
- package/dist/chunk-KCWMS7P4.js +0 -3
- package/dist/chunk-UPAMLKOQ.js.map +0 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import { defineComponent, h } from 'vue'
|
|
3
|
+
import { mount } from '@vue/test-utils'
|
|
4
|
+
import { useReviewContent, type ReviewItem } from './useReviewContent'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Honesty guard: consumer-supplied `defaults` (placeholder / sample reviews)
|
|
8
|
+
* are a DEV-ONLY affordance. In a production build the composable must render an
|
|
9
|
+
* honest empty state instead of fabricated social proof — this is the fix for
|
|
10
|
+
* the flagship SSR bug where a fabricated fallback was baked into the crawler
|
|
11
|
+
* HTML while the client showed the real reviews.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SAMPLE_DEFAULTS: ReviewItem[] = [
|
|
15
|
+
{ id: 'seed-1', platform: 'google', rating: 5, authorName: 'Sample Person', text: 'Placeholder review' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
function mountReviews(config: Parameters<typeof useReviewContent>[0]) {
|
|
19
|
+
let api!: ReturnType<typeof useReviewContent>
|
|
20
|
+
const Comp = defineComponent({
|
|
21
|
+
setup() {
|
|
22
|
+
api = useReviewContent(config)
|
|
23
|
+
return () => h('div')
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
const wrapper = mount(Comp)
|
|
27
|
+
return { api, wrapper }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.unstubAllEnvs()
|
|
32
|
+
vi.unstubAllGlobals()
|
|
33
|
+
vi.restoreAllMocks()
|
|
34
|
+
delete (globalThis as Record<string, unknown>).__DCS_CONTENT__
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('useReviewContent honesty guard', () => {
|
|
38
|
+
it('production build with no content define renders an empty state, NOT the defaults', () => {
|
|
39
|
+
vi.stubEnv('DEV', false)
|
|
40
|
+
// __DCS_CONTENT__ intentionally undefined (simulates a production SSR pass
|
|
41
|
+
// where the define never reached the composable / no curated reviews).
|
|
42
|
+
const { api, wrapper } = mountReviews({ sectionKey: 'testimonials', defaults: SAMPLE_DEFAULTS })
|
|
43
|
+
|
|
44
|
+
expect(api.reviews.value).toEqual([])
|
|
45
|
+
expect(api.hasReviews.value).toBe(false)
|
|
46
|
+
expect(api.count.value).toBe(0)
|
|
47
|
+
wrapper.unmount()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('production build with no reviews for the section renders empty, NOT the defaults', () => {
|
|
51
|
+
vi.stubEnv('DEV', false)
|
|
52
|
+
vi.stubGlobal('__DCS_CONTENT__', { global: {}, pages: {} })
|
|
53
|
+
const { api, wrapper } = mountReviews({ sectionKey: 'testimonials', defaults: SAMPLE_DEFAULTS })
|
|
54
|
+
|
|
55
|
+
expect(api.reviews.value).toEqual([])
|
|
56
|
+
expect(api.hasReviews.value).toBe(false)
|
|
57
|
+
wrapper.unmount()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('production build DOES render the real curated reviews from the content define', () => {
|
|
61
|
+
vi.stubEnv('DEV', false)
|
|
62
|
+
vi.stubGlobal('__DCS_CONTENT__', {
|
|
63
|
+
global: {
|
|
64
|
+
'reviews.testimonials.items': [
|
|
65
|
+
{ id: 'real-1', platform: 'google', rating: 5, authorName: 'Real Reviewer', text: 'Genuine review' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
pages: {},
|
|
69
|
+
})
|
|
70
|
+
const { api, wrapper } = mountReviews({ sectionKey: 'testimonials', defaults: SAMPLE_DEFAULTS })
|
|
71
|
+
|
|
72
|
+
expect(api.reviews.value).toHaveLength(1)
|
|
73
|
+
expect(api.reviews.value[0].authorName).toBe('Real Reviewer')
|
|
74
|
+
expect(api.hasReviews.value).toBe(true)
|
|
75
|
+
wrapper.unmount()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('dev build still surfaces the sample defaults (DX affordance)', () => {
|
|
79
|
+
vi.stubEnv('DEV', true)
|
|
80
|
+
const { api, wrapper } = mountReviews({ sectionKey: 'testimonials', defaults: SAMPLE_DEFAULTS })
|
|
81
|
+
|
|
82
|
+
expect(api.reviews.value).toHaveLength(1)
|
|
83
|
+
expect(api.reviews.value[0].id).toBe('seed-1')
|
|
84
|
+
wrapper.unmount()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -25,6 +25,38 @@ function withoutAuthorPhotos(items: ReviewItem[]): ReviewItem[] {
|
|
|
25
25
|
}))
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* True only in a Vite dev build. Vite statically replaces `import.meta.env.DEV`
|
|
30
|
+
* with a boolean literal (`true` in dev, `false` in production). The
|
|
31
|
+
* `typeof import.meta` guard + try/catch keep this safe if the package is ever
|
|
32
|
+
* SSR-externalized (where `import.meta.env` is undefined at the Node runtime and
|
|
33
|
+
* accessing it would throw) — in that case we deliberately return `false` so the
|
|
34
|
+
* production-safe branch is taken.
|
|
35
|
+
*/
|
|
36
|
+
function isDevBuild(): boolean {
|
|
37
|
+
try {
|
|
38
|
+
if (typeof import.meta !== 'undefined' && import.meta.env) {
|
|
39
|
+
return import.meta.env.DEV === true
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// import.meta.env unavailable (externalized SSR / non-Vite host) → prod-safe.
|
|
43
|
+
}
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Honesty guard: consumer-supplied `defaults` are a DEV-ONLY affordance so a
|
|
49
|
+
* freshly-scaffolded site shows sample reviews while it is being wired up. In a
|
|
50
|
+
* production build they must NEVER render — an SSR production pass emitting
|
|
51
|
+
* placeholder reviews is exactly the fabricated-social-proof / hydration bug
|
|
52
|
+
* this guards against (the content define can be missing server-side if the cms
|
|
53
|
+
* package is SSR-externalized). Production therefore falls back to an honest
|
|
54
|
+
* empty state; real reviews always come from `.dcs/content.yaml`.
|
|
55
|
+
*/
|
|
56
|
+
function fallbackReviews(defaults: ReviewItem[]): ReviewItem[] {
|
|
57
|
+
return isDevBuild() ? withoutAuthorPhotos(defaults) : []
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
export interface UseReviewContentConfig {
|
|
29
61
|
/** The section key matching the data-dcs-reviews attribute value */
|
|
30
62
|
sectionKey: string
|
|
@@ -130,7 +162,7 @@ export function useReviewContent(config: UseReviewContentConfig): UseReviewConte
|
|
|
130
162
|
}
|
|
131
163
|
|
|
132
164
|
if (typeof __DCS_CONTENT__ === 'undefined' || __DCS_CONTENT__ == null) {
|
|
133
|
-
return
|
|
165
|
+
return fallbackReviews(defaults)
|
|
134
166
|
}
|
|
135
167
|
|
|
136
168
|
let reviewData: unknown = null
|
|
@@ -146,7 +178,7 @@ export function useReviewContent(config: UseReviewContentConfig): UseReviewConte
|
|
|
146
178
|
}
|
|
147
179
|
|
|
148
180
|
if (!reviewData || !Array.isArray(reviewData)) {
|
|
149
|
-
return
|
|
181
|
+
return fallbackReviews(defaults)
|
|
150
182
|
}
|
|
151
183
|
|
|
152
184
|
return normalizeReviewList(reviewData)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import { fetchSiteVisitorSession } from './useSiteVisitorSession'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The anonymous session probe is the common case and MUST be silent — no
|
|
6
|
+
* console.error / console.warn — whether the server answers with the new
|
|
7
|
+
* `200 { visitor: null }` contract or an old server's `401`. A red console
|
|
8
|
+
* error on every signed-out page load is the console-noise this fixes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function installConsoleSpies() {
|
|
12
|
+
return {
|
|
13
|
+
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
|
|
14
|
+
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function expectSilent(spies: ReturnType<typeof installConsoleSpies>) {
|
|
19
|
+
expect(spies.error).not.toHaveBeenCalled()
|
|
20
|
+
expect(spies.warn).not.toHaveBeenCalled()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.unstubAllGlobals()
|
|
25
|
+
vi.restoreAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('fetchSiteVisitorSession — anonymous probe is silent', () => {
|
|
29
|
+
it('new server 200 {visitor:null} → signed out, no console noise', async () => {
|
|
30
|
+
const spies = installConsoleSpies()
|
|
31
|
+
vi.stubGlobal(
|
|
32
|
+
'fetch',
|
|
33
|
+
vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ visitor: null }) }),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const result = await fetchSiteVisitorSession()
|
|
37
|
+
|
|
38
|
+
expect(result.visitor).toBeNull()
|
|
39
|
+
expect(result.authenticated).toBe(false)
|
|
40
|
+
expectSilent(spies)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('old server 401 → signed out, no console noise', async () => {
|
|
44
|
+
const spies = installConsoleSpies()
|
|
45
|
+
vi.stubGlobal(
|
|
46
|
+
'fetch',
|
|
47
|
+
vi.fn().mockResolvedValue({
|
|
48
|
+
ok: false,
|
|
49
|
+
status: 401,
|
|
50
|
+
json: async () => ({ error: 'Not authenticated' }),
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const result = await fetchSiteVisitorSession()
|
|
55
|
+
|
|
56
|
+
expect(result.visitor).toBeNull()
|
|
57
|
+
expect(result.authenticated).toBe(false)
|
|
58
|
+
expectSilent(spies)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('network error → signed out, no console noise', async () => {
|
|
62
|
+
const spies = installConsoleSpies()
|
|
63
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('offline')))
|
|
64
|
+
|
|
65
|
+
const result = await fetchSiteVisitorSession()
|
|
66
|
+
|
|
67
|
+
expect(result.visitor).toBeNull()
|
|
68
|
+
expect(result.authenticated).toBe(false)
|
|
69
|
+
expectSilent(spies)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('fetchSiteVisitorSession — authenticated shapes', () => {
|
|
74
|
+
it('200 {visitor:{...}} → returns the visitor', async () => {
|
|
75
|
+
vi.stubGlobal(
|
|
76
|
+
'fetch',
|
|
77
|
+
vi.fn().mockResolvedValue({
|
|
78
|
+
ok: true,
|
|
79
|
+
status: 200,
|
|
80
|
+
json: async () => ({
|
|
81
|
+
visitor: { id: 'g-1', email: 'v@example.com', name: 'Visitor', createdAt: '2026-01-01T00:00:00Z' },
|
|
82
|
+
}),
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const result = await fetchSiteVisitorSession()
|
|
87
|
+
|
|
88
|
+
expect(result.authenticated).toBe(true)
|
|
89
|
+
expect(result.visitor?.email).toBe('v@example.com')
|
|
90
|
+
expect(result.visitor?.name).toBe('Visitor')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('contracts-spec shape {authenticated,user:{...}} → returns the visitor', async () => {
|
|
94
|
+
vi.stubGlobal(
|
|
95
|
+
'fetch',
|
|
96
|
+
vi.fn().mockResolvedValue({
|
|
97
|
+
ok: true,
|
|
98
|
+
status: 200,
|
|
99
|
+
json: async () => ({ authenticated: true, user: { email: 'u@example.com', name: 'User' } }),
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const result = await fetchSiteVisitorSession()
|
|
104
|
+
|
|
105
|
+
expect(result.authenticated).toBe(true)
|
|
106
|
+
expect(result.visitor?.email).toBe('u@example.com')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('probes the slug-free /site-auth/session path with credentials', async () => {
|
|
110
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ visitor: null }) })
|
|
111
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
112
|
+
|
|
113
|
+
await fetchSiteVisitorSession()
|
|
114
|
+
|
|
115
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
116
|
+
const [url, init] = fetchMock.mock.calls[0]
|
|
117
|
+
expect(url).toBe('/api/v1/site-auth/session')
|
|
118
|
+
expect(init.credentials).toBe('include')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site-visitor session probe client.
|
|
3
|
+
*
|
|
4
|
+
* Customer sites poll "who is signed in?" on load to render the right auth UI.
|
|
5
|
+
* Signed-out is the EXPECTED state for the vast majority of visits, so this
|
|
6
|
+
* client treats it as normal and never logs:
|
|
7
|
+
*
|
|
8
|
+
* - New servers answer an anonymous probe with `200 { "visitor": null }`.
|
|
9
|
+
* - Old servers (pre-fix) answer an anonymous probe with `401`.
|
|
10
|
+
*
|
|
11
|
+
* Sites bump `@duffcloudservices/cms` independently of server deploys, so this
|
|
12
|
+
* client must accept BOTH shapes and stay silent on the anonymous path — a
|
|
13
|
+
* `console.error`/`console.warn` on every signed-out page load is exactly the
|
|
14
|
+
* console-noise this fixes. A genuine signed-in visitor is returned as a
|
|
15
|
+
* normalized `SiteVisitor`; anything else resolves to `null`.
|
|
16
|
+
*/
|
|
17
|
+
import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'
|
|
18
|
+
|
|
19
|
+
/** A signed-in site visitor. */
|
|
20
|
+
export interface SiteVisitor {
|
|
21
|
+
id?: string
|
|
22
|
+
email: string
|
|
23
|
+
name: string
|
|
24
|
+
picture?: string
|
|
25
|
+
createdAt?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SiteVisitorSessionResult {
|
|
29
|
+
/** The signed-in visitor, or `null` when signed out. */
|
|
30
|
+
visitor: SiteVisitor | null
|
|
31
|
+
/** Convenience flag — `true` iff a visitor is present. */
|
|
32
|
+
authenticated: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FetchSiteVisitorSessionOptions {
|
|
36
|
+
/** API base (default `/api/v1`, same-origin via Front Door). */
|
|
37
|
+
apiBaseUrl?: string
|
|
38
|
+
/** Optional AbortSignal to cancel an in-flight probe. */
|
|
39
|
+
signal?: AbortSignal
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SIGNED_OUT: SiteVisitorSessionResult = { visitor: null, authenticated: false }
|
|
43
|
+
|
|
44
|
+
const apiBase = (value?: string): string => (value ?? '/api/v1').replace(/\/$/u, '')
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize the various session-response shapes into a `SiteVisitor | null`.
|
|
48
|
+
* Accepts the primary/legacy `{ visitor: {...} | null }` envelope and the
|
|
49
|
+
* contracts-spec `{ authenticated, user: {...} }` shape defensively.
|
|
50
|
+
*/
|
|
51
|
+
function extractVisitor(data: unknown): SiteVisitor | null {
|
|
52
|
+
if (!data || typeof data !== 'object') {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
const obj = data as Record<string, unknown>
|
|
56
|
+
const raw = obj.visitor ?? obj.user
|
|
57
|
+
if (!raw || typeof raw !== 'object') {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
const v = raw as Record<string, unknown>
|
|
61
|
+
const email = typeof v.email === 'string' ? v.email : ''
|
|
62
|
+
if (!email) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
id: typeof v.id === 'string' ? v.id : undefined,
|
|
67
|
+
email,
|
|
68
|
+
name: typeof v.name === 'string' ? v.name : email,
|
|
69
|
+
picture: typeof v.picture === 'string' ? v.picture : undefined,
|
|
70
|
+
createdAt: typeof v.createdAt === 'string' ? v.createdAt : undefined,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch the current site-visitor session. Never throws and never logs for the
|
|
76
|
+
* anonymous case: any network error, non-OK status (incl. a legacy `401`), or
|
|
77
|
+
* `{ visitor: null }` body resolves to a signed-out result.
|
|
78
|
+
*/
|
|
79
|
+
export async function fetchSiteVisitorSession(
|
|
80
|
+
options: FetchSiteVisitorSessionOptions = {},
|
|
81
|
+
): Promise<SiteVisitorSessionResult> {
|
|
82
|
+
const base = apiBase(options.apiBaseUrl)
|
|
83
|
+
|
|
84
|
+
let response: Response
|
|
85
|
+
try {
|
|
86
|
+
response = await fetch(`${base}/site-auth/session`, {
|
|
87
|
+
method: 'GET',
|
|
88
|
+
credentials: 'include',
|
|
89
|
+
headers: { Accept: 'application/json' },
|
|
90
|
+
signal: options.signal,
|
|
91
|
+
})
|
|
92
|
+
} catch {
|
|
93
|
+
// Network error / aborted / CORS — signed-out is the safe probe assumption.
|
|
94
|
+
return SIGNED_OUT
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Old servers reply 401 to an anonymous probe; new servers reply 200-null.
|
|
98
|
+
// Treat any non-OK status as signed-out WITHOUT logging.
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
return SIGNED_OUT
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let data: unknown
|
|
104
|
+
try {
|
|
105
|
+
data = await response.json()
|
|
106
|
+
} catch {
|
|
107
|
+
return SIGNED_OUT
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const visitor = extractVisitor(data)
|
|
111
|
+
return { visitor, authenticated: visitor !== null }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface UseSiteVisitorSessionOptions extends FetchSiteVisitorSessionOptions {
|
|
115
|
+
/** Probe automatically on mount (browser only). Default `true`. */
|
|
116
|
+
fetchOnMount?: boolean
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface UseSiteVisitorSessionReturn {
|
|
120
|
+
/** The signed-in visitor, or `null` when signed out / not yet loaded. */
|
|
121
|
+
visitor: Ref<SiteVisitor | null>
|
|
122
|
+
/** `true` iff a visitor is present. */
|
|
123
|
+
isAuthenticated: ComputedRef<boolean>
|
|
124
|
+
/** `true` while a probe is in flight. */
|
|
125
|
+
isLoading: Ref<boolean>
|
|
126
|
+
/** Re-run the probe. */
|
|
127
|
+
refresh: () => Promise<void>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Vue composable wrapper around {@link fetchSiteVisitorSession}. Reactive
|
|
132
|
+
* `visitor` / `isAuthenticated` / `isLoading`, and probes on mount by default.
|
|
133
|
+
*/
|
|
134
|
+
export function useSiteVisitorSession(
|
|
135
|
+
options: UseSiteVisitorSessionOptions = {},
|
|
136
|
+
): UseSiteVisitorSessionReturn {
|
|
137
|
+
const { fetchOnMount = true, ...fetchOptions } = options
|
|
138
|
+
|
|
139
|
+
const visitor = ref<SiteVisitor | null>(null)
|
|
140
|
+
const isLoading = ref(false)
|
|
141
|
+
const isAuthenticated = computed(() => visitor.value !== null)
|
|
142
|
+
|
|
143
|
+
async function refresh(): Promise<void> {
|
|
144
|
+
isLoading.value = true
|
|
145
|
+
try {
|
|
146
|
+
const result = await fetchSiteVisitorSession(fetchOptions)
|
|
147
|
+
visitor.value = result.visitor
|
|
148
|
+
} finally {
|
|
149
|
+
isLoading.value = false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (fetchOnMount && typeof window !== 'undefined') {
|
|
154
|
+
onMounted(() => {
|
|
155
|
+
void refresh()
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { visitor, isAuthenticated, isLoading, refresh }
|
|
160
|
+
}
|
package/dist/chunk-KCWMS7P4.js
DELETED