@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 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
- > **Status: early, 0.x, not on npm yet.** The API may still change between
20
- > minor versions. See the [board](https://github.com/orgs/Mesub-io/projects/5)
21
- > for what is next.
19
+ [![npm](https://img.shields.io/npm/v/@mesub/react)](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:devnet` (the default) or `solana:mainnet`. It must match your plans' `network`. |
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
- date when it has one. A plan that ends before a second charge can run is
92
- priced `once`, with the date its access runs to.
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 Array.isArray(answer.subscriptions) ? answer.subscriptions : [];
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 = moment(plan.ends_at);
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) ?? ends : null;
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 ${until}, when the plan ends. Nothing more is charged.`;
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.jsx)(
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: body
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
- moment(plan?.ends_at) ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { "data-mesub-row": "", children: [
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: moment(plan?.ends_at) })
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:devnet",
2809
+ chain = "solana:mainnet",
2780
2810
  fetch,
2781
2811
  theme,
2782
2812
  manageUrl,