@mparticle/web-sdk 3.24.0 → 3.26.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.
@@ -4,6 +4,9 @@ import { BaseEvent } from '../sdkRuntimeModels';
4
4
  import { IButtonClick } from './buttonDescription';
5
5
  export declare const AUTO_BUTTON_ATTRIBUTE = "is_auto_button_click";
6
6
  export declare const BUTTON_CLICK_EVENT_NAME = "Auto Button Click";
7
+ export declare const AUTO_LINK_ATTRIBUTE = "is_auto_link_click";
8
+ export declare const LINK_CLICK_EVENT_NAME = "Auto Link Click";
7
9
  export declare const buildButtonClickEvent: (click: IButtonClick, pathname: string) => BaseEvent;
10
+ export declare const buildLinkClickEvent: (click: IButtonClick, pathname: string) => BaseEvent;
8
11
  export type ButtonClickBlocker = 'data plan' | 'forwarding rule' | 'event name filter' | 'event type filter';
9
- export declare const getButtonClickBlocker: (forwarder: IKitFilterSettings, kitBlocker?: KitBlocker) => ButtonClickBlocker | undefined;
12
+ export declare const getButtonClickBlocker: (forwarder: IKitFilterSettings, kitBlocker?: KitBlocker, link?: boolean) => ButtonClickBlocker | undefined;
@@ -1,5 +1,5 @@
1
1
  export type ButtonElementKind = 'button' | 'input' | 'role' | 'wallet' | 'link';
2
- export type LinkType = 'same_page' | 'outbound' | 'download';
2
+ export type LinkType = 'same_page' | 'internal' | 'outbound' | 'download';
3
3
  export type ButtonLabelSource = 'labelledby' | 'aria' | 'text' | 'alt' | 'title' | 'value' | 'none';
4
4
  export interface IButtonClick {
5
5
  label?: string;
@@ -18,4 +18,11 @@ export interface IButtonClick {
18
18
  fileExtension?: string;
19
19
  checked?: boolean;
20
20
  }
21
+ export declare function isLinkClick(click: IButtonClick): boolean;
21
22
  export declare function describeButtonClick(control: Element, path?: Array<EventTarget | null>): IButtonClick | undefined;
23
+ export interface IPaymentChoice {
24
+ radio: Element;
25
+ names: string[];
26
+ }
27
+ export declare function describePaymentChoice(control: Element, path?: Array<EventTarget | null>): IPaymentChoice | undefined;
28
+ export declare function isPicked(radio: Element): boolean;
@@ -2,6 +2,8 @@ import { IMParticleWebSDKInstance } from '../mp-instance';
2
2
  export declare const WIN_BTN_KEY = "__mpBtn__";
3
3
  export interface IButtonTrackerOptions {
4
4
  hasRoktKit: boolean;
5
+ logButtons: boolean;
6
+ logLinks: boolean;
5
7
  }
6
8
  export declare const getActiveButtonTracker: () => ButtonTracker | undefined;
7
9
  export declare const resetButtonTracking: () => void;
@@ -9,6 +11,8 @@ export declare class ButtonTracker {
9
11
  private readonly mpInstance;
10
12
  private active;
11
13
  private hasRoktKit;
14
+ private logButtons;
15
+ private logLinks;
12
16
  private generation;
13
17
  private lastCommitClick;
14
18
  private lastClick;
@@ -37,7 +41,9 @@ export declare class ButtonTracker {
37
41
  private uploadNow;
38
42
  private cancelUpload;
39
43
  private emit;
44
+ private pickPayment;
40
45
  private eventsEnabled;
46
+ private hasTargetingOptOut;
41
47
  private isKitSuppressed;
42
48
  private log;
43
49
  }
@@ -1,9 +1,15 @@
1
1
  export type CheckoutIntentKind = 'commit' | 'wallet' | 'checkout_step' | 'frame' | 'cart';
2
+ export type PaymentFrameFamily = 'apple_pay' | 'google_pay' | 'amazon_pay' | 'shop_pay' | 'card';
2
3
  export type CheckoutIntentSource = 'vendor' | 'label' | 'brand' | 'frame' | 'submit' | 'name' | 'api';
3
4
  export interface ICheckoutIntent {
4
5
  kind: Exclude<CheckoutIntentKind, 'frame'>;
5
6
  source: CheckoutIntentSource;
7
+ adds?: true;
6
8
  }
7
9
  export declare function classifyIntent(control: Element, pathname: string): ICheckoutIntent | undefined;
8
10
  export declare function classifyIntentControl(control: Element, pathname: string): Exclude<CheckoutIntentKind, 'frame'> | undefined;
9
- export declare function classifyIntentFrame(target: EventTarget | null, win: Window): 'wallet' | 'frame' | undefined;
11
+ export interface IPaymentFrame {
12
+ kind: 'wallet' | 'frame';
13
+ family?: PaymentFrameFamily;
14
+ }
15
+ export declare function classifyPaymentFrame(target: EventTarget | null, win: Window): IPaymentFrame | undefined;
@@ -3,10 +3,11 @@ export declare function someComposedAncestor(element: Element, test: (node: Elem
3
3
  export declare function hasComposedAncestor(element: Element, selector: string): boolean;
4
4
  export declare function isRoktElement(element: Element): boolean;
5
5
  export declare function vendorButtonKind(control: Element): 'wallet' | 'checkout_step' | 'none' | undefined;
6
+ export declare function nameWords(value: string | null): string;
6
7
  export declare function isWalletNamed(element: Element): boolean;
7
8
  export declare function isWalletElement(element: Element): boolean;
8
9
  export declare function isQuietIntentControl(control: Element, isVendorWallet: boolean, isCart?: boolean): boolean;
9
10
  export declare function isWalletNamedControl(element: Element): boolean;
10
11
  export declare function isExcludedPath(path: Array<EventTarget | null>): boolean;
11
12
  export declare function findControl(path: Array<EventTarget | null>, included?: boolean): Element | undefined;
12
- export declare function isUnsafeTextSource(element: Element): boolean;
13
+ export declare function isUnsafeTextSource(element: Element, own?: Element): boolean;
@@ -163,6 +163,7 @@ declare const Constants: {
163
163
  readonly AstBackgroundEvents: "astBackgroundEvents";
164
164
  readonly AutoLogPageView: "autoLogPageView";
165
165
  readonly AutoButtonTracking: "autoButtonTracking";
166
+ readonly AutoLinkTracking: "autoLinkTracking";
166
167
  };
167
168
  readonly DefaultInstance: "default_instance";
168
169
  readonly CCPAPurpose: "data_sale_opt_out";
@@ -15,6 +15,11 @@ export interface ICookieConsentFlags {
15
15
  * Default: false (targeting is allowed)
16
16
  */
17
17
  noTargeting: boolean;
18
+ /**
19
+ * When true, indicates the user has opted out of the sale or sharing of their data.
20
+ * Default: false (sharing is allowed)
21
+ */
22
+ doNotShareOrSell?: boolean;
18
23
  }
19
24
  export interface ICookieConsentManager {
20
25
  /**
@@ -25,6 +30,10 @@ export interface ICookieConsentManager {
25
30
  * Functional tracking is allowed when noFunctional is false (default)
26
31
  */
27
32
  getNoFunctional: () => boolean;
33
+ /**
34
+ * Sharing is allowed when doNotShareOrSell is false (default)
35
+ */
36
+ getDoNotShareOrSell: () => boolean;
28
37
  /**
29
38
  * Syncs the $NoTargeting user attribute based on the current noTargeting flag.
30
39
  * Sets the attribute when noTargeting is true, removes it when false.
@@ -32,12 +41,12 @@ export interface ICookieConsentManager {
32
41
  syncNoTargetingAttribute: (user: IMParticleUser | null) => void;
33
42
  }
34
43
  /**
35
- * CookieConsentManager handles storage and access of consent flags (noFunctional, noTargeting)
36
- * that are passed via launcherOptions during SDK initialization.
44
+ * CookieConsentManager handles storage and access of consent flags (noFunctional, noTargeting,
45
+ * doNotShareOrSell) that are passed via launcherOptions during SDK initialization.
37
46
  *
38
47
  * These flags allow Rokt integration to respect user privacy choices.
39
48
  *
40
- * Default behavior: Both flags default to false, meaning all features are allowed
49
+ * Default behavior: All flags default to false, meaning all features are allowed
41
50
  * unless explicitly opted out by the user.
42
51
  */
43
52
  export default class CookieConsentManager implements ICookieConsentManager {
@@ -53,5 +62,10 @@ export default class CookieConsentManager implements ICookieConsentManager {
53
62
  * Functional tracking is allowed when noFunctional is false (default).
54
63
  */
55
64
  getNoFunctional(): boolean;
65
+ /**
66
+ * Returns true if the user has opted out of the sale or sharing of their data.
67
+ * Sharing is allowed when doNotShareOrSell is false (default).
68
+ */
69
+ getDoNotShareOrSell(): boolean;
56
70
  syncNoTargetingAttribute(user: IMParticleUser | null): void;
57
71
  }
@@ -4,6 +4,7 @@ export interface IRoktLauncherOptions extends Dictionary<any> {
4
4
  noDeviceID?: boolean;
5
5
  noFunctional?: boolean;
6
6
  noTargeting?: boolean;
7
+ doNotShareOrSell?: boolean;
7
8
  sessionId?: string;
8
9
  sessionToken?: string;
9
10
  }
@@ -9,7 +9,7 @@ import { MPID } from "@mparticle/web-sdk";
9
9
  import { IErrorReportingService, ILoggingService } from "./reporting/types";
10
10
  import { IRoktLauncherOptions } from "./roktLauncherOptions";
11
11
  import IntegrationCapture from "./integrationCapture";
12
- import type { CheckoutIntentKind, CheckoutIntentSource } from "./buttonTracking/checkoutIntent";
12
+ import type { CheckoutIntentKind, CheckoutIntentSource, PaymentFrameFamily } from "./buttonTracking/checkoutIntent";
13
13
  export type RoktAttributeValueArray = Array<string | number | boolean>;
14
14
  export type RoktAttributeValueType = string | number | boolean | undefined | null;
15
15
  export type RoktAttributeValue = RoktAttributeValueType | RoktAttributeValueArray;
@@ -49,6 +49,13 @@ export interface ICheckoutIntentSignal {
49
49
  t: number;
50
50
  source?: CheckoutIntentSource;
51
51
  pathname?: string;
52
+ adds?: true;
53
+ }
54
+ export interface IPaymentPickSignal {
55
+ t: number;
56
+ pathname: string;
57
+ names?: readonly string[];
58
+ family?: PaymentFrameFamily;
52
59
  }
53
60
  export interface IRoktKitSettings {
54
61
  accountId?: string;
@@ -68,6 +75,7 @@ export interface IRoktKit {
68
75
  terminate?: () => Promise<void>;
69
76
  onRouteChange?: () => void;
70
77
  onCheckoutIntent?: (signal: ICheckoutIntentSignal) => void;
78
+ onPaymentPick?: (signal: IPaymentPickSignal) => void;
71
79
  launcherOptions?: Dictionary<any>;
72
80
  settings?: IRoktKitSettings;
73
81
  integrationName?: string;
@@ -129,10 +137,12 @@ export default class RoktManager {
129
137
  private watchRouteChanges;
130
138
  private notifyRouteChange;
131
139
  setAutoButtonClicksForwarded(forwarded: boolean): void;
140
+ hasTargetingOptOut(): boolean;
132
141
  isCheckoutIntentSuppressed(): boolean;
133
142
  isCheckoutIntentLive(): boolean;
134
143
  notifyCheckoutIntent(signal: ICheckoutIntentSignal, pathname?: string): void;
135
144
  discardCheckoutIntents(): void;
145
+ notifyPaymentPick(signal: IPaymentPickSignal): void;
136
146
  private flushCheckoutIntents;
137
147
  isAutoLogPageViewEnabled(): boolean;
138
148
  /**
@@ -82,6 +82,7 @@ export interface IFeatureFlags {
82
82
  astBackgroundEvents?: boolean;
83
83
  autoLogPageView?: boolean;
84
84
  autoButtonTracking?: boolean;
85
+ autoLinkTracking?: boolean;
85
86
  }
86
87
  export interface IStore {
87
88
  isEnabled: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "3.24.0",
3
+ "version": "3.26.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -12,22 +12,25 @@ export const AUTO_BUTTON_ATTRIBUTE = 'is_auto_button_click';
12
12
  // Specific enough not to merge with a customer's own 'Button Click' event.
13
13
  export const BUTTON_CLICK_EVENT_NAME = 'Auto Button Click';
14
14
 
15
- // One custom event per click, logged like logLink() with the auto marker and the
16
- // page fields that auto page views carry. No intent: classifying clicks is left
17
- // to whoever reads the data.
18
- export const buildButtonClickEvent = (
15
+ // Marks an automatically logged click on a link that leaves the page.
16
+ export const AUTO_LINK_ATTRIBUTE = 'is_auto_link_click';
17
+
18
+ export const LINK_CLICK_EVENT_NAME = 'Auto Link Click';
19
+
20
+ // The attributes both click events carry, with the page fields that auto page
21
+ // views carry. No intent: classifying clicks is left to whoever reads the data.
22
+ const clickAttributes = (
19
23
  click: IButtonClick,
20
24
  pathname: string
21
- ): BaseEvent => {
25
+ ): { [key: string]: string | boolean } => {
22
26
  const data: { [key: string]: string | boolean } = {
23
27
  label_source: click.labelSource,
24
28
  element: click.element,
25
29
  type: click.type,
26
30
  path: pathname,
27
31
  hostname: window.location.hostname,
28
- [AUTO_BUTTON_ATTRIBUTE]: true,
29
32
  };
30
- // Optional fields are left out when the button has no safe value for them.
33
+ // Optional fields are left out when the control has no safe value for them.
31
34
  const optional: { [key: string]: string | undefined } = {
32
35
  label: click.label,
33
36
  aria_label: click.ariaLabel,
@@ -49,6 +52,16 @@ export const buildButtonClickEvent = (
49
52
  if (click.checked !== undefined) {
50
53
  data.checked = click.checked;
51
54
  }
55
+ return data;
56
+ };
57
+
58
+ // One custom event per button click, logged like logLink() with the auto marker.
59
+ export const buildButtonClickEvent = (
60
+ click: IButtonClick,
61
+ pathname: string
62
+ ): BaseEvent => {
63
+ const data = clickAttributes(click, pathname);
64
+ data[AUTO_BUTTON_ATTRIBUTE] = true;
52
65
  return {
53
66
  messageType: MessageType.PageEvent,
54
67
  name: BUTTON_CLICK_EVENT_NAME,
@@ -57,6 +70,34 @@ export const buildButtonClickEvent = (
57
70
  };
58
71
  };
59
72
 
73
+ // One custom event per click on a link that leaves the page. Typed Other, not
74
+ // Navigation, so no kit reads it as a page change. A plain link to another page
75
+ // of the site goes without its text, which can name the shopper ("Hi, Jane");
76
+ // a button or wallet acting as a link keeps its name.
77
+ export const buildLinkClickEvent = (
78
+ click: IButtonClick,
79
+ pathname: string
80
+ ): BaseEvent => {
81
+ const data = clickAttributes(
82
+ click.linkType === 'internal' && click.element === 'link'
83
+ ? {
84
+ ...click,
85
+ label: undefined,
86
+ labelSource: 'none',
87
+ ariaLabel: undefined,
88
+ }
89
+ : click,
90
+ pathname
91
+ );
92
+ data[AUTO_LINK_ATTRIBUTE] = true;
93
+ return {
94
+ messageType: MessageType.PageEvent,
95
+ name: LINK_CLICK_EVENT_NAME,
96
+ data,
97
+ eventType: EventType.Other,
98
+ };
99
+ };
100
+
60
101
  // Every attribute buildButtonClickEvent can set apart from the marker and the
61
102
  // hostname, which is the same on every click on this page. A spec checks the
62
103
  // two stay in step.
@@ -85,14 +126,15 @@ export type ButtonClickBlocker =
85
126
  | 'event name filter'
86
127
  | 'event type filter';
87
128
 
88
- // Which check keeps some or all auto button clicks from a kit, or undefined
89
- // when the kit receives every one. A forwarding rule on an attribute whose
90
- // value differs between clicks lets some through and not others. Each other
91
- // check goes through isEventForwarded on its own, so together they decide as
92
- // it does for each click.
129
+ // Which check keeps some or all auto button clicks, or auto link clicks with
130
+ // `link`, from a kit, or undefined when the kit receives every one. A forwarding
131
+ // rule on an attribute whose value differs between clicks lets some through and
132
+ // not others. Each other check goes through isEventForwarded on its own, so
133
+ // together they decide as it does for each click.
93
134
  export const getButtonClickBlocker = (
94
135
  forwarder: IKitFilterSettings,
95
- kitBlocker?: KitBlocker
136
+ kitBlocker?: KitBlocker,
137
+ link = false
96
138
  ): ButtonClickBlocker | undefined => {
97
139
  const rule = forwarder.filteringEventAttributeValue;
98
140
  if (
@@ -106,14 +148,15 @@ export const getButtonClickBlocker = (
106
148
  ) {
107
149
  return 'forwarding rule';
108
150
  }
151
+ const attributes: { [key: string]: string | boolean } = {
152
+ hostname: window.location.hostname,
153
+ };
154
+ attributes[link ? AUTO_LINK_ATTRIBUTE : AUTO_BUTTON_ATTRIBUTE] = true;
109
155
  const click = ({
110
- EventName: BUTTON_CLICK_EVENT_NAME,
156
+ EventName: link ? LINK_CLICK_EVENT_NAME : BUTTON_CLICK_EVENT_NAME,
111
157
  EventCategory: EventType.Other,
112
158
  EventDataType: MessageType.PageEvent,
113
- EventAttributes: {
114
- [AUTO_BUTTON_ATTRIBUTE]: true,
115
- hostname: window.location.hostname,
116
- },
159
+ EventAttributes: attributes,
117
160
  } as unknown) as SDKEvent;
118
161
  const checks: Array<[ButtonClickBlocker, Partial<IKitFilterSettings>]> = [
119
162
  ['data plan', {}],
@@ -3,6 +3,7 @@ import {
3
3
  hasComposedAncestor,
4
4
  isUnsafeTextSource,
5
5
  isWalletElement,
6
+ nameWords,
6
7
  someComposedAncestor,
7
8
  } from './controls';
8
9
  import { maskLabel, MAX_RAW_LABEL_LENGTH } from './labelMasking';
@@ -11,8 +12,9 @@ import { maskLabel, MAX_RAW_LABEL_LENGTH } from './labelMasking';
11
12
  // hand-written identifiers and kind. Never reads form field values.
12
13
  export type ButtonElementKind = 'button' | 'input' | 'role' | 'wallet' | 'link';
13
14
 
14
- // The link clicks page views can't see: ones that stay on the page, leave the site or download a file.
15
- export type LinkType = 'same_page' | 'outbound' | 'download';
15
+ // A same_page link acts as a button. The others leave the page: to another page
16
+ // of the site, another site or a file.
17
+ export type LinkType = 'same_page' | 'internal' | 'outbound' | 'download';
16
18
 
17
19
  const DOWNLOAD_EXTENSIONS = /\.(pdf|zip|csv|txt|ics|docx?|xlsx?|pptx?|dmg|exe|apk|mp3|mp4)$/i;
18
20
 
@@ -392,8 +394,8 @@ interface ILinkDetails {
392
394
  }
393
395
 
394
396
  // Reads the link's own resolved URL parts, never its full href: only the hostname of
395
- // a link that leaves the site and the extension of a file it downloads travel. Navigation
396
- // within the site is left to page views, and mailto:, tel: and other schemes are skipped.
397
+ // a link that leaves the site and the extension of a file it downloads travel.
398
+ // mailto:, tel: and other schemes give undefined.
397
399
  function describeLink(link: Element): ILinkDetails | undefined {
398
400
  const href = (link.getAttribute('href') || '').trim();
399
401
  if (!href || href.charAt(0) === '#') {
@@ -427,7 +429,13 @@ function describeLink(link: Element): ILinkDetails | undefined {
427
429
  return link.pathname === window.location.pathname &&
428
430
  link.search === window.location.search
429
431
  ? { type: 'same_page' }
430
- : undefined;
432
+ : { type: 'internal' };
433
+ }
434
+
435
+ // Whether a described click is on a link that leaves the page, logged as a link
436
+ // click rather than a button click.
437
+ export function isLinkClick(click: IButtonClick): boolean {
438
+ return !!click.linkType && click.linkType !== 'same_page';
431
439
  }
432
440
 
433
441
  // Checkboxes and radios, and the roles that act as them. A role can list
@@ -442,6 +450,12 @@ function boxOf(element: Element): Element | null {
442
450
  return box && box.matches(CHOICE) ? box : null;
443
451
  }
444
452
 
453
+ function isRadio(choice: Element | null): boolean {
454
+ return choice instanceof HTMLInputElement
455
+ ? choice.type === 'radio'
456
+ : !!choice && choice.matches(RADIO_ROLE);
457
+ }
458
+
445
459
  // The box a click on this control picks. One the click passed through below
446
460
  // the control comes first: querySelector can't reach a slotted box, a box in a
447
461
  // shadow root or the box of a label inside the control, and a row can hold
@@ -463,8 +477,9 @@ function choiceOf(
463
477
  return boxOf(control) || control.querySelector(CHOICE);
464
478
  }
465
479
 
466
- // Describes a clicked control for auto button tracking. A link counts only when page
467
- // views can't see the click, and carries its link type.
480
+ // Describes a clicked control for auto button and link tracking. A link carries its
481
+ // link type, and one to another scheme is skipped. A role="button" anchor carries one
482
+ // only when it leaves the page; otherwise it stays a plain button.
468
483
  export function describeButtonClick(
469
484
  control: Element,
470
485
  path: Array<EventTarget | null> = []
@@ -475,6 +490,11 @@ export function describeButtonClick(
475
490
  if (!link) {
476
491
  return undefined;
477
492
  }
493
+ } else if (control.matches('a[href]')) {
494
+ link = describeLink(control);
495
+ if (link && link.type === 'same_page') {
496
+ link = undefined;
497
+ }
478
498
  }
479
499
  const element = elementKind(control);
480
500
  const box =
@@ -492,10 +512,7 @@ export function describeButtonClick(
492
512
  // radio's id, test id and the panel it opens name the option picked, so
493
513
  // only the name of its input, the question, goes with it. A wrapper's own
494
514
  // name, or a role radio's, can name the option instead.
495
- const radio =
496
- choice instanceof HTMLInputElement
497
- ? choice.type === 'radio'
498
- : !!choice && choice.matches(RADIO_ROLE);
515
+ const radio = isRadio(choice);
499
516
  const named = !radio
500
517
  ? control
501
518
  : choice instanceof HTMLInputElement
@@ -531,3 +548,79 @@ export function describeButtonClick(
531
548
  checked: box ? box.checked : undefined,
532
549
  };
533
550
  }
551
+
552
+ // A question about payment, read from the names its author gave it, so
553
+ // "checkout[payment_gateway]", "paymentMethod" and "tender_type" all count and
554
+ // "payee" does not. A radio's input name is its question; its group or
555
+ // fieldset speaks only for a radio with no name, as a role radio usually is.
556
+ const PAYMENT_QUESTION = /(?:^| )(?:pay|payment\w*|tenders?|gateways?)(?: |$)/;
557
+ const QUESTION_GROUP = '[role="radiogroup"], fieldset';
558
+
559
+ function isPaymentQuestion(radio: Element): boolean {
560
+ const name = radio.getAttribute('name');
561
+ const group = name ? null : radio.closest(QUESTION_GROUP);
562
+ const question = group
563
+ ? [
564
+ group.getAttribute('id'),
565
+ group.getAttribute('name'),
566
+ group.getAttribute('aria-label'),
567
+ ]
568
+ : [name];
569
+ return question.some(words => PAYMENT_QUESTION.test(nameWords(words)));
570
+ }
571
+
572
+ // What names a payment option: the radio's accessible name, the text and
573
+ // image of each label, and its hand-written ids, text masked like a label.
574
+ function optionNames(radio: Element): string[] {
575
+ const names: Array<string | undefined> = [
576
+ maskLabel(accessibleName(radio).raw),
577
+ ];
578
+ const labels =
579
+ radio instanceof HTMLInputElement && radio.labels
580
+ ? Array.from(radio.labels).slice(0, 2)
581
+ : [];
582
+ labels.forEach(label => {
583
+ if (isShown(label) && !isUnsafeTextSource(label, radio)) {
584
+ names.push(maskLabel(renderedText(label)));
585
+ names.push(maskLabel(shownImageAlt(label)));
586
+ }
587
+ });
588
+ names.push(
589
+ stableIdentifier(radio.getAttribute('id')),
590
+ firstIdentifier(radio, TEST_ID_ATTRIBUTES)
591
+ );
592
+ return names.filter(
593
+ (name, index) => !!name && names.indexOf(name) === index
594
+ );
595
+ }
596
+
597
+ // The payment radio a click picks and the names of its option, for the Rokt
598
+ // kit alone to read a payment method from. A radio's option is the shopper's
599
+ // answer, so these names never go into the logged click.
600
+ export interface IPaymentChoice {
601
+ radio: Element;
602
+ names: string[];
603
+ }
604
+
605
+ export function describePaymentChoice(
606
+ control: Element,
607
+ path: Array<EventTarget | null> = []
608
+ ): IPaymentChoice | undefined {
609
+ const choice = choiceOf(control, path);
610
+ if (
611
+ !isRadio(choice) ||
612
+ hasComposedAncestor(choice, '[data-mp-no-track]') ||
613
+ !isPaymentQuestion(choice)
614
+ ) {
615
+ return undefined;
616
+ }
617
+ const names = optionNames(choice);
618
+ return names.length ? { radio: choice, names } : undefined;
619
+ }
620
+
621
+ // Read once the page has handled the click, which may have cancelled it.
622
+ export function isPicked(radio: Element): boolean {
623
+ return radio instanceof HTMLInputElement
624
+ ? radio.checked
625
+ : radio.getAttribute('aria-checked') === 'true';
626
+ }