Skip to content

Commit c50588b

Browse files
author
Mingyu Cui
authored
feat: add useAreAllConsentsAccepted() hook (#22)
1 parent e20843b commit c50588b

5 files changed

Lines changed: 171 additions & 0 deletions

File tree

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,25 @@ Returns `true` or `false` based on consent status, or `null` when unknown (not y
194194
}
195195
```
196196

197+
#### `useAreAllConsentsAccepted`
198+
199+
Whether all Usercentrics services have been given consent.
200+
Returns `true` or `false` based on consent status, or `null` when unknown (not yet loaded).
201+
202+
**Warning:** it's best to assume no consent until this hook returns `true`
203+
204+
```tsx
205+
;() => {
206+
const hasAllConsents = useAreAllConsentsAccepted()
207+
208+
useEffect(() => {
209+
if (hasAllConsents) {
210+
loadMyService()
211+
}
212+
}, [hasAllConsents])
213+
}
214+
```
215+
197216
#### `useIsFailed`
198217

199218
Returns `true` if Usercentrics failed to load inside the
@@ -386,6 +405,18 @@ if (hasConsent) {
386405
}
387406
```
388407

408+
#### `areAllConsentsAccepted`
409+
410+
Returns true if all Usercentrics services have been given consent
411+
412+
```tsx
413+
const hasAllConsents = areAllConsentsAccepted()
414+
415+
if (hasAllConsents) {
416+
loadMyService()
417+
}
418+
```
419+
389420
#### `acceptService`
390421

391422
A method for accepting a single service.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useContext } from 'react'
2+
3+
import { UsercentricsContext } from '../context.js'
4+
import { areAllConsentsAccepted } from '../utils.js'
5+
6+
/**
7+
* Whether all Usercentrics services have been given consent.
8+
* Returns `true` or `false` based on consent status, or `null` when unknown (not yet loaded).
9+
*
10+
* @warn it's best to assume no consent until this hook returns `true`
11+
*/
12+
export const useAreAllConsentsAccepted = (): boolean | null => {
13+
const { isClientSide, isInitialized, localStorageState } = useContext(UsercentricsContext)
14+
15+
/** Consent status is unknown during SSR because CMP is only available client-side */
16+
if (!isClientSide) {
17+
return null
18+
}
19+
20+
/**
21+
* Until Usercentrics CMP has loaded, try to get consent status from localStorage.
22+
* If it's not loaded, and there's nothing in localStorage, this will return `null`
23+
*/
24+
if (!isInitialized) {
25+
return localStorageState.length > 0 ? localStorageState.every((service) => service.status === true) : null
26+
}
27+
28+
return areAllConsentsAccepted()
29+
}

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ type UC_UI = {
8383
* @see https://docs.usercentrics.com/#/cmp-v2-ui-api?id=acceptservice
8484
*/
8585
acceptService?: (serviceId: ServiceId, consentType?: ConsentType) => Promise<void>
86+
87+
/**
88+
* A method to check if all consents were accepted
89+
* @see https://docs.usercentrics.com/#/cmp-v2-ui-api?id=areallconsentsaccepted
90+
*/
91+
areAllConsentsAccepted?: () => boolean
8692
}
8793

8894
/**

src/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,9 @@ export const acceptService = async (serviceId: ServiceId, consentType?: ConsentT
129129
await (window as UCWindow).UC_UI?.acceptService?.(serviceId, consentType)
130130
}
131131
}
132+
133+
/**
134+
* A method to check if all consents were accepted
135+
* @see https://docs.usercentrics.com/#/cmp-v2-ui-api?id=areallconsentsaccepted
136+
*/
137+
export const areAllConsentsAccepted = () => !!(IS_BROWSER && (window as UCWindow).UC_UI?.areAllConsentsAccepted?.())
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { renderHook } from '@testing-library/react'
2+
import type { ContextType, FC, ReactNode } from 'react'
3+
import React from 'react'
4+
5+
import { UsercentricsContext } from '../../src/context.js'
6+
import { useAreAllConsentsAccepted } from '../../src/hooks/use-are-all-consents-accepted.js'
7+
import * as utils from '../../src/utils.js'
8+
9+
const mockAreAllConsentsAccepted = jest.spyOn(utils, 'areAllConsentsAccepted')
10+
11+
describe('Usercentrics', () => {
12+
describe('hooks', () => {
13+
describe('useAreAllConsentsAccepted', () => {
14+
const CONTEXT: ContextType<typeof UsercentricsContext> = {
15+
hasInteracted: false,
16+
isClientSide: true,
17+
isFailed: false,
18+
isInitialized: true,
19+
isOpen: false,
20+
localStorageState: [],
21+
ping: Symbol(),
22+
strictMode: false,
23+
}
24+
25+
const getWrapper =
26+
(context?: Partial<ContextType<typeof UsercentricsContext>>): FC<{ children: ReactNode }> =>
27+
// eslint-disable-next-line react/display-name
28+
({ children }) => (
29+
<UsercentricsContext.Provider value={{ ...CONTEXT, ...context }}>
30+
{children}
31+
</UsercentricsContext.Provider>
32+
)
33+
34+
it('should return null during SSR', () => {
35+
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
36+
wrapper: getWrapper({ isClientSide: false }),
37+
})
38+
39+
expect(result.current).toEqual(null)
40+
})
41+
42+
it('should return null when not initialized and localStorage is empty', () => {
43+
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
44+
wrapper: getWrapper({ isInitialized: false }),
45+
})
46+
47+
expect(result.current).toEqual(null)
48+
})
49+
50+
it('should return false when not initialized and some services in localStorage do not have consent', () => {
51+
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
52+
wrapper: getWrapper({
53+
isInitialized: false,
54+
localStorageState: [
55+
{ id: 'test-id', status: true },
56+
{ id: 'test-id2', status: false },
57+
],
58+
}),
59+
})
60+
61+
expect(result.current).toEqual(false)
62+
})
63+
64+
it('should return true when not initialized and all services in localStorage have consent', () => {
65+
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
66+
wrapper: getWrapper({
67+
isInitialized: false,
68+
localStorageState: [
69+
{ id: 'test-id', status: true },
70+
{ id: 'test-id2', status: true },
71+
],
72+
}),
73+
})
74+
75+
expect(result.current).toEqual(true)
76+
})
77+
78+
it('should return false when not all consents are given', () => {
79+
mockAreAllConsentsAccepted.mockReturnValue(false)
80+
81+
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
82+
wrapper: getWrapper(),
83+
})
84+
85+
expect(result.current).toEqual(false)
86+
})
87+
88+
it('should return true when all consents are given', () => {
89+
mockAreAllConsentsAccepted.mockReturnValue(true)
90+
91+
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
92+
wrapper: getWrapper(),
93+
})
94+
95+
expect(result.current).toEqual(true)
96+
})
97+
})
98+
})
99+
})

0 commit comments

Comments
 (0)