@washingtonpost/subs-de-inputs 1.0.0-react18.1 → 1.0.0-react18.10

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.
@@ -0,0 +1,23 @@
1
+ import { ICookieStore } from '@washingtonpost/subs-sdk';
2
+ interface TCData {
3
+ eventStatus: 'tcloaded' | 'cmpuishown' | 'useractioncomplete';
4
+ listenerId: number;
5
+ [key: string]: any;
6
+ }
7
+ type TCFAPIAddListener = (command: 'addEventListener', version: number, callback: (tcData: TCData, success: boolean) => void) => void;
8
+ type TCFAPIRmListener = (command: 'removeEventListener', version: number, callback: (success: boolean) => void, listenerId: number) => void;
9
+ type TCFAPIPing = (command: 'ping', version: number, callback: (pingReturn: any, success: boolean) => void) => void;
10
+ declare global {
11
+ interface Window {
12
+ cookieStore?: ICookieStore;
13
+ __tcfapi?: TCFAPIAddListener & TCFAPIRmListener & TCFAPIPing;
14
+ }
15
+ }
16
+ export declare const useOneTrustAlertBoxClosed: ({ allowCookieStore, }: {
17
+ allowCookieStore: boolean;
18
+ }) => {
19
+ alertBoxClosed: boolean | undefined;
20
+ listenToCookieStore: boolean;
21
+ listenToTcfApi: boolean;
22
+ };
23
+ export {};
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ interface DisclosureProps {
3
+ /** callback function to be called when the disclosure is finished loading */
4
+ onFinished?: ({ isFinished, isError, }: {
5
+ isFinished: boolean;
6
+ isError: boolean;
7
+ }) => void;
8
+ /** ability to turn off cookiestore listener, primarily for testing purposes */
9
+ allowCookieStore?: boolean;
10
+ }
11
+ export declare const DEDisclosure: React.FC<DisclosureProps>;
12
+ export {};
@@ -0,0 +1 @@
1
+ export declare const checkCookie: () => boolean;
@@ -0,0 +1,2 @@
1
+ import { DisclosureConfigValue } from '../../../interfaces/disclosure';
2
+ export declare const getConfig: () => Promise<DisclosureConfigValue | undefined>;
@@ -0,0 +1,3 @@
1
+ type hydrateLinksType = (str: string) => string | JSX.Element;
2
+ export declare const hydrateLinks: hydrateLinksType;
3
+ export {};
@@ -4,7 +4,7 @@ interface DESelectProps {
4
4
  source: string;
5
5
  fieldName: string;
6
6
  label?: string;
7
- dataDictionaryConfig?: Attribute;
7
+ dataDictionaryConfig?: Attribute | Readonly<Attribute>;
8
8
  defaultValue?: string;
9
9
  disabled?: boolean;
10
10
  submit: boolean;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './interfaces/index';
2
2
  export * from './interfaces/twpdeu';
3
3
  export * from './utils/hasRequiredPrivacyCookies';
4
- export * from './services/dataEnrichment';
4
+ export * from './utils/push';
5
+ export * from './services/getAttributes';
5
6
  export * from './components/DESelect';
7
+ export * from './components/DEDisclosure';
@@ -0,0 +1,8 @@
1
+ export type DisclosureConfigValue = {
2
+ disclosure: string[];
3
+ } & {
4
+ checkBannerStatus: boolean;
5
+ disclosure_beforebanner: string[];
6
+ disclosure_afterbanner: string[];
7
+ };
8
+ export type DisclosureConfig = Record<string, DisclosureConfigValue>;
@@ -13,12 +13,12 @@ export type Attribute = {
13
13
  name: string;
14
14
  approved_for_use?: boolean;
15
15
  collection_behavior?: (typeof CollectionBehaviors)[keyof typeof CollectionBehaviors];
16
- datatype: 'string';
16
+ datatype: string;
17
17
  explicit: boolean;
18
18
  multiple_value: boolean;
19
19
  last_modified_date: Number;
20
20
  date_created: Number;
21
- values: Array<AttributeValue>;
21
+ values: AttributeValue[] | Readonly<AttributeValue[]>;
22
22
  };
23
23
  export declare const AttributesState: {
24
24
  readonly SUCCESS: "100";
@@ -1,7 +1,8 @@
1
- import { getAttributes, ingest } from '../services/dataEnrichment';
1
+ import { getAttributes } from '../services/getAttributes';
2
+ import { push } from '../utils/push';
2
3
  export type TWPDEU = {
3
4
  getFieldConfigs: typeof getAttributes;
4
- push: typeof ingest;
5
+ push: typeof push;
5
6
  version?: string;
6
7
  };
7
8
  declare global {
@@ -0,0 +1,6 @@
1
+ import { Attribute } from '../interfaces';
2
+ export declare const getAttributes: GetAttributesType;
3
+ type GetAttributesType = ({ fieldName, }: {
4
+ fieldName: string;
5
+ }) => Promise<Attribute[]>;
6
+ export {};
@@ -1,16 +1,11 @@
1
1
  import { ResponseStatus } from '@washingtonpost/subs-sdk';
2
- import { Attribute, IngestResponseState, IngestType } from '../interfaces';
3
- export declare const getAttributes: GetAttributesType;
4
- type GetAttributesType = ({ fieldName, }: {
5
- fieldName: string;
6
- }) => Promise<Attribute[]>;
2
+ import { IngestResponseState, IngestType } from '../interfaces';
7
3
  export declare const ingest: IngestType;
8
4
  type IngestType = ({ submitData: { fieldName, value }, source, }: {
9
5
  submitData: {
10
6
  fieldName: string;
11
7
  value: string;
12
8
  };
13
- type?: (typeof IngestType)[keyof typeof IngestType];
14
9
  source: string;
15
10
  }) => Promise<{
16
11
  status: ResponseStatus;
@@ -0,0 +1,9 @@
1
+ export declare const sendToGA: SendToGaType;
2
+ type SendToGaType = ({ submitData: { fieldName, value }, source, }: {
3
+ submitData: {
4
+ fieldName: string;
5
+ value: string;
6
+ };
7
+ source: string;
8
+ }) => Promise<true>;
9
+ export {};
@@ -43,7 +43,7 @@ const hasRequiredPrivacyCookies = () => {
43
43
  return !!(wp_usp && countryCode === 'US');
44
44
  };
45
45
 
46
- const base = `${subsSdk.ENDPOINTS.base}/de/v1`;
46
+ const base$1 = `${subsSdk.ENDPOINTS.base}/de/v1`;
47
47
  const attributesCache = {};
48
48
  const getAttributes = async ({
49
49
  fieldName
@@ -53,7 +53,7 @@ const getAttributes = async ({
53
53
  }
54
54
  const fieldNames = [fieldName];
55
55
  try {
56
- const url = new URL(`${base}/attributes`);
56
+ const url = new URL(`${base$1}/attributes`);
57
57
  url.searchParams.set('attributes', fieldNames.join(','));
58
58
  const data = await fetch(url.toString(), {
59
59
  credentials: 'include',
@@ -72,34 +72,55 @@ const getAttributes = async ({
72
72
  return [];
73
73
  }
74
74
  };
75
+
76
+ const sendGAEvent = props => {
77
+ if (typeof window === 'undefined') {
78
+ console.warn('NO WINDOW');
79
+ return;
80
+ }
81
+ // Initialize dataLayer if needed
82
+ window.dataLayer = window.dataLayer || [];
83
+ const eventData = {
84
+ ...props
85
+ };
86
+ window.dataLayer.push(eventData);
87
+ };
88
+ const sendToGA = async ({
89
+ submitData: {
90
+ fieldName,
91
+ value
92
+ },
93
+ source
94
+ }) => {
95
+ sendGAEvent({
96
+ event: 'site-onpage-click',
97
+ action: 'site-onpage-click',
98
+ category: 'profile',
99
+ label: fieldName,
100
+ 'de-label': fieldName,
101
+ [fieldName]: value,
102
+ section: 'profile',
103
+ subsection: source
104
+ });
105
+ return true;
106
+ };
107
+
108
+ const base = `${subsSdk.ENDPOINTS.base}/de/v1`;
75
109
  const ingest = async ({
76
110
  submitData: {
77
111
  fieldName,
78
112
  value
79
113
  },
80
- type = IngestType.IMPLICIT,
81
114
  source
82
115
  }) => {
83
116
  const url = `${base}/ingest`;
84
117
  const wapo_login_id = subsSdk.getCookie('wapo_login_id');
85
- if (!hasRequiredPrivacyCookies()) {
86
- throw new Error('does not satisfy cookie check');
87
- }
88
- let attributeInfo = attributesCache[fieldName];
89
- if (!attributeInfo) {
90
- attributeInfo = await getAttributes({
91
- fieldName
92
- });
93
- }
94
- if (attributeInfo[0] && attributeInfo[0].name === fieldName && attributeInfo[0].collection_behavior === CollectionBehaviors.DO_NOT_COLLECT) {
95
- throw new Error('do not collect');
96
- }
97
118
  const jucid = localStorage.getItem('uuid');
98
119
  const ga = subsSdk.getCookie('_ga');
99
120
  const payload = {
100
121
  jucid,
101
122
  ga,
102
- type,
123
+ type: IngestType.EXPLICIT,
103
124
  wapo_login_id,
104
125
  // TODO: move this to BE to read from cookie headers
105
126
  data: {
@@ -124,6 +145,50 @@ const ingest = async ({
124
145
  }
125
146
  };
126
147
 
148
+ const isAnonymousWebview = () => {
149
+ if (typeof window === 'undefined') {
150
+ return false;
151
+ }
152
+ const wp_wv = subsSdk.getCookie('wp_wv');
153
+ return !!(wp_wv && !subsSdk.isLoggedIn());
154
+ };
155
+
156
+ const push = async ({
157
+ submitData,
158
+ source
159
+ }) => {
160
+ if (!hasRequiredPrivacyCookies()) {
161
+ throw new Error('does not satisfy cookie check');
162
+ }
163
+ if (isAnonymousWebview()) {
164
+ throw new Error('does not satisfy cookie check');
165
+ }
166
+ const {
167
+ fieldName
168
+ } = submitData;
169
+ const attributeInfo = await getAttributes({
170
+ fieldName
171
+ });
172
+ if (attributeInfo[0] && attributeInfo[0].name === fieldName && attributeInfo[0].collection_behavior === CollectionBehaviors.DO_NOT_COLLECT) {
173
+ throw new Error('do not collect');
174
+ }
175
+ const type = attributeInfo[0] && attributeInfo[0].explicit === true ? IngestType.EXPLICIT : IngestType.IMPLICIT;
176
+ if (!attributeInfo[0] && "development" !== "production") {
177
+ console.warn(`no attribute info found for ${fieldName}, assuming implicit`);
178
+ }
179
+ if (type === IngestType.EXPLICIT) {
180
+ return ingest({
181
+ submitData,
182
+ source
183
+ });
184
+ } else {
185
+ return sendToGA({
186
+ submitData,
187
+ source
188
+ });
189
+ }
190
+ };
191
+
127
192
  const StyledMobileSelect = /*#__PURE__*/wpdsUiKit.styled('select', {
128
193
  padding: '12px 16px 12px 6px',
129
194
  display: 'flex',
@@ -296,16 +361,14 @@ const DESelect = ({
296
361
  const submitSelected = async () => {
297
362
  try {
298
363
  var _window2;
299
- // TODO: Log to GA?
300
364
  const result = await ((_window2 = window) === null || _window2 === void 0 || (_window2 = _window2.__twpdeu) === null || _window2 === void 0 ? void 0 : _window2.push({
301
365
  submitData: {
302
366
  fieldName,
303
367
  value: selected
304
368
  },
305
- type: config !== null && config !== void 0 && config.explicit ? IngestType.EXPLICIT : IngestType.IMPLICIT,
306
369
  source
307
370
  }));
308
- const isError = result ? result.status !== subsSdk.ResponseStatus.SUCCESS : true;
371
+ const isError = result === true ? false : result ? result.status !== subsSdk.ResponseStatus.SUCCESS : true;
309
372
  onFinished({
310
373
  isFinished: true,
311
374
  isError
@@ -329,7 +392,8 @@ const DESelect = ({
329
392
  disabled: true
330
393
  } : {};
331
394
  // sort and filter out archived values
332
- const values = config ? config.values.sort((a, b) => a.order - b.order).filter(value => value.archived !== true).filter(valuesFilter) : [];
395
+ // Note: config.values may be readonly
396
+ const values = config ? [...config.values].sort((a, b) => a.order - b.order).filter(value => value.archived !== true).filter(valuesFilter) : [];
333
397
  return React.createElement(SelectWrapper, null, children && React.createElement(wpdsUiKit.Select.Root, {
334
398
  onValueChange: e => {
335
399
  setSelected(e);
@@ -371,12 +435,194 @@ const SelectWrapper = /*#__PURE__*/wpdsUiKit.styled('div', {
371
435
  }
372
436
  });
373
437
 
438
+ const configSrc = `${subsSdk.ENDPOINTS.base === 'https://subscribe.washingtonpost.com' ? 'https://www.washingtonpost.com/subscribe' : subsSdk.ENDPOINTS.base}/config/de/disclosure.json`;
439
+ const getConfig = async () => {
440
+ let myConfig = undefined;
441
+ // step 1: fetch config
442
+ const response = await fetch(configSrc);
443
+ const remoteConfig = await response.json();
444
+ // step 2: figure out which part of the config to use
445
+ // if country- or region-specific config found, use that
446
+ const {
447
+ country_code,
448
+ intl_region
449
+ } = subsSdk.WPGeo();
450
+ Object.keys(remoteConfig).forEach(configKey => {
451
+ if (country_code && configKey.split('|').includes(country_code.toLowerCase())) {
452
+ myConfig = remoteConfig[configKey];
453
+ } else if (intl_region === 'EEA' && configKey === 'eea') {
454
+ myConfig = remoteConfig[configKey];
455
+ }
456
+ });
457
+ // TODO: Check for billing country also
458
+ // else if no country-specific config, use the default config
459
+ if (typeof myConfig === 'undefined' && remoteConfig['_']) {
460
+ myConfig = remoteConfig['_'];
461
+ }
462
+ return myConfig;
463
+ };
464
+
465
+ const hydrateLinks = str => {
466
+ const array = str.split(/({{PRIVACY_POLICY}})/g);
467
+ const chunks = array.map(str => {
468
+ if (str === '{{PRIVACY_POLICY}}') {
469
+ return React.createElement("a", {
470
+ target: "_blank",
471
+ style: {
472
+ color: 'inherit'
473
+ },
474
+ className: "underline",
475
+ href: "https://www.washingtonpost.com/privacy-policy/"
476
+ }, "Privacy Policy");
477
+ }
478
+ return str;
479
+ });
480
+ const toReturn = chunks.reduce((prev, current) => React.createElement(React.Fragment, null, prev, current), React.createElement(React.Fragment, null));
481
+ return toReturn;
482
+ };
483
+
484
+ const COOKIE$1 = 'OptanonAlertBoxClosed';
485
+ const checkCookie = () => {
486
+ const value = subsSdk.getCookie(COOKIE$1) || '';
487
+ // Wed May 15 2024 06:29:23 GMT-0500 (Central Daylight Time)
488
+ // "Invalid date" is 12 characters long
489
+ return value.length > 12;
490
+ };
491
+
492
+ const COOKIE = 'OptanonAlertBoxClosed';
493
+ const useOneTrustAlertBoxClosed = ({
494
+ allowCookieStore
495
+ }) => {
496
+ const [alertBoxClosed, setAlertBoxClosed] = React.useState();
497
+ const [listenToCookieStore, setListenToCookieStore] = React.useState(false);
498
+ const [listenToTcfApi, setListenToTcfApi] = React.useState(false);
499
+ React.useEffect(() => {
500
+ var _window;
501
+ if (checkCookie()) {
502
+ setAlertBoxClosed(true);
503
+ return;
504
+ }
505
+ if (!window.__tcfapi) {
506
+ console.warn('warning: __tcfapi not found');
507
+ }
508
+ if ((_window = window) !== null && _window !== void 0 && _window.cookieStore && allowCookieStore) {
509
+ setListenToCookieStore(true);
510
+ } else if (window.__tcfapi) {
511
+ setListenToTcfApi(true);
512
+ } else {
513
+ console.warn('warning: neither cookieStore nor __tcfapi found');
514
+ }
515
+ }, []);
516
+ React.useEffect(() => {
517
+ let cleanupFn = () => {};
518
+ if (listenToCookieStore && window.cookieStore) {
519
+ cleanupFn = subsSdk.listenToCookieStore(COOKIE, () => {
520
+ if (checkCookie()) {
521
+ setAlertBoxClosed(true);
522
+ }
523
+ });
524
+ }
525
+ return cleanupFn || (() => {});
526
+ }, [listenToCookieStore]);
527
+ React.useEffect(() => {
528
+ let listenerId;
529
+ if (listenToTcfApi && window.__tcfapi) {
530
+ const callback = (_tcData, success) => {
531
+ if (success) {
532
+ listenerId = _tcData.listenerId;
533
+ // tcData.eventStatus can be:
534
+ // tcloaded means user has made a choice and we’re ready to check it
535
+ // cmpuishown means the banner is shown
536
+ // useractioncomplete means the user has interacted with the banner
537
+ // but actually if the result for any of these is true, we just use the value of the cookie
538
+ if (checkCookie()) {
539
+ setAlertBoxClosed(true);
540
+ }
541
+ }
542
+ };
543
+ window.__tcfapi('addEventListener', 2, callback);
544
+ }
545
+ // cleanup fn
546
+ return () => {
547
+ if (window.__tcfapi && listenerId) window.__tcfapi('removeEventListener', 2, success => {
548
+ console.debug(success);
549
+ }, listenerId);
550
+ };
551
+ }, [listenToTcfApi]);
552
+ return {
553
+ alertBoxClosed,
554
+ listenToCookieStore,
555
+ listenToTcfApi
556
+ };
557
+ };
558
+
559
+ const DEDisclosure = ({
560
+ onFinished = () => {},
561
+ allowCookieStore = true
562
+ }) => {
563
+ const [disclosure, setDisclosure] = React.useState(null);
564
+ const [disclosureRendering, setDisclosureRendering] = React.useState(null);
565
+ const [myConfig, setMyConfig] = React.useState();
566
+ const {
567
+ alertBoxClosed
568
+ } = useOneTrustAlertBoxClosed({
569
+ allowCookieStore
570
+ });
571
+ React.useEffect(() => {
572
+ (async () => {
573
+ const config = await getConfig();
574
+ setMyConfig(config);
575
+ if (!config) {
576
+ console.error('No config found');
577
+ }
578
+ })();
579
+ }, []);
580
+ React.useEffect(() => {
581
+ if (myConfig) {
582
+ // step 3: set disclosure based on config
583
+ // if config says to check onetrust, check onetrust
584
+ if ('checkBannerStatus' in myConfig && myConfig.checkBannerStatus) {
585
+ // check if onetrust is closed
586
+ // if it is, show the after banner disclosure
587
+ // if it is not, show the before banner disclosure
588
+ if (alertBoxClosed) {
589
+ setDisclosure(myConfig.disclosure_afterbanner);
590
+ } else {
591
+ setDisclosure(myConfig.disclosure_beforebanner);
592
+ }
593
+ } else if ('disclosure' in myConfig) {
594
+ setDisclosure(myConfig.disclosure);
595
+ } else {
596
+ console.error('Invalid config');
597
+ }
598
+ }
599
+ }, [myConfig, alertBoxClosed]);
600
+ React.useEffect(() => {
601
+ if (disclosure && Array.isArray(disclosure)) {
602
+ setDisclosureRendering(disclosure.reduce((prev, current) => {
603
+ return React.createElement(React.Fragment, null, prev, React.createElement("p", null, hydrateLinks(current)));
604
+ }, React.createElement(React.Fragment, null)));
605
+ // Is it ok to fire `onFinished` if still waiting for onetrust to load on the page?
606
+ onFinished({
607
+ isFinished: true,
608
+ isError: false
609
+ });
610
+ }
611
+ }, [disclosure]);
612
+ return disclosure === null ? React.createElement("div", {
613
+ "data-test-id": "de-disclosure-loading"
614
+ }) : React.createElement("div", {
615
+ "data-test-id": "de-disclosure"
616
+ }, disclosureRendering);
617
+ };
618
+
374
619
  exports.AttributesState = AttributesState;
375
620
  exports.CollectionBehaviors = CollectionBehaviors;
621
+ exports.DEDisclosure = DEDisclosure;
376
622
  exports.DESelect = DESelect;
377
623
  exports.IngestResponseState = IngestResponseState;
378
624
  exports.IngestType = IngestType;
379
625
  exports.getAttributes = getAttributes;
380
626
  exports.hasRequiredPrivacyCookies = hasRequiredPrivacyCookies;
381
- exports.ingest = ingest;
627
+ exports.push = push;
382
628
  //# sourceMappingURL=subs-de-inputs.cjs.development.js.map