@osano/osano-cmp-react-native 0.18.4 → 0.18.6
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/lib/commonjs/ConsentManager.js +70 -3
- package/lib/commonjs/ConsentManager.js.map +1 -1
- package/lib/commonjs/constants.js +2 -3
- package/lib/commonjs/constants.js.map +1 -1
- package/lib/commonjs/context.js +1 -0
- package/lib/commonjs/context.js.map +1 -1
- package/lib/commonjs/doNotSell.js +95 -0
- package/lib/commonjs/doNotSell.js.map +1 -0
- package/lib/commonjs/index.js +13 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/reducer.js +41 -41
- package/lib/commonjs/reducer.js.map +1 -1
- package/lib/commonjs/selectors.js +20 -2
- package/lib/commonjs/selectors.js.map +1 -1
- package/lib/commonjs/services/consent.js +163 -34
- package/lib/commonjs/services/consent.js.map +1 -1
- package/lib/commonjs/services/gdprMappings.js +115 -0
- package/lib/commonjs/services/gdprMappings.js.map +1 -0
- package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js +1 -1
- package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
- package/lib/module/ConsentManager.js +70 -3
- package/lib/module/ConsentManager.js.map +1 -1
- package/lib/module/constants.js +2 -3
- package/lib/module/constants.js.map +1 -1
- package/lib/module/context.js +1 -0
- package/lib/module/context.js.map +1 -1
- package/lib/module/doNotSell.js +87 -0
- package/lib/module/doNotSell.js.map +1 -0
- package/lib/module/index.js +13 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/reducer.js +41 -41
- package/lib/module/reducer.js.map +1 -1
- package/lib/module/selectors.js +16 -1
- package/lib/module/selectors.js.map +1 -1
- package/lib/module/services/consent.js +161 -33
- package/lib/module/services/consent.js.map +1 -1
- package/lib/module/services/gdprMappings.js +109 -0
- package/lib/module/services/gdprMappings.js.map +1 -0
- package/lib/module/views/Drawer/Drawer.ConsentCategories.js +2 -2
- package/lib/module/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
- package/lib/typescript/src/ConsentManager.d.ts +29 -1
- package/lib/typescript/src/ConsentManager.d.ts.map +1 -1
- package/lib/typescript/src/constants.d.ts.map +1 -1
- package/lib/typescript/src/context.d.ts +1 -0
- package/lib/typescript/src/context.d.ts.map +1 -1
- package/lib/typescript/src/doNotSell.d.ts +48 -0
- package/lib/typescript/src/doNotSell.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/reducer.d.ts.map +1 -1
- package/lib/typescript/src/selectors.d.ts +3 -0
- package/lib/typescript/src/selectors.d.ts.map +1 -1
- package/lib/typescript/src/services/consent.d.ts +8 -3
- package/lib/typescript/src/services/consent.d.ts.map +1 -1
- package/lib/typescript/src/services/gdprMappings.d.ts +32 -0
- package/lib/typescript/src/services/gdprMappings.d.ts.map +1 -0
- package/lib/typescript/src/views/Drawer/Drawer.ConsentCategories.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ConsentManager.ts +88 -7
- package/src/constants.ts +2 -3
- package/src/context.ts +2 -0
- package/src/doNotSell.ts +122 -0
- package/src/index.tsx +20 -1
- package/src/reducer.ts +53 -56
- package/src/selectors.ts +25 -2
- package/src/services/consent.ts +217 -44
- package/src/services/gdprMappings.ts +126 -0
- package/src/types/consent-config.d.ts +6 -0
- package/src/types/index.d.ts +9 -0
- package/src/views/Drawer/Drawer.ConsentCategories.tsx +2 -1
package/src/doNotSell.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConsentCategory,
|
|
3
|
+
type ConsentCategoryType,
|
|
4
|
+
OsanoCategories,
|
|
5
|
+
} from './constants';
|
|
6
|
+
|
|
7
|
+
// "Do Not Sell / Share" opt-out logic as pure functions, shared by the service
|
|
8
|
+
// layer, reducer, and selectors.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Visible only when `enableDoNotSell` is set and CCPA applies: `ccpaApplies`
|
|
12
|
+
* when the config provides it, else the US geo fallback (explicit `false` hides
|
|
13
|
+
* it).
|
|
14
|
+
*/
|
|
15
|
+
export const isDoNotSellVisible = (opts: {
|
|
16
|
+
enableDoNotSell?: boolean;
|
|
17
|
+
ccpaApplies?: boolean | null;
|
|
18
|
+
browsingFromUS?: boolean;
|
|
19
|
+
}): boolean =>
|
|
20
|
+
!!opts.enableDoNotSell && (opts.ccpaApplies ?? !!opts.browsingFromUS);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The governed categories: config's `doNotSellCategories`, upper-cased and
|
|
24
|
+
* mapped, falling back to Marketing. ESSENTIAL and OPT_OUT are never governed —
|
|
25
|
+
* governing OPT_OUT would make it toggle itself off. Tolerant of malformed
|
|
26
|
+
* input (a non-array, or null/non-string elements from remote/cached config) so
|
|
27
|
+
* a bad field can't crash the consent UI.
|
|
28
|
+
*/
|
|
29
|
+
export const resolveDoNotSellCategories = (
|
|
30
|
+
categories?: string[],
|
|
31
|
+
): ConsentCategoryType[] => {
|
|
32
|
+
const list = Array.isArray(categories) ? categories : [];
|
|
33
|
+
const mapped = list
|
|
34
|
+
.filter((category): category is string => typeof category === 'string')
|
|
35
|
+
.map((category) => category.toUpperCase())
|
|
36
|
+
.filter(
|
|
37
|
+
(category): category is ConsentCategoryType =>
|
|
38
|
+
(OsanoCategories as string[]).includes(category) &&
|
|
39
|
+
category !== ConsentCategory.ESSENTIAL &&
|
|
40
|
+
category !== ConsentCategory.OPT_OUT,
|
|
41
|
+
);
|
|
42
|
+
return mapped.length ? mapped : [ConsentCategory.MARKETING];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Opt-out precedence: when opted out, forces the governed categories off.
|
|
47
|
+
* Config-aware, so custom `doNotSellCategories` are honored. No-op when
|
|
48
|
+
* Do-Not-Sell is off or not opted out.
|
|
49
|
+
*/
|
|
50
|
+
export const normalizeDoNotSellConsent = <
|
|
51
|
+
T extends Record<ConsentCategoryType, boolean | undefined>,
|
|
52
|
+
>(
|
|
53
|
+
consent: T,
|
|
54
|
+
opts: { enableDoNotSell?: boolean; doNotSellCategories?: string[] },
|
|
55
|
+
): T => {
|
|
56
|
+
if (!opts.enableDoNotSell || !consent[ConsentCategory.OPT_OUT]) {
|
|
57
|
+
return consent;
|
|
58
|
+
}
|
|
59
|
+
const governed = resolveDoNotSellCategories(opts.doNotSellCategories);
|
|
60
|
+
const result: T = { ...consent };
|
|
61
|
+
for (const category of governed) {
|
|
62
|
+
result[category] = false as T[ConsentCategoryType];
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Seeds the Do-Not-Sell default into a starting selection (no-op unless
|
|
69
|
+
* enabled): default-on opts out and drops the governed categories; default-off
|
|
70
|
+
* consents them with no opt-out signal.
|
|
71
|
+
*/
|
|
72
|
+
export const applyDoNotSellDefault = (
|
|
73
|
+
categories: ConsentCategoryType[],
|
|
74
|
+
opts: {
|
|
75
|
+
enableDoNotSell?: boolean;
|
|
76
|
+
enableDoNotSellDefault?: boolean;
|
|
77
|
+
doNotSellCategories?: string[];
|
|
78
|
+
},
|
|
79
|
+
): ConsentCategoryType[] => {
|
|
80
|
+
if (!opts.enableDoNotSell) {
|
|
81
|
+
return categories;
|
|
82
|
+
}
|
|
83
|
+
const governed = resolveDoNotSellCategories(opts.doNotSellCategories);
|
|
84
|
+
const result: ConsentCategoryType[] = categories.filter(
|
|
85
|
+
(category) => category !== ConsentCategory.OPT_OUT,
|
|
86
|
+
);
|
|
87
|
+
if (opts.enableDoNotSellDefault) {
|
|
88
|
+
return [
|
|
89
|
+
...result.filter((category) => !governed.includes(category)),
|
|
90
|
+
ConsentCategory.OPT_OUT,
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
for (const category of governed) {
|
|
94
|
+
if (!result.includes(category)) {
|
|
95
|
+
result.push(category);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Categories to force off to keep the opt-out and its governed categories
|
|
103
|
+
* mutually exclusive: opt-out on clears the governed ones; a governed one on
|
|
104
|
+
* clears the opt-out. Empty when disabled or toggling off.
|
|
105
|
+
*/
|
|
106
|
+
export const doNotSellSideEffects = (
|
|
107
|
+
category: ConsentCategoryType,
|
|
108
|
+
isConsented: boolean,
|
|
109
|
+
opts: { enableDoNotSell?: boolean; doNotSellCategories?: string[] },
|
|
110
|
+
): ConsentCategoryType[] => {
|
|
111
|
+
if (!opts.enableDoNotSell || !isConsented) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const governed = resolveDoNotSellCategories(opts.doNotSellCategories);
|
|
115
|
+
if (category === ConsentCategory.OPT_OUT) {
|
|
116
|
+
return governed;
|
|
117
|
+
}
|
|
118
|
+
if (governed.includes(category)) {
|
|
119
|
+
return [ConsentCategory.OPT_OUT];
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
};
|
package/src/index.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
makeConsentFromCategories,
|
|
20
20
|
type ConsentCategoryType,
|
|
21
21
|
} from './constants';
|
|
22
|
+
import { normalizeDoNotSellConsent } from './doNotSell';
|
|
22
23
|
import { OsanoPublicContext } from './context';
|
|
23
24
|
import { usePrevious } from './hooks/usePrevious';
|
|
24
25
|
import useThunkReducer, { type Store } from './hooks/useThunkReducer';
|
|
@@ -97,7 +98,11 @@ export const Osano: React.FunctionComponent<
|
|
|
97
98
|
*/
|
|
98
99
|
const categories = Array.isArray(category) ? category : [category];
|
|
99
100
|
consentedCategories = formatConsentArray(
|
|
100
|
-
makeConsentFromCategories(categories),
|
|
101
|
+
normalizeDoNotSellConsent(makeConsentFromCategories(categories), {
|
|
102
|
+
enableDoNotSell: consentManager.current.config.enableDoNotSell,
|
|
103
|
+
doNotSellCategories:
|
|
104
|
+
consentManager.current.config.doNotSellCategories,
|
|
105
|
+
}),
|
|
101
106
|
);
|
|
102
107
|
}
|
|
103
108
|
return recordConsent(consentManager.current, consentedCategories);
|
|
@@ -161,6 +166,15 @@ export const Osano: React.FunctionComponent<
|
|
|
161
166
|
return consentManager.current.jurisdiction;
|
|
162
167
|
}, []);
|
|
163
168
|
|
|
169
|
+
const handleSetConsentExpirationTestingMode = useCallback(
|
|
170
|
+
(expirationSeconds: number | null) => {
|
|
171
|
+
consentManager.current.setConsentExpirationTestingMode(
|
|
172
|
+
expirationSeconds,
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
[],
|
|
176
|
+
);
|
|
177
|
+
|
|
164
178
|
const context = useMemo(() => {
|
|
165
179
|
return {
|
|
166
180
|
locale: consentManager.current.translator.locale,
|
|
@@ -172,6 +186,7 @@ export const Osano: React.FunctionComponent<
|
|
|
172
186
|
setLocale: handleSetLocale,
|
|
173
187
|
showBanner: handleShowDialog,
|
|
174
188
|
showDrawer: handleShowDrawer,
|
|
189
|
+
setConsentExpirationTestingMode: handleSetConsentExpirationTestingMode,
|
|
175
190
|
isReady: consentManager.current.loaded,
|
|
176
191
|
};
|
|
177
192
|
}, [
|
|
@@ -195,6 +210,10 @@ export const Osano: React.FunctionComponent<
|
|
|
195
210
|
// We need to reset the state when the configId or customerId changes
|
|
196
211
|
dispatch(
|
|
197
212
|
reload({
|
|
213
|
+
// Pass config so init-time opt-out normalization runs on the
|
|
214
|
+
// stored consent even in headless consumers, where SET_CONFIG's
|
|
215
|
+
// UI effect never fires.
|
|
216
|
+
config: consentManager.current.config,
|
|
198
217
|
configId: reloadProps.configId,
|
|
199
218
|
customerId: reloadProps.customerId,
|
|
200
219
|
consent: reloadProps.consent,
|
package/src/reducer.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
OsanoCategories,
|
|
25
25
|
type ConsentCategoryType,
|
|
26
26
|
} from './constants';
|
|
27
|
+
import { doNotSellSideEffects, normalizeDoNotSellConsent } from './doNotSell';
|
|
27
28
|
import type { Disclosure, OsanoAction, OsanoState } from './types/store';
|
|
28
29
|
import {
|
|
29
30
|
getFinalOptOutValue,
|
|
@@ -36,11 +37,17 @@ import { ConsentConfig } from './ConsentManager';
|
|
|
36
37
|
|
|
37
38
|
export const init = (initialArgs: OsanoState & any): OsanoState => {
|
|
38
39
|
const { consent, ...reloadProps } = initialArgs;
|
|
39
|
-
const consentRecord =
|
|
40
|
-
|
|
41
|
-
?
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const consentRecord = normalizeDoNotSellConsent(
|
|
41
|
+
consent
|
|
42
|
+
? Array.isArray(consent)
|
|
43
|
+
? makeConsentFromCategories(consent)
|
|
44
|
+
: { ...consent }
|
|
45
|
+
: initialState.consent,
|
|
46
|
+
{
|
|
47
|
+
enableDoNotSell: reloadProps.config?.enableDoNotSell,
|
|
48
|
+
doNotSellCategories: reloadProps.config?.doNotSellCategories,
|
|
49
|
+
},
|
|
50
|
+
);
|
|
44
51
|
return {
|
|
45
52
|
...initialState,
|
|
46
53
|
...reloadProps,
|
|
@@ -109,11 +116,17 @@ export function reducer(state: OsanoState, action: OsanoAction): OsanoState {
|
|
|
109
116
|
*/
|
|
110
117
|
case SAVE_BEGIN: {
|
|
111
118
|
const consentedCategories: ConsentCategoryType[] = action.payload;
|
|
112
|
-
const consent =
|
|
119
|
+
const consent = normalizeDoNotSellConsent(
|
|
120
|
+
makeConsentFromCategories(consentedCategories),
|
|
121
|
+
{
|
|
122
|
+
enableDoNotSell: state.config?.enableDoNotSell,
|
|
123
|
+
doNotSellCategories: state.config?.doNotSellCategories,
|
|
124
|
+
},
|
|
125
|
+
);
|
|
113
126
|
return {
|
|
114
127
|
...state,
|
|
115
128
|
consent,
|
|
116
|
-
savedConsent: { ...consent },
|
|
129
|
+
savedConsent: { ...consent } as Record<ConsentCategoryType, boolean>,
|
|
117
130
|
};
|
|
118
131
|
}
|
|
119
132
|
case CLEAR_CONSENT: {
|
|
@@ -144,64 +157,48 @@ export function reducer(state: OsanoState, action: OsanoAction): OsanoState {
|
|
|
144
157
|
}
|
|
145
158
|
case SET_ALL_CONSENT: {
|
|
146
159
|
const acceptOrDeny = action.payload;
|
|
147
|
-
|
|
160
|
+
const next = setAllConsent(acceptOrDeny)(state);
|
|
161
|
+
return {
|
|
162
|
+
...next,
|
|
163
|
+
consent: normalizeDoNotSellConsent(next.consent, {
|
|
164
|
+
enableDoNotSell: state.config?.enableDoNotSell,
|
|
165
|
+
doNotSellCategories: state.config?.doNotSellCategories,
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
148
168
|
}
|
|
149
169
|
case TOGGLE_CATEGORY: {
|
|
150
|
-
|
|
170
|
+
// The action creator accepts either the shorthand string form
|
|
171
|
+
// (toggleCategory(ConsentCategory.X)) or an explicit { category, value }.
|
|
172
|
+
const { category, value } =
|
|
173
|
+
typeof action.payload === 'string'
|
|
174
|
+
? { category: action.payload, value: undefined }
|
|
175
|
+
: action.payload;
|
|
151
176
|
if (typeof category !== 'string') {
|
|
152
177
|
return state;
|
|
153
178
|
}
|
|
179
|
+
const key = category as ConsentCategoryType;
|
|
154
180
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
181
|
+
// Resolve the new value: an explicit boolean, otherwise flip the current
|
|
182
|
+
// value (an unset category defaults to on).
|
|
183
|
+
const current = state.consent[key];
|
|
184
|
+
const newValue =
|
|
185
|
+
typeof value === 'boolean'
|
|
186
|
+
? value
|
|
187
|
+
: current !== undefined
|
|
188
|
+
? !current
|
|
189
|
+
: true;
|
|
162
190
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
) {
|
|
169
|
-
return {
|
|
170
|
-
...state,
|
|
171
|
-
consent: {
|
|
172
|
-
...state.consent,
|
|
173
|
-
[ConsentCategory.MARKETING]: false,
|
|
174
|
-
[category as ConsentCategoryType]: value,
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
}
|
|
191
|
+
// Keep the opt-out and its governed categories mutually exclusive.
|
|
192
|
+
const forcedOff = doNotSellSideEffects(key, newValue, {
|
|
193
|
+
enableDoNotSell: state.config?.enableDoNotSell,
|
|
194
|
+
doNotSellCategories: state.config?.doNotSellCategories,
|
|
195
|
+
});
|
|
178
196
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
consent: {
|
|
183
|
-
...state.consent,
|
|
184
|
-
[category as ConsentCategoryType]: value,
|
|
185
|
-
},
|
|
186
|
-
};
|
|
197
|
+
const consent = { ...state.consent, [key]: newValue };
|
|
198
|
+
for (const forced of forcedOff) {
|
|
199
|
+
consent[forced] = false;
|
|
187
200
|
}
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
...state,
|
|
191
|
-
consent: {
|
|
192
|
-
...state.consent,
|
|
193
|
-
[category as ConsentCategoryType]:
|
|
194
|
-
!state.consent[category as ConsentCategoryType],
|
|
195
|
-
},
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
return {
|
|
199
|
-
...state,
|
|
200
|
-
consent: {
|
|
201
|
-
...state.consent,
|
|
202
|
-
[category as ConsentCategoryType]: true,
|
|
203
|
-
},
|
|
204
|
-
};
|
|
201
|
+
return { ...state, consent };
|
|
205
202
|
}
|
|
206
203
|
case SHOW_DIALOG:
|
|
207
204
|
return {
|
package/src/selectors.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from './constants';
|
|
6
6
|
import type { Disclosure, OsanoState } from './types/store';
|
|
7
7
|
|
|
8
|
+
import { resolveDoNotSellCategories } from './doNotSell';
|
|
8
9
|
import { get } from './utils';
|
|
9
10
|
|
|
10
11
|
export const isCategoryAccepted = (
|
|
@@ -14,14 +15,36 @@ export const isCategoryAccepted = (
|
|
|
14
15
|
|
|
15
16
|
export const isAnalyticsAlwaysAllowed = (state: OsanoState) =>
|
|
16
17
|
!!state.config?.type.analyticsAlways;
|
|
18
|
+
|
|
19
|
+
export const isBrowsingFromUS = (state: OsanoState) =>
|
|
20
|
+
!!state.config?.browsingFromUS;
|
|
21
|
+
|
|
22
|
+
export const isDoNotSellEnabled = (state: OsanoState) =>
|
|
23
|
+
!!state.config?.enableDoNotSell;
|
|
24
|
+
|
|
25
|
+
// Whether to render the "Do Not Sell / Share" opt-out toggle. Delegates to the
|
|
26
|
+
// config's visibility rule (tenant flag AND ccpaApplies/US).
|
|
27
|
+
export const shouldRenderOptOut = (state: OsanoState) =>
|
|
28
|
+
!!state.config?.isDoNotSellVisible;
|
|
29
|
+
|
|
30
|
+
// The categories the opt-out governs (config's doNotSellCategories, defaulting
|
|
31
|
+
// to Marketing). ESSENTIAL and OPT_OUT are never governed.
|
|
32
|
+
const isGovernedByOptOut = (state: OsanoState, category: ConsentCategoryType) =>
|
|
33
|
+
resolveDoNotSellCategories(state.config?.doNotSellCategories).includes(
|
|
34
|
+
category,
|
|
35
|
+
);
|
|
36
|
+
|
|
17
37
|
export const isCategoryDisabled = (
|
|
18
38
|
state: OsanoState,
|
|
19
39
|
category: ConsentCategoryType,
|
|
20
40
|
) =>
|
|
21
41
|
category === ConsentCategory.ESSENTIAL ||
|
|
22
42
|
(category === ConsentCategory.OPT_OUT && isGPCSupportEnabled(state)) ||
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
// A governed category is locked off while the opt-out is active. Keyed off the
|
|
44
|
+
// configured governed categories, so a non-governed category (e.g. Marketing
|
|
45
|
+
// when the tenant governs Analytics) stays enabled.
|
|
46
|
+
(isGovernedByOptOut(state, category) &&
|
|
47
|
+
(shouldRenderOptOut(state) || isGPCSupportEnabled(state)) &&
|
|
25
48
|
isCategoryAccepted(state, ConsentCategory.OPT_OUT));
|
|
26
49
|
|
|
27
50
|
export const isDrawerOpen = (state: OsanoState) => state.stack.length > 0;
|