@amos.com/amos-js 0.9.11 → 0.9.12

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 CHANGED
@@ -14,7 +14,7 @@ npm install @amos.com/amos-js
14
14
 
15
15
  - **Types** for the `postMessage` protocol used between your page and the Amos iframe (`Message`, `Appearance`, `ThemeVariable`). OpenAPI schema types (for example `components["schemas"]["PaymentIntent"]`) come from `@amos.com/node`.
16
16
  - **Iframe-targeted helpers** to validate the form, confirm a payment intent, confirm a setup intent, update appearance, etc.
17
- - **Mount functions** (`mountAmosCreditCardPaymentMethodForm`, `mountAmosBankAccountPaymentMethodForm`, `mountAmosGooglePayButton`, `mountAmosApplePayButton`) that create the iframe, wire up its message protocol, manage its height/opacity, and return a small controller for updating options and tearing it down.
17
+ - **Mount functions** (`mountAmosCreditCardPaymentMethodForm`, `mountAmosBankAccountPaymentMethodForm`, `mountAmosGooglePayButton`, `mountAmosApplePayButton`) that create the iframe, wire up its message protocol, manage its height/opacity, show a field-shaped loading skeleton for card/bank forms, and return a small controller for updating options and tearing it down.
18
18
  - **Lower-level building blocks** (`getCreditCardFormSrc`, `attachPaymentMethodFormListeners`, `attachGooglePayButtonListeners`, `attachApplePayButtonListeners`, ...) for integrators (such as `@amos.com/react-amos-js`) that want to render the iframe element themselves.
19
19
 
20
20
  > **Note:** A server-side SDK (for example `@amos.com/node`) must be used alongside `@amos.com/amos-js` for end-to-end payment processing. `@amos.com/amos-js` is the client-side half.
@@ -96,7 +96,7 @@ form.destroy();
96
96
  The following flow is for credit card and bank account payment method types only.
97
97
 
98
98
  1. **Set up prerequisites**: create a `renderToken` (safe for client), and keep `apiKey` and `accountId` server-side only.
99
- 2. **Render your checkout UI** by calling `mountAmosCreditCardPaymentMethodForm(container, options)` (or `mountAmosBankAccountPaymentMethodForm(...)`) along with the required `onResult` callback. The iframe height is auto-managed by the SDK.
99
+ 2. **Render your checkout UI** by calling `mountAmosCreditCardPaymentMethodForm(container, options)` (or `mountAmosBankAccountPaymentMethodForm(...)`) along with the required `onResult` callback. The SDK shows a field-shaped skeleton immediately (sized from `appearance`, `additionalFields`, and `billingAddressRequirement`) and auto-manages iframe height.
100
100
  3. **User clicks "Pay now" button**: call `validateForm({ iframe: form.iframe })`, which returns `Promise<true>` if the embedded form is valid, and `Promise<false>` otherwise.
101
101
  4. **Create payment intent on your server**: use your server-side Amos client to call `POST /payment_intents`. You may also associate this payment intent with a new or existing customer via `POST /customers`. This must be server-side because it uses your private API key.
102
102
  5. **Return the payment intent token to the browser**: your backend responds with the embed token (`components["schemas"]["EmbedToken"]`) needed for confirmation.
@@ -268,13 +268,13 @@ Mount the secure credit-card payment method form into a container element (an `H
268
268
 
269
269
 
270
270
  - `onValidityChange` (`(event: { isValid: boolean }) => void`) — called when form validity changes. `isValid` is true when all required fields are present and valid. Does not include PCI data. Use this to enable or disable your checkout button.
271
- - `onHeightChange`, `onAppearanceReady` (advanced — override the default iframe styling logic)
271
+ - `onHeightChange`, `onAppearanceReady` (advanced — override the default iframe styling logic). The skeleton is removed and the iframe faded in when `onAppearanceReady` fires.
272
272
 
273
273
  **Returns** `AmosPaymentMethodFormMountController`:
274
274
 
275
275
  - `iframe` — the underlying `<iframe>` element.
276
276
  - `update(patch)` — patch any of the options listed above.
277
- - `destroy()` — remove the iframe and detach listeners.
277
+ - `destroy()` — remove the iframe (and any loading skeleton) and detach listeners.
278
278
 
279
279
  ### `mountAmosBankAccountPaymentMethodForm(container, options)`
280
280
 
@@ -0,0 +1,20 @@
1
+ import { BillingAddressRequirement, CreditCardAdditionalFields } from './payment-method-form';
2
+ import { Appearance } from './types';
3
+ export type PaymentMethodFormSkeletonKind = "card" | "bank";
4
+ export type PaymentMethodFormSkeletonOptions = {
5
+ kind: PaymentMethodFormSkeletonKind;
6
+ appearance?: Appearance;
7
+ additionalFields?: CreditCardAdditionalFields;
8
+ billingAddressRequirement?: BillingAddressRequirement;
9
+ };
10
+ export type PaymentMethodFormSkeleton = {
11
+ element: HTMLElement;
12
+ update: (options: PaymentMethodFormSkeletonOptions) => void;
13
+ };
14
+ /**
15
+ * Host-page placeholder that mirrors card/bank field layout using the
16
+ * same appearance variables the iframe will apply. Shown immediately
17
+ * while the iframe document loads, then removed when appearance is
18
+ * ready.
19
+ */
20
+ export declare function createPaymentMethodFormSkeleton(options: PaymentMethodFormSkeletonOptions): PaymentMethodFormSkeleton;
package/dist/index.js CHANGED
@@ -1 +1,64 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=`data-amos-apple-pay-waiting`;function t({onCancel:t}){let n=document.querySelector(`[${e}]`);if(n)return n;let r=document.createElement(`div`);r.setAttribute(e,`true`),r.setAttribute(`role`,`dialog`),r.setAttribute(`aria-modal`,`true`),r.setAttribute(`aria-labelledby`,`amos-apple-pay-waiting-title`),Object.assign(r.style,{position:`fixed`,inset:`0`,zIndex:`2147483646`,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,boxSizing:`border-box`,background:`rgba(0, 0, 0, 0.55)`,fontFamily:`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`});let i=document.createElement(`div`);Object.assign(i.style,{width:`100%`,maxWidth:`360px`,borderRadius:`12px`,background:`#fff`,padding:`28px 24px 20px`,boxSizing:`border-box`,textAlign:`center`,boxShadow:`0 12px 40px rgba(0, 0, 0, 0.25)`});let a=document.createElement(`div`);a.setAttribute(`aria-hidden`,`true`),Object.assign(a.style,{display:`inline-flex`,alignItems:`center`,justifyContent:`center`,gap:`6px`,marginBottom:`16px`,fontSize:`28px`,fontWeight:`600`,letterSpacing:`-0.02em`,color:`#000`,lineHeight:`1`}),a.innerHTML=`<svg width="22" height="26" viewBox="0 0 814 1000" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/></svg><span>Pay</span>`;let o=document.createElement(`p`);o.id=`amos-apple-pay-waiting-title`,Object.assign(o.style,{margin:`0 0 20px`,fontSize:`15px`,lineHeight:`1.45`,color:`#1a1a1a`}),o.textContent=`Complete your payment in the open Apple Pay window, or close Apple Pay to continue paying another way.`;let s=document.createElement(`button`);return s.type=`button`,s.textContent=`Cancel payment`,Object.assign(s.style,{display:`block`,width:`100%`,border:`none`,borderRadius:`8px`,padding:`12px 16px`,background:`#2c2c2e`,color:`#fff`,fontSize:`15px`,fontWeight:`500`,cursor:`pointer`}),s.addEventListener(`click`,t),i.append(a,o,s),r.append(i),document.body.append(r),r}function n(){document.querySelector(`[${e}]`)?.remove()}function r(e){let[t=``,n=``,r=``]=e?.split(`.`)??[],i=typeof atob==`function`?atob:e=>Buffer.from(e,`base64`).toString(`utf8`);return{header:JSON.parse(i(t)),payload:JSON.parse(i(n)),signature:r}}function i(e){let{env:t=`sandbox`}=r(e).payload;switch(t){case`production`:return`https://embed.amos.com`;case`sandbox`:return`https://embed-sandbox.amos.com`;default:return`https://embed-sandbox.amos.com`}}function a(e){if(!e||typeof e!=`object`)return;let t={};for(let[n,r]of Object.entries(e)){if(typeof r==`string`){if(r.length===0)continue;t[n]=r;continue}typeof r==`number`&&Number.isFinite(r)&&(t[n]=r)}return Object.keys(t).length>0?t:void 0}function o(e){let t={};if(e.buttonstyle!==void 0&&(t.buttonstyle=e.buttonstyle),e.type!==void 0&&(t.type=e.type),e.locale!==void 0&&(t.locale=e.locale),e.style!==void 0){let n=a(e.style);n&&(t.style=n)}return t}function s(e){let t={};if(e.buttonType!==void 0&&(t.buttonType=e.buttonType),e.buttonColor!==void 0&&(t.buttonColor=e.buttonColor),e.buttonRadius!==void 0&&(t.buttonRadius=e.buttonRadius),e.buttonSizeMode!==void 0&&(t.buttonSizeMode=e.buttonSizeMode),e.buttonLocale!==void 0&&(t.buttonLocale=e.buttonLocale),e.buttonBorderType!==void 0&&(t.buttonBorderType=e.buttonBorderType),e.style!==void 0){let n=a(e.style);n&&(t.style=n)}return t}function c(e){return e}function l(e){return new URL(e.src).origin}function u(e){e?.contentWindow&&e.contentWindow.postMessage(c({type:`PARENT_ACKNOWLEDGED_IFRAME_READY`}),l(e))}function d({iframe:e,appearance:t={}}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_APPEARANCE`,appearance:t}),l(e))}function f({iframe:e,props:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_APPLE_PAY_BUTTON`,props:o(t)}),l(e))}function p({iframe:e,props:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_GOOGLE_PAY_BUTTON`,props:s(t)}),l(e))}function m({iframe:e,amount:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_AMOUNT`,amount:t}),l(e))}function h({iframe:e,merchantName:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_MERCHANT_NAME`,merchantName:t}),l(e))}function g({iframe:e}){let t=crypto.randomUUID();return new Promise(n=>{e?.contentWindow&&e.contentWindow.postMessage(c({type:`VALIDATE_FORM`,requestId:t}),l(e));let r=setTimeout(()=>{window.removeEventListener(`message`,i),n(!1)},5e3);function i(e){e.data.type===`VALIDATE_FORM`&&e.data.requestId===t&&(window.removeEventListener(`message`,i),clearTimeout(r),n(e.data.isValid??!1))}window.addEventListener(`message`,i)})}function _({iframe:e}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`RESET_FORM`}),l(e))}function v({iframe:e,token:t}){if(!e?.contentWindow)return;let{payment_intent_id:n}=r(t).payload;e.contentWindow.postMessage(c({type:`CONFIRM_PAYMENT_INTENT`,token:t,id:n??void 0}),l(e))}function y({iframe:e,token:t}){if(!e?.contentWindow)return;let{setup_intent_id:n}=r(t).payload;e.contentWindow.postMessage(c({type:`CONFIRM_SETUP_INTENT`,token:t,id:n??void 0}),l(e))}function b({iframe:e,result:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`CONFIRMATION_RESULT`,result:t}),l(e))}function x(e){return`${i(e)}/iframe/apple-pay?token=${e}`}function S(){return`40px`}function C(e){e.contentWindow?.postMessage(c({type:`APPLE_PAY_CANCEL`}),l(e))}function w(e,r){let i={...r};function a(){m({iframe:e,amount:i.amount})}function o(){h({iframe:e,merchantName:i.merchantName})}function s(){f({iframe:e,props:i})}function c(r){if(r.source===e.contentWindow)switch(r.data.type){case`IFRAME_READY`:u(e),d({iframe:e,appearance:i.appearance}),a(),o(),s();break;case`UPDATE_HEIGHT`:i.onHeightChange?.(r.data.height);break;case`UPDATE_APPEARANCE`:d({iframe:e,appearance:r.data.appearance});break;case`UPDATED_APPEARANCE`:i.onAppearanceReady?.();break;case`APPLE_PAY_WINDOW_OPEN`:t({onCancel:()=>{C(e),n()}});break;case`APPLE_PAY_WINDOW_CLOSE`:n();break;case`CREATE_PAYMENT_INTENT`:i.onInitiatePaymentIntentRequest({paymentIntentCreateAttributes:r.data.paymentIntentCreateAttributes,customerCreateAttributes:r.data.customerCreateAttributes}).then(t=>{v({iframe:e,token:t})}).catch(e=>{n(),i.onResult({status:`failed`,errorMessage:e instanceof Error?e.message:`Unknown error`})});break;case`CONFIRMATION_RESULT`:n(),i.onResult(r.data.result)}}return window.addEventListener(`message`,c),{update(t){let n=`appearance`in t,r=`amount`in t,c=`merchantName`in t,l=`buttonstyle`in t||`type`in t||`locale`in t||`style`in t;i={...i,...t},n&&d({iframe:e,appearance:i.appearance}),r&&a(),c&&o(),l&&s()},destroy(){window.removeEventListener(`message`,c),n()}}}function T(e){return`${i(e)}/iframe/google-pay?token=${e}`}function E(){return`40px`}function D(e,t){let n={...t};function r(){m({iframe:e,amount:n.amount})}function i(){h({iframe:e,merchantName:n.merchantName})}function a(){p({iframe:e,props:n})}function o(t){if(t.source===e.contentWindow)switch(t.data.type){case`IFRAME_READY`:u(e),d({iframe:e,appearance:n.appearance}),r(),i(),a();break;case`UPDATE_HEIGHT`:n.onHeightChange?.(t.data.height);break;case`UPDATE_APPEARANCE`:d({iframe:e,appearance:t.data.appearance});break;case`UPDATED_APPEARANCE`:n.onAppearanceReady?.();break;case`CREATE_PAYMENT_INTENT`:n.onInitiatePaymentIntentRequest({paymentIntentCreateAttributes:t.data.paymentIntentCreateAttributes,customerCreateAttributes:t.data.customerCreateAttributes}).then(t=>{v({iframe:e,token:t})}).catch(e=>{n.onResult({status:`failed`,errorMessage:e instanceof Error?e.message:`Unknown error`})});break;case`CONFIRMATION_RESULT`:n.onResult(t.data.result)}}return window.addEventListener(`message`,o),{update(t){let o=`appearance`in t,s=`amount`in t,c=`merchantName`in t,l=`buttonType`in t||`buttonColor`in t||`buttonRadius`in t||`buttonSizeMode`in t||`buttonLocale`in t||`buttonBorderType`in t||`style`in t;n={...n,...t},o&&d({iframe:e,appearance:n.appearance}),s&&r(),c&&i(),l&&a()},destroy(){window.removeEventListener(`message`,o)}}}function O({paymentData:e}){return{paymentMethod:{type:`googlepay`,billing_address_attributes:{name:e.shippingAddress?.name,address_line1:e.shippingAddress?.address1,address_line2:e.shippingAddress?.address2,city:e.shippingAddress?.locality,state:e.shippingAddress?.administrativeArea,postal_code:e.shippingAddress?.postalCode,country:e.shippingAddress?.countryCode,email:e.email,phone:e.shippingAddress?.phoneNumber},card_profile_attributes:{wallet_payload:e.paymentMethodData.tokenizationData.token}}}}var k={country:212,full:452},A=80,j={country:400,full:640};function M(e,t={cardholderName:!1},n=`country`){let r=Object.entries(t).filter(([,e])=>e).map(([e])=>e).join(`,`),a=new URLSearchParams({token:e,additionalFields:r,billingAddressRequirement:n});return`${i(e)}/iframe/card?${a}`}function N(e,t=`country`){let n=new URLSearchParams({token:e,billingAddressRequirement:t});return`${i(e)}/iframe/bank?${n}`}function P(e={cardholderName:!1},t=`country`){return`${(k[t]??k.country)+(e.cardholderName?A:0)}px`}function F(e=`country`){return`${j[e]??j.country}px`}function I(e,t){let n={...t};function r(t){if(t.source===e.contentWindow)switch(t.data.type){case`IFRAME_READY`:u(e),d({iframe:e,appearance:n.appearance});break;case`UPDATE_HEIGHT`:n.onHeightChange?.(t.data.height);break;case`UPDATE_APPEARANCE`:d({iframe:e,appearance:t.data.appearance});break;case`UPDATED_APPEARANCE`:n.onAppearanceReady?.();break;case`FORM_VALIDITY_CHANGE`:n.onValidityChange?.({isValid:t.data.isValid});break;case`CONFIRMATION_RESULT`:n.onResult(t.data.result)}}return window.addEventListener(`message`,r),{update(t){let r=`appearance`in t;n={...n,...t},r&&d({iframe:e,appearance:n.appearance})},destroy(){window.removeEventListener(`message`,r)}}}function L(e){if(typeof e==`string`){let t=document.querySelector(e);if(!(t instanceof HTMLElement))throw Error(`[amos-js] Container "${e}" did not match any HTMLElement.`);return t}return e}var R={width:`calc(100% + 8px)`,transition:`opacity 150ms ease-in, height 200ms ease-in-out`,margin:`0 -4px`,opacity:`0`,border:`0`};function z({src:e,title:t,name:n,height:r,allow:i}){let a=document.createElement(`iframe`);return a.src=e,a.title=t,a.name=n,a.setAttribute(`role`,`presentation`),a.scrolling=`no`,i&&(a.allow=i),Object.assign(a.style,R,{height:r}),a}function B(e,t){let n=L(e),{renderToken:r,additionalFields:i={cardholderName:!1},billingAddressRequirement:a=`country`,...o}=t,s=z({src:M(r,i,a),title:`Secure credit card payment method form powered by Amos`,name:`amos-credit-card-payment-method-form`,height:P(i,a)});n.appendChild(s);let c=I(s,{...o,onHeightChange:e=>{s.style.height=e,o.onHeightChange?.(e)},onAppearanceReady:()=>{s.style.opacity=`1`,o.onAppearanceReady?.()}});return{iframe:s,update:c.update,destroy(){c.destroy(),s.remove()}}}function V(e,t){let n=L(e),{renderToken:r,billingAddressRequirement:i=`country`,...a}=t,o=z({src:N(r,i),title:`Secure bank account payment method form powered by Amos`,name:`amos-bank-account-payment-method-form`,height:F(i)});n.appendChild(o);let s=I(o,{...a,onHeightChange:e=>{o.style.height=e,a.onHeightChange?.(e)},onAppearanceReady:()=>{o.style.opacity=`1`,a.onAppearanceReady?.()}});return{iframe:o,update:s.update,destroy(){s.destroy(),o.remove()}}}function H(e,t){let n=L(e),{renderToken:r,...i}=t,a=z({src:T(r),title:`Secure Google Pay button powered by Amos`,name:`amos-google-pay-button`,height:E(),allow:`payment`});n.appendChild(a);let o=D(a,{...i,onHeightChange:e=>{a.style.height=e,i.onHeightChange?.(e)},onAppearanceReady:()=>{a.style.opacity=`1`,i.onAppearanceReady?.()}});return{iframe:a,update:o.update,destroy(){o.destroy(),a.remove()}}}function U(e,t){let n=L(e),{renderToken:r,...i}=t,a=z({src:x(r),title:`Secure Apple Pay button powered by Amos`,name:`amos-apple-pay-button`,height:S(),allow:`payment`});n.appendChild(a);let o=w(a,{...i,onHeightChange:e=>{a.style.height=e,i.onHeightChange?.(e)},onAppearanceReady:()=>{a.style.opacity=`1`,i.onAppearanceReady?.()}});return{iframe:a,update:o.update,destroy(){o.destroy(),a.remove()}}}exports.attachApplePayButtonListeners=w,exports.attachGooglePayButtonListeners=D,exports.attachPaymentMethodFormListeners=I,exports.confirmPaymentIntent=v,exports.confirmSetupIntent=y,exports.createMessage=c,exports.decodeJwt=r,exports.formatGooglePayPaymentData=O,exports.getApplePayButtonInitialHeight=S,exports.getApplePayButtonSrc=x,exports.getBankAccountFormInitialHeight=F,exports.getBankAccountFormSrc=N,exports.getCreditCardFormInitialHeight=P,exports.getCreditCardFormSrc=M,exports.getEmbedOrigin=i,exports.getGooglePayButtonInitialHeight=E,exports.getGooglePayButtonSrc=T,exports.mountAmosApplePayButton=U,exports.mountAmosBankAccountPaymentMethodForm=V,exports.mountAmosCreditCardPaymentMethodForm=B,exports.mountAmosGooglePayButton=H,exports.pickApplePayButtonElementProps=o,exports.pickGooglePayButtonElementProps=s,exports.resetForm=_,exports.sendConfirmationResult=b,exports.sendParentReadyMessage=u,exports.serializeWalletButtonStyle=a,exports.updateAmount=m,exports.updateAppearance=d,exports.updateApplePayButton=f,exports.updateGooglePayButton=p,exports.updateMerchantName=h,exports.validateForm=g;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=`data-amos-apple-pay-waiting`;function t({onCancel:t}){let n=document.querySelector(`[${e}]`);if(n)return n;let r=document.createElement(`div`);r.setAttribute(e,`true`),r.setAttribute(`role`,`dialog`),r.setAttribute(`aria-modal`,`true`),r.setAttribute(`aria-labelledby`,`amos-apple-pay-waiting-title`),Object.assign(r.style,{position:`fixed`,inset:`0`,zIndex:`2147483646`,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,boxSizing:`border-box`,background:`rgba(0, 0, 0, 0.55)`,fontFamily:`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`});let i=document.createElement(`div`);Object.assign(i.style,{width:`100%`,maxWidth:`360px`,borderRadius:`12px`,background:`#fff`,padding:`28px 24px 20px`,boxSizing:`border-box`,textAlign:`center`,boxShadow:`0 12px 40px rgba(0, 0, 0, 0.25)`});let a=document.createElement(`div`);a.setAttribute(`aria-hidden`,`true`),Object.assign(a.style,{display:`inline-flex`,alignItems:`center`,justifyContent:`center`,gap:`6px`,marginBottom:`16px`,fontSize:`28px`,fontWeight:`600`,letterSpacing:`-0.02em`,color:`#000`,lineHeight:`1`}),a.innerHTML=`<svg width="22" height="26" viewBox="0 0 814 1000" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/></svg><span>Pay</span>`;let o=document.createElement(`p`);o.id=`amos-apple-pay-waiting-title`,Object.assign(o.style,{margin:`0 0 20px`,fontSize:`15px`,lineHeight:`1.45`,color:`#1a1a1a`}),o.textContent=`Complete your payment in the open Apple Pay window, or close Apple Pay to continue paying another way.`;let s=document.createElement(`button`);return s.type=`button`,s.textContent=`Cancel payment`,Object.assign(s.style,{display:`block`,width:`100%`,border:`none`,borderRadius:`8px`,padding:`12px 16px`,background:`#2c2c2e`,color:`#fff`,fontSize:`15px`,fontWeight:`500`,cursor:`pointer`}),s.addEventListener(`click`,t),i.append(a,o,s),r.append(i),document.body.append(r),r}function n(){document.querySelector(`[${e}]`)?.remove()}function r(e){let[t=``,n=``,r=``]=e?.split(`.`)??[],i=typeof atob==`function`?atob:e=>Buffer.from(e,`base64`).toString(`utf8`);return{header:JSON.parse(i(t)),payload:JSON.parse(i(n)),signature:r}}function i(e){let{env:t=`sandbox`}=r(e).payload;switch(t){case`production`:return`https://embed.amos.com`;case`sandbox`:return`https://embed-sandbox.amos.com`;default:return`https://embed-sandbox.amos.com`}}function a(e){if(!e||typeof e!=`object`)return;let t={};for(let[n,r]of Object.entries(e)){if(typeof r==`string`){if(r.length===0)continue;t[n]=r;continue}typeof r==`number`&&Number.isFinite(r)&&(t[n]=r)}return Object.keys(t).length>0?t:void 0}function o(e){let t={};if(e.buttonstyle!==void 0&&(t.buttonstyle=e.buttonstyle),e.type!==void 0&&(t.type=e.type),e.locale!==void 0&&(t.locale=e.locale),e.style!==void 0){let n=a(e.style);n&&(t.style=n)}return t}function s(e){let t={};if(e.buttonType!==void 0&&(t.buttonType=e.buttonType),e.buttonColor!==void 0&&(t.buttonColor=e.buttonColor),e.buttonRadius!==void 0&&(t.buttonRadius=e.buttonRadius),e.buttonSizeMode!==void 0&&(t.buttonSizeMode=e.buttonSizeMode),e.buttonLocale!==void 0&&(t.buttonLocale=e.buttonLocale),e.buttonBorderType!==void 0&&(t.buttonBorderType=e.buttonBorderType),e.style!==void 0){let n=a(e.style);n&&(t.style=n)}return t}function c(e){return e}function l(e){return new URL(e.src).origin}function u(e){e?.contentWindow&&e.contentWindow.postMessage(c({type:`PARENT_ACKNOWLEDGED_IFRAME_READY`}),l(e))}function d({iframe:e,appearance:t={}}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_APPEARANCE`,appearance:t}),l(e))}function f({iframe:e,props:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_APPLE_PAY_BUTTON`,props:o(t)}),l(e))}function p({iframe:e,props:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_GOOGLE_PAY_BUTTON`,props:s(t)}),l(e))}function m({iframe:e,amount:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_AMOUNT`,amount:t}),l(e))}function h({iframe:e,merchantName:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`UPDATE_MERCHANT_NAME`,merchantName:t}),l(e))}function g({iframe:e}){let t=crypto.randomUUID();return new Promise(n=>{e?.contentWindow&&e.contentWindow.postMessage(c({type:`VALIDATE_FORM`,requestId:t}),l(e));let r=setTimeout(()=>{window.removeEventListener(`message`,i),n(!1)},5e3);function i(e){e.data.type===`VALIDATE_FORM`&&e.data.requestId===t&&(window.removeEventListener(`message`,i),clearTimeout(r),n(e.data.isValid??!1))}window.addEventListener(`message`,i)})}function _({iframe:e}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`RESET_FORM`}),l(e))}function v({iframe:e,token:t}){if(!e?.contentWindow)return;let{payment_intent_id:n}=r(t).payload;e.contentWindow.postMessage(c({type:`CONFIRM_PAYMENT_INTENT`,token:t,id:n??void 0}),l(e))}function y({iframe:e,token:t}){if(!e?.contentWindow)return;let{setup_intent_id:n}=r(t).payload;e.contentWindow.postMessage(c({type:`CONFIRM_SETUP_INTENT`,token:t,id:n??void 0}),l(e))}function b({iframe:e,result:t}){e?.contentWindow&&e.contentWindow.postMessage(c({type:`CONFIRMATION_RESULT`,result:t}),l(e))}function x(e){return`${i(e)}/iframe/apple-pay?token=${e}`}function S(){return`40px`}function C(e){e.contentWindow?.postMessage(c({type:`APPLE_PAY_CANCEL`}),l(e))}function w(e,r){let i={...r};function a(){m({iframe:e,amount:i.amount})}function o(){h({iframe:e,merchantName:i.merchantName})}function s(){f({iframe:e,props:i})}function c(r){if(r.source===e.contentWindow)switch(r.data.type){case`IFRAME_READY`:u(e),d({iframe:e,appearance:i.appearance}),a(),o(),s();break;case`UPDATE_HEIGHT`:i.onHeightChange?.(r.data.height);break;case`UPDATE_APPEARANCE`:d({iframe:e,appearance:r.data.appearance});break;case`UPDATED_APPEARANCE`:i.onAppearanceReady?.();break;case`APPLE_PAY_WINDOW_OPEN`:t({onCancel:()=>{C(e),n()}});break;case`APPLE_PAY_WINDOW_CLOSE`:n();break;case`CREATE_PAYMENT_INTENT`:i.onInitiatePaymentIntentRequest({paymentIntentCreateAttributes:r.data.paymentIntentCreateAttributes,customerCreateAttributes:r.data.customerCreateAttributes}).then(t=>{v({iframe:e,token:t})}).catch(e=>{n(),i.onResult({status:`failed`,errorMessage:e instanceof Error?e.message:`Unknown error`})});break;case`CONFIRMATION_RESULT`:n(),i.onResult(r.data.result)}}return window.addEventListener(`message`,c),{update(t){let n=`appearance`in t,r=`amount`in t,c=`merchantName`in t,l=`buttonstyle`in t||`type`in t||`locale`in t||`style`in t;i={...i,...t},n&&d({iframe:e,appearance:i.appearance}),r&&a(),c&&o(),l&&s()},destroy(){window.removeEventListener(`message`,c),n()}}}function T(e){return`${i(e)}/iframe/google-pay?token=${e}`}function E(){return`40px`}function D(e,t){let n={...t};function r(){m({iframe:e,amount:n.amount})}function i(){h({iframe:e,merchantName:n.merchantName})}function a(){p({iframe:e,props:n})}function o(t){if(t.source===e.contentWindow)switch(t.data.type){case`IFRAME_READY`:u(e),d({iframe:e,appearance:n.appearance}),r(),i(),a();break;case`UPDATE_HEIGHT`:n.onHeightChange?.(t.data.height);break;case`UPDATE_APPEARANCE`:d({iframe:e,appearance:t.data.appearance});break;case`UPDATED_APPEARANCE`:n.onAppearanceReady?.();break;case`CREATE_PAYMENT_INTENT`:n.onInitiatePaymentIntentRequest({paymentIntentCreateAttributes:t.data.paymentIntentCreateAttributes,customerCreateAttributes:t.data.customerCreateAttributes}).then(t=>{v({iframe:e,token:t})}).catch(e=>{n.onResult({status:`failed`,errorMessage:e instanceof Error?e.message:`Unknown error`})});break;case`CONFIRMATION_RESULT`:n.onResult(t.data.result)}}return window.addEventListener(`message`,o),{update(t){let o=`appearance`in t,s=`amount`in t,c=`merchantName`in t,l=`buttonType`in t||`buttonColor`in t||`buttonRadius`in t||`buttonSizeMode`in t||`buttonLocale`in t||`buttonBorderType`in t||`style`in t;n={...n,...t},o&&d({iframe:e,appearance:n.appearance}),s&&r(),c&&i(),l&&a()},destroy(){window.removeEventListener(`message`,o)}}}function O({paymentData:e}){return{paymentMethod:{type:`googlepay`,billing_address_attributes:{name:e.shippingAddress?.name,address_line1:e.shippingAddress?.address1,address_line2:e.shippingAddress?.address2,city:e.shippingAddress?.locality,state:e.shippingAddress?.administrativeArea,postal_code:e.shippingAddress?.postalCode,country:e.shippingAddress?.countryCode,email:e.email,phone:e.shippingAddress?.phoneNumber},card_profile_attributes:{wallet_payload:e.paymentMethodData.tokenizationData.token}}}}var k=`amos-js-form-skeleton-styles`,A={"--accent":`oklch(0.97 0 0)`,"--radius":`0.625rem`,"--input-height":`2.25rem`,"--floating-input-height":`3.25rem`,"--field-gap":`1rem`,"--control-gap":`0.5rem`,"--label-font-size":`0.875rem`},j=`
2
+ .amos-js-form-skeleton {
3
+ box-sizing: border-box;
4
+ container-type: inline-size;
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--field-gap);
8
+ margin: 0 -4px;
9
+ padding-block: 0.25rem;
10
+ pointer-events: none;
11
+ width: calc(100% + 8px);
12
+ }
13
+ .amos-js-form-skeleton-field {
14
+ display: flex;
15
+ flex: 1 1 0;
16
+ flex-direction: column;
17
+ min-width: 0;
18
+ width: 100%;
19
+ }
20
+ .amos-js-form-skeleton-label {
21
+ flex-shrink: 0;
22
+ font-size: var(--label-font-size);
23
+ height: 1.75rem;
24
+ line-height: 1.75rem;
25
+ }
26
+ .amos-js-form-skeleton-input {
27
+ animation: amos-js-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
28
+ background: var(--accent);
29
+ border-radius: calc(var(--radius) * 0.8);
30
+ box-sizing: border-box;
31
+ height: var(--input-height);
32
+ width: 100%;
33
+ }
34
+ .amos-js-form-skeleton-input-floating {
35
+ height: var(--floating-input-height);
36
+ }
37
+ .amos-js-form-skeleton-row {
38
+ align-items: flex-start;
39
+ display: flex;
40
+ gap: var(--control-gap);
41
+ width: 100%;
42
+ }
43
+ .amos-js-form-skeleton-row-stack {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: var(--field-gap);
47
+ width: 100%;
48
+ }
49
+ @container (min-width: 24rem) {
50
+ .amos-js-form-skeleton-row-stack {
51
+ align-items: flex-start;
52
+ flex-direction: row;
53
+ gap: var(--control-gap);
54
+ }
55
+ }
56
+ @keyframes amos-js-skeleton-pulse {
57
+ 50% { opacity: 0.5; }
58
+ }
59
+ @media (prefers-reduced-motion: reduce) {
60
+ .amos-js-form-skeleton-input {
61
+ animation: none;
62
+ }
63
+ }
64
+ `;function M(){if(document.getElementById(k))return;let e=document.createElement(`style`);e.id=k,e.textContent=j,document.head.appendChild(e)}function N(e,t){for(let[t,n]of Object.entries(A))e.style.setProperty(t,n);let n=t?.themeVariables;if(n)for(let[t,r]of Object.entries(n))typeof r==`string`&&r.trim()!==``&&e.style.setProperty(t,r.trim())}function P(e,t){let n=document.createElement(`div`);if(n.className=e,t)for(let e of t)n.appendChild(e);return n}function F(e,t){let n=P(`amos-js-form-skeleton-field`);t!==void 0&&(n.style.flexGrow=String(t)),e===`above`&&n.appendChild(P(`amos-js-form-skeleton-label`));let r=P(`amos-js-form-skeleton-input`);return e===`floating`&&r.classList.add(`amos-js-form-skeleton-input-floating`),n.appendChild(r),n}function I(e,t){return P(t?`amos-js-form-skeleton-row-stack`:`amos-js-form-skeleton-row`,e)}function L({labels:e,requirement:t,wrapCountryZip:n}){return t===`full`?[F(e),F(e),I([F(e,1.4),F(e,.7),F(e,.8)],!1),F(e)]:[I([F(e),F(e)],n)]}function R(e){let t=e.appearance?.labels??`above`,n=e.billingAddressRequirement??`country`;if(e.kind===`card`){let r=[F(t),I([F(t),F(t)],!1)];return e.additionalFields?.cardholderName&&r.push(F(t)),r.push(...L({labels:t,requirement:n,wrapCountryZip:!1})),r}return[F(t),I([F(t),F(t)],!0),F(t),I([F(`above`),F(`above`)],!0),...L({labels:t,requirement:n,wrapCountryZip:!0})]}function z(e){M();let t=P(`amos-js-form-skeleton`);t.setAttribute(`aria-hidden`,`true`);function n(e){N(t,e.appearance),t.replaceChildren(...R(e))}return n(e),{element:t,update:n}}var B={country:212,full:452},V=80,H={country:400,full:640};function U(e,t={cardholderName:!1},n=`country`){let r=Object.entries(t).filter(([,e])=>e).map(([e])=>e).join(`,`),a=new URLSearchParams({token:e,additionalFields:r,billingAddressRequirement:n});return`${i(e)}/iframe/card?${a}`}function W(e,t=`country`){let n=new URLSearchParams({token:e,billingAddressRequirement:t});return`${i(e)}/iframe/bank?${n}`}function G(e={cardholderName:!1},t=`country`){return`${(B[t]??B.country)+(e.cardholderName?V:0)}px`}function K(e=`country`){return`${H[e]??H.country}px`}function q(e,t){let n={...t};function r(t){if(t.source===e.contentWindow)switch(t.data.type){case`IFRAME_READY`:u(e),d({iframe:e,appearance:n.appearance});break;case`UPDATE_HEIGHT`:n.onHeightChange?.(t.data.height);break;case`UPDATE_APPEARANCE`:d({iframe:e,appearance:t.data.appearance});break;case`UPDATED_APPEARANCE`:n.onAppearanceReady?.();break;case`FORM_VALIDITY_CHANGE`:n.onValidityChange?.({isValid:t.data.isValid});break;case`CONFIRMATION_RESULT`:n.onResult(t.data.result)}}return window.addEventListener(`message`,r),{update(t){let r=`appearance`in t;n={...n,...t},r&&d({iframe:e,appearance:n.appearance})},destroy(){window.removeEventListener(`message`,r)}}}function J(e){if(typeof e==`string`){let t=document.querySelector(e);if(!(t instanceof HTMLElement))throw Error(`[amos-js] Container "${e}" did not match any HTMLElement.`);return t}return e}var Y={width:`calc(100% + 8px)`,transition:`opacity 150ms ease-in, height 200ms ease-in-out`,margin:`0 -4px`,opacity:`0`,border:`0`},X={position:`absolute`,top:`0`,left:`-4px`,width:`calc(100% + 8px)`,height:`100%`,margin:`0`,transition:`none`,pointerEvents:`none`};function Z({src:e,title:t,name:n,height:r,allow:i}){let a=document.createElement(`iframe`);return a.src=e,a.title=t,a.name=n,a.setAttribute(`role`,`presentation`),a.scrolling=`no`,i&&(a.allow=i),Object.assign(a.style,Y,{height:r}),a}function Q({host:e,iframe:t,listenerOptions:n,skeletonOptions:r}){let i=document.createElement(`div`);i.style.position=`relative`,i.style.width=`100%`,i.setAttribute(`aria-busy`,`true`);let a=z(r);Object.assign(t.style,X),i.append(a.element,t),e.appendChild(i);let o=!1,s=!1,c,l=r.appearance,u,d;function f(){return i.getBoundingClientRect().height}function p(){return c?Number.parseFloat(c):NaN}function m(){if(o)return;o=!0,d!==void 0&&(clearTimeout(d),d=void 0);let e=f(),n=p(),r=Number.isFinite(n)?Math.max(n,e):e;t.style.transition=`none`,t.style.position=``,t.style.top=``,t.style.left=``,t.style.margin=Y.margin??``,t.style.height=`${r}px`,t.style.opacity=`1`,t.style.pointerEvents=``,a.element.remove(),i.removeAttribute(`aria-busy`),u=setTimeout(()=>{t.style.transition=`height 200ms ease-in-out`},400)}function h(){if(o||!s)return;let e=p(),t=f();Number.isFinite(e)&&e>=t-2&&m()}let g=q(t,{...n,onHeightChange:e=>{c=e,o?t.style.height=e:h(),n.onHeightChange?.(e)},onAppearanceReady:()=>{s=!0,h(),!o&&d===void 0&&(d=setTimeout(()=>{m()},1500)),n.onAppearanceReady?.()}});return{iframe:t,update(e){g.update(e),!o&&`appearance`in e&&(l=e.appearance,a.update({...r,appearance:l}))},destroy(){u!==void 0&&clearTimeout(u),d!==void 0&&clearTimeout(d),g.destroy(),i.remove()}}}function $(e,t){let n=J(e),{renderToken:r,additionalFields:i={cardholderName:!1},billingAddressRequirement:a=`country`,...o}=t;return Q({host:n,iframe:Z({src:U(r,i,a),title:`Secure credit card payment method form powered by Amos`,name:`amos-credit-card-payment-method-form`,height:G(i,a)}),listenerOptions:o,skeletonOptions:{kind:`card`,appearance:o.appearance,additionalFields:i,billingAddressRequirement:a}})}function ee(e,t){let n=J(e),{renderToken:r,billingAddressRequirement:i=`country`,...a}=t;return Q({host:n,iframe:Z({src:W(r,i),title:`Secure bank account payment method form powered by Amos`,name:`amos-bank-account-payment-method-form`,height:K(i)}),listenerOptions:a,skeletonOptions:{kind:`bank`,appearance:a.appearance,billingAddressRequirement:i}})}function te(e,t){let n=J(e),{renderToken:r,...i}=t,a=Z({src:T(r),title:`Secure Google Pay button powered by Amos`,name:`amos-google-pay-button`,height:E(),allow:`payment`});n.appendChild(a);let o=D(a,{...i,onHeightChange:e=>{a.style.height=e,i.onHeightChange?.(e)},onAppearanceReady:()=>{a.style.opacity=`1`,i.onAppearanceReady?.()}});return{iframe:a,update:o.update,destroy(){o.destroy(),a.remove()}}}function ne(e,t){let n=J(e),{renderToken:r,...i}=t,a=Z({src:x(r),title:`Secure Apple Pay button powered by Amos`,name:`amos-apple-pay-button`,height:S(),allow:`payment`});n.appendChild(a);let o=w(a,{...i,onHeightChange:e=>{a.style.height=e,i.onHeightChange?.(e)},onAppearanceReady:()=>{a.style.opacity=`1`,i.onAppearanceReady?.()}});return{iframe:a,update:o.update,destroy(){o.destroy(),a.remove()}}}exports.attachApplePayButtonListeners=w,exports.attachGooglePayButtonListeners=D,exports.attachPaymentMethodFormListeners=q,exports.confirmPaymentIntent=v,exports.confirmSetupIntent=y,exports.createMessage=c,exports.decodeJwt=r,exports.formatGooglePayPaymentData=O,exports.getApplePayButtonInitialHeight=S,exports.getApplePayButtonSrc=x,exports.getBankAccountFormInitialHeight=K,exports.getBankAccountFormSrc=W,exports.getCreditCardFormInitialHeight=G,exports.getCreditCardFormSrc=U,exports.getEmbedOrigin=i,exports.getGooglePayButtonInitialHeight=E,exports.getGooglePayButtonSrc=T,exports.mountAmosApplePayButton=ne,exports.mountAmosBankAccountPaymentMethodForm=ee,exports.mountAmosCreditCardPaymentMethodForm=$,exports.mountAmosGooglePayButton=te,exports.pickApplePayButtonElementProps=o,exports.pickGooglePayButtonElementProps=s,exports.resetForm=_,exports.sendConfirmationResult=b,exports.sendParentReadyMessage=u,exports.serializeWalletButtonStyle=a,exports.updateAmount=m,exports.updateAppearance=d,exports.updateApplePayButton=f,exports.updateGooglePayButton=p,exports.updateMerchantName=h,exports.validateForm=g;
package/dist/index.mjs CHANGED
@@ -392,15 +392,96 @@ function O({ paymentData: e }) {
392
392
  } };
393
393
  }
394
394
  //#endregion
395
+ //#region src/form-skeleton.ts
396
+ var k = "amos-js-form-skeleton-styles", A = {
397
+ "--accent": "oklch(0.97 0 0)",
398
+ "--radius": "0.625rem",
399
+ "--input-height": "2.25rem",
400
+ "--floating-input-height": "3.25rem",
401
+ "--field-gap": "1rem",
402
+ "--control-gap": "0.5rem",
403
+ "--label-font-size": "0.875rem"
404
+ }, j = "\n.amos-js-form-skeleton {\n box-sizing: border-box;\n container-type: inline-size;\n display: flex;\n flex-direction: column;\n gap: var(--field-gap);\n margin: 0 -4px;\n padding-block: 0.25rem;\n pointer-events: none;\n width: calc(100% + 8px);\n}\n.amos-js-form-skeleton-field {\n display: flex;\n flex: 1 1 0;\n flex-direction: column;\n min-width: 0;\n width: 100%;\n}\n.amos-js-form-skeleton-label {\n flex-shrink: 0;\n font-size: var(--label-font-size);\n height: 1.75rem;\n line-height: 1.75rem;\n}\n.amos-js-form-skeleton-input {\n animation: amos-js-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n background: var(--accent);\n border-radius: calc(var(--radius) * 0.8);\n box-sizing: border-box;\n height: var(--input-height);\n width: 100%;\n}\n.amos-js-form-skeleton-input-floating {\n height: var(--floating-input-height);\n}\n.amos-js-form-skeleton-row {\n align-items: flex-start;\n display: flex;\n gap: var(--control-gap);\n width: 100%;\n}\n.amos-js-form-skeleton-row-stack {\n display: flex;\n flex-direction: column;\n gap: var(--field-gap);\n width: 100%;\n}\n@container (min-width: 24rem) {\n .amos-js-form-skeleton-row-stack {\n align-items: flex-start;\n flex-direction: row;\n gap: var(--control-gap);\n }\n}\n@keyframes amos-js-skeleton-pulse {\n 50% { opacity: 0.5; }\n}\n@media (prefers-reduced-motion: reduce) {\n .amos-js-form-skeleton-input {\n animation: none;\n }\n}\n";
405
+ function M() {
406
+ if (document.getElementById(k)) return;
407
+ let e = document.createElement("style");
408
+ e.id = k, e.textContent = j, document.head.appendChild(e);
409
+ }
410
+ function N(e, t) {
411
+ for (let [t, n] of Object.entries(A)) e.style.setProperty(t, n);
412
+ let n = t?.themeVariables;
413
+ if (n) for (let [t, r] of Object.entries(n)) typeof r == "string" && r.trim() !== "" && e.style.setProperty(t, r.trim());
414
+ }
415
+ function P(e, t) {
416
+ let n = document.createElement("div");
417
+ if (n.className = e, t) for (let e of t) n.appendChild(e);
418
+ return n;
419
+ }
420
+ function F(e, t) {
421
+ let n = P("amos-js-form-skeleton-field");
422
+ t !== void 0 && (n.style.flexGrow = String(t)), e === "above" && n.appendChild(P("amos-js-form-skeleton-label"));
423
+ let r = P("amos-js-form-skeleton-input");
424
+ return e === "floating" && r.classList.add("amos-js-form-skeleton-input-floating"), n.appendChild(r), n;
425
+ }
426
+ function I(e, t) {
427
+ return P(t ? "amos-js-form-skeleton-row-stack" : "amos-js-form-skeleton-row", e);
428
+ }
429
+ function L({ labels: e, requirement: t, wrapCountryZip: n }) {
430
+ return t === "full" ? [
431
+ F(e),
432
+ F(e),
433
+ I([
434
+ F(e, 1.4),
435
+ F(e, .7),
436
+ F(e, .8)
437
+ ], !1),
438
+ F(e)
439
+ ] : [I([F(e), F(e)], n)];
440
+ }
441
+ function R(e) {
442
+ let t = e.appearance?.labels ?? "above", n = e.billingAddressRequirement ?? "country";
443
+ if (e.kind === "card") {
444
+ let r = [F(t), I([F(t), F(t)], !1)];
445
+ return e.additionalFields?.cardholderName && r.push(F(t)), r.push(...L({
446
+ labels: t,
447
+ requirement: n,
448
+ wrapCountryZip: !1
449
+ })), r;
450
+ }
451
+ return [
452
+ F(t),
453
+ I([F(t), F(t)], !0),
454
+ F(t),
455
+ I([F("above"), F("above")], !0),
456
+ ...L({
457
+ labels: t,
458
+ requirement: n,
459
+ wrapCountryZip: !0
460
+ })
461
+ ];
462
+ }
463
+ function z(e) {
464
+ M();
465
+ let t = P("amos-js-form-skeleton");
466
+ t.setAttribute("aria-hidden", "true");
467
+ function n(e) {
468
+ N(t, e.appearance), t.replaceChildren(...R(e));
469
+ }
470
+ return n(e), {
471
+ element: t,
472
+ update: n
473
+ };
474
+ }
475
+ //#endregion
395
476
  //#region src/payment-method-form.ts
396
- var k = {
477
+ var B = {
397
478
  country: 212,
398
479
  full: 452
399
- }, A = 80, j = {
480
+ }, V = 80, H = {
400
481
  country: 400,
401
482
  full: 640
402
483
  };
403
- function M(e, t = { cardholderName: !1 }, n = "country") {
484
+ function U(e, t = { cardholderName: !1 }, n = "country") {
404
485
  let r = Object.entries(t).filter(([, e]) => e).map(([e]) => e).join(","), a = new URLSearchParams({
405
486
  token: e,
406
487
  additionalFields: r,
@@ -408,20 +489,20 @@ function M(e, t = { cardholderName: !1 }, n = "country") {
408
489
  });
409
490
  return `${i(e)}/iframe/card?${a}`;
410
491
  }
411
- function N(e, t = "country") {
492
+ function W(e, t = "country") {
412
493
  let n = new URLSearchParams({
413
494
  token: e,
414
495
  billingAddressRequirement: t
415
496
  });
416
497
  return `${i(e)}/iframe/bank?${n}`;
417
498
  }
418
- function P(e = { cardholderName: !1 }, t = "country") {
419
- return `${(k[t] ?? k.country) + (e.cardholderName ? A : 0)}px`;
499
+ function G(e = { cardholderName: !1 }, t = "country") {
500
+ return `${(B[t] ?? B.country) + (e.cardholderName ? V : 0)}px`;
420
501
  }
421
- function F(e = "country") {
422
- return `${j[e] ?? j.country}px`;
502
+ function K(e = "country") {
503
+ return `${H[e] ?? H.country}px`;
423
504
  }
424
- function I(e, t) {
505
+ function q(e, t) {
425
506
  let n = { ...t };
426
507
  function r(t) {
427
508
  if (t.source === e.contentWindow) switch (t.data.type) {
@@ -467,7 +548,7 @@ function I(e, t) {
467
548
  }
468
549
  //#endregion
469
550
  //#region src/mount.ts
470
- function L(e) {
551
+ function J(e) {
471
552
  if (typeof e == "string") {
472
553
  let t = document.querySelector(e);
473
554
  if (!(t instanceof HTMLElement)) throw Error(`[amos-js] Container "${e}" did not match any HTMLElement.`);
@@ -475,69 +556,114 @@ function L(e) {
475
556
  }
476
557
  return e;
477
558
  }
478
- var R = {
559
+ var Y = {
479
560
  width: "calc(100% + 8px)",
480
561
  transition: "opacity 150ms ease-in, height 200ms ease-in-out",
481
562
  margin: "0 -4px",
482
563
  opacity: "0",
483
564
  border: "0"
565
+ }, X = {
566
+ position: "absolute",
567
+ top: "0",
568
+ left: "-4px",
569
+ width: "calc(100% + 8px)",
570
+ height: "100%",
571
+ margin: "0",
572
+ transition: "none",
573
+ pointerEvents: "none"
484
574
  };
485
- function z({ src: e, title: t, name: n, height: r, allow: i }) {
575
+ function Z({ src: e, title: t, name: n, height: r, allow: i }) {
486
576
  let a = document.createElement("iframe");
487
- return a.src = e, a.title = t, a.name = n, a.setAttribute("role", "presentation"), a.scrolling = "no", i && (a.allow = i), Object.assign(a.style, R, { height: r }), a;
488
- }
489
- function B(e, t) {
490
- let n = L(e), { renderToken: r, additionalFields: i = { cardholderName: !1 }, billingAddressRequirement: a = "country", ...o } = t, s = z({
491
- src: M(r, i, a),
492
- title: "Secure credit card payment method form powered by Amos",
493
- name: "amos-credit-card-payment-method-form",
494
- height: P(i, a)
495
- });
496
- n.appendChild(s);
497
- let c = I(s, {
498
- ...o,
577
+ return a.src = e, a.title = t, a.name = n, a.setAttribute("role", "presentation"), a.scrolling = "no", i && (a.allow = i), Object.assign(a.style, Y, { height: r }), a;
578
+ }
579
+ function Q({ host: e, iframe: t, listenerOptions: n, skeletonOptions: r }) {
580
+ let i = document.createElement("div");
581
+ i.style.position = "relative", i.style.width = "100%", i.setAttribute("aria-busy", "true");
582
+ let a = z(r);
583
+ Object.assign(t.style, X), i.append(a.element, t), e.appendChild(i);
584
+ let o = !1, s = !1, c, l = r.appearance, u, d;
585
+ function f() {
586
+ return i.getBoundingClientRect().height;
587
+ }
588
+ function p() {
589
+ return c ? Number.parseFloat(c) : NaN;
590
+ }
591
+ function m() {
592
+ if (o) return;
593
+ o = !0, d !== void 0 && (clearTimeout(d), d = void 0);
594
+ let e = f(), n = p(), r = Number.isFinite(n) ? Math.max(n, e) : e;
595
+ t.style.transition = "none", t.style.position = "", t.style.top = "", t.style.left = "", t.style.margin = Y.margin ?? "", t.style.height = `${r}px`, t.style.opacity = "1", t.style.pointerEvents = "", a.element.remove(), i.removeAttribute("aria-busy"), u = setTimeout(() => {
596
+ t.style.transition = "height 200ms ease-in-out";
597
+ }, 400);
598
+ }
599
+ function h() {
600
+ if (o || !s) return;
601
+ let e = p(), t = f();
602
+ Number.isFinite(e) && e >= t - 2 && m();
603
+ }
604
+ let g = q(t, {
605
+ ...n,
499
606
  onHeightChange: (e) => {
500
- s.style.height = e, o.onHeightChange?.(e);
607
+ c = e, o ? t.style.height = e : h(), n.onHeightChange?.(e);
501
608
  },
502
609
  onAppearanceReady: () => {
503
- s.style.opacity = "1", o.onAppearanceReady?.();
610
+ s = !0, h(), !o && d === void 0 && (d = setTimeout(() => {
611
+ m();
612
+ }, 1500)), n.onAppearanceReady?.();
504
613
  }
505
614
  });
506
615
  return {
507
- iframe: s,
508
- update: c.update,
616
+ iframe: t,
617
+ update(e) {
618
+ g.update(e), !o && "appearance" in e && (l = e.appearance, a.update({
619
+ ...r,
620
+ appearance: l
621
+ }));
622
+ },
509
623
  destroy() {
510
- c.destroy(), s.remove();
624
+ u !== void 0 && clearTimeout(u), d !== void 0 && clearTimeout(d), g.destroy(), i.remove();
511
625
  }
512
626
  };
513
627
  }
514
- function V(e, t) {
515
- let n = L(e), { renderToken: r, billingAddressRequirement: i = "country", ...a } = t, o = z({
516
- src: N(r, i),
517
- title: "Secure bank account payment method form powered by Amos",
518
- name: "amos-bank-account-payment-method-form",
519
- height: F(i)
520
- });
521
- n.appendChild(o);
522
- let s = I(o, {
523
- ...a,
524
- onHeightChange: (e) => {
525
- o.style.height = e, a.onHeightChange?.(e);
526
- },
527
- onAppearanceReady: () => {
528
- o.style.opacity = "1", a.onAppearanceReady?.();
628
+ function $(e, t) {
629
+ let n = J(e), { renderToken: r, additionalFields: i = { cardholderName: !1 }, billingAddressRequirement: a = "country", ...o } = t;
630
+ return Q({
631
+ host: n,
632
+ iframe: Z({
633
+ src: U(r, i, a),
634
+ title: "Secure credit card payment method form powered by Amos",
635
+ name: "amos-credit-card-payment-method-form",
636
+ height: G(i, a)
637
+ }),
638
+ listenerOptions: o,
639
+ skeletonOptions: {
640
+ kind: "card",
641
+ appearance: o.appearance,
642
+ additionalFields: i,
643
+ billingAddressRequirement: a
529
644
  }
530
645
  });
531
- return {
532
- iframe: o,
533
- update: s.update,
534
- destroy() {
535
- s.destroy(), o.remove();
646
+ }
647
+ function ee(e, t) {
648
+ let n = J(e), { renderToken: r, billingAddressRequirement: i = "country", ...a } = t;
649
+ return Q({
650
+ host: n,
651
+ iframe: Z({
652
+ src: W(r, i),
653
+ title: "Secure bank account payment method form powered by Amos",
654
+ name: "amos-bank-account-payment-method-form",
655
+ height: K(i)
656
+ }),
657
+ listenerOptions: a,
658
+ skeletonOptions: {
659
+ kind: "bank",
660
+ appearance: a.appearance,
661
+ billingAddressRequirement: i
536
662
  }
537
- };
663
+ });
538
664
  }
539
- function H(e, t) {
540
- let n = L(e), { renderToken: r, ...i } = t, a = z({
665
+ function te(e, t) {
666
+ let n = J(e), { renderToken: r, ...i } = t, a = Z({
541
667
  src: T(r),
542
668
  title: "Secure Google Pay button powered by Amos",
543
669
  name: "amos-google-pay-button",
@@ -562,8 +688,8 @@ function H(e, t) {
562
688
  }
563
689
  };
564
690
  }
565
- function U(e, t) {
566
- let n = L(e), { renderToken: r, ...i } = t, a = z({
691
+ function ne(e, t) {
692
+ let n = J(e), { renderToken: r, ...i } = t, a = Z({
567
693
  src: x(r),
568
694
  title: "Secure Apple Pay button powered by Amos",
569
695
  name: "amos-apple-pay-button",
@@ -589,4 +715,4 @@ function U(e, t) {
589
715
  };
590
716
  }
591
717
  //#endregion
592
- export { w as attachApplePayButtonListeners, D as attachGooglePayButtonListeners, I as attachPaymentMethodFormListeners, v as confirmPaymentIntent, y as confirmSetupIntent, c as createMessage, r as decodeJwt, O as formatGooglePayPaymentData, S as getApplePayButtonInitialHeight, x as getApplePayButtonSrc, F as getBankAccountFormInitialHeight, N as getBankAccountFormSrc, P as getCreditCardFormInitialHeight, M as getCreditCardFormSrc, i as getEmbedOrigin, E as getGooglePayButtonInitialHeight, T as getGooglePayButtonSrc, U as mountAmosApplePayButton, V as mountAmosBankAccountPaymentMethodForm, B as mountAmosCreditCardPaymentMethodForm, H as mountAmosGooglePayButton, o as pickApplePayButtonElementProps, s as pickGooglePayButtonElementProps, _ as resetForm, b as sendConfirmationResult, u as sendParentReadyMessage, a as serializeWalletButtonStyle, m as updateAmount, d as updateAppearance, f as updateApplePayButton, p as updateGooglePayButton, h as updateMerchantName, g as validateForm };
718
+ export { w as attachApplePayButtonListeners, D as attachGooglePayButtonListeners, q as attachPaymentMethodFormListeners, v as confirmPaymentIntent, y as confirmSetupIntent, c as createMessage, r as decodeJwt, O as formatGooglePayPaymentData, S as getApplePayButtonInitialHeight, x as getApplePayButtonSrc, K as getBankAccountFormInitialHeight, W as getBankAccountFormSrc, G as getCreditCardFormInitialHeight, U as getCreditCardFormSrc, i as getEmbedOrigin, E as getGooglePayButtonInitialHeight, T as getGooglePayButtonSrc, ne as mountAmosApplePayButton, ee as mountAmosBankAccountPaymentMethodForm, $ as mountAmosCreditCardPaymentMethodForm, te as mountAmosGooglePayButton, o as pickApplePayButtonElementProps, s as pickGooglePayButtonElementProps, _ as resetForm, b as sendConfirmationResult, u as sendParentReadyMessage, a as serializeWalletButtonStyle, m as updateAmount, d as updateAppearance, f as updateApplePayButton, p as updateGooglePayButton, h as updateMerchantName, g as validateForm };
package/dist/mount.d.ts CHANGED
@@ -45,6 +45,9 @@ export type AmosPaymentMethodFormMountController = PaymentMethodFormController &
45
45
  * element. Returns a controller exposing the underlying iframe, an
46
46
  * `update()` method, and a `destroy()` method.
47
47
  *
48
+ * A field-shaped skeleton is shown immediately and replaced by the
49
+ * iframe once appearance is applied.
50
+ *
48
51
  * Use the returned `controller.iframe` when calling
49
52
  * {@link validateForm}, {@link confirmPaymentIntent}, or
50
53
  * {@link confirmSetupIntent}.
@@ -73,6 +76,9 @@ export type AmosBankAccountPaymentMethodFormOptions = PaymentMethodFormListenerO
73
76
  * element. Returns a controller exposing the underlying iframe, an
74
77
  * `update()` method, and a `destroy()` method.
75
78
  *
79
+ * A field-shaped skeleton is shown immediately and replaced by the
80
+ * iframe once appearance is applied.
81
+ *
76
82
  * Use the returned `controller.iframe` when calling
77
83
  * {@link validateForm}, {@link confirmPaymentIntent}, or
78
84
  * {@link confirmSetupIntent}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amos.com/amos-js",
3
- "version": "0.9.11",
3
+ "version": "0.9.12",
4
4
  "main": "dist/index.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,244 @@
1
+ import type {
2
+ BillingAddressRequirement,
3
+ CreditCardAdditionalFields,
4
+ } from "./payment-method-form";
5
+ import type { Appearance, AppearanceLabels, ThemeVariable } from "./types";
6
+
7
+ const STYLE_ID = "amos-js-form-skeleton-styles";
8
+
9
+ const SKELETON_THEME_DEFAULTS: Record<string, string> = {
10
+ "--accent": "oklch(0.97 0 0)",
11
+ "--radius": "0.625rem",
12
+ "--input-height": "2.25rem",
13
+ "--floating-input-height": "3.25rem",
14
+ "--field-gap": "1rem",
15
+ "--control-gap": "0.5rem",
16
+ "--label-font-size": "0.875rem",
17
+ };
18
+
19
+ const SKELETON_STYLES = `
20
+ .amos-js-form-skeleton {
21
+ box-sizing: border-box;
22
+ container-type: inline-size;
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: var(--field-gap);
26
+ margin: 0 -4px;
27
+ padding-block: 0.25rem;
28
+ pointer-events: none;
29
+ width: calc(100% + 8px);
30
+ }
31
+ .amos-js-form-skeleton-field {
32
+ display: flex;
33
+ flex: 1 1 0;
34
+ flex-direction: column;
35
+ min-width: 0;
36
+ width: 100%;
37
+ }
38
+ .amos-js-form-skeleton-label {
39
+ flex-shrink: 0;
40
+ font-size: var(--label-font-size);
41
+ height: 1.75rem;
42
+ line-height: 1.75rem;
43
+ }
44
+ .amos-js-form-skeleton-input {
45
+ animation: amos-js-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
46
+ background: var(--accent);
47
+ border-radius: calc(var(--radius) * 0.8);
48
+ box-sizing: border-box;
49
+ height: var(--input-height);
50
+ width: 100%;
51
+ }
52
+ .amos-js-form-skeleton-input-floating {
53
+ height: var(--floating-input-height);
54
+ }
55
+ .amos-js-form-skeleton-row {
56
+ align-items: flex-start;
57
+ display: flex;
58
+ gap: var(--control-gap);
59
+ width: 100%;
60
+ }
61
+ .amos-js-form-skeleton-row-stack {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: var(--field-gap);
65
+ width: 100%;
66
+ }
67
+ @container (min-width: 24rem) {
68
+ .amos-js-form-skeleton-row-stack {
69
+ align-items: flex-start;
70
+ flex-direction: row;
71
+ gap: var(--control-gap);
72
+ }
73
+ }
74
+ @keyframes amos-js-skeleton-pulse {
75
+ 50% { opacity: 0.5; }
76
+ }
77
+ @media (prefers-reduced-motion: reduce) {
78
+ .amos-js-form-skeleton-input {
79
+ animation: none;
80
+ }
81
+ }
82
+ `;
83
+
84
+ function ensureSkeletonStyles(): void {
85
+ if (document.getElementById(STYLE_ID)) {
86
+ return;
87
+ }
88
+ const style = document.createElement("style");
89
+ style.id = STYLE_ID;
90
+ style.textContent = SKELETON_STYLES;
91
+ document.head.appendChild(style);
92
+ }
93
+
94
+ function applyTheme(
95
+ element: HTMLElement,
96
+ appearance: Appearance | undefined,
97
+ ): void {
98
+ for (const [property, value] of Object.entries(SKELETON_THEME_DEFAULTS)) {
99
+ element.style.setProperty(property, value);
100
+ }
101
+ const themeVariables = appearance?.themeVariables;
102
+ if (!themeVariables) {
103
+ return;
104
+ }
105
+ for (const [property, value] of Object.entries(themeVariables) as Array<
106
+ [ThemeVariable, string | undefined]
107
+ >) {
108
+ if (typeof value === "string" && value.trim() !== "") {
109
+ element.style.setProperty(property, value.trim());
110
+ }
111
+ }
112
+ }
113
+
114
+ function div(className: string, children?: Array<HTMLElement>): HTMLDivElement {
115
+ const node = document.createElement("div");
116
+ node.className = className;
117
+ if (children) {
118
+ for (const child of children) {
119
+ node.appendChild(child);
120
+ }
121
+ }
122
+ return node;
123
+ }
124
+
125
+ function field(labels: AppearanceLabels, grow?: number): HTMLDivElement {
126
+ const wrap = div("amos-js-form-skeleton-field");
127
+ if (grow !== undefined) {
128
+ wrap.style.flexGrow = String(grow);
129
+ }
130
+ if (labels === "above") {
131
+ wrap.appendChild(div("amos-js-form-skeleton-label"));
132
+ }
133
+ const input = div("amos-js-form-skeleton-input");
134
+ if (labels === "floating") {
135
+ input.classList.add("amos-js-form-skeleton-input-floating");
136
+ }
137
+ wrap.appendChild(input);
138
+ return wrap;
139
+ }
140
+
141
+ function row(children: Array<HTMLElement>, wrapAtSm: boolean): HTMLDivElement {
142
+ return div(
143
+ wrapAtSm ? "amos-js-form-skeleton-row-stack" : "amos-js-form-skeleton-row",
144
+ children,
145
+ );
146
+ }
147
+
148
+ function billingFields({
149
+ labels,
150
+ requirement,
151
+ wrapCountryZip,
152
+ }: {
153
+ labels: AppearanceLabels;
154
+ requirement: BillingAddressRequirement;
155
+ wrapCountryZip: boolean;
156
+ }): Array<HTMLElement> {
157
+ if (requirement === "full") {
158
+ return [
159
+ field(labels),
160
+ field(labels),
161
+ row([field(labels, 1.4), field(labels, 0.7), field(labels, 0.8)], false),
162
+ field(labels),
163
+ ];
164
+ }
165
+ return [row([field(labels), field(labels)], wrapCountryZip)];
166
+ }
167
+
168
+ export type PaymentMethodFormSkeletonKind = "card" | "bank";
169
+
170
+ export type PaymentMethodFormSkeletonOptions = {
171
+ kind: PaymentMethodFormSkeletonKind;
172
+ appearance?: Appearance;
173
+ additionalFields?: CreditCardAdditionalFields;
174
+ billingAddressRequirement?: BillingAddressRequirement;
175
+ };
176
+
177
+ function buildChildren(
178
+ options: PaymentMethodFormSkeletonOptions,
179
+ ): Array<HTMLElement> {
180
+ const labels = options.appearance?.labels ?? "above";
181
+ const billingAddressRequirement =
182
+ options.billingAddressRequirement ?? "country";
183
+
184
+ if (options.kind === "card") {
185
+ const children: Array<HTMLElement> = [
186
+ field(labels),
187
+ row([field(labels), field(labels)], false),
188
+ ];
189
+ if (options.additionalFields?.cardholderName) {
190
+ children.push(field(labels));
191
+ }
192
+ children.push(
193
+ ...billingFields({
194
+ labels,
195
+ requirement: billingAddressRequirement,
196
+ wrapCountryZip: false,
197
+ }),
198
+ );
199
+ return children;
200
+ }
201
+
202
+ return [
203
+ field(labels),
204
+ row([field(labels), field(labels)], true),
205
+ field(labels),
206
+ row([field("above"), field("above")], true),
207
+ ...billingFields({
208
+ labels,
209
+ requirement: billingAddressRequirement,
210
+ wrapCountryZip: true,
211
+ }),
212
+ ];
213
+ }
214
+
215
+ export type PaymentMethodFormSkeleton = {
216
+ element: HTMLElement;
217
+ update: (options: PaymentMethodFormSkeletonOptions) => void;
218
+ };
219
+
220
+ /**
221
+ * Host-page placeholder that mirrors card/bank field layout using the
222
+ * same appearance variables the iframe will apply. Shown immediately
223
+ * while the iframe document loads, then removed when appearance is
224
+ * ready.
225
+ */
226
+ export function createPaymentMethodFormSkeleton(
227
+ options: PaymentMethodFormSkeletonOptions,
228
+ ): PaymentMethodFormSkeleton {
229
+ ensureSkeletonStyles();
230
+ const element = div("amos-js-form-skeleton");
231
+ element.setAttribute("aria-hidden", "true");
232
+
233
+ function render(next: PaymentMethodFormSkeletonOptions): void {
234
+ applyTheme(element, next.appearance);
235
+ element.replaceChildren(...buildChildren(next));
236
+ }
237
+
238
+ render(options);
239
+
240
+ return {
241
+ element,
242
+ update: render,
243
+ };
244
+ }
package/src/mount.ts CHANGED
@@ -5,6 +5,10 @@ import {
5
5
  getApplePayButtonInitialHeight,
6
6
  getApplePayButtonSrc,
7
7
  } from "./apple-pay";
8
+ import {
9
+ createPaymentMethodFormSkeleton,
10
+ type PaymentMethodFormSkeletonOptions,
11
+ } from "./form-skeleton";
8
12
  import {
9
13
  attachGooglePayButtonListeners,
10
14
  type GooglePayButtonController,
@@ -47,6 +51,18 @@ const SHARED_IFRAME_STYLE: Partial<CSSStyleDeclaration> = {
47
51
  border: "0",
48
52
  };
49
53
 
54
+ const SKELETON_IFRAME_STYLE: Partial<CSSStyleDeclaration> = {
55
+ position: "absolute",
56
+ top: "0",
57
+ left: "-4px",
58
+ width: "calc(100% + 8px)",
59
+ height: "100%",
60
+ margin: "0",
61
+ // Do not interpolate opacity while the skeleton is showing.
62
+ transition: "none",
63
+ pointerEvents: "none",
64
+ };
65
+
50
66
  function createIframe({
51
67
  src,
52
68
  title,
@@ -73,6 +89,133 @@ function createIframe({
73
89
  return iframe;
74
90
  }
75
91
 
92
+ function mountPaymentMethodFormWithSkeleton({
93
+ host,
94
+ iframe,
95
+ listenerOptions,
96
+ skeletonOptions,
97
+ }: {
98
+ host: HTMLElement;
99
+ iframe: HTMLIFrameElement;
100
+ listenerOptions: PaymentMethodFormListenerOptions;
101
+ skeletonOptions: PaymentMethodFormSkeletonOptions;
102
+ }): AmosPaymentMethodFormMountController {
103
+ const wrapper = document.createElement("div");
104
+ wrapper.style.position = "relative";
105
+ wrapper.style.width = "100%";
106
+ wrapper.setAttribute("aria-busy", "true");
107
+
108
+ const skeleton = createPaymentMethodFormSkeleton(skeletonOptions);
109
+ Object.assign(iframe.style, SKELETON_IFRAME_STYLE);
110
+
111
+ wrapper.append(skeleton.element, iframe);
112
+ host.appendChild(wrapper);
113
+
114
+ let revealed = false;
115
+ let appearanceReady = false;
116
+ let lastHeight: string | undefined;
117
+ let appearance = skeletonOptions.appearance;
118
+ let heightTransitionTimer: ReturnType<typeof setTimeout> | undefined;
119
+ let fallbackRevealTimer: ReturnType<typeof setTimeout> | undefined;
120
+
121
+ function skeletonHeightPx(): number {
122
+ return wrapper.getBoundingClientRect().height;
123
+ }
124
+
125
+ function reportedHeightPx(): number {
126
+ return lastHeight ? Number.parseFloat(lastHeight) : Number.NaN;
127
+ }
128
+
129
+ function reveal(): void {
130
+ if (revealed) {
131
+ return;
132
+ }
133
+ revealed = true;
134
+ if (fallbackRevealTimer !== undefined) {
135
+ clearTimeout(fallbackRevealTimer);
136
+ fallbackRevealTimer = undefined;
137
+ }
138
+
139
+ const skeletonPx = skeletonHeightPx();
140
+ const reportedPx = reportedHeightPx();
141
+ const revealPx = Number.isFinite(reportedPx)
142
+ ? Math.max(reportedPx, skeletonPx)
143
+ : skeletonPx;
144
+
145
+ iframe.style.transition = "none";
146
+ iframe.style.position = "";
147
+ iframe.style.top = "";
148
+ iframe.style.left = "";
149
+ iframe.style.margin = SHARED_IFRAME_STYLE.margin ?? "";
150
+ iframe.style.height = `${revealPx}px`;
151
+ iframe.style.opacity = "1";
152
+ iframe.style.pointerEvents = "";
153
+ skeleton.element.remove();
154
+ wrapper.removeAttribute("aria-busy");
155
+
156
+ // Height easing is for later layout changes (ZIP show/hide, errors),
157
+ // not the first paint — that would animate the last row into place.
158
+ heightTransitionTimer = setTimeout(() => {
159
+ iframe.style.transition = "height 200ms ease-in-out";
160
+ }, 400);
161
+ }
162
+
163
+ function tryReveal(): void {
164
+ if (revealed || !appearanceReady) {
165
+ return;
166
+ }
167
+ const reportedPx = reportedHeightPx();
168
+ const skeletonPx = skeletonHeightPx();
169
+ if (Number.isFinite(reportedPx) && reportedPx >= skeletonPx - 2) {
170
+ reveal();
171
+ }
172
+ }
173
+
174
+ const controller = attachPaymentMethodFormListeners(iframe, {
175
+ ...listenerOptions,
176
+ onHeightChange: (height) => {
177
+ lastHeight = height;
178
+ if (revealed) {
179
+ iframe.style.height = height;
180
+ } else {
181
+ tryReveal();
182
+ }
183
+ listenerOptions.onHeightChange?.(height);
184
+ },
185
+ onAppearanceReady: () => {
186
+ appearanceReady = true;
187
+ tryReveal();
188
+ if (!revealed && fallbackRevealTimer === undefined) {
189
+ fallbackRevealTimer = setTimeout(() => {
190
+ reveal();
191
+ }, 1500);
192
+ }
193
+ listenerOptions.onAppearanceReady?.();
194
+ },
195
+ });
196
+
197
+ return {
198
+ iframe,
199
+ update(patch) {
200
+ controller.update(patch);
201
+ if (!revealed && "appearance" in patch) {
202
+ appearance = patch.appearance;
203
+ skeleton.update({ ...skeletonOptions, appearance });
204
+ }
205
+ },
206
+ destroy() {
207
+ if (heightTransitionTimer !== undefined) {
208
+ clearTimeout(heightTransitionTimer);
209
+ }
210
+ if (fallbackRevealTimer !== undefined) {
211
+ clearTimeout(fallbackRevealTimer);
212
+ }
213
+ controller.destroy();
214
+ wrapper.remove();
215
+ },
216
+ };
217
+ }
218
+
76
219
  /**
77
220
  * Options accepted by {@link mountAmosCreditCardPaymentMethodForm}.
78
221
  */
@@ -120,6 +263,9 @@ export type AmosPaymentMethodFormMountController =
120
263
  * element. Returns a controller exposing the underlying iframe, an
121
264
  * `update()` method, and a `destroy()` method.
122
265
  *
266
+ * A field-shaped skeleton is shown immediately and replaced by the
267
+ * iframe once appearance is applied.
268
+ *
123
269
  * Use the returned `controller.iframe` when calling
124
270
  * {@link validateForm}, {@link confirmPaymentIntent}, or
125
271
  * {@link confirmSetupIntent}.
@@ -149,28 +295,18 @@ export function mountAmosCreditCardPaymentMethodForm(
149
295
  billingAddressRequirement,
150
296
  ),
151
297
  });
152
- host.appendChild(iframe);
153
-
154
- const controller = attachPaymentMethodFormListeners(iframe, {
155
- ...listenerOptions,
156
- onHeightChange: (height) => {
157
- iframe.style.height = height;
158
- listenerOptions.onHeightChange?.(height);
159
- },
160
- onAppearanceReady: () => {
161
- iframe.style.opacity = "1";
162
- listenerOptions.onAppearanceReady?.();
163
- },
164
- });
165
298
 
166
- return {
299
+ return mountPaymentMethodFormWithSkeleton({
300
+ host,
167
301
  iframe,
168
- update: controller.update,
169
- destroy() {
170
- controller.destroy();
171
- iframe.remove();
302
+ listenerOptions,
303
+ skeletonOptions: {
304
+ kind: "card",
305
+ appearance: listenerOptions.appearance,
306
+ additionalFields,
307
+ billingAddressRequirement,
172
308
  },
173
- };
309
+ });
174
310
  }
175
311
 
176
312
  /**
@@ -198,6 +334,9 @@ export type AmosBankAccountPaymentMethodFormOptions =
198
334
  * element. Returns a controller exposing the underlying iframe, an
199
335
  * `update()` method, and a `destroy()` method.
200
336
  *
337
+ * A field-shaped skeleton is shown immediately and replaced by the
338
+ * iframe once appearance is applied.
339
+ *
201
340
  * Use the returned `controller.iframe` when calling
202
341
  * {@link validateForm}, {@link confirmPaymentIntent}, or
203
342
  * {@link confirmSetupIntent}.
@@ -219,28 +358,17 @@ export function mountAmosBankAccountPaymentMethodForm(
219
358
  name: "amos-bank-account-payment-method-form",
220
359
  height: getBankAccountFormInitialHeight(billingAddressRequirement),
221
360
  });
222
- host.appendChild(iframe);
223
-
224
- const controller = attachPaymentMethodFormListeners(iframe, {
225
- ...listenerOptions,
226
- onHeightChange: (height) => {
227
- iframe.style.height = height;
228
- listenerOptions.onHeightChange?.(height);
229
- },
230
- onAppearanceReady: () => {
231
- iframe.style.opacity = "1";
232
- listenerOptions.onAppearanceReady?.();
233
- },
234
- });
235
361
 
236
- return {
362
+ return mountPaymentMethodFormWithSkeleton({
363
+ host,
237
364
  iframe,
238
- update: controller.update,
239
- destroy() {
240
- controller.destroy();
241
- iframe.remove();
365
+ listenerOptions,
366
+ skeletonOptions: {
367
+ kind: "bank",
368
+ appearance: listenerOptions.appearance,
369
+ billingAddressRequirement,
242
370
  },
243
- };
371
+ });
244
372
  }
245
373
 
246
374
  /**