@amos.com/amos-js 0.9.1 → 0.9.3

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
@@ -1,6 +1,6 @@
1
1
  # Amos JavaScript SDK
2
2
 
3
- `@amos.com/amos-js` is a framework-agnostic JavaScript SDK for embedding Amos payment methods (credit card, bank account, Google Pay) into your web app via secure iframes, and for communicating with those iframes from the host page.
3
+ `@amos.com/amos-js` is a framework-agnostic JavaScript SDK for embedding Amos payment methods (credit card, bank account, Google Pay, Apple Pay) into your web app via secure iframes, and for communicating with those iframes from the host page.
4
4
 
5
5
  It is the foundation for `@amos.com/react-amos-js`, but is fully usable on its own from vanilla JavaScript / TypeScript / any other framework.
6
6
 
@@ -14,8 +14,8 @@ 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`) 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.
18
- - **Lower-level building blocks** (`getCreditCardFormSrc`, `attachPaymentMethodFormListeners`, `attachGooglePayButtonListeners`, ...) for integrators (such as `@amos.com/react-amos-js`) that want to render the iframe element themselves.
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.
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.
21
21
 
@@ -27,7 +27,7 @@ npm install @amos.com/amos-js
27
27
  3. Amos account ID (provided once your application has been approved)
28
28
  ```
29
29
 
30
- The render token configures the iframe's allowed origin(s), allowed payment methods, and the range of valid payment amounts. If the render token does not allow an origin, the iframe will not render. Similarly, components corresponding to different payment method types will not render if not allowed by the render token.
30
+ The render token configures the iframe's allowed origin(s), allowed payment methods, accepted billing countries or US states, and the range of valid payment amounts. These settings come from the render template and are embedded in the token JWT (`RenderTokenJwt`); billing geography is controlled by `billing_address_options` (`allowed_countries` for international templates, `allowed_states` for US-only). If the render token does not allow an origin, the iframe will not render. Similarly, components corresponding to different payment method types will not render if not allowed by the render token, and billing addresses outside the configured countries or states will be rejected.
31
31
 
32
32
  > **Note**: The render token also determines the environment (`production` or `sandbox`). Render tokens created on `dashboard.amos.com` have a `production` environment. Render tokens created on `dashboard-sandbox.amos.com` have a `sandbox` environment. Similarly, API keys can only access the environment that they were created in.
33
33
 
@@ -58,7 +58,10 @@ const form = mountAmosCreditCardPaymentMethodForm(
58
58
  } else if (result.status === "failed") {
59
59
  console.error("Confirm failed:", result.errorMessage);
60
60
  }
61
- // status === "incomplete": field errors shown in the iframe; customer can retry
61
+ // status === "incomplete": field errors or validation_failed unlock UI
62
+ if (result.status === "incomplete") {
63
+ console.log("Recoverable:", result.reason);
64
+ }
62
65
  },
63
66
  },
64
67
  );
@@ -97,7 +100,7 @@ The following flow is for credit card and bank account payment method types only
97
100
  6. **Confirm the payment intent from the client**: call `confirmPaymentIntent({ iframe: form.iframe, token })` in the browser to continue the payment flow.
98
101
  7. **Handle UX**: show the user a "processing" state when the "Pay now" button is clicked, and handle `onResult`. Do not treat `onResult` as settlement proof — verify payment success on your backend via webhooks. Recoverable field errors are shown in the iframe (`status: "incomplete"`).
99
102
 
100
- ### Google Pay
103
+ ### Google Pay & Apple Pay
101
104
 
102
105
  Google Pay and Apple Pay are forms of express checkout. Their buttons are alternatives to the "Pay now" button in your payment forms. Users can make a payment with either flow.
103
106
 
@@ -145,6 +148,8 @@ const button = mountAmosGooglePayButton(
145
148
  button.update({ amount: "7500" });
146
149
  ```
147
150
 
151
+ Apple Pay uses the same express-checkout flow and options — swap `mountAmosGooglePayButton` for `mountAmosApplePayButton`. When Apple Pay Code opens in a separate window, the SDK shows a host-page waiting overlay and tears it down when the session ends.
152
+
148
153
  ## Understanding the flow for creating and confirming setup intents
149
154
 
150
155
  Setup intents are used to save payment methods for future use (e.g. recurring payments, subscriptions) without charging the customer immediately. The flow is identical to a payment intent, except:
@@ -165,7 +170,7 @@ Why this matters:
165
170
  - Raw payment details are submitted from the iframe directly to Amos-controlled infrastructure.
166
171
  - Your backend only creates payment intents (or setup intents) and returns a short-lived token used to continue the iframe flow.
167
172
  - `confirmPaymentIntent` / `confirmSetupIntent` sends the token back to the iframe to complete confirmation; it does not pass full payment method payloads through your app server.
168
- - In express flows (e.g. Google Pay), the iframe component handles payment data exchange and only asks your server to create a payment intent token.
173
+ - In express flows (e.g. Google Pay, Apple Pay), the iframe component handles payment data exchange and only asks your server to create a payment intent token.
169
174
 
170
175
  In short, your app orchestrates the payment flow, while sensitive payment data stays within Amos-controlled components and APIs.
171
176
 
@@ -230,7 +235,7 @@ Mount the secure credit-card payment method form into a container element (an `H
230
235
  **Required `options`:**
231
236
 
232
237
  - `renderToken` (`string`)
233
- - `onResult` (`(result: ConfirmationResult) => void`) — required. Called when the interactive confirmation attempt finishes (`succeeded`, `failed`, or `incomplete`). Not settlement proof; verify via webhooks.
238
+ - `onResult` (`(result: ConfirmationResult) => void`) — required. Called when the interactive confirmation attempt finishes (`succeeded`, `failed`, or `incomplete` with `reason`). Not settlement proof; verify via webhooks.
234
239
 
235
240
  **Optional `options`:**
236
241
 
@@ -262,7 +267,7 @@ Mount the secure Google Pay button (express checkout) into a container element.
262
267
  - `merchantName` (`string`)
263
268
  - `onInitiatePaymentIntentRequest` (`({ paymentIntentCreateAttributes, customerCreateAttributes }) => Promise<components["schemas"]["EmbedToken"]["token"]>`)
264
269
 
265
- - `onResult` (`(result: ConfirmationResult) => void`) — required. Called when the interactive confirmation attempt finishes (`succeeded`, `failed`, or `incomplete`). Not settlement proof; verify via webhooks.
270
+ - `onResult` (`(result: ConfirmationResult) => void`) — required. Called when the interactive confirmation attempt finishes (`succeeded`, `failed`, or `incomplete` with `reason`). Not settlement proof; verify via webhooks.
266
271
 
267
272
  **Optional `options`:** `appearance`, `onHeightChange`, `onAppearanceReady`.
268
273
 
@@ -288,11 +293,11 @@ Lower-level helper that wires up the host-page side of the credit-card or bank-a
288
293
 
289
294
  This is what `@amos.com/react-amos-js` uses to integrate with React's rendering model.
290
295
 
291
- ### `attachGooglePayButtonListeners(iframe, options)`
296
+ ### `attachGooglePayButtonListeners(iframe, options)` / `attachApplePayButtonListeners(iframe, options)`
292
297
 
293
- The Google Pay equivalent of `attachPaymentMethodFormListeners`.
298
+ The Google Pay / Apple Pay equivalents of `attachPaymentMethodFormListeners`.
294
299
 
295
- ### `getCreditCardFormSrc(renderToken, additionalFields?, billingAddressRequirement?)` / `getBankAccountFormSrc(renderToken, billingAddressRequirement?)` / `getGooglePayButtonSrc(renderToken)`
300
+ ### `getCreditCardFormSrc(renderToken, additionalFields?, billingAddressRequirement?)` / `getBankAccountFormSrc(renderToken, billingAddressRequirement?)` / `getGooglePayButtonSrc(renderToken)` / `getApplePayButtonSrc(renderToken)`
296
301
 
297
302
  Build the iframe `src` URL for each form type.
298
303
 
@@ -312,7 +317,7 @@ Advanced helpers exposed for integrators that need to construct or inspect the m
312
317
 
313
318
  - **`iframe` argument**: every messaging helper (`validateForm`, `confirmPaymentIntent`, `confirmSetupIntent`) accepts the `iframe` element directly. With the mount helpers, use `controller.iframe`.
314
319
  - **Same components for payment vs setup intents**: `mountAmosCreditCardPaymentMethodForm` and `mountAmosBankAccountPaymentMethodForm` support both payment intents and setup intents. The flow differs only by which server call you make and which confirmation function you use. Handle both outcomes via `onResult`.
315
- - **Amount format**: for `mountAmosGooglePayButton`, `amount` is a string (e.g. `"5000"` for $50.00). For `components["schemas"]["CreatePaymentIntentInput"]` on the server, `amount` is a number in cents (e.g. `5000`).
320
+ - **Amount format**: for `mountAmosGooglePayButton` and `mountAmosApplePayButton`, `amount` is a string (e.g. `"5000"` for $50.00). For `components["schemas"]["CreatePaymentIntentInput"]` on the server, `amount` is a number in cents (e.g. `5000`).
316
321
  - **Browser-only**: the mount and messaging helpers require `window` and the DOM. They are not safe to call during server-side rendering — call them from client-side code only (for example, inside a `useEffect`-like hook in your framework of choice).
317
322
 
318
323
  ---
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, ConfirmationResult, Message, ThemeVariable, } from './types';
12
+ export type { Appearance, AppearanceLabels, ConfirmationIncompleteReason, ConfirmationResult, Message, ThemeVariable, } from './types';
13
13
  export { createMessage } from './types';
package/dist/index.js CHANGED
@@ -1 +1 @@
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 14 17" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M13.072 5.846c-.1.08-1.873 1.09-1.873 3.34 0 2.61 2.29 3.53 2.36 3.55-.04.1-.368 1.27-.766 2.52-.35 1.09-.72 2.18-1.28 2.18-.55 0-.73-.36-1.43-.36-.71 0-.93.37-1.48.37-.55 0-.93-.99-1.35-2.01-.5-1.2-.88-2.43-.88-3.85 0-2.26 1.47-3.46 2.91-3.46.57 0 1.11.38 1.49.38.37 0 .98-.45 1.7-.45.28 0 1.27.03 1.97 1.01ZM9.52 3.37c.3-.36.52-.86.52-1.36 0-.07 0-.14-.01-.2-.5.02-1.1.33-1.46.75-.28.32-.55.84-.55 1.35 0 .07.01.15.02.17.04.01.1.02.16.02.45 0 1.02-.3 1.32-.73Z"/></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){return e}function o(e){e?.contentWindow?.postMessage(a({type:`PARENT_ACKNOWLEDGED_IFRAME_READY`}),`*`)}function s({iframe:e,appearance:t={}}){e?.contentWindow?.postMessage(a({type:`UPDATE_APPEARANCE`,appearance:t}),`*`)}function c({iframe:e,amount:t}){e?.contentWindow?.postMessage(a({type:`UPDATE_AMOUNT`,amount:t}),`*`)}function l({iframe:e,merchantName:t}){e?.contentWindow?.postMessage(a({type:`UPDATE_MERCHANT_NAME`,merchantName:t}),`*`)}function u({iframe:e}){let t=crypto.randomUUID();return new Promise(n=>{e?.contentWindow?.postMessage(a({type:`VALIDATE_FORM`,requestId:t}),`*`);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 d({iframe:e,token:t}){let{payment_intent_id:n}=r(t).payload;e?.contentWindow?.postMessage(a({type:`CONFIRM_PAYMENT_INTENT`,token:t,id:n??void 0}),`*`)}function f({iframe:e,token:t}){let{setup_intent_id:n}=r(t).payload;e?.contentWindow?.postMessage(a({type:`CONFIRM_SETUP_INTENT`,token:t,id:n??void 0}),`*`)}function p({iframe:e,result:t}){e?.contentWindow?.postMessage(a({type:`CONFIRMATION_RESULT`,result:t}),`*`)}function m(e){return`${i(e)}/iframe/apple-pay?token=${e}`}function h(){return`40px`}function g(e){e.contentWindow?.postMessage(a({type:`APPLE_PAY_CANCEL`}),`*`)}function _(e,r){let i={...r};function a(){c({iframe:e,amount:i.amount})}function u(){l({iframe:e,merchantName:i.merchantName})}function f(r){if(r.source===e.contentWindow)switch(r.data.type){case`IFRAME_READY`:o(e),s({iframe:e,appearance:i.appearance}),a(),u();break;case`UPDATE_HEIGHT`:i.onHeightChange?.(r.data.height);break;case`UPDATE_APPEARANCE`:s({iframe:e,appearance:r.data.appearance});break;case`UPDATED_APPEARANCE`:i.onAppearanceReady?.();break;case`APPLE_PAY_WINDOW_OPEN`:t({onCancel:()=>{g(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=>{d({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`,f),{update(t){let n=`appearance`in t,r=`amount`in t,o=`merchantName`in t;i={...i,...t},n&&s({iframe:e,appearance:i.appearance}),r&&a(),o&&u()},destroy(){window.removeEventListener(`message`,f),n()}}}function v(e){return`${i(e)}/iframe/google-pay?token=${e}`}function y(){return`40px`}function b(e,t){let n={...t};function r(){c({iframe:e,amount:n.amount})}function i(){l({iframe:e,merchantName:n.merchantName})}function a(t){switch(t.data.type){case`IFRAME_READY`:o(e),s({iframe:e,appearance:n.appearance}),r(),i();break;case`UPDATE_HEIGHT`:n.onHeightChange?.(t.data.height);break;case`UPDATE_APPEARANCE`:s({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=>{d({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`,a),{update(t){let a=`appearance`in t,o=`amount`in t,c=`merchantName`in t;n={...n,...t},a&&s({iframe:e,appearance:n.appearance}),o&&r(),c&&i()},destroy(){window.removeEventListener(`message`,a)}}}function x({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_provider:`googlepay`,wallet_payload:e.paymentMethodData.tokenizationData.token,wallet_last4:e.paymentMethodData.info?.cardDetails,wallet_brand:(()=>{switch(e.paymentMethodData.info?.cardNetwork){case`AMEX`:return`american_express`;case`VISA`:return`visa`;case`MASTERCARD`:return`master`;case`DISCOVER`:return`discover`;default:return}})()}}}}var S={country:212,full:452},C=80,w={country:400,full:640};function T(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 E(e,t=`country`){let n=new URLSearchParams({token:e,billingAddressRequirement:t});return`${i(e)}/iframe/bank?${n}`}function D(e={cardholderName:!1},t=`country`){return`${(S[t]??S.country)+(e.cardholderName?C:0)}px`}function O(e=`country`){return`${w[e]??w.country}px`}function k(e,t){let n={...t};function r(t){switch(t.data.type){case`IFRAME_READY`:o(e),s({iframe:e,appearance:n.appearance});break;case`UPDATE_HEIGHT`:n.onHeightChange?.(t.data.height);break;case`UPDATE_APPEARANCE`:s({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&&s({iframe:e,appearance:n.appearance})},destroy(){window.removeEventListener(`message`,r)}}}function A(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 j={width:`calc(100% + 8px)`,transition:`opacity 150ms ease-in, height 200ms ease-in-out`,margin:`0 -4px`,opacity:`0`,border:`0`};function M({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,j,{height:r}),a}function N(e,t){let n=A(e),{renderToken:r,additionalFields:i={cardholderName:!1},billingAddressRequirement:a=`country`,...o}=t,s=M({src:T(r,i,a),title:`Secure credit card payment method form powered by Amos`,name:`amos-credit-card-payment-method-form`,height:D(i,a)});n.appendChild(s);let c=k(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 P(e,t){let n=A(e),{renderToken:r,billingAddressRequirement:i=`country`,...a}=t,o=M({src:E(r,i),title:`Secure bank account payment method form powered by Amos`,name:`amos-bank-account-payment-method-form`,height:O(i)});n.appendChild(o);let s=k(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 F(e,t){let n=A(e),{renderToken:r,...i}=t,a=M({src:v(r),title:`Secure Google Pay button powered by Amos`,name:`amos-google-pay-button`,height:y(),allow:`payment`});n.appendChild(a);let o=b(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 I(e,t){let n=A(e),{renderToken:r,...i}=t,a=M({src:m(r),title:`Secure Apple Pay button powered by Amos`,name:`amos-apple-pay-button`,height:h(),allow:`payment`});n.appendChild(a);let o=_(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=_,exports.attachGooglePayButtonListeners=b,exports.attachPaymentMethodFormListeners=k,exports.confirmPaymentIntent=d,exports.confirmSetupIntent=f,exports.createMessage=a,exports.decodeJwt=r,exports.formatGooglePayPaymentData=x,exports.getApplePayButtonInitialHeight=h,exports.getApplePayButtonSrc=m,exports.getBankAccountFormInitialHeight=O,exports.getBankAccountFormSrc=E,exports.getCreditCardFormInitialHeight=D,exports.getCreditCardFormSrc=T,exports.getEmbedOrigin=i,exports.getGooglePayButtonInitialHeight=y,exports.getGooglePayButtonSrc=v,exports.mountAmosApplePayButton=I,exports.mountAmosBankAccountPaymentMethodForm=P,exports.mountAmosCreditCardPaymentMethodForm=N,exports.mountAmosGooglePayButton=F,exports.sendConfirmationResult=p,exports.sendParentReadyMessage=o,exports.updateAmount=c,exports.updateAppearance=s,exports.updateMerchantName=l,exports.validateForm=u;
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 14 17" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M13.072 5.846c-.1.08-1.873 1.09-1.873 3.34 0 2.61 2.29 3.53 2.36 3.55-.04.1-.368 1.27-.766 2.52-.35 1.09-.72 2.18-1.28 2.18-.55 0-.73-.36-1.43-.36-.71 0-.93.37-1.48.37-.55 0-.93-.99-1.35-2.01-.5-1.2-.88-2.43-.88-3.85 0-2.26 1.47-3.46 2.91-3.46.57 0 1.11.38 1.49.38.37 0 .98-.45 1.7-.45.28 0 1.27.03 1.97 1.01ZM9.52 3.37c.3-.36.52-.86.52-1.36 0-.07 0-.14-.01-.2-.5.02-1.1.33-1.46.75-.28.32-.55.84-.55 1.35 0 .07.01.15.02.17.04.01.1.02.16.02.45 0 1.02-.3 1.32-.73Z"/></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){return e}function o(e){e?.contentWindow?.postMessage(a({type:`PARENT_ACKNOWLEDGED_IFRAME_READY`}),`*`)}function s({iframe:e,appearance:t={}}){e?.contentWindow?.postMessage(a({type:`UPDATE_APPEARANCE`,appearance:t}),`*`)}function c({iframe:e,amount:t}){e?.contentWindow?.postMessage(a({type:`UPDATE_AMOUNT`,amount:t}),`*`)}function l({iframe:e,merchantName:t}){e?.contentWindow?.postMessage(a({type:`UPDATE_MERCHANT_NAME`,merchantName:t}),`*`)}function u({iframe:e}){let t=crypto.randomUUID();return new Promise(n=>{e?.contentWindow?.postMessage(a({type:`VALIDATE_FORM`,requestId:t}),`*`);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 d({iframe:e,token:t}){let{payment_intent_id:n}=r(t).payload;e?.contentWindow?.postMessage(a({type:`CONFIRM_PAYMENT_INTENT`,token:t,id:n??void 0}),`*`)}function f({iframe:e,token:t}){let{setup_intent_id:n}=r(t).payload;e?.contentWindow?.postMessage(a({type:`CONFIRM_SETUP_INTENT`,token:t,id:n??void 0}),`*`)}function p({iframe:e,result:t}){e?.contentWindow?.postMessage(a({type:`CONFIRMATION_RESULT`,result:t}),`*`)}function m(e){return`${i(e)}/iframe/apple-pay?token=${e}`}function h(){return`40px`}function g(e){e.contentWindow?.postMessage(a({type:`APPLE_PAY_CANCEL`}),`*`)}function _(e,r){let i={...r};function a(){c({iframe:e,amount:i.amount})}function u(){l({iframe:e,merchantName:i.merchantName})}function f(r){if(r.source===e.contentWindow)switch(r.data.type){case`IFRAME_READY`:o(e),s({iframe:e,appearance:i.appearance}),a(),u();break;case`UPDATE_HEIGHT`:i.onHeightChange?.(r.data.height);break;case`UPDATE_APPEARANCE`:s({iframe:e,appearance:r.data.appearance});break;case`UPDATED_APPEARANCE`:i.onAppearanceReady?.();break;case`APPLE_PAY_WINDOW_OPEN`:t({onCancel:()=>{g(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=>{d({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`,f),{update(t){let n=`appearance`in t,r=`amount`in t,o=`merchantName`in t;i={...i,...t},n&&s({iframe:e,appearance:i.appearance}),r&&a(),o&&u()},destroy(){window.removeEventListener(`message`,f),n()}}}function v(e){return`${i(e)}/iframe/google-pay?token=${e}`}function y(){return`40px`}function b(e,t){let n={...t};function r(){c({iframe:e,amount:n.amount})}function i(){l({iframe:e,merchantName:n.merchantName})}function a(t){if(t.source===e.contentWindow)switch(t.data.type){case`IFRAME_READY`:o(e),s({iframe:e,appearance:n.appearance}),r(),i();break;case`UPDATE_HEIGHT`:n.onHeightChange?.(t.data.height);break;case`UPDATE_APPEARANCE`:s({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=>{d({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`,a),{update(t){let a=`appearance`in t,o=`amount`in t,c=`merchantName`in t;n={...n,...t},a&&s({iframe:e,appearance:n.appearance}),o&&r(),c&&i()},destroy(){window.removeEventListener(`message`,a)}}}function x({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 S={country:212,full:452},C=80,w={country:400,full:640};function T(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 E(e,t=`country`){let n=new URLSearchParams({token:e,billingAddressRequirement:t});return`${i(e)}/iframe/bank?${n}`}function D(e={cardholderName:!1},t=`country`){return`${(S[t]??S.country)+(e.cardholderName?C:0)}px`}function O(e=`country`){return`${w[e]??w.country}px`}function k(e,t){let n={...t};function r(t){if(t.source===e.contentWindow)switch(t.data.type){case`IFRAME_READY`:o(e),s({iframe:e,appearance:n.appearance});break;case`UPDATE_HEIGHT`:n.onHeightChange?.(t.data.height);break;case`UPDATE_APPEARANCE`:s({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&&s({iframe:e,appearance:n.appearance})},destroy(){window.removeEventListener(`message`,r)}}}function A(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 j={width:`calc(100% + 8px)`,transition:`opacity 150ms ease-in, height 200ms ease-in-out`,margin:`0 -4px`,opacity:`0`,border:`0`};function M({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,j,{height:r}),a}function N(e,t){let n=A(e),{renderToken:r,additionalFields:i={cardholderName:!1},billingAddressRequirement:a=`country`,...o}=t,s=M({src:T(r,i,a),title:`Secure credit card payment method form powered by Amos`,name:`amos-credit-card-payment-method-form`,height:D(i,a)});n.appendChild(s);let c=k(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 P(e,t){let n=A(e),{renderToken:r,billingAddressRequirement:i=`country`,...a}=t,o=M({src:E(r,i),title:`Secure bank account payment method form powered by Amos`,name:`amos-bank-account-payment-method-form`,height:O(i)});n.appendChild(o);let s=k(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 F(e,t){let n=A(e),{renderToken:r,...i}=t,a=M({src:v(r),title:`Secure Google Pay button powered by Amos`,name:`amos-google-pay-button`,height:y(),allow:`payment`});n.appendChild(a);let o=b(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 I(e,t){let n=A(e),{renderToken:r,...i}=t,a=M({src:m(r),title:`Secure Apple Pay button powered by Amos`,name:`amos-apple-pay-button`,height:h(),allow:`payment`});n.appendChild(a);let o=_(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=_,exports.attachGooglePayButtonListeners=b,exports.attachPaymentMethodFormListeners=k,exports.confirmPaymentIntent=d,exports.confirmSetupIntent=f,exports.createMessage=a,exports.decodeJwt=r,exports.formatGooglePayPaymentData=x,exports.getApplePayButtonInitialHeight=h,exports.getApplePayButtonSrc=m,exports.getBankAccountFormInitialHeight=O,exports.getBankAccountFormSrc=E,exports.getCreditCardFormInitialHeight=D,exports.getCreditCardFormSrc=T,exports.getEmbedOrigin=i,exports.getGooglePayButtonInitialHeight=y,exports.getGooglePayButtonSrc=v,exports.mountAmosApplePayButton=I,exports.mountAmosBankAccountPaymentMethodForm=P,exports.mountAmosCreditCardPaymentMethodForm=N,exports.mountAmosGooglePayButton=F,exports.sendConfirmationResult=p,exports.sendParentReadyMessage=o,exports.updateAmount=c,exports.updateAppearance=s,exports.updateMerchantName=l,exports.validateForm=u;
package/dist/index.mjs CHANGED
@@ -259,7 +259,7 @@ function b(e, t) {
259
259
  });
260
260
  }
261
261
  function a(t) {
262
- switch (t.data.type) {
262
+ if (t.source === e.contentWindow) switch (t.data.type) {
263
263
  case "IFRAME_READY":
264
264
  o(e), s({
265
265
  iframe: e,
@@ -327,20 +327,7 @@ function x({ paymentData: e }) {
327
327
  email: e.email,
328
328
  phone: e.shippingAddress?.phoneNumber
329
329
  },
330
- card_profile_attributes: {
331
- wallet_provider: "googlepay",
332
- wallet_payload: e.paymentMethodData.tokenizationData.token,
333
- wallet_last4: e.paymentMethodData.info?.cardDetails,
334
- wallet_brand: (() => {
335
- switch (e.paymentMethodData.info?.cardNetwork) {
336
- case "AMEX": return "american_express";
337
- case "VISA": return "visa";
338
- case "MASTERCARD": return "master";
339
- case "DISCOVER": return "discover";
340
- default: return;
341
- }
342
- })()
343
- }
330
+ card_profile_attributes: { wallet_payload: e.paymentMethodData.tokenizationData.token }
344
331
  } };
345
332
  }
346
333
  //#endregion
@@ -376,7 +363,7 @@ function O(e = "country") {
376
363
  function k(e, t) {
377
364
  let n = { ...t };
378
365
  function r(t) {
379
- switch (t.data.type) {
366
+ if (t.source === e.contentWindow) switch (t.data.type) {
380
367
  case "IFRAME_READY":
381
368
  o(e), s({
382
369
  iframe: e,
@@ -62,7 +62,9 @@ export type PaymentMethodFormListenerOptions = {
62
62
  /**
63
63
  * Called when the interactive confirmation flow finishes (success or
64
64
  * terminal failure). Not settlement proof — verify via webhooks.
65
- * Recoverable field errors stay in the iframe and do not invoke this.
65
+ * Recoverable validation posts `status: "incomplete"` with a `reason`
66
+ * (`field_errors` or `validation_failed`) instead of invoking this for
67
+ * terminal outcomes only — hosts should unlock UI on `incomplete`.
66
68
  */
67
69
  onResult: (result: ConfirmationResult) => void;
68
70
  };
package/dist/types.d.ts CHANGED
@@ -1,4 +1,15 @@
1
1
  import { components } from '@amos.com/node';
2
+ /**
3
+ * Why an interactive confirmation attempt ended with `status: "incomplete"`.
4
+ *
5
+ * Helps hosts distinguish recoverable iframe states for analytics and
6
+ * support without exposing PCI data.
7
+ */
8
+ export type ConfirmationIncompleteReason =
9
+ /** Pay API field errors were mapped onto iframe inputs for retry. */
10
+ "field_errors"
11
+ /** Client-side / HTML5 validation failed when confirm was requested. */
12
+ | "validation_failed";
2
13
  /**
3
14
  * Outcome of an interactive confirmation flow inside the Amos iframe.
4
15
  *
@@ -8,9 +19,9 @@ import { components } from '@amos.com/node';
8
19
  * retrieving the PaymentIntent / SetupIntent with your secret key),
9
20
  * the same way Stripe recommends.
10
21
  *
11
- * Recoverable field validation errors (e.g. bad expiration year) are
12
- * shown inside the iframe and do **not** produce a result; the customer
13
- * can fix the form and retry.
22
+ * Recoverable validation (field errors shown in the iframe, or client-side
23
+ * validation on confirm) posts `status: "incomplete"` with a `reason` so
24
+ * the host can unlock its UI; the customer can fix the form and retry.
14
25
  */
15
26
  export type ConfirmationResult = {
16
27
  status: "succeeded";
@@ -22,10 +33,12 @@ export type ConfirmationResult = {
22
33
  setupIntent: components["schemas"]["SetupIntent"];
23
34
  } | {
24
35
  /**
25
- * Recoverable validation was shown in the iframe. Unlock the host UI;
26
- * the customer can fix fields and retry. Do not treat as settlement.
36
+ * Recoverable validation was shown in the iframe or confirm was
37
+ * blocked by client-side validation. Unlock the host UI; the customer
38
+ * can fix fields and retry. Do not treat as settlement.
27
39
  */
28
40
  status: "incomplete";
41
+ reason: ConfirmationIncompleteReason;
29
42
  } | {
30
43
  status: "failed";
31
44
  errorMessage: string;
@@ -61,42 +74,57 @@ export type Appearance = {
61
74
  * embedded Amos iframe.
62
75
  */
63
76
  export type Message = {
77
+ /** Embed → parent: iframe finished loading and is ready for messages. */
64
78
  type: "IFRAME_READY";
65
79
  } | {
80
+ /** Parent → embed: host acknowledged `IFRAME_READY`. */
66
81
  type: "PARENT_ACKNOWLEDGED_IFRAME_READY";
67
82
  } | {
83
+ /** Embed → parent: iframe content height changed (resize container). */
68
84
  type: "UPDATE_HEIGHT";
69
85
  height: string;
70
86
  } | {
87
+ /** Parent → embed: express-checkout amount changed. */
71
88
  type: "UPDATE_AMOUNT";
72
89
  amount: string;
73
90
  } | {
91
+ /** Parent → embed: express-checkout merchant display name changed. */
74
92
  type: "UPDATE_MERCHANT_NAME";
75
93
  merchantName: string;
76
94
  } | {
95
+ /** Parent → embed: push appearance overrides into the iframe. */
77
96
  type: "UPDATE_APPEARANCE";
78
97
  appearance: Appearance;
79
98
  } | {
99
+ /**
100
+ * Parent → embed: validate form inputs (`requestId` only).
101
+ * Embed → parent: validation response (`isValid` set).
102
+ */
80
103
  type: "VALIDATE_FORM";
81
104
  requestId: string;
82
105
  isValid?: boolean;
83
106
  } | {
107
+ /** Embed → parent: express checkout requests a payment intent from the host. */
84
108
  type: "CREATE_PAYMENT_INTENT";
85
109
  paymentIntentCreateAttributes: components["schemas"]["CreatePaymentIntentInput"];
86
110
  customerCreateAttributes: components["schemas"]["CreateCustomerInput"];
87
111
  } | ({
112
+ /** Parent → embed: confirm a payment intent with an embed token. */
88
113
  type: "CONFIRM_PAYMENT_INTENT";
89
114
  } & Pick<components["schemas"]["PaymentIntent"], "id"> & Pick<components["schemas"]["EmbedToken"], "token">) | ({
115
+ /** Parent → embed: confirm a setup intent with an embed token. */
90
116
  type: "CONFIRM_SETUP_INTENT";
91
117
  } & Pick<components["schemas"]["SetupIntent"], "id"> & Pick<components["schemas"]["EmbedToken"], "token">) | {
92
118
  /**
93
119
  * Embed → parent: the interactive confirmation flow finished.
120
+ * Parent → embed: express-checkout initiation failed on the host.
94
121
  * Not settlement proof — verify payment/setup success on your
95
122
  * backend via webhooks (or by retrieving the intent).
96
123
  */
97
124
  type: "CONFIRMATION_RESULT";
98
125
  result: ConfirmationResult;
99
126
  } | {
127
+ /** Embed → parent: appearance overrides were applied in the iframe. */
100
128
  type: "UPDATED_APPEARANCE";
101
129
  } | {
102
130
  /** Embed → parent: Apple Pay Code window / waiting UI should show. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amos.com/amos-js",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "main": "dist/index.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,6 +24,7 @@
24
24
  "dev": "vite",
25
25
  "prebuild": "npm run typecheck",
26
26
  "build": "vite build",
27
+ "watch": "vite build --watch",
27
28
  "preview": "vite preview",
28
29
  "typecheck": "tsc --noEmit",
29
30
  "prepack": "npm run build",
@@ -44,7 +45,7 @@
44
45
  "vite-plugin-dts": "5.0.3"
45
46
  },
46
47
  "dependencies": {
47
- "@amos.com/node": "0.1.35",
48
+ "@amos.com/node": "0.1.36",
48
49
  "@types/googlepay": "0.7.11"
49
50
  }
50
51
  }
package/src/google-pay.ts CHANGED
@@ -107,6 +107,10 @@ export function attachGooglePayButtonListeners(
107
107
  }
108
108
 
109
109
  function handleMessage(event: MessageEvent<Message>) {
110
+ if (event.source !== iframe.contentWindow) {
111
+ return;
112
+ }
113
+
110
114
  switch (event.data.type) {
111
115
  case "IFRAME_READY":
112
116
  sendParentReadyMessage(iframe);
@@ -209,23 +213,7 @@ export function formatGooglePayPaymentData({
209
213
  phone: paymentData.shippingAddress?.phoneNumber,
210
214
  },
211
215
  card_profile_attributes: {
212
- wallet_provider: "googlepay",
213
216
  wallet_payload: paymentData.paymentMethodData.tokenizationData.token,
214
- wallet_last4: paymentData.paymentMethodData.info?.cardDetails,
215
- wallet_brand: (() => {
216
- switch (paymentData.paymentMethodData.info?.cardNetwork) {
217
- case "AMEX":
218
- return "american_express";
219
- case "VISA":
220
- return "visa";
221
- case "MASTERCARD":
222
- return "master";
223
- case "DISCOVER":
224
- return "discover";
225
- default:
226
- return undefined;
227
- }
228
- })(),
229
217
  },
230
218
  },
231
219
  };
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ export {
65
65
  export type {
66
66
  Appearance,
67
67
  AppearanceLabels,
68
+ ConfirmationIncompleteReason,
68
69
  ConfirmationResult,
69
70
  Message,
70
71
  ThemeVariable,
@@ -129,7 +129,9 @@ export type PaymentMethodFormListenerOptions = {
129
129
  /**
130
130
  * Called when the interactive confirmation flow finishes (success or
131
131
  * terminal failure). Not settlement proof — verify via webhooks.
132
- * Recoverable field errors stay in the iframe and do not invoke this.
132
+ * Recoverable validation posts `status: "incomplete"` with a `reason`
133
+ * (`field_errors` or `validation_failed`) instead of invoking this for
134
+ * terminal outcomes only — hosts should unlock UI on `incomplete`.
133
135
  */
134
136
  onResult: (result: ConfirmationResult) => void;
135
137
  };
@@ -167,6 +169,10 @@ export function attachPaymentMethodFormListeners(
167
169
  let current = { ...options };
168
170
 
169
171
  function handleMessage(event: MessageEvent<Message>) {
172
+ if (event.source !== iframe.contentWindow) {
173
+ return;
174
+ }
175
+
170
176
  switch (event.data.type) {
171
177
  case "IFRAME_READY":
172
178
  sendParentReadyMessage(iframe);
package/src/types.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  import type { components } from "@amos.com/node";
2
2
 
3
+ /**
4
+ * Why an interactive confirmation attempt ended with `status: "incomplete"`.
5
+ *
6
+ * Helps hosts distinguish recoverable iframe states for analytics and
7
+ * support without exposing PCI data.
8
+ */
9
+ export type ConfirmationIncompleteReason =
10
+ /** Pay API field errors were mapped onto iframe inputs for retry. */
11
+ | "field_errors"
12
+ /** Client-side / HTML5 validation failed when confirm was requested. */
13
+ | "validation_failed";
14
+
3
15
  /**
4
16
  * Outcome of an interactive confirmation flow inside the Amos iframe.
5
17
  *
@@ -9,9 +21,9 @@ import type { components } from "@amos.com/node";
9
21
  * retrieving the PaymentIntent / SetupIntent with your secret key),
10
22
  * the same way Stripe recommends.
11
23
  *
12
- * Recoverable field validation errors (e.g. bad expiration year) are
13
- * shown inside the iframe and do **not** produce a result; the customer
14
- * can fix the form and retry.
24
+ * Recoverable validation (field errors shown in the iframe, or client-side
25
+ * validation on confirm) posts `status: "incomplete"` with a `reason` so
26
+ * the host can unlock its UI; the customer can fix the form and retry.
15
27
  */
16
28
  export type ConfirmationResult =
17
29
  | {
@@ -26,10 +38,12 @@ export type ConfirmationResult =
26
38
  }
27
39
  | {
28
40
  /**
29
- * Recoverable validation was shown in the iframe. Unlock the host UI;
30
- * the customer can fix fields and retry. Do not treat as settlement.
41
+ * Recoverable validation was shown in the iframe or confirm was
42
+ * blocked by client-side validation. Unlock the host UI; the customer
43
+ * can fix fields and retry. Do not treat as settlement.
31
44
  */
32
45
  status: "incomplete";
46
+ reason: ConfirmationIncompleteReason;
33
47
  }
34
48
  | {
35
49
  status: "failed";
@@ -174,48 +188,62 @@ export type Appearance = {
174
188
  */
175
189
  export type Message =
176
190
  | {
191
+ /** Embed → parent: iframe finished loading and is ready for messages. */
177
192
  type: "IFRAME_READY";
178
193
  }
179
194
  | {
195
+ /** Parent → embed: host acknowledged `IFRAME_READY`. */
180
196
  type: "PARENT_ACKNOWLEDGED_IFRAME_READY";
181
197
  }
182
198
  | {
199
+ /** Embed → parent: iframe content height changed (resize container). */
183
200
  type: "UPDATE_HEIGHT";
184
201
  height: string;
185
202
  }
186
203
  | {
204
+ /** Parent → embed: express-checkout amount changed. */
187
205
  type: "UPDATE_AMOUNT";
188
206
  amount: string;
189
207
  }
190
208
  | {
209
+ /** Parent → embed: express-checkout merchant display name changed. */
191
210
  type: "UPDATE_MERCHANT_NAME";
192
211
  merchantName: string;
193
212
  }
194
213
  | {
214
+ /** Parent → embed: push appearance overrides into the iframe. */
195
215
  type: "UPDATE_APPEARANCE";
196
216
  appearance: Appearance;
197
217
  }
198
218
  | {
219
+ /**
220
+ * Parent → embed: validate form inputs (`requestId` only).
221
+ * Embed → parent: validation response (`isValid` set).
222
+ */
199
223
  type: "VALIDATE_FORM";
200
224
  requestId: string;
201
225
  isValid?: boolean;
202
226
  }
203
227
  | {
228
+ /** Embed → parent: express checkout requests a payment intent from the host. */
204
229
  type: "CREATE_PAYMENT_INTENT";
205
230
  paymentIntentCreateAttributes: components["schemas"]["CreatePaymentIntentInput"];
206
231
  customerCreateAttributes: components["schemas"]["CreateCustomerInput"];
207
232
  }
208
233
  | ({
234
+ /** Parent → embed: confirm a payment intent with an embed token. */
209
235
  type: "CONFIRM_PAYMENT_INTENT";
210
236
  } & Pick<components["schemas"]["PaymentIntent"], "id"> &
211
237
  Pick<components["schemas"]["EmbedToken"], "token">)
212
238
  | ({
239
+ /** Parent → embed: confirm a setup intent with an embed token. */
213
240
  type: "CONFIRM_SETUP_INTENT";
214
241
  } & Pick<components["schemas"]["SetupIntent"], "id"> &
215
242
  Pick<components["schemas"]["EmbedToken"], "token">)
216
243
  | {
217
244
  /**
218
245
  * Embed → parent: the interactive confirmation flow finished.
246
+ * Parent → embed: express-checkout initiation failed on the host.
219
247
  * Not settlement proof — verify payment/setup success on your
220
248
  * backend via webhooks (or by retrieving the intent).
221
249
  */
@@ -223,6 +251,7 @@ export type Message =
223
251
  result: ConfirmationResult;
224
252
  }
225
253
  | {
254
+ /** Embed → parent: appearance overrides were applied in the iframe. */
226
255
  type: "UPDATED_APPEARANCE";
227
256
  }
228
257
  | {