@ianmenethil/zp-observer 6.0.0 → 6.1.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 +19 -276
- package/dist/adapters/browser-lifecycle-adapter.cjs +51 -0
- package/dist/adapters/browser-lifecycle-adapter.js +48 -0
- package/dist/adapters/iframe-detector-adapter.cjs +108 -0
- package/dist/adapters/iframe-detector-adapter.js +106 -0
- package/dist/client/create-telemetry-client.cjs +136 -0
- package/dist/client/create-telemetry-client.js +134 -0
- package/dist/client/state-machine.cjs +20 -0
- package/dist/client/state-machine.js +18 -0
- package/dist/diagnostics/diagnostics-buffer.cjs +34 -0
- package/dist/diagnostics/diagnostics-buffer.js +32 -0
- package/dist/diagnostics/preflight.cjs +36 -0
- package/dist/diagnostics/preflight.js +34 -0
- package/dist/events/envelope.cjs +23 -0
- package/dist/events/envelope.js +21 -0
- package/dist/index.cjs +23 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +10 -0
- package/dist/persistence/local-storage-outbox.cjs +56 -0
- package/dist/persistence/local-storage-outbox.js +54 -0
- package/dist/persistence/memory-outbox.cjs +23 -0
- package/dist/persistence/memory-outbox.js +21 -0
- package/dist/runtime/event-pipeline.cjs +64 -0
- package/dist/runtime/event-pipeline.js +62 -0
- package/dist/runtime/heartbeat-scheduler.cjs +46 -0
- package/dist/runtime/heartbeat-scheduler.js +44 -0
- package/dist/runtime/session-manager.cjs +47 -0
- package/dist/runtime/session-manager.js +45 -0
- package/dist/transport/beacon.cjs +14 -0
- package/dist/transport/beacon.js +12 -0
- package/dist/transport/callback-transport.cjs +19 -0
- package/dist/transport/callback-transport.js +17 -0
- package/dist/transport/http-transport.cjs +62 -0
- package/dist/transport/http-transport.js +60 -0
- package/dist/types/internal.cjs +3 -0
- package/dist/types/internal.js +0 -0
- package/dist/types/public.cjs +3 -0
- package/dist/types/public.js +0 -0
- package/dist/utils/ids.cjs +20 -0
- package/dist/utils/ids.js +17 -0
- package/dist/utils/safe-globals.cjs +11 -0
- package/dist/utils/safe-globals.js +9 -0
- package/dist/version.cjs +5 -0
- package/dist/version.js +2 -0
- package/package.json +29 -89
- package/PRIVACY.md +0 -67
- package/dist/auto-patch.cjs +0 -171
- package/dist/auto-patch.cjs.map +0 -7
- package/dist/auto-patch.mjs +0 -148
- package/dist/auto-patch.mjs.map +0 -7
- package/dist/session.cjs +0 -1186
- package/dist/session.cjs.map +0 -7
- package/dist/session.mjs +0 -1163
- package/dist/session.mjs.map +0 -7
- package/dist/types/auto-patch.d.ts +0 -9
- package/dist/types/auto-patch.d.ts.map +0 -1
- package/dist/types/core/beacon.d.ts +0 -6
- package/dist/types/core/beacon.d.ts.map +0 -1
- package/dist/types/core/detection.d.ts +0 -34
- package/dist/types/core/detection.d.ts.map +0 -1
- package/dist/types/core/event-bus.d.ts +0 -21
- package/dist/types/core/event-bus.d.ts.map +0 -1
- package/dist/types/core/experimental.d.ts +0 -35
- package/dist/types/core/experimental.d.ts.map +0 -1
- package/dist/types/core/heartbeat.d.ts +0 -32
- package/dist/types/core/heartbeat.d.ts.map +0 -1
- package/dist/types/core/lifecycle.d.ts +0 -31
- package/dist/types/core/lifecycle.d.ts.map +0 -1
- package/dist/types/core/observer.d.ts +0 -10
- package/dist/types/core/observer.d.ts.map +0 -1
- package/dist/types/core/outbox.d.ts +0 -20
- package/dist/types/core/outbox.d.ts.map +0 -1
- package/dist/types/core/random.d.ts +0 -8
- package/dist/types/core/random.d.ts.map +0 -1
- package/dist/types/core/shortcode.d.ts +0 -17
- package/dist/types/core/shortcode.d.ts.map +0 -1
- package/dist/types/core/types.d.ts +0 -292
- package/dist/types/core/types.d.ts.map +0 -1
- package/dist/types/diagnostics/preflight.d.ts +0 -17
- package/dist/types/diagnostics/preflight.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -41
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/integration/devicefp-bridge.d.ts +0 -31
- package/dist/types/integration/devicefp-bridge.d.ts.map +0 -1
- package/dist/types/integration/hpp-bridge.d.ts +0 -13
- package/dist/types/integration/hpp-bridge.d.ts.map +0 -1
- package/dist/types/integration/zenpay-auto-patch.d.ts +0 -28
- package/dist/types/integration/zenpay-auto-patch.d.ts.map +0 -1
- package/dist/types/outcome.d.ts +0 -20
- package/dist/types/outcome.d.ts.map +0 -1
- package/dist/types/session.d.ts +0 -54
- package/dist/types/session.d.ts.map +0 -1
- package/dist/types/transport/callback-transport.d.ts +0 -17
- package/dist/types/transport/callback-transport.d.ts.map +0 -1
- package/dist/types/transport/http-transport.d.ts +0 -30
- package/dist/types/transport/http-transport.d.ts.map +0 -1
- package/dist/types/umd.d.ts +0 -16
- package/dist/types/umd.d.ts.map +0 -1
- package/dist/zp-observer.cjs +0 -1375
- package/dist/zp-observer.cjs.map +0 -7
- package/dist/zp-observer.js +0 -1377
- package/dist/zp-observer.js.map +0 -7
- package/dist/zp-observer.min.js +0 -2
- package/dist/zp-observer.min.js.map +0 -7
- package/dist/zp-observer.min.obf.js +0 -1
- package/dist/zp-observer.mjs +0 -1352
- package/dist/zp-observer.mjs.map +0 -7
package/README.md
CHANGED
|
@@ -1,284 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ZP Telemetry Rewrite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package is the clean native-only telemetry SDK for internal checkout integrations.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Current scope:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
- typed native event map
|
|
8
|
+
- single telemetry envelope contract across client and transports
|
|
9
|
+
- session manager with explicit state machine
|
|
10
|
+
- callback and HTTP transports that both send the same envelope shape
|
|
11
|
+
- memory and local-storage outboxes
|
|
12
|
+
- diagnostics snapshot buffer
|
|
13
|
+
- core telemetry client with middleware, persistence policy, heartbeat, and flush support
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
Removed from this package:
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
- legacy observer compatibility wrappers
|
|
18
|
+
- staged migration session helpers
|
|
19
|
+
- one-shot compatibility reporters
|
|
20
|
+
- browser-global compatibility entrypoints
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
npm install @ianmenethil/zp-observer
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Quickstart (unified session — v6)
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
import { createZenPaySession, httpTransport } from '@ianmenethil/zp-observer/session';
|
|
27
|
-
|
|
28
|
-
const session = createZenPaySession({
|
|
29
|
-
sessionId: merchantUPID,
|
|
30
|
-
transport: httpTransport({ openUrl, aliveUrl, closeUrl }),
|
|
31
|
-
persistence: 'localStorage',
|
|
32
|
-
metadata: { merchantId: 'ACME', env: 'prod' },
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// session.observer is started automatically.
|
|
36
|
-
// Pass session.hppDefaults.onPluginClose to ZenPay.
|
|
37
|
-
// Pass session.fingerprint.hooks into DEVICEFP's captureThumbmark() options.
|
|
38
|
-
// session.correlationId ties every event together.
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Correlation IDs
|
|
42
|
-
|
|
43
|
-
Every event carries a `correlationId` — a UUID minted by the observer (or supplied via `createObserver({ correlationId })`). It flows through the fingerprint, HPP init, modal open, heartbeats, close, and outcome events so your backend can join the full payment story without timestamp guessing. Pass it to DEVICEFP via the bridge hooks, or embed it in `merchantUniquePaymentId` metadata.
|
|
44
|
-
|
|
45
|
-
## npm — ES modules (`import`)
|
|
46
|
-
|
|
47
|
-
**`headers` on `httpTransport` is optional.** If you omit it, requests still run: heartbeats and fetch fallbacks send **`Content-Type: application/json`** only, and use **`credentials: 'include'`** so **first-party / same-site cookies** are included when the browser allows it. **`navigator.sendBeacon`** (used for `open` / `close` when it succeeds) **cannot set custom headers** — only the JSON body.
|
|
48
|
-
|
|
49
|
-
So integrators often:
|
|
50
|
-
|
|
51
|
-
- **No extra auth** — omit `headers`; rely on **session cookies** on your own origin, or **signed query params** baked into the three URLs.
|
|
52
|
-
- **Bearer / API key** — pass `headers: () => ({ Authorization: 'Bearer …' })` (or any static object) so **heartbeat** and **fetch fallbacks** include them; beacons still won’t.
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
import {
|
|
56
|
-
createObserver,
|
|
57
|
-
httpTransport,
|
|
58
|
-
installZenPayAutoPatch,
|
|
59
|
-
} from '@ianmenethil/zp-observer';
|
|
60
|
-
|
|
61
|
-
const mUPID = 'your-merchant-unique-payment-id';
|
|
62
|
-
|
|
63
|
-
const observer = createObserver({
|
|
64
|
-
sessionId: mUPID,
|
|
65
|
-
transport: httpTransport({
|
|
66
|
-
openUrl: `https://your-api.example/telemetry/${mUPID}?e=open`,
|
|
67
|
-
aliveUrl: `https://your-api.example/telemetry/${mUPID}?e=alive`,
|
|
68
|
-
closeUrl: `https://your-api.example/telemetry/${mUPID}?e=close`,
|
|
69
|
-
// Optional — only if your server needs extra headers on fetch() calls:
|
|
70
|
-
// headers: () => ({ Authorization: `Bearer ${getLogsToken()}` }),
|
|
71
|
-
}),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Optional: rewrite ZenPay init so onPluginClose uses observer.shortcode automatically
|
|
75
|
-
installZenPayAutoPatch(observer);
|
|
76
|
-
|
|
77
|
-
observer.start();
|
|
78
|
-
|
|
79
|
-
// Then open ZenPay as you already do, e.g.:
|
|
80
|
-
// $.zpPayment({ merchantUniquePaymentId: mUPID, onPluginClose: observer.shortcode }).init();
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
**Without auto-patch:** after `start()`, pass **`observer.shortcode`** (a string like `window.zpcb_…`) to ZenPay’s **`onPluginClose`** — same value as the v4 bridge.
|
|
84
|
-
|
|
85
|
-
**Optional subpath** (only the ZenPay patch helper):
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import { installZenPayAutoPatch } from '@ianmenethil/zp-observer/auto-patch';
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## npm — CommonJS (`require`)
|
|
94
|
-
|
|
95
|
-
```js
|
|
96
|
-
const {
|
|
97
|
-
createObserver,
|
|
98
|
-
httpTransport,
|
|
99
|
-
installZenPayAutoPatch,
|
|
100
|
-
} = require('@ianmenethil/zp-observer');
|
|
101
|
-
|
|
102
|
-
const mUPID = 'your-merchant-unique-payment-id';
|
|
103
|
-
|
|
104
|
-
const observer = createObserver({
|
|
105
|
-
sessionId: mUPID,
|
|
106
|
-
transport: httpTransport({
|
|
107
|
-
openUrl: `https://your-api.example/telemetry/${mUPID}?e=open`,
|
|
108
|
-
aliveUrl: `https://your-api.example/telemetry/${mUPID}?e=alive`,
|
|
109
|
-
closeUrl: `https://your-api.example/telemetry/${mUPID}?e=close`,
|
|
110
|
-
}),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
installZenPayAutoPatch(observer);
|
|
114
|
-
observer.start();
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## CDN — `<script>` tag (no bundler)
|
|
120
|
-
|
|
121
|
-
1. Add **one** script from **`dist/`** (published path on unpkg/jsDelivr includes `dist/`).
|
|
122
|
-
|
|
123
|
-
2. Use the global **`ZPObserver`** — same names as the npm API: `createObserver`, `httpTransport`, `installZenPayAutoPatch`, `callbackTransport`, `VERSION`.
|
|
124
|
-
|
|
125
|
-
```html
|
|
126
|
-
<script src="https://unpkg.com/@ianmenethil/zp-observer/dist/zp-observer.min.js"></script>
|
|
127
|
-
<script>
|
|
128
|
-
(function () {
|
|
129
|
-
var mUPID = ‘your-merchant-unique-payment-id’;
|
|
22
|
+
Run checks:
|
|
130
23
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
openUrl: ‘/telemetry/’ + mUPID + ‘?e=open’,
|
|
135
|
-
aliveUrl: ‘/telemetry/’ + mUPID + ‘?e=alive’,
|
|
136
|
-
closeUrl: ‘/telemetry/’ + mUPID + ‘?e=close’,
|
|
137
|
-
}),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
ZPObserver.installZenPayAutoPatch(observer);
|
|
141
|
-
observer.start();
|
|
142
|
-
|
|
143
|
-
// ZenPay init — if you don’t use auto-patch, pass onPluginClose: observer.shortcode
|
|
144
|
-
})();
|
|
145
|
-
</script>
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Which CDN file?**
|
|
149
|
-
|
|
150
|
-
| File | When |
|
|
151
|
-
|------|------|
|
|
152
|
-
| `dist/zp-observer.js` | Debugging (readable) |
|
|
153
|
-
| `dist/zp-observer.min.js` | **Default** production |
|
|
154
|
-
| `dist/zp-observer.min.obf.js` | Minified + obfuscated |
|
|
155
|
-
|
|
156
|
-
---
|
|
157
|
-
|
|
158
|
-
## API (quick reference)
|
|
159
|
-
|
|
160
|
-
| Export | Role |
|
|
161
|
-
|--------|------|
|
|
162
|
-
| **`createObserver(options)`** | Returns `{ sessionId, correlationId, shortcode, start, stop, getState, emit, diagnostics }`. |
|
|
163
|
-
| **`httpTransport({ openUrl, aliveUrl, closeUrl, eventUrl?, headers?, fetch? })`** | Default HTTP transport. `eventUrl` (optional) receives domain events. |
|
|
164
|
-
| **`callbackTransport({ onOpen?, onHeartbeat?, onClose?, onEvent? })`** | Bring-your-own-sink. `onEvent` receives domain events. |
|
|
165
|
-
| **`installZenPayAutoPatch(observer)`** | Wraps `$.fn.zpPayment` / `window.ZenPay` to inject `observer.shortcode` and emit `payment.hpp_init`. |
|
|
166
|
-
| **`installHPPBridge(observer)`** | Wraps `window.onPaymentPluginLoaded` to emit `payment.hpp_iframe_loaded`. |
|
|
167
|
-
| **`installDeviceFPBridge(observer, opts?)`** | Returns typed hooks (`onFingerprintStarted`, `onFingerprintCacheHit`, `onFingerprintSucceeded`, `onFingerprintFailed`) that emit domain events. Spread into DEVICEFP's `captureThumbmark()`. |
|
|
168
|
-
| **`preflight({ transport, sample? })`** | Sends a `payment.preflight` event and returns a structured `PreflightReport` for integration debugging. |
|
|
169
|
-
| **`reportOutcome({ correlationId, sessionId, outcome, transport })`** | Posts a single `payment.outcome` event from your return/thank-you page. |
|
|
170
|
-
| **`createZenPaySession(options)`** | Unified entry (subpath `@ianmenethil/zp-observer/session`). Composes observer, auto-patch, HPP bridge, and fingerprint hooks. |
|
|
171
|
-
| **`VERSION`** | Package version string. |
|
|
172
|
-
| **`ExperimentalOptions` / `onDiagnostic`** | See [Experimental / lab](#experimental--lab-optional). |
|
|
173
|
-
|
|
174
|
-
**`observer.emit(event)`** — push any `ObserverEvent` onto the bus (bridges, merchant code).
|
|
175
|
-
**`observer.diagnostics.dump()`** — JSON-serializable snapshot for support tickets.
|
|
176
|
-
**`observer.shortcode`** — string to pass to ZenPay as **`onPluginClose`** (e.g. `window.zpcb_abc123`).
|
|
177
|
-
**`observer.correlationId`** — the UUID tying this session's events together.
|
|
178
|
-
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
## Experimental / lab (optional)
|
|
182
|
-
|
|
183
|
-
Use this to **turn on one browser signal at a time** and record what actually fires in your merchants’ browsers. Nothing here changes the core **`open` / `heartbeat` / `close`** contract unless you opt in to extra fields or heartbeats.
|
|
184
|
-
|
|
185
|
-
- **`onDiagnostic`** — callback with typed **`DiagnosticEvent`** (`exp.pageshow`, `exp.freeze`, `exp.perf_entry`, …).
|
|
186
|
-
- **`experimental.*`** — booleans such as **`capturePageShow`**, **`captureFreezeResume`**, **`capturePerformanceEntries`**, **`captureIframeResize`**, **`captureIframeSrcChanges`**, **`captureVisibilityDetail`** (includes **`navigator.onLine`** as **informational only**), **`captureWindowFocus`**, **`captureWasDiscarded`**, **`captureUserActivation`**, **`captureNavigationTimingDetail`** (adds **`userActivation`** / **`navigationDetail`** on the **`open`** payload when set).
|
|
187
|
-
- **`pageLifecycleBeaconUrl`** — optional URL; sends JSON **`sendBeacon`** for **`navigation`** (at `start()`), **`pageshow`**, **`pagehide`** (mirrors the v1.2 experiment’s page-lifecycle beacons). **`exp.page_lifecycle_beacon`** diagnostics report whether **`sendBeacon`** returned true.
|
|
188
|
-
- **`attachDiagnosticsToHeartbeat`** — append a ring buffer to **`metadata.zpObserverDiagnostics`** on each heartbeat (cap with **`diagnosticsMax`**, default 50).
|
|
189
|
-
|
|
190
|
-
```ts
|
|
191
|
-
createObserver({
|
|
192
|
-
sessionId: mUPID,
|
|
193
|
-
transport: /* … */,
|
|
194
|
-
debug: true,
|
|
195
|
-
onDiagnostic: (ev) => {
|
|
196
|
-
console.table(ev);
|
|
197
|
-
// or POST to your lab sink
|
|
198
|
-
},
|
|
199
|
-
experimental: {
|
|
200
|
-
capturePageShow: true,
|
|
201
|
-
captureFreezeResume: true,
|
|
202
|
-
capturePerformanceEntries: true,
|
|
203
|
-
captureIframeResize: true,
|
|
204
|
-
captureIframeSrcChanges: true,
|
|
205
|
-
attachDiagnosticsToHeartbeat: true,
|
|
206
|
-
// pageLifecycleBeaconUrl: 'https://your-lab.example/beacon',
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## Bring your own transport (npm ESM example)
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
import { createObserver, callbackTransport } from '@ianmenethil/zp-observer';
|
|
217
|
-
|
|
218
|
-
const observer = createObserver({
|
|
219
|
-
sessionId: mUPID,
|
|
220
|
-
transport: callbackTransport({
|
|
221
|
-
onOpen: (ev) => {
|
|
222
|
-
void navigator.sendBeacon('/telemetry', JSON.stringify(ev));
|
|
223
|
-
},
|
|
224
|
-
onHeartbeat: async (ev) => {
|
|
225
|
-
const res = await fetch('/telemetry', {
|
|
226
|
-
method: 'POST',
|
|
227
|
-
headers: { 'Content-Type': 'application/json' },
|
|
228
|
-
body: JSON.stringify(ev),
|
|
229
|
-
});
|
|
230
|
-
return { ok: res.ok, status: res.status };
|
|
231
|
-
},
|
|
232
|
-
onClose: (ev) => {
|
|
233
|
-
void navigator.sendBeacon('/telemetry', JSON.stringify(ev));
|
|
234
|
-
},
|
|
235
|
-
}),
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
observer.start();
|
|
24
|
+
```bash
|
|
25
|
+
node --test ./tests/**/*.test.ts
|
|
26
|
+
node ./scripts/build.mjs
|
|
239
27
|
```
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
## Build output layout (`dist/`)
|
|
244
|
-
|
|
245
|
-
| File | Purpose |
|
|
246
|
-
|------|---------|
|
|
247
|
-
| **`dist/zp-observer.mjs`** | **npm ESM** — `import` resolves here |
|
|
248
|
-
| **`dist/zp-observer.cjs`** | **npm CJS** — `require()` resolves here |
|
|
249
|
-
| **`dist/auto-patch.mjs`** | **npm ESM subpath** — `@ianmenethil/zp-observer/auto-patch` |
|
|
250
|
-
| **`dist/auto-patch.cjs`** | **npm CJS subpath** |
|
|
251
|
-
| **`dist/session.mjs`** | **npm ESM subpath** — `@ianmenethil/zp-observer/session` |
|
|
252
|
-
| **`dist/session.cjs`** | **npm CJS subpath** |
|
|
253
|
-
| **`dist/zp-observer.min.js`** | **CDN** — `<script src>` IIFE, global `ZPObserver` |
|
|
254
|
-
| **`dist/zp-observer.js`** | **CDN debug** — unminified IIFE |
|
|
255
|
-
| **`dist/zp-observer.min.obf.js`** | **CDN obfuscated** — minified + obfuscated |
|
|
256
|
-
| **`dist/types/`** | **TypeScript** — `.d.ts` declarations |
|
|
257
|
-
|
|
258
|
-
Do not point a raw `<script>` at `.mjs` / `.cjs` files (those use ESM `import`/`export` or CJS `require`).
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## Close reasons
|
|
263
|
-
|
|
264
|
-
Typed as **`CloseReason`** in the published `.d.ts`. Examples: `user.callback_invoked`, `user.modal_closed`, `system.idle_timeout`, `system.network_abandoned`, `page.pagehide`, `user.manual_close`.
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## Publishing (`@ianmenethil/zp-observer`)
|
|
269
|
-
|
|
270
|
-
1. `npm login` with an account that can publish the **`@ianmenethil`** scope.
|
|
271
|
-
2. Bump **`version`** in `package.json` (or run `npm version patch|minor|major`).
|
|
272
|
-
3. Run **`npm run publish:npm`** (same as **`npm publish --access public`**). **`prepack`** runs **`npm run check`** (lint + typecheck + test + knip + jscpd) and **`npm run build`** before the tarball is packed.
|
|
273
|
-
|
|
274
|
-
**Internal CDN:** On the Anticide build machine, run `npm run build:local` to copy the IIFE bundles to `_ZP-CDN-server\public`.
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
## Privacy
|
|
279
|
-
|
|
280
|
-
See [`PRIVACY.md`](PRIVACY.md).
|
|
281
|
-
|
|
282
|
-
## License
|
|
283
|
-
|
|
284
|
-
MIT © Zenith Payments
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function observeElementVisibility(element, onChange) {
|
|
2
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
3
|
+
return { uninstall() {} };
|
|
4
|
+
}
|
|
5
|
+
const observer = new IntersectionObserver((entries) => {
|
|
6
|
+
const entry = entries[entries.length - 1];
|
|
7
|
+
if (entry) {
|
|
8
|
+
onChange(entry.isIntersecting);
|
|
9
|
+
}
|
|
10
|
+
}, { threshold: 0.01 });
|
|
11
|
+
observer.observe(element);
|
|
12
|
+
return {
|
|
13
|
+
uninstall() {
|
|
14
|
+
observer.disconnect();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function installBrowserLifecycleAdapter(client, options = {}) {
|
|
19
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
20
|
+
return { uninstall() {} };
|
|
21
|
+
}
|
|
22
|
+
const visibilityHandler = () => {
|
|
23
|
+
if (document.visibilityState === "hidden") {
|
|
24
|
+
options.onHidden?.();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (document.visibilityState === "visible") {
|
|
28
|
+
options.onVisible?.();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const pageHideHandler = (event) => {
|
|
32
|
+
const persisted = event.persisted === true;
|
|
33
|
+
options.onPageHide?.(persisted);
|
|
34
|
+
if (options.emitCloseOnPageHide) {
|
|
35
|
+
client.emit("checkout.modal_closed", {
|
|
36
|
+
reason: options.closeReason ?? "pagehide"
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
document.addEventListener("visibilitychange", visibilityHandler);
|
|
41
|
+
window.addEventListener("pagehide", pageHideHandler);
|
|
42
|
+
return {
|
|
43
|
+
uninstall() {
|
|
44
|
+
document.removeEventListener("visibilitychange", visibilityHandler);
|
|
45
|
+
window.removeEventListener("pagehide", pageHideHandler);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.observeElementVisibility = observeElementVisibility;
|
|
51
|
+
exports.installBrowserLifecycleAdapter = installBrowserLifecycleAdapter;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function observeElementVisibility(element, onChange) {
|
|
2
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
3
|
+
return { uninstall() {} };
|
|
4
|
+
}
|
|
5
|
+
const observer = new IntersectionObserver((entries) => {
|
|
6
|
+
const entry = entries[entries.length - 1];
|
|
7
|
+
if (entry) {
|
|
8
|
+
onChange(entry.isIntersecting);
|
|
9
|
+
}
|
|
10
|
+
}, { threshold: 0.01 });
|
|
11
|
+
observer.observe(element);
|
|
12
|
+
return {
|
|
13
|
+
uninstall() {
|
|
14
|
+
observer.disconnect();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function installBrowserLifecycleAdapter(client, options = {}) {
|
|
19
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
20
|
+
return { uninstall() {} };
|
|
21
|
+
}
|
|
22
|
+
const visibilityHandler = () => {
|
|
23
|
+
if (document.visibilityState === "hidden") {
|
|
24
|
+
options.onHidden?.();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (document.visibilityState === "visible") {
|
|
28
|
+
options.onVisible?.();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const pageHideHandler = (event) => {
|
|
32
|
+
const persisted = event.persisted === true;
|
|
33
|
+
options.onPageHide?.(persisted);
|
|
34
|
+
if (options.emitCloseOnPageHide) {
|
|
35
|
+
client.emit("checkout.modal_closed", {
|
|
36
|
+
reason: options.closeReason ?? "pagehide"
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
document.addEventListener("visibilitychange", visibilityHandler);
|
|
41
|
+
window.addEventListener("pagehide", pageHideHandler);
|
|
42
|
+
return {
|
|
43
|
+
uninstall() {
|
|
44
|
+
document.removeEventListener("visibilitychange", visibilityHandler);
|
|
45
|
+
window.removeEventListener("pagehide", pageHideHandler);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const {observeElementVisibility} = require('./browser-lifecycle-adapter.cjs');
|
|
2
|
+
function installIframeDetectorAdapter(client, options) {
|
|
3
|
+
if (typeof document === "undefined") {
|
|
4
|
+
return { uninstall() {} };
|
|
5
|
+
}
|
|
6
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
7
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
8
|
+
const startedAt = Date.now();
|
|
9
|
+
let pollTimer = null;
|
|
10
|
+
let mutationObserver = null;
|
|
11
|
+
let visibilityHandle = null;
|
|
12
|
+
let bootstrapHandler = null;
|
|
13
|
+
let detectedIframe = null;
|
|
14
|
+
const findIframe = () => {
|
|
15
|
+
for (const selector of options.selectors) {
|
|
16
|
+
const element = document.querySelector(selector);
|
|
17
|
+
if (element instanceof HTMLIFrameElement) {
|
|
18
|
+
return element;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
const stopPolling = () => {
|
|
24
|
+
if (pollTimer !== null) {
|
|
25
|
+
clearInterval(pollTimer);
|
|
26
|
+
pollTimer = null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const installRemovalObserver = (iframe) => {
|
|
30
|
+
if (typeof MutationObserver === "undefined" || !document.body) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
mutationObserver = new MutationObserver((mutations) => {
|
|
34
|
+
for (const mutation of mutations) {
|
|
35
|
+
for (const node of Array.from(mutation.removedNodes)) {
|
|
36
|
+
if (node === iframe || node instanceof Node && node.contains(iframe)) {
|
|
37
|
+
options.onRemoved?.();
|
|
38
|
+
if (options.emitCloseOnRemoval) {
|
|
39
|
+
client.emit("checkout.modal_closed", {
|
|
40
|
+
reason: options.closedReason ?? "iframe_removed"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
49
|
+
};
|
|
50
|
+
const installBootstrapObserver = () => {
|
|
51
|
+
if (typeof document.addEventListener !== "function") {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
bootstrapHandler = () => {
|
|
55
|
+
options.onBootstrapHidden?.();
|
|
56
|
+
if (options.emitCloseOnBootstrapHidden) {
|
|
57
|
+
client.emit("checkout.modal_closed", {
|
|
58
|
+
reason: options.bootstrapClosedReason ?? options.closedReason ?? "bootstrap_hidden"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
document.addEventListener("hidden.bs.modal", bootstrapHandler, true);
|
|
63
|
+
};
|
|
64
|
+
const handleDetected = (iframe) => {
|
|
65
|
+
detectedIframe = iframe;
|
|
66
|
+
stopPolling();
|
|
67
|
+
installRemovalObserver(iframe);
|
|
68
|
+
installBootstrapObserver();
|
|
69
|
+
visibilityHandle = observeElementVisibility(iframe, (visible) => {
|
|
70
|
+
options.onVisibilityChange?.(visible);
|
|
71
|
+
});
|
|
72
|
+
options.onDetected?.(iframe);
|
|
73
|
+
client.emit("checkout.iframe_detected", {
|
|
74
|
+
iframeSrc: iframe.src || null
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const existing = findIframe();
|
|
78
|
+
if (existing) {
|
|
79
|
+
queueMicrotask(() => handleDetected(existing));
|
|
80
|
+
} else {
|
|
81
|
+
pollTimer = setInterval(() => {
|
|
82
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
83
|
+
stopPolling();
|
|
84
|
+
options.onTimeout?.();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const iframe = findIframe();
|
|
88
|
+
if (iframe && iframe !== detectedIframe) {
|
|
89
|
+
handleDetected(iframe);
|
|
90
|
+
}
|
|
91
|
+
}, intervalMs);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
uninstall() {
|
|
95
|
+
stopPolling();
|
|
96
|
+
mutationObserver?.disconnect();
|
|
97
|
+
mutationObserver = null;
|
|
98
|
+
visibilityHandle?.uninstall();
|
|
99
|
+
visibilityHandle = null;
|
|
100
|
+
if (bootstrapHandler) {
|
|
101
|
+
document.removeEventListener("hidden.bs.modal", bootstrapHandler, true);
|
|
102
|
+
bootstrapHandler = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
exports.installIframeDetectorAdapter = installIframeDetectorAdapter;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { observeElementVisibility } from "./browser-lifecycle-adapter.js";
|
|
2
|
+
export function installIframeDetectorAdapter(client, options) {
|
|
3
|
+
if (typeof document === "undefined") {
|
|
4
|
+
return { uninstall() {} };
|
|
5
|
+
}
|
|
6
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
7
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
8
|
+
const startedAt = Date.now();
|
|
9
|
+
let pollTimer = null;
|
|
10
|
+
let mutationObserver = null;
|
|
11
|
+
let visibilityHandle = null;
|
|
12
|
+
let bootstrapHandler = null;
|
|
13
|
+
let detectedIframe = null;
|
|
14
|
+
const findIframe = () => {
|
|
15
|
+
for (const selector of options.selectors) {
|
|
16
|
+
const element = document.querySelector(selector);
|
|
17
|
+
if (element instanceof HTMLIFrameElement) {
|
|
18
|
+
return element;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
const stopPolling = () => {
|
|
24
|
+
if (pollTimer !== null) {
|
|
25
|
+
clearInterval(pollTimer);
|
|
26
|
+
pollTimer = null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const installRemovalObserver = (iframe) => {
|
|
30
|
+
if (typeof MutationObserver === "undefined" || !document.body) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
mutationObserver = new MutationObserver((mutations) => {
|
|
34
|
+
for (const mutation of mutations) {
|
|
35
|
+
for (const node of Array.from(mutation.removedNodes)) {
|
|
36
|
+
if (node === iframe || node instanceof Node && node.contains(iframe)) {
|
|
37
|
+
options.onRemoved?.();
|
|
38
|
+
if (options.emitCloseOnRemoval) {
|
|
39
|
+
client.emit("checkout.modal_closed", {
|
|
40
|
+
reason: options.closedReason ?? "iframe_removed"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
49
|
+
};
|
|
50
|
+
const installBootstrapObserver = () => {
|
|
51
|
+
if (typeof document.addEventListener !== "function") {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
bootstrapHandler = () => {
|
|
55
|
+
options.onBootstrapHidden?.();
|
|
56
|
+
if (options.emitCloseOnBootstrapHidden) {
|
|
57
|
+
client.emit("checkout.modal_closed", {
|
|
58
|
+
reason: options.bootstrapClosedReason ?? options.closedReason ?? "bootstrap_hidden"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
document.addEventListener("hidden.bs.modal", bootstrapHandler, true);
|
|
63
|
+
};
|
|
64
|
+
const handleDetected = (iframe) => {
|
|
65
|
+
detectedIframe = iframe;
|
|
66
|
+
stopPolling();
|
|
67
|
+
installRemovalObserver(iframe);
|
|
68
|
+
installBootstrapObserver();
|
|
69
|
+
visibilityHandle = observeElementVisibility(iframe, (visible) => {
|
|
70
|
+
options.onVisibilityChange?.(visible);
|
|
71
|
+
});
|
|
72
|
+
options.onDetected?.(iframe);
|
|
73
|
+
client.emit("checkout.iframe_detected", {
|
|
74
|
+
iframeSrc: iframe.src || null
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const existing = findIframe();
|
|
78
|
+
if (existing) {
|
|
79
|
+
queueMicrotask(() => handleDetected(existing));
|
|
80
|
+
} else {
|
|
81
|
+
pollTimer = setInterval(() => {
|
|
82
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
83
|
+
stopPolling();
|
|
84
|
+
options.onTimeout?.();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const iframe = findIframe();
|
|
88
|
+
if (iframe && iframe !== detectedIframe) {
|
|
89
|
+
handleDetected(iframe);
|
|
90
|
+
}
|
|
91
|
+
}, intervalMs);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
uninstall() {
|
|
95
|
+
stopPolling();
|
|
96
|
+
mutationObserver?.disconnect();
|
|
97
|
+
mutationObserver = null;
|
|
98
|
+
visibilityHandle?.uninstall();
|
|
99
|
+
visibilityHandle = null;
|
|
100
|
+
if (bootstrapHandler) {
|
|
101
|
+
document.removeEventListener("hidden.bs.modal", bootstrapHandler, true);
|
|
102
|
+
bootstrapHandler = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|