@osano/osano-cmp-react-native 0.19.2 → 0.19.3

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 (46) hide show
  1. package/lib/commonjs/ConsentManager.js +18 -5
  2. package/lib/commonjs/ConsentManager.js.map +1 -1
  3. package/lib/commonjs/ConsentUiBuilder.js +8 -4
  4. package/lib/commonjs/ConsentUiBuilder.js.map +1 -1
  5. package/lib/commonjs/doNotSell.js +4 -6
  6. package/lib/commonjs/doNotSell.js.map +1 -1
  7. package/lib/commonjs/reducer.js +5 -1
  8. package/lib/commonjs/reducer.js.map +1 -1
  9. package/lib/commonjs/services/consent.js +11 -5
  10. package/lib/commonjs/services/consent.js.map +1 -1
  11. package/lib/commonjs/utils/safeArea.js +38 -0
  12. package/lib/commonjs/utils/safeArea.js.map +1 -0
  13. package/lib/commonjs/views/Dialog/index.js +19 -5
  14. package/lib/commonjs/views/Dialog/index.js.map +1 -1
  15. package/lib/module/ConsentManager.js +18 -5
  16. package/lib/module/ConsentManager.js.map +1 -1
  17. package/lib/module/ConsentUiBuilder.js +8 -4
  18. package/lib/module/ConsentUiBuilder.js.map +1 -1
  19. package/lib/module/doNotSell.js +4 -6
  20. package/lib/module/doNotSell.js.map +1 -1
  21. package/lib/module/reducer.js +5 -1
  22. package/lib/module/reducer.js.map +1 -1
  23. package/lib/module/services/consent.js +11 -5
  24. package/lib/module/services/consent.js.map +1 -1
  25. package/lib/module/utils/safeArea.js +33 -0
  26. package/lib/module/utils/safeArea.js.map +1 -0
  27. package/lib/module/views/Dialog/index.js +20 -6
  28. package/lib/module/views/Dialog/index.js.map +1 -1
  29. package/lib/typescript/src/ConsentManager.d.ts +13 -4
  30. package/lib/typescript/src/ConsentManager.d.ts.map +1 -1
  31. package/lib/typescript/src/ConsentUiBuilder.d.ts.map +1 -1
  32. package/lib/typescript/src/doNotSell.d.ts +4 -1
  33. package/lib/typescript/src/doNotSell.d.ts.map +1 -1
  34. package/lib/typescript/src/reducer.d.ts.map +1 -1
  35. package/lib/typescript/src/services/consent.d.ts.map +1 -1
  36. package/lib/typescript/src/utils/safeArea.d.ts +12 -0
  37. package/lib/typescript/src/utils/safeArea.d.ts.map +1 -0
  38. package/lib/typescript/src/views/Dialog/index.d.ts.map +1 -1
  39. package/package.json +11 -5
  40. package/src/ConsentManager.ts +19 -5
  41. package/src/ConsentUiBuilder.tsx +8 -3
  42. package/src/doNotSell.ts +4 -6
  43. package/src/reducer.ts +5 -1
  44. package/src/services/consent.ts +11 -5
  45. package/src/utils/safeArea.ts +39 -0
  46. package/src/views/Dialog/index.tsx +23 -6
@@ -152,7 +152,11 @@ export const storeConsent = async (
152
152
  ): Promise<ConsentCategoryType[]> => {
153
153
  cmp.consent = consent ?? cmp.consent;
154
154
  return new Promise<ConsentCategoryType[]>((resolve, reject) => {
155
+ // Only once both writes land: `consentSaved` mirrors web's isSaved (a
156
+ // stored record exists), and a failed save must leave the implied-consent
157
+ // close path able to record a choice later.
155
158
  const resConsent = () => {
159
+ cmp.consentSaved = true;
156
160
  resolve(cmp.consent ?? []);
157
161
  return cmp.consent;
158
162
  };
@@ -280,11 +284,11 @@ export const migrateConsentStorage = async (
280
284
  }
281
285
  };
282
286
 
283
- // The consent to fall back to when a visitor has no stored consent yet. Flavor 7
284
- // shows analytics enabled on first load; every other flavor stays essential-only.
285
- // The Do-Not-Sell default is then applied, but only when the opt-out would
286
- // actually be visible (tenant flag + CCPA/US), so we never seed an OPT_OUT the
287
- // user cannot see or control.
287
+ // The consent to fall back to when a visitor has no stored consent yet:
288
+ // essential-only, plus analytics for analytics-always flavors, matching the web
289
+ // CMP's pre-choice state. The Do-Not-Sell default is then applied, but only
290
+ // when the opt-out would actually be visible (tenant flag + CCPA/US), so we
291
+ // never seed an OPT_OUT the user cannot see or control.
288
292
  const firstLoadConsent = (cmp: ConsentManager): ConsentCategoryType[] => {
289
293
  let categories: ConsentCategoryType[] = [ConsentCategory.ESSENTIAL];
290
294
  if (cmp.config?.analyticsDefaultsOnAtFirstLoad) {
@@ -309,6 +313,7 @@ const clearToFirstLoadDefault = async (
309
313
  await clearConsent(cmp);
310
314
  const seed = firstLoadConsent(cmp);
311
315
  cmp.consent = seed;
316
+ cmp.consentSaved = false;
312
317
  return seed;
313
318
  };
314
319
 
@@ -388,6 +393,7 @@ const retrieveConsent = async (cmp: ConsentManager) => {
388
393
  const consent = JSON.parse(consentJson) as ConsentCategoryType[];
389
394
  if (consent.length > 0) {
390
395
  cmp.consent = consent;
396
+ cmp.consentSaved = true;
391
397
  }
392
398
  }
393
399
  if (cmp.consent && cmp.consent.length > 0) {
@@ -0,0 +1,39 @@
1
+ export type SafeAreaInsets = {
2
+ top: number;
3
+ right: number;
4
+ bottom: number;
5
+ left: number;
6
+ };
7
+
8
+ const FALLBACK_INSETS: SafeAreaInsets = {
9
+ top: 0,
10
+ right: 0,
11
+ bottom: 0,
12
+ left: 0,
13
+ };
14
+
15
+ // react-native-safe-area-context is an optional peer: it is a native module,
16
+ // so a hard dependency would force every host app to relink on upgrade.
17
+ // Resolved once at module load, so the hook below branches on a constant.
18
+ let useLibInsets: (() => SafeAreaInsets) | undefined;
19
+ try {
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const lib = require('react-native-safe-area-context') as {
22
+ useSafeAreaInsets: () => SafeAreaInsets;
23
+ };
24
+ useLibInsets = lib.useSafeAreaInsets;
25
+ } catch {
26
+ useLibInsets = undefined;
27
+ }
28
+
29
+ /**
30
+ * Device safe-area insets, or zeros when react-native-safe-area-context is not
31
+ * installed or no SafeAreaProvider is mounted (its hook throws in that case).
32
+ */
33
+ export const useSafeAreaInsetsSafe = (): SafeAreaInsets => {
34
+ try {
35
+ return useLibInsets ? useLibInsets() : FALLBACK_INSETS;
36
+ } catch {
37
+ return FALLBACK_INSETS;
38
+ }
39
+ };
@@ -10,6 +10,7 @@ import {
10
10
  Linking,
11
11
  Modal,
12
12
  Pressable,
13
+ ScrollView,
13
14
  StyleSheet,
14
15
  Text,
15
16
  useWindowDimensions,
@@ -21,6 +22,7 @@ import {
21
22
  } from 'react-native';
22
23
  import { acceptAll, close, toggleCategory } from '../../actions';
23
24
  import { saveConsent } from '../../commands';
25
+ import { useSafeAreaInsetsSafe } from '../../utils/safeArea';
24
26
  import { getConsent, isCategoryDisabled, isDialogOpen } from '../../selectors';
25
27
 
26
28
  import { CloseButton } from '../../components/Button';
@@ -60,7 +62,10 @@ export const Dialog = (): ReactElement<Modal> => {
60
62
  const categories = useTcf
61
63
  ? config.iabCategories
62
64
  : config?.nonEssentialCategories || [];
63
- const { width: windowWidth } = useWindowDimensions();
65
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
66
+ const insets = useSafeAreaInsetsSafe();
67
+ const safePaddingTop = Math.max(insets.top, 20);
68
+ const safePaddingBottom = Math.max(insets.bottom, 20);
64
69
 
65
70
  const clearImpliedConsentTimeout = useCallback(() => {
66
71
  if (!impliedConsentTimeout.current) return;
@@ -70,9 +75,10 @@ export const Dialog = (): ReactElement<Modal> => {
70
75
 
71
76
  const handleClose = useCallback(() => {
72
77
  clearImpliedConsentTimeout();
73
- if (config?.isConsentImplied && !cmp?.consent) {
78
+ // `consentSaved`, not `consent` — the first-load seed populates `consent`
79
+ // and would otherwise suppress the implied accept-all forever.
80
+ if (config?.isConsentImplied && !cmp?.consentSaved) {
74
81
  dispatch(acceptAll());
75
- // Only save here if consent is implied and no consent has been given yet
76
82
  cmp && dispatch(saveConsent(cmp));
77
83
  }
78
84
  dispatch(close());
@@ -142,6 +148,9 @@ export const Dialog = (): ReactElement<Modal> => {
142
148
  {
143
149
  backgroundColor: dialogBackgroundColor,
144
150
  maxWidth: 1024,
151
+ // Content-heavy flavors overflow small screens; without a cap the
152
+ // buttons are pushed off-screen and the banner can't be acted on.
153
+ maxHeight: Math.max(0, windowHeight - safePaddingTop - safePaddingBottom),
145
154
  width: '100%',
146
155
  borderRadius: dialogType === 'box' ? 8 : 0,
147
156
  padding: 28,
@@ -184,9 +193,16 @@ export const Dialog = (): ReactElement<Modal> => {
184
193
  presentationStyle="overFullScreen"
185
194
  supportedOrientations={['portrait', 'landscape']}
186
195
  >
187
- <View style={[styles.modalContainer, dialogOverlay]}>
196
+ <View
197
+ style={[
198
+ styles.modalContainer,
199
+ dialogOverlay,
200
+ { paddingTop: safePaddingTop, paddingBottom: safePaddingBottom },
201
+ ]}
202
+ >
188
203
  <View style={dialogStyles}>
189
- <View style={styles.dialogContent}>
204
+ {/* Buttons stay outside the scroll area so they are always reachable. */}
205
+ <ScrollView style={styles.dialogContent} nestedScrollEnabled>
190
206
  <View style={[styles.row, styles.fitContent]}>
191
207
  <Text style={dialogForeground} testID="dialog-message">
192
208
  <Message />
@@ -256,7 +272,7 @@ export const Dialog = (): ReactElement<Modal> => {
256
272
  ))}
257
273
  </View>
258
274
  ) : null}
259
- </View>
275
+ </ScrollView>
260
276
  <Buttons
261
277
  style={windowWidth >= 768 ? styles.dialogRow : styles.dialogCol}
262
278
  buttonStyle={
@@ -325,6 +341,7 @@ const styles = StyleSheet.create({
325
341
  justifyContent: 'center',
326
342
  },
327
343
  dialogContent: {
344
+ flexShrink: 1,
328
345
  marginBottom: 16,
329
346
  },
330
347
  bannerLogo: {