@maytes/checkout-button 1.1.1 → 1.2.1

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +93 -74
  3. package/dist/checkout-button.299a037b.js +188 -0
  4. package/dist/checkout-button.299a037b.js.map +1 -0
  5. package/dist/{checkout-button.2aad12c3.cjs → checkout-button.332d26cc.cjs} +314 -156
  6. package/dist/checkout-button.332d26cc.cjs.map +1 -0
  7. package/dist/{checkout-button.aed57a66.mjs → checkout-button.67fdb2e1.mjs} +314 -156
  8. package/dist/checkout-button.67fdb2e1.mjs.map +1 -0
  9. package/dist/checkout-button.cjs +314 -156
  10. package/dist/checkout-button.cjs.map +1 -1
  11. package/dist/checkout-button.d.cts +40 -4
  12. package/dist/checkout-button.d.ts +40 -4
  13. package/dist/checkout-button.element.afe71cd5.js +187 -0
  14. package/dist/checkout-button.element.afe71cd5.js.map +1 -0
  15. package/dist/checkout-button.element.js +187 -0
  16. package/dist/checkout-button.element.js.map +1 -0
  17. package/dist/checkout-button.js +58 -21
  18. package/dist/checkout-button.js.map +1 -1
  19. package/dist/checkout-button.mjs +314 -156
  20. package/dist/checkout-button.mjs.map +1 -1
  21. package/dist/element.cjs +201 -0
  22. package/dist/element.cjs.map +1 -0
  23. package/dist/element.d.cts +44 -0
  24. package/dist/element.d.ts +44 -0
  25. package/dist/element.mjs +180 -0
  26. package/dist/element.mjs.map +1 -0
  27. package/dist/integrity.json +37 -26
  28. package/dist/react.cjs +139 -0
  29. package/dist/react.cjs.map +1 -0
  30. package/dist/react.d.cts +30 -0
  31. package/dist/react.d.ts +30 -0
  32. package/dist/react.mjs +117 -0
  33. package/dist/react.mjs.map +1 -0
  34. package/docs/guides/angular.md +56 -0
  35. package/docs/guides/api.md +108 -0
  36. package/docs/guides/events.md +48 -0
  37. package/docs/guides/launch-behaviour.md +41 -0
  38. package/docs/guides/nextjs.md +49 -0
  39. package/docs/guides/own-button.md +41 -0
  40. package/docs/guides/quickstart-html.md +31 -0
  41. package/docs/guides/quickstart-npm.md +34 -0
  42. package/docs/guides/react.md +88 -0
  43. package/docs/guides/security-csp.md +32 -0
  44. package/docs/guides/server.md +76 -0
  45. package/docs/guides/solid.md +44 -0
  46. package/docs/guides/stripe-payment-element.md +14 -0
  47. package/docs/guides/svelte.md +46 -0
  48. package/docs/guides/troubleshooting.md +17 -0
  49. package/docs/guides/vue-nuxt.md +64 -0
  50. package/docs/guides/web-component.md +109 -0
  51. package/package.json +40 -3
  52. package/dist/checkout-button.2aad12c3.cjs.map +0 -1
  53. package/dist/checkout-button.4b780d52.js +0 -151
  54. package/dist/checkout-button.4b780d52.js.map +0 -1
  55. package/dist/checkout-button.aed57a66.mjs.map +0 -1
@@ -0,0 +1,56 @@
1
+ # Angular
2
+
3
+ Add the Split with Maytes button to an Angular app with a standalone component that also works with Angular's server-side rendering.
4
+
5
+ You need an endpoint on your server that creates a Maytes checkout and returns `{ checkoutId, checkoutUrl }`. The example calls it `POST /api/maytes/checkout`; [Your server](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/server.md) shows how to build it.
6
+
7
+ ```bash
8
+ npm install @maytes/checkout-button
9
+ ```
10
+
11
+ ```ts
12
+ import { AfterViewInit, Component, ElementRef, Input, OnDestroy, PLATFORM_ID, ViewChild, inject } from '@angular/core';
13
+ import { isPlatformBrowser } from '@angular/common';
14
+ import { Maytes, type MaytesSDK } from '@maytes/checkout-button';
15
+
16
+ @Component({
17
+ selector: 'app-split-with-maytes',
18
+ standalone: true,
19
+ template: '<div #slot></div>',
20
+ })
21
+ export class SplitWithMaytesComponent implements AfterViewInit, OnDestroy {
22
+ @Input({ required: true }) cartId!: string;
23
+ @ViewChild('slot', { static: true }) slot!: ElementRef<HTMLDivElement>;
24
+
25
+ private readonly platformId = inject(PLATFORM_ID);
26
+ private maytes: MaytesSDK | null = null;
27
+
28
+ ngAfterViewInit(): void {
29
+ if (!isPlatformBrowser(this.platformId)) return;
30
+ this.maytes = Maytes({
31
+ environment: 'sandbox',
32
+ createCheckout: async () => {
33
+ const res = await fetch('/api/maytes/checkout', {
34
+ method: 'POST',
35
+ headers: { 'content-type': 'application/json' },
36
+ body: JSON.stringify({ cartId: this.cartId }),
37
+ });
38
+ if (!res.ok) throw new Error('Could not create the Maytes checkout');
39
+ return res.json(); // { checkoutId, checkoutUrl }
40
+ },
41
+ });
42
+ this.maytes.renderButton(this.slot.nativeElement, { block: true });
43
+ }
44
+
45
+ ngOnDestroy(): void {
46
+ this.maytes?.destroy();
47
+ }
48
+ }
49
+ ```
50
+
51
+ - The `isPlatformBrowser` check keeps the button out of server-side rendering; without SSR it is always true.
52
+ - `this.cartId` is read when the shopper clicks, so it is always the latest value.
53
+
54
+ Prefer a standard custom element? [`<maytes-checkout-button>`](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/web-component.md) works here too.
55
+
56
+ Next: [API reference](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/api.md) · [Troubleshooting](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/troubleshooting.md)
@@ -0,0 +1,108 @@
1
+ # API reference
2
+
3
+ Every option, method and default of the `@maytes/checkout-button` browser SDK.
4
+
5
+ `Maytes` is a callable factory, like `Stripe()`. Each call returns an isolated instance. From the script tag it is `window.Maytes`; from npm it is `import { Maytes } from '@maytes/checkout-button'`.
6
+
7
+ ## `Maytes(options)`
8
+
9
+ | Option | Type | Description |
10
+ |---|---|---|
11
+ | `createCheckout` | `() => Promise<{ checkoutId: string; checkoutUrl?: string }>` | Required. Called when the shopper clicks; asks your server for a checkout. Throw (or reject) to report a failure. |
12
+ | `environment` | `'sandbox' \| 'production'` | Required. Which Maytes environment the checkout opens in when you don't return `checkoutUrl`: `sandbox` opens `https://sandbox-checkout.maytes.co`, `production` opens `https://checkout.maytes.co`. |
13
+
14
+ Returns a `MaytesSDK` instance. Invalid options throw `MaytesError` with `code: 'CONFIG'`.
15
+
16
+ An optional second argument, `{ cspNonce }`, sets a nonce on the styles the SDK injects; see [Security and CSP](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/security-csp.md).
17
+
18
+ ## Instance methods
19
+
20
+ | Method | Returns | Description |
21
+ |---|---|---|
22
+ | `renderButton(container, options?)` | `() => void` | Renders the button into `container` (an `HTMLElement`) and returns a function that removes it. |
23
+ | `openCheckout({ mode?, redirectOverlay? })` | `Promise<OpenCheckoutResult>` | Starts the checkout from your own button, exactly as a click on the rendered button does. Takes the same `mode` and `redirectOverlay` as `renderButton()`. Call it inside your click handler, before any `await`. Resolves with how the launch ended and never rejects. See [Using your own button](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/own-button.md). |
24
+ | `redirectToCheckout({ checkoutId, replace? })` | `void` | Sends the current tab to a checkout you already created, without a button. `replace: true` replaces the history entry. Inside an iframe it navigates the top-level window and throws `MaytesError` if the browser refuses. |
25
+ | `instanceId` | `string` | Read-only id of this instance. Every `maytes:checkout-*` event carries it as `event.detail.instanceId`. |
26
+ | `onBusyChange(listener)` | `() => void` | Calls `listener(true)` when a launch on this instance starts and `listener(false)` when it ends; returns a function that stops listening. Useful to disable your own Pay button. |
27
+ | `checkoutUrl({ checkoutId })` | `string` | Builds the hosted checkout URL for a checkout you already created. |
28
+ | `destroy()` | `void` | Tears down the instance: buttons, listeners, the overlay and a popup that hasn't loaded yet. Idempotent. |
29
+
30
+ Call `destroy()` when the page or component goes away, or when you need an instance with a different `environment`. Don't call it because your own code saw the payment succeed: that can tear down a checkout that's still in progress. To remove one button, call the function `renderButton()` returned.
31
+
32
+ ## `renderButton` options
33
+
34
+ | Option | Type | Default | Description |
35
+ |---|---|---|---|
36
+ | `label` | `string` | `'Split with'` | Text before the Maytes logo. The accessible name is `"<label> Maytes"`. |
37
+ | `block` | `boolean` | `false` | `true` makes the button fill the width of its container. |
38
+ | `mode` | `'popup' \| 'redirect'` | `'popup'` | `'popup'` opens a centred window on desktop and falls back to the same tab on phones or when the popup is blocked. `'redirect'` always uses the same tab. |
39
+ | `radius` | `number` | the default pill | Corner radius in whole pixels, from 0 to 999, and at most half the `height` when `height` is set. Use it to match neighbouring buttons. |
40
+ | `height` | `number` | the default height | Button height in whole pixels, from 40 to 55. A button with a `height` keeps its label on one line. |
41
+ | `redirectOverlay` | `boolean` | `false` | `true` shows the Maytes loading overlay while `createCheckout` runs whenever the checkout opens in the same tab (redirect mode, phones, blocked popups). See [Popup, redirect and iframes](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/launch-behaviour.md). |
42
+
43
+ Two buttons rendered from one instance share one checkout at a time: clicks and `openCheckout()` are ignored while that instance's `createCheckout` is running.
44
+
45
+ ## Fitting next to wallet buttons
46
+
47
+ In an express-checkout row next to Apple Pay, Google Pay or Link, match the row's height and corners:
48
+
49
+ ```ts
50
+ import { Maytes } from '@maytes/checkout-button';
51
+
52
+ const maytes = Maytes({ createCheckout, environment: 'sandbox' });
53
+ maytes.renderButton(document.getElementById('maytes-slot')!, { block: true, radius: 6, height: 48 });
54
+ ```
55
+
56
+ That matches Stripe's Express Checkout Element at `buttonHeight: 48` with 6px corners. The colours, logo and label stay Maytes'. Without `radius` and `height` the button renders exactly as before. Invalid values throw `MaytesError` (`CONFIG`).
57
+
58
+ ## Styling with CSS custom properties
59
+
60
+ The button reads three CSS custom properties, so you can style it from your own CSS or bind it to your design tokens:
61
+
62
+ | Property | Default | Sets |
63
+ |---|---|---|
64
+ | `--maytes-button-height` | the button's natural height | Minimum height. Keep it from 40px to 55px. |
65
+ | `--maytes-button-radius` | `999px` (the pill) | Corner radius. |
66
+ | `--maytes-button-font-size` | `14px` | Label size. |
67
+
68
+ ```css
69
+ .express-row {
70
+ --maytes-button-height: 48px;
71
+ --maytes-button-radius: var(--radius-md);
72
+ }
73
+ ```
74
+
75
+ Set them on the button's container; they're inherited. The `height` and `radius` options set the same properties on the button and validate them; values set in CSS aren't validated. A button given a `height` option also keeps its label on one line. Colours, logo and label stay Maytes'.
76
+
77
+ ## Examples
78
+
79
+ ```ts
80
+ import { Maytes } from '@maytes/checkout-button';
81
+
82
+ const maytes = Maytes({
83
+ environment: 'production',
84
+ createCheckout: async () => {
85
+ const res = await fetch('/api/maytes/checkout', { method: 'POST' });
86
+ if (!res.ok) throw new Error('Could not create the Maytes checkout');
87
+ return res.json();
88
+ },
89
+ });
90
+
91
+ const removeButton = maytes.renderButton(document.getElementById('maytes-button')!, {
92
+ label: 'Split with',
93
+ block: true,
94
+ mode: 'redirect',
95
+ });
96
+
97
+ const url = maytes.checkoutUrl({ checkoutId: '019fbb9e-ecd1-7059-99f3-f8bf47b63931' });
98
+ maytes.redirectToCheckout({ checkoutId: '019fbb9e-ecd1-7059-99f3-f8bf47b63931', replace: true });
99
+
100
+ removeButton();
101
+ maytes.destroy();
102
+ ```
103
+
104
+ ## Errors
105
+
106
+ `MaytesError` (exported from the package) is thrown for invalid configuration: options that aren't an object, a missing `createCheckout`, an unknown `environment`, a `renderButton` container that isn't an `HTMLElement`, an unknown `mode`, or a call on a destroyed instance. Its `code` is always `'CONFIG'`. Problems after the click (your request failing, the wrong response shape) are reported as [events](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/events.md), not thrown.
107
+
108
+ Next: [Events](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/events.md) · [Popup, redirect and iframes](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/launch-behaviour.md)
@@ -0,0 +1,48 @@
1
+ # Events
2
+
3
+ The four `maytes:checkout-*` events the button dispatches, and how to listen to them.
4
+
5
+ The button reports what happened with `CustomEvent`s on `document`. Use them for your own UI; your webhook and capture remain the source of truth for payment.
6
+
7
+ | Event | When | `event.detail` |
8
+ |---|---|---|
9
+ | `maytes:checkout-opened` | The popup opened. | `{ instanceId, source }` |
10
+ | `maytes:checkout-closed` | The popup was closed while your page was still open, usually by the shopper. (When the checkout finishes, it sends your page to your `return_url` instead.) | `{ instanceId, source }` |
11
+ | `maytes:checkout-redirected` | The checkout opened without a popup. | `{ url, target, instanceId, source }`, where `target` is `'self'` (this tab), `'top'` (the page around your iframe) or `'tab'` (a new tab) |
12
+ | `maytes:checkout-failed` | The checkout could not open. | `{ reason, cause?, instanceId, source }`, where `reason` is `'create-checkout-rejected'`, `'invalid-shape'` or `'navigation-blocked'` |
13
+
14
+ ```ts
15
+ import type { CheckoutFailedDetail, CheckoutRedirectedDetail } from '@maytes/checkout-button';
16
+
17
+ document.addEventListener('maytes:checkout-failed', (event) => {
18
+ const { reason } = (event as CustomEvent<CheckoutFailedDetail>).detail;
19
+ showMessage(reason === 'create-checkout-rejected'
20
+ ? 'We could not start Split with Maytes. Please try again.'
21
+ : 'Split with Maytes is unavailable right now.');
22
+ });
23
+
24
+ document.addEventListener('maytes:checkout-redirected', (event) => {
25
+ const { target } = (event as CustomEvent<CheckoutRedirectedDetail>).detail;
26
+ if (target === 'tab') showMessage('Split with Maytes opened in a new tab.');
27
+ });
28
+ ```
29
+
30
+ Every `detail` also carries `instanceId`, the id of the instance that sent it (`maytes.instanceId`), and `source`: `'button'` for a click on a rendered button, `'api'` for `openCheckout()`. With several instances on one page, filter by id:
31
+
32
+ ```ts
33
+ import { Maytes } from '@maytes/checkout-button';
34
+
35
+ const maytes = Maytes({ createCheckout, environment: 'sandbox' });
36
+
37
+ document.addEventListener('maytes:checkout-closed', (event) => {
38
+ if (event.detail.instanceId !== maytes.instanceId) return;
39
+ showMessage('Split with Maytes was closed.');
40
+ });
41
+ ```
42
+
43
+ - `failed` reasons: `create-checkout-rejected` means your `createCheckout` threw or rejected; `invalid-shape` means it didn't resolve to `{ checkoutId, checkoutUrl? }`; `navigation-blocked` means the button is inside an iframe and the browser refused both the top-level navigation and a new tab.
44
+ - A blocked popup is not a failure: the button opens the checkout in the same tab and dispatches `maytes:checkout-redirected`.
45
+ - There are no completion callbacks by design: in redirect mode your page is gone before the checkout finishes, and the hosted checkout returns the shopper to your `return_url`.
46
+ - In React, Vue or another framework, add listeners when the component mounts and remove them when it unmounts.
47
+
48
+ Next: [Popup, redirect and iframes](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/launch-behaviour.md) · [Troubleshooting](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/troubleshooting.md)
@@ -0,0 +1,41 @@
1
+ # Popup, redirect and iframes
2
+
3
+ Where the Maytes checkout opens on desktop, on phones, when popups are blocked and inside an iframe.
4
+
5
+ ## Where the checkout opens
6
+
7
+ - **Desktop, `mode: 'popup'` (default):** the button opens a 500 × 800 window immediately on click and shows a Maytes loading screen in it. Your page is covered with a dimmed "Completing checkout with Maytes…" overlay that has a "Return to Maytes" button. Once `createCheckout` resolves, the window loads the checkout. Pressing Esc closes the popup.
8
+ - **Phones (viewport 600px wide or less):** the same tab, even in popup mode.
9
+ - **Popup blocked:** the same tab. This is a normal outcome, reported with `maytes:checkout-redirected`, not `maytes:checkout-failed`.
10
+ - **`mode: 'redirect'`:** always the same tab.
11
+
12
+ When the shopper finishes, the hosted checkout returns them to your `return_url`. In popup mode it sends your page there and closes the popup.
13
+
14
+ The popup opens synchronously inside the click, before your `createCheckout` runs, so browsers treat it as a direct result of the shopper's click.
15
+
16
+ ### A loading overlay for same-tab launches
17
+
18
+ By default a same-tab launch shows only the button's spinner while `createCheckout` runs. Pass `redirectOverlay: true` to `renderButton()` or `openCheckout()` to cover the page with the Maytes overlay ("Taking you to Maytes…", without the "Return to Maytes" button) until the page leaves:
19
+
20
+ ```ts
21
+ import { Maytes } from '@maytes/checkout-button';
22
+
23
+ const maytes = Maytes({ createCheckout, environment: 'sandbox' });
24
+ maytes.renderButton(document.getElementById('maytes-button')!, { mode: 'redirect', redirectOverlay: true });
25
+ ```
26
+
27
+ The overlay goes away if `createCheckout` fails, and clears itself when the shopper comes back with the back button. Invalid values throw `MaytesError` (`CONFIG`).
28
+
29
+ ## Inside an iframe
30
+
31
+ The hosted checkout must run in the top-level window, because its session cookie is refused inside a cross-site frame. If you render the button inside an iframe:
32
+
33
+ 1. The SDK navigates the top-level window.
34
+ 2. If the browser refuses, it opens a new tab.
35
+ 3. If both are refused, it dispatches `maytes:checkout-failed` with `reason: 'navigation-blocked'` and leaves the button usable.
36
+
37
+ 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'`. Read `event.detail.target` (`'self'`, `'top'` or `'tab'`) if your page needs to know where the checkout went.
38
+
39
+ A sandboxed iframe needs `allow-scripts allow-same-origin allow-top-navigation`, plus `allow-popups allow-popups-to-escape-sandbox` for the tab fallback. Rendering the button in the top-level page avoids all of this.
40
+
41
+ Next: [Events](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/events.md) · [Troubleshooting](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/troubleshooting.md)
@@ -0,0 +1,49 @@
1
+ # Next.js
2
+
3
+ Add the Split with Maytes button to a Next.js app with a client component, and keep your Maytes credentials in a Route Handler.
4
+
5
+ ```bash
6
+ npm install @maytes/checkout-button
7
+ ```
8
+
9
+ ```tsx
10
+ 'use client';
11
+
12
+ import { useEffect, useRef } from 'react';
13
+ import { Maytes } from '@maytes/checkout-button';
14
+
15
+ export default function SplitWithMaytesButton({ cartId }: { cartId: string }) {
16
+ const slotRef = useRef<HTMLDivElement>(null);
17
+ const cartIdRef = useRef(cartId);
18
+
19
+ useEffect(() => {
20
+ cartIdRef.current = cartId;
21
+ }, [cartId]);
22
+
23
+ useEffect(() => {
24
+ const maytes = Maytes({
25
+ environment: process.env.NEXT_PUBLIC_MAYTES_ENVIRONMENT === 'production' ? 'production' : 'sandbox',
26
+ createCheckout: async () => {
27
+ const res = await fetch('/api/maytes/checkout', {
28
+ method: 'POST',
29
+ headers: { 'content-type': 'application/json' },
30
+ body: JSON.stringify({ cartId: cartIdRef.current }),
31
+ });
32
+ if (!res.ok) throw new Error('Could not create the Maytes checkout');
33
+ return res.json(); // { checkoutId, checkoutUrl }
34
+ },
35
+ });
36
+ maytes.renderButton(slotRef.current!, { block: true });
37
+ return () => maytes.destroy();
38
+ }, []);
39
+
40
+ return <div ref={slotRef} />;
41
+ }
42
+ ```
43
+
44
+ - **Server rendering is safe.** The package doesn't touch `window` when it's imported, and the button is created in `useEffect`, which only runs in the browser.
45
+ - **The server half** goes in a Route Handler, `app/api/maytes/checkout/route.ts`, so your Maytes credentials never reach the browser. [Your server](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/server.md) shows what it must return.
46
+ - **Prefer the CDN?** Load it with `next/script` (`<Script src="https://js.maytes.co/v1/checkout-button.js" strategy="afterInteractive" onReady={…} />`) and call `window.Maytes(…)` in `onReady` instead of importing.
47
+ - The component follows the [React guide](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/react.md): one instance per mount, StrictMode-safe, the latest cart read through a ref.
48
+
49
+ Next: [API reference](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/api.md) · [Troubleshooting](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/troubleshooting.md)
@@ -0,0 +1,41 @@
1
+ # Using your own button
2
+
3
+ Start the Maytes checkout from a button you already have, such as the Pay button of a Stripe Payment Element checkout.
4
+
5
+ If your page already has its own Pay button, for example a Stripe Payment Element checkout that offers **Split with Maytes** as a Stripe custom payment method, start Maytes from that button with `openCheckout()`:
6
+
7
+ ```ts
8
+ import { Maytes } from '@maytes/checkout-button';
9
+
10
+ const maytes = Maytes({ createCheckout, environment: 'sandbox' });
11
+
12
+ let maytesSelected = false;
13
+ paymentElement.on('change', (event) => {
14
+ maytesSelected = event.value.type === MAYTES_CPM_ID;
15
+ });
16
+
17
+ payButton.addEventListener('click', async () => {
18
+ if (maytesSelected) {
19
+ const result = await maytes.openCheckout();
20
+ if (result.outcome === 'failed') showMessage('Could not start Split with Maytes. Please try again.');
21
+ return;
22
+ }
23
+ // ... your normal card flow
24
+ });
25
+ ```
26
+
27
+ **Call `openCheckout()` inside the click handler, before any `await`.** Browsers only allow a popup straight from a click: the SDK opens the window during the call and creates the checkout afterwards. Called later, the popup may be blocked; the SDK then redirects instead, so the payment still works.
28
+
29
+ `openCheckout({ mode })` takes the same `mode` as `renderButton()`: `'popup'` (default) or `'redirect'`. It resolves with how the launch ended:
30
+
31
+ | `outcome` | Meaning |
32
+ |---|---|
33
+ | `popup` | The popup loaded the Maytes checkout. |
34
+ | `redirected` | The page (or the top window, or a new tab: `target`) went to the Maytes checkout. |
35
+ | `failed` | `createCheckout` failed or returned the wrong shape, or every way out of an iframe was refused (`reason`, same as `maytes:checkout-failed`). |
36
+ | `closed` | The customer closed the popup before the checkout loaded. |
37
+ | `ignored` | A checkout launch from this instance was already in flight. |
38
+
39
+ It never rejects. It throws `MaytesError` (`CONFIG`) synchronously on a destroyed instance or an invalid `mode`. Buttons rendered by the same instance ignore clicks while any launch is in flight.
40
+
41
+ Next: [With Stripe's Payment Element](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/stripe-payment-element.md) · [API reference](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/api.md)
@@ -0,0 +1,31 @@
1
+ # Quickstart: plain HTML
2
+
3
+ Add the Split with Maytes button to any page with a `<script>` tag: server-rendered HTML, PHP, Rails, Django, Laravel, WordPress or any page without a bundler.
4
+
5
+ You need an endpoint on your server that creates a Maytes checkout and returns `{ checkoutId, checkoutUrl }`. The examples call it `POST /api/maytes/checkout`; [Your server](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/server.md) shows how to build it.
6
+
7
+ ```html
8
+ <div id="maytes-button"></div>
9
+
10
+ <script src="https://js.maytes.co/v1/checkout-button.js" crossorigin="anonymous"></script>
11
+ <script>
12
+ const maytes = window.Maytes({
13
+ environment: 'sandbox',
14
+ createCheckout: async () => {
15
+ const res = await fetch('/api/maytes/checkout', { method: 'POST' });
16
+ if (!res.ok) throw new Error('Could not create the Maytes checkout');
17
+ return res.json(); // { checkoutId, checkoutUrl }
18
+ },
19
+ });
20
+
21
+ maytes.renderButton(document.getElementById('maytes-button'), { block: true });
22
+ </script>
23
+ ```
24
+
25
+ - The second script must run after the first, so keep them in this order, both without `async`.
26
+ - Use `environment: 'sandbox'` with sandbox API credentials while you test, and `'production'` with production credentials when you go live.
27
+ - `https://js.maytes.co/v1/checkout-button.js` always serves the newest `1.x` release. To freeze on a tested build with SRI, use a pinned URL from [Install and versioning](https://github.com/kttipay/maytes-checkout-button#install).
28
+
29
+ Prefer a standard custom element? [`<maytes-checkout-button>`](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/web-component.md) works here too.
30
+
31
+ Next: [API reference](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/api.md) · [Events](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/events.md) · [Troubleshooting](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/troubleshooting.md)
@@ -0,0 +1,34 @@
1
+ # Quickstart: npm with a bundler
2
+
3
+ Add the Split with Maytes button to a page built with Vite, webpack, esbuild or another bundler, without a UI framework.
4
+
5
+ You need an endpoint on your server that creates a Maytes checkout and returns `{ checkoutId, checkoutUrl }`. The examples call it `POST /api/maytes/checkout`; [Your server](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/server.md) shows how to build it.
6
+
7
+ ```bash
8
+ npm install @maytes/checkout-button
9
+ ```
10
+
11
+ ```ts
12
+ import { Maytes } from '@maytes/checkout-button';
13
+
14
+ const maytes = Maytes({
15
+ environment: 'sandbox',
16
+ createCheckout: async () => {
17
+ const res = await fetch('/api/maytes/checkout', { method: 'POST' });
18
+ if (!res.ok) throw new Error('Could not create the Maytes checkout');
19
+ return res.json(); // { checkoutId, checkoutUrl }
20
+ },
21
+ });
22
+
23
+ const removeButton = maytes.renderButton(document.getElementById('maytes-button')!, { block: true });
24
+
25
+ // When the checkout page goes away:
26
+ // removeButton(); removes just this button
27
+ // maytes.destroy(); tears down the instance, its buttons and any open popup
28
+ ```
29
+
30
+ - TypeScript types ship with the package: `MaytesSDK`, `MaytesOptions`, `RenderButtonOptions`, the event detail types and more.
31
+ - Use `environment: 'sandbox'` with sandbox API credentials while you test, and `'production'` with production credentials when you go live.
32
+ - ESM (`import`) and CommonJS (`require`) builds ship unminified; your bundler minifies them.
33
+
34
+ Next: [API reference](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/api.md) · [Events](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/events.md) · [Troubleshooting](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/troubleshooting.md)
@@ -0,0 +1,88 @@
1
+ # React
2
+
3
+ Add the Split with Maytes button to a React app (Vite, Create React App, Remix) with a small component that is safe under StrictMode.
4
+
5
+ You need an endpoint on your server that creates a Maytes checkout and returns `{ checkoutId, checkoutUrl }`. The example calls it `POST /api/maytes/checkout`; [Your server](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/server.md) shows how to build it.
6
+
7
+ ```bash
8
+ npm install @maytes/checkout-button
9
+ ```
10
+
11
+ ```tsx
12
+ import { useEffect, useRef } from 'react';
13
+ import { Maytes } from '@maytes/checkout-button';
14
+
15
+ export function SplitWithMaytesButton({ cartId }: { cartId: string }) {
16
+ const slotRef = useRef<HTMLDivElement>(null);
17
+ const cartIdRef = useRef(cartId);
18
+
19
+ useEffect(() => {
20
+ cartIdRef.current = cartId;
21
+ }, [cartId]);
22
+
23
+ useEffect(() => {
24
+ const maytes = Maytes({
25
+ environment: 'sandbox',
26
+ createCheckout: async () => {
27
+ const res = await fetch('/api/maytes/checkout', {
28
+ method: 'POST',
29
+ headers: { 'content-type': 'application/json' },
30
+ body: JSON.stringify({ cartId: cartIdRef.current }),
31
+ });
32
+ if (!res.ok) throw new Error('Could not create the Maytes checkout');
33
+ return res.json(); // { checkoutId, checkoutUrl }
34
+ },
35
+ });
36
+ maytes.renderButton(slotRef.current!, { block: true });
37
+ return () => maytes.destroy();
38
+ }, []);
39
+
40
+ return <div ref={slotRef} />;
41
+ }
42
+ ```
43
+
44
+ - **One instance per mount.** The instance is created in an effect and destroyed in its cleanup. StrictMode mounts effects twice in development; the first instance is destroyed before the second is created, so one button remains.
45
+ - **Latest cart, no rebuild.** `createCheckout` runs on click, so it reads `cartIdRef.current` rather than closing over the first render's `cartId`, and the button is not rebuilt when the cart changes.
46
+ - **Reacting to the checkout.** To show your own message when the checkout can't open, add a [`maytes:checkout-*` event](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/events.md) listener in another `useEffect` and remove it in the cleanup.
47
+ - Using Next.js? See the [Next.js guide](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/nextjs.md).
48
+
49
+ ## React bindings
50
+
51
+ The package also ships React components at `@maytes/checkout-button/react`. They do the same as the component above, without writing it yourself:
52
+
53
+ ```tsx
54
+ import { MaytesProvider, MaytesButton } from '@maytes/checkout-button/react';
55
+
56
+ export function Checkout({ cartId }: { cartId: string }) {
57
+ return (
58
+ <MaytesProvider environment="sandbox" createCheckout={() => createMaytesCheckout(cartId)}>
59
+ <MaytesButton block mode="popup" onFailed={(detail) => showMessage(detail.reason)} />
60
+ </MaytesProvider>
61
+ );
62
+ }
63
+ ```
64
+
65
+ `MaytesButton` takes the same options as `renderButton()` (`mode`, `label`, `block`, `radius`, `height`, `redirectOverlay`) plus `onOpened`, `onClosed`, `onRedirected` and `onFailed`. The callbacks fire only for launches started by a `MaytesButton` under the same provider.
66
+
67
+ From your own button, for example a Stripe Payment Element where Split with Maytes is a custom payment method:
68
+
69
+ ```tsx
70
+ import { useMaytes } from '@maytes/checkout-button/react';
71
+
72
+ export function PayButton({ maytesSelected }: { maytesSelected: boolean }) {
73
+ const { openCheckout, busy } = useMaytes();
74
+ return (
75
+ <button disabled={busy} onClick={() => (maytesSelected ? openCheckout() : payWithStripe())}>
76
+ Pay
77
+ </button>
78
+ );
79
+ }
80
+ ```
81
+
82
+ Track `maytesSelected` from the Payment Element's `change` event (`event.value.type === '<your cpmt_ id>'`) and call `openCheckout()` inside the click, before any `await`, so the popup isn't blocked.
83
+
84
+ - `createCheckout` can change on every render; the provider always calls the latest one and never re-creates the SDK. Changing `environment` replaces the instance.
85
+ - Works with React 18 and 19, StrictMode and server rendering (Next.js); the entry is marked `"use client"`.
86
+ - React is an optional peer dependency: the core and the CDN script don't include it.
87
+
88
+ Next: [API reference](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/api.md) · [Troubleshooting](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/troubleshooting.md)
@@ -0,0 +1,32 @@
1
+ # Security and CSP
2
+
3
+ Where credentials live, and how to run the button under a strict Content Security Policy.
4
+
5
+ - **Keep credentials on your server.** The browser only ever sees `checkoutId` and `checkoutUrl`. Treat `checkoutId` like a magic link: it identifies the shopper's checkout session.
6
+ - **Price on the server.** Send a cart or order reference from the browser and price it from your own catalogue; never trust a browser-supplied total.
7
+
8
+ ## Scripts
9
+
10
+ Allow the CDN:
11
+
12
+ ```
13
+ Content-Security-Policy: script-src 'self' https://js.maytes.co;
14
+ ```
15
+
16
+ With a pinned URL you can also add an `integrity` attribute (SRI); the evergreen `/v1/` URL can't have one because its bytes change with each `1.x` release. Pinned URLs and their SRI hashes are in [Install and versioning](https://github.com/kttipay/maytes-checkout-button#install) and the [CHANGELOG](https://github.com/kttipay/maytes-checkout-button/blob/main/CHANGELOG.md).
17
+
18
+ ## Styles
19
+
20
+ The button injects a `<style>` tag. On a strict `style-src` policy, pass your nonce as the second argument so it's set on every style tag the SDK adds, including the popup's loading screen and the page overlay:
21
+
22
+ ```ts
23
+ import { Maytes } from '@maytes/checkout-button';
24
+
25
+ const maytes = Maytes({ createCheckout, environment: 'production' }, { cspNonce: 'your-nonce' });
26
+ ```
27
+
28
+ ## No eval
29
+
30
+ The bundle uses no `eval`, `Function` or string-form timers, and every release is scanned for them, so `'unsafe-eval'` is never needed.
31
+
32
+ Next: [Troubleshooting](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/troubleshooting.md)
@@ -0,0 +1,76 @@
1
+ # Your server
2
+
3
+ What the endpoint behind `createCheckout` must do and return, in any backend language, with a Node.js example.
4
+
5
+ The button never holds your Maytes credentials. Your page calls your own endpoint; your endpoint creates the checkout with the [Maytes merchant API](https://developers.maytes.co/create-checkout) and returns two fields:
6
+
7
+ ```json
8
+ { "checkoutId": "019fbb9e-ecd1-7059-99f3-f8bf47b63931", "checkoutUrl": "https://sandbox-checkout.maytes.co/?id=019fbb9e-ecd1-7059-99f3-f8bf47b63931" }
9
+ ```
10
+
11
+ - `checkoutId` is required. `checkoutUrl` is optional, but return it: the button then opens exactly the URL Maytes gave you.
12
+ - The merchant API answers in snake_case (`checkout_uuid`, `checkout_url`). Map them to `checkoutId` and `checkoutUrl`; anything else makes the button report `invalid-shape`.
13
+ - Price the cart on your server. Send a cart or order reference from the browser, never a total.
14
+
15
+ ## Node.js example
16
+
17
+ Express with the Maytes backend SDK (`npm install @maytes/api-client-js`). SDKs for Python, PHP, Go, Java, Ruby and .NET are listed in [Build your integration](https://developers.maytes.co/build-your-integration).
18
+
19
+ ```ts
20
+ import express from 'express';
21
+ import { createMaytesApiClient } from '@maytes/api-client-js';
22
+
23
+ const maytesApi = createMaytesApiClient({
24
+ endpoint: 'https://sandbox-api.maytes.co', // https://api.maytes.co in production
25
+ clientId: process.env.MAYTES_CLIENT_ID!,
26
+ clientSecret: process.env.MAYTES_CLIENT_SECRET!,
27
+ });
28
+
29
+ const app = express();
30
+ app.use(express.json());
31
+
32
+ app.post('/api/maytes/checkout', async (req, res) => {
33
+ const cart = await loadCart(req.body.cartId); // your catalogue, your prices
34
+ const merchantOrderId = await createPendingOrder(cart);
35
+
36
+ const created = await maytesApi.createCheckout({
37
+ createCheckoutRequest: {
38
+ merchantOrderId,
39
+ totalAmount: cart.total, // minor units: 4500 = AUD 45.00
40
+ currency: 'AUD',
41
+ returnUrl: `https://shop.example.com/thanks?order=${merchantOrderId}`,
42
+ cancelUrl: 'https://shop.example.com/cart',
43
+ items: cart.lines.map((line) => ({
44
+ itemRef: line.sku,
45
+ name: line.name,
46
+ quantity: line.quantity,
47
+ unitPrice: line.unitPrice,
48
+ currency: 'AUD',
49
+ category: 'other',
50
+ })),
51
+ },
52
+ });
53
+
54
+ await saveCheckoutId(merchantOrderId, created.data.checkoutUuid);
55
+ res.json({ checkoutId: created.data.checkoutUuid, checkoutUrl: created.data.checkoutUrl });
56
+ });
57
+ ```
58
+
59
+ ## Taking the money
60
+
61
+ The button only opens the checkout. Your server finishes the payment:
62
+
63
+ - [Webhooks](https://developers.maytes.co/webhooks): subscribe to `checkout.authorized` and verify the `X-Maytes-Signature` header.
64
+ - [Capture and checkout status](https://developers.maytes.co/capture-and-status): capture within 2 minutes of `checkout.authorized`, or the hold is voided. A successful capture is your "order paid" signal.
65
+ - The shopper comes back to the `return_url` you set. In popup mode the checkout sends your page there and closes the popup.
66
+
67
+ ## Environments
68
+
69
+ | Browser `environment` | Hosted checkout | Merchant API |
70
+ |---|---|---|
71
+ | `'sandbox'` | `https://sandbox-checkout.maytes.co` | `https://sandbox-api.maytes.co` |
72
+ | `'production'` | `https://checkout.maytes.co` | `https://api.maytes.co` |
73
+
74
+ Sandbox credentials only work against the sandbox API. Keep the browser's `environment` on the same side as the credentials your server uses.
75
+
76
+ Next: [Troubleshooting](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/troubleshooting.md) · [Security and CSP](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/security-csp.md)