@osano/osano-cmp-react-native 0.18.5 → 0.19.0
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 +28 -2
- package/lib/commonjs/ConsentManager.js.map +1 -1
- package/lib/commonjs/components/PoweredByOsano.js +72 -0
- package/lib/commonjs/components/PoweredByOsano.js.map +1 -0
- package/lib/commonjs/constants.js +2 -3
- package/lib/commonjs/constants.js.map +1 -1
- package/lib/commonjs/doNotSell.js +95 -0
- package/lib/commonjs/doNotSell.js.map +1 -0
- package/lib/commonjs/index.js +9 -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 +34 -5
- package/lib/commonjs/services/consent.js.map +1 -1
- package/lib/commonjs/views/Dialog/index.js +15 -2
- package/lib/commonjs/views/Dialog/index.js.map +1 -1
- package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js +161 -13
- package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
- package/lib/module/ConsentManager.js +28 -2
- package/lib/module/ConsentManager.js.map +1 -1
- package/lib/module/components/PoweredByOsano.js +68 -0
- package/lib/module/components/PoweredByOsano.js.map +1 -0
- package/lib/module/constants.js +2 -3
- package/lib/module/constants.js.map +1 -1
- package/lib/module/doNotSell.js +87 -0
- package/lib/module/doNotSell.js.map +1 -0
- package/lib/module/index.js +9 -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 +34 -5
- package/lib/module/services/consent.js.map +1 -1
- package/lib/module/views/Dialog/index.js +16 -3
- package/lib/module/views/Dialog/index.js.map +1 -1
- package/lib/module/views/Drawer/Drawer.ConsentCategories.js +164 -16
- package/lib/module/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
- package/lib/typescript/src/ConsentManager.d.ts +7 -1
- package/lib/typescript/src/ConsentManager.d.ts.map +1 -1
- package/lib/typescript/src/components/PoweredByOsano.d.ts +5 -0
- package/lib/typescript/src/components/PoweredByOsano.d.ts.map +1 -0
- package/lib/typescript/src/constants.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 +2 -2
- package/lib/typescript/src/services/consent.d.ts.map +1 -1
- package/lib/typescript/src/views/Dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/views/Drawer/Drawer.ConsentCategories.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/ConsentManager.ts +28 -5
- package/src/components/PoweredByOsano.tsx +79 -0
- package/src/constants.ts +2 -3
- package/src/doNotSell.ts +122 -0
- package/src/index.tsx +10 -1
- package/src/reducer.ts +53 -56
- package/src/selectors.ts +25 -2
- package/src/services/consent.ts +49 -9
- package/src/types/consent-config.d.ts +12 -0
- package/src/views/Dialog/index.tsx +19 -1
- package/src/views/Drawer/Drawer.ConsentCategories.tsx +170 -12
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);
|
|
@@ -205,6 +210,10 @@ export const Osano: React.FunctionComponent<
|
|
|
205
210
|
// We need to reset the state when the configId or customerId changes
|
|
206
211
|
dispatch(
|
|
207
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,
|
|
208
217
|
configId: reloadProps.configId,
|
|
209
218
|
customerId: reloadProps.customerId,
|
|
210
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;
|
package/src/services/consent.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
formatConsentArray,
|
|
10
10
|
makeConsentFromCategories,
|
|
11
11
|
} from '../constants';
|
|
12
|
+
import { applyDoNotSellDefault, normalizeDoNotSellConsent } from '../doNotSell';
|
|
12
13
|
import { clearStorage, getStorage, setStorage } from '../native/OsanoCmp';
|
|
13
14
|
import { getConsentExpirationSeconds } from './gdprMappings';
|
|
14
15
|
import type { Config } from '../types/consent-config';
|
|
@@ -220,7 +221,13 @@ export const migrateConsentStorage = async (
|
|
|
220
221
|
let consent = JSON.parse(oldConsent) as ConsentCategoryType[];
|
|
221
222
|
if (consent.length > 0) {
|
|
222
223
|
consent = formatConsentArray(
|
|
223
|
-
|
|
224
|
+
normalizeDoNotSellConsent(
|
|
225
|
+
makeConsentFromCategories(consent),
|
|
226
|
+
{
|
|
227
|
+
enableDoNotSell: cmp.config.enableDoNotSell,
|
|
228
|
+
doNotSellCategories: cmp.config.doNotSellCategories,
|
|
229
|
+
},
|
|
230
|
+
),
|
|
224
231
|
);
|
|
225
232
|
// Store with new user-specific key
|
|
226
233
|
storeConsent(cmp, consent).then(() => {
|
|
@@ -247,10 +254,23 @@ export const migrateConsentStorage = async (
|
|
|
247
254
|
|
|
248
255
|
// The consent to fall back to when a visitor has no stored consent yet. Flavor 7
|
|
249
256
|
// shows analytics enabled on first load; every other flavor stays essential-only.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
// The Do-Not-Sell default is then applied, but only when the opt-out would
|
|
258
|
+
// actually be visible (tenant flag + CCPA/US), so we never seed an OPT_OUT the
|
|
259
|
+
// user cannot see or control.
|
|
260
|
+
const firstLoadConsent = (cmp: ConsentManager): ConsentCategoryType[] => {
|
|
261
|
+
let categories: ConsentCategoryType[] = [ConsentCategory.ESSENTIAL];
|
|
262
|
+
if (cmp.config?.analyticsDefaultsOnAtFirstLoad) {
|
|
263
|
+
categories.push(ConsentCategory.ANALYTICS);
|
|
264
|
+
}
|
|
265
|
+
if (cmp.config?.isDoNotSellVisible) {
|
|
266
|
+
categories = applyDoNotSellDefault(categories, {
|
|
267
|
+
enableDoNotSell: cmp.config.enableDoNotSell,
|
|
268
|
+
enableDoNotSellDefault: cmp.config.enableDoNotSellDefault,
|
|
269
|
+
doNotSellCategories: cmp.config.doNotSellCategories,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return categories;
|
|
273
|
+
};
|
|
254
274
|
|
|
255
275
|
// Clear any stale storage, then seed the first-load default. Because this only
|
|
256
276
|
// runs when there is no stored consent, a returning visitor's saved opt-out is
|
|
@@ -442,7 +462,13 @@ export const recordConsent = (
|
|
|
442
462
|
.then(() => consent ?? cmp.consent ?? [ConsentCategory.ESSENTIAL])
|
|
443
463
|
.then((consentCategories) => {
|
|
444
464
|
consentCategories = formatConsentArray(
|
|
445
|
-
|
|
465
|
+
normalizeDoNotSellConsent(
|
|
466
|
+
makeConsentFromCategories(consentCategories),
|
|
467
|
+
{
|
|
468
|
+
enableDoNotSell: cmp.config.enableDoNotSell,
|
|
469
|
+
doNotSellCategories: cmp.config.doNotSellCategories,
|
|
470
|
+
},
|
|
471
|
+
),
|
|
446
472
|
);
|
|
447
473
|
storeConsent(cmp, consentCategories, true);
|
|
448
474
|
return consentCategories;
|
|
@@ -526,8 +552,19 @@ export const fetchConsent = async (cmp: ConsentManager) => {
|
|
|
526
552
|
// first-load default.
|
|
527
553
|
return settleEssentialOnly(cmp);
|
|
528
554
|
}
|
|
529
|
-
const remoteConsentCategories =
|
|
530
|
-
(
|
|
555
|
+
const remoteConsentCategories = formatConsentArray(
|
|
556
|
+
normalizeDoNotSellConsent(
|
|
557
|
+
makeConsentFromCategories(
|
|
558
|
+
consented.map(
|
|
559
|
+
(category) =>
|
|
560
|
+
ConsentCategory[category as keyof typeof ConsentCategory],
|
|
561
|
+
),
|
|
562
|
+
),
|
|
563
|
+
{
|
|
564
|
+
enableDoNotSell: cmp.config.enableDoNotSell,
|
|
565
|
+
doNotSellCategories: cmp.config.doNotSellCategories,
|
|
566
|
+
},
|
|
567
|
+
),
|
|
531
568
|
);
|
|
532
569
|
// Preserve the server timestamp so restoring can't re-date the record.
|
|
533
570
|
await storeConsent(
|
|
@@ -550,7 +587,10 @@ export const fetchConsent = async (cmp: ConsentManager) => {
|
|
|
550
587
|
// Remote consent is newer, unexpired, or we haven't recorded any
|
|
551
588
|
// consent yet. Store it, preserving the server timestamp.
|
|
552
589
|
const remoteConsentCategories = formatConsentArray(
|
|
553
|
-
makeConsentFromCategories(consented),
|
|
590
|
+
normalizeDoNotSellConsent(makeConsentFromCategories(consented), {
|
|
591
|
+
enableDoNotSell: cmp.config.enableDoNotSell,
|
|
592
|
+
doNotSellCategories: cmp.config.doNotSellCategories,
|
|
593
|
+
}),
|
|
554
594
|
);
|
|
555
595
|
await storeConsent(
|
|
556
596
|
cmp,
|
|
@@ -44,6 +44,12 @@ export type Config = {
|
|
|
44
44
|
categories: ConsentCategoryType[];
|
|
45
45
|
ccpaApplies: boolean;
|
|
46
46
|
ccpaRelaxed: boolean;
|
|
47
|
+
// "Do Not Sell / Share" opt-out controls. `enableDoNotSell` gates whether the
|
|
48
|
+
// opt-out toggle renders; `enableDoNotSellDefault` seeds it on at first load;
|
|
49
|
+
// `doNotSellCategories` lists the categories the opt-out applies to.
|
|
50
|
+
enableDoNotSell: boolean;
|
|
51
|
+
enableDoNotSellDefault: boolean;
|
|
52
|
+
doNotSellCategories: string[];
|
|
47
53
|
configId: string;
|
|
48
54
|
country: string;
|
|
49
55
|
crossDomain: boolean;
|
|
@@ -65,6 +71,12 @@ export type Config = {
|
|
|
65
71
|
policyLinkText: string;
|
|
66
72
|
region: string;
|
|
67
73
|
remoteConsent: boolean;
|
|
74
|
+
// When true, the categories drawer shows a row with the visitor's consent
|
|
75
|
+
// UUID (the device identifier). Defaults false.
|
|
76
|
+
showConsentUuid: boolean;
|
|
77
|
+
// When true, the consent banner shows the "Powered by Osano" logo. Defaults
|
|
78
|
+
// false. Does not affect the categories drawer, which always shows the logo.
|
|
79
|
+
showBannerLogo: boolean;
|
|
68
80
|
storagePolicyHref: string;
|
|
69
81
|
timeoutSeconds: number;
|
|
70
82
|
type?: Flavor;
|
|
@@ -7,7 +7,9 @@ import React, {
|
|
|
7
7
|
useRef,
|
|
8
8
|
} from 'react';
|
|
9
9
|
import {
|
|
10
|
+
Linking,
|
|
10
11
|
Modal,
|
|
12
|
+
Pressable,
|
|
11
13
|
StyleSheet,
|
|
12
14
|
Text,
|
|
13
15
|
useWindowDimensions,
|
|
@@ -23,6 +25,7 @@ import { getConsent, isCategoryDisabled, isDialogOpen } from '../../selectors';
|
|
|
23
25
|
|
|
24
26
|
import { CloseButton } from '../../components/Button';
|
|
25
27
|
import Locale from '../../components/Locale';
|
|
28
|
+
import PoweredByOsano from '../../components/PoweredByOsano';
|
|
26
29
|
import StoragePolicyLink from '../../components/StoragePolicyLink';
|
|
27
30
|
import Toggle from '../../components/Toggle';
|
|
28
31
|
import { type ConsentCategoryType } from '../../constants';
|
|
@@ -34,7 +37,7 @@ import Message from './Message';
|
|
|
34
37
|
import PreferenceManagementDeepLinks from './PreferenceManagementDeepLinks';
|
|
35
38
|
|
|
36
39
|
export const Dialog = (): ReactElement<Modal> => {
|
|
37
|
-
const { cmp, config, dispatch, getState, locale } =
|
|
40
|
+
const { cmp, config, dispatch, getState, locale, translate } =
|
|
38
41
|
useContext(OsanoPrivateContext);
|
|
39
42
|
const impliedConsentTimeout = useRef<ReturnType<typeof setTimeout> | null>(
|
|
40
43
|
null,
|
|
@@ -260,6 +263,17 @@ export const Dialog = (): ReactElement<Modal> => {
|
|
|
260
263
|
windowWidth >= 768 ? styles.buttonRow : styles.buttonCol
|
|
261
264
|
}
|
|
262
265
|
/>
|
|
266
|
+
{config?.showBannerLogo ? (
|
|
267
|
+
<Pressable
|
|
268
|
+
accessibilityRole="link"
|
|
269
|
+
accessibilityLabel={`${translate('messaging.poweredBy')}`}
|
|
270
|
+
onPress={() => Linking.openURL('https://www.osano.com')}
|
|
271
|
+
style={styles.bannerLogo}
|
|
272
|
+
testID="dialog-banner-logo"
|
|
273
|
+
>
|
|
274
|
+
<PoweredByOsano />
|
|
275
|
+
</Pressable>
|
|
276
|
+
) : null}
|
|
263
277
|
{(config?.showDismissButton ?? true) && (
|
|
264
278
|
<CloseButton
|
|
265
279
|
testID="close-dialog"
|
|
@@ -313,6 +327,10 @@ const styles = StyleSheet.create({
|
|
|
313
327
|
dialogContent: {
|
|
314
328
|
marginBottom: 16,
|
|
315
329
|
},
|
|
330
|
+
bannerLogo: {
|
|
331
|
+
alignItems: 'center',
|
|
332
|
+
paddingTop: 28,
|
|
333
|
+
},
|
|
316
334
|
buttonCol: {
|
|
317
335
|
maxWidth: Number.MAX_SAFE_INTEGER,
|
|
318
336
|
},
|