@artaio/arta-browser 2.22.0 → 2.23.1
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 +38 -3
- package/dist/arta.d.ts +9 -0
- package/dist/arta.js +41 -0
- package/dist/bundle.js +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +6 -0
- package/dist/infoConfig.d.ts +13 -0
- package/dist/infoConfig.js +10 -0
- package/dist/infoModal.d.ts +29 -0
- package/dist/infoModal.js +234 -0
- package/dist/messaging.d.ts +60 -0
- package/dist/messaging.js +86 -0
- package/dist/monthlyEstimate.d.ts +9 -0
- package/dist/monthlyEstimate.js +35 -0
- package/dist/pay.d.ts +37 -0
- package/dist/pay.js +351 -0
- package/dist/payConfig.d.ts +39 -0
- package/dist/payConfig.js +24 -0
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,17 @@ import './components/Modal/index.css';
|
|
|
3
3
|
import './components/TrackingDrawer/index.css';
|
|
4
4
|
import Estimate from './estimate';
|
|
5
5
|
import Tracking from './tracking';
|
|
6
|
+
import Pay from './pay';
|
|
7
|
+
import InfoModal from './infoModal';
|
|
6
8
|
export * from './arta';
|
|
7
9
|
export * from './estimate';
|
|
8
10
|
export * from './tracking';
|
|
9
|
-
export { Estimate, Tracking };
|
|
11
|
+
export { Estimate, Tracking, Pay, InfoModal };
|
|
12
|
+
export { monthlyEstimate } from './monthlyEstimate';
|
|
13
|
+
export type { InfoInput, InfoConfig, PartialInfoConfig, InfoFullConfig, } from './infoConfig';
|
|
14
|
+
export type { MonthlyEstimate, MonthlyEstimateInput } from './monthlyEstimate';
|
|
15
|
+
export type { PayInput, PayCallbacks, PayCompletion, PayCompletionStatus, PayCloseEvent, PayCloseReason, PayError, PayPosition, PayConfig, PartialPayConfig, PayFullConfig, } from './payConfig';
|
|
16
|
+
export type { PayInboundMessage, PayOutboundMessage } from './messaging';
|
|
10
17
|
export * from './MetadataTypes';
|
|
11
18
|
export type { EstimateConfig, EstimateBody, PartialEstimateConfig, } from './estimateConfig';
|
|
12
19
|
export type { TrackingConfig, PartialTrackingConfig } from './trackingConfig';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var arta = require('./arta.js');
|
|
6
6
|
var estimate = require('./estimate.js');
|
|
7
7
|
var tracking = require('./tracking.js');
|
|
8
|
+
var pay = require('./pay.js');
|
|
9
|
+
var infoModal = require('./infoModal.js');
|
|
10
|
+
var monthlyEstimate = require('./monthlyEstimate.js');
|
|
8
11
|
|
|
9
12
|
const init = () => {
|
|
10
13
|
window.Arta = new arta.default();
|
|
@@ -14,4 +17,7 @@ var index = window.Arta;
|
|
|
14
17
|
|
|
15
18
|
exports.Estimate = estimate.default;
|
|
16
19
|
exports.Tracking = tracking.default;
|
|
20
|
+
exports.Pay = pay.default;
|
|
21
|
+
exports.InfoModal = infoModal.default;
|
|
22
|
+
exports.monthlyEstimate = monthlyEstimate.monthlyEstimate;
|
|
17
23
|
exports.default = index;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ArtaJsFullConfig } from './arta';
|
|
2
|
+
import { type PayPosition, DEFAULT_PAY_ORIGIN } from './payConfig';
|
|
3
|
+
export interface InfoInput {
|
|
4
|
+
totalPrice?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface InfoConfig {
|
|
7
|
+
position: PayPosition;
|
|
8
|
+
}
|
|
9
|
+
export type PartialInfoConfig = Partial<InfoConfig>;
|
|
10
|
+
export interface InfoFullConfig extends InfoConfig, ArtaJsFullConfig {
|
|
11
|
+
}
|
|
12
|
+
export declare const getFullInfoConfig: (artaConfig: ArtaJsFullConfig, infoConfig?: PartialInfoConfig) => InfoFullConfig;
|
|
13
|
+
export { DEFAULT_PAY_ORIGIN };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const defaultInfoConfig = {
|
|
4
|
+
position: 'center',
|
|
5
|
+
};
|
|
6
|
+
const getFullInfoConfig = (artaConfig, infoConfig = {}) => {
|
|
7
|
+
return Object.assign({}, defaultInfoConfig, artaConfig, infoConfig);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
exports.getFullInfoConfig = getFullInfoConfig;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InfoFullConfig, InfoInput } from './infoConfig';
|
|
2
|
+
export default class InfoModal {
|
|
3
|
+
private readonly input;
|
|
4
|
+
private readonly config;
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
private overlay;
|
|
7
|
+
private iframe;
|
|
8
|
+
private destroyed;
|
|
9
|
+
private frameShown;
|
|
10
|
+
private revealTimer;
|
|
11
|
+
private lastFrameHeight;
|
|
12
|
+
private readonly payOrigin;
|
|
13
|
+
private readonly messageListener;
|
|
14
|
+
private readonly keydownListener;
|
|
15
|
+
constructor(input: InfoInput, config: InfoFullConfig);
|
|
16
|
+
open(): void;
|
|
17
|
+
close(): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
private mount;
|
|
20
|
+
private centerHeightPx;
|
|
21
|
+
private frameSrc;
|
|
22
|
+
private reveal;
|
|
23
|
+
private showFrame;
|
|
24
|
+
private enableFrameTransition;
|
|
25
|
+
private clearRevealTimer;
|
|
26
|
+
private handleMessage;
|
|
27
|
+
private handleResize;
|
|
28
|
+
private applyFrameHeight;
|
|
29
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var messaging = require('./messaging.js');
|
|
6
|
+
var payConfig = require('./payConfig.js');
|
|
7
|
+
|
|
8
|
+
// Arta Pay CTA info modal: a login-free marketing modal opened on demand via
|
|
9
|
+
// `Arta.openArtaPayInfoModal()`. Unlike the checkout modal it has no purchase
|
|
10
|
+
// request, no client token and no handshake — it mounts on `open()` and only
|
|
11
|
+
// listens for the iframe's resize/close reports. The backdrop shows at once;
|
|
12
|
+
// the card itself appears on the widget's first height report (or after a
|
|
13
|
+
// short grace period), so it arrives at its final size instead of resizing in
|
|
14
|
+
// front of the buyer. Every message is accepted only from the iframe this
|
|
15
|
+
// modal created.
|
|
16
|
+
const OVERLAY_ID = 'arta-pay-info-overlay';
|
|
17
|
+
const FRAME_MIN_HEIGHT_PX = 240;
|
|
18
|
+
const RESIZE_DEAD_BAND_PX = 2;
|
|
19
|
+
const FRAME_HEIGHT_TRANSITION = 'height 0.15s ease';
|
|
20
|
+
// How long the card waits for the widget's first height report before it is
|
|
21
|
+
// shown at the placeholder height anyway.
|
|
22
|
+
const FRAME_REVEAL_GRACE_MS = 1_000;
|
|
23
|
+
const overlayBaseCss = 'box-sizing:border-box;position:fixed;inset:0;display:flex;background:rgba(17,15,16,0.55);' +
|
|
24
|
+
'z-index:2147483000;visibility:hidden;pointer-events:none;';
|
|
25
|
+
const overlayPositionCss = {
|
|
26
|
+
center: 'align-items:center;justify-content:center;',
|
|
27
|
+
full_screen: 'align-items:stretch;justify-content:stretch;',
|
|
28
|
+
left: 'align-items:stretch;justify-content:flex-start;',
|
|
29
|
+
right: 'align-items:stretch;justify-content:flex-end;',
|
|
30
|
+
};
|
|
31
|
+
// The card starts transparent and without a height transition: the first
|
|
32
|
+
// report sizes it instantly, and only visible changes animate (see
|
|
33
|
+
// `showFrame`). Opacity, not `visibility`: an iframe made explicitly visible
|
|
34
|
+
// would keep painting after the overlay is hidden on close, since CSS lets a
|
|
35
|
+
// visible child override a hidden parent, while opacity leaves the overlay's
|
|
36
|
+
// visibility in charge of both.
|
|
37
|
+
const frameBaseCss = 'box-sizing:content-box;border:0;background:#fff;opacity:0;';
|
|
38
|
+
// Placeholder heights for the centered card until the widget reports its own.
|
|
39
|
+
// The view is taller when it carries a financing estimate (rendered only when
|
|
40
|
+
// a total price is supplied), so the placeholder follows the same condition;
|
|
41
|
+
// neither value has to match the widget exactly.
|
|
42
|
+
const CENTER_HEIGHT_PX = 412;
|
|
43
|
+
const CENTER_WITH_ESTIMATE_HEIGHT_PX = 753;
|
|
44
|
+
const framePositionCss = (centerHeightPx) => ({
|
|
45
|
+
center: `width:min(576px, calc(100vw - 32px));height:min(${centerHeightPx}px, calc(100vh - 32px));` +
|
|
46
|
+
'border:1px solid #d2d2d2;border-radius:8px;' +
|
|
47
|
+
'box-shadow:0 24px 64px rgba(17,15,16,0.35);',
|
|
48
|
+
full_screen: 'width:100vw;height:100vh;',
|
|
49
|
+
left: 'width:min(460px, 100vw);height:100vh;border-right:1px solid #d2d2d2;' +
|
|
50
|
+
'box-shadow:24px 0 64px rgba(17,15,16,0.35);',
|
|
51
|
+
right: 'width:min(460px, 100vw);height:100vh;border-left:1px solid #d2d2d2;' +
|
|
52
|
+
'box-shadow:-24px 0 64px rgba(17,15,16,0.35);',
|
|
53
|
+
});
|
|
54
|
+
class InfoModal {
|
|
55
|
+
input;
|
|
56
|
+
config;
|
|
57
|
+
isOpen = false;
|
|
58
|
+
overlay;
|
|
59
|
+
iframe;
|
|
60
|
+
destroyed = false;
|
|
61
|
+
frameShown = false;
|
|
62
|
+
revealTimer;
|
|
63
|
+
lastFrameHeight;
|
|
64
|
+
payOrigin;
|
|
65
|
+
messageListener = (event) => this.handleMessage(event);
|
|
66
|
+
keydownListener = (event) => {
|
|
67
|
+
if (event.key === 'Escape' && this.isOpen) {
|
|
68
|
+
this.close();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
constructor(input, config) {
|
|
72
|
+
this.input = input;
|
|
73
|
+
this.config = config;
|
|
74
|
+
this.payOrigin = config.payOrigin ?? payConfig.DEFAULT_PAY_ORIGIN;
|
|
75
|
+
}
|
|
76
|
+
open() {
|
|
77
|
+
if (this.destroyed) {
|
|
78
|
+
throw new Error('This Arta Pay info modal has been destroyed');
|
|
79
|
+
}
|
|
80
|
+
if (!this.overlay) {
|
|
81
|
+
this.mount();
|
|
82
|
+
}
|
|
83
|
+
this.reveal();
|
|
84
|
+
}
|
|
85
|
+
// Hides the overlay without tearing down the iframe, so a later `open()`
|
|
86
|
+
// shows it again instantly.
|
|
87
|
+
close() {
|
|
88
|
+
this.clearRevealTimer();
|
|
89
|
+
if (this.overlay) {
|
|
90
|
+
this.overlay.style.visibility = 'hidden';
|
|
91
|
+
this.overlay.style.pointerEvents = 'none';
|
|
92
|
+
}
|
|
93
|
+
if (this.iframe) {
|
|
94
|
+
this.iframe.style.transition = '';
|
|
95
|
+
}
|
|
96
|
+
document.removeEventListener('keydown', this.keydownListener);
|
|
97
|
+
this.isOpen = false;
|
|
98
|
+
}
|
|
99
|
+
destroy() {
|
|
100
|
+
this.close();
|
|
101
|
+
window.removeEventListener('message', this.messageListener);
|
|
102
|
+
this.overlay?.remove();
|
|
103
|
+
this.overlay = undefined;
|
|
104
|
+
this.iframe = undefined;
|
|
105
|
+
this.destroyed = true;
|
|
106
|
+
}
|
|
107
|
+
mount() {
|
|
108
|
+
if (this.destroyed || this.overlay || !document.body) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
window.addEventListener('message', this.messageListener);
|
|
112
|
+
const position = this.config.position;
|
|
113
|
+
this.overlay = document.createElement('div');
|
|
114
|
+
this.overlay.id = OVERLAY_ID;
|
|
115
|
+
this.overlay.style.cssText = overlayBaseCss + overlayPositionCss[position];
|
|
116
|
+
this.overlay.addEventListener('click', (event) => {
|
|
117
|
+
if (event.target === this.overlay) {
|
|
118
|
+
this.close();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
this.iframe = document.createElement('iframe');
|
|
122
|
+
this.iframe.style.cssText =
|
|
123
|
+
frameBaseCss + framePositionCss(this.centerHeightPx())[position];
|
|
124
|
+
this.iframe.setAttribute('title', 'Arta Pay');
|
|
125
|
+
this.iframe.src = this.frameSrc();
|
|
126
|
+
this.overlay.appendChild(this.iframe);
|
|
127
|
+
document.body.appendChild(this.overlay);
|
|
128
|
+
}
|
|
129
|
+
// The estimate is what makes the view taller, and it is rendered exactly when
|
|
130
|
+
// a total price rides the URL, so the same condition picks the opening height.
|
|
131
|
+
centerHeightPx() {
|
|
132
|
+
return this.input.totalPrice === undefined
|
|
133
|
+
? CENTER_HEIGHT_PX
|
|
134
|
+
: CENTER_WITH_ESTIMATE_HEIGHT_PX;
|
|
135
|
+
}
|
|
136
|
+
// The public key is an identifier, not a credential; the optional total price
|
|
137
|
+
// only drives a financing estimate. Both ride the URL because the info modal
|
|
138
|
+
// has no postMessage handshake.
|
|
139
|
+
frameSrc() {
|
|
140
|
+
let src = this.payOrigin +
|
|
141
|
+
'/artapay-widget/info?pk=' +
|
|
142
|
+
encodeURIComponent(this.config.apiKey);
|
|
143
|
+
if (this.input.totalPrice !== undefined) {
|
|
144
|
+
src +=
|
|
145
|
+
'&total_price=' + encodeURIComponent(String(this.input.totalPrice));
|
|
146
|
+
}
|
|
147
|
+
return src;
|
|
148
|
+
}
|
|
149
|
+
reveal() {
|
|
150
|
+
if (!this.overlay) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.overlay.style.visibility = 'visible';
|
|
154
|
+
this.overlay.style.pointerEvents = 'auto';
|
|
155
|
+
document.addEventListener('keydown', this.keydownListener);
|
|
156
|
+
this.isOpen = true;
|
|
157
|
+
if (this.frameShown) {
|
|
158
|
+
// Reopened: the card already has its measured height, so it shows at
|
|
159
|
+
// once and later changes animate.
|
|
160
|
+
this.enableFrameTransition();
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.revealTimer = window.setTimeout(() => this.showFrame(), FRAME_REVEAL_GRACE_MS);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Called on the first height report (or the grace timeout). The card is
|
|
167
|
+
// sized before it becomes visible, so the buyer never sees it resize.
|
|
168
|
+
showFrame() {
|
|
169
|
+
this.clearRevealTimer();
|
|
170
|
+
if (!this.iframe || this.frameShown) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.frameShown = true;
|
|
174
|
+
this.iframe.style.opacity = '1';
|
|
175
|
+
this.enableFrameTransition();
|
|
176
|
+
}
|
|
177
|
+
enableFrameTransition() {
|
|
178
|
+
if (!this.iframe) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Flush any height applied while hidden before turning the transition on,
|
|
182
|
+
// or that pending change would animate as the card appears.
|
|
183
|
+
void this.iframe.offsetHeight;
|
|
184
|
+
this.iframe.style.transition = FRAME_HEIGHT_TRANSITION;
|
|
185
|
+
}
|
|
186
|
+
clearRevealTimer() {
|
|
187
|
+
if (this.revealTimer !== undefined) {
|
|
188
|
+
window.clearTimeout(this.revealTimer);
|
|
189
|
+
this.revealTimer = undefined;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
handleMessage(event) {
|
|
193
|
+
if (event.origin !== this.payOrigin ||
|
|
194
|
+
!this.iframe ||
|
|
195
|
+
event.source !== this.iframe.contentWindow ||
|
|
196
|
+
!messaging.isPayInboundMessage(event.data)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const data = event.data;
|
|
200
|
+
if (data.type === 'arta-pay:resize') {
|
|
201
|
+
this.handleResize(data);
|
|
202
|
+
}
|
|
203
|
+
else if (data.type === 'arta-pay:close') {
|
|
204
|
+
this.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
handleResize(data) {
|
|
208
|
+
const height = messaging.parseResizeHeight(data);
|
|
209
|
+
if (height === undefined) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Side panels and full screen keep the whole viewport height.
|
|
213
|
+
if (this.config.position === 'center') {
|
|
214
|
+
this.applyFrameHeight(height);
|
|
215
|
+
}
|
|
216
|
+
if (this.isOpen) {
|
|
217
|
+
this.showFrame();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
applyFrameHeight(height) {
|
|
221
|
+
if (!this.iframe) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const px = Math.max(FRAME_MIN_HEIGHT_PX, Math.round(height));
|
|
225
|
+
if (this.lastFrameHeight !== undefined &&
|
|
226
|
+
Math.abs(px - this.lastFrameHeight) < RESIZE_DEAD_BAND_PX) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.lastFrameHeight = px;
|
|
230
|
+
this.iframe.style.height = `min(${px}px, calc(100vh - 32px))`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
exports.default = InfoModal;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type PayCloseReason, type PayCompletionStatus, type PayError, type PayPosition } from './payConfig';
|
|
2
|
+
export interface PayHandshakeMessage {
|
|
3
|
+
type: 'arta-pay:handshake';
|
|
4
|
+
}
|
|
5
|
+
export interface PayReadyMessage {
|
|
6
|
+
type: 'arta-pay:ready';
|
|
7
|
+
}
|
|
8
|
+
export interface PayResizeMessage {
|
|
9
|
+
type: 'arta-pay:resize';
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
export interface PayCompleteMessage {
|
|
13
|
+
type: 'arta-pay:complete';
|
|
14
|
+
purchaseRequestId: string;
|
|
15
|
+
purchaseId?: string;
|
|
16
|
+
status: PayCompletionStatus;
|
|
17
|
+
}
|
|
18
|
+
export interface PayCloseMessage {
|
|
19
|
+
type: 'arta-pay:close';
|
|
20
|
+
purchaseRequestId: string;
|
|
21
|
+
reason: PayCloseReason;
|
|
22
|
+
}
|
|
23
|
+
export interface PayErrorMessage {
|
|
24
|
+
type: 'arta-pay:error';
|
|
25
|
+
code: string;
|
|
26
|
+
message: string;
|
|
27
|
+
recoverable: boolean;
|
|
28
|
+
requestId?: string;
|
|
29
|
+
}
|
|
30
|
+
export type PayInboundMessage = PayHandshakeMessage | PayReadyMessage | PayResizeMessage | PayCompleteMessage | PayCloseMessage | PayErrorMessage;
|
|
31
|
+
export type PayInboundMessageType = PayInboundMessage['type'];
|
|
32
|
+
export interface PayInitMessage {
|
|
33
|
+
type: 'arta-pay:init';
|
|
34
|
+
purchaseRequestId: string;
|
|
35
|
+
clientToken: string;
|
|
36
|
+
position: PayPosition;
|
|
37
|
+
}
|
|
38
|
+
export interface PayOpenMessage {
|
|
39
|
+
type: 'arta-pay:open';
|
|
40
|
+
}
|
|
41
|
+
export interface PayCloseRequestMessage {
|
|
42
|
+
type: 'arta-pay:close-request';
|
|
43
|
+
}
|
|
44
|
+
export type PayOutboundMessage = PayInitMessage | PayOpenMessage | PayCloseRequestMessage;
|
|
45
|
+
export type PayOutboundMessageType = PayOutboundMessage['type'];
|
|
46
|
+
export interface RawPayMessage {
|
|
47
|
+
type: PayInboundMessageType;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
export declare const isPayInboundMessage: (data: unknown) => data is RawPayMessage;
|
|
51
|
+
export declare const parseResizeHeight: (data: unknown) => number | undefined;
|
|
52
|
+
export interface ParsedCompletion {
|
|
53
|
+
purchaseRequestId?: string;
|
|
54
|
+
purchaseId?: string;
|
|
55
|
+
status: PayCompletionStatus;
|
|
56
|
+
}
|
|
57
|
+
export declare const parseCompletion: (data: unknown) => ParsedCompletion | undefined;
|
|
58
|
+
export declare const parseError: (data: unknown) => PayError;
|
|
59
|
+
export declare const parseCloseReason: (data: unknown) => PayCloseReason;
|
|
60
|
+
export declare const postPayMessage: (target: Window, targetOrigin: string, message: PayOutboundMessage) => void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var payConfig = require('./payConfig.js');
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Parsing. Anything can arrive over postMessage, so the SDK narrows the raw
|
|
7
|
+
// data on the `type` discriminator first and validates each payload
|
|
8
|
+
// separately: the parsers return `undefined` (or a safe default) instead of
|
|
9
|
+
// trusting the shape.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const INBOUND_TYPES = [
|
|
12
|
+
'arta-pay:handshake',
|
|
13
|
+
'arta-pay:ready',
|
|
14
|
+
'arta-pay:resize',
|
|
15
|
+
'arta-pay:complete',
|
|
16
|
+
'arta-pay:close',
|
|
17
|
+
'arta-pay:error',
|
|
18
|
+
];
|
|
19
|
+
const isRecord = (data) => typeof data === 'object' && data !== null;
|
|
20
|
+
const optionalString = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
21
|
+
const isPayInboundMessage = (data) => {
|
|
22
|
+
return (isRecord(data) &&
|
|
23
|
+
typeof data.type === 'string' &&
|
|
24
|
+
INBOUND_TYPES.indexOf(data.type) !== -1);
|
|
25
|
+
};
|
|
26
|
+
const parseResizeHeight = (data) => {
|
|
27
|
+
if (!isRecord(data)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const height = data.height;
|
|
31
|
+
return typeof height === 'number' && isFinite(height) && height > 0
|
|
32
|
+
? height
|
|
33
|
+
: undefined;
|
|
34
|
+
};
|
|
35
|
+
// `undefined` for anything that is not a v2 outcome (including the v1
|
|
36
|
+
// `authenticated` signal).
|
|
37
|
+
const parseCompletion = (data) => {
|
|
38
|
+
if (!isRecord(data)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const status = data.status;
|
|
42
|
+
if (typeof status !== 'string' ||
|
|
43
|
+
payConfig.PAY_COMPLETION_STATUSES.indexOf(status) === -1) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (data.purchaseId !== undefined && typeof data.purchaseId !== 'string') {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
purchaseRequestId: optionalString(data.purchaseRequestId),
|
|
51
|
+
purchaseId: optionalString(data.purchaseId),
|
|
52
|
+
status: status,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
// Never returns `undefined`: an error message with a broken payload is still
|
|
56
|
+
// an error worth reporting, so malformed fields fall back to safe defaults
|
|
57
|
+
// (`unknown`, non-recoverable).
|
|
58
|
+
const parseError = (data) => {
|
|
59
|
+
const record = isRecord(data) ? data : {};
|
|
60
|
+
return {
|
|
61
|
+
code: optionalString(record.code) ?? 'unknown',
|
|
62
|
+
message: optionalString(record.message) ?? 'Unknown error',
|
|
63
|
+
recoverable: record.recoverable === true,
|
|
64
|
+
requestId: optionalString(record.requestId),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const parseCloseReason = (data) => {
|
|
68
|
+
if (!isRecord(data)) {
|
|
69
|
+
return 'customer';
|
|
70
|
+
}
|
|
71
|
+
const reason = data.reason;
|
|
72
|
+
return typeof reason === 'string' &&
|
|
73
|
+
payConfig.PAY_CLOSE_REASONS.indexOf(reason) !== -1
|
|
74
|
+
? reason
|
|
75
|
+
: 'customer';
|
|
76
|
+
};
|
|
77
|
+
const postPayMessage = (target, targetOrigin, message) => {
|
|
78
|
+
target.postMessage(message, targetOrigin);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
exports.isPayInboundMessage = isPayInboundMessage;
|
|
82
|
+
exports.parseCloseReason = parseCloseReason;
|
|
83
|
+
exports.parseCompletion = parseCompletion;
|
|
84
|
+
exports.parseError = parseError;
|
|
85
|
+
exports.parseResizeHeight = parseResizeHeight;
|
|
86
|
+
exports.postPayMessage = postPayMessage;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface MonthlyEstimateInput {
|
|
2
|
+
totalPrice: number;
|
|
3
|
+
currency?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MonthlyEstimate {
|
|
6
|
+
amountPerMonth: number;
|
|
7
|
+
amountPerMonthCurrency: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const monthlyEstimate: (input: MonthlyEstimateInput) => Promise<MonthlyEstimate>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MINIMUM_DEPOSIT_RATIO = 0.1;
|
|
4
|
+
const FLAT_FEE_RATE = 0.125;
|
|
5
|
+
const INSTALLMENT_COUNT = 9;
|
|
6
|
+
// A lightweight "from $X/mo" estimate for placements and CTAs, mirroring the
|
|
7
|
+
// checkout slider's math (Buyers.ArtaPay.DepositMath + FinancingPreview): the
|
|
8
|
+
// minimum 10% deposit, a flat 12.5% finance charge on the financed amount, split
|
|
9
|
+
// over 9 monthly payments, and rounded up to the next whole dollar as shown in
|
|
10
|
+
// the interface. Computed in integer cents to avoid floating-point drift. The
|
|
11
|
+
// checkout shows the buyer's exact, quoted schedule.
|
|
12
|
+
//
|
|
13
|
+
// Async on purpose: the rates above are a copy of the server's model, and this
|
|
14
|
+
// is meant to move behind an API call so the two cannot drift. Returning a
|
|
15
|
+
// promise now means that move is an implementation change rather than a
|
|
16
|
+
// breaking one for callers.
|
|
17
|
+
const monthlyEstimate = async (input) => {
|
|
18
|
+
const currency = input.currency || 'USD';
|
|
19
|
+
const totalCents = Math.round(input.totalPrice * 100);
|
|
20
|
+
if (!isFinite(totalCents) || totalCents <= 0) {
|
|
21
|
+
return { amountPerMonth: 0, amountPerMonthCurrency: currency };
|
|
22
|
+
}
|
|
23
|
+
const minDepositCents = Math.ceil(totalCents * MINIMUM_DEPOSIT_RATIO);
|
|
24
|
+
const financedCents = totalCents - minDepositCents;
|
|
25
|
+
const financeChargeCents = Math.round(financedCents * FLAT_FEE_RATE);
|
|
26
|
+
const principalCents = Math.round(financedCents / INSTALLMENT_COUNT);
|
|
27
|
+
const feeCents = Math.round(financeChargeCents / INSTALLMENT_COUNT);
|
|
28
|
+
const monthlyCents = principalCents + feeCents;
|
|
29
|
+
return {
|
|
30
|
+
amountPerMonth: Math.ceil(monthlyCents / 100),
|
|
31
|
+
amountPerMonthCurrency: currency,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
exports.monthlyEstimate = monthlyEstimate;
|
package/dist/pay.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type PayCallbacks, type PayFullConfig, type PayInput } from './payConfig';
|
|
2
|
+
export default class Pay {
|
|
3
|
+
private readonly input;
|
|
4
|
+
private readonly callbacks;
|
|
5
|
+
private readonly config;
|
|
6
|
+
isReady: boolean;
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
private overlay;
|
|
9
|
+
private iframe;
|
|
10
|
+
private handshakeTimer;
|
|
11
|
+
private destroyed;
|
|
12
|
+
private completed;
|
|
13
|
+
private openWhenMounted;
|
|
14
|
+
private lastFrameHeight;
|
|
15
|
+
private readonly payOrigin;
|
|
16
|
+
private readonly messageListener;
|
|
17
|
+
private readonly keydownListener;
|
|
18
|
+
private readonly domReadyListener;
|
|
19
|
+
constructor(input: PayInput, callbacks: PayCallbacks, config: PayFullConfig);
|
|
20
|
+
open(): void;
|
|
21
|
+
close(): void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
private mount;
|
|
24
|
+
private reveal;
|
|
25
|
+
private requestClose;
|
|
26
|
+
private handleMessage;
|
|
27
|
+
private handleHandshake;
|
|
28
|
+
private handleReady;
|
|
29
|
+
private handleResize;
|
|
30
|
+
private handleComplete;
|
|
31
|
+
private handleError;
|
|
32
|
+
private handleHandshakeTimeout;
|
|
33
|
+
private abort;
|
|
34
|
+
private postToFrame;
|
|
35
|
+
private startHandshakeTimer;
|
|
36
|
+
private clearHandshakeTimer;
|
|
37
|
+
}
|