@mparticle/web-sdk 3.16.1 → 3.17.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.
@@ -0,0 +1,231 @@
1
+ // Classifies a click, submit or frame focus as checkout intent. Reads only short
2
+ // control labels, never form field values, and stores nothing.
3
+ export type CheckoutIntentKind =
4
+ | 'commit'
5
+ | 'wallet'
6
+ | 'checkout_step'
7
+ | 'frame';
8
+
9
+ const checkoutPath = /(?:^|\/)(?:checkouts?|payment|order|kasse|paiement|commander?|pago)(?:\/|$)/i;
10
+ const exclusions = new RegExp(
11
+ [
12
+ 'coupon|promo|newsletter|sign in|add to cart|gift card|pay later|pay in|payment plan|learn more',
13
+ 'gutschein|anmelden|warenkorb|bon cadeau|connexion|panier|cupón|iniciar sesión|carrito',
14
+ 'carrello|codice sconto|accedi|buono regalo|winkelwagen|kortingscode|inloggen|cadeaubon|achteraf betalen',
15
+ 'カート|クーポン|ログイン|会員登録|ギフト|後払い|メルマガ',
16
+ ].join('|')
17
+ );
18
+ const commits = new RegExp(
19
+ '^(?:' +
20
+ [
21
+ '(?:place|submit|complete|confirm|finali[sz]e)(?!(?: [^ ]+){0,2} (?:another|new|next)(?: |$))(?: [^ ]+){0,2} (?:order|purchase|booking|reservation|payment|checkout)',
22
+ 'pay(?: securely)?(?: now)?|confirm and pay|order and pay|order with obligation to pay',
23
+ '(?:jetzt )?(?:zahlungs|kosten)pflichtig (?:bestellen|buchen)|(?:jetzt )?bezahlen|kauf abschließen|bestellung abschließen',
24
+ 'commander et payer|payer(?: maintenant)?|(?:confirmer|finaliser) (?:ma |la )?commande',
25
+ "valider et payer|confirmer et payer|commande avec obligation de paiement|finaliser l'achat",
26
+ '(?:realizar|confirmar|completar) (?:el |mi |la )?(?:pedido|compra|reserva)|finalizar (?:el |mi )?pedido|pagar(?: ahora)?',
27
+ 'pedido con obligación de pago',
28
+ "(?:effettua|conferma|concludi|completa) (?:l'|il )?(?:ordine|acquisto)|ordine con obbligo di pagamento",
29
+ 'paga(?: ora| adesso)?|ordina e paga',
30
+ '(?:nu )?betalen|bestellen en betalen|bestelling plaatsen|plaats bestelling|bestelling afronden',
31
+ 'bestelling met betalingsverplichting',
32
+ '(?:この内容で|上記の?内容で)?ご?(?:注文|購入|予約|決済|お?支払い?)を?(?:確定|完了)(?:する)?',
33
+ '(?:この内容で|上記の?内容で)ご?(?:注文|購入|予約)する|ご?注文する|決済する|(?:今すぐ|を)?支払う',
34
+ ].join('|') +
35
+ ')$'
36
+ );
37
+ const routeGatedCommits = new RegExp(
38
+ '^(?:buy now|purchase|jetzt kaufen|kaufen|commander|(?:passer|valider) (?:la |ma )?commande|finalizar (?:la |mi )?compra|' +
39
+ 'comprar ahora|acquista ora|nu kopen|今すぐ購入|購入する|この内容で申し込む)$'
40
+ );
41
+ const steps = new RegExp(
42
+ '^(?:' +
43
+ [
44
+ 'checkout|check out|check-out|continue to payment|proceed to checkout',
45
+ 'zur kasse|zur kasse gehen|auschecken|weiter zur zahlung',
46
+ 'passer à la caisse|continuer vers le paiement|procéder au paiement',
47
+ 'ir a caja|continuar al pago|tramitar pedido',
48
+ "procedi al checkout|procedi all'acquisto|procedi al pagamento|vai alla cassa",
49
+ 'afrekenen|verder naar betalen|doorgaan naar betalen',
50
+ 'レジに進む|ご?(?:購入|注文)手続き(?:へ|に)(?:進む)?|お?支払い(?:へ|に)進む|(?:ご注文内容の)?確認画面へ(?:進む)?',
51
+ ].join('|') +
52
+ ')$'
53
+ );
54
+ const wallets = /^(?:apple pay|google pay|paypal|venmo|amazon pay|shop pay|klarna|afterpay|affirm)$/;
55
+ // Exact HTTPS hosts of hosted wallet and payment frames.
56
+ const frameHosts = new Set([
57
+ 'www.paypal.com',
58
+ 'www.sandbox.paypal.com',
59
+ 'pay.google.com',
60
+ 'applepay.cdn-apple.com',
61
+ 'js.stripe.com',
62
+ 'checkoutshopper-live.adyen.com',
63
+ 'payments.amazon.com',
64
+ 'pay.shopify.com',
65
+ ]);
66
+
67
+ const CONTROL_SELECTOR =
68
+ 'button, input[type="submit"], input[type="button"], input[type="image"], [role="button"]';
69
+
70
+ export function getIntentRouteClass(
71
+ pathname: string
72
+ ): 'checkout_path' | 'other' {
73
+ return checkoutPath.test(pathname) ? 'checkout_path' : 'other';
74
+ }
75
+
76
+ // Rokt's own placements and frames never count as partner checkout controls.
77
+ function isRoktElement(element: Element): boolean {
78
+ if (element.closest('[data-core-type], #rokt-controller-frame')) {
79
+ return true;
80
+ }
81
+ const src = element.getAttribute('src');
82
+ if (src) {
83
+ try {
84
+ if (
85
+ new URL(src, 'https://example.test').pathname.indexOf(
86
+ '/wsdk/'
87
+ ) !== -1
88
+ ) {
89
+ return true;
90
+ }
91
+ } catch (e) {
92
+ // An unparseable src is not a Rokt element.
93
+ }
94
+ }
95
+ return false;
96
+ }
97
+
98
+ function isWalletElement(element: Element): boolean {
99
+ return element.matches(
100
+ 'apple-pay-button, shop-pay-button, .gpay-button, #AmazonPayButton, [data-amazon-pay-button]'
101
+ );
102
+ }
103
+
104
+ function normalize(text: string | null | undefined): string | undefined {
105
+ if (!text || text.length > 128) {
106
+ return undefined;
107
+ }
108
+ return text
109
+ .toLowerCase()
110
+ .replace(/[’‘]/g, "'")
111
+ .replace(/[0-90-9$€£¥¥₹円.,,]/g, '')
112
+ .replace(/\s+/g, ' ')
113
+ .trim();
114
+ }
115
+
116
+ // Labels come from aria-label then visible text. Only submit and button inputs
117
+ // have their value read, so a typed field value is never touched.
118
+ function labels(element: Element): string[] {
119
+ if (
120
+ element.children.length > 4 ||
121
+ element.querySelector('input, select, textarea')
122
+ ) {
123
+ return [];
124
+ }
125
+ let text: string | null = null;
126
+ if (element.tagName.toLowerCase() === 'input') {
127
+ const typeAttr = element.getAttribute('type');
128
+ const type = typeAttr ? typeAttr.toLowerCase() : typeAttr;
129
+ if (type === 'submit' || type === 'button') {
130
+ text = (element as HTMLInputElement).value;
131
+ } else if (type === 'image') {
132
+ text = element.getAttribute('alt');
133
+ }
134
+ } else {
135
+ text = element.textContent;
136
+ }
137
+ return [
138
+ normalize(element.getAttribute('aria-label')),
139
+ normalize(text),
140
+ ].filter((label): label is string => !!label);
141
+ }
142
+
143
+ function classifyLabel(
144
+ text: string,
145
+ button: boolean,
146
+ pathname: string
147
+ ): CheckoutIntentKind | undefined {
148
+ if (
149
+ commits.test(text) ||
150
+ (routeGatedCommits.test(text) &&
151
+ getIntentRouteClass(pathname) === 'checkout_path')
152
+ ) {
153
+ return 'commit';
154
+ }
155
+ if (button && wallets.test(text)) {
156
+ return 'wallet';
157
+ }
158
+ if (steps.test(text)) {
159
+ return 'checkout_step';
160
+ }
161
+ return undefined;
162
+ }
163
+
164
+ // Walks at most 12 entries of an event's composed path for the nearest control.
165
+ export function classifyIntentControl(
166
+ path: Array<EventTarget | null>,
167
+ pathname: string
168
+ ): CheckoutIntentKind | undefined {
169
+ let control: Element | undefined;
170
+ for (const target of path.slice(0, 12)) {
171
+ if (!(target instanceof Element)) {
172
+ continue;
173
+ }
174
+ if (isRoktElement(target)) {
175
+ return undefined;
176
+ }
177
+ if (
178
+ !control &&
179
+ (target.matches(CONTROL_SELECTOR + ', a[href]') ||
180
+ isWalletElement(target))
181
+ ) {
182
+ control = target;
183
+ }
184
+ }
185
+ if (!control || isRoktElement(control)) {
186
+ return undefined;
187
+ }
188
+ const texts = labels(control);
189
+ if (texts.some(text => exclusions.test(text))) {
190
+ return undefined;
191
+ }
192
+ let button = false;
193
+ if (!control.matches('a[href]')) {
194
+ button = control.matches(CONTROL_SELECTOR) || isWalletElement(control);
195
+ }
196
+ if (button && isWalletElement(control)) {
197
+ return 'wallet';
198
+ }
199
+ for (const text of texts) {
200
+ const kind = classifyLabel(text, button, pathname);
201
+ if (kind) {
202
+ return kind;
203
+ }
204
+ }
205
+ return undefined;
206
+ }
207
+
208
+ // Focus moving into a visible hosted wallet or payment frame.
209
+ export function classifyIntentFrame(
210
+ target: EventTarget | null,
211
+ win: Window
212
+ ): 'frame' | undefined {
213
+ if (
214
+ win.document.visibilityState !== 'visible' ||
215
+ !(target instanceof HTMLIFrameElement)
216
+ ) {
217
+ return undefined;
218
+ }
219
+ if (isRoktElement(target)) {
220
+ return undefined;
221
+ }
222
+ try {
223
+ const url = new URL(target.src);
224
+ if (url.protocol === 'https:' && frameHosts.has(url.hostname)) {
225
+ return 'frame';
226
+ }
227
+ } catch (e) {
228
+ // An unparseable frame src is not a wallet frame.
229
+ }
230
+ return undefined;
231
+ }
@@ -0,0 +1,244 @@
1
+ import { IMParticleWebSDKInstance } from './mp-instance';
2
+ import {
3
+ CheckoutIntentKind,
4
+ classifyIntentControl,
5
+ classifyIntentFrame,
6
+ } from './checkoutIntentClassifier';
7
+
8
+ // State lives on `window`, like APV, so a re-executed bundle never doubles the
9
+ // listeners. Inspect `window.__mpCit__` in a console to see if one is live.
10
+ export const WIN_CIT_KEY = '__mpCit__';
11
+
12
+ const THROTTLE_MS = 2000;
13
+ const SUBMIT_AFTER_COMMIT_CLICK_MS = 1000;
14
+ const PATH_LIMIT = 12;
15
+
16
+ interface ICitState {
17
+ tracker?: CheckoutIntentTracker;
18
+ }
19
+
20
+ type WindowWithCit = Window & {
21
+ [WIN_CIT_KEY]?: ICitState;
22
+ };
23
+
24
+ const citWindow = (): WindowWithCit | null =>
25
+ typeof window === 'undefined' ? null : (window as WindowWithCit);
26
+
27
+ export const getActiveCheckoutIntentTracker = ():
28
+ | CheckoutIntentTracker
29
+ | undefined => {
30
+ const win = citWindow();
31
+ const state = win ? win[WIN_CIT_KEY] : undefined;
32
+ return state ? state.tracker : undefined;
33
+ };
34
+
35
+ // Stops whichever tracker is active, including one from an earlier bundle load.
36
+ export const resetCheckoutIntentTracking = (): void => {
37
+ const win = citWindow();
38
+ if (!win) {
39
+ return;
40
+ }
41
+ const active = getActiveCheckoutIntentTracker();
42
+ if (active) {
43
+ active.teardown();
44
+ }
45
+ win[WIN_CIT_KEY] = {};
46
+ };
47
+
48
+ const CAPTURE_PASSIVE: AddEventListenerOptions = {
49
+ capture: true,
50
+ passive: true,
51
+ };
52
+ const PASSIVE: AddEventListenerOptions = { passive: true };
53
+
54
+ // Listens for checkout clicks, submits and wallet frame focus, and hands a
55
+ // frozen {kind, t} to the Rokt kit. No storage, no network, no event logging.
56
+ export class CheckoutIntentTracker {
57
+ private readonly mpInstance: IMParticleWebSDKInstance;
58
+ private active = false;
59
+ // Bumped on reset so classification queued before it is dropped.
60
+ private generation = 0;
61
+ private lastCommitClick = -Infinity;
62
+ private lastSent: { [kind: string]: { t: number; pathname: string } } = {};
63
+
64
+ constructor(mpInstance: IMParticleWebSDKInstance) {
65
+ this.mpInstance = mpInstance;
66
+ }
67
+
68
+ public get isActive(): boolean {
69
+ return this.active;
70
+ }
71
+
72
+ public init(): void {
73
+ const win = citWindow();
74
+ if (!win) {
75
+ return;
76
+ }
77
+
78
+ const previous = getActiveCheckoutIntentTracker();
79
+ if (previous) {
80
+ previous.teardown();
81
+ }
82
+ if (this.active) {
83
+ this.teardown();
84
+ }
85
+
86
+ if (this.isSuppressed()) {
87
+ this.log('privacy settings suppress checkout intent, not starting');
88
+ return;
89
+ }
90
+
91
+ win.document.addEventListener('click', this.onEvent, CAPTURE_PASSIVE);
92
+ win.document.addEventListener('submit', this.onEvent, CAPTURE_PASSIVE);
93
+ win.addEventListener('blur', this.onEvent, PASSIVE);
94
+ win.addEventListener('pageshow', this.onPageShow, PASSIVE);
95
+
96
+ this.active = true;
97
+ if (!win[WIN_CIT_KEY]) {
98
+ win[WIN_CIT_KEY] = {};
99
+ }
100
+ win[WIN_CIT_KEY].tracker = this;
101
+ this.log('listening');
102
+ }
103
+
104
+ public teardown(): void {
105
+ const win = citWindow();
106
+ if (win) {
107
+ win.document.removeEventListener('click', this.onEvent, true);
108
+ win.document.removeEventListener('submit', this.onEvent, true);
109
+ win.removeEventListener('blur', this.onEvent);
110
+ win.removeEventListener('pageshow', this.onPageShow);
111
+
112
+ const state = win[WIN_CIT_KEY];
113
+ if (state && state.tracker === this) {
114
+ state.tracker = undefined;
115
+ }
116
+ }
117
+ this.active = false;
118
+ // Signals not yet handed to a kit go with the tracker.
119
+ this.clearPending();
120
+ }
121
+
122
+ // Drops queued classification, commit timing and buffered signals, so
123
+ // nothing captured before an opt-out is delivered after an opt-in.
124
+ public clearPending(): void {
125
+ this.reset();
126
+ const roktManager = this.mpInstance._RoktManager;
127
+ if (roktManager) {
128
+ roktManager.discardCheckoutIntents();
129
+ }
130
+ }
131
+
132
+ private reset(): void {
133
+ this.generation++;
134
+ this.lastCommitClick = -Infinity;
135
+ this.lastSent = {};
136
+ }
137
+
138
+ private readonly onPageShow = (event: PageTransitionEvent): void => {
139
+ // A page restored from the back-forward cache starts fresh.
140
+ if (event.persisted) {
141
+ this.reset();
142
+ }
143
+ };
144
+
145
+ private readonly onEvent = (event: Event): void => {
146
+ try {
147
+ this.observe(event);
148
+ } catch (e) {
149
+ this.log('[error] event handler threw: ' + e);
150
+ }
151
+ };
152
+
153
+ private observe(event: Event): void {
154
+ if (!this.active || !event.isTrusted || this.isSuppressed()) {
155
+ return;
156
+ }
157
+
158
+ const t = Date.now();
159
+ const pathname = window.location.pathname;
160
+ const layer = event.type;
161
+ // The composed path is only populated during dispatch, so copy it now.
162
+ const path =
163
+ layer === 'click' && typeof event.composedPath === 'function'
164
+ ? event.composedPath().slice(0, PATH_LIMIT)
165
+ : [];
166
+ let target: EventTarget | null;
167
+ if (layer === 'submit') {
168
+ target = (event as Event & { submitter?: Element | null })
169
+ .submitter;
170
+ } else if (layer === 'blur') {
171
+ // Read after the blur settles: some browsers have not moved focus
172
+ // into the frame yet when the window blurs.
173
+ target = null;
174
+ } else {
175
+ target = event.target;
176
+ }
177
+
178
+ const generation = this.generation;
179
+ setTimeout(() => {
180
+ if (generation !== this.generation) {
181
+ return;
182
+ }
183
+ try {
184
+ let kind: CheckoutIntentKind | undefined;
185
+ if (layer === 'blur') {
186
+ kind = classifyIntentFrame(document.activeElement, window);
187
+ } else if (layer === 'submit' && target === null) {
188
+ // A form submitted without a submitter only counts right
189
+ // after a commit click.
190
+ kind =
191
+ t - this.lastCommitClick <= SUBMIT_AFTER_COMMIT_CLICK_MS
192
+ ? 'commit'
193
+ : undefined;
194
+ } else {
195
+ kind = classifyIntentControl(
196
+ layer === 'submit' ? [target] : path,
197
+ pathname
198
+ );
199
+ }
200
+ if (kind) {
201
+ this.emit(kind, layer, t, pathname);
202
+ }
203
+ } catch (e) {
204
+ this.log('[error] classification threw: ' + e);
205
+ }
206
+ }, 0);
207
+ }
208
+
209
+ private emit(
210
+ kind: CheckoutIntentKind,
211
+ layer: string,
212
+ t: number,
213
+ pathname: string
214
+ ): void {
215
+ if (this.isSuppressed()) {
216
+ return;
217
+ }
218
+ if (kind === 'commit' && layer === 'click') {
219
+ this.lastCommitClick = t;
220
+ }
221
+
222
+ const last = this.lastSent[kind];
223
+ if (last && last.pathname === pathname && t - last.t < THROTTLE_MS) {
224
+ return;
225
+ }
226
+ this.lastSent[kind] = { t, pathname };
227
+
228
+ this.mpInstance._RoktManager.notifyCheckoutIntent(
229
+ Object.freeze({ kind, t }),
230
+ pathname
231
+ );
232
+ }
233
+
234
+ // The Rokt manager owns the privacy gates: flag, opt-out, launcher options
235
+ // and Global Privacy Control.
236
+ private isSuppressed(): boolean {
237
+ const roktManager = this.mpInstance._RoktManager;
238
+ return !roktManager || roktManager.isCheckoutIntentSuppressed();
239
+ }
240
+
241
+ private log(message: string): void {
242
+ this.mpInstance.Logger.verbose('mParticle checkout intent: ' + message);
243
+ }
244
+ }
package/src/constants.ts CHANGED
@@ -190,6 +190,7 @@ const Constants = {
190
190
  CaptureIntegrationSpecificIdsV2: 'captureIntegrationSpecificIds.V2',
191
191
  AstBackgroundEvents: 'astBackgroundEvents',
192
192
  AutoLogPageView: 'autoLogPageView',
193
+ CheckoutIntent: 'checkoutIntent',
193
194
  },
194
195
  DefaultInstance: 'default_instance',
195
196
  CCPAPurpose: 'data_sale_opt_out',
@@ -34,6 +34,7 @@ export type {
34
34
  RoktKitFilterSettings,
35
35
  IRoktKitSettings,
36
36
  IRoktKit,
37
+ ICheckoutIntentSignal,
37
38
  IRoktOptions,
38
39
  IRoktLauncherOptions,
39
40
  } from './roktManager';
@@ -66,6 +66,10 @@ import {
66
66
  PageViewTracker,
67
67
  resetPageViewTracking,
68
68
  } from './pageViewTracker';
69
+ import {
70
+ CheckoutIntentTracker,
71
+ resetCheckoutIntentTracking,
72
+ } from './checkoutIntentTracker';
69
73
 
70
74
  export interface IErrorLogMessage {
71
75
  message?: string;
@@ -99,6 +103,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK {
99
103
  _IntegrationCapture: IntegrationCapture;
100
104
  _NativeSdkHelpers: INativeSdkHelpers;
101
105
  _PageViewTracker?: PageViewTracker;
106
+ _CheckoutIntentTracker?: CheckoutIntentTracker;
102
107
  _Persistence: IPersistence;
103
108
  _CookieConsentManager: ICookieConsentManager;
104
109
  _ErrorReportingDispatcher: ErrorReportingDispatcher;
@@ -286,6 +291,10 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
286
291
  this.reset = function(instance) {
287
292
  try {
288
293
  instance._Persistence.resetPersistence();
294
+ // The Rokt manager keeps its own store reference, so stop checkout
295
+ // intent here rather than relying on the deleted store.
296
+ resetCheckoutIntentTracking();
297
+ instance._CheckoutIntentTracker = undefined;
289
298
  if (instance._Store) {
290
299
  delete instance._Store;
291
300
  }
@@ -311,6 +320,8 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
311
320
 
312
321
  resetPageViewTracking();
313
322
  instance._PageViewTracker = undefined;
323
+ resetCheckoutIntentTracking();
324
+ instance._CheckoutIntentTracker = undefined;
314
325
 
315
326
  if (!keepPersistence) {
316
327
  instance._Persistence.resetPersistence();
@@ -1133,6 +1144,13 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
1133
1144
  self._SessionManager.resetSessionTimer();
1134
1145
  self._Store.isEnabled = !isOptingOut;
1135
1146
 
1147
+ if (isOptingOut) {
1148
+ if (self._CheckoutIntentTracker) {
1149
+ self._CheckoutIntentTracker.clearPending();
1150
+ }
1151
+ self._RoktManager.discardCheckoutIntents();
1152
+ }
1153
+
1136
1154
  self._Events.logOptOut();
1137
1155
  self._Persistence.update();
1138
1156
 
@@ -1347,7 +1365,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
1347
1365
  function completeSDKInitialization(apiKey, config, mpInstance) {
1348
1366
  const kitBlocker = createKitBlocker(config, mpInstance);
1349
1367
  const { getFeatureFlag } = mpInstance._Helpers;
1350
- const { AutoLogPageView } = Constants.FeatureFlags;
1368
+ const { AutoLogPageView, CheckoutIntent } = Constants.FeatureFlags;
1351
1369
 
1352
1370
  // Destroy previous batch uploader to prevent leaked timers and event listeners
1353
1371
  if (mpInstance._APIClient?.uploader) {
@@ -1367,6 +1385,8 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1367
1385
  // persistence or Identity directly.
1368
1386
  if (mpInstance._Store.webviewBridgeEnabled) {
1369
1387
  mpInstance._NativeSdkHelpers.initializeSessionAttributes(apiKey);
1388
+ // Checkout intent never runs as a native passthrough.
1389
+ resetCheckoutIntentTracking();
1370
1390
  } else {
1371
1391
  // Main SDK initialization flow
1372
1392
 
@@ -1477,6 +1497,21 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1477
1497
  mpInstance._PageViewTracker.teardown();
1478
1498
  }
1479
1499
 
1500
+ // Checkout intent only feeds the Rokt kit, so it listens only when the
1501
+ // workspace opts in and this init configures a Rokt kit.
1502
+ if (getFeatureFlag(CheckoutIntent) && roktConfig) {
1503
+ if (!mpInstance._CheckoutIntentTracker) {
1504
+ mpInstance._CheckoutIntentTracker = new CheckoutIntentTracker(
1505
+ mpInstance
1506
+ );
1507
+ }
1508
+ mpInstance._CheckoutIntentTracker.init();
1509
+ } else {
1510
+ // Also retires a tracker left by an earlier bundle load. Like APV,
1511
+ // this assumes a single active SDK instance.
1512
+ resetCheckoutIntentTracking();
1513
+ }
1514
+
1480
1515
  processIdentityCallback(
1481
1516
  mpInstance,
1482
1517
  currentUser,
@@ -1596,6 +1631,8 @@ function runPreConfigFetchInitialization(mpInstance, apiKey, config) {
1596
1631
  mpInstance._LoggingDispatcher.logger = mpInstance.Logger;
1597
1632
  mpInstance._Store = new Store(config, mpInstance, apiKey);
1598
1633
  window.mParticle.Store = mpInstance._Store;
1634
+ // Checkout intent stops until this init's config decides whether it runs.
1635
+ resetCheckoutIntentTracking();
1599
1636
  mpInstance.Logger.verbose(StartingInitialization);
1600
1637
 
1601
1638
  // Initialize CookieConsentManager with privacy flags from launcherOptions