@mesub/react 0.1.0 → 0.1.3
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/README.md +27 -6
- package/dist/index.cjs +47 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +47 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/styles.css +10 -1
package/README.md
CHANGED
|
@@ -16,9 +16,22 @@ part in the browser: picking one, connecting it, signing in the right order.
|
|
|
16
16
|
|
|
17
17
|
**[Read the docs](https://docs.mesub.io/docs/react)** for the walkthrough.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
>
|
|
19
|
+
[](https://www.npmjs.com/package/@mesub/react)
|
|
20
|
+
|
|
21
|
+
> **Status: early, 0.x.** The API may still change between minor versions:
|
|
22
|
+
> `npm update` takes patches (0.1.x), a new minor (0.2.0) takes
|
|
23
|
+
> `npm install @mesub/react@latest`. See the
|
|
24
|
+
> [board](https://github.com/orgs/Mesub-io/projects/5) for what is next.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install @mesub/react
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`react` and `react-dom` 18 or later are peer dependencies. Every release is
|
|
33
|
+
built by GitHub Actions from a tagged commit and published with a provenance
|
|
34
|
+
statement: `npm audit signatures` verifies it.
|
|
22
35
|
|
|
23
36
|
## Requirements
|
|
24
37
|
|
|
@@ -57,7 +70,7 @@ export function App({ children }) {
|
|
|
57
70
|
| Prop | What it is |
|
|
58
71
|
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
59
72
|
| `endpoint` | Where you mounted the routes. A path on your site, or a full URL. |
|
|
60
|
-
| `chain` | The network the wallet signs for: `solana:
|
|
73
|
+
| `chain` | The network the wallet signs for: `solana:mainnet` (the default, where Mesub runs). It must match your plans' `network`. |
|
|
61
74
|
| `fetch` | Your own `(url, init) => Promise<Response>`, to add headers or credentials. |
|
|
62
75
|
| `theme` | `light`, `dark` or `auto`, set on the widget as `data-mesub-theme`. |
|
|
63
76
|
| `manageUrl` | Where "Cancel any time" leads once subscribed: your own page (`/account`, or a full URL). Mesub's page by default; `null` for no button. |
|
|
@@ -88,8 +101,10 @@ import { SubscribeButton } from '@mesub/react';
|
|
|
88
101
|
dialog, from the plan to the receipt:
|
|
89
102
|
|
|
90
103
|
1. **The plan**: its name, price and period, from `GET /plans/:slug`, and its end
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
when it has one. A plan ends on any second, so its end is always written
|
|
105
|
+
with its time, in UTC, the same for every subscriber: `Oct 20, 2026, 23:59 UTC`.
|
|
106
|
+
A plan that ends before a second charge can run is priced `once`, with the
|
|
107
|
+
end its access runs to.
|
|
93
108
|
2. **A wallet**: the installed wallets that can sign
|
|
94
109
|
([Wallet Standard](https://github.com/wallet-standard/wallet-standard), so
|
|
95
110
|
Phantom, Solflare and the others). The one picked is asked for its account.
|
|
@@ -199,6 +214,9 @@ nor past its date to pay by. What came of it is said under the button: the
|
|
|
199
214
|
payment on its way, or why not (a short wallet spends no retry). On Free,
|
|
200
215
|
where Mesub does not try again on its own, the card shows that date to pay by.
|
|
201
216
|
|
|
217
|
+
When the server could not read a subscription's past payments, its window
|
|
218
|
+
says so in one line rather than show none.
|
|
219
|
+
|
|
202
220
|
One whose charges are held back for a cause that is not the customer's
|
|
203
221
|
(`blocked_since`) says so in one line: payments are on hold, nothing is asked
|
|
204
222
|
of them, and until when their access runs. The cause is never named to them,
|
|
@@ -230,6 +248,9 @@ function Mine() {
|
|
|
230
248
|
- `subscriptions` are newest first, each with `action`: `cancel`, `resume`,
|
|
231
249
|
`close` or null, and `payable`: whether **Pay now** can succeed. Checkouts
|
|
232
250
|
nobody signed are left out.
|
|
251
|
+
- `hasMore` is true when the customer holds more than the server listed (its
|
|
252
|
+
500 newest): older ones are missing. `ManageSubscriptions` says so under its
|
|
253
|
+
list.
|
|
233
254
|
- `payNow(id)` pays that late payment now and resolves `{ ok, message }`, the
|
|
234
255
|
words to show; it never throws.
|
|
235
256
|
- `manage(id)` opens the dialog for that action and resolves when it closes:
|
package/dist/index.cjs
CHANGED
|
@@ -71,6 +71,10 @@ function paidOf(answer) {
|
|
|
71
71
|
const whole = Array.isArray(answer.payments) && answer.payments.length < LISTED_AT_MOST;
|
|
72
72
|
return whole ? read(answer.listed_paid) : null;
|
|
73
73
|
}
|
|
74
|
+
function paymentsErrorOf(answer) {
|
|
75
|
+
const error = answer.payments_error;
|
|
76
|
+
return isObject(error) && typeof error.code === "string" && typeof error.message === "string" ? { code: error.code, message: error.message } : null;
|
|
77
|
+
}
|
|
74
78
|
function createApi(options) {
|
|
75
79
|
const base = options.endpoint.replace(/\/+$/, "");
|
|
76
80
|
const doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
@@ -141,7 +145,11 @@ function createApi(options) {
|
|
|
141
145
|
),
|
|
142
146
|
subscriptions: async () => {
|
|
143
147
|
const answer = await request("GET", "/subscriptions", void 0, REQUEST_TIMEOUT_MS);
|
|
144
|
-
return
|
|
148
|
+
return {
|
|
149
|
+
subscriptions: Array.isArray(answer.subscriptions) ? answer.subscriptions : [],
|
|
150
|
+
// An older server does not say: read as whole.
|
|
151
|
+
has_more: answer.has_more === true
|
|
152
|
+
};
|
|
145
153
|
},
|
|
146
154
|
subscription: async (id) => {
|
|
147
155
|
const answer = await request("GET", at(id), void 0, REQUEST_TIMEOUT_MS);
|
|
@@ -149,7 +157,8 @@ function createApi(options) {
|
|
|
149
157
|
subscription: answer.subscription,
|
|
150
158
|
upcoming: Array.isArray(answer.upcoming) ? answer.upcoming : [],
|
|
151
159
|
payments: Array.isArray(answer.payments) ? answer.payments : null,
|
|
152
|
-
paid: paidOf(answer)
|
|
160
|
+
paid: paidOf(answer),
|
|
161
|
+
payments_error: paymentsErrorOf(answer)
|
|
153
162
|
};
|
|
154
163
|
},
|
|
155
164
|
prepare: async (plan, wallet) => await request(
|
|
@@ -457,6 +466,14 @@ function moment(iso, now = Date.now()) {
|
|
|
457
466
|
function shortMoment(iso, now = Date.now()) {
|
|
458
467
|
return written(iso, { month: "short", day: "numeric" }, now);
|
|
459
468
|
}
|
|
469
|
+
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
470
|
+
var two = (n) => String(n).padStart(2, "0");
|
|
471
|
+
function planEnd(iso) {
|
|
472
|
+
if (!iso) return null;
|
|
473
|
+
const date = new Date(iso);
|
|
474
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
475
|
+
return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}, ${date.getUTCFullYear()}, ${two(date.getUTCHours())}:${two(date.getUTCMinutes())} UTC`;
|
|
476
|
+
}
|
|
460
477
|
function shortAddress(address) {
|
|
461
478
|
return `${address.slice(0, 4)}\u2026${address.slice(-4)}`;
|
|
462
479
|
}
|
|
@@ -1426,7 +1443,7 @@ function planScreen(loaded, titleId, reload, close) {
|
|
|
1426
1443
|
function stageScreen(props) {
|
|
1427
1444
|
const { view, plan, chain, titleId } = props;
|
|
1428
1445
|
const { text: price, title: exact } = briefPrice(plan);
|
|
1429
|
-
const ends =
|
|
1446
|
+
const ends = planEnd(plan.ends_at);
|
|
1430
1447
|
const single = view.name === "review" ? singleCharge(plan, view.prepared.terms.message) : view.name === "signing" || view.name === "subscribed" ? view.single : singleCharge(plan);
|
|
1431
1448
|
const every = single ? "once" : cadence(plan.period_hours);
|
|
1432
1449
|
const merchant = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
@@ -1626,7 +1643,7 @@ function stageScreen(props) {
|
|
|
1626
1643
|
};
|
|
1627
1644
|
}
|
|
1628
1645
|
const nextCharge = view.single ? null : moment(view.subscription.next_charge_at);
|
|
1629
|
-
const accessUntil = view.single ? moment(view.subscription.access_until)
|
|
1646
|
+
const accessUntil = view.single ? ends ?? moment(view.subscription.access_until) : null;
|
|
1630
1647
|
const manage = manageLink(props.manageUrl, view.cancelUrl);
|
|
1631
1648
|
return {
|
|
1632
1649
|
step: "subscribed",
|
|
@@ -1747,7 +1764,7 @@ function consequence(action, subscription, planEndsAt) {
|
|
|
1747
1764
|
}
|
|
1748
1765
|
if (action === "resume") {
|
|
1749
1766
|
if (end && until && noChargeAt(Date.parse(end), planEndsAt)) {
|
|
1750
|
-
return `It runs again until ${
|
|
1767
|
+
return `It runs again until ${planEnd(planEndsAt)}, when the plan ends. Nothing more is charged.`;
|
|
1751
1768
|
}
|
|
1752
1769
|
return until ? `Charges start again on ${until}, as before.` : "Charges start again, as before.";
|
|
1753
1770
|
}
|
|
@@ -2177,6 +2194,7 @@ function payableOf(row, now) {
|
|
|
2177
2194
|
const deadline = Date.parse(row.retry_deadline ?? "");
|
|
2178
2195
|
return Number.isNaN(deadline) || deadline > now;
|
|
2179
2196
|
}
|
|
2197
|
+
var MORE_NOT_LISTED = "Only your most recent subscriptions are listed here.";
|
|
2180
2198
|
var PAY_NOW_SENT = "Payment sent. It shows here once the network confirms it.";
|
|
2181
2199
|
function payNowRefusal(failure) {
|
|
2182
2200
|
if (!(failure instanceof MesubClientError)) return "Something went wrong.";
|
|
@@ -2202,6 +2220,7 @@ function useSubscriptions() {
|
|
|
2202
2220
|
const { api, revision, manage: open } = useMesubInternal();
|
|
2203
2221
|
const [state, setState] = (0, import_react9.useState)("loading");
|
|
2204
2222
|
const [rows, setRows] = (0, import_react9.useState)([]);
|
|
2223
|
+
const [hasMore, setHasMore] = (0, import_react9.useState)(false);
|
|
2205
2224
|
const [error, setError] = (0, import_react9.useState)(null);
|
|
2206
2225
|
const [now, setNow] = (0, import_react9.useState)(() => Date.now());
|
|
2207
2226
|
const read = (0, import_react9.useRef)(0);
|
|
@@ -2217,7 +2236,8 @@ function useSubscriptions() {
|
|
|
2217
2236
|
try {
|
|
2218
2237
|
const served = await api.subscriptions();
|
|
2219
2238
|
if (!alive.current || id !== read.current) return;
|
|
2220
|
-
setRows(served.filter((row) => HELD.has(row.status)));
|
|
2239
|
+
setRows(served.subscriptions.filter((row) => HELD.has(row.status)));
|
|
2240
|
+
setHasMore(served.has_more);
|
|
2221
2241
|
setNow(Date.now());
|
|
2222
2242
|
setError(null);
|
|
2223
2243
|
setState("ready");
|
|
@@ -2225,6 +2245,7 @@ function useSubscriptions() {
|
|
|
2225
2245
|
if (!alive.current || id !== read.current) return;
|
|
2226
2246
|
if (signedOut(failure)) {
|
|
2227
2247
|
setRows([]);
|
|
2248
|
+
setHasMore(false);
|
|
2228
2249
|
setError(null);
|
|
2229
2250
|
setState("signed-out");
|
|
2230
2251
|
return;
|
|
@@ -2278,7 +2299,7 @@ function useSubscriptions() {
|
|
|
2278
2299
|
},
|
|
2279
2300
|
[api, reload]
|
|
2280
2301
|
);
|
|
2281
|
-
return { state, subscriptions, error, reload, payNow, manage };
|
|
2302
|
+
return { state, subscriptions, hasMore, error, reload, payNow, manage };
|
|
2282
2303
|
}
|
|
2283
2304
|
var STATUS = {
|
|
2284
2305
|
active: "Active",
|
|
@@ -2410,7 +2431,7 @@ function PayNow({ pay }) {
|
|
|
2410
2431
|
}
|
|
2411
2432
|
function ManageSubscriptions({ onChanged, ...rest }) {
|
|
2412
2433
|
const { theme } = useMesubInternal();
|
|
2413
|
-
const { state, subscriptions, error, reload, payNow, manage } = useSubscriptions();
|
|
2434
|
+
const { state, subscriptions, hasMore, error, reload, payNow, manage } = useSubscriptions();
|
|
2414
2435
|
const plans = usePlans(subscriptions.flatMap((row) => row.plan ? [row.plan] : []));
|
|
2415
2436
|
const [retrying, setRetrying] = (0, import_react9.useState)(false);
|
|
2416
2437
|
async function act(id) {
|
|
@@ -2445,7 +2466,7 @@ function ManageSubscriptions({ onChanged, ...rest }) {
|
|
|
2445
2466
|
}
|
|
2446
2467
|
)
|
|
2447
2468
|
] });
|
|
2448
|
-
} else if (subscriptions.length === 0) {
|
|
2469
|
+
} else if (subscriptions.length === 0 && !hasMore) {
|
|
2449
2470
|
body = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { "data-mesub-empty": "none", children: "You have no subscription yet." });
|
|
2450
2471
|
} else {
|
|
2451
2472
|
body = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { "data-mesub-subscription-list": "", "aria-label": "Your subscriptions", children: subscriptions.map((row) => {
|
|
@@ -2507,14 +2528,17 @@ function ManageSubscriptions({ onChanged, ...rest }) {
|
|
|
2507
2528
|
] }, row.id);
|
|
2508
2529
|
}) });
|
|
2509
2530
|
}
|
|
2510
|
-
return /* @__PURE__ */ (0, import_jsx_runtime6.
|
|
2531
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
2511
2532
|
"div",
|
|
2512
2533
|
{
|
|
2513
2534
|
...rest,
|
|
2514
2535
|
"data-mesub-subscriptions": "",
|
|
2515
2536
|
"data-mesub-state": state,
|
|
2516
2537
|
"data-mesub-theme": theme,
|
|
2517
|
-
children:
|
|
2538
|
+
children: [
|
|
2539
|
+
body,
|
|
2540
|
+
state === "ready" && hasMore ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { "data-mesub-subscription-note": "", children: MORE_NOT_LISTED }) : null
|
|
2541
|
+
]
|
|
2518
2542
|
}
|
|
2519
2543
|
);
|
|
2520
2544
|
}
|
|
@@ -2579,6 +2603,7 @@ function useDetail(row) {
|
|
|
2579
2603
|
}, [api, id, revision]);
|
|
2580
2604
|
return detail?.subscription.id === id ? detail : null;
|
|
2581
2605
|
}
|
|
2606
|
+
var PAYMENTS_UNREAD = "Past payments could not be read just now.";
|
|
2582
2607
|
var PAST_SHOWN = 5;
|
|
2583
2608
|
var PAST_STEP = 10;
|
|
2584
2609
|
function SubscriptionDialog({
|
|
@@ -2588,7 +2613,7 @@ function SubscriptionDialog({
|
|
|
2588
2613
|
const [shown, setShown] = (0, import_react10.useState)(PAST_SHOWN);
|
|
2589
2614
|
const titleId = (0, import_react10.useId)();
|
|
2590
2615
|
const { chain } = useMesubInternal();
|
|
2591
|
-
const { state, subscriptions, error, reload, payNow, manage } = useSubscriptions();
|
|
2616
|
+
const { state, subscriptions, hasMore, error, reload, payNow, manage } = useSubscriptions();
|
|
2592
2617
|
const row = pickSubscription(subscriptions, slug) ?? null;
|
|
2593
2618
|
const plan = usePlan(row?.plan);
|
|
2594
2619
|
const detail = useDetail(row);
|
|
@@ -2612,7 +2637,7 @@ function SubscriptionDialog({
|
|
|
2612
2637
|
} else if (!row) {
|
|
2613
2638
|
body = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
2614
2639
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h2", { id: titleId, children: "No subscription yet" }),
|
|
2615
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: "You are not subscribed to this plan." })
|
|
2640
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: hasMore ? "None of your most recent subscriptions is to this plan." : "You are not subscribed to this plan." })
|
|
2616
2641
|
] });
|
|
2617
2642
|
} else {
|
|
2618
2643
|
const name = plan?.name ?? row.plan ?? "Subscription";
|
|
@@ -2621,6 +2646,7 @@ function SubscriptionDialog({
|
|
|
2621
2646
|
const note = noteOf(row);
|
|
2622
2647
|
const upcoming = detail?.upcoming ?? [];
|
|
2623
2648
|
const past = detail?.payments ?? [];
|
|
2649
|
+
const unread = detail?.payments === null && detail.payments_error !== null;
|
|
2624
2650
|
const total = detail?.paid?.amount ? paidBrief(detail.paid.amount, plan, formatAmount) : null;
|
|
2625
2651
|
const rate = plan ? briefPrice(plan) : null;
|
|
2626
2652
|
const due = (display) => display ? `${grouped(display)}${symbolOf(plan)}` : "";
|
|
@@ -2659,9 +2685,9 @@ function SubscriptionDialog({
|
|
|
2659
2685
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dt", { children: "Total paid" }),
|
|
2660
2686
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dd", { title: total.title, children: total.text })
|
|
2661
2687
|
] }) : null,
|
|
2662
|
-
|
|
2688
|
+
planEnd(plan?.ends_at) ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { "data-mesub-row": "", children: [
|
|
2663
2689
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dt", { children: "Plan ends" }),
|
|
2664
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dd", { children:
|
|
2690
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dd", { children: planEnd(plan?.ends_at) })
|
|
2665
2691
|
] }) : null,
|
|
2666
2692
|
closeFrom(row) ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { "data-mesub-row": "", children: [
|
|
2667
2693
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dt", { children: "Close from" }),
|
|
@@ -2672,7 +2698,7 @@ function SubscriptionDialog({
|
|
|
2672
2698
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dd", { title: row.wallet, children: shortAddress(row.wallet) })
|
|
2673
2699
|
] })
|
|
2674
2700
|
] }),
|
|
2675
|
-
upcoming.length > 0 || past.length > 0 ? (
|
|
2701
|
+
upcoming.length > 0 || past.length > 0 || unread ? (
|
|
2676
2702
|
// Closed until asked for, as the terms are on the checkout: same toggle.
|
|
2677
2703
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("details", { "data-mesub-terms": "", "data-mesub-payments": "", children: [
|
|
2678
2704
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("summary", { children: "Payments" }),
|
|
@@ -2694,6 +2720,10 @@ function SubscriptionDialog({
|
|
|
2694
2720
|
`next-${next.due_at}`
|
|
2695
2721
|
)) })
|
|
2696
2722
|
] }) : null,
|
|
2723
|
+
unread ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Past payments", children: [
|
|
2724
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h3", { children: "Past" }),
|
|
2725
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { "data-mesub-payments-error": "", children: PAYMENTS_UNREAD })
|
|
2726
|
+
] }) : null,
|
|
2697
2727
|
past.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Past payments", children: [
|
|
2698
2728
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h3", { children: "Past" }),
|
|
2699
2729
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { children: past.slice(0, shown).map((payment) => {
|
|
@@ -2776,7 +2806,7 @@ function SubscriptionDialog({
|
|
|
2776
2806
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
2777
2807
|
function MesubProvider({
|
|
2778
2808
|
endpoint,
|
|
2779
|
-
chain = "solana:
|
|
2809
|
+
chain = "solana:mainnet",
|
|
2780
2810
|
fetch,
|
|
2781
2811
|
theme,
|
|
2782
2812
|
manageUrl,
|