@maytes/checkout-button 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,37 +1,91 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#11](https://github.com/kttipay/maytes-checkout-button/pull/11) [`def33e0`](https://github.com/kttipay/maytes-checkout-button/commit/def33e010bf23b11bc94871e236d35a65deb825e) Thanks [@kos-maytes](https://github.com/kos-maytes)! - `renderButton`'s default `mode` is now `'popup'` instead of `'redirect'`. Pass `mode: 'redirect'` explicitly to keep the previous same-window navigation behavior. Popup mode already falls back to same-window redirect on mobile viewports and when the popup is blocked, so this only changes the desktop no-`mode`-specified path.
8
+
9
+ - [#8](https://github.com/kttipay/maytes-checkout-button/pull/8) [`a6d85e2`](https://github.com/kttipay/maytes-checkout-button/commit/a6d85e23653e340b7a850ad61e882ec2f6cef5d2) Thanks [@kttipaydiogocavaiar](https://github.com/kttipaydiogocavaiar)! - The button never loads the hosted checkout inside an iframe any more. When the button is rendered inside a frame (for example a booking widget embedded on the merchant's page), a phone or a narrow frame used to take the same-window fallback and navigate the frame itself, so the checkout ran as a cross-site iframe where its session cookie is rejected and sign-in ended on "Your session expired". The button now measures the top-level window (or the screen when the top window is cross-origin) and navigates the top-level window; if the browser refuses, it opens the checkout in a new tab; if both are refused, it dispatches `maytes:checkout-failed` with `reason: 'navigation-blocked'` and leaves the button usable. `maytes:checkout-redirected` now carries `detail.target` (`'self' | 'top' | 'tab'`). `redirectToCheckout()` follows the same rule and throws `MaytesError` when it cannot leave the frame. Merchants who deliberately relied on in-frame navigation will see the top-level page change instead; render the button in the top-level page if that is not wanted.
10
+
11
+ <!-- @hash-sri-start -->
12
+
13
+ **SRI hashes** (use these in `<script integrity="..." crossorigin="anonymous">`):
14
+
15
+ ```
16
+ checkout-button.js sha384-+hQ9khxZbVEUhR685659hqmPyN/3RYfbNEI7itac3SkcyYor/94/0mIwVnVwMa8M
17
+ checkout-button.mjs sha384-1areqNcTs9sephTl6S1+sOgACv1/NqIqXBG0Ej11/4OfSDcN92GvQvhhu4K76R5i
18
+ checkout-button.cjs sha384-4c1JuweT47cnpvGlQfv+qQw/3CS+7o2/Dg4qQnLiWM1GDChhHQo0dT+z8705Hy50
19
+ ```
20
+
21
+ **SemVer CDN links** (readable production pins; use with the SRI hashes above):
22
+
23
+ - [checkout-button.js](https://js.maytes.co/v1.1.0/checkout-button.js)
24
+ - [checkout-button.mjs](https://js.maytes.co/v1.1.0/checkout-button.mjs)
25
+ - [checkout-button.cjs](https://js.maytes.co/v1.1.0/checkout-button.cjs)
26
+
27
+ **Hashed CDN links** (byte-level production pins; use with the SRI hashes above):
28
+
29
+ - [checkout-button.811263f9.js](https://js.maytes.co/checkout-button.811263f9.js)
30
+ - [checkout-button.f96c49ee.mjs](https://js.maytes.co/checkout-button.f96c49ee.mjs)
31
+ - [checkout-button.178d8394.cjs](https://js.maytes.co/checkout-button.178d8394.cjs)
32
+
33
+ <!-- @hash-sri-end -->
34
+ ## 1.0.1
35
+
36
+ ### Patch Changes
37
+
38
+ - [#2](https://github.com/kttipay/maytes-checkout-button/pull/2) [`e5b8e63`](https://github.com/kttipay/maytes-checkout-button/commit/e5b8e6399dd33e0b3b24131d9f808ebd8eae44ef) Thanks [@kos-maytes](https://github.com/kos-maytes)! - `destroy()` no longer force-closes a popup that has already navigated to the checkout URL. Previously it would close any open popup unconditionally, which could kill an in-progress checkout if the host page called `destroy()` while the popup was still completing (e.g. reacting to its own payment-status polling). A popup is now only auto-closed while it's still on the blank loading screen, before checkout has started.
39
+
40
+ <!-- @hash-sri-start -->
41
+
42
+ **SRI hashes** (use these in `<script integrity="..." crossorigin="anonymous">`):
43
+
44
+ ```
45
+ checkout-button.js sha384-scp0qo7rcOJGuuI+gpadCOUQMn7g7hx3lqY9p7Xr2fd7/9Sb/L79DaHKStTTX7Ko
46
+ checkout-button.mjs sha384-5+TplHKGAk10ajWGdKwpG3pKutLD+v3cOYqtPEvRNLWudyOELPUKuU6sqlAhuy1h
47
+ checkout-button.cjs sha384-+O2S6KfO4Pc0VEvL+kTsFzxU14W4EILV6+U5mAdW8X43Xm8Qk1aV0y/KbM3Fnp9M
48
+ ```
49
+
50
+ **SemVer CDN links** (readable production pins; use with the SRI hashes above):
51
+
52
+ - [checkout-button.js](https://js.maytes.co/v1.0.1/checkout-button.js)
53
+ - [checkout-button.mjs](https://js.maytes.co/v1.0.1/checkout-button.mjs)
54
+ - [checkout-button.cjs](https://js.maytes.co/v1.0.1/checkout-button.cjs)
55
+
56
+ **Hashed CDN links** (byte-level production pins; use with the SRI hashes above):
57
+
58
+ - [checkout-button.8c71493e.js](https://js.maytes.co/checkout-button.8c71493e.js)
59
+ - [checkout-button.81ea2037.mjs](https://js.maytes.co/checkout-button.81ea2037.mjs)
60
+ - [checkout-button.6d259930.cjs](https://js.maytes.co/checkout-button.6d259930.cjs)
61
+
62
+ <!-- @hash-sri-end -->
63
+
3
64
  ## 1.0.0
4
65
 
5
- ### Major Changes
66
+ First public release on npm.
6
67
 
7
- - [`c17857f`](https://github.com/kttipay/checkout-button/commit/c17857fcb38bf045ae89fde207a1859ae12214e3) Thanks [@Merkost](https://github.com/Merkost)! - First public release on npm.
68
+ ### Major Changes
8
69
 
9
- The button is now installable as `@maytes/checkout-button` alongside the existing
10
- CDN `<script>` distribution — same package, same API, both fed from this source.
70
+ - **`@maytes/checkout-button` is now installable via npm**, alongside the existing CDN `<script>` distribution — same package, same API, both built from this repo.
11
71
 
12
- Framework apps (React, Angular, Vue, Svelte, plain bundled JS) should import the
13
- factory directly:
72
+ Framework apps (React, Angular, Vue, Svelte, plain bundled JS) should import the factory directly:
14
73
 
15
74
  ```ts
16
75
  import { Maytes } from "@maytes/checkout-button";
17
76
  ```
18
77
 
19
- That removes the script-injection dance those apps previously needed: no
20
- `window.Maytes` global to feature-detect, no polling for the CDN script to
21
- finish loading before mounting, and no hand-written `declare global` typings —
22
- the package ships its own `.d.ts`.
78
+ That removes the script-injection dance those apps previously needed: no `window.Maytes` global to feature-detect, no polling for the CDN script to finish loading before mounting, and no hand-written `declare global` typings — the package ships its own `.d.ts`.
23
79
 
24
- The `<script>` tag remains the right integration for no-build sites and jQuery,
25
- and is unchanged: pin a SemVer or content-hash URL with SRI in every environment,
26
- development included.
80
+ The `<script>` tag remains the right integration for no-build sites and jQuery, and is unchanged: pin a SemVer or content-hash URL with SRI in every environment, development included.
27
81
 
28
82
  ### Minor Changes
29
83
 
30
- - [`c17857f`](https://github.com/kttipay/checkout-button/commit/c17857fcb38bf045ae89fde207a1859ae12214e3) Thanks [@Merkost](https://github.com/Merkost)! - Brand the checkout button with the Maytes wordmark logo and maroon brand color, and make its size auto-scale from a single `--maytes-button-font-size` custom property (default 14px).
84
+ - Branded the checkout button with the Maytes wordmark logo and maroon brand colour, and made its size auto-scale from a single `--maytes-button-font-size` custom property (default 14px).
31
85
 
32
- - [`c17857f`](https://github.com/kttipay/checkout-button/commit/c17857fcb38bf045ae89fde207a1859ae12214e3) Thanks [@Merkost](https://github.com/Merkost)! - Add a launch `mode` to `renderButton` and a branded popup loading screen.
86
+ - Added a launch `mode` to `renderButton` and a branded popup loading screen.
33
87
 
34
- - `renderButton(container, { mode })` accepts `'redirect'` (new default) or `'popup'`. Invalid values throw `MaytesError(CONFIG)`.
88
+ - `renderButton(container, { mode })` accepts `'redirect'` (default) or `'popup'`. Invalid values throw `MaytesError(CONFIG)`.
35
89
  - `mode: 'popup'` opens a blank popup synchronously on click (preserving the user gesture so browsers do not block it), paints a branded Maytes loading screen into it (logo, spinner, `role="status"`/`aria-live`, a slow-network message that appears after ~8s, and a `prefers-reduced-motion` guard), then `popup.location.replace(url)`s once `createCheckout` resolves.
36
90
  - On mobile viewports `mode: 'popup'` behaves as a same-window redirect.
37
91
  - The misleading `maytes:checkout-failed` (`reason: 'popup-blocked'`) is replaced by a new `maytes:checkout-redirected` event (`detail.url`). A same-window redirect is a normal outcome, not a failure. The `failed` event now only carries `'create-checkout-rejected'` | `'invalid-shape'`.
@@ -40,21 +94,21 @@
40
94
 
41
95
  ### Patch Changes
42
96
 
43
- - [`c17857f`](https://github.com/kttipay/checkout-button/commit/c17857fcb38bf045ae89fde207a1859ae12214e3) Thanks [@Merkost](https://github.com/Merkost)! - Harden the checkout button popup lifecycle:
44
-
45
- - Open the hosted checkout URL exactly once (it was navigated twice — `window.open(url)` plus a redundant `location.href` write).
46
- - Give each SDK instance a unique popup window name so concurrent merchant tabs no longer share or steal one popup; this also removes the cross-origin `SecurityError` that could hang the overlay.
47
- - `destroy()` now detaches every button's click listener and is ignored by an in-flight `createCheckout` that resolves after teardown (no late popup).
48
- - Remove the cloned overlay `<style>` from the top document on teardown (same-origin iframe embeds).
49
- - Scope the injected style marker and ref-count by SDK version so two SDK versions on one page don't clash.
50
- - Clear the overlay immediately when the shopper presses Escape (was delayed up to one poll interval).
51
- - Validate that a merchant-supplied `checkoutUrl` is an `http(s)` URL before opening it — matching the protocol guard already applied to the constructed URL, so a buggy backend can't turn the button into a `javascript:`/`data:` popup.
97
+ **Popup lifecycle**
52
98
 
53
- - [`c17857f`](https://github.com/kttipay/checkout-button/commit/c17857fcb38bf045ae89fde207a1859ae12214e3) Thanks [@Merkost](https://github.com/Merkost)! - Take the Maytes brand colours from the design foundation (`kttipay/designsystem` v0.3.0, vendored by `npm run sync:foundation` and pinned in `foundation.lock.json`) instead of hand-written hexes. The only rendered change: the button's hover shade is now the foundation's burgundy/700 `#3B021D` (was `#3A021C`).
99
+ - Open the hosted checkout URL exactly once (it was navigated twice — `window.open(url)` plus a redundant `location.href` write).
100
+ - Give each SDK instance a unique popup window name so concurrent merchant tabs no longer share or steal one popup; this also removes the cross-origin `SecurityError` that could hang the overlay.
101
+ - `destroy()` now detaches every button's click listener and is ignored by an in-flight `createCheckout` that resolves after teardown (no late popup).
102
+ - Remove the cloned overlay `<style>` from the top document on teardown (same-origin iframe embeds).
103
+ - Scope the injected style marker and ref-count by SDK version so two SDK versions on one page don't clash.
104
+ - Clear the overlay immediately when the shopper presses Escape (was delayed up to one poll interval).
105
+ - Validate that a merchant-supplied `checkoutUrl` is an `http(s)` URL before opening it — matching the protocol guard already applied to the constructed URL, so a buggy backend can't turn the button into a `javascript:`/`data:` popup.
106
+ - Fixed a race in `mode: 'popup'`: if the shopper closed the popup while `createCheckout` was still in flight, the SDK would try to navigate (or, on rejection, report a failure for) a window that was already gone once the promise settled — on top of the `maytes:checkout-closed` event the close-poll had already fired. Both the success and rejection paths now check whether the popup is still open before acting, so closing the popup early produces exactly one `checkout-closed` event and nothing else.
54
107
 
55
- - [`c17857f`](https://github.com/kttipay/checkout-button/commit/c17857fcb38bf045ae89fde207a1859ae12214e3) Thanks [@Merkost](https://github.com/Merkost)! - Serve the CDN (IIFE) bundle minified, with a sourcemap. The ESM/CJS npm bundles stay unminified so the consumer's bundler can tree-shake them.
108
+ **Design & build**
56
109
 
57
- - [`c17857f`](https://github.com/kttipay/checkout-button/commit/c17857fcb38bf045ae89fde207a1859ae12214e3) Thanks [@Merkost](https://github.com/Merkost)! - Fix a race in `mode: 'popup'`: if the shopper closed the popup while `createCheckout` was still in flight, the SDK would try to navigate (or, on rejection, report a failure for) a window that was already gone once the promise settled — on top of the `maytes:checkout-closed` event the close-poll had already fired. Both the success and rejection paths now check whether the popup is still open before acting, so closing the popup early produces exactly one `checkout-closed` event and nothing else.
110
+ - Brand colours now come from the design foundation (`kttipay/designsystem` v0.3.0, vendored by `npm run sync:foundation` and pinned in `foundation.lock.json`) instead of hand-written hexes. The only rendered change: the button's hover shade is now the foundation's burgundy/700 `#3B021D` (was `#3A021C`).
111
+ - The CDN (IIFE) bundle now ships minified, with a sourcemap. The ESM/CJS npm bundles stay unminified so the consumer's bundler can tree-shake them.
58
112
 
59
113
  This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
60
114
 
package/README.md CHANGED
@@ -1,28 +1,85 @@
1
- # @maytes/checkout-button
1
+ <p align="center">
2
+ <img src=".github/logo.svg" height="32" alt="Maytes">
3
+ </p>
2
4
 
3
- Drop-in **Split with Maytes** button. Renders a button and launches the Maytes-hosted checkout. Default mode is same-window redirect; `mode: 'popup'` opts desktop browsers into a centered popup with a branded loader and redirect fallback on mobile or when blocked.
5
+ <h1 align="center">@maytes/checkout-button</h1>
6
+
7
+ <p align="center">
8
+ Drop-in <strong>Split with Maytes</strong> button — one script or import, and shoppers can split any checkout with friends.
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@maytes/checkout-button"><img alt="npm version" src="https://img.shields.io/npm/v/@maytes/checkout-button?color=4A0324&label=npm"></a>
13
+ <a href="https://bundlephobia.com/package/@maytes/checkout-button"><img alt="bundle size" src="https://img.shields.io/bundlephobia/minzip/@maytes/checkout-button?color=FE572A&label=gzip"></a>
14
+ <a href="./CHANGELOG.md"><img alt="provenance" src="https://img.shields.io/badge/npm-provenance-4A0324"></a>
15
+ <a href="./LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-4A0324"></a>
16
+ <a href="https://developers.maytes.co/checkout-button"><img alt="docs" src="https://img.shields.io/badge/docs-developers.maytes.co-FE572A"></a>
17
+ </p>
18
+
19
+ ---
20
+
21
+ Renders a button and launches the Maytes-hosted checkout in the top-level window. Default mode opens a centered popup on wide viewports, with a branded loader and automatic redirect fallback on phones or when the popup is blocked; `mode: 'redirect'` opts into a redirect always. Inside an iframe every redirect targets the top-level window.
22
+
23
+ **📖 Full integration guide:** [developers.maytes.co/checkout-button](https://developers.maytes.co/checkout-button)
24
+
25
+ - **Framework-agnostic** — same factory whether it's a `<script>` tag or `import { Maytes }`; no React/Vue/Angular binding to keep in sync.
26
+ - **Zero runtime dependencies** — a small, dependency-free bundle; ESM/CJS ship unminified so your bundler can tree-shake it.
27
+ - **CSP-safe by construction** — no `eval`, `Function`, or string-form timers; every release is scanned for it before shipping.
28
+ - **Evergreen CDN by default** — the recommended `<script>` tag always serves the newest release; pin a SemVer or content-hash URL instead if you'd rather freeze on a tested build.
29
+ - **Signed provenance** — every npm release carries a [SLSA](https://slsa.dev) provenance attestation back to this repo's build.
30
+
31
+ ## Contents
32
+
33
+ - [Install](#install)
34
+ - [Usage](#usage)
35
+ - [API](#api)
36
+ - [Mobile app](#mobile-app)
37
+ - [Development](#development)
4
38
 
5
39
  ## Install
6
40
 
7
41
  ### Script tag (CDN)
8
42
 
9
- For production, pin an exact release. The readable SemVer URL and the content-hash URL are two names for the **same release bytes** and share the **same SRI value** — pick either:
43
+ | | Evergreen (recommended) | Pinned |
44
+ |---|---|---|
45
+ | **Use when** | Default — always get the newest `1.x` release | You'd rather freeze on a tested build |
46
+ | **Guarantee** | Always current; a bad release reaches you immediately | The bytes you tested are the bytes shipped, forever |
47
+ | **SRI** | Not possible | Yes |
48
+
49
+ ### Evergreen (recommended)
10
50
 
11
51
  ```html
12
- <script src="https://js.maytes.co/v1.0.0/checkout-button.js"
13
- integrity="sha384-…"
52
+ <script src="https://js.maytes.co/v1/checkout-button.js"
14
53
  crossorigin="anonymous"></script>
15
54
  ```
16
55
 
56
+ No `integrity` attribute is possible here — the bytes change without notice as new `1.x` releases ship. Trust the origin via CSP instead:
57
+
58
+ ```
59
+ Content-Security-Policy: script-src 'self' https://js.maytes.co;
60
+ ```
61
+
62
+ `/v1/` tracks the newest `1.x` release only; it won't jump to a future `2.x` (move to `/v2/checkout-button.js` explicitly once that ships). Poll `https://js.maytes.co/integrity.json` if you want to detect changes yourself.
63
+
64
+ ### Pinned (SemVer or content hash)
65
+
66
+ For a guarantee instead of convenience — freeze on a tested build, update on your own schedule:
67
+
68
+ <!-- @cdn-example-start -->
17
69
  ```html
18
- <script src="https://js.maytes.co/checkout-button.795d508b.js"
70
+ <script src="https://js.maytes.co/v1.1.0/checkout-button.js"
19
71
  integrity="sha384-…"
20
72
  crossorigin="anonymous"></script>
21
73
  ```
22
74
 
23
- `v1.0.0` / `795d508b` are examples. The real version, hash, and SRI for each release live in [`CHANGELOG.md`](./CHANGELOG.md), the matching [release](https://github.com/kttipay/checkout-button/releases), and `https://js.maytes.co/integrity.json`.
75
+ ```html
76
+ <script src="https://js.maytes.co/checkout-button.811263f9.js"
77
+ integrity="sha384-…"
78
+ crossorigin="anonymous"></script>
79
+ ```
80
+ <!-- @cdn-example-end -->
24
81
 
25
- Use a pinned URL in every environment, development included — pinning is what makes the bytes you tested the bytes your shoppers get.
82
+ Both URLs are the same release bytes with the same SRI value — pick either. Full version/hash/SRI history: [`CHANGELOG.md`](./CHANGELOG.md), [releases](https://github.com/kttipay/maytes-checkout-button/releases), `https://js.maytes.co/integrity.json`. Pinned URLs never change once published.
26
83
 
27
84
  ### npm
28
85
 
@@ -47,8 +104,8 @@ const maytes = Maytes({
47
104
 
48
105
  const cleanup = maytes.renderButton(document.getElementById('slot'), { block: true });
49
106
 
50
- // Optional popup mode:
51
- maytes.renderButton(document.getElementById('popup-slot'), { block: true, mode: 'popup' });
107
+ // Opt into same-window redirect instead of the default popup:
108
+ maytes.renderButton(document.getElementById('redirect-slot'), { block: true, mode: 'redirect' });
52
109
 
53
110
  // Imperative alternatives:
54
111
  maytes.redirectToCheckout({ checkoutId });
@@ -66,10 +123,14 @@ Via the script tag, the same factory is available as the global `window.Maytes(.
66
123
  | Method | Purpose |
67
124
  |---|---|
68
125
  | `Maytes(options)` | Create an SDK instance. `options.createCheckout` mints a checkout server-side; `options.environment` selects the Maytes environment. |
69
- | `renderButton(container, options?)` | Render the button into `container`; returns a cleanup function. Options include `label`, `block`, and `mode: 'redirect' \| 'popup'`. |
126
+ | `renderButton(container, options?)` | Render the button into `container`; returns a cleanup function. Options include `label`, `block`, and `mode: 'redirect' \| 'popup'` (default `'popup'`). |
70
127
  | `redirectToCheckout(options)` | Launch checkout directly (no button). |
71
128
  | `checkoutUrl(options)` | Build the hosted checkout URL. |
72
- | `destroy()` | Tear down the instance and its listeners. |
129
+ | `destroy()` | Tear down the instance and its listeners — call this on unmount or when the instance's config/environment changes, not as a reaction to detecting payment success from your own polling (that can tear down a checkout that's still in progress). To remove a single button, use the cleanup function returned by `renderButton()` or hide/disable the button element instead. |
130
+
131
+ ### Inside an iframe?
132
+
133
+ The hosted checkout must run in the top-level window (its session cookie is refused inside a cross-site frame). If you render the button inside an iframe, the SDK navigates the top-level window; if the browser refuses, it opens a new tab; if both are refused it dispatches `maytes:checkout-failed` with `reason: 'navigation-blocked'`. When the checkout opens in a new tab, no popup poll starts and no `maytes:checkout-opened` / `maytes:checkout-closed` pair fires — you'll only see `maytes:checkout-redirected` with `target: 'tab'`. A sandboxed iframe needs `allow-scripts allow-same-origin allow-top-navigation` (plus `allow-popups allow-popups-to-escape-sandbox` for the tab fallback). Listen to `maytes:checkout-redirected` and read `event.detail.target` (`'self'`, `'top'` or `'tab'`) if your page needs to know where the checkout went. Rendering the button in the top-level page avoids all of this.
73
134
 
74
135
  ## Mobile app
75
136
 
@@ -85,22 +146,8 @@ npm run test:run
85
146
  npm run build # bundles + SRI hashes + CSP scan
86
147
  ```
87
148
 
88
- Versioning and changelog are managed with [Changesets](./.changeset/README.md). Run `npm run changeset` with your PR.
89
-
90
- ## Brand colours come from the design foundation
91
-
92
- The Maytes brand colours the button paints (`brand.primary` burgundy, `brand.secondary` orange, the burgundy/700 hover shade, and the inverse text white) are **not hand-written** in this package. They are vendored from the design foundation — `kttipay/designsystem` `web/tokens.ts` — into `src/foundation/brand.generated.ts`, pinned to a release tag recorded in `foundation.lock.json`:
93
-
94
- ```json
95
- { "repo": "kttipay/designsystem", "tag": "v0.3.0", "source": "web/tokens.ts" }
96
- ```
97
-
98
- - `src/branding.ts`, `src/styles.ts` and `src/button.ts` read every brand colour from that generated module. Never type a Maytes hex into `src/`.
99
- - To move to a newer foundation release: `npm run sync:foundation -- --tag vX.Y.Z` (fetches the file at that tag with `gh api`, so you need a logged-in `gh`; `--from ../path/to/designsystem/web/tokens.ts` works offline). The script rewrites the generated module and the lock file. Commit both together.
100
- - A vitest test fails if the generated header's tag drifts from `foundation.lock.json`, so the file can only change through the script.
101
-
102
- The design-system repo does not publish an npm package yet. When it does (`@kttipay/foundation` on GitHub Packages), the upgrade path is to replace the vendored module with that dependency and delete the script and lock file.
149
+ Versioning and changelog are managed with [Changesets](./.changeset/README.md). Run `npm run changeset` with your PR. Brand colours are vendored, not hand-written — see [`RELEASING.md`](./RELEASING.md#brand-colours-come-from-the-design-foundation) for how that works.
103
150
 
104
151
  ## License
105
152
 
106
- MIT
153
+ [MIT](./LICENSE)
@@ -86,21 +86,79 @@ var MaytesError = class extends Error {
86
86
  }
87
87
  };
88
88
 
89
+ // src/framing.ts
90
+ function isSecurityError(err) {
91
+ return typeof err === "object" && err !== null && err.name === "SecurityError";
92
+ }
93
+ function isFramed() {
94
+ return window.top !== null && window.top !== window;
95
+ }
96
+ function sameOriginTop() {
97
+ if (!isFramed()) return null;
98
+ const top = window.top;
99
+ try {
100
+ void top.document;
101
+ return top;
102
+ } catch (err) {
103
+ if (!isSecurityError(err)) throw err;
104
+ return null;
105
+ }
106
+ }
107
+ function viewportWidth() {
108
+ if (!isFramed()) return window.innerWidth;
109
+ const top = sameOriginTop();
110
+ if (top !== null) return top.innerWidth;
111
+ const screenWidth = window.screen?.width;
112
+ return typeof screenWidth === "number" && screenWidth > 0 ? screenWidth : window.innerWidth;
113
+ }
114
+ function navigateWindow(target, url, replace) {
115
+ if (replace) {
116
+ target.location.replace(url);
117
+ } else {
118
+ target.location.href = url;
119
+ }
120
+ }
121
+ function severOpener(tab) {
122
+ try {
123
+ tab.opener = null;
124
+ } catch (err) {
125
+ if (!isSecurityError(err)) throw err;
126
+ }
127
+ }
128
+ function navigateTopLevel(url, replace) {
129
+ if (!isFramed()) {
130
+ navigateWindow(window, url, replace);
131
+ return { target: "self" };
132
+ }
133
+ let refusal;
134
+ try {
135
+ navigateWindow(window.top, url, replace);
136
+ return { target: "top" };
137
+ } catch (err) {
138
+ if (!isSecurityError(err)) throw err;
139
+ refusal = err;
140
+ }
141
+ try {
142
+ const tab = window.open(url, "_blank");
143
+ if (tab !== null) {
144
+ severOpener(tab);
145
+ return { target: "tab" };
146
+ }
147
+ return { target: null, cause: refusal };
148
+ } catch (err) {
149
+ return { target: null, cause: err };
150
+ }
151
+ }
152
+
89
153
  // src/overlay.ts
90
154
  var TITLE_ID = "maytes-overlay-title";
91
155
  var STYLE_MARKER = "data-maytes-checkout-button-overlay-styles";
92
156
  var NATIVE_BACKDROP_FALLBACK = "rgba(0, 0, 0, 0.6)";
93
157
  function tryReadTopDocument() {
94
- if (window.top === null || window.top === window) return null;
95
- try {
96
- const topDoc = window.top.document;
97
- return topDoc.body !== null ? topDoc : null;
98
- } catch (err) {
99
- if (!(err instanceof DOMException && err.name === "SecurityError")) {
100
- console.warn("[maytes/checkout-button] unexpected error resolving overlay host", err);
101
- }
102
- return null;
103
- }
158
+ const top = sameOriginTop();
159
+ if (top === null) return null;
160
+ const topDoc = top.document;
161
+ return topDoc.body !== null ? topDoc : null;
104
162
  }
105
163
  function resolveOverlayHost() {
106
164
  return tryReadTopDocument() ?? document;
@@ -148,7 +206,7 @@ function buildOverlay(host, state) {
148
206
  try {
149
207
  state.popupWindow.focus();
150
208
  } catch (err) {
151
- if (!(err instanceof DOMException && err.name === "SecurityError")) throw err;
209
+ if (!isSecurityError(err)) throw err;
152
210
  }
153
211
  }
154
212
  });
@@ -258,10 +316,12 @@ function performRedirect(url, replace) {
258
316
  "redirectToCheckout requires a browser context (window is undefined)"
259
317
  );
260
318
  }
261
- if (replace) {
262
- window.location.replace(url);
263
- } else {
264
- window.location.href = url;
319
+ const outcome = navigateTopLevel(url, replace);
320
+ if (outcome.target === null) {
321
+ throw new MaytesError(
322
+ MaytesErrorCode.Config,
323
+ "redirectToCheckout could not leave the embedding iframe; call it from a user gesture or from the top-level page"
324
+ );
265
325
  }
266
326
  }
267
327
  function makeRedirect(environment, baseUrlOverride) {
@@ -277,7 +337,7 @@ function makeRedirect(environment, baseUrlOverride) {
277
337
  }
278
338
 
279
339
  // src/version.ts
280
- var SDK_VERSION = "1.0.0";
340
+ var SDK_VERSION = "1.1.0";
281
341
 
282
342
  // src/styles.ts
283
343
  var STYLE_ATTR = "data-maytes-checkout-button";
@@ -488,9 +548,6 @@ function isValidCheckoutResult(value) {
488
548
  }
489
549
  return true;
490
550
  }
491
- function isSecurityError(err) {
492
- return err instanceof DOMException && err.name === "SecurityError";
493
- }
494
551
  function refocusPopup(popup) {
495
552
  try {
496
553
  popup.focus();
@@ -531,7 +588,7 @@ function startPopupPoll(state, onClose) {
531
588
  }, POPUP_POLL_INTERVAL_MS);
532
589
  }
533
590
  function isMobileViewport() {
534
- return window.innerWidth <= MOBILE_MAX_WIDTH;
591
+ return viewportWidth() <= MOBILE_MAX_WIDTH;
535
592
  }
536
593
  function openBlankPopup(state) {
537
594
  return window.open("about:blank", state.popupName, popupFeatures());
@@ -592,7 +649,7 @@ function renderButton(state, container, options = {}) {
592
649
  ensureStylesInjected(state.config.cspNonce);
593
650
  const label = options.label ?? "Split with";
594
651
  const block = options.block === true;
595
- const mode = options.mode ?? "redirect";
652
+ const mode = options.mode ?? "popup";
596
653
  if (mode !== "redirect" && mode !== "popup") {
597
654
  throw new MaytesError(
598
655
  MaytesErrorCode.Config,
@@ -633,13 +690,19 @@ function renderButton(state, container, options = {}) {
633
690
  detail: cause === void 0 ? { reason } : { reason, cause }
634
691
  }));
635
692
  };
636
- const dispatchRedirected = (url) => {
637
- document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail: { url } }));
693
+ const dispatchRedirected = (url, target) => {
694
+ const detail = { url, target };
695
+ document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
638
696
  };
639
- const redirectSameWindow = (url) => {
697
+ const navigateAway = (url) => {
698
+ const outcome = navigateTopLevel(url, false);
640
699
  teardownActiveCheckout();
641
- dispatchRedirected(url);
642
- window.location.href = url;
700
+ if (outcome.target === null) {
701
+ console.error("[maytes/checkout-button] could not leave the embedding frame:", outcome.cause);
702
+ dispatchFailed("navigation-blocked", outcome.cause);
703
+ return;
704
+ }
705
+ dispatchRedirected(url, outcome.target);
643
706
  };
644
707
  const closeOrphanPopup = (popup) => {
645
708
  closePopupWindow(popup);
@@ -654,6 +717,7 @@ function renderButton(state, container, options = {}) {
654
717
  const popup = shouldUsePopup ? openBlankPopup(state) : null;
655
718
  if (popup !== null) {
656
719
  state.popupWindow = popup;
720
+ state.popupNavigated = false;
657
721
  refocusPopup(popup);
658
722
  paintPopupLoadingScreen(popup, state.config.cspNonce);
659
723
  showOverlay(state);
@@ -682,12 +746,13 @@ function renderButton(state, container, options = {}) {
682
746
  result.checkoutId
683
747
  );
684
748
  if (popup !== null) {
749
+ state.popupNavigated = true;
685
750
  navigatePopup(popup, url);
686
751
  } else {
687
752
  if (shouldUsePopup) {
688
- console.warn("[maytes/checkout-button] popup was blocked; falling back to same-window redirect");
753
+ console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
689
754
  }
690
- redirectSameWindow(url);
755
+ navigateAway(url);
691
756
  }
692
757
  } catch (err) {
693
758
  if (popup !== null && popup.closed) return;
@@ -737,6 +802,7 @@ function createInstanceState(options, internal) {
737
802
  busy: false,
738
803
  overlayEl: null,
739
804
  popupWindow: null,
805
+ popupNavigated: false,
740
806
  popupName: generatePopupName(),
741
807
  popupPollHandle: null,
742
808
  destroyed: false,
@@ -789,7 +855,7 @@ var Maytes = (options, internal) => {
789
855
  state.destroyed = true;
790
856
  stopPopupPoll(state);
791
857
  hideOverlay(state);
792
- closePopupWindow(state.popupWindow);
858
+ if (!state.popupNavigated) closePopupWindow(state.popupWindow);
793
859
  state.popupWindow = null;
794
860
  state.busy = false;
795
861
  for (const teardown of [...state.teardowns]) teardown();