@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.
Files changed (107) hide show
  1. package/README.md +19 -276
  2. package/dist/adapters/browser-lifecycle-adapter.cjs +51 -0
  3. package/dist/adapters/browser-lifecycle-adapter.js +48 -0
  4. package/dist/adapters/iframe-detector-adapter.cjs +108 -0
  5. package/dist/adapters/iframe-detector-adapter.js +106 -0
  6. package/dist/client/create-telemetry-client.cjs +136 -0
  7. package/dist/client/create-telemetry-client.js +134 -0
  8. package/dist/client/state-machine.cjs +20 -0
  9. package/dist/client/state-machine.js +18 -0
  10. package/dist/diagnostics/diagnostics-buffer.cjs +34 -0
  11. package/dist/diagnostics/diagnostics-buffer.js +32 -0
  12. package/dist/diagnostics/preflight.cjs +36 -0
  13. package/dist/diagnostics/preflight.js +34 -0
  14. package/dist/events/envelope.cjs +23 -0
  15. package/dist/events/envelope.js +21 -0
  16. package/dist/index.cjs +23 -0
  17. package/dist/index.d.ts +230 -0
  18. package/dist/index.js +10 -0
  19. package/dist/persistence/local-storage-outbox.cjs +56 -0
  20. package/dist/persistence/local-storage-outbox.js +54 -0
  21. package/dist/persistence/memory-outbox.cjs +23 -0
  22. package/dist/persistence/memory-outbox.js +21 -0
  23. package/dist/runtime/event-pipeline.cjs +64 -0
  24. package/dist/runtime/event-pipeline.js +62 -0
  25. package/dist/runtime/heartbeat-scheduler.cjs +46 -0
  26. package/dist/runtime/heartbeat-scheduler.js +44 -0
  27. package/dist/runtime/session-manager.cjs +47 -0
  28. package/dist/runtime/session-manager.js +45 -0
  29. package/dist/transport/beacon.cjs +14 -0
  30. package/dist/transport/beacon.js +12 -0
  31. package/dist/transport/callback-transport.cjs +19 -0
  32. package/dist/transport/callback-transport.js +17 -0
  33. package/dist/transport/http-transport.cjs +62 -0
  34. package/dist/transport/http-transport.js +60 -0
  35. package/dist/types/internal.cjs +3 -0
  36. package/dist/types/internal.js +0 -0
  37. package/dist/types/public.cjs +3 -0
  38. package/dist/types/public.js +0 -0
  39. package/dist/utils/ids.cjs +20 -0
  40. package/dist/utils/ids.js +17 -0
  41. package/dist/utils/safe-globals.cjs +11 -0
  42. package/dist/utils/safe-globals.js +9 -0
  43. package/dist/version.cjs +5 -0
  44. package/dist/version.js +2 -0
  45. package/package.json +29 -89
  46. package/PRIVACY.md +0 -67
  47. package/dist/auto-patch.cjs +0 -171
  48. package/dist/auto-patch.cjs.map +0 -7
  49. package/dist/auto-patch.mjs +0 -148
  50. package/dist/auto-patch.mjs.map +0 -7
  51. package/dist/session.cjs +0 -1186
  52. package/dist/session.cjs.map +0 -7
  53. package/dist/session.mjs +0 -1163
  54. package/dist/session.mjs.map +0 -7
  55. package/dist/types/auto-patch.d.ts +0 -9
  56. package/dist/types/auto-patch.d.ts.map +0 -1
  57. package/dist/types/core/beacon.d.ts +0 -6
  58. package/dist/types/core/beacon.d.ts.map +0 -1
  59. package/dist/types/core/detection.d.ts +0 -34
  60. package/dist/types/core/detection.d.ts.map +0 -1
  61. package/dist/types/core/event-bus.d.ts +0 -21
  62. package/dist/types/core/event-bus.d.ts.map +0 -1
  63. package/dist/types/core/experimental.d.ts +0 -35
  64. package/dist/types/core/experimental.d.ts.map +0 -1
  65. package/dist/types/core/heartbeat.d.ts +0 -32
  66. package/dist/types/core/heartbeat.d.ts.map +0 -1
  67. package/dist/types/core/lifecycle.d.ts +0 -31
  68. package/dist/types/core/lifecycle.d.ts.map +0 -1
  69. package/dist/types/core/observer.d.ts +0 -10
  70. package/dist/types/core/observer.d.ts.map +0 -1
  71. package/dist/types/core/outbox.d.ts +0 -20
  72. package/dist/types/core/outbox.d.ts.map +0 -1
  73. package/dist/types/core/random.d.ts +0 -8
  74. package/dist/types/core/random.d.ts.map +0 -1
  75. package/dist/types/core/shortcode.d.ts +0 -17
  76. package/dist/types/core/shortcode.d.ts.map +0 -1
  77. package/dist/types/core/types.d.ts +0 -292
  78. package/dist/types/core/types.d.ts.map +0 -1
  79. package/dist/types/diagnostics/preflight.d.ts +0 -17
  80. package/dist/types/diagnostics/preflight.d.ts.map +0 -1
  81. package/dist/types/index.d.ts +0 -41
  82. package/dist/types/index.d.ts.map +0 -1
  83. package/dist/types/integration/devicefp-bridge.d.ts +0 -31
  84. package/dist/types/integration/devicefp-bridge.d.ts.map +0 -1
  85. package/dist/types/integration/hpp-bridge.d.ts +0 -13
  86. package/dist/types/integration/hpp-bridge.d.ts.map +0 -1
  87. package/dist/types/integration/zenpay-auto-patch.d.ts +0 -28
  88. package/dist/types/integration/zenpay-auto-patch.d.ts.map +0 -1
  89. package/dist/types/outcome.d.ts +0 -20
  90. package/dist/types/outcome.d.ts.map +0 -1
  91. package/dist/types/session.d.ts +0 -54
  92. package/dist/types/session.d.ts.map +0 -1
  93. package/dist/types/transport/callback-transport.d.ts +0 -17
  94. package/dist/types/transport/callback-transport.d.ts.map +0 -1
  95. package/dist/types/transport/http-transport.d.ts +0 -30
  96. package/dist/types/transport/http-transport.d.ts.map +0 -1
  97. package/dist/types/umd.d.ts +0 -16
  98. package/dist/types/umd.d.ts.map +0 -1
  99. package/dist/zp-observer.cjs +0 -1375
  100. package/dist/zp-observer.cjs.map +0 -7
  101. package/dist/zp-observer.js +0 -1377
  102. package/dist/zp-observer.js.map +0 -7
  103. package/dist/zp-observer.min.js +0 -2
  104. package/dist/zp-observer.min.js.map +0 -7
  105. package/dist/zp-observer.min.obf.js +0 -1
  106. package/dist/zp-observer.mjs +0 -1352
  107. package/dist/zp-observer.mjs.map +0 -7
package/README.md CHANGED
@@ -1,284 +1,27 @@
1
- # @ianmenethil/zp-observer
1
+ # ZP Telemetry Rewrite
2
2
 
3
- Telemetry for **Zenith hosted checkout**: v6 adds **domain events**, **correlation IDs**, **persistent outbox**, and a **unified session entry point** on top of the proven open/heartbeat/close model. Cross-page outcome reporting, device fingerprint bridge hooks, and HPP lifecycle observation are all wired through a single typed event bus.
3
+ This package is the clean native-only telemetry SDK for internal checkout integrations.
4
4
 
5
- **Pick one path:**
5
+ Current scope:
6
6
 
7
- | You are… | Go to |
8
- |----------|--------|
9
- | Using **Vite / webpack / Node ESM** | [npm — ES modules](#npm--es-modules-import) |
10
- | Using **CommonJS** (`require`) | [npm CommonJS](#npm--commonjs-require) |
11
- | Using a **plain HTML page** with no bundler | [CDN — `<script>` tag](#cdn--script-tag-no-bundler) |
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
- ## Install (npm only)
17
+ - legacy observer compatibility wrappers
18
+ - staged migration session helpers
19
+ - one-shot compatibility reporters
20
+ - browser-global compatibility entrypoints
16
21
 
17
- ```bash
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
- var observer = ZPObserver.createObserver({
132
- sessionId: mUPID,
133
- transport: ZPObserver.httpTransport({
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
+ }