@railbed/embed 0.0.0-stage → 0.1.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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MediaGlobe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,123 @@
1
- # Temporary Holding Version
1
+ # @railbed/embed
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Railbed's embedded checkout for React, Vue and plain pages: the payment widget or pricing table in your page, a buy
4
+ button that opens it over the page, and checkout sessions your server made. Typed events tell your page what happened.
5
+
6
+ It's a thin layer over Railbed's embed script (`https://pay.railbed.io/embed.js`): this package adds the script to the
7
+ page once, renders the elements it draws, and turns its DOM events into props and emits. No runtime dependencies; React
8
+ 18+ and Vue 3.3+ are optional peers.
9
+
10
+ Full guide: https://railbed.com/docs/guides/embedded-checkout/
11
+
12
+ ```bash
13
+ npm install @railbed/embed
14
+ ```
15
+
16
+ The package is ESM only. Bundlers and Next.js find it as is, as does `require()` in Node 20.19+ or 22.12+ (where it
17
+ loads ES modules without a flag), and TypeScript finds its types with `moduleResolution` `bundler`, `node16` (from an
18
+ ES module) or `node`. A Jest setup that runs CommonJS has to transform it:
19
+ `transformIgnorePatterns: ['/node_modules/(?!@railbed/embed/)']`.
20
+
21
+ ## React and Next.js
22
+
23
+ The components are client components (`'use client'` is built in), so a Next.js app router page can render
24
+ `<RailbedCheckout checkout="pro-plan" />` directly. Event handlers are functions, which can't cross from a server
25
+ component, so pass them from a component of your own marked `'use client'`. On the server the components render the
26
+ element with a plain link to the checkout, which stays if a site blocks the script.
27
+
28
+ ```tsx
29
+ 'use client';
30
+ import { useRouter } from 'next/navigation';
31
+ import { RailbedCheckout } from '@railbed/embed/react';
32
+
33
+ export function ProCheckout({ email }: { email?: string }) {
34
+ const router = useRouter();
35
+ return (
36
+ <RailbedCheckout
37
+ checkout="pro-plan" // the slug on the checkout's Share tab
38
+ sessionEndpoint="/api/railbed/session" // optional: your server makes the session at Continue
39
+ email={email} // optional: fills in the buyer's email
40
+ style={{ '--railbed-accent': '#1F4D3A', '--railbed-font': 'Inter' }}
41
+ onPaid={(detail, event) => {
42
+ event.preventDefault(); // stay on this page instead of going to the success URL
43
+ router.push(`/thanks?payment=${detail.session}`);
44
+ }}
45
+ onError={(detail) => console.warn(detail.code)}
46
+ />
47
+ );
48
+ }
49
+ ```
50
+
51
+ `<RailbedButton checkout="…" label="Buy now" shape="pill">` takes the same props and opens the checkout over the page
52
+ (a full-screen sheet on phones).
53
+
54
+ Instead of `sessionEndpoint`, pass `createSession`, a function that gets `{ checkout, plan, option, email }` and returns
55
+ the session id (or `{ session }`, or the API's session object):
56
+
57
+ ```tsx
58
+ <RailbedCheckout checkout="pro-plan" createSession={(request) => api.post('/railbed/session', request)} />
59
+ ```
60
+
61
+ ## Vue
62
+
63
+ ```js
64
+ import { RailbedCheckout } from '@railbed/embed/vue';
65
+ ```
66
+
67
+ ```html
68
+ <RailbedCheckout
69
+ checkout="pro-plan"
70
+ session-endpoint="/api/railbed/session"
71
+ :email="user.email"
72
+ class="checkout"
73
+ @paid="(detail, event) => onPaid(detail, event)"
74
+ @error="(detail) => console.warn(detail.code)"
75
+ />
76
+ ```
77
+
78
+ Events are `ready`, `step`, `handoff`, `paid`, `held`, `failed`, `expired`, `error`, `close` (each with the detail and
79
+ the DOM event) and `load-error` when the script can't load.
80
+
81
+ ## Any page
82
+
83
+ ```js
84
+ import { openCheckout } from '@railbed/embed';
85
+
86
+ // Your server made a checkout session and returned its url (or its id, pay_…).
87
+ const { status, session } = await openCheckout(url);
88
+ // status: 'paid' | 'held' | 'failed' | 'expired' | 'closed'. Fulfil from your server, not from this.
89
+ ```
90
+
91
+ `loadRailbed()` adds the script and resolves with `window.Railbed`; plain HTML can skip this package entirely:
92
+
93
+ ```html
94
+ <script src="https://pay.railbed.io/embed.js" async></script>
95
+ <railbed-checkout checkout="pro-plan"></railbed-checkout>
96
+ ```
97
+
98
+ ## Props
99
+
100
+ | Prop | What it does |
101
+ | --- | --- |
102
+ | `checkout` | The checkout's slug. |
103
+ | `session` | A session your server already made (`pay_…`). |
104
+ | `sessionEndpoint` | Your endpoint that makes a session when the buyer presses Continue. |
105
+ | `createSession` | A function that makes the session, instead of the endpoint. |
106
+ | `email` | Fills in the buyer's email. Sent by message, never in a URL. |
107
+ | `label`, `shape` | `RailbedButton` only: its text, and `pill` or `rounded`. |
108
+ | `origin` | Default `https://pay.railbed.io`. |
109
+ | `nonce` | For a Content Security Policy that uses nonces. |
110
+ | `className`/`class`, `style` | Passed to the element, so the `--railbed-*` variables work from either. |
111
+
112
+ Event details never carry the buyer's email or details. Fulfil orders from the signed webhook or
113
+ `GET /v1/payments/:id`, never from a browser event.
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ npm install
119
+ npm test # vitest in jsdom; no network
120
+ npm run build # ESM and .d.ts in dist/
121
+ ```
122
+
123
+ MIT licensed.
@@ -0,0 +1,108 @@
1
+ /** Where the checkout lives. Override it only to point at a Railbed test host. */
2
+ export declare const DEFAULT_ORIGIN = "https://pay.railbed.io";
3
+ /** The events every element fires, as `railbed:<name>` DOM events that bubble and cross shadow roots. */
4
+ export declare const RAILBED_EVENTS: readonly ["ready", "step", "handoff", "paid", "held", "failed", "expired", "error", "close"];
5
+ export type RailbedEventName = (typeof RAILBED_EVENTS)[number];
6
+ /** Where the buyer is in the checkout (`railbed:step`). */
7
+ export type RailbedStep = 'plan' | 'details' | 'method' | 'opening' | 'waiting' | 'crypto' | 'receipt';
8
+ /**
9
+ * What an event carries. Never the buyer's email or details, a reference, metadata or a deposit address: ask your
10
+ * server (GET /v1/payments/:id) or use the signed webhook for anything you fulfil from.
11
+ */
12
+ export interface RailbedEventDetail {
13
+ /** The checkout session (`pay_…`), once there is one. */
14
+ session: string | null;
15
+ /** The checkout's slug, for an element made from a checkout. */
16
+ checkout: string | null;
17
+ mode?: 'test' | 'live';
18
+ /** In the checkout's currency, as a decimal string: "49.00". */
19
+ amount?: string;
20
+ currency?: string;
21
+ /** A pricing table's plan, as the buyer saw it. */
22
+ plan?: string;
23
+ status?: string;
24
+ /** `railbed:step` only. */
25
+ step?: RailbedStep;
26
+ /** `railbed:error` only: what went wrong, for example `session_failed` when your server didn't make a session. */
27
+ code?: string;
28
+ /** `railbed:handoff` only: the card provider the buyer went on to. */
29
+ provider?: string;
30
+ }
31
+ export type RailbedEvent = CustomEvent<RailbedEventDetail>;
32
+ /** What the checkout sends your session endpoint or `createSession` when the buyer presses Continue. */
33
+ export interface SessionRequest {
34
+ checkout: string | null;
35
+ /** A pricing table's plan id. */
36
+ plan: string | null;
37
+ /** A pricing table's billing option (its switch): 0 or 1. */
38
+ option: 0 | 1 | null;
39
+ /** What the buyer typed. Your server's own record of the user is the one to trust. */
40
+ email: string;
41
+ }
42
+ /** A session id (`pay_…`), or an object carrying one as `session` or `id` (the API's session object works as is). */
43
+ export type SessionAnswer = string | {
44
+ session: string;
45
+ } | {
46
+ id: string;
47
+ };
48
+ /** Makes the session on your server and returns it. A rejection or a wrong answer shows the buyer a retry. */
49
+ export type CreateSession = (request: SessionRequest) => SessionAnswer | Promise<SessionAnswer>;
50
+ /** How a pop-up opened with `openCheckout` ended. */
51
+ export interface OpenResult {
52
+ status: 'paid' | 'held' | 'failed' | 'expired' | 'closed';
53
+ session: string;
54
+ }
55
+ /** What embed.js puts on `window.Railbed`. */
56
+ export interface RailbedGlobal {
57
+ open(urlOrId: string): Promise<OpenResult>;
58
+ }
59
+ export interface LoadOptions {
60
+ /** Default https://pay.railbed.io. */
61
+ origin?: string;
62
+ /** For a Content Security Policy that uses nonces. */
63
+ nonce?: string;
64
+ /** How long to wait for the script, in ms. Default 20 000. */
65
+ timeout?: number;
66
+ }
67
+ /** The `on…` prop for each event, as the React components name them. */
68
+ export declare const HANDLER_NAMES: {
69
+ readonly ready: "onReady";
70
+ readonly step: "onStep";
71
+ readonly handoff: "onHandoff";
72
+ readonly paid: "onPaid";
73
+ readonly held: "onHeld";
74
+ readonly failed: "onFailed";
75
+ readonly expired: "onExpired";
76
+ readonly error: "onError";
77
+ readonly close: "onClose";
78
+ };
79
+ export type RailbedHandler = (detail: RailbedEventDetail, event: RailbedEvent) => void;
80
+ /** The origin, checked: http or https only, nothing after the host. */
81
+ export declare function railbedOrigin(origin?: string): string;
82
+ /**
83
+ * Adds `{origin}/embed.js` to the page once and resolves when it has run, with `window.Railbed`. Safe to call as often
84
+ * as you like; a script tag already on the page is reused. Browser only.
85
+ */
86
+ export declare function loadRailbed(options?: LoadOptions): Promise<RailbedGlobal>;
87
+ /**
88
+ * Opens a checkout session your server made (its `url`, or its id `pay_…`) over the page and resolves when the buyer
89
+ * closes it. Fulfil from your server (the webhook, or GET /v1/payments/:id), not from `status`.
90
+ */
91
+ export declare function openCheckout(urlOrId: string, options?: LoadOptions): Promise<OpenResult>;
92
+ /** The element's plain link while embed.js loads, or if a site blocks it (the script draws over it once it runs). */
93
+ export declare function fallbackHref(origin: string, checkout?: string | null, session?: string | null): string | null;
94
+ /** The element's attributes from the components' props, leaving out what isn't set. */
95
+ export declare function elementAttributes(props: {
96
+ checkout?: string | null;
97
+ session?: string | null;
98
+ sessionEndpoint?: string | null;
99
+ email?: string | null;
100
+ label?: string | null;
101
+ shape?: 'pill' | 'rounded' | null;
102
+ }): Record<string, string>;
103
+ /** The element's `createSession` hook (embed.js reads it at Continue; set before or after the script runs). */
104
+ export interface SessionHost extends HTMLElement {
105
+ createSession: CreateSession | null;
106
+ }
107
+ /** Listens for every `railbed:*` event on `el`, calling `handle(name, event)`; returns the clean-up. */
108
+ export declare function listen(el: EventTarget, handle: (name: RailbedEventName, event: RailbedEvent) => void): () => void;
package/dist/index.js ADDED
@@ -0,0 +1,143 @@
1
+ // @railbed/embed: a thin, typed layer over Railbed's embed script ({origin}/embed.js). The script does the work (the
2
+ // <railbed-checkout> and <railbed-button> elements, their frames, Railbed.open); this package loads it once and gives
3
+ // React and Vue components, and plain pages, the same API with types. No runtime dependencies.
4
+ // Reference: https://railbed.com/docs/guides/embedded-checkout/
5
+ /** Where the checkout lives. Override it only to point at a Railbed test host. */
6
+ export const DEFAULT_ORIGIN = 'https://pay.railbed.io';
7
+ /** The events every element fires, as `railbed:<name>` DOM events that bubble and cross shadow roots. */
8
+ export const RAILBED_EVENTS = ['ready', 'step', 'handoff', 'paid', 'held', 'failed', 'expired', 'error', 'close'];
9
+ /** The `on…` prop for each event, as the React components name them. */
10
+ export const HANDLER_NAMES = {
11
+ ready: 'onReady',
12
+ step: 'onStep',
13
+ handoff: 'onHandoff',
14
+ paid: 'onPaid',
15
+ held: 'onHeld',
16
+ failed: 'onFailed',
17
+ expired: 'onExpired',
18
+ error: 'onError',
19
+ close: 'onClose',
20
+ };
21
+ /** The origin, checked: http or https only, nothing after the host. */
22
+ export function railbedOrigin(origin = DEFAULT_ORIGIN) {
23
+ let url;
24
+ try {
25
+ url = new URL(origin);
26
+ }
27
+ catch {
28
+ throw new TypeError(`Not an origin: ${origin}`);
29
+ }
30
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
31
+ throw new TypeError(`Not an http or https origin: ${origin}`);
32
+ return url.origin;
33
+ }
34
+ function railbed() {
35
+ const r = window.Railbed;
36
+ return r && typeof r.open === 'function' ? r : null;
37
+ }
38
+ const loading = new Map();
39
+ /**
40
+ * Adds `{origin}/embed.js` to the page once and resolves when it has run, with `window.Railbed`. Safe to call as often
41
+ * as you like; a script tag already on the page is reused. Browser only.
42
+ */
43
+ export function loadRailbed(options = {}) {
44
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
45
+ return Promise.reject(new Error('loadRailbed runs in the browser only.'));
46
+ }
47
+ let origin;
48
+ try {
49
+ origin = railbedOrigin(options.origin);
50
+ }
51
+ catch (err) {
52
+ return Promise.reject(err);
53
+ }
54
+ const ready = railbed();
55
+ if (ready)
56
+ return Promise.resolve(ready);
57
+ const pending = loading.get(origin);
58
+ if (pending)
59
+ return pending;
60
+ const src = `${origin}/embed.js`;
61
+ const promise = new Promise((resolve, reject) => {
62
+ let script = Array.from(document.querySelectorAll('script[src]')).find((s) => s.src === src);
63
+ if (!script) {
64
+ script = document.createElement('script');
65
+ script.src = src;
66
+ script.async = true;
67
+ if (options.nonce)
68
+ script.nonce = options.nonce;
69
+ document.head.appendChild(script);
70
+ }
71
+ const tag = script;
72
+ const fail = (message) => {
73
+ cleanup();
74
+ loading.delete(origin); // the next call tries again
75
+ reject(new Error(message));
76
+ };
77
+ const onLoad = () => {
78
+ const r = railbed();
79
+ if (r) {
80
+ cleanup();
81
+ resolve(r);
82
+ }
83
+ else
84
+ fail('Railbed’s embed script ran but can’t work in this browser.');
85
+ };
86
+ const onError = () => {
87
+ tag.remove();
88
+ fail(`Couldn’t load ${src}. If your site sets a Content Security Policy, allow ${new URL(origin).host} in it.`);
89
+ };
90
+ const timer = setTimeout(() => fail(`${src} took too long to load.`), options.timeout ?? 20000);
91
+ function cleanup() {
92
+ clearTimeout(timer);
93
+ tag.removeEventListener('load', onLoad);
94
+ tag.removeEventListener('error', onError);
95
+ }
96
+ tag.addEventListener('load', onLoad);
97
+ tag.addEventListener('error', onError);
98
+ });
99
+ loading.set(origin, promise);
100
+ return promise;
101
+ }
102
+ /**
103
+ * Opens a checkout session your server made (its `url`, or its id `pay_…`) over the page and resolves when the buyer
104
+ * closes it. Fulfil from your server (the webhook, or GET /v1/payments/:id), not from `status`.
105
+ */
106
+ export function openCheckout(urlOrId, options = {}) {
107
+ return loadRailbed(options).then((r) => r.open(urlOrId));
108
+ }
109
+ // ---------- Shared by the React and Vue components ----------
110
+ /** The element's plain link while embed.js loads, or if a site blocks it (the script draws over it once it runs). */
111
+ export function fallbackHref(origin, checkout, session) {
112
+ if (checkout)
113
+ return `${origin}/c/${encodeURIComponent(checkout)}`;
114
+ if (session)
115
+ return `${origin}/p/${encodeURIComponent(session)}`;
116
+ return null;
117
+ }
118
+ /** The element's attributes from the components' props, leaving out what isn't set. */
119
+ export function elementAttributes(props) {
120
+ const out = {};
121
+ if (props.checkout)
122
+ out.checkout = props.checkout;
123
+ if (props.session)
124
+ out.session = props.session;
125
+ if (props.sessionEndpoint)
126
+ out['session-endpoint'] = props.sessionEndpoint;
127
+ if (props.email)
128
+ out.email = props.email;
129
+ if (props.label)
130
+ out.label = props.label;
131
+ if (props.shape)
132
+ out.shape = props.shape;
133
+ return out;
134
+ }
135
+ /** Listens for every `railbed:*` event on `el`, calling `handle(name, event)`; returns the clean-up. */
136
+ export function listen(el, handle) {
137
+ const off = RAILBED_EVENTS.map((name) => {
138
+ const fn = (e) => handle(name, e);
139
+ el.addEventListener(`railbed:${name}`, fn);
140
+ return () => el.removeEventListener(`railbed:${name}`, fn);
141
+ });
142
+ return () => off.forEach((f) => f());
143
+ }
@@ -0,0 +1,47 @@
1
+ import { type CSSProperties, type ReactNode } from 'react';
2
+ import { HANDLER_NAMES, type CreateSession, type RailbedEventName, type RailbedHandler } from './index.js';
3
+ export type { CreateSession, OpenResult, RailbedEvent, RailbedEventDetail, RailbedEventName, RailbedHandler, RailbedStep, SessionAnswer, SessionRequest, } from './index.js';
4
+ export { DEFAULT_ORIGIN, loadRailbed, openCheckout } from './index.js';
5
+ /** `onReady`, `onStep`, `onHandoff`, `onPaid`, `onHeld`, `onFailed`, `onExpired`, `onError`, `onClose`. */
6
+ export type RailbedEventProps = {
7
+ [K in RailbedEventName as (typeof HANDLER_NAMES)[K]]?: RailbedHandler;
8
+ };
9
+ /** Inline styles, including the --railbed-* variables that match the checkout to your site. */
10
+ export type RailbedStyle = CSSProperties & {
11
+ [name: `--railbed-${string}`]: string | number | undefined;
12
+ };
13
+ export interface RailbedCheckoutProps extends RailbedEventProps {
14
+ /** The checkout's slug, from its Share tab. */
15
+ checkout?: string;
16
+ /** A session your server already made (`pay_…`). With `checkout`, the buyer pays that session. */
17
+ session?: string;
18
+ /** Your endpoint that makes a session when the buyer presses Continue (POST, same site, JSON). */
19
+ sessionEndpoint?: string;
20
+ /** Makes the session instead of `sessionEndpoint`, for example through your own API client. */
21
+ createSession?: CreateSession;
22
+ /** Fills in the buyer's email. Sent to the checkout by message, never in a URL. */
23
+ email?: string;
24
+ className?: string;
25
+ style?: RailbedStyle;
26
+ id?: string;
27
+ /**
28
+ * What shows until embed.js has drawn the checkout, and stays if a site blocks it. Defaults to a plain link to the
29
+ * checkout; pass `null` for nothing.
30
+ */
31
+ children?: ReactNode;
32
+ /** Default https://pay.railbed.io. */
33
+ origin?: string;
34
+ /** For a Content Security Policy that uses nonces. */
35
+ nonce?: string;
36
+ /** embed.js couldn't load (blocked, offline): the plain link is still there. */
37
+ onLoadError?: (error: Error) => void;
38
+ }
39
+ export interface RailbedButtonProps extends RailbedCheckoutProps {
40
+ /** The button's text. Default "Buy now" ("Pay now" for a session alone). */
41
+ label?: string;
42
+ shape?: 'pill' | 'rounded';
43
+ }
44
+ /** The payment widget or pricing table in your page. */
45
+ export declare const RailbedCheckout: import("react").ForwardRefExoticComponent<RailbedCheckoutProps & import("react").RefAttributes<HTMLElement>>;
46
+ /** A buy button that opens the checkout over your page (a full-screen sheet on phones). */
47
+ export declare const RailbedButton: import("react").ForwardRefExoticComponent<RailbedButtonProps & import("react").RefAttributes<HTMLElement>>;
package/dist/react.js ADDED
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+ // React components for Railbed's embedded checkout: <RailbedCheckout> (the payment widget or pricing table in the page)
3
+ // and <RailbedButton> (a buy button that opens it over the page). Both render the custom element embed.js draws, load
4
+ // embed.js once, and turn its `railbed:*` DOM events into props. Works with React 18 and 19, and in Next.js's app router
5
+ // (this file is a client component). Reference: https://railbed.com/docs/guides/embedded-checkout/
6
+ import { createElement, forwardRef, useCallback, useEffect, useRef, version } from 'react';
7
+ import { DEFAULT_ORIGIN, elementAttributes, fallbackHref, HANDLER_NAMES, listen, loadRailbed, railbedOrigin, } from './index.js';
8
+ export { DEFAULT_ORIGIN, loadRailbed, openCheckout } from './index.js';
9
+ // React 19 sets `className` on a custom element as its class; React 18 passes props through as attributes, so it needs
10
+ // `class` itself.
11
+ const REACT_MAJOR = Number.parseInt(version, 10);
12
+ const CLASS_PROP = REACT_MAJOR >= 19 ? 'className' : 'class';
13
+ function assignRef(ref, value) {
14
+ if (typeof ref === 'function')
15
+ ref(value);
16
+ else if (ref)
17
+ ref.current = value;
18
+ }
19
+ function useRailbedElement(tag, props, ref, words) {
20
+ const node = useRef(null);
21
+ const latest = useRef(props);
22
+ latest.current = props;
23
+ const origin = railbedOrigin(props.origin ?? DEFAULT_ORIGIN);
24
+ useEffect(() => {
25
+ let live = true;
26
+ loadRailbed({ origin, nonce: props.nonce }).catch((err) => {
27
+ if (live)
28
+ latest.current.onLoadError?.(err);
29
+ });
30
+ return () => {
31
+ live = false;
32
+ };
33
+ }, [origin, props.nonce]);
34
+ useEffect(() => {
35
+ const el = node.current;
36
+ if (!el)
37
+ return;
38
+ return listen(el, (name, event) => latest.current[HANDLER_NAMES[name]]?.(event.detail, event));
39
+ }, []);
40
+ // One stable function while a createSession prop is given, so a new arrow each render doesn't re-send anything.
41
+ const hasCreate = typeof props.createSession === 'function';
42
+ useEffect(() => {
43
+ const el = node.current;
44
+ if (!el)
45
+ return;
46
+ el.createSession = hasCreate ? (request) => latest.current.createSession(request) : null;
47
+ }, [hasCreate]);
48
+ const setNode = useCallback((el) => {
49
+ node.current = el;
50
+ assignRef(ref, el);
51
+ }, [ref]);
52
+ const href = fallbackHref(origin, props.checkout, props.session);
53
+ const children = props.children !== undefined ? props.children : href ? createElement('a', { href }, words) : null;
54
+ return createElement(tag, {
55
+ ...elementAttributes(props),
56
+ ref: setNode,
57
+ id: props.id,
58
+ style: props.style,
59
+ [CLASS_PROP]: props.className,
60
+ suppressHydrationWarning: true,
61
+ }, children);
62
+ }
63
+ /** The payment widget or pricing table in your page. */
64
+ export const RailbedCheckout = forwardRef(function RailbedCheckout(props, ref) {
65
+ return useRailbedElement('railbed-checkout', props, ref, 'Open the checkout');
66
+ });
67
+ /** A buy button that opens the checkout over your page (a full-screen sheet on phones). */
68
+ export const RailbedButton = forwardRef(function RailbedButton(props, ref) {
69
+ return useRailbedElement('railbed-button', props, ref, props.label || (props.checkout ? 'Buy now' : 'Pay now'));
70
+ });
package/dist/vue.d.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { type PropType, type SlotsType, type VNodeChild } from 'vue';
2
+ import { type CreateSession, type RailbedEvent, type RailbedEventDetail } from './index.js';
3
+ export type { CreateSession, OpenResult, RailbedEvent, RailbedEventDetail, RailbedEventName, RailbedStep, SessionAnswer, SessionRequest, } from './index.js';
4
+ export { DEFAULT_ORIGIN, loadRailbed, openCheckout } from './index.js';
5
+ type Handler = (detail: RailbedEventDetail, event: RailbedEvent) => true;
6
+ type Slots = SlotsType<{
7
+ default?: () => VNodeChild;
8
+ }>;
9
+ /** The payment widget or pricing table in your page. */
10
+ export declare const RailbedCheckout: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
11
+ /** The checkout's slug, from its Share tab. */
12
+ readonly checkout: StringConstructor;
13
+ /** A session your server already made (`pay_…`). */
14
+ readonly session: StringConstructor;
15
+ /** Your endpoint that makes a session when the buyer presses Continue (POST, same site, JSON). */
16
+ readonly sessionEndpoint: StringConstructor;
17
+ /** Makes the session instead of `sessionEndpoint`. */
18
+ readonly createSession: PropType<CreateSession>;
19
+ /** Fills in the buyer's email. Sent to the checkout by message, never in a URL. */
20
+ readonly email: StringConstructor;
21
+ /** Default https://pay.railbed.io. */
22
+ readonly origin: {
23
+ readonly type: StringConstructor;
24
+ readonly default: "https://pay.railbed.io";
25
+ };
26
+ /** For a Content Security Policy that uses nonces. */
27
+ readonly nonce: StringConstructor;
28
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
29
+ [key: string]: any;
30
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
31
+ ready: Handler;
32
+ step: Handler;
33
+ handoff: Handler;
34
+ paid: Handler;
35
+ held: Handler;
36
+ failed: Handler;
37
+ expired: Handler;
38
+ error: Handler;
39
+ close: Handler;
40
+ /** embed.js couldn't load (blocked, offline): the plain link is still there. */
41
+ 'load-error': (error: Error) => true;
42
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
43
+ /** The checkout's slug, from its Share tab. */
44
+ readonly checkout: StringConstructor;
45
+ /** A session your server already made (`pay_…`). */
46
+ readonly session: StringConstructor;
47
+ /** Your endpoint that makes a session when the buyer presses Continue (POST, same site, JSON). */
48
+ readonly sessionEndpoint: StringConstructor;
49
+ /** Makes the session instead of `sessionEndpoint`. */
50
+ readonly createSession: PropType<CreateSession>;
51
+ /** Fills in the buyer's email. Sent to the checkout by message, never in a URL. */
52
+ readonly email: StringConstructor;
53
+ /** Default https://pay.railbed.io. */
54
+ readonly origin: {
55
+ readonly type: StringConstructor;
56
+ readonly default: "https://pay.railbed.io";
57
+ };
58
+ /** For a Content Security Policy that uses nonces. */
59
+ readonly nonce: StringConstructor;
60
+ }>> & Readonly<{
61
+ onReady?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
62
+ onStep?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
63
+ onHandoff?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
64
+ onPaid?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
65
+ onHeld?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
66
+ onFailed?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
67
+ onExpired?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
68
+ onError?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
69
+ onClose?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
70
+ "onLoad-error"?: ((error: Error) => any) | undefined;
71
+ }>, {
72
+ readonly origin: string;
73
+ }, Slots, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
74
+ /** A buy button that opens the checkout over your page (a full-screen sheet on phones). */
75
+ export declare const RailbedButton: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
76
+ /** The button's text. Default "Buy now" ("Pay now" for a session alone). */
77
+ readonly label: StringConstructor;
78
+ readonly shape: PropType<"pill" | "rounded">;
79
+ /** The checkout's slug, from its Share tab. */
80
+ readonly checkout: StringConstructor;
81
+ /** A session your server already made (`pay_…`). */
82
+ readonly session: StringConstructor;
83
+ /** Your endpoint that makes a session when the buyer presses Continue (POST, same site, JSON). */
84
+ readonly sessionEndpoint: StringConstructor;
85
+ /** Makes the session instead of `sessionEndpoint`. */
86
+ readonly createSession: PropType<CreateSession>;
87
+ /** Fills in the buyer's email. Sent to the checkout by message, never in a URL. */
88
+ readonly email: StringConstructor;
89
+ /** Default https://pay.railbed.io. */
90
+ readonly origin: {
91
+ readonly type: StringConstructor;
92
+ readonly default: "https://pay.railbed.io";
93
+ };
94
+ /** For a Content Security Policy that uses nonces. */
95
+ readonly nonce: StringConstructor;
96
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
97
+ [key: string]: any;
98
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
99
+ ready: Handler;
100
+ step: Handler;
101
+ handoff: Handler;
102
+ paid: Handler;
103
+ held: Handler;
104
+ failed: Handler;
105
+ expired: Handler;
106
+ error: Handler;
107
+ close: Handler;
108
+ /** embed.js couldn't load (blocked, offline): the plain link is still there. */
109
+ 'load-error': (error: Error) => true;
110
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
111
+ /** The button's text. Default "Buy now" ("Pay now" for a session alone). */
112
+ readonly label: StringConstructor;
113
+ readonly shape: PropType<"pill" | "rounded">;
114
+ /** The checkout's slug, from its Share tab. */
115
+ readonly checkout: StringConstructor;
116
+ /** A session your server already made (`pay_…`). */
117
+ readonly session: StringConstructor;
118
+ /** Your endpoint that makes a session when the buyer presses Continue (POST, same site, JSON). */
119
+ readonly sessionEndpoint: StringConstructor;
120
+ /** Makes the session instead of `sessionEndpoint`. */
121
+ readonly createSession: PropType<CreateSession>;
122
+ /** Fills in the buyer's email. Sent to the checkout by message, never in a URL. */
123
+ readonly email: StringConstructor;
124
+ /** Default https://pay.railbed.io. */
125
+ readonly origin: {
126
+ readonly type: StringConstructor;
127
+ readonly default: "https://pay.railbed.io";
128
+ };
129
+ /** For a Content Security Policy that uses nonces. */
130
+ readonly nonce: StringConstructor;
131
+ }>> & Readonly<{
132
+ onReady?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
133
+ onStep?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
134
+ onHandoff?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
135
+ onPaid?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
136
+ onHeld?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
137
+ onFailed?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
138
+ onExpired?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
139
+ onError?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
140
+ onClose?: ((detail: RailbedEventDetail, event: RailbedEvent) => any) | undefined;
141
+ "onLoad-error"?: ((error: Error) => any) | undefined;
142
+ }>, {
143
+ readonly origin: string;
144
+ }, Slots, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
145
+ /** The event names, for anyone wiring listeners by hand. */
146
+ export declare const EVENTS: readonly ["ready", "step", "handoff", "paid", "held", "failed", "expired", "error", "close"];
package/dist/vue.js ADDED
@@ -0,0 +1,98 @@
1
+ // Vue 3 components for Railbed's embedded checkout: <RailbedCheckout> (the payment widget or pricing table in the page)
2
+ // and <RailbedButton> (a buy button that opens it over the page). Same props as the React components; events are
3
+ // emitted as `ready`, `step`, `handoff`, `paid`, `held`, `failed`, `expired`, `error`, `close` with (detail, event).
4
+ // class and style fall through to the element, so the --railbed-* variables work from either.
5
+ // Reference: https://railbed.com/docs/guides/embedded-checkout/
6
+ import { defineComponent, h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
7
+ import { DEFAULT_ORIGIN, elementAttributes, fallbackHref, listen, loadRailbed, railbedOrigin, RAILBED_EVENTS, } from './index.js';
8
+ export { DEFAULT_ORIGIN, loadRailbed, openCheckout } from './index.js';
9
+ const props = {
10
+ /** The checkout's slug, from its Share tab. */
11
+ checkout: String,
12
+ /** A session your server already made (`pay_…`). */
13
+ session: String,
14
+ /** Your endpoint that makes a session when the buyer presses Continue (POST, same site, JSON). */
15
+ sessionEndpoint: String,
16
+ /** Makes the session instead of `sessionEndpoint`. */
17
+ createSession: Function,
18
+ /** Fills in the buyer's email. Sent to the checkout by message, never in a URL. */
19
+ email: String,
20
+ /** Default https://pay.railbed.io. */
21
+ origin: { type: String, default: DEFAULT_ORIGIN },
22
+ /** For a Content Security Policy that uses nonces. */
23
+ nonce: String,
24
+ };
25
+ const buttonProps = {
26
+ ...props,
27
+ /** The button's text. Default "Buy now" ("Pay now" for a session alone). */
28
+ label: String,
29
+ shape: String,
30
+ };
31
+ const emits = {
32
+ ready: (() => true),
33
+ step: (() => true),
34
+ handoff: (() => true),
35
+ paid: (() => true),
36
+ held: (() => true),
37
+ failed: (() => true),
38
+ expired: (() => true),
39
+ error: (() => true),
40
+ close: (() => true),
41
+ /** embed.js couldn't load (blocked, offline): the plain link is still there. */
42
+ 'load-error': ((_error) => true),
43
+ };
44
+ /** The shared part of both components: load embed.js, forward events, keep createSession on the element, render. */
45
+ function useRailbed(tag, p, emit, slots, words) {
46
+ const el = ref(null);
47
+ let off = null;
48
+ let live = true;
49
+ const load = () => loadRailbed({ origin: p.origin, nonce: p.nonce }).catch((err) => {
50
+ if (live)
51
+ emit('load-error', err);
52
+ });
53
+ // One stable function while a createSession prop is given; it calls whatever the prop is when the buyer continues.
54
+ const hook = () => {
55
+ if (!el.value)
56
+ return;
57
+ el.value.createSession = typeof p.createSession === 'function' ? (request) => p.createSession(request) : null;
58
+ };
59
+ onMounted(() => {
60
+ if (el.value)
61
+ off = listen(el.value, (name, event) => emit(name, event.detail, event));
62
+ hook();
63
+ load();
64
+ });
65
+ watch(() => typeof p.createSession === 'function', hook);
66
+ watch(() => p.origin, load);
67
+ onBeforeUnmount(() => {
68
+ live = false;
69
+ off?.();
70
+ });
71
+ return () => {
72
+ const href = fallbackHref(railbedOrigin(p.origin), p.checkout, p.session);
73
+ const inside = slots.default ? slots.default() : href ? h('a', { href }, words()) : null;
74
+ return h(tag, { ...elementAttributes(p), ref: el }, inside ?? undefined);
75
+ };
76
+ }
77
+ /** The payment widget or pricing table in your page. */
78
+ export const RailbedCheckout = defineComponent({
79
+ name: 'RailbedCheckout',
80
+ props,
81
+ emits,
82
+ slots: Object,
83
+ setup(p, { emit, slots }) {
84
+ return useRailbed('railbed-checkout', p, emit, slots, () => 'Open the checkout');
85
+ },
86
+ });
87
+ /** A buy button that opens the checkout over your page (a full-screen sheet on phones). */
88
+ export const RailbedButton = defineComponent({
89
+ name: 'RailbedButton',
90
+ props: buttonProps,
91
+ emits,
92
+ slots: Object,
93
+ setup(p, { emit, slots }) {
94
+ return useRailbed('railbed-button', p, emit, slots, () => p.label || (p.checkout ? 'Buy now' : 'Pay now'));
95
+ },
96
+ });
97
+ /** The event names, for anyone wiring listeners by hand. */
98
+ export const EVENTS = RAILBED_EVENTS;
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@railbed/embed",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Railbed's embedded checkout for React, Vue and any page: the payment widget, pricing table and buy button, with typed events.",
5
+ "license": "MIT",
6
+ "homepage": "https://railbed.com/docs/guides/embedded-checkout/",
7
+ "keywords": [
8
+ "railbed",
9
+ "checkout",
10
+ "payments",
11
+ "embed",
12
+ "react",
13
+ "vue"
14
+ ],
15
+ "author": {
16
+ "name": "Railbed by MediaGlobe",
17
+ "email": "support@railbed.com",
18
+ "url": "https://railbed.com"
19
+ },
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./react": {
31
+ "types": "./dist/react.d.ts",
32
+ "import": "./dist/react.js",
33
+ "default": "./dist/react.js"
34
+ },
35
+ "./vue": {
36
+ "types": "./dist/vue.d.ts",
37
+ "import": "./dist/vue.js",
38
+ "default": "./dist/vue.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "typesVersions": {
43
+ "*": {
44
+ "react": [
45
+ "dist/react.d.ts"
46
+ ],
47
+ "vue": [
48
+ "dist/vue.d.ts"
49
+ ]
50
+ }
51
+ },
52
+ "files": [
53
+ "dist",
54
+ "!dist/*.tsbuildinfo"
55
+ ],
56
+ "scripts": {
57
+ "build": "rm -rf dist && tsc -p tsconfig.json",
58
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
59
+ "test": "vitest run",
60
+ "prepublishOnly": "npm run build && npm test"
61
+ },
62
+ "peerDependencies": {
63
+ "react": ">=18",
64
+ "vue": ">=3.3"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "react": {
68
+ "optional": true
69
+ },
70
+ "vue": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "devDependencies": {
75
+ "@types/react": "^19.2.0",
76
+ "jsdom": "^27.0.0",
77
+ "react": "^19.2.0",
78
+ "react-dom": "^19.2.0",
79
+ "typescript": "~5.9.3",
80
+ "vitest": "^4.1.12",
81
+ "vue": "^3.5.0"
82
+ }
83
+ }