@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.
- package/README.md +4 -3
- package/dist/mparticle.common.js +32 -15
- package/dist/mparticle.esm.js +32 -15
- package/dist/mparticle.js +435 -4
- package/dist/types/src/checkoutIntentClassifier.d.ts +4 -0
- package/dist/types/src/checkoutIntentTracker.d.ts +23 -0
- package/dist/types/src/constants.d.ts +1 -0
- package/dist/types/src/internal-types.d.ts +1 -1
- package/dist/types/src/mp-instance.d.ts +2 -0
- package/dist/types/src/roktManager.d.ts +11 -0
- package/dist/types/src/store.d.ts +1 -0
- package/package.json +1 -1
- package/src/checkoutIntentClassifier.ts +231 -0
- package/src/checkoutIntentTracker.ts +244 -0
- package/src/constants.ts +1 -0
- package/src/internal-types.ts +1 -0
- package/src/mp-instance.ts +38 -1
- package/src/roktManager.ts +76 -0
- package/src/store.ts +4 -1
|
@@ -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',
|
package/src/internal-types.ts
CHANGED
package/src/mp-instance.ts
CHANGED
|
@@ -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
|