Skip to content

Commit 544cd2a

Browse files
committed
fix(useScriptTriggerConsent): prefer lazy postConsentTrigger promises
Fixes #391
1 parent 2f6f5fe commit 544cd2a

File tree

5 files changed

+131
-11
lines changed

5 files changed

+131
-11
lines changed

docs/content/docs/1.guides/3.consent.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ useScript('https://www.google-analytics.com/analytics.js', {
7474
trigger: useScriptTriggerConsent({
7575
consent: agreedToCookies,
7676
// load 3 seconds after consent is granted
77-
postConsentTrigger: new Promise<void>(resolve => setTimeout(resolve, 3000))
77+
postConsentTrigger: () => new Promise<void>(resolve =>
78+
setTimeout(resolve, 3000),
79+
),
7880
})
7981
})
8082
```

playground/pages/issue-391.vue

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script lang="ts" setup>
2+
import { ref } from 'vue'
3+
import { useScriptGoogleAnalytics, useScriptTriggerConsent } from '#imports'
4+
5+
const agreedCookies = ref(false)
6+
const triggerCalled = ref(false)
7+
8+
useScriptGoogleAnalytics({
9+
id: 'G-H9LK49C4ZH',
10+
scriptOptions: {
11+
trigger: useScriptTriggerConsent({
12+
consent: agreedCookies,
13+
// load 3 seconds after consent is granted
14+
postConsentTrigger: () => new Promise<void>(resolve =>
15+
setTimeout(resolve, 3000),
16+
),
17+
}),
18+
},
19+
})
20+
</script>
21+
22+
<template>
23+
<div>Post Consent Trigger is called: {{ triggerCalled }}</div>
24+
</template>

src/runtime/composables/useScriptTriggerConsent.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,24 @@ export function useScriptTriggerConsent(options?: ConsentScriptTriggerOptions):
2424
watch(consented, (ready) => {
2525
if (ready) {
2626
const runner = nuxtApp?.runWithContext || ((cb: () => void) => cb())
27+
// TODO drop support in v1
2728
if (options?.postConsentTrigger instanceof Promise) {
2829
options.postConsentTrigger.then(() => runner(resolve))
2930
return
3031
}
32+
if (typeof options?.postConsentTrigger === 'function') {
33+
// check if function has an argument
34+
if (options?.postConsentTrigger.length === 1) {
35+
options.postConsentTrigger(resolve)
36+
return
37+
}
38+
// else it's returning a promise to await
39+
const val = options.postConsentTrigger()
40+
if (val instanceof Promise) {
41+
return val.then(() => runner(resolve))
42+
}
43+
return
44+
}
3145
if (options?.postConsentTrigger === 'onNuxtReady') {
3246
const idleTimeout = options?.postConsentTrigger ? (nuxtApp ? onNuxtReady : requestIdleCallback) : (cb: () => void) => cb()
3347
runner(() => idleTimeout(resolve))
@@ -38,23 +52,22 @@ export function useScriptTriggerConsent(options?: ConsentScriptTriggerOptions):
3852
}
3953
})
4054
if (options?.consent) {
55+
if (isRef(options?.consent)) {
56+
watch(options.consent, (_val) => {
57+
const val = toValue(_val)
58+
consented.value = Boolean(val)
59+
}, { immediate: true })
60+
}
4161
// check for boolean primitive
42-
if (typeof options?.consent === 'boolean') {
43-
consented.value = true
62+
else if (typeof options?.consent === 'boolean') {
63+
consented.value = options?.consent
4464
}
4565
// consent is a promise
4666
else if (options?.consent instanceof Promise) {
4767
options?.consent.then((res) => {
4868
consented.value = typeof res === 'boolean' ? res : true
4969
})
5070
}
51-
else if (isRef(options?.consent)) {
52-
watch(options.consent, (_val) => {
53-
const val = toValue(_val)
54-
if (typeof val === 'boolean')
55-
consented.value = val
56-
}, { immediate: true })
57-
}
5871
}
5972
}) as UseConsentScriptTriggerApi
6073
// we augment the promise with a consent API

src/runtime/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export interface TrackedPage {
108108
path: string
109109
}
110110

111+
type ExcludePromises<T> = T extends Promise<any> ? never : T
112+
111113
export interface ConsentScriptTriggerOptions {
112114
/**
113115
* An optional reactive (or promise) reference to the consent state. You can use this to accept the consent for scripts
@@ -118,7 +120,7 @@ export interface ConsentScriptTriggerOptions {
118120
* Should the script be loaded on the `requestIdleCallback` callback. This is useful for non-essential scripts that
119121
* have already been consented to be loaded.
120122
*/
121-
postConsentTrigger?: NuxtUseScriptOptions['trigger']
123+
postConsentTrigger?: ExcludePromises<NuxtUseScriptOptions['trigger']> | (() => Promise<any>)
122124
}
123125

124126
export interface NuxtDevToolsScriptInstance {

test/unit/consent.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// @vitest-environment happy-dom
2+
import { ref } from 'vue'
3+
import { useScriptTriggerConsent } from '../../src/runtime/composables/useScriptTriggerConsent'
4+
5+
function getPromiseState(promise: Promise<any>) {
6+
const temp = {}
7+
return Promise.race([promise, temp])
8+
.then(value => value === temp ? 'pending' : 'fulfilled')
9+
.catch(() => 'rejected')
10+
}
11+
12+
describe('consent', () => {
13+
it('promise post consent trigger', async () => {
14+
const consent = ref(false)
15+
const triggerCalled = ref(false)
16+
const p = useScriptTriggerConsent({
17+
consent,
18+
postConsentTrigger: () => new Promise<void>(resolve =>
19+
setTimeout(resolve, 30),
20+
).then(() => (triggerCalled.value = true)),
21+
})
22+
23+
// Check initial state
24+
expect(await getPromiseState(p)).toBe('pending')
25+
26+
// wait 50ms
27+
await new Promise(resolve => setTimeout(resolve, 50))
28+
expect(await getPromiseState(p)).toBe('pending')
29+
expect(triggerCalled.value).toBe(false)
30+
31+
consent.value = true
32+
33+
// await next tick
34+
await new Promise(resolve => setTimeout(resolve, 0))
35+
36+
// only loads 30 ms after
37+
expect(await getPromiseState(p)).toBe('pending')
38+
39+
// await 50ms
40+
await new Promise(resolve => setTimeout(resolve, 50))
41+
42+
// should be fulfilled
43+
expect(await getPromiseState(p)).toBe('fulfilled')
44+
45+
expect(triggerCalled.value).toBe(true)
46+
}, 60000)
47+
it('function post consent trigger', async () => {
48+
const consent = ref(false)
49+
const triggerCalled = ref(false)
50+
const p = useScriptTriggerConsent({
51+
consent,
52+
postConsentTrigger: (fn) => {
53+
// load in 30ms
54+
setTimeout(() => {
55+
fn()
56+
triggerCalled.value = true
57+
}, 30)
58+
},
59+
})
60+
61+
// Check initial state
62+
expect(await getPromiseState(p)).toBe('pending')
63+
64+
// wait 50ms
65+
await new Promise(resolve => setTimeout(resolve, 50))
66+
expect(await getPromiseState(p)).toBe('pending')
67+
expect(triggerCalled.value).toBe(false)
68+
69+
consent.value = true
70+
71+
// await next tick
72+
await new Promise(resolve => setTimeout(resolve, 35))
73+
74+
// should be fulfilled
75+
expect(await getPromiseState(p)).toBe('fulfilled')
76+
77+
expect(triggerCalled.value).toBe(true)
78+
})
79+
})

0 commit comments

Comments
 (0)