@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 +82 -28
- package/README.md +75 -28
- package/dist/{checkout-button.2a6f6a35.cjs → checkout-button.178d8394.cjs} +95 -29
- package/dist/checkout-button.178d8394.cjs.map +1 -0
- package/dist/checkout-button.811263f9.js +151 -0
- package/dist/checkout-button.811263f9.js.map +1 -0
- package/dist/checkout-button.cjs +95 -29
- package/dist/checkout-button.cjs.map +1 -1
- package/dist/checkout-button.d.cts +12 -2
- package/dist/checkout-button.d.ts +12 -2
- package/dist/{checkout-button.5403f24b.mjs → checkout-button.f96c49ee.mjs} +95 -29
- package/dist/checkout-button.f96c49ee.mjs.map +1 -0
- package/dist/checkout-button.js +4 -4
- package/dist/checkout-button.js.map +1 -1
- package/dist/checkout-button.mjs +95 -29
- package/dist/checkout-button.mjs.map +1 -1
- package/dist/integrity.json +26 -26
- package/package.json +6 -4
- package/dist/checkout-button.2a6f6a35.cjs.map +0 -1
- package/dist/checkout-button.3cf081dc.js +0 -151
- package/dist/checkout-button.3cf081dc.js.map +0 -1
- package/dist/checkout-button.5403f24b.mjs.map +0 -1
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
|
-
|
|
66
|
+
First public release on npm.
|
|
6
67
|
|
|
7
|
-
|
|
68
|
+
### Major Changes
|
|
8
69
|
|
|
9
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
86
|
+
- Added a launch `mode` to `renderButton` and a branded popup loading screen.
|
|
33
87
|
|
|
34
|
-
- `renderButton(container, { mode })` accepts `'redirect'` (
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
108
|
+
**Design & build**
|
|
56
109
|
|
|
57
|
-
-
|
|
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
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/logo.svg" height="32" alt="Maytes">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
51
|
-
maytes.renderButton(document.getElementById('
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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.
|
|
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
|
|
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 ?? "
|
|
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
|
-
|
|
693
|
+
const dispatchRedirected = (url, target) => {
|
|
694
|
+
const detail = { url, target };
|
|
695
|
+
document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
|
|
638
696
|
};
|
|
639
|
-
const
|
|
697
|
+
const navigateAway = (url) => {
|
|
698
|
+
const outcome = navigateTopLevel(url, false);
|
|
640
699
|
teardownActiveCheckout();
|
|
641
|
-
|
|
642
|
-
|
|
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;
|
|
753
|
+
console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
|
|
689
754
|
}
|
|
690
|
-
|
|
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();
|