-
Notifications
You must be signed in to change notification settings - Fork 83
feat: consent trigger revocation support #631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { isRef, ref, toValue, watch } from 'vue' | ||
| import { isRef, type Ref, ref, toValue, watch } from 'vue' | ||
| import { tryUseNuxtApp, onNuxtReady, requestIdleCallback } from 'nuxt/app' | ||
| import type { ConsentScriptTriggerOptions } from '../types' | ||
|
|
||
|
|
@@ -7,28 +7,55 @@ interface UseConsentScriptTriggerApi extends Promise<void> { | |
| * A function that can be called to accept the consent and load the script. | ||
| */ | ||
| accept: () => void | ||
| /** | ||
| * A function that can be called to revoke consent. Since the trigger promise is already resolved | ||
| * once accepted, revocation is signaled via the reactive `consented` ref rather than promise rejection. | ||
| * Consumers should `watch(consent.consented, ...)` to react to revocation. | ||
| */ | ||
| revoke: () => void | ||
| /** | ||
| * Reactive reference to the consent state | ||
| */ | ||
| consented: Ref<boolean> | ||
| } | ||
|
|
||
| /** | ||
| * Load a script once consent has been provided either through a resolvable `consent` or calling the `accept` method. | ||
| * Supports revoking consent via the reactive `consented` ref. Consumers should watch `consented` to react to revocation. | ||
| * @param options | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| */ | ||
| export function useScriptTriggerConsent(options?: ConsentScriptTriggerOptions): UseConsentScriptTriggerApi { | ||
| if (import.meta.server) | ||
| return new Promise(() => {}) as UseConsentScriptTriggerApi | ||
|
|
||
| const consented = ref<boolean>(false) | ||
| // user may want ot still load the script on idle | ||
| const nuxtApp = tryUseNuxtApp() | ||
|
|
||
| // Setup initial consent value | ||
| if (options?.consent) { | ||
| if (isRef(options?.consent)) { | ||
| watch(options.consent, (_val) => { | ||
| const val = toValue(_val) | ||
| consented.value = Boolean(val) | ||
| }, { immediate: true }) | ||
| } | ||
| // check for boolean primitive | ||
| else if (typeof options?.consent === 'boolean') { | ||
| consented.value = options?.consent | ||
| } | ||
| // consent is a promise | ||
| else if (options?.consent instanceof Promise) { | ||
| options?.consent.then((res) => { | ||
| consented.value = typeof res === 'boolean' ? res : true | ||
| }) | ||
| } | ||
| } | ||
|
Comment on lines
+34
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: cat -n src/runtime/composables/useScriptTriggerConsent.ts | head -80Repository: nuxt/scripts Length of output: 3546 π Web query:
π‘ Result: In Vue 3 this is expected:
Fix options
watch(source, (nv, ov) => {
// runs once immediately, then on future changes
}, { immediate: true })[1]
const ready = ref(false)
watch(source, (nv) => {
if (!ready.value) return
// ...
})
onMounted(() => { ready.value = true })(avoids missing changes while still preventing early side effects)
Sources: Citations: Consent is lost if granted before the watcher registers. Lines 38, 42, and 47 can set This occurs when:
π€ Prompt for AI Agents |
||
|
|
||
| const promise = new Promise<void>((resolve) => { | ||
| watch(consented, (ready) => { | ||
| if (ready) { | ||
| watch(consented, (newValue, oldValue) => { | ||
| if (newValue && !oldValue) { | ||
| // Consent granted - load script | ||
| const runner = nuxtApp?.runWithContext || ((cb: () => void) => cb()) | ||
| // TODO drop support in v1 | ||
| if (options?.postConsentTrigger instanceof Promise) { | ||
| options.postConsentTrigger.then(() => runner(resolve)) | ||
| return | ||
| } | ||
| if (typeof options?.postConsentTrigger === 'function') { | ||
| // check if function has an argument | ||
| if (options?.postConsentTrigger.length === 1) { | ||
|
|
@@ -50,29 +77,21 @@ export function useScriptTriggerConsent(options?: ConsentScriptTriggerOptions): | |
| // other trigger not supported | ||
| runner(resolve) | ||
| } | ||
| // Revocation is handled via the reactive `consented` ref, not promise rejection. | ||
| // Once resolved, a promise cannot be rejected β consumers should watch `consented` instead. | ||
| }) | ||
| if (options?.consent) { | ||
| if (isRef(options?.consent)) { | ||
| watch(options.consent, (_val) => { | ||
| const val = toValue(_val) | ||
| consented.value = Boolean(val) | ||
| }, { immediate: true }) | ||
| } | ||
| // check for boolean primitive | ||
| else if (typeof options?.consent === 'boolean') { | ||
| consented.value = options?.consent | ||
| } | ||
| // consent is a promise | ||
| else if (options?.consent instanceof Promise) { | ||
| options?.consent.then((res) => { | ||
| consented.value = typeof res === 'boolean' ? res : true | ||
| }) | ||
| } | ||
| } | ||
| }) as UseConsentScriptTriggerApi | ||
|
|
||
| // we augment the promise with a consent API | ||
| promise.accept = () => { | ||
| consented.value = true | ||
| } | ||
|
|
||
| promise.revoke = () => { | ||
| consented.value = false | ||
| } | ||
|
|
||
| promise.consented = consented | ||
|
|
||
| return promise as UseConsentScriptTriggerApi | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the SSR return value API-compatible.
The new
revoke/consentedmembers are only attached on the client path. Theimport.meta.serverbranch at Line 28 still returns a barePromise, so SSR callers getundefinedfor both and can break as soon as they readconsented.valueor bindrevoke.Suggested fix
export function useScriptTriggerConsent(options?: ConsentScriptTriggerOptions): UseConsentScriptTriggerApi { - if (import.meta.server) - return new Promise(() => {}) as UseConsentScriptTriggerApi + if (import.meta.server) { + const serverPromise = new Promise<void>(() => {}) as UseConsentScriptTriggerApi + serverPromise.accept = () => {} + serverPromise.revoke = () => {} + serverPromise.consented = ref(false) + return serverPromise + }π€ Prompt for AI Agents