@morefin/cashier-bootstrapper 0.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 ADDED
@@ -0,0 +1,147 @@
1
+ # Cashier Bootstrapper
2
+
3
+ Lightweight iframe bootstrapper for the Morefin cashier. It builds the cashier URL from request params and mounts the cashier inside an iframe to keep host styles isolated.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @morefin/cashier-bootstrapper
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Throughout the examples below, set a host variable once and reuse it:
14
+
15
+ ```typescript
16
+ const CASHIER_HOST = 'http://localhost:7082'; // change per environment
17
+ ```
18
+
19
+ ### Basic Embed
20
+
21
+ ```typescript
22
+ import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
23
+
24
+ new CashierBootstrapper('#cashier-root', {
25
+ properties: { host: CASHIER_HOST }
26
+ });
27
+ ```
28
+
29
+ ### Custom Request Params and Iframe Options
30
+
31
+ ```typescript
32
+ import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
33
+
34
+ new CashierBootstrapper('#cashier-root', {
35
+ requestParams: {
36
+ merchantId: 'merchant-123',
37
+ terminalId: 'terminal-456',
38
+ userId: 'user-789',
39
+ sessionId: 'session-abc',
40
+ predefinedAmounts: [100, 200, 300],
41
+ layout: 'default'
42
+ },
43
+ properties: {
44
+ host: CASHIER_HOST,
45
+ cashierPath: '/cashier',
46
+ iframe: {
47
+ height: '720px',
48
+ minHeight: '640px',
49
+ title: 'Morefin Cashier'
50
+ }
51
+ }
52
+ }, api => {
53
+ console.log('Cashier iframe ready', api.iframe);
54
+ });
55
+ ```
56
+
57
+ ### Provide Container via Config
58
+
59
+ ```typescript
60
+ import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
61
+
62
+ new CashierBootstrapper(null, {
63
+ properties: {
64
+ host: CASHIER_HOST,
65
+ container: '#cashier-root'
66
+ }
67
+ });
68
+ ```
69
+
70
+ ### Runtime Controls (CSS + Data)
71
+
72
+ ```typescript
73
+ import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
74
+
75
+ new CashierBootstrapper('#cashier-root', {
76
+ properties: { host: CASHIER_HOST }
77
+ }, api => {
78
+ api.setCss(`
79
+ .payment-layout-root .payment-container {
80
+ border: 2px dashed hotpink;
81
+ }
82
+ `);
83
+
84
+ api.updateData({ userId: 'updated-user' });
85
+ });
86
+ ```
87
+
88
+ ## Examples Folder
89
+
90
+ - `examples/npm` contains a minimal app that installs the package from npm and bundles it with esbuild.
91
+ - `src/example-usage.ts` contains additional snippets for reference.
92
+
93
+ ## API
94
+
95
+ ### `new CashierBootstrapper(container, config?, onReady?)`
96
+
97
+ Iframe-based embed that loads the cashier URL and exposes runtime controls once ready.
98
+
99
+ **Parameters:**
100
+ - `container: string | HTMLElement | null | undefined` - Where the iframe is appended. If omitted, `config.properties.container` is used (falling back to `document.body`).
101
+ - `config?: CashierIframeConfig` - Request params and iframe properties. `properties.host` is required.
102
+ - `onReady?: (api: CashierIframeApi) => void` - Called when the iframe loads; exposes helpers:
103
+ - `api.setCss(css: string)` – inject CSS inside the cashier iframe
104
+ - `api.updateData(data: object)` – post updated `APP_DATA` to the cashier
105
+ - `api.pause()` / `api.resume()` – forward pause/resume signals
106
+
107
+ ## Types
108
+
109
+ ```typescript
110
+ interface CashierRequestParams {
111
+ merchantId?: string;
112
+ terminalId?: string;
113
+ userId?: string;
114
+ sessionId?: string;
115
+ predefinedAmounts?: number[];
116
+ layout?: string;
117
+ fingerprint?: string;
118
+ }
119
+
120
+ interface CashierIframeOptions {
121
+ width?: string;
122
+ height?: string;
123
+ minHeight?: string;
124
+ title?: string;
125
+ allow?: string;
126
+ sandbox?: string;
127
+ attributes?: Record<string, string>;
128
+ }
129
+
130
+ interface CashierIframeProperties {
131
+ host: string;
132
+ container?: string | HTMLElement;
133
+ cashierPath?: string;
134
+ iframe?: CashierIframeOptions;
135
+ }
136
+
137
+ interface CashierIframeConfig {
138
+ requestParams?: CashierRequestParams;
139
+ properties?: CashierIframeProperties;
140
+ }
141
+ ```
142
+
143
+ ## Defaults
144
+
145
+ - `cashierPath`: `/cashier`
146
+ - `iframe.width`/`iframe.height`: `100%`
147
+ - `iframe.allow`: `geolocation *;camera *;payment *;clipboard-read *;clipboard-write *;autoplay *;microphone *;fullscreen *;accelerometer *;magnetometer *;gyroscope *;picture-in-picture *;otp-credentials *;`
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Example usage file - demonstrates how to use the cashier bootstrapper.
3
+ * This file is not included in the npm package, it's just for reference.
4
+ */
5
+ export declare function example1(): void;
6
+ export declare function example2(container: string | HTMLElement): void;
7
+ export declare function example3(): void;
8
+ export declare function example4(container: string | HTMLElement): void;
9
+ export declare function example5(container: string | HTMLElement): void;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Example usage file - demonstrates how to use the cashier bootstrapper.
3
+ * This file is not included in the npm package, it's just for reference.
4
+ */
5
+ import { CashierBootstrapper } from './index';
6
+ const CASHIER_HOST = 'http://localhost:7082'; // change per environment
7
+ // Example 1: Minimal configuration with selector
8
+ export function example1() {
9
+ new CashierBootstrapper('#cashier-root', {
10
+ properties: { host: CASHIER_HOST }
11
+ });
12
+ }
13
+ // Example 2: Custom request params + iframe options
14
+ export function example2(container) {
15
+ new CashierBootstrapper(container, {
16
+ requestParams: {
17
+ merchantId: 'merchant-123',
18
+ terminalId: 'terminal-456',
19
+ userId: 'user-789',
20
+ sessionId: 'session-abc',
21
+ predefinedAmounts: [100, 200, 300],
22
+ layout: 'default'
23
+ },
24
+ properties: {
25
+ host: CASHIER_HOST,
26
+ cashierPath: '/cashier',
27
+ iframe: {
28
+ height: '720px',
29
+ minHeight: '640px',
30
+ title: 'Morefin Cashier'
31
+ }
32
+ }
33
+ }, api => {
34
+ console.log('Cashier iframe ready', api.iframe);
35
+ });
36
+ }
37
+ // Example 3: Provide container via config instead of constructor
38
+ export function example3() {
39
+ new CashierBootstrapper(null, {
40
+ properties: { host: CASHIER_HOST, container: '#cashier-root' }
41
+ });
42
+ }
43
+ // Example 4: Update CSS at runtime after iframe is ready
44
+ export function example4(container) {
45
+ new CashierBootstrapper(container, {
46
+ requestParams: {
47
+ merchantId: 'merchant-123',
48
+ terminalId: 'terminal-456',
49
+ userId: 'user-789',
50
+ predefinedAmounts: [100, 200, 300],
51
+ layout: 'default'
52
+ },
53
+ properties: {
54
+ host: CASHIER_HOST
55
+ }
56
+ }, api => {
57
+ api.setCss(`
58
+ .payment-layout-root .payment-container {
59
+ border: 2px dashed hotpink;
60
+ }
61
+ `);
62
+ });
63
+ }
64
+ // Example 5: Updating data via the postMessage API
65
+ export function example5(container) {
66
+ new CashierBootstrapper(container, {
67
+ properties: { host: CASHIER_HOST }
68
+ }, api => {
69
+ api.updateData({
70
+ userId: 'updated-user'
71
+ });
72
+ });
73
+ }
@@ -0,0 +1,30 @@
1
+ import { CashierIframeApi, CashierIframeConfig } from './types';
2
+ export type { CashierRequestParams, CashierIframeApi, CashierIframeConfig, CashierIframeProperties } from './types';
3
+ type FingerprintJSGlobal = {
4
+ load: () => Promise<{
5
+ get: () => Promise<{
6
+ visitorId: string;
7
+ }>;
8
+ }>;
9
+ };
10
+ declare global {
11
+ interface Window {
12
+ FingerprintJS?: FingerprintJSGlobal;
13
+ }
14
+ }
15
+ export declare class CashierBootstrapper {
16
+ private iframe?;
17
+ private origin;
18
+ private ready;
19
+ private readonly fullConfig;
20
+ private readonly onReady?;
21
+ constructor(container: string | HTMLElement | null | undefined, config?: CashierIframeConfig, onReady?: (api: CashierIframeApi) => void);
22
+ private bootstrapIframe;
23
+ private createIframeShell;
24
+ private handleLoad;
25
+ private postMessage;
26
+ /**
27
+ * API exposed to host pages for runtime control.
28
+ */
29
+ api(): CashierIframeApi;
30
+ }
package/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ const DEFAULT_IFRAME_ALLOW = 'geolocation *;camera *;payment *;clipboard-read *;clipboard-write *;autoplay *;microphone *;fullscreen *;accelerometer *;magnetometer *;gyroscope *;picture-in-picture *;otp-credentials *;';
2
+ const FINGERPRINT_CDN = 'https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@4/dist/fp.min.js';
3
+ let fpLoaderPromise;
4
+ function loadFingerprintLibrary() {
5
+ if (typeof window === 'undefined') {
6
+ return Promise.reject(new Error('FingerprintJS requires a browser environment'));
7
+ }
8
+ if (window.FingerprintJS) {
9
+ return Promise.resolve(window.FingerprintJS);
10
+ }
11
+ if (fpLoaderPromise) {
12
+ return fpLoaderPromise;
13
+ }
14
+ fpLoaderPromise = new Promise((resolve, reject) => {
15
+ const script = document.createElement('script');
16
+ script.src = FINGERPRINT_CDN;
17
+ script.async = true;
18
+ script.onload = () => {
19
+ if (window.FingerprintJS) {
20
+ resolve(window.FingerprintJS);
21
+ }
22
+ else {
23
+ reject(new Error('FingerprintJS did not register on window'));
24
+ }
25
+ };
26
+ script.onerror = () => reject(new Error('Failed to load FingerprintJS from CDN'));
27
+ document.head.appendChild(script);
28
+ });
29
+ return fpLoaderPromise;
30
+ }
31
+ async function generateFingerprint() {
32
+ try {
33
+ const fpLib = await loadFingerprintLibrary();
34
+ const fp = await fpLib.load();
35
+ const result = await fp.get();
36
+ return result.visitorId;
37
+ }
38
+ catch (err) {
39
+ console.warn('Fingerprint generation failed; continuing without it', err);
40
+ return undefined;
41
+ }
42
+ }
43
+ function buildQueryString(requestParams) {
44
+ const query = new URLSearchParams();
45
+ if (requestParams.merchantId != null) {
46
+ query.append('merchantId', String(requestParams.merchantId));
47
+ }
48
+ if (requestParams.terminalId != null) {
49
+ query.append('terminalId', String(requestParams.terminalId));
50
+ }
51
+ if (requestParams.userId != null) {
52
+ query.append('userId', String(requestParams.userId));
53
+ }
54
+ if (requestParams.sessionId != null) {
55
+ query.append('sessionId', String(requestParams.sessionId));
56
+ }
57
+ if (Array.isArray(requestParams.predefinedAmounts)) {
58
+ requestParams.predefinedAmounts.forEach(amount => {
59
+ query.append('predefinedAmounts', String(amount));
60
+ });
61
+ }
62
+ if (requestParams.layout != null) {
63
+ query.append('layout', requestParams.layout);
64
+ }
65
+ if (requestParams.fingerprint) {
66
+ query.append('fingerprint', requestParams.fingerprint);
67
+ }
68
+ const qs = query.toString();
69
+ return qs ? `?${qs}` : '';
70
+ }
71
+ function normalizePath(path) {
72
+ if (!path) {
73
+ return '/cashier';
74
+ }
75
+ return path.startsWith('/') ? path : `/${path}`;
76
+ }
77
+ function resolveContainer(container) {
78
+ if (container instanceof HTMLElement) {
79
+ return container;
80
+ }
81
+ if (typeof container === 'string') {
82
+ const target = document.querySelector(container);
83
+ if (target) {
84
+ return target;
85
+ }
86
+ console.warn(`Container selector "${container}" not found, defaulting to body`);
87
+ }
88
+ return document.body;
89
+ }
90
+ function buildIframeUrl(host, cashierPath, requestParams) {
91
+ const trimmedHost = host.endsWith('/') ? host.slice(0, -1) : host;
92
+ const path = normalizePath(cashierPath);
93
+ return `${trimmedHost}${path}${buildQueryString(requestParams)}`;
94
+ }
95
+ function getOriginFromUrl(url) {
96
+ try {
97
+ return new URL(url).origin;
98
+ }
99
+ catch {
100
+ return '*';
101
+ }
102
+ }
103
+ export class CashierBootstrapper {
104
+ constructor(container, config = {}, onReady) {
105
+ this.origin = '*';
106
+ this.ready = false;
107
+ this.onReady = onReady;
108
+ const host = config.properties?.host;
109
+ if (!host) {
110
+ throw new Error('Cashier host is required to bootstrap the iframe');
111
+ }
112
+ this.fullConfig = {
113
+ requestParams: { ...config.requestParams },
114
+ properties: {
115
+ ...config.properties,
116
+ host,
117
+ cashierPath: normalizePath(config.properties?.cashierPath),
118
+ iframe: {
119
+ width: '100%',
120
+ height: '100%',
121
+ allow: DEFAULT_IFRAME_ALLOW,
122
+ ...config.properties?.iframe
123
+ }
124
+ }
125
+ };
126
+ const target = resolveContainer(container ?? this.fullConfig.properties.container);
127
+ this.iframe = this.createIframeShell(target);
128
+ void this.bootstrapIframe();
129
+ }
130
+ async bootstrapIframe() {
131
+ const fp = await generateFingerprint();
132
+ if (fp) {
133
+ this.fullConfig.requestParams.fingerprint = fp;
134
+ }
135
+ const src = buildIframeUrl(this.fullConfig.properties.host, this.fullConfig.properties.cashierPath ?? '/cashier', this.fullConfig.requestParams);
136
+ this.origin = getOriginFromUrl(src);
137
+ if (this.iframe) {
138
+ this.iframe.src = src;
139
+ this.iframe.onload = () => this.handleLoad();
140
+ }
141
+ }
142
+ createIframeShell(container) {
143
+ const iframe = document.createElement('iframe');
144
+ const { iframe: iframeOptions } = this.fullConfig.properties;
145
+ iframe.style.border = 'none';
146
+ iframe.style.width = iframeOptions?.width ?? '100%';
147
+ iframe.style.height = iframeOptions?.height ?? '100%';
148
+ if (iframeOptions?.minHeight) {
149
+ iframe.style.minHeight = iframeOptions.minHeight;
150
+ }
151
+ iframe.title = iframeOptions?.title ?? 'Cashier';
152
+ iframe.allow = iframeOptions?.allow ?? DEFAULT_IFRAME_ALLOW;
153
+ if (iframeOptions?.sandbox) {
154
+ iframe.setAttribute('sandbox', iframeOptions.sandbox);
155
+ }
156
+ if (iframeOptions?.attributes) {
157
+ Object.entries(iframeOptions.attributes).forEach(([key, value]) => iframe.setAttribute(key, value));
158
+ }
159
+ iframe.setAttribute('data-cashier-iframe', 'true');
160
+ iframe.setAttribute('data-cy', 'cashier-iframe');
161
+ container.innerHTML = '';
162
+ container.appendChild(iframe);
163
+ return iframe;
164
+ }
165
+ handleLoad() {
166
+ if (this.ready)
167
+ return;
168
+ this.ready = true;
169
+ if (this.onReady) {
170
+ this.onReady(this.api());
171
+ }
172
+ }
173
+ postMessage(eventType, payload) {
174
+ if (!this.iframe?.contentWindow) {
175
+ return;
176
+ }
177
+ this.iframe.contentWindow.postMessage({ eventType, payload }, this.origin);
178
+ }
179
+ /**
180
+ * API exposed to host pages for runtime control.
181
+ */
182
+ api() {
183
+ if (!this.iframe) {
184
+ throw new Error('Cashier iframe not initialized');
185
+ }
186
+ return {
187
+ iframe: this.iframe,
188
+ setCss: (css) => this.postMessage('SET_CSS', css),
189
+ updateData: (data) => this.postMessage('UPDATE_DATA', data),
190
+ pause: () => this.postMessage('CASHIER_PAUSE', {}),
191
+ resume: () => this.postMessage('CASHIER_RESUME', {})
192
+ };
193
+ }
194
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Cashier request parameters
3
+ */
4
+ export interface CashierRequestParams {
5
+ merchantId?: string;
6
+ terminalId?: string;
7
+ userId?: string;
8
+ sessionId?: string;
9
+ predefinedAmounts?: number[];
10
+ layout?: string;
11
+ fingerprint?: string;
12
+ }
13
+ /**
14
+ * Bootstrap properties configuration
15
+ */
16
+ export interface BootstrapProperties {
17
+ host: string;
18
+ /**
19
+ * Selector or element where HTML should be rendered
20
+ * Default: 'body' (replaces entire body)
21
+ */
22
+ container?: string | HTMLElement;
23
+ }
24
+ /**
25
+ * Bootstrap configuration combining request params and properties
26
+ */
27
+ export interface CashierBootstrapConfig {
28
+ requestParams: CashierRequestParams;
29
+ properties: BootstrapProperties;
30
+ }
31
+ /**
32
+ * Additional iframe options for isolated embedding.
33
+ */
34
+ export interface CashierIframeOptions {
35
+ /**
36
+ * Width of the iframe element. Default: '100%'.
37
+ */
38
+ width?: string;
39
+ /**
40
+ * Height of the iframe element. Default: '100%'.
41
+ */
42
+ height?: string;
43
+ /**
44
+ * Min height used to avoid layout jumps while cashier loads.
45
+ */
46
+ minHeight?: string;
47
+ /**
48
+ * Title attribute for accessibility.
49
+ */
50
+ title?: string;
51
+ /**
52
+ * Allow attribute controlling available browser features.
53
+ */
54
+ allow?: string;
55
+ /**
56
+ * Sandbox attribute if stricter isolation is desired.
57
+ */
58
+ sandbox?: string;
59
+ /**
60
+ * Extra attributes applied to the iframe element.
61
+ */
62
+ attributes?: Record<string, string>;
63
+ }
64
+ /**
65
+ * Properties for iframe embedding.
66
+ */
67
+ export interface CashierIframeProperties extends BootstrapProperties {
68
+ /**
69
+ * Optional DOM container (selector or element) where the iframe is appended.
70
+ */
71
+ container?: string | HTMLElement;
72
+ /**
73
+ * Path to the cashier page. Default: '/cashier'.
74
+ */
75
+ cashierPath?: string;
76
+ /**
77
+ * Iframe tuning options (dimensions, attributes).
78
+ */
79
+ iframe?: CashierIframeOptions;
80
+ }
81
+ /**
82
+ * Config used by the iframe-based bootstrapper class.
83
+ */
84
+ export interface CashierIframeConfig {
85
+ requestParams?: CashierRequestParams;
86
+ properties?: CashierIframeProperties;
87
+ }
88
+ /**
89
+ * Minimal API surface returned to hosts after iframe creation.
90
+ */
91
+ export interface CashierIframeApi {
92
+ iframe: HTMLIFrameElement;
93
+ setCss: (css: string) => void;
94
+ updateData: (data: Record<string, unknown>) => void;
95
+ pause: () => void;
96
+ resume: () => void;
97
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@morefin/cashier-bootstrapper",
3
+ "version": "0.1.0",
4
+ "description": "Bootstrap service for initializing cashier payment page data from API",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "prepublishOnly": "npm run build",
11
+ "clean": "rm -rf dist"
12
+ },
13
+ "keywords": [
14
+ "morefin",
15
+ "cashier",
16
+ "payment",
17
+ "bootstrap",
18
+ "hosted-payment-page"
19
+ ],
20
+ "author": "",
21
+ "license": "ISC",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md"
28
+ ],
29
+ "devDependencies": {
30
+ "@types/node": "^20.11.17",
31
+ "typescript": "^5.8.3"
32
+ }
33
+ }