@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.
Files changed (69) hide show
  1. package/lib/commonjs/ConsentManager.js +70 -3
  2. package/lib/commonjs/ConsentManager.js.map +1 -1
  3. package/lib/commonjs/constants.js +2 -3
  4. package/lib/commonjs/constants.js.map +1 -1
  5. package/lib/commonjs/context.js +1 -0
  6. package/lib/commonjs/context.js.map +1 -1
  7. package/lib/commonjs/doNotSell.js +95 -0
  8. package/lib/commonjs/doNotSell.js.map +1 -0
  9. package/lib/commonjs/index.js +13 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/reducer.js +41 -41
  12. package/lib/commonjs/reducer.js.map +1 -1
  13. package/lib/commonjs/selectors.js +20 -2
  14. package/lib/commonjs/selectors.js.map +1 -1
  15. package/lib/commonjs/services/consent.js +163 -34
  16. package/lib/commonjs/services/consent.js.map +1 -1
  17. package/lib/commonjs/services/gdprMappings.js +115 -0
  18. package/lib/commonjs/services/gdprMappings.js.map +1 -0
  19. package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js +1 -1
  20. package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
  21. package/lib/module/ConsentManager.js +70 -3
  22. package/lib/module/ConsentManager.js.map +1 -1
  23. package/lib/module/constants.js +2 -3
  24. package/lib/module/constants.js.map +1 -1
  25. package/lib/module/context.js +1 -0
  26. package/lib/module/context.js.map +1 -1
  27. package/lib/module/doNotSell.js +87 -0
  28. package/lib/module/doNotSell.js.map +1 -0
  29. package/lib/module/index.js +13 -1
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/module/reducer.js +41 -41
  32. package/lib/module/reducer.js.map +1 -1
  33. package/lib/module/selectors.js +16 -1
  34. package/lib/module/selectors.js.map +1 -1
  35. package/lib/module/services/consent.js +161 -33
  36. package/lib/module/services/consent.js.map +1 -1
  37. package/lib/module/services/gdprMappings.js +109 -0
  38. package/lib/module/services/gdprMappings.js.map +1 -0
  39. package/lib/module/views/Drawer/Drawer.ConsentCategories.js +2 -2
  40. package/lib/module/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
  41. package/lib/typescript/src/ConsentManager.d.ts +29 -1
  42. package/lib/typescript/src/ConsentManager.d.ts.map +1 -1
  43. package/lib/typescript/src/constants.d.ts.map +1 -1
  44. package/lib/typescript/src/context.d.ts +1 -0
  45. package/lib/typescript/src/context.d.ts.map +1 -1
  46. package/lib/typescript/src/doNotSell.d.ts +48 -0
  47. package/lib/typescript/src/doNotSell.d.ts.map +1 -0
  48. package/lib/typescript/src/index.d.ts.map +1 -1
  49. package/lib/typescript/src/reducer.d.ts.map +1 -1
  50. package/lib/typescript/src/selectors.d.ts +3 -0
  51. package/lib/typescript/src/selectors.d.ts.map +1 -1
  52. package/lib/typescript/src/services/consent.d.ts +8 -3
  53. package/lib/typescript/src/services/consent.d.ts.map +1 -1
  54. package/lib/typescript/src/services/gdprMappings.d.ts +32 -0
  55. package/lib/typescript/src/services/gdprMappings.d.ts.map +1 -0
  56. package/lib/typescript/src/views/Drawer/Drawer.ConsentCategories.d.ts.map +1 -1
  57. package/package.json +4 -4
  58. package/src/ConsentManager.ts +88 -7
  59. package/src/constants.ts +2 -3
  60. package/src/context.ts +2 -0
  61. package/src/doNotSell.ts +122 -0
  62. package/src/index.tsx +20 -1
  63. package/src/reducer.ts +53 -56
  64. package/src/selectors.ts +25 -2
  65. package/src/services/consent.ts +217 -44
  66. package/src/services/gdprMappings.ts +126 -0
  67. package/src/types/consent-config.d.ts +6 -0
  68. package/src/types/index.d.ts +9 -0
  69. package/src/views/Drawer/Drawer.ConsentCategories.tsx +2 -1
@@ -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 = consent
40
- ? Array.isArray(consent)
41
- ? makeConsentFromCategories(consent)
42
- : { ...consent }
43
- : initialState.consent;
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 = makeConsentFromCategories(consentedCategories);
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
- return setAllConsent(acceptOrDeny)(state);
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
- const { category, value } = action.payload;
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
- // don't turn MARKETING on if OPT_OUT is already on
156
- if (
157
- category === ConsentCategory.MARKETING &&
158
- state.consent[ConsentCategory.OPT_OUT] === true
159
- ) {
160
- return state;
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
- // turn MARKETING off if turning on OPT_OUT
164
- if (
165
- category === ConsentCategory.OPT_OUT &&
166
- value === true &&
167
- state.consent[ConsentCategory.MARKETING]
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
- if (typeof value === 'boolean') {
180
- return {
181
- ...state,
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
- if (state.consent[category as ConsentCategoryType] !== undefined) {
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
- (category === ConsentCategory.MARKETING &&
24
- (state.config?.shouldRenderOptOut || isGPCSupportEnabled(state)) &&
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;