@amos.com/amos-js 0.9.10 → 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.
@@ -63,6 +63,9 @@ const form = mountAmosCreditCardPaymentMethodForm(
63
63
  console.log("Recoverable:", result.reason);
64
64
  }
65
65
  },
66
+ onValidityChange: ({ isValid }) => {
67
+ document.querySelector("#pay-now")!.disabled = !isValid;
68
+ },
66
69
  },
67
70
  );
68
71
 
@@ -93,7 +96,7 @@ form.destroy();
93
96
  The following flow is for credit card and bank account payment method types only.
94
97
 
95
98
  1. **Set up prerequisites**: create a `renderToken` (safe for client), and keep `apiKey` and `accountId` server-side only.
96
- 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.
97
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.
98
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.
99
102
  5. **Return the payment intent token to the browser**: your backend responds with the embed token (`components["schemas"]["EmbedToken"]`) needed for confirmation.
@@ -264,13 +267,14 @@ Mount the secure credit-card payment method form into a container element (an `H
264
267
  - `billingAddressRequirement` (`"country" | "full"`, defaults to `"country"`) — how much billing address the iframe collects. `country` collects country / region and, for CA / PR / GB / US, a postal code (labeled ZIP for the United States). `full` shows a full street address form with Smarty autocomplete.
265
268
 
266
269
 
267
- - `onHeightChange`, `onAppearanceReady` (advanced override the default iframe styling logic)
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). The skeleton is removed and the iframe faded in when `onAppearanceReady` fires.
268
272
 
269
273
  **Returns** `AmosPaymentMethodFormMountController`:
270
274
 
271
275
  - `iframe` — the underlying `<iframe>` element.
272
276
  - `update(patch)` — patch any of the options listed above.
273
- - `destroy()` — remove the iframe and detach listeners.
277
+ - `destroy()` — remove the iframe (and any loading skeleton) and detach listeners.
274
278
 
275
279
  ### `mountAmosBankAccountPaymentMethodForm(container, options)`
276
280
 
@@ -363,7 +367,7 @@ Advanced helpers exposed for integrators that need to construct or inspect the m
363
367
 
364
368
  ### Exported types
365
369
 
366
- `Message`, `Appearance`, `ThemeVariable`, `FormattedGooglePayPaymentData`, plus the per-form `*Options` and `*Controller` types. For OpenAPI schema types, import `components` from `@amos.com/node`.
370
+ `Message`, `Appearance`, `ThemeVariable`, `FormattedGooglePayPaymentData`, `PaymentMethodFormValidityChangeEvent`, plus the per-form `*Options` and `*Controller` types. For OpenAPI schema types, import `components` from `@amos.com/node`.
367
371
 
368
372
  ## Notes and potential gotchas
369
373
 
@@ -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.d.ts CHANGED
@@ -9,5 +9,5 @@ export type { AmosApplePayButtonMountController, AmosApplePayButtonOptions, Amos
9
9
  export { mountAmosApplePayButton, mountAmosBankAccountPaymentMethodForm, mountAmosCreditCardPaymentMethodForm, mountAmosGooglePayButton, } from './mount';
10
10
  export type { BillingAddressRequirement, CreditCardAdditionalFields, PaymentMethodFormController, PaymentMethodFormListenerOptions, } from './payment-method-form';
11
11
  export { attachPaymentMethodFormListeners, getBankAccountFormInitialHeight, getBankAccountFormSrc, getCreditCardFormInitialHeight, getCreditCardFormSrc, } from './payment-method-form';
12
- export type { Appearance, AppearanceLabels, ApplePayButtonElementProps, ApplePayButtonStyle, ApplePayButtonType, ConfirmationIncompleteReason, ConfirmationResult, GooglePayButtonElementProps, Message, ThemeVariable, WalletButtonStyle, } from './types';
12
+ export type { Appearance, AppearanceLabels, ApplePayButtonElementProps, ApplePayButtonStyle, ApplePayButtonType, ConfirmationIncompleteReason, ConfirmationResult, GooglePayButtonElementProps, Message, PaymentMethodFormValidityChangeEvent, ThemeVariable, WalletButtonStyle, } from './types';
13
13
  export { createMessage, pickApplePayButtonElementProps, pickGooglePayButtonElementProps, serializeWalletButtonStyle, } from './types';
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`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) {
@@ -443,6 +524,9 @@ function I(e, t) {
443
524
  case "UPDATED_APPEARANCE":
444
525
  n.onAppearanceReady?.();
445
526
  break;
527
+ case "FORM_VALIDITY_CHANGE":
528
+ n.onValidityChange?.({ isValid: t.data.isValid });
529
+ break;
446
530
  case "CONFIRMATION_RESULT": n.onResult(t.data.result);
447
531
  }
448
532
  }
@@ -464,7 +548,7 @@ function I(e, t) {
464
548
  }
465
549
  //#endregion
466
550
  //#region src/mount.ts
467
- function L(e) {
551
+ function J(e) {
468
552
  if (typeof e == "string") {
469
553
  let t = document.querySelector(e);
470
554
  if (!(t instanceof HTMLElement)) throw Error(`[amos-js] Container "${e}" did not match any HTMLElement.`);
@@ -472,69 +556,114 @@ function L(e) {
472
556
  }
473
557
  return e;
474
558
  }
475
- var R = {
559
+ var Y = {
476
560
  width: "calc(100% + 8px)",
477
561
  transition: "opacity 150ms ease-in, height 200ms ease-in-out",
478
562
  margin: "0 -4px",
479
563
  opacity: "0",
480
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"
481
574
  };
482
- 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 }) {
483
576
  let a = document.createElement("iframe");
484
- 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;
485
- }
486
- function B(e, t) {
487
- let n = L(e), { renderToken: r, additionalFields: i = { cardholderName: !1 }, billingAddressRequirement: a = "country", ...o } = t, s = z({
488
- src: M(r, i, a),
489
- title: "Secure credit card payment method form powered by Amos",
490
- name: "amos-credit-card-payment-method-form",
491
- height: P(i, a)
492
- });
493
- n.appendChild(s);
494
- let c = I(s, {
495
- ...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,
496
606
  onHeightChange: (e) => {
497
- s.style.height = e, o.onHeightChange?.(e);
607
+ c = e, o ? t.style.height = e : h(), n.onHeightChange?.(e);
498
608
  },
499
609
  onAppearanceReady: () => {
500
- s.style.opacity = "1", o.onAppearanceReady?.();
610
+ s = !0, h(), !o && d === void 0 && (d = setTimeout(() => {
611
+ m();
612
+ }, 1500)), n.onAppearanceReady?.();
501
613
  }
502
614
  });
503
615
  return {
504
- iframe: s,
505
- 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
+ },
506
623
  destroy() {
507
- c.destroy(), s.remove();
624
+ u !== void 0 && clearTimeout(u), d !== void 0 && clearTimeout(d), g.destroy(), i.remove();
508
625
  }
509
626
  };
510
627
  }
511
- function V(e, t) {
512
- let n = L(e), { renderToken: r, billingAddressRequirement: i = "country", ...a } = t, o = z({
513
- src: N(r, i),
514
- title: "Secure bank account payment method form powered by Amos",
515
- name: "amos-bank-account-payment-method-form",
516
- height: F(i)
517
- });
518
- n.appendChild(o);
519
- let s = I(o, {
520
- ...a,
521
- onHeightChange: (e) => {
522
- o.style.height = e, a.onHeightChange?.(e);
523
- },
524
- onAppearanceReady: () => {
525
- 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
526
644
  }
527
645
  });
528
- return {
529
- iframe: o,
530
- update: s.update,
531
- destroy() {
532
- 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
533
662
  }
534
- };
663
+ });
535
664
  }
536
- function H(e, t) {
537
- 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({
538
667
  src: T(r),
539
668
  title: "Secure Google Pay button powered by Amos",
540
669
  name: "amos-google-pay-button",
@@ -559,8 +688,8 @@ function H(e, t) {
559
688
  }
560
689
  };
561
690
  }
562
- function U(e, t) {
563
- 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({
564
693
  src: x(r),
565
694
  title: "Secure Apple Pay button powered by Amos",
566
695
  name: "amos-apple-pay-button",
@@ -586,4 +715,4 @@ function U(e, t) {
586
715
  };
587
716
  }
588
717
  //#endregion
589
- 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}.
@@ -1,4 +1,4 @@
1
- import { Appearance, ConfirmationResult } from './types';
1
+ import { Appearance, ConfirmationResult, PaymentMethodFormValidityChangeEvent } from './types';
2
2
  /**
3
3
  * The additional fields beyond the standard card number, expiration
4
4
  * date, CVV, and billing address fields that are required to be filled
@@ -59,6 +59,12 @@ export type PaymentMethodFormListenerOptions = {
59
59
  * iframe's opacity from `0` to `1` to fade it in.
60
60
  */
61
61
  onAppearanceReady?: () => void;
62
+ /**
63
+ * Called when card/bank form validity changes. `isValid` is true
64
+ * when all required fields are present and valid. Does not include
65
+ * PCI data. Use this to enable or disable a host checkout button.
66
+ */
67
+ onValidityChange?: (event: PaymentMethodFormValidityChangeEvent) => void;
62
68
  /**
63
69
  * Called when the interactive confirmation flow finishes (success or
64
70
  * terminal failure). Not settlement proof — verify via webhooks.
package/dist/types.d.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  import { components } from '@amos.com/node';
2
+ /**
3
+ * PCI-safe snapshot of card/bank form validity, posted when HTML
4
+ * constraint validation changes. Use this to enable or disable a host
5
+ * checkout button.
6
+ */
7
+ export type PaymentMethodFormValidityChangeEvent = {
8
+ isValid: boolean;
9
+ };
2
10
  /**
3
11
  * Why an interactive confirmation attempt ended with `status: "incomplete"`.
4
12
  *
@@ -243,6 +251,14 @@ export type Message = {
243
251
  } | {
244
252
  /** Parent → embed: clear all form field values and API errors. */
245
253
  type: "RESET_FORM";
254
+ } | {
255
+ /**
256
+ * Embed → parent: card/bank form validity changed.
257
+ * `isValid` is true when all required fields are present and
258
+ * valid. Does not include PCI data.
259
+ */
260
+ type: "FORM_VALIDITY_CHANGE";
261
+ isValid: boolean;
246
262
  };
247
263
  /**
248
264
  * Identity helper that brands an object as a typed `Message`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amos.com/amos-js",
3
- "version": "0.9.10",
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/index.ts CHANGED
@@ -75,6 +75,7 @@ export type {
75
75
  ConfirmationResult,
76
76
  GooglePayButtonElementProps,
77
77
  Message,
78
+ PaymentMethodFormValidityChangeEvent,
78
79
  ThemeVariable,
79
80
  WalletButtonStyle,
80
81
  } from "./types";
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
  /**
@@ -3,7 +3,12 @@ import {
3
3
  sendParentReadyMessage,
4
4
  updateAppearance as sendUpdateAppearance,
5
5
  } from "./messaging";
6
- import type { Appearance, ConfirmationResult, Message } from "./types";
6
+ import type {
7
+ Appearance,
8
+ ConfirmationResult,
9
+ Message,
10
+ PaymentMethodFormValidityChangeEvent,
11
+ } from "./types";
7
12
 
8
13
  /**
9
14
  * The additional fields beyond the standard card number, expiration
@@ -126,6 +131,12 @@ export type PaymentMethodFormListenerOptions = {
126
131
  * iframe's opacity from `0` to `1` to fade it in.
127
132
  */
128
133
  onAppearanceReady?: () => void;
134
+ /**
135
+ * Called when card/bank form validity changes. `isValid` is true
136
+ * when all required fields are present and valid. Does not include
137
+ * PCI data. Use this to enable or disable a host checkout button.
138
+ */
139
+ onValidityChange?: (event: PaymentMethodFormValidityChangeEvent) => void;
129
140
  /**
130
141
  * Called when the interactive confirmation flow finishes (success or
131
142
  * terminal failure). Not settlement proof — verify via webhooks.
@@ -191,6 +202,10 @@ export function attachPaymentMethodFormListeners(
191
202
  current.onAppearanceReady?.();
192
203
  break;
193
204
 
205
+ case "FORM_VALIDITY_CHANGE":
206
+ current.onValidityChange?.({ isValid: event.data.isValid });
207
+ break;
208
+
194
209
  case "CONFIRMATION_RESULT":
195
210
  current.onResult(event.data.result);
196
211
  break;
package/src/types.ts CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  import type { components } from "@amos.com/node";
4
4
 
5
+ /**
6
+ * PCI-safe snapshot of card/bank form validity, posted when HTML
7
+ * constraint validation changes. Use this to enable or disable a host
8
+ * checkout button.
9
+ */
10
+ export type PaymentMethodFormValidityChangeEvent = {
11
+ isValid: boolean;
12
+ };
13
+
5
14
  /**
6
15
  * Why an interactive confirmation attempt ended with `status: "incomplete"`.
7
16
  *
@@ -593,6 +602,15 @@ export type Message =
593
602
  | {
594
603
  /** Parent → embed: clear all form field values and API errors. */
595
604
  type: "RESET_FORM";
605
+ }
606
+ | {
607
+ /**
608
+ * Embed → parent: card/bank form validity changed.
609
+ * `isValid` is true when all required fields are present and
610
+ * valid. Does not include PCI data.
611
+ */
612
+ type: "FORM_VALIDITY_CHANGE";
613
+ isValid: boolean;
596
614
  };
597
615
 
598
616
  /**