@mparticle/web-sdk 3.28.0 → 3.29.1

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.
@@ -3,6 +3,7 @@ import { isFunction } from '../utils';
3
3
  import { buildButtonClickEvent, buildLinkClickEvent } from './buttonClickEvent';
4
4
  import {
5
5
  describeButtonClick,
6
+ describeCheckedPaymentChoice,
6
7
  describePaymentChoice,
7
8
  IButtonClick,
8
9
  IPaymentChoice,
@@ -14,6 +15,7 @@ import {
14
15
  CheckoutIntentSource,
15
16
  classifyIntent,
16
17
  classifyPaymentFrame,
18
+ namesWallet,
17
19
  PaymentFrameFamily,
18
20
  } from './checkoutIntent';
19
21
  import { findControl, findRoleControl, isExcludedPath } from './controls';
@@ -160,8 +162,9 @@ export class ButtonTracker {
160
162
  // Bumped on reset so classification queued before it is dropped.
161
163
  private generation = 0;
162
164
  private lastCommitClick = -Infinity;
163
- // The latest trusted click: whether the page excluded it, and the intent it
164
- // already carries to the kit, with its source, if any.
165
+ // The latest trusted click: whether the page excluded it or it was withheld
166
+ // from the kit, and the intent it already carries to the kit, with its
167
+ // source, if any.
165
168
  private lastClick: {
166
169
  t: number;
167
170
  excluded: boolean;
@@ -390,6 +393,7 @@ export class ButtonTracker {
390
393
  let source: CheckoutIntentSource | undefined;
391
394
  let adds: true | undefined;
392
395
  let choice: IPaymentChoice | undefined;
396
+ let checkedNames: string[] | undefined;
393
397
  let family: PaymentFrameFamily | undefined;
394
398
  let unconfirmedCommit = false;
395
399
  if (layer === 'click') {
@@ -405,15 +409,26 @@ export class ButtonTracker {
405
409
  ? undefined
406
410
  : findControl(path, true, PATH_LIMIT) ||
407
411
  findRoleControl(path, PATH_LIMIT));
412
+ // A click that link tracking logs as Auto Link Click gives the kit no
413
+ // intent when the Rokt connection does not receive those events.
414
+ let withheld = false;
408
415
  if (logged) {
409
416
  // With links alone, only an anchor can be logged.
410
- click =
417
+ const logsThis =
411
418
  logsEvents &&
412
- (this.logButtons || logged.matches('a[href]'))
419
+ (this.logButtons || logged.matches('a[href]'));
420
+ const linksWithheld =
421
+ signalsKit && this.logLinks && !this.linkClicksReachKit();
422
+ const described =
423
+ logsThis || linksWithheld
413
424
  ? describeButtonClick(logged, path)
414
425
  : undefined;
426
+ const linkClick =
427
+ this.logLinks && !!described && isLinkClick(described);
428
+ withheld = linksWithheld && linkClick;
429
+ click = logsThis ? described : undefined;
415
430
  if (click) {
416
- asLink = this.logLinks && isLinkClick(click);
431
+ asLink = linkClick;
417
432
  // Without link tracking, a link that leaves the page stays a
418
433
  // button click, except a plain link to another page of the
419
434
  // site, which button tracking never logged.
@@ -432,7 +447,7 @@ export class ButtonTracker {
432
447
  box = logged as HTMLInputElement;
433
448
  }
434
449
  }
435
- if (control) {
450
+ if (control && !withheld) {
436
451
  // Checkout intent is only worked out when the kit can use it.
437
452
  const intent = signalsKit
438
453
  ? classifyIntent(control, pathname)
@@ -443,8 +458,28 @@ export class ButtonTracker {
443
458
  choice = signalsKit
444
459
  ? describePaymentChoice(control, path)
445
460
  : undefined;
461
+ // A confirm or checkout-step click picks no radio itself, so it
462
+ // carries the one the page holds checked as read here. It is not
463
+ // read again at delivery: the page's own handlers may clear or
464
+ // replace it by then. A wallet tap names its own method, and
465
+ // so does a confirm that names a wallet.
466
+ if (
467
+ !choice &&
468
+ (kind === 'commit' || kind === 'checkout_step') &&
469
+ !namesWallet(control, click)
470
+ ) {
471
+ const checked = describeCheckedPaymentChoice();
472
+ checkedNames = checked ? checked.names : undefined;
473
+ }
446
474
  }
447
- this.lastClick = { t, excluded, kind, source };
475
+ // A withheld click cannot start an Apple Pay session for the kit
476
+ // either.
477
+ this.lastClick = {
478
+ t,
479
+ excluded: excluded || withheld,
480
+ kind,
481
+ source,
482
+ };
448
483
  } else if (layer === 'submit') {
449
484
  const submitter = (event as Event & { submitter?: Element | null })
450
485
  .submitter;
@@ -503,6 +538,8 @@ export class ButtonTracker {
503
538
  // with this pick.
504
539
  if (choice && isPicked(choice.radio)) {
505
540
  this.pickPayment(t, pathname, choice.names);
541
+ } else if (checkedNames) {
542
+ this.pickPayment(t, pathname, checkedNames);
506
543
  } else if (family) {
507
544
  this.pickPayment(t, pathname, undefined, family);
508
545
  }
@@ -625,6 +662,11 @@ export class ButtonTracker {
625
662
  );
626
663
  }
627
664
 
665
+ private linkClicksReachKit(): boolean {
666
+ const roktManager = this.mpInstance._RoktManager;
667
+ return !!roktManager && roktManager.areAutoLinkClicksForwarded();
668
+ }
669
+
628
670
  private log(message: string): void {
629
671
  this.mpInstance.Logger.verbose('mParticle button tracking: ' + message);
630
672
  }
@@ -7,6 +7,7 @@ import {
7
7
  isWalletNamedControl,
8
8
  vendorButtonKind,
9
9
  } from './controls';
10
+ import { IButtonClick, TEST_ID_ATTRIBUTES } from './buttonDescription';
10
11
 
11
12
  // Classifies a click, submit or frame focus as checkout intent for the Rokt kit.
12
13
  // Reads only short control labels, never form field values, and stores nothing.
@@ -126,6 +127,9 @@ const walletBrands = new RegExp(
126
127
  '^(?:' + WALLET_NAMES + ')(?: (?:checkout|check out|express(?: checkout)?|express pay|buy now|pay now|pay|now)' +
127
128
  '| (?:logo|icon|mark|button)(?: (?:logo|icon|mark|button))*)?$'
128
129
  );
130
+ // The wallets the Rokt kit reads from a click's label or identifiers, as words:
131
+ // "Place order with PayPal!" and "paypal-pay-button" both name PayPal.
132
+ const KIT_WALLET_WORDS = /(?:^| )(?:apple ?pay|google ?pay|g ?pay|android ?pay|venmo|pay ?pal|amazon ?pay|shop ?pay|klarna|after ?pay|clear ?pay|affirm|cash ?app|sezzle|samsung ?pay)(?= |$)/;
129
133
  // The payment method a frame's focus picks, in the Rokt kit's family names.
130
134
  export type PaymentFrameFamily =
131
135
  | 'apple_pay'
@@ -393,6 +397,38 @@ export function classifyIntent(
393
397
  return cart;
394
398
  }
395
399
 
400
+ // The Rokt kit's own word split, so "PayPal!" and "paypal2" both read as
401
+ // "paypal" here as they do there.
402
+ function kitWords(value: string | null | undefined): string {
403
+ return (value || '')
404
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
405
+ .toLowerCase()
406
+ .replace(/[^a-z]+/g, ' ')
407
+ .trim();
408
+ }
409
+
410
+ // A confirm control may name its wallet, so "Place order with PayPal" and a
411
+ // "Place order" button with id paypal-pay-button are confirm clicks that still
412
+ // name their own method. A logged click is judged on the fields the kit reads
413
+ // a method from; a click that isn't logged, on the control's own label and
414
+ // identifiers.
415
+ export function namesWallet(control: Element, click?: IButtonClick): boolean {
416
+ const values = click
417
+ ? [
418
+ click.label,
419
+ click.ariaLabel,
420
+ click.elementId,
421
+ click.elementName,
422
+ click.testId,
423
+ ]
424
+ : labelSources(control).concat(
425
+ ['id', 'name']
426
+ .concat(TEST_ID_ATTRIBUTES)
427
+ .map(attribute => control.getAttribute(attribute))
428
+ );
429
+ return values.some(value => KIT_WALLET_WORDS.test(kitWords(value)));
430
+ }
431
+
396
432
  export function classifyIntentControl(
397
433
  control: Element,
398
434
  pathname: string
@@ -1390,6 +1390,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1390
1390
  // Button tracking never runs as a native passthrough.
1391
1391
  resetButtonTracking();
1392
1392
  mpInstance._RoktManager.setAutoButtonClicksForwarded(false);
1393
+ mpInstance._RoktManager.setAutoLinkClicksForwarded(false);
1393
1394
  } else {
1394
1395
  // Main SDK initialization flow
1395
1396
 
@@ -1459,8 +1460,8 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1459
1460
  // init or from a returning shopper's landing page view.
1460
1461
  const logButtons = !!getFeatureFlag(AutoButtonTracking);
1461
1462
  const logLinks = !!getFeatureFlag(AutoLinkTracking);
1462
- // Links feed intent too, so with link tracking on the kit must also
1463
- // receive Auto Link Click events.
1463
+ // With link tracking on, a click logged as Auto Link Click feeds intent
1464
+ // only when the kit also receives those events; button clicks still do.
1464
1465
  const buttonClickBlocker = roktConfig
1465
1466
  ? getButtonClickBlocker(roktConfig, kitBlocker)
1466
1467
  : undefined;
@@ -1472,11 +1473,14 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1472
1473
  mpInstance.Logger.warning(
1473
1474
  buttonClickBlocker
1474
1475
  ? `Checkout intent is off: the ${buttonClickBlocker} keeps some or all Auto Button Click events from the Rokt kit.`
1475
- : `Checkout intent is off: the ${linkClickBlocker} keeps some or all Auto Link Click events from the Rokt kit.`
1476
+ : `Checkout intent skips link clicks: the ${linkClickBlocker} keeps some or all Auto Link Click events from the Rokt kit.`
1476
1477
  );
1477
1478
  }
1478
1479
  mpInstance._RoktManager.setAutoButtonClicksForwarded(
1479
- !!roktConfig && !buttonClickBlocker && !linkClickBlocker
1480
+ !!roktConfig && !buttonClickBlocker
1481
+ );
1482
+ mpInstance._RoktManager.setAutoLinkClicksForwarded(
1483
+ !!roktConfig && !linkClickBlocker
1480
1484
  );
1481
1485
 
1482
1486
  mpInstance._Forwarders.processForwarders(
@@ -175,6 +175,9 @@ export default class RoktManager {
175
175
  private pendingCheckoutIntents: Array<{ signal: ICheckoutIntentSignal; pathname: string }> = [];
176
176
  // Off until SDK init finds the Rokt connection receives auto button clicks.
177
177
  private autoButtonClicksForwarded = false;
178
+ // Off until SDK init finds the Rokt connection receives auto link clicks, or
179
+ // link tracking is off. Only link clicks lose their intent without it.
180
+ private autoLinkClicksForwarded = false;
178
181
 
179
182
  // Keys this manager's route subscription, so a re-executed bundle's manager replaces
180
183
  // it while a second named instance keeps its own.
@@ -331,6 +334,16 @@ export default class RoktManager {
331
334
  this.autoButtonClicksForwarded = forwarded;
332
335
  }
333
336
 
337
+ public setAutoLinkClicksForwarded(forwarded: boolean): void {
338
+ this.autoLinkClicksForwarded = forwarded;
339
+ }
340
+
341
+ // The Rokt kit reads this too: while false, its page views keep
342
+ // dispatchDelayMs, since a checkout link could never release the wait.
343
+ public areAutoLinkClicksForwarded(): boolean {
344
+ return this.autoLinkClicksForwarded;
345
+ }
346
+
334
347
  // noTargeting (which noDeviceId implies), doNotShareOrSell or Global Privacy
335
348
  // Control. Unlike noFunctional, these also stop auto button and link clicks
336
349
  // from being logged.