Skip to content

Commit 88a369c

Browse files
committed
refactor: save consents in context
1 parent e352e21 commit 88a369c

14 files changed

+123
-123
lines changed

MIGRATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- The `uiVersion` prop for `<UsercentricsScript />` is no longer supported and should be removed. The CMP v3 loader script currently supports only the "latest" version.
88
- This also means that hard-coding a version number and its [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hash is no longer supported. Instead, use a random `nonce` value when implementing a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP).
99
- The `showSecondLayer()` util no longer supports passing a service id argument to directly open to the service info. Instead, use the new `showServiceDetails(serviceId: ServiceId)` util.
10-
- The `getServicesFromLocalStorage(): ServiceInfoFromLocalStorage[]` util has been replaced with `getServicesConsentsFromLocalStorage(): Record<ServiceId, ConsentStatusFromLocalStorage>` with a different format.
10+
- The `getServicesFromLocalStorage()` util has been replaced with `getConsentsFromLocalStorage()` with a different format.
1111
- The following utils and hooks have been removed because CMP v3 no longer supports them:
1212
- `getServicesBaseInfo()`
1313
- `useServiceInfo()`

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,12 +339,12 @@ See also https://usercentrics.com/docs/web/features/api/control-ui/#showserviced
339339
await showServiceDetails('my-service-id')
340340
```
341341

342-
#### `getServicesConsentsFromLocalStorage`
342+
#### `getConsentsFromLocalStorage`
343343

344-
A method to get array of all service consent statuses from local storage.
344+
A method to get consent status saved to localStorage.
345345

346346
```tsx
347-
const consents = getServicesConsentsFromLocalStorage()
347+
const consents = getConsentsFromLocalStorage()
348348
const hasConsent = consents['my-service-id']?.consent === true
349349
```
350350

src/components/UsercentricsProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ export const UsercentricsProvider: FC<UsercentricsProviderProps> = ({ children,
2727
return (
2828
<UsercentricsContext.Provider
2929
value={{
30+
consents: state.consents,
3031
hasInteracted: state.hasInteracted,
3132
isClientSide: state.isClientSide,
3233
isFailed: state.isFailed,
3334
isInitialized: state.isInitialized,
3435
isOpen: state.isOpen,
35-
localStorageState: state.localStorageState,
3636
strictMode,
3737
}}
3838
>

src/context.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import { createContext } from 'react'
22

3-
import type { ConsentStatusFromLocalStorage, ServiceId } from './types.js'
3+
import type { ServiceId } from './types.js'
44

55
export type UsercentricsBrowserIntegrationState = {
6+
consents: Record<ServiceId, boolean>
67
hasInteracted: boolean
78
isClientSide: boolean
89
isFailed: boolean
910
isInitialized: boolean
1011
isOpen: boolean
11-
localStorageState: Record<ServiceId, ConsentStatusFromLocalStorage>
1212
}
1313

1414
export const SSR_INITIAL_STATE: UsercentricsBrowserIntegrationState = {
15+
consents: {},
1516
hasInteracted: false,
1617
isClientSide: false,
1718
isFailed: false,
1819
isInitialized: false,
1920
isOpen: false,
20-
localStorageState: {},
2121
}
2222

2323
type UsercentricsContextType = UsercentricsBrowserIntegrationState & {

src/hooks/use-are-all-consents-accepted.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { UsercentricsContext } from '../context.js'
99
* @warn it's best to assume no consent until this hook returns `true`
1010
*/
1111
export const useAreAllConsentsAccepted = (): boolean | null => {
12-
const { isClientSide, isInitialized, localStorageState } = useContext(UsercentricsContext)
12+
const { consents, isClientSide, isInitialized } = useContext(UsercentricsContext)
1313

1414
/** Consent status is unknown during SSR because CMP is only available client-side */
1515
if (!isClientSide) {
@@ -21,10 +21,8 @@ export const useAreAllConsentsAccepted = (): boolean | null => {
2121
* If it's not loaded, and there's nothing in localStorage, this will return `null`
2222
*/
2323
if (!isInitialized) {
24-
return Object.keys(localStorageState).length > 0
25-
? Object.values(localStorageState).every((service) => service.consent === true)
26-
: null
24+
return Object.keys(consents).length > 0 ? Object.values(consents).every((consent) => consent === true) : null
2725
}
2826

29-
return Object.values(localStorageState).every((service) => service.consent === true)
27+
return Object.values(consents).every((consent) => consent === true)
3028
}

src/hooks/use-has-service-consent.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { ServiceId } from '../types.js'
1010
* @warn it's best to assume no consent until this hook returns `true`
1111
*/
1212
export const useHasServiceConsent = (serviceId: ServiceId): boolean | null => {
13-
const { isClientSide, isInitialized, localStorageState, strictMode } = useContext(UsercentricsContext)
13+
const { consents, isInitialized, isClientSide, strictMode } = useContext(UsercentricsContext)
1414

1515
/** Consent status is unknown during SSR because CMP is only available client-side */
1616
if (!isClientSide) {
@@ -22,12 +22,12 @@ export const useHasServiceConsent = (serviceId: ServiceId): boolean | null => {
2222
* If it's not loaded, and there's nothing in localStorage, this will return `null`
2323
*/
2424
if (!isInitialized) {
25-
return localStorageState[serviceId]?.consent ?? null
25+
return consents[serviceId] ?? null
2626
}
2727

28-
if (strictMode && !localStorageState[serviceId]) {
28+
if (strictMode) {
2929
throw new Error(`Usercentrics Service not found for id "${serviceId}"`)
3030
}
3131

32-
return !!localStorageState[serviceId]?.consent
32+
return !!consents[serviceId]
3333
}

src/hooks/use-usercentrics-event-listener.ts

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import { useEffect, useState } from 'react'
22

33
import { SSR_INITIAL_STATE, type UsercentricsBrowserIntegrationState } from '../context.js'
4-
import { type UCUICMPEvent, UCUICMPEventType, UCUIView, type UCUIVIewChanged } from '../types.js'
5-
import { getServicesConsentsFromLocalStorage, hasUserInteracted, isOpen, setUserHasInteracted } from '../utils.js'
4+
import {
5+
type ServiceId,
6+
type UCConsentEvent,
7+
type UCUICMPEvent,
8+
UCUICMPEventType,
9+
UCUIView,
10+
type UCUIVIewChanged,
11+
} from '../types.js'
12+
import { getConsentsFromLocalStorage, hasUserInteracted, isOpen, setUserHasInteracted } from '../utils.js'
613

714
const UC_UI_CMP_EVENT = 'UC_UI_CMP_EVENT'
815
const UC_UI_VIEW_CHANGED = 'UC_UI_VIEW_CHANGED'
916
const UC_CONSENT = 'UC_CONSENT'
1017

1118
const isUCUICMPEvent = (event: Event): event is UCUICMPEvent => event.type === UC_UI_CMP_EVENT
1219
const isUCUIViewEvent = (event: Event): event is UCUIVIewChanged => event.type === UC_UI_VIEW_CHANGED
20+
const isUCConsentEvent = (event: Event): event is UCConsentEvent => event.type === UC_CONSENT
1321

1422
export const useUsercentricsEventListener = ({
1523
timeout = 5000,
@@ -23,11 +31,16 @@ export const useUsercentricsEventListener = ({
2331
* to react to the UC UI dialog opening/closing.
2432
*/
2533
useEffect(() => {
26-
setState((current) => ({
27-
...current,
28-
isClientSide: true,
29-
localStorageState: getServicesConsentsFromLocalStorage(),
30-
}))
34+
const consentsFromLocalStorage = getConsentsFromLocalStorage()
35+
const consents = Object.entries(consentsFromLocalStorage).reduce(
36+
(acc, [serviceId, status]) => ({
37+
...acc,
38+
[serviceId]: status.consent,
39+
}),
40+
{} as Record<ServiceId, boolean>,
41+
)
42+
43+
setState((current) => ({ ...current, consents, isClientSide: true }))
3144

3245
if ('__ucCmp' in window) {
3346
/** UC_UI already started before this mounted, dialog might be open */
@@ -48,28 +61,21 @@ export const useUsercentricsEventListener = ({
4861

4962
const handleViewChangedEvent = (event: Event) => {
5063
if (isUCUIViewEvent(event)) {
51-
switch (event.detail.view) {
52-
case UCUIView.FIRST_LAYER:
53-
case UCUIView.SECOND_LAYER: {
54-
setState((current) => ({ ...current, isOpen: true }))
55-
break
56-
}
57-
case UCUIView.NONE: {
58-
setState((current) => ({ ...current, isOpen: false }))
59-
break
64+
setState((current) => {
65+
if (event.detail.view === UCUIView.NONE) {
66+
return { ...current, isOpen: false }
67+
} else if (!current.isOpen) {
68+
return { ...current, isOpen: true }
6069
}
61-
}
70+
71+
return current
72+
})
6273
}
6374
}
6475

6576
const handleCMPEvent = (event: Event) => {
6677
if (isUCUICMPEvent(event)) {
6778
switch (event.detail.type) {
68-
case UCUICMPEventType.CMP_SHOWN: {
69-
setState((current) => ({ ...current, isOpen: true }))
70-
break
71-
}
72-
7379
case UCUICMPEventType.ACCEPT_ALL:
7480
case UCUICMPEventType.DENY_ALL:
7581
case UCUICMPEventType.SAVE: {
@@ -81,11 +87,18 @@ export const useUsercentricsEventListener = ({
8187
}
8288
}
8389

84-
const handleConsentEvent = () => {
85-
setState((current) => ({
86-
...current,
87-
localStorageState: getServicesConsentsFromLocalStorage(),
88-
}))
90+
const handleConsentEvent = (event: Event) => {
91+
if (isUCConsentEvent(event)) {
92+
const consents = Object.entries(event.detail.services).reduce(
93+
(acc, [serviceId, data]) => ({
94+
...acc,
95+
[serviceId]: data.consent?.given === true,
96+
}),
97+
{} as Record<ServiceId, boolean>,
98+
)
99+
100+
setState((current) => ({ ...current, consents }))
101+
}
89102
}
90103

91104
window.addEventListener(UC_UI_VIEW_CHANGED, handleViewChangedEvent)
@@ -114,11 +127,11 @@ export const useUsercentricsEventListener = ({
114127

115128
return { ...current, isFailed: true }
116129
})
117-
118-
return () => {
119-
clearTimeout(handleTimeout)
120-
}
121130
}, timeout)
131+
132+
return () => {
133+
clearTimeout(handleTimeout)
134+
}
122135
}, [timeout])
123136

124137
return state

src/types.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,11 @@
66
export type ServiceId = import('@s-group/react-usercentrics/augmented').ServiceId
77

88
/** Partial type for service info read from local storage, if available. Unused values are left out. */
9-
export type ConsentStatusFromLocalStorage = {
9+
export type ServiceData = {
1010
name: string
11-
consent: boolean
12-
}
13-
14-
/** Partial type for uc data read from local storage, if available. Unused values are left out. */
15-
export type UCDataFromLocalStorage = {
16-
consent: {
17-
services: Record<ServiceId, ConsentStatusFromLocalStorage>
11+
consent?: {
12+
given: boolean
13+
type: 'IMPLICIT' | 'EXPLICIT'
1814
}
1915
}
2016

@@ -25,15 +21,21 @@ export type UCDataFromLocalStorage = {
2521
type ConsentData = {
2622
status: 'ALL_ACCEPTED' | 'ALL_DENIED' | 'SOME_ACCEPTED' | 'SOME_DENIED'
2723
required: boolean
28-
type: 'IMPLICIT' | 'EXPLICIT'
2924
}
3025

3126
/**
3227
* Partial type, unused values are left out.
3328
* @see https://usercentrics.com/docs/web/features/api/interfaces/#consentdetails
3429
*/
35-
type ConsentDetails = {
30+
export type ConsentDetails = {
3631
consent: ConsentData
32+
services: Record<ServiceId, ServiceData>
33+
}
34+
35+
export type UCDataFromLocalStorage = {
36+
consent: {
37+
services: Record<ServiceId, { name: string; consent: boolean }>
38+
}
3739
}
3840

3941
/**
@@ -96,6 +98,8 @@ type UC_CMP = {
9698
*/
9799
export type UCWindow = Window & typeof globalThis & { __ucCmp?: UC_CMP }
98100

101+
export type UCConsentEvent = CustomEvent<ConsentDetails>
102+
99103
export enum UCUICMPEventType {
100104
ACCEPT_ALL = 'ACCEPT_ALL',
101105
CMP_SHOWN = 'CMP_SHOWN',

src/utils.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ConsentStatusFromLocalStorage, ServiceId, UCDataFromLocalStorage, UCWindow } from './types.js'
1+
import type { ServiceId, UCDataFromLocalStorage, UCWindow } from './types.js'
22

33
/**
44
* A method to check if user has interacted with the consent prompt and given consent information.
@@ -72,20 +72,17 @@ export const showServiceDetails = async (serviceId: ServiceId): Promise<void> =>
7272
}
7373

7474
/**
75-
* A method to get array of all service consent statuses from local storage
75+
* A method to get consent status saved to localStorage
7676
*
7777
* @example
78-
* const consents = getServicesConsentsFromLocalStorage()
78+
* const consents = getConsentsFromLocalStorage()
7979
* const hasConsent = consents['my-service-id']?.consent === true
8080
*/
81-
export const getServicesConsentsFromLocalStorage = (): Record<ServiceId, ConsentStatusFromLocalStorage> => {
81+
export const getConsentsFromLocalStorage = (): UCDataFromLocalStorage['consent']['services'] => {
8282
try {
83-
const ucData = localStorage?.getItem('ucData')
84-
if (ucData) {
85-
const ucDataParsed = JSON.parse(ucData) as UCDataFromLocalStorage
86-
/** Leave out any other untyped fields */
87-
return ucDataParsed.consent.services
88-
}
83+
const data = localStorage?.getItem('ucData')
84+
const consentDetails = data ? (JSON.parse(data) as UCDataFromLocalStorage) : { consent: { services: {} } }
85+
return consentDetails.consent.services
8986
} catch {
9087
/** Ignore failures */
9188
}

tests/hooks/use-are-all-consents-accepted.test.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ describe('Usercentrics', () => {
99
describe('hooks', () => {
1010
describe('useAreAllConsentsAccepted', () => {
1111
const CONTEXT: ContextType<typeof UsercentricsContext> = {
12+
consents: {},
1213
hasInteracted: false,
1314
isClientSide: true,
1415
isFailed: false,
1516
isInitialized: true,
1617
isOpen: false,
17-
localStorageState: {},
1818
strictMode: false,
1919
}
2020

@@ -50,9 +50,9 @@ describe('Usercentrics', () => {
5050
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
5151
wrapper: getWrapper({
5252
isInitialized: false,
53-
localStorageState: {
54-
'test-id': { consent: true, name: '' },
55-
'test-id2': { consent: false, name: '' },
53+
consents: {
54+
'test-id': true,
55+
'test-id2': false,
5656
},
5757
}),
5858
})
@@ -64,9 +64,9 @@ describe('Usercentrics', () => {
6464
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
6565
wrapper: getWrapper({
6666
isInitialized: false,
67-
localStorageState: {
68-
'test-id': { consent: true, name: '' },
69-
'test-id2': { consent: true, name: '' },
67+
consents: {
68+
'test-id': true,
69+
'test-id2': true,
7070
},
7171
}),
7272
})
@@ -78,9 +78,9 @@ describe('Usercentrics', () => {
7878
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
7979
wrapper: getWrapper({
8080
isInitialized: true,
81-
localStorageState: {
82-
'test-id': { consent: true, name: '' },
83-
'test-id2': { consent: false, name: '' },
81+
consents: {
82+
'test-id': true,
83+
'test-id2': false,
8484
},
8585
}),
8686
})
@@ -92,9 +92,9 @@ describe('Usercentrics', () => {
9292
const { result } = renderHook(() => useAreAllConsentsAccepted(), {
9393
wrapper: getWrapper({
9494
isInitialized: false,
95-
localStorageState: {
96-
'test-id': { consent: true, name: '' },
97-
'test-id2': { consent: true, name: '' },
95+
consents: {
96+
'test-id': true,
97+
'test-id2': true,
9898
},
9999
}),
100100
})

0 commit comments

Comments
 (0)