@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 +21 -0
- package/README.md +171 -0
- package/dist/index.cjs +456 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +208 -0
- package/dist/index.d.ts +208 -0
- package/dist/index.js +438 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +214 -0
- package/dist/types-CTW_egjj.d.cts +19 -0
- package/dist/types-CTW_egjj.d.ts +19 -0
- package/dist/wallet.cjs +44 -0
- package/dist/wallet.cjs.map +1 -0
- package/dist/wallet.d.cts +16 -0
- package/dist/wallet.d.ts +16 -0
- package/dist/wallet.js +41 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +79 -0
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
|