@amos.com/amos-js 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 +295 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +422 -0
- package/dist/src/google-pay.d.ts +105 -0
- package/dist/src/index.d.ts +52 -0
- package/dist/src/jwt.d.ts +16 -0
- package/dist/src/messaging.d.ts +73 -0
- package/dist/src/mount.d.ts +95 -0
- package/dist/src/payment-method-form.d.ts +90 -0
- package/dist/src/types.d.ts +64 -0
- package/package.json +47 -0
- package/src/google-pay.ts +234 -0
- package/src/index.ts +101 -0
- package/src/jwt.ts +44 -0
- package/src/messaging.ts +175 -0
- package/src/mount.ts +278 -0
- package/src/payment-method-form.ts +180 -0
- package/src/types.ts +171 -0
package/src/messaging.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { components } from "@amos.com/node";
|
|
2
|
+
import { decodeJwt } from "./jwt";
|
|
3
|
+
import { type Appearance, createMessage, type Message } from "./types";
|
|
4
|
+
|
|
5
|
+
type Iframe = HTMLIFrameElement | null | undefined;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Notify the embedded iframe that the host page is ready to receive
|
|
9
|
+
* messages. The SDK calls this internally after receiving an
|
|
10
|
+
* `IFRAME_READY` event from the iframe.
|
|
11
|
+
*/
|
|
12
|
+
export function sendParentReadyMessage(iframe: Iframe): void {
|
|
13
|
+
iframe?.contentWindow?.postMessage(
|
|
14
|
+
createMessage({ type: "PARENT_ACKNOWLEDGED_IFRAME_READY" }),
|
|
15
|
+
"*",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Push appearance overrides into the embedded iframe.
|
|
21
|
+
*
|
|
22
|
+
* Only the provided `themeVariables` are sent; omitted variables keep
|
|
23
|
+
* their defaults.
|
|
24
|
+
*/
|
|
25
|
+
export function updateAppearance({
|
|
26
|
+
iframe,
|
|
27
|
+
appearance = {},
|
|
28
|
+
}: {
|
|
29
|
+
iframe: Iframe;
|
|
30
|
+
appearance?: Appearance;
|
|
31
|
+
}): void {
|
|
32
|
+
iframe?.contentWindow?.postMessage(
|
|
33
|
+
createMessage({ type: "UPDATE_APPEARANCE", appearance }),
|
|
34
|
+
"*",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Push the express-checkout amount into the embedded Google Pay iframe.
|
|
40
|
+
*/
|
|
41
|
+
export function updateAmount({
|
|
42
|
+
iframe,
|
|
43
|
+
amount,
|
|
44
|
+
}: {
|
|
45
|
+
iframe: Iframe;
|
|
46
|
+
amount: string;
|
|
47
|
+
}): void {
|
|
48
|
+
iframe?.contentWindow?.postMessage(
|
|
49
|
+
createMessage({ type: "UPDATE_AMOUNT", amount }),
|
|
50
|
+
"*",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Push the user-visible merchant name into the embedded Google Pay
|
|
56
|
+
* iframe.
|
|
57
|
+
*/
|
|
58
|
+
export function updateMerchantName({
|
|
59
|
+
iframe,
|
|
60
|
+
merchantName,
|
|
61
|
+
}: {
|
|
62
|
+
iframe: Iframe;
|
|
63
|
+
merchantName: string;
|
|
64
|
+
}): void {
|
|
65
|
+
iframe?.contentWindow?.postMessage(
|
|
66
|
+
createMessage({ type: "UPDATE_MERCHANT_NAME", merchantName }),
|
|
67
|
+
"*",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ask the embedded credit-card or bank-account iframe form to validate
|
|
73
|
+
* its inputs.
|
|
74
|
+
*
|
|
75
|
+
* Resolves to `true` if the form is valid, `false` if it is not, or
|
|
76
|
+
* `false` if the iframe does not respond within 5 seconds.
|
|
77
|
+
*/
|
|
78
|
+
export function validateForm({ iframe }: { iframe: Iframe }): Promise<boolean> {
|
|
79
|
+
const requestId = crypto.randomUUID();
|
|
80
|
+
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
iframe?.contentWindow?.postMessage(
|
|
83
|
+
createMessage({ type: "VALIDATE_FORM", requestId }),
|
|
84
|
+
"*",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const timeoutId = setTimeout(() => {
|
|
88
|
+
window.removeEventListener("message", handleMessage);
|
|
89
|
+
resolve(false);
|
|
90
|
+
}, 5000);
|
|
91
|
+
|
|
92
|
+
function handleMessage(event: MessageEvent<Message>) {
|
|
93
|
+
if (
|
|
94
|
+
event.data.type === "VALIDATE_FORM" &&
|
|
95
|
+
event.data.requestId === requestId
|
|
96
|
+
) {
|
|
97
|
+
window.removeEventListener("message", handleMessage);
|
|
98
|
+
clearTimeout(timeoutId);
|
|
99
|
+
resolve(event.data.isValid ?? false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
window.addEventListener("message", handleMessage);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Confirm a payment intent in the embedded iframe flow.
|
|
109
|
+
*
|
|
110
|
+
* Pass the embed JWT (`token`) returned by your server's
|
|
111
|
+
* `POST /payment_intents` call. The matching `payment_intent_id` is
|
|
112
|
+
* extracted from the JWT payload and forwarded to the iframe.
|
|
113
|
+
*/
|
|
114
|
+
export function confirmPaymentIntent({
|
|
115
|
+
iframe,
|
|
116
|
+
token,
|
|
117
|
+
}: {
|
|
118
|
+
iframe: Iframe;
|
|
119
|
+
} & Pick<components["schemas"]["EmbedToken"], "token">): void {
|
|
120
|
+
const { payment_intent_id: id }: components["schemas"]["EmbedTokenJwt"] =
|
|
121
|
+
decodeJwt(token).payload;
|
|
122
|
+
iframe?.contentWindow?.postMessage(
|
|
123
|
+
createMessage({
|
|
124
|
+
type: "CONFIRM_PAYMENT_INTENT",
|
|
125
|
+
token,
|
|
126
|
+
id: id ?? undefined,
|
|
127
|
+
}),
|
|
128
|
+
"*",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Confirm a setup intent in the embedded iframe flow.
|
|
134
|
+
*
|
|
135
|
+
* Pass the embed JWT (`token`) returned by your server's
|
|
136
|
+
* `POST /setup_intents` call. The matching `setup_intent_id` is
|
|
137
|
+
* extracted from the JWT payload and forwarded to the iframe.
|
|
138
|
+
*/
|
|
139
|
+
export function confirmSetupIntent({
|
|
140
|
+
iframe,
|
|
141
|
+
token,
|
|
142
|
+
}: {
|
|
143
|
+
iframe: Iframe;
|
|
144
|
+
} & Pick<components["schemas"]["EmbedToken"], "token">): void {
|
|
145
|
+
const { setup_intent_id: id }: components["schemas"]["EmbedTokenJwt"] =
|
|
146
|
+
decodeJwt(token).payload;
|
|
147
|
+
iframe?.contentWindow?.postMessage(
|
|
148
|
+
createMessage({
|
|
149
|
+
type: "CONFIRM_SETUP_INTENT",
|
|
150
|
+
token,
|
|
151
|
+
id: id ?? undefined,
|
|
152
|
+
}),
|
|
153
|
+
"*",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Notify the iframe that confirmation failed (used by express-checkout
|
|
159
|
+
* flows after `onInitiatePaymentIntentRequest` rejects).
|
|
160
|
+
*/
|
|
161
|
+
export function sendConfirmationFailed({
|
|
162
|
+
iframe,
|
|
163
|
+
errorMessage,
|
|
164
|
+
}: {
|
|
165
|
+
iframe: Iframe;
|
|
166
|
+
errorMessage: string;
|
|
167
|
+
}): void {
|
|
168
|
+
iframe?.contentWindow?.postMessage(
|
|
169
|
+
createMessage({
|
|
170
|
+
type: "CONFIRMATION_FAILED",
|
|
171
|
+
errorMessage,
|
|
172
|
+
}),
|
|
173
|
+
"*",
|
|
174
|
+
);
|
|
175
|
+
}
|
package/src/mount.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachGooglePayButtonListeners,
|
|
3
|
+
type GooglePayButtonController,
|
|
4
|
+
type GooglePayButtonListenerOptions,
|
|
5
|
+
getGooglePayButtonInitialHeight,
|
|
6
|
+
getGooglePayButtonSrc,
|
|
7
|
+
} from "./google-pay";
|
|
8
|
+
import {
|
|
9
|
+
attachPaymentMethodFormListeners,
|
|
10
|
+
type CreditCardAdditionalFields,
|
|
11
|
+
getBankAccountFormInitialHeight,
|
|
12
|
+
getBankAccountFormSrc,
|
|
13
|
+
getCreditCardFormInitialHeight,
|
|
14
|
+
getCreditCardFormSrc,
|
|
15
|
+
type PaymentMethodFormController,
|
|
16
|
+
type PaymentMethodFormListenerOptions,
|
|
17
|
+
} from "./payment-method-form";
|
|
18
|
+
|
|
19
|
+
type Container = HTMLElement | string;
|
|
20
|
+
|
|
21
|
+
function resolveContainer(container: Container): HTMLElement {
|
|
22
|
+
if (typeof container === "string") {
|
|
23
|
+
const element = document.querySelector(container);
|
|
24
|
+
if (!(element instanceof HTMLElement)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`[amos-js] Container "${container}" did not match any HTMLElement.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return element;
|
|
30
|
+
}
|
|
31
|
+
return container;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SHARED_IFRAME_STYLE: Partial<CSSStyleDeclaration> = {
|
|
35
|
+
width: "calc(100% + 8px)",
|
|
36
|
+
transition: "opacity 150ms ease-in, height 200ms ease-in-out",
|
|
37
|
+
margin: "0 -4px",
|
|
38
|
+
opacity: "0",
|
|
39
|
+
border: "0",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function createIframe({
|
|
43
|
+
src,
|
|
44
|
+
title,
|
|
45
|
+
name,
|
|
46
|
+
height,
|
|
47
|
+
allow,
|
|
48
|
+
}: {
|
|
49
|
+
src: string;
|
|
50
|
+
title: string;
|
|
51
|
+
name: string;
|
|
52
|
+
height: string;
|
|
53
|
+
allow?: string;
|
|
54
|
+
}): HTMLIFrameElement {
|
|
55
|
+
const iframe = document.createElement("iframe");
|
|
56
|
+
iframe.src = src;
|
|
57
|
+
iframe.title = title;
|
|
58
|
+
iframe.name = name;
|
|
59
|
+
iframe.setAttribute("role", "presentation");
|
|
60
|
+
iframe.scrolling = "no";
|
|
61
|
+
if (allow) {
|
|
62
|
+
iframe.allow = allow;
|
|
63
|
+
}
|
|
64
|
+
Object.assign(iframe.style, SHARED_IFRAME_STYLE, { height });
|
|
65
|
+
return iframe;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Options accepted by {@link mountAmosCreditCardPaymentMethodForm}.
|
|
70
|
+
*/
|
|
71
|
+
export type AmosCreditCardPaymentMethodFormOptions =
|
|
72
|
+
PaymentMethodFormListenerOptions & {
|
|
73
|
+
/**
|
|
74
|
+
* The Amos render token for the credit-card payment method form.
|
|
75
|
+
*
|
|
76
|
+
* It is safe to pass this to the client. Create this on
|
|
77
|
+
* https://dashboard.amos.com.
|
|
78
|
+
*/
|
|
79
|
+
renderToken: string;
|
|
80
|
+
/**
|
|
81
|
+
* The additional fields that are required to be filled out in the
|
|
82
|
+
* form in addition to the card number, expiration date, CVV,
|
|
83
|
+
* country, and postal code.
|
|
84
|
+
*
|
|
85
|
+
* @default { cardholderName: false }
|
|
86
|
+
*/
|
|
87
|
+
additionalFields?: CreditCardAdditionalFields;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Controller returned by {@link mountAmosCreditCardPaymentMethodForm}
|
|
92
|
+
* and {@link mountAmosBankAccountPaymentMethodForm}.
|
|
93
|
+
*/
|
|
94
|
+
export type AmosPaymentMethodFormMountController =
|
|
95
|
+
PaymentMethodFormController & {
|
|
96
|
+
/**
|
|
97
|
+
* The underlying `<iframe>` element. Pass this as the `iframe`
|
|
98
|
+
* argument to {@link validateForm}, {@link confirmPaymentIntent}, or
|
|
99
|
+
* {@link confirmSetupIntent}.
|
|
100
|
+
*/
|
|
101
|
+
iframe: HTMLIFrameElement;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Mount the secure credit-card payment method form into a container
|
|
106
|
+
* element. Returns a controller exposing the underlying iframe, an
|
|
107
|
+
* `update()` method, and a `destroy()` method.
|
|
108
|
+
*
|
|
109
|
+
* Use the returned `controller.iframe` when calling
|
|
110
|
+
* {@link validateForm}, {@link confirmPaymentIntent}, or
|
|
111
|
+
* {@link confirmSetupIntent}.
|
|
112
|
+
*/
|
|
113
|
+
export function mountAmosCreditCardPaymentMethodForm(
|
|
114
|
+
container: Container,
|
|
115
|
+
options: AmosCreditCardPaymentMethodFormOptions,
|
|
116
|
+
): AmosPaymentMethodFormMountController {
|
|
117
|
+
const host = resolveContainer(container);
|
|
118
|
+
const {
|
|
119
|
+
renderToken,
|
|
120
|
+
additionalFields = { cardholderName: false },
|
|
121
|
+
...listenerOptions
|
|
122
|
+
} = options;
|
|
123
|
+
|
|
124
|
+
const iframe = createIframe({
|
|
125
|
+
src: getCreditCardFormSrc(renderToken, additionalFields),
|
|
126
|
+
title: "Secure credit card payment method form powered by Amos",
|
|
127
|
+
name: "amos-credit-card-payment-method-form",
|
|
128
|
+
height: getCreditCardFormInitialHeight(additionalFields),
|
|
129
|
+
});
|
|
130
|
+
host.appendChild(iframe);
|
|
131
|
+
|
|
132
|
+
const controller = attachPaymentMethodFormListeners(iframe, {
|
|
133
|
+
...listenerOptions,
|
|
134
|
+
onHeightChange: (height) => {
|
|
135
|
+
iframe.style.height = height;
|
|
136
|
+
listenerOptions.onHeightChange?.(height);
|
|
137
|
+
},
|
|
138
|
+
onAppearanceReady: () => {
|
|
139
|
+
iframe.style.opacity = "1";
|
|
140
|
+
listenerOptions.onAppearanceReady?.();
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
iframe,
|
|
146
|
+
update: controller.update,
|
|
147
|
+
destroy() {
|
|
148
|
+
controller.destroy();
|
|
149
|
+
iframe.remove();
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Options accepted by {@link mountAmosBankAccountPaymentMethodForm}.
|
|
156
|
+
*/
|
|
157
|
+
export type AmosBankAccountPaymentMethodFormOptions =
|
|
158
|
+
PaymentMethodFormListenerOptions & {
|
|
159
|
+
/**
|
|
160
|
+
* The Amos render token for the bank-account payment method form.
|
|
161
|
+
*
|
|
162
|
+
* It is safe to pass this to the client. Create this on
|
|
163
|
+
* https://dashboard.amos.com.
|
|
164
|
+
*/
|
|
165
|
+
renderToken: string;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Mount the secure bank-account payment method form into a container
|
|
170
|
+
* element. Returns a controller exposing the underlying iframe, an
|
|
171
|
+
* `update()` method, and a `destroy()` method.
|
|
172
|
+
*
|
|
173
|
+
* Use the returned `controller.iframe` when calling
|
|
174
|
+
* {@link validateForm}, {@link confirmPaymentIntent}, or
|
|
175
|
+
* {@link confirmSetupIntent}.
|
|
176
|
+
*/
|
|
177
|
+
export function mountAmosBankAccountPaymentMethodForm(
|
|
178
|
+
container: Container,
|
|
179
|
+
options: AmosBankAccountPaymentMethodFormOptions,
|
|
180
|
+
): AmosPaymentMethodFormMountController {
|
|
181
|
+
const host = resolveContainer(container);
|
|
182
|
+
const { renderToken, ...listenerOptions } = options;
|
|
183
|
+
|
|
184
|
+
const iframe = createIframe({
|
|
185
|
+
src: getBankAccountFormSrc(renderToken),
|
|
186
|
+
title: "Secure bank account payment method form powered by Amos",
|
|
187
|
+
name: "amos-bank-account-payment-method-form",
|
|
188
|
+
height: getBankAccountFormInitialHeight(),
|
|
189
|
+
});
|
|
190
|
+
host.appendChild(iframe);
|
|
191
|
+
|
|
192
|
+
const controller = attachPaymentMethodFormListeners(iframe, {
|
|
193
|
+
...listenerOptions,
|
|
194
|
+
onHeightChange: (height) => {
|
|
195
|
+
iframe.style.height = height;
|
|
196
|
+
listenerOptions.onHeightChange?.(height);
|
|
197
|
+
},
|
|
198
|
+
onAppearanceReady: () => {
|
|
199
|
+
iframe.style.opacity = "1";
|
|
200
|
+
listenerOptions.onAppearanceReady?.();
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
iframe,
|
|
206
|
+
update: controller.update,
|
|
207
|
+
destroy() {
|
|
208
|
+
controller.destroy();
|
|
209
|
+
iframe.remove();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Options accepted by {@link mountAmosGooglePayButton}.
|
|
216
|
+
*/
|
|
217
|
+
export type AmosGooglePayButtonOptions = GooglePayButtonListenerOptions & {
|
|
218
|
+
/**
|
|
219
|
+
* The Amos render token for the Google Pay button.
|
|
220
|
+
*
|
|
221
|
+
* It is safe to pass this to the client. Create this on
|
|
222
|
+
* https://dashboard.amos.com.
|
|
223
|
+
*/
|
|
224
|
+
renderToken: string;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Controller returned by {@link mountAmosGooglePayButton}.
|
|
229
|
+
*/
|
|
230
|
+
export type AmosGooglePayButtonMountController = GooglePayButtonController & {
|
|
231
|
+
/**
|
|
232
|
+
* The underlying `<iframe>` element.
|
|
233
|
+
*/
|
|
234
|
+
iframe: HTMLIFrameElement;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Mount the secure Google Pay button (express checkout) into a
|
|
239
|
+
* container element. Returns a controller exposing the underlying
|
|
240
|
+
* iframe, an `update()` method, and a `destroy()` method.
|
|
241
|
+
*/
|
|
242
|
+
export function mountAmosGooglePayButton(
|
|
243
|
+
container: Container,
|
|
244
|
+
options: AmosGooglePayButtonOptions,
|
|
245
|
+
): AmosGooglePayButtonMountController {
|
|
246
|
+
const host = resolveContainer(container);
|
|
247
|
+
const { renderToken, ...listenerOptions } = options;
|
|
248
|
+
|
|
249
|
+
const iframe = createIframe({
|
|
250
|
+
src: getGooglePayButtonSrc(renderToken),
|
|
251
|
+
title: "Secure Google Pay button powered by Amos",
|
|
252
|
+
name: "amos-google-pay-button",
|
|
253
|
+
height: getGooglePayButtonInitialHeight(),
|
|
254
|
+
allow: "payment",
|
|
255
|
+
});
|
|
256
|
+
host.appendChild(iframe);
|
|
257
|
+
|
|
258
|
+
const controller = attachGooglePayButtonListeners(iframe, {
|
|
259
|
+
...listenerOptions,
|
|
260
|
+
onHeightChange: (height) => {
|
|
261
|
+
iframe.style.height = height;
|
|
262
|
+
listenerOptions.onHeightChange?.(height);
|
|
263
|
+
},
|
|
264
|
+
onAppearanceReady: () => {
|
|
265
|
+
iframe.style.opacity = "1";
|
|
266
|
+
listenerOptions.onAppearanceReady?.();
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
iframe,
|
|
272
|
+
update: controller.update,
|
|
273
|
+
destroy() {
|
|
274
|
+
controller.destroy();
|
|
275
|
+
iframe.remove();
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { components } from "@amos.com/node";
|
|
2
|
+
import { getEmbedOrigin } from "./jwt";
|
|
3
|
+
import {
|
|
4
|
+
sendParentReadyMessage,
|
|
5
|
+
updateAppearance as sendUpdateAppearance,
|
|
6
|
+
} from "./messaging";
|
|
7
|
+
import type { Appearance, Message } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The additional fields beyond the standard card number, expiration
|
|
11
|
+
* date, CVV, country, and postal code that are required to be filled
|
|
12
|
+
* out in the embedded credit-card form.
|
|
13
|
+
*/
|
|
14
|
+
export type CreditCardAdditionalFields = {
|
|
15
|
+
cardholderName: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build the iframe `src` URL for the embedded credit-card form.
|
|
20
|
+
*/
|
|
21
|
+
export function getCreditCardFormSrc(
|
|
22
|
+
renderToken: string,
|
|
23
|
+
additionalFields: CreditCardAdditionalFields = { cardholderName: false },
|
|
24
|
+
): string {
|
|
25
|
+
const enabled = Object.entries(additionalFields)
|
|
26
|
+
.filter(([, value]) => value)
|
|
27
|
+
.map(([key]) => key)
|
|
28
|
+
.join(",");
|
|
29
|
+
|
|
30
|
+
return `${getEmbedOrigin(renderToken)}/iframe/card?token=${renderToken}&additionalFields=${enabled}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the iframe `src` URL for the embedded bank-account form.
|
|
35
|
+
*/
|
|
36
|
+
export function getBankAccountFormSrc(renderToken: string): string {
|
|
37
|
+
return `${getEmbedOrigin(renderToken)}/iframe/bank?token=${renderToken}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default iframe pixel height for the credit-card form, taking the
|
|
42
|
+
* configured `additionalFields` into account.
|
|
43
|
+
*/
|
|
44
|
+
export function getCreditCardFormInitialHeight(
|
|
45
|
+
additionalFields: CreditCardAdditionalFields = { cardholderName: false },
|
|
46
|
+
): string {
|
|
47
|
+
return additionalFields.cardholderName ? "292px" : "212px";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default iframe pixel height for the bank-account form.
|
|
52
|
+
*/
|
|
53
|
+
export function getBankAccountFormInitialHeight(): string {
|
|
54
|
+
return "400px";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Options accepted by {@link attachPaymentMethodFormListeners}.
|
|
59
|
+
*
|
|
60
|
+
* Used by both the credit-card and bank-account forms, which share the
|
|
61
|
+
* same message protocol.
|
|
62
|
+
*/
|
|
63
|
+
export type PaymentMethodFormListenerOptions = {
|
|
64
|
+
/**
|
|
65
|
+
* Custom appearance to apply when the iframe first becomes ready and
|
|
66
|
+
* whenever the appearance changes. Can be updated later via the
|
|
67
|
+
* returned controller's `update({ appearance })` method.
|
|
68
|
+
*/
|
|
69
|
+
appearance?: Appearance;
|
|
70
|
+
/**
|
|
71
|
+
* Called whenever the iframe asks the host page to resize it. Update
|
|
72
|
+
* the iframe's `height` style here.
|
|
73
|
+
*/
|
|
74
|
+
onHeightChange?: (height: string) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Called once the iframe has applied the requested appearance and is
|
|
77
|
+
* ready to be revealed. A common implementation is to set the
|
|
78
|
+
* iframe's opacity from `0` to `1` to fade it in.
|
|
79
|
+
*/
|
|
80
|
+
onAppearanceReady?: () => void;
|
|
81
|
+
/**
|
|
82
|
+
* Called when payment intent confirmation succeeds.
|
|
83
|
+
*/
|
|
84
|
+
onPaymentIntentConfirmationSucceeded?: (
|
|
85
|
+
paymentIntent: components["schemas"]["PaymentIntent"],
|
|
86
|
+
) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Called when setup intent confirmation succeeds.
|
|
89
|
+
*/
|
|
90
|
+
onSetupIntentConfirmationSucceeded?: (
|
|
91
|
+
setupIntent: components["schemas"]["SetupIntent"],
|
|
92
|
+
) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Called when payment or setup intent confirmation fails.
|
|
95
|
+
*/
|
|
96
|
+
onConfirmationFailed: (errorMessage: string) => void;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Controller returned by {@link attachPaymentMethodFormListeners} and
|
|
101
|
+
* the credit-card / bank-account `mount*` helpers.
|
|
102
|
+
*/
|
|
103
|
+
export type PaymentMethodFormController = {
|
|
104
|
+
/**
|
|
105
|
+
* Update one or more listener options without re-attaching the
|
|
106
|
+
* message listener. Pass `{ appearance }` to push new appearance
|
|
107
|
+
* overrides into the iframe.
|
|
108
|
+
*/
|
|
109
|
+
update: (patch: Partial<PaymentMethodFormListenerOptions>) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Detach the iframe message listener.
|
|
112
|
+
*/
|
|
113
|
+
destroy: () => void;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wire up the host-page side of the credit-card or bank-account iframe
|
|
118
|
+
* message protocol on an existing `<iframe>` element. Returns a
|
|
119
|
+
* controller for updating options and tearing down the listener.
|
|
120
|
+
*
|
|
121
|
+
* The iframe is expected to have already been added to the DOM with the
|
|
122
|
+
* correct `src` (see {@link getCreditCardFormSrc} /
|
|
123
|
+
* {@link getBankAccountFormSrc}).
|
|
124
|
+
*/
|
|
125
|
+
export function attachPaymentMethodFormListeners(
|
|
126
|
+
iframe: HTMLIFrameElement,
|
|
127
|
+
options: PaymentMethodFormListenerOptions,
|
|
128
|
+
): PaymentMethodFormController {
|
|
129
|
+
let current = { ...options };
|
|
130
|
+
|
|
131
|
+
function handleMessage(event: MessageEvent<Message>) {
|
|
132
|
+
switch (event.data.type) {
|
|
133
|
+
case "IFRAME_READY":
|
|
134
|
+
sendParentReadyMessage(iframe);
|
|
135
|
+
sendUpdateAppearance({ iframe, appearance: current.appearance });
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case "UPDATE_HEIGHT":
|
|
139
|
+
current.onHeightChange?.(event.data.height);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case "UPDATE_APPEARANCE":
|
|
143
|
+
sendUpdateAppearance({ iframe, appearance: event.data.appearance });
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "UPDATED_APPEARANCE":
|
|
147
|
+
current.onAppearanceReady?.();
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case "PAYMENT_INTENT_CONFIRMATION_SUCCEEDED":
|
|
151
|
+
current.onPaymentIntentConfirmationSucceeded?.(
|
|
152
|
+
event.data.paymentIntent,
|
|
153
|
+
);
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case "SETUP_INTENT_CONFIRMATION_SUCCEEDED":
|
|
157
|
+
current.onSetupIntentConfirmationSucceeded?.(event.data.setupIntent);
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case "CONFIRMATION_FAILED":
|
|
161
|
+
current.onConfirmationFailed(event.data.errorMessage);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
window.addEventListener("message", handleMessage);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
update(patch) {
|
|
170
|
+
const hadAppearance = "appearance" in patch;
|
|
171
|
+
current = { ...current, ...patch };
|
|
172
|
+
if (hadAppearance) {
|
|
173
|
+
sendUpdateAppearance({ iframe, appearance: current.appearance });
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
destroy() {
|
|
177
|
+
window.removeEventListener("message", handleMessage);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|