@relai-fi/subscriptions-react 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RelAI
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 ADDED
@@ -0,0 +1,171 @@
1
+ # @relai-fi/subscriptions-react
2
+
3
+ Drop-in **React UI** for [RelAI subscriptions](https://relai.fi/documentation/subscriptions) — a
4
+ Stripe-style pricing table and subscribe button for **recurring USDC billing on Solana**.
5
+
6
+ - **One component** — `<PricingTable>` fetches your plans, renders cards, and runs the subscribe flow.
7
+ - **No API key in the browser** — uses only RelAI's public endpoints (plan terms, status, subscribe).
8
+ - **Wallet-agnostic** — pass a `signAndSend`, or use the built-in `@solana/wallet-adapter` helper.
9
+ - **Themeable** — ship the stylesheet, override a few CSS variables, done.
10
+
11
+ ```bash
12
+ npm install @relai-fi/subscriptions-react
13
+ ```
14
+
15
+ > Peer deps: `react`/`react-dom` (≥18). The wallet helper additionally needs
16
+ > `@solana/wallet-adapter-react` + `@solana/web3.js` (optional — skip them if you bring your own `signAndSend`).
17
+
18
+ Create and price your plans first with the [`@relai-fi/subscriptions`](https://relai.fi/documentation/subscriptions)
19
+ SDK or the dashboard, then drop their `planId`s into the table below.
20
+
21
+ ---
22
+
23
+ ## Quick start
24
+
25
+ Import the stylesheet once (e.g. in your root layout), then render the table.
26
+
27
+ ```tsx
28
+ import "@relai-fi/subscriptions-react/styles.css";
29
+ import { PricingTable } from "@relai-fi/subscriptions-react";
30
+ import { useRelaiSignAndSend, useRelaiWallet } from "@relai-fi/subscriptions-react/wallet";
31
+
32
+ export function Pricing() {
33
+ const wallet = useRelaiWallet(); // from your <WalletProvider>
34
+ const signAndSend = useRelaiSignAndSend();
35
+
36
+ return (
37
+ <PricingTable
38
+ planIds={["pl_basic", "pl_pro"]}
39
+ highlightPlanId="pl_pro"
40
+ wallet={wallet}
41
+ signAndSend={signAndSend}
42
+ cards={{
43
+ pl_basic: { description: "For side projects", features: ["1 project", "Community support"] },
44
+ pl_pro: { description: "For teams", features: ["Unlimited projects", "Webhooks", "Priority support"] },
45
+ }}
46
+ onSubscribed={(sub) => console.log("active:", sub.subscriptionId)}
47
+ />
48
+ );
49
+ }
50
+ ```
51
+
52
+ That renders a pricing table, and clicking **Subscribe** runs the full on-chain flow
53
+ (prepare → sign → confirm) against the subscriber's wallet. Cards the wallet already
54
+ holds show **Subscribed** automatically.
55
+
56
+ ### Not using wallet-adapter?
57
+
58
+ Pass your own `signAndSend` — anything that signs + broadcasts a base64 Solana tx and
59
+ returns the signature (Privy, Crossmint, a backend signer, a raw `Keypair`…):
60
+
61
+ ```tsx
62
+ <PricingTable
63
+ planId="pl_pro"
64
+ wallet={address}
65
+ signAndSend={async (txBase64) => {
66
+ const sig = await myWallet.signAndSendBase64(txBase64);
67
+ return sig; // transaction signature
68
+ }}
69
+ />
70
+ ```
71
+
72
+ ### Skip the props with a provider
73
+
74
+ ```tsx
75
+ import { RelaiProvider } from "@relai-fi/subscriptions-react";
76
+
77
+ <RelaiProvider wallet={wallet} signAndSend={signAndSend} theme={{ primary: "#7c3aed" }}>
78
+ <PricingTable planIds={["pl_basic", "pl_pro"]} highlightPlanId="pl_pro" />
79
+ </RelaiProvider>
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Components
85
+
86
+ ### `<PricingTable>`
87
+
88
+ | Prop | Type | Notes |
89
+ |---|---|---|
90
+ | `planIds` | `string[]` | Plans to show, in order. Or use `plans` / `planId`. |
91
+ | `plans` | `PlanMeta[]` | Preloaded plan metadata (skips fetching). |
92
+ | `planId` | `string` | Shorthand for a one-plan table. |
93
+ | `cards` | `Record<planId, { features?, description?, highlight?, badge? }>` | Per-plan presentation. |
94
+ | `highlightPlanId` | `string` | Emphasize one plan (adds a "Popular" ribbon). |
95
+ | `wallet` | `string` | Connected subscriber address. |
96
+ | `signAndSend` | `SignAndSend` | Signs + sends the wire tx. |
97
+ | `theme` | `RelaiTheme` | CSS-variable overrides. |
98
+ | `baseUrl` | `string` | Defaults to `https://api.relai.fi`. |
99
+ | `onSubscribed` / `onError` / `onConnectWallet` | callbacks | |
100
+ | `labels` | `SubscribeButtonLabels` | Button copy overrides. |
101
+
102
+ ### `<PricingCard>`
103
+
104
+ One card for a `PlanMeta` you already have. Same subscribe props as the button, plus
105
+ `features`, `description`, `highlight`, `badge`.
106
+
107
+ ### `<SubscribeButton>`
108
+
109
+ The atomic piece — a single button for a `planId` that resolves wallet / `signAndSend`
110
+ from props or `<RelaiProvider>`, runs the flow, and reflects live state
111
+ (`Preparing… → Confirm in wallet… → Activating… → Subscribed`).
112
+
113
+ ```tsx
114
+ <SubscribeButton planId="pl_pro" wallet={address} signAndSend={signAndSend} />
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Hooks
120
+
121
+ For fully custom UI:
122
+
123
+ ```tsx
124
+ import { usePlanMeta, useSubscriptionStatus, useSubscribe, createRelaiClient } from "@relai-fi/subscriptions-react";
125
+
126
+ const client = createRelaiClient(); // public endpoints, no key
127
+ const { plan } = usePlanMeta("pl_pro", client);
128
+ const { status } = useSubscriptionStatus("pl_pro", wallet, client);
129
+ const { subscribe, state, busy, error } = useSubscribe({ planId: "pl_pro", wallet, signAndSend });
130
+ ```
131
+
132
+ `state`: `idle | preparing | signing | confirming | done | error`.
133
+
134
+ ---
135
+
136
+ ## Theming
137
+
138
+ Set the `theme` prop (mapped to CSS variables on the component root) or override the
139
+ variables in your own CSS. Defaults to the RelAI electric-violet palette, light surface.
140
+
141
+ ```tsx
142
+ <PricingTable planIds={ids} theme={{ primary: "#7c3aed", radius: "20px", card: "#0f1117", foreground: "#fff" }} />
143
+ ```
144
+
145
+ ```css
146
+ .relai-root {
147
+ --relai-primary: #7c3aed;
148
+ --relai-card: #ffffff;
149
+ --relai-fg: #0c0e16;
150
+ --relai-muted-fg: #6b7280;
151
+ --relai-border: #e7e8ee;
152
+ --relai-radius: 16px;
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## How it works
159
+
160
+ All calls hit RelAI's **public** subscription endpoints — no service key is exposed to the browser:
161
+
162
+ - `GET /s/:planId/meta` — plan terms for the card
163
+ - `GET /s/:planId/status?wallet=…` — drives the "Subscribed" state
164
+ - `POST /s/:planId/subscribe` → `POST /s/:planId/confirm` — the two-stage, wallet-signed subscribe
165
+
166
+ The subscriber signs at most two transactions: a one-time delegate-authority init
167
+ (first subscribe per wallet), then the subscribe itself. RelAI fee-pays the recurring pulls.
168
+
169
+ ## License
170
+
171
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,456 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/context.tsx
7
+
8
+ // src/client.ts
9
+ var DEFAULT_BASE_URL = "https://api.relai.fi";
10
+ var RelaiApiError = class extends Error {
11
+ constructor(status, message, body) {
12
+ super(message);
13
+ this.status = status;
14
+ this.body = body;
15
+ this.name = "RelaiApiError";
16
+ }
17
+ };
18
+ function createRelaiClient(opts = {}) {
19
+ const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
20
+ const _fetch = opts.fetch ?? globalThis.fetch;
21
+ if (!_fetch) throw new Error("No fetch available \u2014 pass `fetch` in options.");
22
+ async function request(method, path, init = {}) {
23
+ const url = new URL(baseUrl + path);
24
+ for (const [k, v] of Object.entries(init.query ?? {})) url.searchParams.set(k, v);
25
+ const res = await _fetch(url.toString(), {
26
+ method,
27
+ headers: { "Content-Type": "application/json" },
28
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
29
+ });
30
+ const text = await res.text();
31
+ let json;
32
+ try {
33
+ json = text ? JSON.parse(text) : void 0;
34
+ } catch {
35
+ json = text;
36
+ }
37
+ if (!res.ok) {
38
+ const msg = json?.error ?? `RelAI ${method} ${path} failed (${res.status})`;
39
+ throw new RelaiApiError(res.status, msg, json ?? text);
40
+ }
41
+ return json;
42
+ }
43
+ const enc = encodeURIComponent;
44
+ return {
45
+ baseUrl,
46
+ meta: (planId) => request("GET", `/s/${enc(planId)}/meta`),
47
+ status: (planId, wallet) => request("GET", `/s/${enc(planId)}/status`, { query: { wallet } }),
48
+ prepareSubscribe: (planId, wallet) => request("POST", `/s/${enc(planId)}/subscribe`, {
49
+ body: { subscriberWallet: wallet }
50
+ }),
51
+ confirmSubscribe: async (planId, wallet, signature) => {
52
+ const { subscription } = await request(
53
+ "POST",
54
+ `/s/${enc(planId)}/confirm`,
55
+ { body: { subscriberWallet: wallet, signature } }
56
+ );
57
+ return subscription;
58
+ }
59
+ };
60
+ }
61
+ var RelaiContext = react.createContext(null);
62
+ function RelaiProvider({
63
+ client,
64
+ baseUrl,
65
+ wallet,
66
+ signAndSend,
67
+ theme,
68
+ children
69
+ }) {
70
+ const value = react.useMemo(
71
+ () => ({
72
+ client: client ?? createRelaiClient({ baseUrl }),
73
+ wallet,
74
+ signAndSend,
75
+ theme
76
+ }),
77
+ [client, baseUrl, wallet, signAndSend, theme]
78
+ );
79
+ return /* @__PURE__ */ jsxRuntime.jsx(RelaiContext.Provider, { value, children });
80
+ }
81
+ function useRelaiContext() {
82
+ return react.useContext(RelaiContext);
83
+ }
84
+ function useRelaiResolved(opts = {}) {
85
+ const ctx = useRelaiContext();
86
+ return react.useMemo(() => {
87
+ const client = opts.client ?? ctx?.client ?? createRelaiClient({ baseUrl: opts.baseUrl });
88
+ return {
89
+ client,
90
+ wallet: opts.wallet ?? ctx?.wallet,
91
+ signAndSend: opts.signAndSend ?? ctx?.signAndSend
92
+ };
93
+ }, [opts.client, opts.baseUrl, opts.wallet, opts.signAndSend, ctx]);
94
+ }
95
+ function usePlanMeta(planId, client) {
96
+ const [state, setState] = react.useState({ loading: true });
97
+ react.useEffect(() => {
98
+ let alive = true;
99
+ setState({ loading: true });
100
+ client.meta(planId).then((plan) => alive && setState({ plan, loading: false })).catch((error) => alive && setState({ loading: false, error }));
101
+ return () => {
102
+ alive = false;
103
+ };
104
+ }, [planId, client]);
105
+ return state;
106
+ }
107
+ function useSubscriptionStatus(planId, wallet, client) {
108
+ const [state, setState] = react.useState({
109
+ loading: false
110
+ });
111
+ const [nonce, setNonce] = react.useState(0);
112
+ const refetch = react.useCallback(() => setNonce((n) => n + 1), []);
113
+ react.useEffect(() => {
114
+ if (!wallet) {
115
+ setState({ loading: false, status: void 0 });
116
+ return;
117
+ }
118
+ let alive = true;
119
+ setState({ loading: true });
120
+ client.status(planId, wallet).then((status) => alive && setState({ status, loading: false })).catch((error) => alive && setState({ loading: false, error }));
121
+ return () => {
122
+ alive = false;
123
+ };
124
+ }, [planId, wallet, client, nonce]);
125
+ return { ...state, refetch };
126
+ }
127
+ function useSubscribe(opts) {
128
+ const { client, wallet, signAndSend } = useRelaiResolved(opts);
129
+ const [state, setState] = react.useState("idle");
130
+ const [error, setError] = react.useState();
131
+ const [subscription, setSubscription] = react.useState();
132
+ const running = react.useRef(false);
133
+ const reset = react.useCallback(() => {
134
+ setState("idle");
135
+ setError(void 0);
136
+ setSubscription(void 0);
137
+ }, []);
138
+ const subscribe = react.useCallback(async () => {
139
+ if (running.current) return void 0;
140
+ if (!wallet) {
141
+ const err = new Error("No wallet \u2014 connect a wallet before subscribing.");
142
+ setError(err);
143
+ setState("error");
144
+ opts.onError?.(err);
145
+ return void 0;
146
+ }
147
+ if (!signAndSend) {
148
+ const err = new Error("No signAndSend \u2014 pass one (see @relai-fi/subscriptions-react/wallet).");
149
+ setError(err);
150
+ setState("error");
151
+ opts.onError?.(err);
152
+ return void 0;
153
+ }
154
+ running.current = true;
155
+ setError(void 0);
156
+ try {
157
+ setState("preparing");
158
+ let prep = await client.prepareSubscribe(opts.planId, wallet);
159
+ if (prep.stage === "init-authority") {
160
+ setState("signing");
161
+ await signAndSend(prep.wireTransaction);
162
+ setState("preparing");
163
+ prep = await client.prepareSubscribe(opts.planId, wallet);
164
+ }
165
+ setState("signing");
166
+ const sig = await signAndSend(prep.wireTransaction);
167
+ setState("confirming");
168
+ const sub = await client.confirmSubscribe(opts.planId, wallet, sig);
169
+ setSubscription(sub);
170
+ setState("done");
171
+ opts.onSubscribed?.(sub);
172
+ return sub;
173
+ } catch (e) {
174
+ const err = e instanceof Error ? e : new Error(String(e));
175
+ setError(err);
176
+ setState("error");
177
+ opts.onError?.(err);
178
+ return void 0;
179
+ } finally {
180
+ running.current = false;
181
+ }
182
+ }, [client, wallet, signAndSend, opts]);
183
+ const busy = state === "preparing" || state === "signing" || state === "confirming";
184
+ return { subscribe, state, error, subscription, busy, reset };
185
+ }
186
+ var DEFAULT_LABELS = {
187
+ subscribe: "Subscribe",
188
+ connect: "Connect wallet",
189
+ preparing: "Preparing\u2026",
190
+ signing: "Confirm in wallet\u2026",
191
+ confirming: "Activating\u2026",
192
+ active: "Subscribed",
193
+ retry: "Try again"
194
+ };
195
+ function SubscribeButton(props) {
196
+ const { planId, onConnectWallet, hideStatus, className, style, children } = props;
197
+ const labels = { ...DEFAULT_LABELS, ...props.labels };
198
+ const { client, wallet } = useRelaiResolved(props);
199
+ const { status, refetch } = useSubscriptionStatus(
200
+ planId,
201
+ hideStatus ? void 0 : wallet,
202
+ client
203
+ );
204
+ const { subscribe, state, busy } = useSubscribe({
205
+ ...props,
206
+ onSubscribed: (sub) => {
207
+ refetch();
208
+ props.onSubscribed?.(sub);
209
+ }
210
+ });
211
+ const alreadyActive = !hideStatus && status?.active === true;
212
+ let label;
213
+ let disabled = false;
214
+ let dataState = state;
215
+ if (alreadyActive || state === "done") {
216
+ label = labels.active;
217
+ disabled = true;
218
+ dataState = "done";
219
+ } else if (!wallet) {
220
+ label = labels.connect;
221
+ disabled = !onConnectWallet;
222
+ } else if (state === "preparing") {
223
+ label = labels.preparing;
224
+ disabled = true;
225
+ } else if (state === "signing") {
226
+ label = labels.signing;
227
+ disabled = true;
228
+ } else if (state === "confirming") {
229
+ label = labels.confirming;
230
+ disabled = true;
231
+ } else if (state === "error") {
232
+ label = labels.retry;
233
+ } else {
234
+ label = labels.subscribe;
235
+ }
236
+ const onClick = () => {
237
+ if (busy || alreadyActive) return;
238
+ if (!wallet) {
239
+ onConnectWallet?.();
240
+ return;
241
+ }
242
+ void subscribe();
243
+ };
244
+ return /* @__PURE__ */ jsxRuntime.jsxs(
245
+ "button",
246
+ {
247
+ type: "button",
248
+ className: ["relai-btn", className].filter(Boolean).join(" "),
249
+ style,
250
+ "data-state": dataState,
251
+ "data-busy": busy ? "true" : void 0,
252
+ "aria-busy": busy || void 0,
253
+ disabled,
254
+ onClick,
255
+ children: [
256
+ busy && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relai-spinner", "aria-hidden": "true" }),
257
+ children ?? label
258
+ ]
259
+ }
260
+ );
261
+ }
262
+
263
+ // src/format.ts
264
+ var USDC_DECIMALS = 6;
265
+ function formatUsdc(amountBaseUnits, opts = {}) {
266
+ const decimals = opts.decimals ?? USDC_DECIMALS;
267
+ const n = Number(amountBaseUnits) / 10 ** decimals;
268
+ const dp = Number.isInteger(n * 100) ? 2 : Math.min(decimals, 6);
269
+ const s = n.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: dp });
270
+ return opts.withTicker ? `${s} USDC` : s;
271
+ }
272
+ function formatPeriodSuffix(periodHours) {
273
+ switch (periodHours) {
274
+ case 1:
275
+ return "/hr";
276
+ case 24:
277
+ return "/day";
278
+ case 168:
279
+ return "/wk";
280
+ case 720:
281
+ return "/mo";
282
+ case 8760:
283
+ return "/yr";
284
+ default:
285
+ return periodHours % 24 === 0 ? `/${periodHours / 24}d` : `/${periodHours}h`;
286
+ }
287
+ }
288
+ function formatPeriodLabel(periodHours) {
289
+ const map = {
290
+ 1: "hourly",
291
+ 24: "daily",
292
+ 168: "weekly",
293
+ 720: "monthly",
294
+ 8760: "yearly"
295
+ };
296
+ const known = map[periodHours];
297
+ if (known) return `Billed ${known}`;
298
+ if (periodHours % 24 === 0) return `Billed every ${periodHours / 24} days`;
299
+ return `Billed every ${periodHours} hours`;
300
+ }
301
+ function formatNetwork(network) {
302
+ return network === "solana-devnet" ? "Solana devnet" : "Solana";
303
+ }
304
+ var Check = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "relai-check", viewBox: "0 0 20 20", width: "18", height: "18", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
305
+ "path",
306
+ {
307
+ d: "M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0L3.3 9.7a1 1 0 1 1 1.4-1.4l3.3 3.29 6.8-6.8a1 1 0 0 1 1.4 0Z",
308
+ fill: "currentColor"
309
+ }
310
+ ) });
311
+ function PricingCard(props) {
312
+ const { plan, features, description, highlight, hideNetwork, className, style } = props;
313
+ const badge = props.badge ?? (highlight ? "Popular" : void 0);
314
+ return /* @__PURE__ */ jsxRuntime.jsxs(
315
+ "div",
316
+ {
317
+ className: ["relai-card", highlight ? "relai-card--highlight" : "", className].filter(Boolean).join(" "),
318
+ style,
319
+ "data-plan": plan.planId,
320
+ children: [
321
+ badge && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relai-badge", children: badge }),
322
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relai-card__head", children: [
323
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "relai-card__name", children: plan.name }),
324
+ description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "relai-card__desc", children: description })
325
+ ] }),
326
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relai-price", children: [
327
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relai-price__amount", children: formatUsdc(plan.amountBaseUnits) }),
328
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relai-price__unit", children: [
329
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relai-price__ticker", children: "USDC" }),
330
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relai-price__period", children: formatPeriodSuffix(plan.periodHours) })
331
+ ] })
332
+ ] }),
333
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "relai-price__caption", children: [
334
+ formatPeriodLabel(plan.periodHours),
335
+ !hideNetwork && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
336
+ " \xB7 ",
337
+ formatNetwork(plan.network)
338
+ ] })
339
+ ] }),
340
+ /* @__PURE__ */ jsxRuntime.jsx(
341
+ SubscribeButton,
342
+ {
343
+ planId: plan.planId,
344
+ client: props.client,
345
+ baseUrl: props.baseUrl,
346
+ wallet: props.wallet,
347
+ signAndSend: props.signAndSend,
348
+ onConnectWallet: props.onConnectWallet,
349
+ onSubscribed: props.onSubscribed,
350
+ onError: props.onError,
351
+ labels: props.labels
352
+ }
353
+ ),
354
+ features && features.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "relai-features", children: features.map((f, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "relai-features__item", children: [
355
+ /* @__PURE__ */ jsxRuntime.jsx(Check, {}),
356
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: f })
357
+ ] }, i)) })
358
+ ]
359
+ }
360
+ );
361
+ }
362
+
363
+ // src/theme.ts
364
+ function themeToCssVars(theme) {
365
+ if (!theme) return {};
366
+ const v = {};
367
+ if (theme.primary) v["--relai-primary"] = theme.primary;
368
+ if (theme.primaryForeground) v["--relai-primary-fg"] = theme.primaryForeground;
369
+ if (theme.background) v["--relai-bg"] = theme.background;
370
+ if (theme.card) v["--relai-card"] = theme.card;
371
+ if (theme.foreground) v["--relai-fg"] = theme.foreground;
372
+ if (theme.mutedForeground) v["--relai-muted-fg"] = theme.mutedForeground;
373
+ if (theme.border) v["--relai-border"] = theme.border;
374
+ if (theme.radius) v["--relai-radius"] = theme.radius;
375
+ return v;
376
+ }
377
+ function useManyPlanMetas(ids, preloaded, client) {
378
+ const key = ids.join(",");
379
+ const [state, setState] = react.useState(
380
+ () => ({ plans: preloaded ?? [], loading: !preloaded })
381
+ );
382
+ react.useEffect(() => {
383
+ if (preloaded) {
384
+ setState({ plans: preloaded, loading: false });
385
+ return;
386
+ }
387
+ if (ids.length === 0) {
388
+ setState({ plans: [], loading: false });
389
+ return;
390
+ }
391
+ let alive = true;
392
+ setState({ plans: [], loading: true });
393
+ Promise.all(ids.map((id) => client.meta(id))).then((plans) => alive && setState({ plans, loading: false })).catch((error) => alive && setState({ plans: [], loading: false, error }));
394
+ return () => {
395
+ alive = false;
396
+ };
397
+ }, [key, preloaded, client]);
398
+ return state;
399
+ }
400
+ function PricingTable(props) {
401
+ const { client, wallet, signAndSend } = useRelaiResolved(props);
402
+ const ctx = useRelaiContext();
403
+ const theme = props.theme ?? ctx?.theme;
404
+ const ids = props.plans ? props.plans.map((p) => p.planId) : props.planIds ?? (props.planId ? [props.planId] : []);
405
+ const { plans, loading, error } = useManyPlanMetas(ids, props.plans, client);
406
+ const rootStyle = { ...themeToCssVars(theme), ...props.style };
407
+ const rootClass = ["relai-root", "relai-table", props.className].filter(Boolean).join(" ");
408
+ if (loading) {
409
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: rootClass, style: rootStyle, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relai-state relai-state--loading", children: props.loadingText ?? "Loading plans\u2026" }) });
410
+ }
411
+ if (error) {
412
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: rootClass, style: rootStyle, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relai-state relai-state--error", children: props.errorText ?? "Couldn't load plans." }) });
413
+ }
414
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: rootClass, style: rootStyle, "data-count": plans.length, children: plans.map((plan) => {
415
+ const cfg = props.cards?.[plan.planId];
416
+ const highlight = cfg?.highlight ?? props.highlightPlanId === plan.planId;
417
+ return /* @__PURE__ */ jsxRuntime.jsx(
418
+ PricingCard,
419
+ {
420
+ plan,
421
+ features: cfg?.features,
422
+ description: cfg?.description,
423
+ highlight,
424
+ badge: cfg?.badge,
425
+ client,
426
+ wallet,
427
+ signAndSend,
428
+ onConnectWallet: props.onConnectWallet,
429
+ onSubscribed: props.onSubscribed,
430
+ onError: props.onError,
431
+ labels: props.labels
432
+ },
433
+ plan.planId
434
+ );
435
+ }) });
436
+ }
437
+
438
+ exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
439
+ exports.PricingCard = PricingCard;
440
+ exports.PricingTable = PricingTable;
441
+ exports.RelaiApiError = RelaiApiError;
442
+ exports.RelaiProvider = RelaiProvider;
443
+ exports.SubscribeButton = SubscribeButton;
444
+ exports.createRelaiClient = createRelaiClient;
445
+ exports.formatNetwork = formatNetwork;
446
+ exports.formatPeriodLabel = formatPeriodLabel;
447
+ exports.formatPeriodSuffix = formatPeriodSuffix;
448
+ exports.formatUsdc = formatUsdc;
449
+ exports.themeToCssVars = themeToCssVars;
450
+ exports.usePlanMeta = usePlanMeta;
451
+ exports.useRelaiContext = useRelaiContext;
452
+ exports.useRelaiResolved = useRelaiResolved;
453
+ exports.useSubscribe = useSubscribe;
454
+ exports.useSubscriptionStatus = useSubscriptionStatus;
455
+ //# sourceMappingURL=index.cjs.map
456
+ //# sourceMappingURL=index.cjs.map