@mesub/react 0.0.0-stage → 0.1.2
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 +340 -2
- package/dist/index.cjs +2974 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +152 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.js +2955 -0
- package/dist/index.js.map +1 -0
- package/package.json +99 -4
- package/styles.css +2764 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2955 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/provider.tsx
|
|
4
|
+
import { useCallback as useCallback2, useEffect as useEffect6, useMemo, useRef as useRef5, useState as useState6 } from "react";
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var MesubClientError = class extends Error {
|
|
8
|
+
name = "MesubClientError";
|
|
9
|
+
status;
|
|
10
|
+
code;
|
|
11
|
+
// Seconds, from Retry-After: on a 429, and on a 503 `unavailable` (Mesub-io/backend#505).
|
|
12
|
+
retryAfter;
|
|
13
|
+
constructor(message, status, code = null, options = {}) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.retryAfter = options.retryAfter ?? null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/api.ts
|
|
22
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
23
|
+
var CHAIN_TIMEOUT_MS = 9e4;
|
|
24
|
+
var deadlines = /* @__PURE__ */ new WeakMap();
|
|
25
|
+
function deadlineOf(answer) {
|
|
26
|
+
return deadlines.get(answer) ?? null;
|
|
27
|
+
}
|
|
28
|
+
function dyingAt(expiresAt, served, received) {
|
|
29
|
+
const at = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
|
|
30
|
+
if (Number.isNaN(at)) return null;
|
|
31
|
+
const now = served === null ? Number.NaN : Date.parse(served);
|
|
32
|
+
const until = Number.isNaN(now) ? at : received + (at - now) - 1e3;
|
|
33
|
+
return until > received ? until : null;
|
|
34
|
+
}
|
|
35
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
+
var LISTED_AT_MOST = 5;
|
|
37
|
+
function paidOf(answer) {
|
|
38
|
+
const read = (value) => isObject(value) && typeof value.count === "number" ? { count: value.count, amount: typeof value.amount === "string" ? value.amount : null } : null;
|
|
39
|
+
const total = read(answer.paid);
|
|
40
|
+
if (total) return total;
|
|
41
|
+
const whole = Array.isArray(answer.payments) && answer.payments.length < LISTED_AT_MOST;
|
|
42
|
+
return whole ? read(answer.listed_paid) : null;
|
|
43
|
+
}
|
|
44
|
+
function createApi(options) {
|
|
45
|
+
const base = options.endpoint.replace(/\/+$/, "");
|
|
46
|
+
const doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
47
|
+
async function request(method, path, body, timeout) {
|
|
48
|
+
const headers = { Accept: "application/json" };
|
|
49
|
+
if (method === "POST") headers["Content-Type"] = "application/json";
|
|
50
|
+
const late = `No answer within ${timeout / 1e3} seconds.`;
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
let timer;
|
|
53
|
+
const deadline = new Promise((_, reject) => {
|
|
54
|
+
timer = setTimeout(() => {
|
|
55
|
+
reject(new MesubClientError(late, null, "timeout"));
|
|
56
|
+
controller.abort();
|
|
57
|
+
}, timeout);
|
|
58
|
+
});
|
|
59
|
+
async function send() {
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await doFetch(`${base}${path}`, {
|
|
63
|
+
method,
|
|
64
|
+
headers,
|
|
65
|
+
...method === "POST" && { body: JSON.stringify(body ?? {}) },
|
|
66
|
+
// The merchant's own session cookie says who is asking.
|
|
67
|
+
credentials: "include",
|
|
68
|
+
signal: controller.signal
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (controller.signal.aborted) throw new MesubClientError(late, null, "timeout");
|
|
72
|
+
throw new MesubClientError("Could not reach the server.", null, "network", {
|
|
73
|
+
cause: error
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
let answer;
|
|
77
|
+
try {
|
|
78
|
+
answer = await response.json();
|
|
79
|
+
} catch {
|
|
80
|
+
answer = void 0;
|
|
81
|
+
}
|
|
82
|
+
if (!response.ok) throw refusal(response, answer);
|
|
83
|
+
if (!isObject(answer)) {
|
|
84
|
+
throw new MesubClientError(
|
|
85
|
+
`${base}${path} did not answer as the @mesub/node routes do.`,
|
|
86
|
+
response.status,
|
|
87
|
+
"bad_response"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const until = dyingAt(
|
|
91
|
+
answer.transaction_expires_at,
|
|
92
|
+
response.headers.get("Date"),
|
|
93
|
+
Date.now()
|
|
94
|
+
);
|
|
95
|
+
if (until !== null) deadlines.set(answer, until);
|
|
96
|
+
return answer;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
return await Promise.race([send(), deadline]);
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const at = (id) => `/subscriptions/${encodeURIComponent(id)}`;
|
|
105
|
+
return {
|
|
106
|
+
plan: async (slug) => await request(
|
|
107
|
+
"GET",
|
|
108
|
+
`/plans/${encodeURIComponent(slug)}`,
|
|
109
|
+
void 0,
|
|
110
|
+
REQUEST_TIMEOUT_MS
|
|
111
|
+
),
|
|
112
|
+
subscriptions: async () => {
|
|
113
|
+
const answer = await request("GET", "/subscriptions", void 0, REQUEST_TIMEOUT_MS);
|
|
114
|
+
return Array.isArray(answer.subscriptions) ? answer.subscriptions : [];
|
|
115
|
+
},
|
|
116
|
+
subscription: async (id) => {
|
|
117
|
+
const answer = await request("GET", at(id), void 0, REQUEST_TIMEOUT_MS);
|
|
118
|
+
return {
|
|
119
|
+
subscription: answer.subscription,
|
|
120
|
+
upcoming: Array.isArray(answer.upcoming) ? answer.upcoming : [],
|
|
121
|
+
payments: Array.isArray(answer.payments) ? answer.payments : null,
|
|
122
|
+
paid: paidOf(answer)
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
prepare: async (plan, wallet) => await request(
|
|
126
|
+
"POST",
|
|
127
|
+
"/subscriptions",
|
|
128
|
+
{ plan, wallet },
|
|
129
|
+
REQUEST_TIMEOUT_MS
|
|
130
|
+
),
|
|
131
|
+
transaction: async (id) => await request(
|
|
132
|
+
"POST",
|
|
133
|
+
`${at(id)}/transaction`,
|
|
134
|
+
{},
|
|
135
|
+
REQUEST_TIMEOUT_MS
|
|
136
|
+
),
|
|
137
|
+
submit: async (id, signed) => await request(
|
|
138
|
+
"POST",
|
|
139
|
+
`${at(id)}/submit`,
|
|
140
|
+
{ transaction: signed.transaction, terms_signature: signed.terms_signature },
|
|
141
|
+
CHAIN_TIMEOUT_MS
|
|
142
|
+
),
|
|
143
|
+
build: async (action, id) => await request(
|
|
144
|
+
"POST",
|
|
145
|
+
`${at(id)}/${action}`,
|
|
146
|
+
{},
|
|
147
|
+
REQUEST_TIMEOUT_MS
|
|
148
|
+
),
|
|
149
|
+
payNow: async (id) => {
|
|
150
|
+
const answer = await request("POST", `${at(id)}/retry`, {}, REQUEST_TIMEOUT_MS);
|
|
151
|
+
return answer.subscription;
|
|
152
|
+
},
|
|
153
|
+
confirm: async (action, id, signature) => await request(
|
|
154
|
+
"POST",
|
|
155
|
+
`${at(id)}/${action}/confirm`,
|
|
156
|
+
{ signature },
|
|
157
|
+
CHAIN_TIMEOUT_MS
|
|
158
|
+
)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function refusal(response, answer) {
|
|
162
|
+
const error = isObject(answer) && isObject(answer.error) ? answer.error : null;
|
|
163
|
+
const message = typeof error?.message === "string" && error.message !== "" ? error.message : `The server answered ${response.status}.`;
|
|
164
|
+
const seconds2 = Number(response.headers.get("Retry-After"));
|
|
165
|
+
return new MesubClientError(
|
|
166
|
+
message,
|
|
167
|
+
response.status,
|
|
168
|
+
typeof error?.code === "string" ? error.code : null,
|
|
169
|
+
{ retryAfter: Number.isFinite(seconds2) && seconds2 > 0 ? seconds2 : null }
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function unreachable(error) {
|
|
173
|
+
return error instanceof MesubClientError && error.status === null;
|
|
174
|
+
}
|
|
175
|
+
function busyFor(error) {
|
|
176
|
+
if (!(error instanceof MesubClientError)) return null;
|
|
177
|
+
if (error.status !== 503 || error.code !== "unavailable") return null;
|
|
178
|
+
return error.retryAfter;
|
|
179
|
+
}
|
|
180
|
+
function signedOut(error) {
|
|
181
|
+
return error instanceof MesubClientError && error.status === 401;
|
|
182
|
+
}
|
|
183
|
+
function routeMissing(error) {
|
|
184
|
+
if (!(error instanceof MesubClientError)) return false;
|
|
185
|
+
return error.status === 405 || error.status === 404 && error.code !== "subscription_not_found";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/checkout.tsx
|
|
189
|
+
import { useWallets } from "@wallet-standard/react";
|
|
190
|
+
import { useEffect as useEffect2, useId, useRef as useRef2, useState as useState2 } from "react";
|
|
191
|
+
|
|
192
|
+
// src/context.ts
|
|
193
|
+
import { createContext, useContext } from "react";
|
|
194
|
+
var MesubContext = createContext(null);
|
|
195
|
+
function useMesubInternal() {
|
|
196
|
+
const value = useContext(MesubContext);
|
|
197
|
+
if (!value) throw new Error("@mesub/react must be used inside <MesubProvider>");
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/dialog.tsx
|
|
202
|
+
import { Fragment, useEffect, useLayoutEffect, useRef } from "react";
|
|
203
|
+
|
|
204
|
+
// src/brand.tsx
|
|
205
|
+
import { jsx } from "react/jsx-runtime";
|
|
206
|
+
var MARK = "M68 0h12v1h-12zM64 1h19v1h-19zM62 2h24v1h-24zM60 3h28v1h-28zM58 4h32v1h-32zM56 5h35v1h-35zM55 6h38v1h-38zM54 7h40v1h-40zM52 8h43v1h-43zM51 9h18v1h-18zM78 9h18v1h-18zM50 10h16v1h-16zM82 10h15v1h-15zM50 11h13v1h-13zM84 11h14v1h-14zM49 12h13v1h-13zM86 12h12v1h-12zM48 13h12v1h-12zM87 13h12v1h-12zM47 14h12v1h-12zM88 14h12v1h-12zM47 15h11v1h-11zM89 15h11v1h-11zM46 16h11v1h-11zM90 16h11v1h-11zM46 17h10v1h-10zM91 17h10v1h-10zM45 18h10v1h-10zM92 18h10v1h-10zM45 19h10v1h-10zM92 19h10v1h-10zM44 20h10v1h-10zM93 20h10v1h-10zM44 21h9v1h-9zM93 21h10v1h-10zM43 22h10v1h-10zM94 22h10v1h-10zM43 23h9v1h-9zM94 23h11v1h-11zM27 24h4v1h-4zM43 24h9v1h-9zM72 24h2v1h-2zM95 24h10v1h-10zM22 25h15v1h-15zM43 25h9v1h-9zM70 25h6v1h-6zM96 25h10v1h-10zM19 26h20v1h-20zM43 26h8v1h-8zM69 26h8v1h-8zM96 26h10v1h-10zM17 27h24v1h-24zM42 27h9v1h-9zM69 27h8v1h-8zM97 27h10v1h-10zM15 28h36v1h-36zM69 28h9v1h-9zM97 28h10v1h-10zM14 29h37v1h-37zM69 29h9v1h-9zM98 29h10v1h-10zM12 30h39v1h-39zM69 30h10v1h-10zM98 30h10v1h-10zM11 31h40v1h-40zM69 31h10v1h-10zM99 31h10v1h-10zM10 32h41v1h-41zM70 32h10v1h-10zM99 32h10v1h-10zM9 33h19v1h-19zM31 33h20v1h-20zM70 33h10v1h-10zM100 33h10v1h-10zM8 34h15v1h-15zM35 34h16v1h-16zM71 34h10v1h-10zM101 34h10v1h-10zM7 35h14v1h-14zM38 35h13v1h-13zM71 35h11v1h-11zM101 35h10v1h-10zM6 36h13v1h-13zM39 36h13v1h-13zM72 36h10v1h-10zM102 36h10v1h-10zM6 37h12v1h-12zM40 37h12v1h-12zM73 37h10v1h-10zM102 37h10v1h-10zM5 38h11v1h-11zM42 38h10v1h-10zM73 38h10v1h-10zM103 38h10v1h-10zM4 39h11v1h-11zM43 39h10v1h-10zM74 39h10v1h-10zM103 39h10v1h-10zM4 40h10v1h-10zM43 40h10v1h-10zM74 40h10v1h-10zM104 40h10v1h-10zM3 41h11v1h-11zM44 41h10v1h-10zM75 41h10v1h-10zM104 41h10v1h-10zM3 42h10v1h-10zM45 42h10v1h-10zM75 42h10v1h-10zM105 42h10v1h-10zM2 43h10v1h-10zM45 43h10v1h-10zM76 43h10v1h-10zM106 43h9v1h-9zM2 44h10v1h-10zM46 44h10v1h-10zM76 44h10v1h-10zM106 44h10v1h-10zM2 45h9v1h-9zM46 45h10v1h-10zM77 45h10v1h-10zM107 45h10v1h-10zM1 46h10v1h-10zM47 46h10v1h-10zM78 46h9v1h-9zM107 46h10v1h-10zM1 47h9v1h-9zM47 47h10v1h-10zM78 47h10v1h-10zM108 47h10v1h-10zM1 48h9v1h-9zM48 48h10v1h-10zM79 48h10v1h-10zM108 48h10v1h-10zM0 49h10v1h-10zM48 49h10v1h-10zM79 49h10v1h-10zM109 49h10v1h-10zM0 50h9v1h-9zM27 50h4v1h-4zM49 50h10v1h-10zM80 50h10v1h-10zM109 50h10v1h-10zM0 51h9v1h-9zM25 51h7v1h-7zM49 51h10v1h-10zM80 51h10v1h-10zM110 51h10v1h-10zM0 52h9v1h-9zM25 52h8v1h-8zM50 52h10v1h-10zM81 52h10v1h-10zM111 52h9v1h-9zM0 53h9v1h-9zM25 53h8v1h-8zM51 53h10v1h-10zM81 53h10v1h-10zM111 53h10v1h-10zM0 54h9v1h-9zM25 54h9v1h-9zM51 54h10v1h-10zM82 54h10v1h-10zM112 54h9v1h-9zM0 55h9v1h-9zM25 55h10v1h-10zM52 55h10v1h-10zM82 55h10v1h-10zM112 55h10v1h-10zM0 56h9v1h-9zM25 56h10v1h-10zM52 56h10v1h-10zM83 56h10v1h-10zM113 56h10v1h-10zM0 57h9v1h-9zM26 57h10v1h-10zM53 57h10v1h-10zM84 57h9v1h-9zM113 57h10v1h-10zM0 58h9v1h-9zM26 58h10v1h-10zM53 58h10v1h-10zM84 58h10v1h-10zM114 58h10v1h-10zM1 59h9v1h-9zM27 59h10v1h-10zM54 59h10v1h-10zM85 59h10v1h-10zM114 59h10v1h-10zM1 60h9v1h-9zM27 60h10v1h-10zM54 60h10v1h-10zM85 60h10v1h-10zM115 60h10v1h-10zM1 61h9v1h-9zM28 61h10v1h-10zM55 61h10v1h-10zM86 61h10v1h-10zM115 61h10v1h-10zM1 62h10v1h-10zM28 62h10v1h-10zM56 62h9v1h-9zM86 62h10v1h-10zM116 62h10v1h-10zM2 63h9v1h-9zM29 63h10v1h-10zM56 63h10v1h-10zM87 63h10v1h-10zM116 63h10v1h-10zM2 64h10v1h-10zM29 64h10v1h-10zM57 64h9v1h-9zM87 64h10v1h-10zM117 64h10v1h-10zM2 65h10v1h-10zM30 65h10v1h-10zM57 65h10v1h-10zM88 65h10v1h-10zM118 65h9v1h-9zM3 66h10v1h-10zM30 66h10v1h-10zM58 66h10v1h-10zM88 66h10v1h-10zM118 66h10v1h-10zM3 67h10v1h-10zM31 67h10v1h-10zM58 67h10v1h-10zM89 67h10v1h-10zM119 67h9v1h-9zM4 68h10v1h-10zM32 68h9v1h-9zM59 68h10v1h-10zM89 68h10v1h-10zM119 68h10v1h-10zM5 69h10v1h-10zM32 69h10v1h-10zM59 69h10v1h-10zM90 69h10v1h-10zM120 69h10v1h-10zM5 70h10v1h-10zM33 70h10v1h-10zM60 70h10v1h-10zM91 70h9v1h-9zM120 70h10v1h-10zM6 71h10v1h-10zM33 71h10v1h-10zM60 71h10v1h-10zM91 71h10v1h-10zM121 71h10v1h-10zM6 72h10v1h-10zM34 72h10v1h-10zM61 72h10v1h-10zM92 72h9v1h-9zM121 72h10v1h-10zM7 73h10v1h-10zM34 73h10v1h-10zM61 73h10v1h-10zM92 73h10v1h-10zM122 73h10v1h-10zM7 74h10v1h-10zM35 74h10v1h-10zM62 74h10v1h-10zM93 74h10v1h-10zM123 74h9v1h-9zM8 75h10v1h-10zM35 75h10v1h-10zM63 75h9v1h-9zM93 75h10v1h-10zM123 75h10v1h-10zM8 76h10v1h-10zM36 76h10v1h-10zM63 76h10v1h-10zM94 76h10v1h-10zM124 76h9v1h-9zM9 77h10v1h-10zM36 77h10v1h-10zM64 77h9v1h-9zM94 77h10v1h-10zM124 77h10v1h-10zM9 78h10v1h-10zM37 78h10v1h-10zM64 78h10v1h-10zM95 78h10v1h-10zM125 78h9v1h-9zM10 79h10v1h-10zM37 79h10v1h-10zM65 79h10v1h-10zM95 79h10v1h-10zM125 79h10v1h-10zM11 80h10v1h-10zM38 80h10v1h-10zM65 80h10v1h-10zM96 80h10v1h-10zM126 80h10v1h-10zM11 81h10v1h-10zM39 81h9v1h-9zM66 81h10v1h-10zM97 81h9v1h-9zM126 81h10v1h-10zM12 82h10v1h-10zM39 82h10v1h-10zM66 82h10v1h-10zM97 82h10v1h-10zM127 82h10v1h-10zM12 83h10v1h-10zM40 83h9v1h-9zM67 83h10v1h-10zM98 83h9v1h-9zM127 83h10v1h-10zM13 84h10v1h-10zM40 84h10v1h-10zM67 84h10v1h-10zM98 84h10v1h-10zM128 84h10v1h-10zM13 85h10v1h-10zM41 85h9v1h-9zM68 85h10v1h-10zM99 85h9v1h-9zM129 85h9v1h-9zM14 86h10v1h-10zM41 86h10v1h-10zM68 86h10v1h-10zM99 86h10v1h-10zM129 86h10v1h-10zM14 87h10v1h-10zM42 87h10v1h-10zM69 87h10v1h-10zM99 87h10v1h-10zM130 87h9v1h-9zM15 88h37v1h-37zM70 88h40v1h-40zM130 88h10v1h-10zM16 89h37v1h-37zM70 89h40v1h-40zM131 89h9v1h-9zM16 90h37v1h-37zM71 90h40v1h-40zM131 90h10v1h-10zM17 91h37v1h-37zM71 91h40v1h-40zM132 91h10v1h-10zM17 92h37v1h-37zM72 92h40v1h-40zM132 92h10v1h-10zM18 93h37v1h-37zM72 93h40v1h-40zM133 93h10v1h-10zM18 94h37v1h-37zM73 94h40v1h-40zM133 94h10v1h-10zM19 95h37v1h-37zM73 95h40v1h-40zM134 95h10v1h-10z";
|
|
207
|
+
function MesubMark({ width, height }) {
|
|
208
|
+
return /* @__PURE__ */ jsx(
|
|
209
|
+
"svg",
|
|
210
|
+
{
|
|
211
|
+
width,
|
|
212
|
+
height,
|
|
213
|
+
viewBox: "0 0 144 96",
|
|
214
|
+
fill: "currentColor",
|
|
215
|
+
"aria-hidden": "true",
|
|
216
|
+
children: /* @__PURE__ */ jsx("path", { d: MARK })
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
var INSTALL_ICONS = {
|
|
221
|
+
Phantom: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHJ4PSI2IiBmaWxsPSIjM2EzZDQ3Ii8+PHBhdGggZD0iTTggMTZWOGg0LjVhMi41IDIuNSAwIDAgMSAwIDVIOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+",
|
|
222
|
+
Solflare: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHJ4PSI2IiBmaWxsPSIjNWE1ZjZiIi8+PHBhdGggZD0iTTE1LjUgOS4yQzE1IDguNSAxMy45IDggMTIuNSA4IDEwLjggOCA5LjUgOC44IDkuNSAxMHMxLjIgMS43IDMgMiAzIC44IDMgMi0xLjMgMi0zIDJjLTEuNCAwLTIuNS0uNS0zLTEuMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+"
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/dialog.tsx
|
|
226
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
227
|
+
var FIRST_CONTROL = "[data-mesub-wallets] button:not(:disabled), [data-mesub-submit], [data-mesub-done], [data-mesub-install], [data-mesub-reload], [data-mesub-cancel]";
|
|
228
|
+
function MesubDialog({
|
|
229
|
+
screen,
|
|
230
|
+
titleId,
|
|
231
|
+
onClose
|
|
232
|
+
}) {
|
|
233
|
+
const { theme } = useMesubInternal();
|
|
234
|
+
const dialog = useRef(null);
|
|
235
|
+
useLayoutEffect(() => {
|
|
236
|
+
const node = dialog.current;
|
|
237
|
+
if (node && !node.open) {
|
|
238
|
+
if (typeof node.showModal === "function") node.showModal();
|
|
239
|
+
else node.setAttribute("open", "");
|
|
240
|
+
}
|
|
241
|
+
}, []);
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
const node = dialog.current;
|
|
244
|
+
const control = node?.querySelector(FIRST_CONTROL);
|
|
245
|
+
(control ?? node?.querySelector("[data-mesub-close]"))?.focus();
|
|
246
|
+
}, [screen.view]);
|
|
247
|
+
return /* @__PURE__ */ jsxs(
|
|
248
|
+
"dialog",
|
|
249
|
+
{
|
|
250
|
+
ref: dialog,
|
|
251
|
+
"aria-labelledby": titleId,
|
|
252
|
+
"aria-modal": "true",
|
|
253
|
+
"data-mesub-dialog": "",
|
|
254
|
+
"data-mesub-step": screen.step,
|
|
255
|
+
"data-mesub-theme": theme,
|
|
256
|
+
onCancel: (event) => {
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
onClose();
|
|
259
|
+
},
|
|
260
|
+
onClick: (event) => {
|
|
261
|
+
if (event.target !== event.currentTarget) return;
|
|
262
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
263
|
+
const inside = event.clientX >= box.left && event.clientX <= box.right && event.clientY >= box.top && event.clientY <= box.bottom;
|
|
264
|
+
if (!inside) onClose();
|
|
265
|
+
},
|
|
266
|
+
onKeyDown: (event) => {
|
|
267
|
+
if (event.key === "Escape") {
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
onClose();
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
children: [
|
|
273
|
+
screen.onBack ? /* @__PURE__ */ jsx2(
|
|
274
|
+
"button",
|
|
275
|
+
{
|
|
276
|
+
type: "button",
|
|
277
|
+
"data-mesub-back": "",
|
|
278
|
+
"aria-label": "Back",
|
|
279
|
+
onClick: screen.onBack
|
|
280
|
+
}
|
|
281
|
+
) : null,
|
|
282
|
+
/* @__PURE__ */ jsx2("button", { type: "button", "data-mesub-close": "", "aria-label": "Close", onClick: onClose }),
|
|
283
|
+
/* @__PURE__ */ jsxs("div", { "data-mesub-brand": "", children: [
|
|
284
|
+
/* @__PURE__ */ jsx2(MesubMark, { width: 27, height: 18 }),
|
|
285
|
+
"mesub.io"
|
|
286
|
+
] }),
|
|
287
|
+
/* @__PURE__ */ jsx2(Fragment, { children: screen.body }, screen.view)
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/format.ts
|
|
294
|
+
function formatAmount(amount, decimals) {
|
|
295
|
+
const units = BigInt(amount);
|
|
296
|
+
const scale = 10n ** BigInt(decimals);
|
|
297
|
+
const whole = (units / scale).toString();
|
|
298
|
+
const fraction = (units % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
299
|
+
return fraction ? `${whole}.${fraction}` : whole;
|
|
300
|
+
}
|
|
301
|
+
function grouped(display) {
|
|
302
|
+
const [whole = "", fraction] = display.split(".");
|
|
303
|
+
if (!/^\d+$/.test(whole)) return display;
|
|
304
|
+
const spaced = whole.replace(/\B(?=(\d{3})+$)/g, ",");
|
|
305
|
+
return fraction === void 0 ? spaced : `${spaced}.${fraction}`;
|
|
306
|
+
}
|
|
307
|
+
function formatUsd(value) {
|
|
308
|
+
if (typeof value !== "string" || !/^\d+(\.\d+)?$/.test(value)) return null;
|
|
309
|
+
const dollars = Number(value);
|
|
310
|
+
if (!Number.isFinite(dollars) || dollars >= 1e15) return null;
|
|
311
|
+
if (dollars < 0.01) return "less than $0.01";
|
|
312
|
+
return `$${grouped(dollars.toFixed(2))}`;
|
|
313
|
+
}
|
|
314
|
+
function aboutUsd(value) {
|
|
315
|
+
const usd = formatUsd(value);
|
|
316
|
+
if (!usd) return null;
|
|
317
|
+
return usd.startsWith("less") ? usd : `about ${usd}`;
|
|
318
|
+
}
|
|
319
|
+
var SYMBOL_MAX = 12;
|
|
320
|
+
var NAME_MAX = 32;
|
|
321
|
+
var VOUCHED = /* @__PURE__ */ new Set(["USDC", "USDT", "PYUSD"]);
|
|
322
|
+
function clean(text, max) {
|
|
323
|
+
if (typeof text !== "string") return null;
|
|
324
|
+
const plain = text.replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
|
|
325
|
+
if (!plain) return null;
|
|
326
|
+
const letters = Array.from(plain);
|
|
327
|
+
return letters.length > max ? `${letters.slice(0, max).join("")}\u2026` : plain;
|
|
328
|
+
}
|
|
329
|
+
function httpsUrl(value) {
|
|
330
|
+
if (typeof value !== "string") return null;
|
|
331
|
+
try {
|
|
332
|
+
return new URL(value).protocol === "https:" ? value : null;
|
|
333
|
+
} catch {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function tokenOf(plan) {
|
|
338
|
+
const token = typeof plan.token === "object" && plan.token !== null ? plan.token : null;
|
|
339
|
+
const vouched = typeof plan.symbol === "string" && plan.symbol !== "";
|
|
340
|
+
const verified = vouched || token?.verified === true;
|
|
341
|
+
const unverified = token !== null && !verified;
|
|
342
|
+
const shortMint = shortAddress(plan.mint);
|
|
343
|
+
let symbol = clean(token?.symbol, SYMBOL_MAX) ?? (vouched ? clean(plan.symbol, SYMBOL_MAX) : null);
|
|
344
|
+
let name = clean(token?.name, NAME_MAX);
|
|
345
|
+
if (unverified && symbol && VOUCHED.has(symbol.toUpperCase())) symbol = null;
|
|
346
|
+
if (unverified && name && VOUCHED.has(name.toUpperCase())) name = null;
|
|
347
|
+
return {
|
|
348
|
+
unit: symbol ?? shortMint,
|
|
349
|
+
named: symbol !== null,
|
|
350
|
+
name,
|
|
351
|
+
logo: verified ? httpsUrl(token?.logo_url) : null,
|
|
352
|
+
unverified,
|
|
353
|
+
vouched,
|
|
354
|
+
shortMint
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function priceOf(plan) {
|
|
358
|
+
return `${grouped(plan.amount_display)} ${tokenOf(plan).unit}`;
|
|
359
|
+
}
|
|
360
|
+
var UNITS = ["K", "M", "B", "T"];
|
|
361
|
+
function short(display) {
|
|
362
|
+
if (!/^\d+$/.test(display)) return null;
|
|
363
|
+
const digits = display.replace(/^0+(?=\d)/, "");
|
|
364
|
+
if (digits.length < 5 || digits.length > 15) return null;
|
|
365
|
+
const unit = Math.floor((digits.length - 1) / 3);
|
|
366
|
+
const cut = digits.length - unit * 3;
|
|
367
|
+
const rest = digits.slice(cut);
|
|
368
|
+
if (/[1-9]/.test(rest.slice(2))) return null;
|
|
369
|
+
const decimals = rest.slice(0, 2).replace(/0+$/, "");
|
|
370
|
+
return `${digits.slice(0, cut)}${decimals ? `.${decimals}` : ""}${UNITS[unit - 1]}`;
|
|
371
|
+
}
|
|
372
|
+
function brief(display, vouched, unit = "") {
|
|
373
|
+
const full = `${grouped(display)}${unit}`;
|
|
374
|
+
const cut = vouched ? null : short(display);
|
|
375
|
+
return cut ? { text: `${cut}${unit}`, title: full } : { text: full, title: void 0 };
|
|
376
|
+
}
|
|
377
|
+
function briefPrice(plan) {
|
|
378
|
+
const token = tokenOf(plan);
|
|
379
|
+
return brief(plan.amount_display, token.vouched, ` ${token.unit}`);
|
|
380
|
+
}
|
|
381
|
+
function fit(text) {
|
|
382
|
+
const length = Array.from(text).length;
|
|
383
|
+
if (length > 30) return "4";
|
|
384
|
+
if (length > 18) return "3";
|
|
385
|
+
return length > 14 ? "2" : void 0;
|
|
386
|
+
}
|
|
387
|
+
function planUsd(plan) {
|
|
388
|
+
return tokenOf(plan).vouched ? null : aboutUsd(plan.amount_usd);
|
|
389
|
+
}
|
|
390
|
+
function unverifiedNote(plan) {
|
|
391
|
+
const token = tokenOf(plan);
|
|
392
|
+
if (!token.unverified) return null;
|
|
393
|
+
return token.name ? `Unverified token: ${token.name}, mint ${token.shortMint}` : `Unverified token, mint ${token.shortMint}`;
|
|
394
|
+
}
|
|
395
|
+
var NAMED = { 24: "day", 168: "week", 720: "month", 8760: "year" };
|
|
396
|
+
function cadence(hours) {
|
|
397
|
+
const named = NAMED[hours];
|
|
398
|
+
if (named) return `every ${named}`;
|
|
399
|
+
if (hours % 24 === 0) return `every ${hours / 24} days`;
|
|
400
|
+
return hours === 1 ? "every hour" : `every ${hours} hours`;
|
|
401
|
+
}
|
|
402
|
+
function formatDate(date) {
|
|
403
|
+
return date.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
|
|
404
|
+
}
|
|
405
|
+
function day(iso) {
|
|
406
|
+
if (!iso) return null;
|
|
407
|
+
const date = new Date(iso);
|
|
408
|
+
return Number.isNaN(date.getTime()) ? null : formatDate(date);
|
|
409
|
+
}
|
|
410
|
+
var CLOSE_MS = 48 * 36e5;
|
|
411
|
+
function written(iso, far, now) {
|
|
412
|
+
if (!iso) return null;
|
|
413
|
+
const date = new Date(iso);
|
|
414
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
415
|
+
if (Math.abs(date.getTime() - now) >= CLOSE_MS) return date.toLocaleDateString(void 0, far);
|
|
416
|
+
return date.toLocaleString(void 0, {
|
|
417
|
+
...far,
|
|
418
|
+
hour: "numeric",
|
|
419
|
+
minute: "2-digit",
|
|
420
|
+
// The subscriber's own zone, named: an hour alone could be anyone's.
|
|
421
|
+
timeZoneName: "short"
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function moment(iso, now = Date.now()) {
|
|
425
|
+
return written(iso, { month: "short", day: "numeric", year: "numeric" }, now);
|
|
426
|
+
}
|
|
427
|
+
function shortMoment(iso, now = Date.now()) {
|
|
428
|
+
return written(iso, { month: "short", day: "numeric" }, now);
|
|
429
|
+
}
|
|
430
|
+
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
431
|
+
var two = (n) => String(n).padStart(2, "0");
|
|
432
|
+
function planEnd(iso) {
|
|
433
|
+
if (!iso) return null;
|
|
434
|
+
const date = new Date(iso);
|
|
435
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
436
|
+
return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}, ${date.getUTCFullYear()}, ${two(date.getUTCHours())}:${two(date.getUTCMinutes())} UTC`;
|
|
437
|
+
}
|
|
438
|
+
function shortAddress(address) {
|
|
439
|
+
return `${address.slice(0, 4)}\u2026${address.slice(-4)}`;
|
|
440
|
+
}
|
|
441
|
+
function explorerUrl(signature, chain) {
|
|
442
|
+
const cluster = chain === "solana:mainnet" ? "" : `?cluster=${chain.slice("solana:".length)}`;
|
|
443
|
+
return `https://explorer.solana.com/tx/${signature}${cluster}`;
|
|
444
|
+
}
|
|
445
|
+
function termsLines(message) {
|
|
446
|
+
return message.split("\n").filter((line) => line.trim() !== "").map((line, index) => {
|
|
447
|
+
const named = index === 0 ? null : /^([A-Za-z][A-Za-z ]{0,30}): (.+)$/.exec(line);
|
|
448
|
+
return named ? { label: named[1], value: named[2] } : { label: null, value: line };
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
var CANCEL_LABEL = "Cancel any time";
|
|
452
|
+
function termsCancelUrl(message) {
|
|
453
|
+
const line = termsLines(message).find((each) => each.label === CANCEL_LABEL);
|
|
454
|
+
return line && /^https?:\/\/\S+$/.test(line.value) ? line.value : null;
|
|
455
|
+
}
|
|
456
|
+
function termsSingleCharge(message) {
|
|
457
|
+
const lines = termsLines(message);
|
|
458
|
+
const amount = lines.find((each) => each.label === "Amount");
|
|
459
|
+
if (amount) return amount.value.endsWith(", a single charge");
|
|
460
|
+
return lines.some((each) => each.label === "Single charge") ? true : null;
|
|
461
|
+
}
|
|
462
|
+
var END_MARGIN_MS = 12e4;
|
|
463
|
+
function noChargeAt(dueMs, endsAt) {
|
|
464
|
+
const end = endsAt ? Date.parse(endsAt) : NaN;
|
|
465
|
+
return !Number.isNaN(end) && dueMs + END_MARGIN_MS > end;
|
|
466
|
+
}
|
|
467
|
+
function chargedOnce(plan, now = Date.now()) {
|
|
468
|
+
return noChargeAt(now - END_MARGIN_MS + plan.period_hours * 36e5, plan.ends_at);
|
|
469
|
+
}
|
|
470
|
+
function manageLink(yours, fromTerms) {
|
|
471
|
+
if (yours === null) return null;
|
|
472
|
+
if (typeof yours === "string") {
|
|
473
|
+
if (/^\/(?!\/)\S*$/.test(yours)) return { href: yours, external: false };
|
|
474
|
+
if (/^https?:\/\/\S+$/.test(yours)) return { href: yours, external: true };
|
|
475
|
+
}
|
|
476
|
+
return fromTerms ? { href: fromTerms, external: true } : null;
|
|
477
|
+
}
|
|
478
|
+
var NOT_SHOWN = /* @__PURE__ */ new Set([
|
|
479
|
+
// Also on a single charge: its own line says it, in full.
|
|
480
|
+
"Amount",
|
|
481
|
+
"Plan",
|
|
482
|
+
"Plan details",
|
|
483
|
+
"Wallet",
|
|
484
|
+
"Nonce",
|
|
485
|
+
"Expires",
|
|
486
|
+
// The merchant may change where it is paid: not a term a subscriber holds them to.
|
|
487
|
+
"Paid to",
|
|
488
|
+
// Shown as a button under the list.
|
|
489
|
+
CANCEL_LABEL
|
|
490
|
+
]);
|
|
491
|
+
var ADDRESS = /[1-9A-HJ-NP-Za-km-z]{32,44}/;
|
|
492
|
+
var escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
493
|
+
function repriced(value, plan) {
|
|
494
|
+
const units = ["tokens", ...plan.symbol ? [escaped(plan.symbol)] : []].join("|");
|
|
495
|
+
const said2 = new RegExp(
|
|
496
|
+
`(^|[^\\d.,])${escaped(plan.amount_display)} (?:${units})(?![\\w.])`,
|
|
497
|
+
"g"
|
|
498
|
+
);
|
|
499
|
+
const price = tokenOf(plan).named ? priceOf(plan) : `${grouped(plan.amount_display)} tokens`;
|
|
500
|
+
return value.replace(said2, (_, before) => `${before}${price}`);
|
|
501
|
+
}
|
|
502
|
+
function jupiterUrl(mint) {
|
|
503
|
+
return `https://jup.ag/tokens/${mint}`;
|
|
504
|
+
}
|
|
505
|
+
function termsFacts(message, plan) {
|
|
506
|
+
const token = plan ? tokenOf(plan) : null;
|
|
507
|
+
const note = token?.unverified ? { note: "Unverified token" } : {};
|
|
508
|
+
return termsLines(message).filter((line) => line.label !== null).filter((line) => !NOT_SHOWN.has(line.label)).map(({ label, value }) => {
|
|
509
|
+
const named = /^(.+), mint ([1-9A-HJ-NP-Za-km-z]{32,44})$/.exec(value);
|
|
510
|
+
if (named) {
|
|
511
|
+
return {
|
|
512
|
+
label,
|
|
513
|
+
value: named[1],
|
|
514
|
+
full: value,
|
|
515
|
+
href: jupiterUrl(named[2]),
|
|
516
|
+
...label === "Token" && note
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const address = ADDRESS.exec(value)?.[0];
|
|
520
|
+
if (address) {
|
|
521
|
+
const short2 = value.replace(address, shortAddress(address));
|
|
522
|
+
const known = label === "Token" && token?.named && address === plan?.mint;
|
|
523
|
+
return {
|
|
524
|
+
label,
|
|
525
|
+
value: known ? `${token.unit}, ${short2}` : short2,
|
|
526
|
+
full: value,
|
|
527
|
+
// A mint with no name is still a token to get.
|
|
528
|
+
...label === "Token" && { href: jupiterUrl(address), ...note }
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const trimmed = value.replace(/, in the transaction you sign next$/, "").replace(/^https?:\/\//, "");
|
|
532
|
+
const shown = plan ? repriced(trimmed, plan) : trimmed;
|
|
533
|
+
return shown === value ? { label, value } : { label, value: shown, full: value };
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/screens.tsx
|
|
538
|
+
import { useState } from "react";
|
|
539
|
+
|
|
540
|
+
// src/wallet.ts
|
|
541
|
+
import { getBase58Decoder } from "@solana/kit";
|
|
542
|
+
import { getWalletFeature } from "@wallet-standard/react";
|
|
543
|
+
var NEEDS = {
|
|
544
|
+
subscribe: ["solana:signMessage", "solana:signTransaction"],
|
|
545
|
+
manage: ["solana:signAndSendTransaction"]
|
|
546
|
+
};
|
|
547
|
+
var IN_WORDS = {
|
|
548
|
+
"solana:signMessage": "sign a message",
|
|
549
|
+
"solana:signTransaction": "sign a transaction without sending it",
|
|
550
|
+
"solana:signAndSendTransaction": "send a transaction"
|
|
551
|
+
};
|
|
552
|
+
var isSolana = (chain) => chain.startsWith("solana:");
|
|
553
|
+
function isSolanaWallet(wallet) {
|
|
554
|
+
return wallet.chains.some(isSolana) && wallet.features.includes("standard:connect");
|
|
555
|
+
}
|
|
556
|
+
function lacking(wallet, purpose) {
|
|
557
|
+
return NEEDS[purpose].filter((feature) => !wallet.features.includes(feature)).map((feature) => IN_WORDS[feature]);
|
|
558
|
+
}
|
|
559
|
+
function usable(wallet, purpose) {
|
|
560
|
+
return isSolanaWallet(wallet) && lacking(wallet, purpose).length === 0;
|
|
561
|
+
}
|
|
562
|
+
var WalletError = class extends Error {
|
|
563
|
+
constructor(message, wallet) {
|
|
564
|
+
super(message);
|
|
565
|
+
this.wallet = wallet;
|
|
566
|
+
}
|
|
567
|
+
wallet;
|
|
568
|
+
};
|
|
569
|
+
var said = (wallet, refusal2) => `${wallet.name} said: ${refusal2 instanceof Error ? refusal2.message : String(refusal2)}`;
|
|
570
|
+
var fromBase64 = (text) => Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
|
|
571
|
+
function toBase64(bytes) {
|
|
572
|
+
let binary = "";
|
|
573
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
574
|
+
return btoa(binary);
|
|
575
|
+
}
|
|
576
|
+
async function connectAccounts(wallet, silent = false) {
|
|
577
|
+
const feature = getWalletFeature(wallet, "standard:connect");
|
|
578
|
+
const { accounts } = await feature.connect(silent ? { silent: true } : void 0);
|
|
579
|
+
return accounts.filter((account) => account.chains.some(isSolana));
|
|
580
|
+
}
|
|
581
|
+
async function findConnected(wallets, address, purpose) {
|
|
582
|
+
const shows = (wallet) => wallet.accounts.some((entry) => entry.address === address);
|
|
583
|
+
const capable = wallets.filter((wallet) => usable(wallet, purpose)).sort((a, b) => Number(shows(b)) - Number(shows(a)));
|
|
584
|
+
for (const wallet of capable) {
|
|
585
|
+
const accounts = await connectAccounts(wallet, true).catch(() => []);
|
|
586
|
+
const account = accounts.find((entry) => entry.address === address);
|
|
587
|
+
if (account) return { wallet, account };
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
async function signText({ wallet, account }, text) {
|
|
592
|
+
const feature = getWalletFeature(wallet, "solana:signMessage");
|
|
593
|
+
let output;
|
|
594
|
+
try {
|
|
595
|
+
[output] = await feature.signMessage({ account, message: new TextEncoder().encode(text) });
|
|
596
|
+
} catch (refusal2) {
|
|
597
|
+
throw new WalletError(said(wallet, refusal2), wallet.name);
|
|
598
|
+
}
|
|
599
|
+
if (!output) throw new WalletError(`${wallet.name} returned no signature.`, wallet.name);
|
|
600
|
+
return getBase58Decoder().decode(output.signature);
|
|
601
|
+
}
|
|
602
|
+
async function signOnly({ wallet, account }, chain, transaction) {
|
|
603
|
+
const feature = getWalletFeature(wallet, "solana:signTransaction");
|
|
604
|
+
let output;
|
|
605
|
+
try {
|
|
606
|
+
[output] = await feature.signTransaction({
|
|
607
|
+
account,
|
|
608
|
+
chain,
|
|
609
|
+
transaction: fromBase64(transaction)
|
|
610
|
+
});
|
|
611
|
+
} catch (refusal2) {
|
|
612
|
+
throw new WalletError(said(wallet, refusal2), wallet.name);
|
|
613
|
+
}
|
|
614
|
+
if (!output) throw new WalletError(`${wallet.name} returned no transaction.`, wallet.name);
|
|
615
|
+
return {
|
|
616
|
+
transaction: toBase64(output.signedTransaction),
|
|
617
|
+
signature: firstSignature(output.signedTransaction)
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
async function signAndSend({ wallet, account }, chain, transaction) {
|
|
621
|
+
const feature = getWalletFeature(wallet, "solana:signAndSendTransaction");
|
|
622
|
+
let output;
|
|
623
|
+
try {
|
|
624
|
+
[output] = await feature.signAndSendTransaction({
|
|
625
|
+
account,
|
|
626
|
+
chain,
|
|
627
|
+
transaction: fromBase64(transaction)
|
|
628
|
+
});
|
|
629
|
+
} catch (refusal2) {
|
|
630
|
+
throw new WalletError(said(wallet, refusal2), wallet.name);
|
|
631
|
+
}
|
|
632
|
+
if (!output) throw new WalletError(`${wallet.name} returned no signature.`, wallet.name);
|
|
633
|
+
return getBase58Decoder().decode(output.signature);
|
|
634
|
+
}
|
|
635
|
+
function firstSignature(wire) {
|
|
636
|
+
const signature = wire.subarray(1, 65);
|
|
637
|
+
if (signature.length < 64 || signature.every((byte) => byte === 0)) return null;
|
|
638
|
+
return getBase58Decoder().decode(signature);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/screens.tsx
|
|
642
|
+
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
643
|
+
var LAST_WALLET_KEY = "mesub:last-wallet";
|
|
644
|
+
function lastWallet() {
|
|
645
|
+
try {
|
|
646
|
+
return localStorage.getItem(LAST_WALLET_KEY);
|
|
647
|
+
} catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function rememberWallet(name) {
|
|
652
|
+
try {
|
|
653
|
+
localStorage.setItem(LAST_WALLET_KEY, name);
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function Hero({ kind, icon }) {
|
|
658
|
+
return /* @__PURE__ */ jsx3("div", { "data-mesub-hero": kind, "aria-hidden": "true", children: icon ? /* @__PURE__ */ jsx3("img", { src: icon, alt: "", width: 56, height: 56 }) : null });
|
|
659
|
+
}
|
|
660
|
+
function Explorer({
|
|
661
|
+
signature,
|
|
662
|
+
chain,
|
|
663
|
+
label
|
|
664
|
+
}) {
|
|
665
|
+
return /* @__PURE__ */ jsx3(
|
|
666
|
+
"a",
|
|
667
|
+
{
|
|
668
|
+
"data-mesub-explorer": "",
|
|
669
|
+
href: explorerUrl(signature, chain),
|
|
670
|
+
target: "_blank",
|
|
671
|
+
rel: "noopener",
|
|
672
|
+
children: label
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
function TokenLogo({ plan }) {
|
|
677
|
+
const { logo } = tokenOf(plan);
|
|
678
|
+
const [failed, setFailed] = useState(null);
|
|
679
|
+
if (!logo || failed === logo) return null;
|
|
680
|
+
return /* @__PURE__ */ jsx3(
|
|
681
|
+
"img",
|
|
682
|
+
{
|
|
683
|
+
"data-mesub-token-logo": "",
|
|
684
|
+
src: logo,
|
|
685
|
+
alt: "",
|
|
686
|
+
width: 20,
|
|
687
|
+
height: 20,
|
|
688
|
+
loading: "lazy",
|
|
689
|
+
decoding: "async",
|
|
690
|
+
referrerPolicy: "no-referrer",
|
|
691
|
+
onError: () => setFailed(logo)
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
function Price({
|
|
696
|
+
plan,
|
|
697
|
+
every,
|
|
698
|
+
single
|
|
699
|
+
}) {
|
|
700
|
+
const usd = planUsd(plan);
|
|
701
|
+
const note = unverifiedNote(plan);
|
|
702
|
+
const price = briefPrice(plan);
|
|
703
|
+
return /* @__PURE__ */ jsxs2("p", { "data-mesub-price": "", children: [
|
|
704
|
+
/* @__PURE__ */ jsxs2("strong", { title: price.title, "data-mesub-fit": fit(price.text), children: [
|
|
705
|
+
/* @__PURE__ */ jsx3(TokenLogo, { plan }),
|
|
706
|
+
price.text
|
|
707
|
+
] }),
|
|
708
|
+
/* @__PURE__ */ jsx3("span", { children: every }),
|
|
709
|
+
usd ? /* @__PURE__ */ jsx3("small", { "data-mesub-usd": "", children: single ? usd : `${usd} ${every}` }) : null,
|
|
710
|
+
note ? /* @__PURE__ */ jsx3("small", { "data-mesub-unverified": "", title: plan.mint, children: note }) : null
|
|
711
|
+
] });
|
|
712
|
+
}
|
|
713
|
+
function Merchant({
|
|
714
|
+
name,
|
|
715
|
+
logoUrl,
|
|
716
|
+
testNetwork
|
|
717
|
+
}) {
|
|
718
|
+
return /* @__PURE__ */ jsxs2("div", { "data-mesub-merchant": "", children: [
|
|
719
|
+
logoUrl ? /* @__PURE__ */ jsx3("img", { src: logoUrl, alt: "", width: 56, height: 56 }) : null,
|
|
720
|
+
/* @__PURE__ */ jsx3("strong", { children: name }),
|
|
721
|
+
testNetwork ? /* @__PURE__ */ jsx3("span", { "data-mesub-network": "devnet", children: "Test network" }) : null
|
|
722
|
+
] });
|
|
723
|
+
}
|
|
724
|
+
function walletScreen({
|
|
725
|
+
titleId,
|
|
726
|
+
view,
|
|
727
|
+
wallets,
|
|
728
|
+
purpose,
|
|
729
|
+
lead,
|
|
730
|
+
step = "wallet",
|
|
731
|
+
setView,
|
|
732
|
+
pick,
|
|
733
|
+
onBack
|
|
734
|
+
}) {
|
|
735
|
+
const toList = () => setView({ name: "list" });
|
|
736
|
+
if (view.name === "waiting") {
|
|
737
|
+
return {
|
|
738
|
+
step,
|
|
739
|
+
view: "wallet-waiting",
|
|
740
|
+
onBack: toList,
|
|
741
|
+
body: /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
742
|
+
/* @__PURE__ */ jsx3(Hero, { kind: "wait", icon: view.wallet.icon }),
|
|
743
|
+
/* @__PURE__ */ jsxs2("h2", { id: titleId, children: [
|
|
744
|
+
"Approve in ",
|
|
745
|
+
view.wallet.name
|
|
746
|
+
] }),
|
|
747
|
+
/* @__PURE__ */ jsxs2("p", { children: [
|
|
748
|
+
"Check the ",
|
|
749
|
+
view.wallet.name,
|
|
750
|
+
" window."
|
|
751
|
+
] }),
|
|
752
|
+
/* @__PURE__ */ jsx3("button", { type: "button", "data-mesub-back": "", onClick: toList, children: "Use another wallet" })
|
|
753
|
+
] })
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (view.name === "rejected") {
|
|
757
|
+
return {
|
|
758
|
+
step,
|
|
759
|
+
view: "wallet-rejected",
|
|
760
|
+
onBack: toList,
|
|
761
|
+
body: /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
762
|
+
/* @__PURE__ */ jsx3(Hero, { kind: "error", icon: view.wallet.icon }),
|
|
763
|
+
/* @__PURE__ */ jsx3("h2", { id: titleId, children: "Request rejected" }),
|
|
764
|
+
/* @__PURE__ */ jsx3("p", { role: "alert", "data-mesub-error": "", children: view.message }),
|
|
765
|
+
/* @__PURE__ */ jsx3("button", { type: "button", "data-mesub-submit": "", onClick: () => pick(view.wallet), children: "Try again" }),
|
|
766
|
+
/* @__PURE__ */ jsx3("button", { type: "button", "data-mesub-back": "", onClick: toList, children: "Use another wallet" })
|
|
767
|
+
] })
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const solana = wallets.filter(isSolanaWallet);
|
|
771
|
+
const capable = solana.filter((wallet) => usable(wallet, purpose));
|
|
772
|
+
const unable = solana.filter((wallet) => !usable(wallet, purpose));
|
|
773
|
+
const reload = /* @__PURE__ */ jsx3("button", { type: "button", "data-mesub-reload": "", onClick: () => window.location.reload(), children: "Reload" });
|
|
774
|
+
if (solana.length === 0) {
|
|
775
|
+
return {
|
|
776
|
+
step,
|
|
777
|
+
view: "wallet-none",
|
|
778
|
+
onBack,
|
|
779
|
+
body: /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
780
|
+
/* @__PURE__ */ jsx3(Hero, { kind: "empty" }),
|
|
781
|
+
/* @__PURE__ */ jsx3("h2", { id: titleId, children: "No Solana wallet found" }),
|
|
782
|
+
/* @__PURE__ */ jsx3("p", { children: "Install one, then reload this page." }),
|
|
783
|
+
/* @__PURE__ */ jsxs2("div", { "data-mesub-no-wallet": "", children: [
|
|
784
|
+
/* @__PURE__ */ jsxs2(
|
|
785
|
+
"a",
|
|
786
|
+
{
|
|
787
|
+
"data-mesub-install": "",
|
|
788
|
+
href: "https://phantom.com",
|
|
789
|
+
target: "_blank",
|
|
790
|
+
rel: "noopener",
|
|
791
|
+
children: [
|
|
792
|
+
/* @__PURE__ */ jsx3("img", { src: INSTALL_ICONS.Phantom, alt: "", width: 28, height: 28 }),
|
|
793
|
+
"Phantom",
|
|
794
|
+
/* @__PURE__ */ jsx3("span", { children: "Install" })
|
|
795
|
+
]
|
|
796
|
+
}
|
|
797
|
+
),
|
|
798
|
+
/* @__PURE__ */ jsxs2(
|
|
799
|
+
"a",
|
|
800
|
+
{
|
|
801
|
+
"data-mesub-install": "",
|
|
802
|
+
href: "https://solflare.com",
|
|
803
|
+
target: "_blank",
|
|
804
|
+
rel: "noopener",
|
|
805
|
+
children: [
|
|
806
|
+
/* @__PURE__ */ jsx3("img", { src: INSTALL_ICONS.Solflare, alt: "", width: 28, height: 28 }),
|
|
807
|
+
"Solflare",
|
|
808
|
+
/* @__PURE__ */ jsx3("span", { children: "Install" })
|
|
809
|
+
]
|
|
810
|
+
}
|
|
811
|
+
)
|
|
812
|
+
] }),
|
|
813
|
+
reload
|
|
814
|
+
] })
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
const cannot = unable.map(
|
|
818
|
+
(wallet) => `${wallet.name} cannot ${lacking(wallet, purpose).join(" or ")}.`
|
|
819
|
+
);
|
|
820
|
+
if (capable.length === 0) {
|
|
821
|
+
return {
|
|
822
|
+
step,
|
|
823
|
+
view: "wallet-unable",
|
|
824
|
+
onBack,
|
|
825
|
+
body: /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
826
|
+
/* @__PURE__ */ jsx3(Hero, { kind: "empty" }),
|
|
827
|
+
/* @__PURE__ */ jsx3("h2", { id: titleId, children: "This wallet cannot do it" }),
|
|
828
|
+
/* @__PURE__ */ jsx3("p", { children: "Use a wallet that can, such as Phantom or Solflare, then reload." }),
|
|
829
|
+
/* @__PURE__ */ jsx3("p", { role: "alert", "data-mesub-error": "", children: cannot.join(" ") }),
|
|
830
|
+
reload
|
|
831
|
+
] })
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
const last = lastWallet();
|
|
835
|
+
return {
|
|
836
|
+
step,
|
|
837
|
+
view: "wallet-list",
|
|
838
|
+
onBack,
|
|
839
|
+
body: /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
840
|
+
/* @__PURE__ */ jsx3("h2", { id: titleId, children: "Connect a wallet" }),
|
|
841
|
+
/* @__PURE__ */ jsx3("p", { children: lead }),
|
|
842
|
+
/* @__PURE__ */ jsx3("ul", { "data-mesub-wallets": "", "aria-label": "Wallets in this browser", children: capable.map((wallet) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs2(
|
|
843
|
+
"button",
|
|
844
|
+
{
|
|
845
|
+
type: "button",
|
|
846
|
+
"data-mesub-wallet": wallet.name,
|
|
847
|
+
onClick: () => pick(wallet),
|
|
848
|
+
children: [
|
|
849
|
+
/* @__PURE__ */ jsx3("img", { src: wallet.icon, alt: "", width: 28, height: 28 }),
|
|
850
|
+
wallet.name,
|
|
851
|
+
wallet.name === last ? /* @__PURE__ */ jsx3("span", { "data-mesub-badge": "", children: "Last used" }) : null
|
|
852
|
+
]
|
|
853
|
+
}
|
|
854
|
+
) }, wallet.name)) }),
|
|
855
|
+
cannot.length > 0 ? /* @__PURE__ */ jsx3("p", { "data-mesub-notice": "", role: "note", children: cannot.join(" ") }) : null
|
|
856
|
+
] })
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
var PRIMARY = {
|
|
860
|
+
retry: "Try again",
|
|
861
|
+
check: "Check again",
|
|
862
|
+
wallet: "Use another wallet"
|
|
863
|
+
};
|
|
864
|
+
function failureScreen({
|
|
865
|
+
failure,
|
|
866
|
+
titleId,
|
|
867
|
+
icon,
|
|
868
|
+
safe,
|
|
869
|
+
pending,
|
|
870
|
+
signature,
|
|
871
|
+
chain,
|
|
872
|
+
busy,
|
|
873
|
+
onAct,
|
|
874
|
+
onBack,
|
|
875
|
+
backLabel
|
|
876
|
+
}) {
|
|
877
|
+
const back = failure.moved === "pending" ? void 0 : onBack;
|
|
878
|
+
return {
|
|
879
|
+
step: failure.step,
|
|
880
|
+
view: `${failure.step}-failed-${failure.title}`,
|
|
881
|
+
onBack: back,
|
|
882
|
+
body: /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
883
|
+
/* @__PURE__ */ jsx3(Hero, { kind: failure.moved === "pending" ? "pending" : "error", icon }),
|
|
884
|
+
/* @__PURE__ */ jsx3("h2", { id: titleId, children: failure.title }),
|
|
885
|
+
/* @__PURE__ */ jsx3("p", { role: "alert", "data-mesub-error": "", children: failure.message }),
|
|
886
|
+
failure.moved === "safe" ? /* @__PURE__ */ jsx3("p", { "data-mesub-funds": "safe", children: safe }) : failure.moved === "pending" ? /* @__PURE__ */ jsx3("p", { "data-mesub-funds": "pending", children: pending }) : null,
|
|
887
|
+
failure.moved === "pending" && signature ? /* @__PURE__ */ jsx3(Explorer, { signature, chain, label: "View transaction" }) : null,
|
|
888
|
+
failure.action === "close" ? /* @__PURE__ */ jsx3("button", { type: "button", "data-mesub-done": "", onClick: () => onAct("close"), children: "Close" }) : /* @__PURE__ */ jsx3(
|
|
889
|
+
"button",
|
|
890
|
+
{
|
|
891
|
+
type: "button",
|
|
892
|
+
"data-mesub-submit": "",
|
|
893
|
+
disabled: busy,
|
|
894
|
+
"aria-busy": busy || void 0,
|
|
895
|
+
onClick: () => onAct(failure.action),
|
|
896
|
+
children: PRIMARY[failure.action]
|
|
897
|
+
}
|
|
898
|
+
),
|
|
899
|
+
back && failure.action === "retry" ? /* @__PURE__ */ jsx3("button", { type: "button", "data-mesub-back": "", onClick: back, children: backLabel }) : null
|
|
900
|
+
] })
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function signedOutScreen({
|
|
904
|
+
titleId,
|
|
905
|
+
step,
|
|
906
|
+
message,
|
|
907
|
+
safe,
|
|
908
|
+
onClose
|
|
909
|
+
}) {
|
|
910
|
+
return {
|
|
911
|
+
step,
|
|
912
|
+
view: "signed-out",
|
|
913
|
+
body: /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
914
|
+
/* @__PURE__ */ jsx3(Hero, { kind: "error" }),
|
|
915
|
+
/* @__PURE__ */ jsx3("h2", { id: titleId, children: "Sign in first" }),
|
|
916
|
+
/* @__PURE__ */ jsx3("p", { role: "alert", "data-mesub-error": "", children: message }),
|
|
917
|
+
/* @__PURE__ */ jsx3("p", { "data-mesub-funds": "safe", children: safe }),
|
|
918
|
+
/* @__PURE__ */ jsx3("button", { type: "button", "data-mesub-done": "", onClick: onClose, children: "Close" })
|
|
919
|
+
] })
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/checkout.tsx
|
|
924
|
+
import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
925
|
+
var SAFE = "Nothing was charged.";
|
|
926
|
+
var PENDING = "Payment may be pending.";
|
|
927
|
+
var FRESH_TERMS = "The terms had expired. These are fresh: check them again.";
|
|
928
|
+
var EXPIRED = "The transaction expired before it was sent. Nothing was charged: approve this one.";
|
|
929
|
+
var EXPIRED_TERMS = "The transaction expired before it was sent, and its terms with it. Nothing was charged: check these.";
|
|
930
|
+
var NOT_LANDED = "The transaction has not landed yet. Wait a moment, then check again: nothing is paid twice.";
|
|
931
|
+
var STATES = {
|
|
932
|
+
signing: "signing",
|
|
933
|
+
submitting: "confirming",
|
|
934
|
+
subscribed: "subscribed"
|
|
935
|
+
};
|
|
936
|
+
var BUSY_WAIT_MAX_S = 10;
|
|
937
|
+
var seconds = (n) => `${n} ${n === 1 ? "second" : "seconds"}`;
|
|
938
|
+
var MARGIN_MS = 5e3;
|
|
939
|
+
function expired(prepared) {
|
|
940
|
+
const until = Date.parse(prepared.terms.expires_at);
|
|
941
|
+
return !Number.isNaN(until) && until - Date.now() < MARGIN_MS;
|
|
942
|
+
}
|
|
943
|
+
function dying(prepared) {
|
|
944
|
+
const until = deadlineOf(prepared);
|
|
945
|
+
return until !== null && until - Date.now() < MARGIN_MS;
|
|
946
|
+
}
|
|
947
|
+
function singleCharge(plan, terms) {
|
|
948
|
+
return (terms === void 0 ? null : termsSingleCharge(terms)) ?? chargedOnce(plan);
|
|
949
|
+
}
|
|
950
|
+
var messageOf = (error) => error instanceof MesubClientError ? error.message : "Something went wrong.";
|
|
951
|
+
function notPrepared(error) {
|
|
952
|
+
const base = { step: "review", moved: "safe", icon: "merchant" };
|
|
953
|
+
if (unreachable(error)) {
|
|
954
|
+
return { ...base, title: "No answer", message: messageOf(error), action: "retry" };
|
|
955
|
+
}
|
|
956
|
+
const status = error instanceof MesubClientError ? error.status : null;
|
|
957
|
+
if (status === 403) {
|
|
958
|
+
return { ...base, title: "Wrong wallet", message: messageOf(error), action: "wallet" };
|
|
959
|
+
}
|
|
960
|
+
if (status === 404) {
|
|
961
|
+
return error instanceof MesubClientError && error.code === "subscription_not_found" ? { ...base, title: "Could not subscribe", message: messageOf(error), action: "retry" } : { ...base, title: "Plan not found", message: messageOf(error), action: "close" };
|
|
962
|
+
}
|
|
963
|
+
if (error instanceof MesubClientError && error.code === "comeback_period_rolling") {
|
|
964
|
+
return {
|
|
965
|
+
...base,
|
|
966
|
+
title: "Almost there",
|
|
967
|
+
message: error.retryAfter ? `Your billing period is turning over. Try again in ${error.retryAfter} seconds.` : messageOf(error),
|
|
968
|
+
action: "retry"
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (status === 409) {
|
|
972
|
+
return { ...base, title: "Cannot subscribe", message: messageOf(error), action: "close" };
|
|
973
|
+
}
|
|
974
|
+
const busy = busyFor(error);
|
|
975
|
+
if (busy !== null) {
|
|
976
|
+
return {
|
|
977
|
+
...base,
|
|
978
|
+
title: "Busy right now",
|
|
979
|
+
message: `Too many requests at once. Try again in ${seconds(busy)}.`,
|
|
980
|
+
action: "retry"
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
if (status === 429) {
|
|
984
|
+
const wait = error instanceof MesubClientError ? error.retryAfter : null;
|
|
985
|
+
return {
|
|
986
|
+
...base,
|
|
987
|
+
title: "Too many requests",
|
|
988
|
+
message: wait ? `Try again in ${wait} seconds.` : messageOf(error),
|
|
989
|
+
action: "retry"
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
return { ...base, title: "Could not subscribe", message: messageOf(error), action: "retry" };
|
|
993
|
+
}
|
|
994
|
+
function Checkout({ plan: slug, onClose, onState, onSubscribed }) {
|
|
995
|
+
const { api, chain, manageUrl } = useMesubInternal();
|
|
996
|
+
const wallets = useWallets();
|
|
997
|
+
const titleId = useId();
|
|
998
|
+
const [loaded, setLoaded] = useState2({ name: "loading" });
|
|
999
|
+
const [view, setViewState] = useState2({ name: "plan" });
|
|
1000
|
+
const [checking, setChecking] = useState2(false);
|
|
1001
|
+
const alive = useRef2(true);
|
|
1002
|
+
const attempt = useRef2(0);
|
|
1003
|
+
const shown = useRef2(view);
|
|
1004
|
+
const checkingNow = useRef2(false);
|
|
1005
|
+
const sent = useRef2(null);
|
|
1006
|
+
const expiries = useRef2(0);
|
|
1007
|
+
const latest = useRef2({ onState, onSubscribed });
|
|
1008
|
+
latest.current = { onState, onSubscribed };
|
|
1009
|
+
function setView(next) {
|
|
1010
|
+
shown.current = next;
|
|
1011
|
+
setViewState(next);
|
|
1012
|
+
latest.current.onState(STATES[next.name] ?? "open");
|
|
1013
|
+
}
|
|
1014
|
+
function begin() {
|
|
1015
|
+
const id = ++attempt.current;
|
|
1016
|
+
return () => alive.current && id === attempt.current;
|
|
1017
|
+
}
|
|
1018
|
+
function loadPlan() {
|
|
1019
|
+
setLoaded({ name: "loading" });
|
|
1020
|
+
api.plan(slug).then(
|
|
1021
|
+
(served) => alive.current && setLoaded({ name: "loaded", plan: served }),
|
|
1022
|
+
(error) => alive.current && setLoaded({ name: "failed", error })
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
useEffect2(() => {
|
|
1026
|
+
alive.current = true;
|
|
1027
|
+
latest.current.onState("open");
|
|
1028
|
+
loadPlan();
|
|
1029
|
+
return () => {
|
|
1030
|
+
alive.current = false;
|
|
1031
|
+
};
|
|
1032
|
+
}, []);
|
|
1033
|
+
function fail(failure, signer, signature = null) {
|
|
1034
|
+
setView({ name: "failed", failure, signer, signature });
|
|
1035
|
+
}
|
|
1036
|
+
function refused(error, signer) {
|
|
1037
|
+
if (signedOut(error)) setView({ name: "signed-out" });
|
|
1038
|
+
else fail(notPrepared(error), signer);
|
|
1039
|
+
}
|
|
1040
|
+
function notSigned(signer, title, error) {
|
|
1041
|
+
fail(
|
|
1042
|
+
{
|
|
1043
|
+
step: "approve",
|
|
1044
|
+
title,
|
|
1045
|
+
message: error instanceof WalletError ? error.message : `${signer.wallet.name} did not sign.`,
|
|
1046
|
+
moved: "safe",
|
|
1047
|
+
action: "retry",
|
|
1048
|
+
icon: "wallet"
|
|
1049
|
+
},
|
|
1050
|
+
signer
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
async function prepare(signer, notice = null) {
|
|
1054
|
+
const current = begin();
|
|
1055
|
+
expiries.current = 0;
|
|
1056
|
+
setView({ name: "preparing", signer });
|
|
1057
|
+
try {
|
|
1058
|
+
const prepared = await api.prepare(slug, signer.account.address);
|
|
1059
|
+
if (current()) setView({ name: "review", signer, prepared, notice });
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
if (current()) refused(error, signer);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async function fresh(id) {
|
|
1065
|
+
try {
|
|
1066
|
+
return await api.transaction(id);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (routeMissing(error)) return null;
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
async function pick(wallet) {
|
|
1073
|
+
if (shown.current.name !== "wallet" || shown.current.pick.name === "waiting") return;
|
|
1074
|
+
const current = begin();
|
|
1075
|
+
const refused2 = (message) => current() && setView({ name: "wallet", pick: { name: "rejected", wallet, message } });
|
|
1076
|
+
setView({ name: "wallet", pick: { name: "waiting", wallet } });
|
|
1077
|
+
let account;
|
|
1078
|
+
try {
|
|
1079
|
+
[account] = await connectAccounts(wallet);
|
|
1080
|
+
} catch (refusal2) {
|
|
1081
|
+
refused2(
|
|
1082
|
+
`${wallet.name} said: ${refusal2 instanceof Error ? refusal2.message : "it did not connect."}`
|
|
1083
|
+
);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (!current()) return;
|
|
1087
|
+
if (!account) {
|
|
1088
|
+
refused2(`${wallet.name} shared no Solana account.`);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
rememberWallet(wallet.name);
|
|
1092
|
+
await prepare({ wallet, account });
|
|
1093
|
+
}
|
|
1094
|
+
async function sign(signer, prepared, plan) {
|
|
1095
|
+
if (shown.current.name !== "review" || shown.current.prepared !== prepared) return;
|
|
1096
|
+
const single = singleCharge(plan, prepared.terms.message);
|
|
1097
|
+
const current = begin();
|
|
1098
|
+
if (expired(prepared)) {
|
|
1099
|
+
await prepare(signer, FRESH_TERMS);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
setView({ name: "signing", signer, what: "terms", single, notice: null });
|
|
1103
|
+
let termsSignature;
|
|
1104
|
+
try {
|
|
1105
|
+
termsSignature = await signText(signer, prepared.terms.message);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
if (current()) notSigned(signer, "Terms not signed", error);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (!current()) return;
|
|
1111
|
+
if (expired(prepared)) {
|
|
1112
|
+
await prepare(signer, FRESH_TERMS);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
setView({ name: "signing", signer, what: "fresh", single, notice: null });
|
|
1116
|
+
let latest2;
|
|
1117
|
+
try {
|
|
1118
|
+
latest2 = await fresh(prepared.subscription.id) ?? prepared;
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
if (current()) refused(error, signer);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (!current()) return;
|
|
1124
|
+
if (latest2.terms.message !== prepared.terms.message) {
|
|
1125
|
+
setView({ name: "review", signer, prepared: latest2, notice: FRESH_TERMS });
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
await pay(signer, latest2, termsSignature, single, null);
|
|
1129
|
+
}
|
|
1130
|
+
async function pay(signer, held, termsSignature, single, notice) {
|
|
1131
|
+
const current = begin();
|
|
1132
|
+
setView({ name: "signing", signer, what: "transaction", single, notice });
|
|
1133
|
+
let signed;
|
|
1134
|
+
try {
|
|
1135
|
+
signed = await signOnly(signer, chain, held.transaction);
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
if (current()) notSigned(signer, "Transaction not signed", error);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (!current()) return;
|
|
1141
|
+
sent.current = {
|
|
1142
|
+
id: held.subscription.id,
|
|
1143
|
+
signed: { transaction: signed.transaction, terms_signature: termsSignature },
|
|
1144
|
+
signer,
|
|
1145
|
+
signature: signed.signature,
|
|
1146
|
+
cancelUrl: termsCancelUrl(held.terms.message),
|
|
1147
|
+
single,
|
|
1148
|
+
terms: held.terms.message
|
|
1149
|
+
};
|
|
1150
|
+
if (dying(held)) {
|
|
1151
|
+
await renew(sent.current, current);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
await submit();
|
|
1155
|
+
}
|
|
1156
|
+
async function renew(handed, current) {
|
|
1157
|
+
const { id, signer, single } = handed;
|
|
1158
|
+
const stop = (message) => fail(
|
|
1159
|
+
{
|
|
1160
|
+
step: "confirming",
|
|
1161
|
+
title: "Transaction expired",
|
|
1162
|
+
message,
|
|
1163
|
+
moved: "safe",
|
|
1164
|
+
action: "retry",
|
|
1165
|
+
icon: "merchant"
|
|
1166
|
+
},
|
|
1167
|
+
signer
|
|
1168
|
+
);
|
|
1169
|
+
if (++expiries.current > 1) {
|
|
1170
|
+
stop("The transaction expired again before it could be sent. Try again in a moment.");
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
setView({ name: "signing", signer, what: "fresh", single, notice: EXPIRED });
|
|
1174
|
+
let latest2;
|
|
1175
|
+
try {
|
|
1176
|
+
latest2 = await fresh(id);
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
if (current()) refused(error, signer);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (!current()) return;
|
|
1182
|
+
if (!latest2) {
|
|
1183
|
+
stop("The transaction expired before it could be sent.");
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (latest2.terms.message !== handed.terms) {
|
|
1187
|
+
setView({ name: "review", signer, prepared: latest2, notice: EXPIRED_TERMS });
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
await pay(signer, latest2, handed.signed.terms_signature, single, EXPIRED);
|
|
1191
|
+
}
|
|
1192
|
+
async function submit(waited = false) {
|
|
1193
|
+
const handed = sent.current;
|
|
1194
|
+
if (!handed) return;
|
|
1195
|
+
const { id, signed, signer, signature, cancelUrl, single } = handed;
|
|
1196
|
+
const current = begin();
|
|
1197
|
+
setView({ name: "submitting", signer, signature });
|
|
1198
|
+
try {
|
|
1199
|
+
const settled = await api.submit(id, signed);
|
|
1200
|
+
if (settled.reason) {
|
|
1201
|
+
if (!current()) return;
|
|
1202
|
+
if (settled.reason_code === "transaction_expired") {
|
|
1203
|
+
await renew(handed, current);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (settled.reason_code === "nothing_landed") {
|
|
1207
|
+
fail(
|
|
1208
|
+
{
|
|
1209
|
+
step: "confirming",
|
|
1210
|
+
title: "Still confirming",
|
|
1211
|
+
message: NOT_LANDED,
|
|
1212
|
+
moved: "pending",
|
|
1213
|
+
action: "check",
|
|
1214
|
+
icon: "merchant"
|
|
1215
|
+
},
|
|
1216
|
+
signer,
|
|
1217
|
+
signature
|
|
1218
|
+
);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
fail(
|
|
1222
|
+
{
|
|
1223
|
+
step: "confirming",
|
|
1224
|
+
title: "Payment did not go through",
|
|
1225
|
+
message: settled.reason,
|
|
1226
|
+
moved: "safe",
|
|
1227
|
+
action: "retry",
|
|
1228
|
+
icon: "merchant"
|
|
1229
|
+
},
|
|
1230
|
+
signer
|
|
1231
|
+
);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
latest.current.onSubscribed(settled.subscription, signature);
|
|
1235
|
+
if (current()) {
|
|
1236
|
+
setView({
|
|
1237
|
+
name: "subscribed",
|
|
1238
|
+
signer,
|
|
1239
|
+
subscription: settled.subscription,
|
|
1240
|
+
signature,
|
|
1241
|
+
cancelUrl,
|
|
1242
|
+
single
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
if (!current()) return;
|
|
1247
|
+
if (signedOut(error)) {
|
|
1248
|
+
setView({ name: "signed-out" });
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (error instanceof MesubClientError && error.code === "transaction_expired") {
|
|
1252
|
+
await renew(handed, current);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const busy = busyFor(error);
|
|
1256
|
+
if (busy !== null) {
|
|
1257
|
+
if (!waited && busy <= BUSY_WAIT_MAX_S) {
|
|
1258
|
+
setView({
|
|
1259
|
+
name: "submitting",
|
|
1260
|
+
signer,
|
|
1261
|
+
signature,
|
|
1262
|
+
notice: `Busy right now: sending again in ${seconds(busy)}.`
|
|
1263
|
+
});
|
|
1264
|
+
await new Promise((resolve) => setTimeout(resolve, busy * 1e3));
|
|
1265
|
+
if (current()) await submit(true);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
fail(
|
|
1269
|
+
{
|
|
1270
|
+
step: "confirming",
|
|
1271
|
+
title: "Busy right now",
|
|
1272
|
+
message: `Too many payments are being sent at once. Wait ${seconds(busy)}, then check again: the same payment is sent, never a second one.`,
|
|
1273
|
+
// The server's own replays may have lost a send before: no way to a second payment.
|
|
1274
|
+
moved: "pending",
|
|
1275
|
+
action: "check",
|
|
1276
|
+
icon: "merchant"
|
|
1277
|
+
},
|
|
1278
|
+
signer,
|
|
1279
|
+
signature
|
|
1280
|
+
);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const status = error instanceof MesubClientError ? error.status : null;
|
|
1284
|
+
if (status === null || status >= 500 || !(error instanceof MesubClientError)) {
|
|
1285
|
+
fail(
|
|
1286
|
+
{
|
|
1287
|
+
step: "confirming",
|
|
1288
|
+
title: "Still confirming",
|
|
1289
|
+
message: "No answer yet on what became of the transaction. Wait a minute, then check again: nothing is paid twice.",
|
|
1290
|
+
moved: "pending",
|
|
1291
|
+
action: "check",
|
|
1292
|
+
icon: "merchant"
|
|
1293
|
+
},
|
|
1294
|
+
signer,
|
|
1295
|
+
signature
|
|
1296
|
+
);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
fail(
|
|
1300
|
+
{
|
|
1301
|
+
step: "confirming",
|
|
1302
|
+
title: "Could not subscribe",
|
|
1303
|
+
message: error.message,
|
|
1304
|
+
moved: "safe",
|
|
1305
|
+
action: "retry",
|
|
1306
|
+
icon: "merchant"
|
|
1307
|
+
},
|
|
1308
|
+
signer
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
async function checkAgain() {
|
|
1313
|
+
if (checkingNow.current) return;
|
|
1314
|
+
checkingNow.current = true;
|
|
1315
|
+
setChecking(true);
|
|
1316
|
+
try {
|
|
1317
|
+
await submit();
|
|
1318
|
+
} finally {
|
|
1319
|
+
checkingNow.current = false;
|
|
1320
|
+
if (alive.current) setChecking(false);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
function toWallets() {
|
|
1324
|
+
attempt.current++;
|
|
1325
|
+
setView({ name: "wallet", pick: { name: "list" } });
|
|
1326
|
+
}
|
|
1327
|
+
function toPlan() {
|
|
1328
|
+
attempt.current++;
|
|
1329
|
+
setView({ name: "plan" });
|
|
1330
|
+
}
|
|
1331
|
+
function act(action, signer) {
|
|
1332
|
+
if (action === "close") onClose();
|
|
1333
|
+
else if (action === "check") void checkAgain();
|
|
1334
|
+
else if (action === "wallet" || !signer) toWallets();
|
|
1335
|
+
else void prepare(signer);
|
|
1336
|
+
}
|
|
1337
|
+
let screen;
|
|
1338
|
+
if (view.name === "signed-out") {
|
|
1339
|
+
screen = signedOutScreen({
|
|
1340
|
+
titleId,
|
|
1341
|
+
step: "review",
|
|
1342
|
+
message: "You are not signed in on this site. Sign in here first, then subscribe again.",
|
|
1343
|
+
safe: SAFE,
|
|
1344
|
+
onClose
|
|
1345
|
+
});
|
|
1346
|
+
} else if (loaded.name !== "loaded") {
|
|
1347
|
+
screen = planScreen(loaded, titleId, loadPlan, onClose);
|
|
1348
|
+
} else if (view.name === "wallet") {
|
|
1349
|
+
screen = walletScreen({
|
|
1350
|
+
titleId,
|
|
1351
|
+
view: view.pick,
|
|
1352
|
+
wallets,
|
|
1353
|
+
purpose: "subscribe",
|
|
1354
|
+
lead: "It pays the subscription. Nothing is signed yet.",
|
|
1355
|
+
setView: (pickView) => {
|
|
1356
|
+
attempt.current++;
|
|
1357
|
+
setView({ name: "wallet", pick: pickView });
|
|
1358
|
+
},
|
|
1359
|
+
pick: (wallet) => void pick(wallet),
|
|
1360
|
+
onBack: toPlan
|
|
1361
|
+
});
|
|
1362
|
+
} else {
|
|
1363
|
+
screen = stageScreen({
|
|
1364
|
+
view,
|
|
1365
|
+
plan: loaded.plan,
|
|
1366
|
+
chain,
|
|
1367
|
+
manageUrl,
|
|
1368
|
+
titleId,
|
|
1369
|
+
checking,
|
|
1370
|
+
onContinue: toWallets,
|
|
1371
|
+
onSign: (signer, prepared) => void sign(signer, prepared, loaded.plan),
|
|
1372
|
+
onAct: act,
|
|
1373
|
+
onWallets: toWallets,
|
|
1374
|
+
onClose
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
return /* @__PURE__ */ jsx4(MesubDialog, { screen, titleId, onClose });
|
|
1378
|
+
}
|
|
1379
|
+
function planScreen(loaded, titleId, reload, close) {
|
|
1380
|
+
if (loaded.name === "loading") {
|
|
1381
|
+
return {
|
|
1382
|
+
step: "plan",
|
|
1383
|
+
view: "plan-loading",
|
|
1384
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1385
|
+
/* @__PURE__ */ jsx4(Hero, { kind: "wait" }),
|
|
1386
|
+
/* @__PURE__ */ jsx4("h2", { id: titleId, children: "Subscribe" }),
|
|
1387
|
+
/* @__PURE__ */ jsx4("p", { "data-mesub-notice": "", role: "status", "aria-busy": "true", children: "Loading the plan" })
|
|
1388
|
+
] })
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
const silent = unreachable(loaded.error);
|
|
1392
|
+
return {
|
|
1393
|
+
step: "plan",
|
|
1394
|
+
view: "plan-failed",
|
|
1395
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1396
|
+
/* @__PURE__ */ jsx4(Hero, { kind: "error" }),
|
|
1397
|
+
/* @__PURE__ */ jsx4("h2", { id: titleId, children: silent ? "No answer" : "Plan not found" }),
|
|
1398
|
+
/* @__PURE__ */ jsx4("p", { role: "alert", "data-mesub-error": "", children: loaded.error instanceof MesubClientError ? loaded.error.message : "Could not read this plan." }),
|
|
1399
|
+
/* @__PURE__ */ jsx4("p", { "data-mesub-funds": "safe", children: SAFE }),
|
|
1400
|
+
silent ? /* @__PURE__ */ jsx4("button", { type: "button", "data-mesub-submit": "", onClick: reload, children: "Try again" }) : /* @__PURE__ */ jsx4("button", { type: "button", "data-mesub-done": "", onClick: close, children: "Close" })
|
|
1401
|
+
] })
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
function stageScreen(props) {
|
|
1405
|
+
const { view, plan, chain, titleId } = props;
|
|
1406
|
+
const { text: price, title: exact } = briefPrice(plan);
|
|
1407
|
+
const ends = planEnd(plan.ends_at);
|
|
1408
|
+
const single = view.name === "review" ? singleCharge(plan, view.prepared.terms.message) : view.name === "signing" || view.name === "subscribed" ? view.single : singleCharge(plan);
|
|
1409
|
+
const every = single ? "once" : cadence(plan.period_hours);
|
|
1410
|
+
const merchant = /* @__PURE__ */ jsx4(
|
|
1411
|
+
Merchant,
|
|
1412
|
+
{
|
|
1413
|
+
name: plan.project_name,
|
|
1414
|
+
logoUrl: plan.logo_url,
|
|
1415
|
+
testNetwork: plan.network !== "mainnet-beta"
|
|
1416
|
+
}
|
|
1417
|
+
);
|
|
1418
|
+
if (view.name === "failed") {
|
|
1419
|
+
const { failure, signer } = view;
|
|
1420
|
+
return failureScreen({
|
|
1421
|
+
failure,
|
|
1422
|
+
titleId,
|
|
1423
|
+
icon: failure.icon === "wallet" ? signer?.wallet.icon : plan.logo_url,
|
|
1424
|
+
safe: SAFE,
|
|
1425
|
+
pending: PENDING,
|
|
1426
|
+
signature: view.signature,
|
|
1427
|
+
chain,
|
|
1428
|
+
busy: props.checking,
|
|
1429
|
+
onAct: (action) => props.onAct(action, signer),
|
|
1430
|
+
onBack: props.onWallets,
|
|
1431
|
+
backLabel: "Use another wallet"
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
if (view.name === "plan") {
|
|
1435
|
+
if (!plan.available) {
|
|
1436
|
+
return {
|
|
1437
|
+
step: "plan",
|
|
1438
|
+
view: "plan-closed",
|
|
1439
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1440
|
+
/* @__PURE__ */ jsx4(Hero, { kind: "error", icon: plan.logo_url }),
|
|
1441
|
+
/* @__PURE__ */ jsx4("h2", { id: titleId, children: "Plan closed" }),
|
|
1442
|
+
/* @__PURE__ */ jsx4("p", { role: "alert", "data-mesub-error": "", children: "This plan is not taking new subscribers right now." }),
|
|
1443
|
+
/* @__PURE__ */ jsx4("p", { "data-mesub-funds": "safe", children: SAFE }),
|
|
1444
|
+
/* @__PURE__ */ jsx4("button", { type: "button", "data-mesub-done": "", onClick: props.onClose, children: "Close" })
|
|
1445
|
+
] })
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
return {
|
|
1449
|
+
step: "plan",
|
|
1450
|
+
view: "plan",
|
|
1451
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1452
|
+
merchant,
|
|
1453
|
+
/* @__PURE__ */ jsx4("h2", { id: titleId, children: plan.name }),
|
|
1454
|
+
/* @__PURE__ */ jsx4(Price, { plan, every, single }),
|
|
1455
|
+
ends ? /* @__PURE__ */ jsx4("p", { "data-mesub-ends": "", children: single ? `Access until ${ends}, when the plan ends. Nothing more is charged.` : `Ends on ${ends}` }) : null,
|
|
1456
|
+
plan.description ? /* @__PURE__ */ jsx4("p", { "data-mesub-description": "", children: plan.description }) : null,
|
|
1457
|
+
/* @__PURE__ */ jsx4("button", { type: "button", "data-mesub-submit": "", onClick: props.onContinue, children: "Continue with a wallet" }),
|
|
1458
|
+
/* @__PURE__ */ jsx4("button", { type: "button", "data-mesub-cancel": "", onClick: props.onClose, children: "Cancel" })
|
|
1459
|
+
] })
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
const { wallet, account } = view.signer;
|
|
1463
|
+
if (view.name === "preparing") {
|
|
1464
|
+
return {
|
|
1465
|
+
step: "wallet",
|
|
1466
|
+
view: "preparing",
|
|
1467
|
+
onBack: props.onWallets,
|
|
1468
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1469
|
+
/* @__PURE__ */ jsx4(Hero, { kind: "wait", icon: wallet.icon }),
|
|
1470
|
+
/* @__PURE__ */ jsx4("h2", { id: titleId, children: "Preparing your subscription" }),
|
|
1471
|
+
/* @__PURE__ */ jsx4("p", { "data-mesub-notice": "", role: "status", "aria-busy": "true", children: "Nothing to sign yet" })
|
|
1472
|
+
] })
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
if (view.name === "review") {
|
|
1476
|
+
const { terms } = view.prepared;
|
|
1477
|
+
const nextCharge2 = new Date(Date.now() + plan.period_hours * 36e5);
|
|
1478
|
+
const chargedAgain = !single && !chargedOnce(plan);
|
|
1479
|
+
return {
|
|
1480
|
+
step: "review",
|
|
1481
|
+
view: "review",
|
|
1482
|
+
onBack: props.onWallets,
|
|
1483
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1484
|
+
merchant,
|
|
1485
|
+
/* @__PURE__ */ jsx4("h2", { id: titleId, children: plan.name }),
|
|
1486
|
+
/* @__PURE__ */ jsx4(Price, { plan, every, single }),
|
|
1487
|
+
view.notice ? /* @__PURE__ */ jsx4("p", { "data-mesub-notice": "", role: "status", children: view.notice }) : null,
|
|
1488
|
+
/* @__PURE__ */ jsxs3("dl", { "data-mesub-summary": "", children: [
|
|
1489
|
+
/* @__PURE__ */ jsxs3("div", { "data-mesub-row": "", children: [
|
|
1490
|
+
/* @__PURE__ */ jsx4("dt", { children: "Pays from" }),
|
|
1491
|
+
/* @__PURE__ */ jsxs3("dd", { title: account.address, children: [
|
|
1492
|
+
/* @__PURE__ */ jsx4("img", { src: wallet.icon, alt: "", width: 16, height: 16 }),
|
|
1493
|
+
shortAddress(account.address)
|
|
1494
|
+
] })
|
|
1495
|
+
] }),
|
|
1496
|
+
chargedAgain ? /* @__PURE__ */ jsxs3("div", { "data-mesub-row": "", children: [
|
|
1497
|
+
/* @__PURE__ */ jsx4("dt", { children: "Next charge" }),
|
|
1498
|
+
/* @__PURE__ */ jsx4("dd", { children: moment(nextCharge2.toISOString()) })
|
|
1499
|
+
] }) : null,
|
|
1500
|
+
ends ? /* @__PURE__ */ jsxs3("div", { "data-mesub-row": "", children: [
|
|
1501
|
+
/* @__PURE__ */ jsx4("dt", { children: single ? "Access until" : "Plan ends" }),
|
|
1502
|
+
/* @__PURE__ */ jsx4("dd", { children: ends })
|
|
1503
|
+
] }) : null
|
|
1504
|
+
] }),
|
|
1505
|
+
/* @__PURE__ */ jsxs3("details", { "data-mesub-terms": "", children: [
|
|
1506
|
+
/* @__PURE__ */ jsx4("summary", { children: "The terms you sign" }),
|
|
1507
|
+
/* @__PURE__ */ jsx4("dl", { "data-mesub-summary": "", children: termsFacts(terms.message, plan).map((fact2) => /* @__PURE__ */ jsxs3("div", { "data-mesub-row": "", children: [
|
|
1508
|
+
/* @__PURE__ */ jsx4("dt", { children: fact2.label }),
|
|
1509
|
+
/* @__PURE__ */ jsxs3("dd", { title: fact2.full, children: [
|
|
1510
|
+
fact2.href ? /* @__PURE__ */ jsx4(
|
|
1511
|
+
"a",
|
|
1512
|
+
{
|
|
1513
|
+
"data-mesub-token": "",
|
|
1514
|
+
href: fact2.href,
|
|
1515
|
+
target: "_blank",
|
|
1516
|
+
rel: "noopener noreferrer",
|
|
1517
|
+
children: fact2.value
|
|
1518
|
+
}
|
|
1519
|
+
) : fact2.value,
|
|
1520
|
+
fact2.note ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
1521
|
+
" ",
|
|
1522
|
+
fact2.note
|
|
1523
|
+
] }) : null
|
|
1524
|
+
] })
|
|
1525
|
+
] }, fact2.label)) })
|
|
1526
|
+
] }),
|
|
1527
|
+
/* @__PURE__ */ jsxs3("p", { "data-mesub-footnote": "", children: [
|
|
1528
|
+
"Two approvals in ",
|
|
1529
|
+
wallet.name,
|
|
1530
|
+
": the terms, then the payment.",
|
|
1531
|
+
" ",
|
|
1532
|
+
single ? "Nothing more is charged." : "Cancel anytime."
|
|
1533
|
+
] }),
|
|
1534
|
+
/* @__PURE__ */ jsxs3(
|
|
1535
|
+
"button",
|
|
1536
|
+
{
|
|
1537
|
+
type: "button",
|
|
1538
|
+
"data-mesub-submit": "",
|
|
1539
|
+
title: exact,
|
|
1540
|
+
"data-mesub-fit": fit(price),
|
|
1541
|
+
onClick: () => props.onSign(view.signer, view.prepared),
|
|
1542
|
+
children: [
|
|
1543
|
+
"Sign and pay ",
|
|
1544
|
+
price
|
|
1545
|
+
]
|
|
1546
|
+
}
|
|
1547
|
+
),
|
|
1548
|
+
/* @__PURE__ */ jsx4("button", { type: "button", "data-mesub-cancel": "", onClick: props.onClose, children: "Cancel" })
|
|
1549
|
+
] })
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
if (view.name === "signing") {
|
|
1553
|
+
const terms = view.what === "terms";
|
|
1554
|
+
const notice = view.notice ? /* @__PURE__ */ jsx4("p", { "data-mesub-notice": "", role: "status", children: view.notice }) : null;
|
|
1555
|
+
if (view.what === "fresh") {
|
|
1556
|
+
return {
|
|
1557
|
+
step: "approve",
|
|
1558
|
+
view: "approve-fresh",
|
|
1559
|
+
onBack: props.onWallets,
|
|
1560
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1561
|
+
/* @__PURE__ */ jsx4(Hero, { kind: "wait", icon: wallet.icon }),
|
|
1562
|
+
/* @__PURE__ */ jsx4("h2", { id: titleId, children: "Preparing the payment" }),
|
|
1563
|
+
notice,
|
|
1564
|
+
/* @__PURE__ */ jsx4("p", { "data-mesub-notice": "", role: "status", "aria-busy": "true", children: "Nothing to approve yet" }),
|
|
1565
|
+
/* @__PURE__ */ jsx4("button", { type: "button", "data-mesub-cancel": "", onClick: props.onClose, children: "Cancel" })
|
|
1566
|
+
] })
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
return {
|
|
1570
|
+
step: "approve",
|
|
1571
|
+
view: `approve-${view.what}`,
|
|
1572
|
+
onBack: props.onWallets,
|
|
1573
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1574
|
+
/* @__PURE__ */ jsx4(Hero, { kind: "wait", icon: wallet.icon }),
|
|
1575
|
+
/* @__PURE__ */ jsxs3("h2", { id: titleId, children: [
|
|
1576
|
+
terms ? "Sign the terms" : "Approve the payment",
|
|
1577
|
+
" in ",
|
|
1578
|
+
wallet.name
|
|
1579
|
+
] }),
|
|
1580
|
+
/* @__PURE__ */ jsx4("p", { title: terms ? void 0 : exact, children: terms ? "Step 1 of 2: a message, it moves nothing." : single ? `Step 2 of 2: ${price} now, a single charge.` : `Step 2 of 2: ${price} now, then ${price} ${every}.` }),
|
|
1581
|
+
notice,
|
|
1582
|
+
/* @__PURE__ */ jsx4("button", { type: "button", "data-mesub-cancel": "", onClick: props.onClose, children: "Cancel" })
|
|
1583
|
+
] })
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
if (view.name === "submitting") {
|
|
1587
|
+
return {
|
|
1588
|
+
step: "confirming",
|
|
1589
|
+
view: "confirming",
|
|
1590
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1591
|
+
/* @__PURE__ */ jsx4(Hero, { kind: "wait", icon: plan.logo_url }),
|
|
1592
|
+
/* @__PURE__ */ jsx4("h2", { id: titleId, children: "Confirming on Solana" }),
|
|
1593
|
+
/* @__PURE__ */ jsx4("p", { children: "This can take up to a minute." }),
|
|
1594
|
+
view.notice ? /* @__PURE__ */ jsx4("p", { "data-mesub-notice": "", role: "status", children: view.notice }) : null,
|
|
1595
|
+
view.signature ? /* @__PURE__ */ jsx4(
|
|
1596
|
+
Explorer,
|
|
1597
|
+
{
|
|
1598
|
+
signature: view.signature,
|
|
1599
|
+
chain,
|
|
1600
|
+
label: "View transaction"
|
|
1601
|
+
}
|
|
1602
|
+
) : null
|
|
1603
|
+
] })
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
const nextCharge = view.single ? null : moment(view.subscription.next_charge_at);
|
|
1607
|
+
const accessUntil = view.single ? ends ?? moment(view.subscription.access_until) : null;
|
|
1608
|
+
const manage = manageLink(props.manageUrl, view.cancelUrl);
|
|
1609
|
+
return {
|
|
1610
|
+
step: "subscribed",
|
|
1611
|
+
view: "subscribed",
|
|
1612
|
+
body: /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
1613
|
+
/* @__PURE__ */ jsx4(Hero, { kind: "check" }),
|
|
1614
|
+
/* @__PURE__ */ jsx4("h2", { id: titleId, children: "You are subscribed" }),
|
|
1615
|
+
/* @__PURE__ */ jsxs3("p", { title: exact, children: [
|
|
1616
|
+
plan.name,
|
|
1617
|
+
", ",
|
|
1618
|
+
price,
|
|
1619
|
+
" ",
|
|
1620
|
+
every
|
|
1621
|
+
] }),
|
|
1622
|
+
/* @__PURE__ */ jsxs3("dl", { "data-mesub-summary": "", children: [
|
|
1623
|
+
/* @__PURE__ */ jsxs3("div", { "data-mesub-row": "", children: [
|
|
1624
|
+
/* @__PURE__ */ jsx4("dt", { children: "Paid today" }),
|
|
1625
|
+
/* @__PURE__ */ jsxs3("dd", { title: exact, children: [
|
|
1626
|
+
price,
|
|
1627
|
+
planUsd(plan) ? /* @__PURE__ */ jsx4("small", { children: planUsd(plan) }) : null
|
|
1628
|
+
] })
|
|
1629
|
+
] }),
|
|
1630
|
+
nextCharge ? /* @__PURE__ */ jsxs3("div", { "data-mesub-row": "", children: [
|
|
1631
|
+
/* @__PURE__ */ jsx4("dt", { children: "Next charge" }),
|
|
1632
|
+
/* @__PURE__ */ jsx4("dd", { children: nextCharge })
|
|
1633
|
+
] }) : null,
|
|
1634
|
+
accessUntil ? /* @__PURE__ */ jsxs3("div", { "data-mesub-row": "", children: [
|
|
1635
|
+
/* @__PURE__ */ jsx4("dt", { children: "Access until" }),
|
|
1636
|
+
/* @__PURE__ */ jsx4("dd", { children: accessUntil })
|
|
1637
|
+
] }) : null,
|
|
1638
|
+
/* @__PURE__ */ jsxs3("div", { "data-mesub-row": "", children: [
|
|
1639
|
+
/* @__PURE__ */ jsx4("dt", { children: "Pays from" }),
|
|
1640
|
+
/* @__PURE__ */ jsxs3("dd", { title: account.address, children: [
|
|
1641
|
+
/* @__PURE__ */ jsx4("img", { src: wallet.icon, alt: "", width: 16, height: 16 }),
|
|
1642
|
+
shortAddress(account.address)
|
|
1643
|
+
] })
|
|
1644
|
+
] }),
|
|
1645
|
+
view.signature ? /* @__PURE__ */ jsxs3("div", { "data-mesub-row": "", children: [
|
|
1646
|
+
/* @__PURE__ */ jsx4("dt", { children: "Receipt" }),
|
|
1647
|
+
/* @__PURE__ */ jsx4("dd", { children: /* @__PURE__ */ jsx4(Explorer, { signature: view.signature, chain, label: "View" }) })
|
|
1648
|
+
] }) : null
|
|
1649
|
+
] }),
|
|
1650
|
+
/* @__PURE__ */ jsx4("button", { type: "button", "data-mesub-done": "", onClick: props.onClose, children: "Done" }),
|
|
1651
|
+
manage ? /* @__PURE__ */ jsx4(
|
|
1652
|
+
"a",
|
|
1653
|
+
{
|
|
1654
|
+
"data-mesub-manage": "",
|
|
1655
|
+
"data-mesub-external": manage.external ? "" : void 0,
|
|
1656
|
+
href: manage.href,
|
|
1657
|
+
...manage.external && { target: "_blank", rel: "noopener noreferrer" },
|
|
1658
|
+
children: "Cancel any time"
|
|
1659
|
+
}
|
|
1660
|
+
) : null
|
|
1661
|
+
] })
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/manage.tsx
|
|
1666
|
+
import { useWallets as useWallets2 } from "@wallet-standard/react";
|
|
1667
|
+
import { useEffect as useEffect3, useId as useId2, useRef as useRef3, useState as useState3 } from "react";
|
|
1668
|
+
import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1669
|
+
var FIND_TIMEOUT_MS = 4e3;
|
|
1670
|
+
var SAFE2 = "Nothing changed.";
|
|
1671
|
+
var PENDING2 = "The transaction may still land.";
|
|
1672
|
+
var WORDS = {
|
|
1673
|
+
cancel: {
|
|
1674
|
+
ask: "Cancel",
|
|
1675
|
+
go: "Cancel subscription",
|
|
1676
|
+
keep: "Keep it",
|
|
1677
|
+
done: "Subscription cancelled"
|
|
1678
|
+
},
|
|
1679
|
+
resume: {
|
|
1680
|
+
ask: "Resume",
|
|
1681
|
+
go: "Resume subscription",
|
|
1682
|
+
keep: "Not now",
|
|
1683
|
+
done: "Subscription resumed"
|
|
1684
|
+
},
|
|
1685
|
+
close: { ask: "Close", go: "Close subscription", keep: "Not now", done: "Subscription closed" }
|
|
1686
|
+
};
|
|
1687
|
+
var messageOf2 = (error) => error instanceof MesubClientError ? error.message : "Something went wrong.";
|
|
1688
|
+
var NOT_YET = ["cancellation_not_landed", "resume_not_landed", "close_not_landed"];
|
|
1689
|
+
function unsettled(action, settled) {
|
|
1690
|
+
const code = settled.reason_code;
|
|
1691
|
+
if (code === "transaction_expired") {
|
|
1692
|
+
return {
|
|
1693
|
+
step: "confirming",
|
|
1694
|
+
title: "Transaction expired",
|
|
1695
|
+
message: "The transaction expired before it landed. Try again with a new one.",
|
|
1696
|
+
moved: "safe",
|
|
1697
|
+
action: "retry",
|
|
1698
|
+
icon: "wallet"
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
if (code && NOT_YET.includes(code)) {
|
|
1702
|
+
return {
|
|
1703
|
+
step: "confirming",
|
|
1704
|
+
title: "Still confirming",
|
|
1705
|
+
message: `The ${action} has not landed yet. Wait a moment, then check again.`,
|
|
1706
|
+
moved: "pending",
|
|
1707
|
+
action: "check",
|
|
1708
|
+
icon: "wallet"
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
step: "confirming",
|
|
1713
|
+
title: "It did not go through",
|
|
1714
|
+
message: settled.reason,
|
|
1715
|
+
moved: "safe",
|
|
1716
|
+
action: "retry",
|
|
1717
|
+
icon: "wallet"
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function consequence(action, subscription, planEndsAt) {
|
|
1721
|
+
const end = subscription.access_until ?? subscription.current_period_end;
|
|
1722
|
+
const until = moment(end);
|
|
1723
|
+
if (action === "cancel") {
|
|
1724
|
+
return subscription.access && until ? `You keep access until ${until}. Nothing more is charged.` : "It stops now. Nothing more is charged.";
|
|
1725
|
+
}
|
|
1726
|
+
if (action === "resume") {
|
|
1727
|
+
if (end && until && noChargeAt(Date.parse(end), planEndsAt)) {
|
|
1728
|
+
return `It runs again until ${planEnd(planEndsAt)}, when the plan ends. Nothing more is charged.`;
|
|
1729
|
+
}
|
|
1730
|
+
return until ? `Charges start again on ${until}, as before.` : "Charges start again, as before.";
|
|
1731
|
+
}
|
|
1732
|
+
return "It is over. Closing it returns its deposit to your wallet.";
|
|
1733
|
+
}
|
|
1734
|
+
function outcome(action, subscription) {
|
|
1735
|
+
const until = moment(subscription.access_until ?? subscription.current_period_end);
|
|
1736
|
+
if (action === "cancel") {
|
|
1737
|
+
return subscription.access && until ? `You keep access until ${until}.` : "Nothing more is charged.";
|
|
1738
|
+
}
|
|
1739
|
+
if (action === "resume") {
|
|
1740
|
+
const next = moment(subscription.next_charge_at);
|
|
1741
|
+
if (next) return `Next charge ${next}.`;
|
|
1742
|
+
return subscription.access && until ? `You keep access until ${until}.` : "It runs again.";
|
|
1743
|
+
}
|
|
1744
|
+
return "Its deposit is back in your wallet.";
|
|
1745
|
+
}
|
|
1746
|
+
function Manage({ subscription, action, onClose, onChanged }) {
|
|
1747
|
+
const { api, chain, plan: readPlan } = useMesubInternal();
|
|
1748
|
+
const wallets = useWallets2();
|
|
1749
|
+
const titleId = useId2();
|
|
1750
|
+
const words = WORDS[action];
|
|
1751
|
+
const payer = subscription.wallet;
|
|
1752
|
+
const [view, setViewState] = useState3({ name: "confirm" });
|
|
1753
|
+
const [checking, setChecking] = useState3(false);
|
|
1754
|
+
const [planName, setPlanName] = useState3(subscription.plan ?? "this subscription");
|
|
1755
|
+
const [planEndsAt, setPlanEndsAt] = useState3(null);
|
|
1756
|
+
const alive = useRef3(true);
|
|
1757
|
+
const attempt = useRef3(0);
|
|
1758
|
+
const shown = useRef3(view);
|
|
1759
|
+
const checkingNow = useRef3(false);
|
|
1760
|
+
const latest = useRef3({ wallets, onChanged });
|
|
1761
|
+
latest.current = { wallets, onChanged };
|
|
1762
|
+
function setView(next) {
|
|
1763
|
+
shown.current = next;
|
|
1764
|
+
setViewState(next);
|
|
1765
|
+
}
|
|
1766
|
+
function begin() {
|
|
1767
|
+
const id = ++attempt.current;
|
|
1768
|
+
return () => alive.current && id === attempt.current;
|
|
1769
|
+
}
|
|
1770
|
+
useEffect3(() => {
|
|
1771
|
+
alive.current = true;
|
|
1772
|
+
if (subscription.plan) {
|
|
1773
|
+
readPlan(subscription.plan).then(
|
|
1774
|
+
(served) => {
|
|
1775
|
+
if (!alive.current) return;
|
|
1776
|
+
setPlanName(served.name);
|
|
1777
|
+
setPlanEndsAt(served.ends_at);
|
|
1778
|
+
},
|
|
1779
|
+
() => void 0
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
return () => {
|
|
1783
|
+
alive.current = false;
|
|
1784
|
+
};
|
|
1785
|
+
}, []);
|
|
1786
|
+
function fail(failure, signer, signature = null, icon = signer?.wallet.icon) {
|
|
1787
|
+
setView({ name: "failed", failure, signer, signature, icon });
|
|
1788
|
+
}
|
|
1789
|
+
async function start() {
|
|
1790
|
+
if (shown.current.name !== "confirm") return;
|
|
1791
|
+
const current = begin();
|
|
1792
|
+
setView({ name: "finding" });
|
|
1793
|
+
const signer = await Promise.race([
|
|
1794
|
+
findConnected(latest.current.wallets, payer, "manage"),
|
|
1795
|
+
new Promise((resolve) => setTimeout(() => resolve(null), FIND_TIMEOUT_MS))
|
|
1796
|
+
]);
|
|
1797
|
+
if (!current()) return;
|
|
1798
|
+
if (signer) await run(signer);
|
|
1799
|
+
else setView({ name: "wallet", pick: { name: "list" } });
|
|
1800
|
+
}
|
|
1801
|
+
async function pick(wallet) {
|
|
1802
|
+
if (shown.current.name !== "wallet" || shown.current.pick.name === "waiting") return;
|
|
1803
|
+
const current = begin();
|
|
1804
|
+
setView({ name: "wallet", pick: { name: "waiting", wallet } });
|
|
1805
|
+
let accounts;
|
|
1806
|
+
try {
|
|
1807
|
+
accounts = await connectAccounts(wallet);
|
|
1808
|
+
} catch (refusal2) {
|
|
1809
|
+
if (!current()) return;
|
|
1810
|
+
const message = `${wallet.name} said: ${refusal2 instanceof Error ? refusal2.message : "it did not connect."}`;
|
|
1811
|
+
setView({ name: "wallet", pick: { name: "rejected", wallet, message } });
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (!current()) return;
|
|
1815
|
+
const account = accounts.find((entry) => entry.address === payer);
|
|
1816
|
+
if (!account) {
|
|
1817
|
+
const on = accounts[0] ? `${wallet.name} is on ${shortAddress(accounts[0].address)}` : `${wallet.name} shared no Solana account`;
|
|
1818
|
+
fail(
|
|
1819
|
+
{
|
|
1820
|
+
step: "wallet",
|
|
1821
|
+
title: "Wrong wallet",
|
|
1822
|
+
message: `${on}, and this subscription is paid from ${shortAddress(payer)}. Switch to that account in your wallet, then try again.`,
|
|
1823
|
+
moved: "safe",
|
|
1824
|
+
action: "wallet",
|
|
1825
|
+
icon: "wallet"
|
|
1826
|
+
},
|
|
1827
|
+
null,
|
|
1828
|
+
null,
|
|
1829
|
+
wallet.icon
|
|
1830
|
+
);
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
rememberWallet(wallet.name);
|
|
1834
|
+
await run({ wallet, account });
|
|
1835
|
+
}
|
|
1836
|
+
async function run(signer) {
|
|
1837
|
+
const current = begin();
|
|
1838
|
+
setView({ name: "building", signer });
|
|
1839
|
+
let transaction;
|
|
1840
|
+
try {
|
|
1841
|
+
({ transaction } = await api.build(action, subscription.id));
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
if (!current()) return;
|
|
1844
|
+
if (signedOut(error)) {
|
|
1845
|
+
setView({ name: "signed-out" });
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const refused = error instanceof MesubClientError && error.status !== null;
|
|
1849
|
+
fail(
|
|
1850
|
+
{
|
|
1851
|
+
step: "approve",
|
|
1852
|
+
title: refused ? `Cannot ${action} it` : "No answer",
|
|
1853
|
+
message: messageOf2(error),
|
|
1854
|
+
moved: "safe",
|
|
1855
|
+
// Mesub's refusal is final for now; a silence is worth another try.
|
|
1856
|
+
action: refused && error.status !== 429 ? "close" : "retry",
|
|
1857
|
+
icon: "wallet"
|
|
1858
|
+
},
|
|
1859
|
+
signer
|
|
1860
|
+
);
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
if (!current()) return;
|
|
1864
|
+
setView({ name: "approve", signer });
|
|
1865
|
+
let signature;
|
|
1866
|
+
try {
|
|
1867
|
+
signature = await signAndSend(signer, chain, transaction);
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
if (!current()) return;
|
|
1870
|
+
fail(
|
|
1871
|
+
{
|
|
1872
|
+
step: "approve",
|
|
1873
|
+
title: "Not approved",
|
|
1874
|
+
message: error instanceof WalletError ? error.message : `${signer.wallet.name} did not send it.`,
|
|
1875
|
+
moved: "safe",
|
|
1876
|
+
action: "retry",
|
|
1877
|
+
icon: "wallet"
|
|
1878
|
+
},
|
|
1879
|
+
signer
|
|
1880
|
+
);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
latest.current.onChanged(null);
|
|
1884
|
+
if (!current()) return;
|
|
1885
|
+
await confirm(signer, signature);
|
|
1886
|
+
}
|
|
1887
|
+
async function confirm(signer, signature) {
|
|
1888
|
+
const current = begin();
|
|
1889
|
+
setView({ name: "confirming", signer, signature });
|
|
1890
|
+
try {
|
|
1891
|
+
const settled = await api.confirm(action, subscription.id, signature);
|
|
1892
|
+
latest.current.onChanged(settled.reason ? null : settled.subscription);
|
|
1893
|
+
if (!current()) return;
|
|
1894
|
+
if (settled.reason) {
|
|
1895
|
+
const failure = unsettled(action, settled);
|
|
1896
|
+
fail(failure, signer, failure.moved === "pending" ? signature : null);
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
setView({ name: "done", subscription: settled.subscription, signature });
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
if (!current()) return;
|
|
1902
|
+
if (signedOut(error)) {
|
|
1903
|
+
setView({ name: "signed-out" });
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const status = error instanceof MesubClientError ? error.status : null;
|
|
1907
|
+
const refused = status !== null && status < 500;
|
|
1908
|
+
fail(
|
|
1909
|
+
refused ? {
|
|
1910
|
+
step: "confirming",
|
|
1911
|
+
title: `Cannot ${action} it`,
|
|
1912
|
+
message: messageOf2(error),
|
|
1913
|
+
moved: null,
|
|
1914
|
+
action: "close",
|
|
1915
|
+
icon: "wallet"
|
|
1916
|
+
} : {
|
|
1917
|
+
step: "confirming",
|
|
1918
|
+
title: "Still confirming",
|
|
1919
|
+
message: `If ${signer.wallet.name} shows the transaction as sent, wait a minute, then check again.`,
|
|
1920
|
+
moved: "pending",
|
|
1921
|
+
action: "check",
|
|
1922
|
+
icon: "wallet"
|
|
1923
|
+
},
|
|
1924
|
+
signer,
|
|
1925
|
+
signature
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
async function checkAgain(signer, signature) {
|
|
1930
|
+
if (checkingNow.current) return;
|
|
1931
|
+
checkingNow.current = true;
|
|
1932
|
+
setChecking(true);
|
|
1933
|
+
try {
|
|
1934
|
+
await confirm(signer, signature);
|
|
1935
|
+
} finally {
|
|
1936
|
+
checkingNow.current = false;
|
|
1937
|
+
if (alive.current) setChecking(false);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function toWallets() {
|
|
1941
|
+
attempt.current++;
|
|
1942
|
+
setView({ name: "wallet", pick: { name: "list" } });
|
|
1943
|
+
}
|
|
1944
|
+
function toConfirm() {
|
|
1945
|
+
attempt.current++;
|
|
1946
|
+
setView({ name: "confirm" });
|
|
1947
|
+
}
|
|
1948
|
+
let screen;
|
|
1949
|
+
if (view.name === "signed-out") {
|
|
1950
|
+
screen = signedOutScreen({
|
|
1951
|
+
titleId,
|
|
1952
|
+
step: "confirm",
|
|
1953
|
+
message: "You are not signed in on this site. Sign in here first, then try again.",
|
|
1954
|
+
safe: SAFE2,
|
|
1955
|
+
onClose
|
|
1956
|
+
});
|
|
1957
|
+
} else if (view.name === "wallet") {
|
|
1958
|
+
screen = walletScreen({
|
|
1959
|
+
titleId,
|
|
1960
|
+
view: view.pick,
|
|
1961
|
+
wallets,
|
|
1962
|
+
purpose: "manage",
|
|
1963
|
+
lead: `The one that pays this subscription: ${shortAddress(payer)}.`,
|
|
1964
|
+
setView: (pickView) => {
|
|
1965
|
+
attempt.current++;
|
|
1966
|
+
setView({ name: "wallet", pick: pickView });
|
|
1967
|
+
},
|
|
1968
|
+
pick: (wallet) => void pick(wallet),
|
|
1969
|
+
onBack: toConfirm
|
|
1970
|
+
});
|
|
1971
|
+
} else if (view.name === "failed") {
|
|
1972
|
+
const { failure, signer, signature } = view;
|
|
1973
|
+
screen = failureScreen({
|
|
1974
|
+
failure,
|
|
1975
|
+
titleId,
|
|
1976
|
+
icon: view.icon,
|
|
1977
|
+
safe: SAFE2,
|
|
1978
|
+
pending: PENDING2,
|
|
1979
|
+
signature,
|
|
1980
|
+
chain,
|
|
1981
|
+
busy: checking,
|
|
1982
|
+
onAct: (next) => {
|
|
1983
|
+
if (next === "close") onClose();
|
|
1984
|
+
else if (next === "check" && signer && signature) {
|
|
1985
|
+
void checkAgain(signer, signature);
|
|
1986
|
+
} else if (next === "wallet" || !signer) toWallets();
|
|
1987
|
+
else void run(signer);
|
|
1988
|
+
},
|
|
1989
|
+
onBack: toConfirm,
|
|
1990
|
+
backLabel: "Back"
|
|
1991
|
+
});
|
|
1992
|
+
} else if (view.name === "confirm") {
|
|
1993
|
+
screen = {
|
|
1994
|
+
step: "confirm",
|
|
1995
|
+
view: "confirm",
|
|
1996
|
+
body: /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
1997
|
+
/* @__PURE__ */ jsxs4("h2", { id: titleId, children: [
|
|
1998
|
+
words.ask,
|
|
1999
|
+
" ",
|
|
2000
|
+
planName,
|
|
2001
|
+
"?"
|
|
2002
|
+
] }),
|
|
2003
|
+
/* @__PURE__ */ jsx5("p", { children: consequence(action, subscription, planEndsAt) }),
|
|
2004
|
+
/* @__PURE__ */ jsx5("dl", { "data-mesub-summary": "", children: /* @__PURE__ */ jsxs4("div", { "data-mesub-row": "", children: [
|
|
2005
|
+
/* @__PURE__ */ jsx5("dt", { children: "Signed by" }),
|
|
2006
|
+
/* @__PURE__ */ jsx5("dd", { title: payer, children: shortAddress(payer) })
|
|
2007
|
+
] }) }),
|
|
2008
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "data-mesub-submit": "", onClick: () => void start(), children: words.go }),
|
|
2009
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "data-mesub-cancel": "", onClick: onClose, children: words.keep })
|
|
2010
|
+
] })
|
|
2011
|
+
};
|
|
2012
|
+
} else if (view.name === "finding" || view.name === "building") {
|
|
2013
|
+
screen = {
|
|
2014
|
+
step: "wallet",
|
|
2015
|
+
view: view.name,
|
|
2016
|
+
onBack: toConfirm,
|
|
2017
|
+
body: /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
2018
|
+
/* @__PURE__ */ jsx5(
|
|
2019
|
+
Hero,
|
|
2020
|
+
{
|
|
2021
|
+
kind: "wait",
|
|
2022
|
+
icon: view.name === "building" ? view.signer.wallet.icon : null
|
|
2023
|
+
}
|
|
2024
|
+
),
|
|
2025
|
+
/* @__PURE__ */ jsx5("h2", { id: titleId, children: view.name === "finding" ? "Looking for your wallet" : "Preparing" }),
|
|
2026
|
+
/* @__PURE__ */ jsx5("p", { "data-mesub-notice": "", role: "status", "aria-busy": "true", children: "Nothing to sign yet" })
|
|
2027
|
+
] })
|
|
2028
|
+
};
|
|
2029
|
+
} else if (view.name === "approve") {
|
|
2030
|
+
screen = {
|
|
2031
|
+
step: "approve",
|
|
2032
|
+
view: "approve",
|
|
2033
|
+
onBack: toConfirm,
|
|
2034
|
+
body: /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
2035
|
+
/* @__PURE__ */ jsx5(Hero, { kind: "wait", icon: view.signer.wallet.icon }),
|
|
2036
|
+
/* @__PURE__ */ jsxs4("h2", { id: titleId, children: [
|
|
2037
|
+
"Approve in ",
|
|
2038
|
+
view.signer.wallet.name
|
|
2039
|
+
] }),
|
|
2040
|
+
/* @__PURE__ */ jsx5("p", { children: "One transaction, sent by your wallet. It pays nothing but the network fee." }),
|
|
2041
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "data-mesub-cancel": "", onClick: onClose, children: "Cancel" })
|
|
2042
|
+
] })
|
|
2043
|
+
};
|
|
2044
|
+
} else if (view.name === "confirming") {
|
|
2045
|
+
screen = {
|
|
2046
|
+
step: "confirming",
|
|
2047
|
+
view: "confirming",
|
|
2048
|
+
body: /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
2049
|
+
/* @__PURE__ */ jsx5(Hero, { kind: "wait", icon: view.signer.wallet.icon }),
|
|
2050
|
+
/* @__PURE__ */ jsx5("h2", { id: titleId, children: "Confirming on Solana" }),
|
|
2051
|
+
/* @__PURE__ */ jsx5("p", { children: "This can take up to a minute." }),
|
|
2052
|
+
/* @__PURE__ */ jsx5(Explorer, { signature: view.signature, chain, label: "View transaction" })
|
|
2053
|
+
] })
|
|
2054
|
+
};
|
|
2055
|
+
} else {
|
|
2056
|
+
screen = {
|
|
2057
|
+
step: "done",
|
|
2058
|
+
view: "done",
|
|
2059
|
+
body: /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
2060
|
+
/* @__PURE__ */ jsx5(Hero, { kind: "check" }),
|
|
2061
|
+
/* @__PURE__ */ jsx5("h2", { id: titleId, children: words.done }),
|
|
2062
|
+
/* @__PURE__ */ jsx5("p", { children: outcome(action, view.subscription) }),
|
|
2063
|
+
/* @__PURE__ */ jsx5(Explorer, { signature: view.signature, chain, label: "View transaction" }),
|
|
2064
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "data-mesub-done": "", onClick: onClose, children: "Done" })
|
|
2065
|
+
] })
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
return /* @__PURE__ */ jsx5(MesubDialog, { screen, titleId, onClose });
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// src/detail.tsx
|
|
2072
|
+
import { useEffect as useEffect5, useId as useId3, useState as useState5 } from "react";
|
|
2073
|
+
|
|
2074
|
+
// src/detail-logic.ts
|
|
2075
|
+
var LIVE = ["active", "unpaid", "cancelled", "stopped"];
|
|
2076
|
+
var NOT_ONE = ["pending", "expired", "failed", "superseded"];
|
|
2077
|
+
function pickSubscription(rows, plan) {
|
|
2078
|
+
const theirs = rows.filter((row) => !NOT_ONE.includes(row.status)).filter((row) => plan === void 0 || row.plan === plan).sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
|
|
2079
|
+
return theirs.find((row) => LIVE.includes(row.status)) ?? theirs[0] ?? null;
|
|
2080
|
+
}
|
|
2081
|
+
var REASONS = {
|
|
2082
|
+
insufficient_balance: "low balance",
|
|
2083
|
+
// The token program's OwnerMismatch: the wallet revoked Mesub's approval, or gave it to another app.
|
|
2084
|
+
program_4: "approval was removed"
|
|
2085
|
+
};
|
|
2086
|
+
var snake = (reason) => reason.replace(/[-:]/g, "_");
|
|
2087
|
+
function paymentSaid(payment) {
|
|
2088
|
+
const why = payment.reason ? REASONS[snake(payment.reason)] : void 0;
|
|
2089
|
+
switch (payment.outcome) {
|
|
2090
|
+
case "paid":
|
|
2091
|
+
return { label: "Paid", tone: "good" };
|
|
2092
|
+
case "rejected":
|
|
2093
|
+
return { label: why ? `Missed, ${why}` : "Missed", tone: "bad" };
|
|
2094
|
+
case "skipped":
|
|
2095
|
+
return { label: "Skipped", tone: "none" };
|
|
2096
|
+
case "blocked":
|
|
2097
|
+
return { label: "Not charged", tone: "none" };
|
|
2098
|
+
default:
|
|
2099
|
+
return { label: payment.outcome.toLowerCase().replace(/_/g, " "), tone: "none" };
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
function paidIn(amount, plan, format) {
|
|
2103
|
+
if (!plan || !/^\d+$/.test(amount)) return null;
|
|
2104
|
+
return `${grouped(format(amount, plan.decimals))}${symbolOf(plan)}`;
|
|
2105
|
+
}
|
|
2106
|
+
function paidBrief(amount, plan, format) {
|
|
2107
|
+
if (!plan || !/^\d+$/.test(amount)) return null;
|
|
2108
|
+
const { vouched } = tokenOf({ mint: "", ...plan });
|
|
2109
|
+
return brief(format(amount, plan.decimals), vouched, symbolOf(plan));
|
|
2110
|
+
}
|
|
2111
|
+
function symbolOf(plan) {
|
|
2112
|
+
if (!plan) return "";
|
|
2113
|
+
const token = tokenOf({ mint: "", ...plan });
|
|
2114
|
+
return token.named ? ` ${token.unit}` : "";
|
|
2115
|
+
}
|
|
2116
|
+
function retryOf(of) {
|
|
2117
|
+
const { retry_number: number, retries_allowed: allowed } = of;
|
|
2118
|
+
if (!Number.isInteger(number) || !Number.isInteger(allowed)) return null;
|
|
2119
|
+
return number > 0 && allowed > 0 ? `${number} / ${allowed}` : null;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// src/subscriptions.tsx
|
|
2123
|
+
import {
|
|
2124
|
+
forwardRef,
|
|
2125
|
+
useCallback,
|
|
2126
|
+
useEffect as useEffect4,
|
|
2127
|
+
useRef as useRef4,
|
|
2128
|
+
useState as useState4
|
|
2129
|
+
} from "react";
|
|
2130
|
+
import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2131
|
+
var HELD = /* @__PURE__ */ new Set(["active", "cancelled", "unpaid", "stopped", "ended"]);
|
|
2132
|
+
function closesAt(subscription) {
|
|
2133
|
+
return Date.parse(
|
|
2134
|
+
subscription.closable_from ?? subscription.access_until ?? subscription.current_period_end ?? ""
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
function actionOf(subscription, now) {
|
|
2138
|
+
const { status } = subscription;
|
|
2139
|
+
if (subscription.closable === true) return "close";
|
|
2140
|
+
if (status === "stopped" && subscription.stop_reason === "authority_closed") return null;
|
|
2141
|
+
if (status === "active" || status === "unpaid" || status === "stopped") return "cancel";
|
|
2142
|
+
if (status === "cancelled") {
|
|
2143
|
+
const end = closesAt(subscription);
|
|
2144
|
+
if (!Number.isNaN(end) && end <= now) return "close";
|
|
2145
|
+
return subscription.access ? "resume" : null;
|
|
2146
|
+
}
|
|
2147
|
+
if (status === "ended") {
|
|
2148
|
+
if (subscription.closable === false) return null;
|
|
2149
|
+
const closed = ["closed", "closed_elsewhere", "authority_closed"].includes(
|
|
2150
|
+
subscription.end_reason ?? ""
|
|
2151
|
+
);
|
|
2152
|
+
return closed ? null : "close";
|
|
2153
|
+
}
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
function payableOf(row, now) {
|
|
2157
|
+
if (row.status !== "unpaid" || row.paused || row.blocked_since) return false;
|
|
2158
|
+
if (row.late_reason === "approval_revoked" || row.late_reason === "authority_closed") {
|
|
2159
|
+
return false;
|
|
2160
|
+
}
|
|
2161
|
+
const deadline = Date.parse(row.retry_deadline ?? "");
|
|
2162
|
+
return Number.isNaN(deadline) || deadline > now;
|
|
2163
|
+
}
|
|
2164
|
+
var PAY_NOW_SENT = "Payment sent. It shows here once the network confirms it.";
|
|
2165
|
+
function payNowRefusal(failure) {
|
|
2166
|
+
if (!(failure instanceof MesubClientError)) return "Something went wrong.";
|
|
2167
|
+
switch (failure.code) {
|
|
2168
|
+
case "insufficient_balance":
|
|
2169
|
+
return `${failure.message} Add funds, then pay again: no retry was spent.`;
|
|
2170
|
+
case "retry_too_soon": {
|
|
2171
|
+
const minutes = failure.retryAfter === null ? null : Math.ceil(failure.retryAfter / 60);
|
|
2172
|
+
return minutes === null ? failure.message : `You can pay again in ${minutes} ${minutes === 1 ? "minute" : "minutes"}.`;
|
|
2173
|
+
}
|
|
2174
|
+
case "unavailable": {
|
|
2175
|
+
return busyFor(failure) === null ? failure.message : "Mesub can't take the payment right now. Nothing was charged.";
|
|
2176
|
+
}
|
|
2177
|
+
case "retries_spent":
|
|
2178
|
+
return "No retries are left for this period.";
|
|
2179
|
+
case "retry_deadline_passed":
|
|
2180
|
+
return "It is too late to pay this period: the subscription stops at its end.";
|
|
2181
|
+
default:
|
|
2182
|
+
return failure.message;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function useSubscriptions() {
|
|
2186
|
+
const { api, revision, manage: open } = useMesubInternal();
|
|
2187
|
+
const [state, setState] = useState4("loading");
|
|
2188
|
+
const [rows, setRows] = useState4([]);
|
|
2189
|
+
const [error, setError] = useState4(null);
|
|
2190
|
+
const [now, setNow] = useState4(() => Date.now());
|
|
2191
|
+
const read = useRef4(0);
|
|
2192
|
+
const alive = useRef4(true);
|
|
2193
|
+
useEffect4(() => {
|
|
2194
|
+
alive.current = true;
|
|
2195
|
+
return () => {
|
|
2196
|
+
alive.current = false;
|
|
2197
|
+
};
|
|
2198
|
+
}, []);
|
|
2199
|
+
const reload = useCallback(async () => {
|
|
2200
|
+
const id = ++read.current;
|
|
2201
|
+
try {
|
|
2202
|
+
const served = await api.subscriptions();
|
|
2203
|
+
if (!alive.current || id !== read.current) return;
|
|
2204
|
+
setRows(served.filter((row) => HELD.has(row.status)));
|
|
2205
|
+
setNow(Date.now());
|
|
2206
|
+
setError(null);
|
|
2207
|
+
setState("ready");
|
|
2208
|
+
} catch (failure) {
|
|
2209
|
+
if (!alive.current || id !== read.current) return;
|
|
2210
|
+
if (signedOut(failure)) {
|
|
2211
|
+
setRows([]);
|
|
2212
|
+
setError(null);
|
|
2213
|
+
setState("signed-out");
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
setError(
|
|
2217
|
+
failure instanceof MesubClientError ? failure.message : "Something went wrong."
|
|
2218
|
+
);
|
|
2219
|
+
setState("error");
|
|
2220
|
+
}
|
|
2221
|
+
}, [api]);
|
|
2222
|
+
useEffect4(() => {
|
|
2223
|
+
void reload();
|
|
2224
|
+
}, [reload, revision]);
|
|
2225
|
+
useEffect4(() => {
|
|
2226
|
+
const ends = rows.filter((row) => row.status === "cancelled").map(closesAt).filter((end) => !Number.isNaN(end) && end > now);
|
|
2227
|
+
if (ends.length === 0) return;
|
|
2228
|
+
const wait = Math.min(Math.min(...ends) - now + 1e3, 2e9);
|
|
2229
|
+
const timer = setTimeout(() => setNow(Date.now()), wait);
|
|
2230
|
+
return () => clearTimeout(timer);
|
|
2231
|
+
}, [rows, now]);
|
|
2232
|
+
const subscriptions = rows.map((row) => ({
|
|
2233
|
+
...row,
|
|
2234
|
+
action: actionOf(row, now),
|
|
2235
|
+
payable: payableOf(row, now)
|
|
2236
|
+
}));
|
|
2237
|
+
const latest = useRef4(subscriptions);
|
|
2238
|
+
latest.current = subscriptions;
|
|
2239
|
+
const manage = useCallback(
|
|
2240
|
+
async (id) => {
|
|
2241
|
+
const held = latest.current.find((row) => row.id === id);
|
|
2242
|
+
if (!held?.action) return null;
|
|
2243
|
+
const { action, payable: _, ...subscription } = held;
|
|
2244
|
+
return open({ subscription, action });
|
|
2245
|
+
},
|
|
2246
|
+
[open]
|
|
2247
|
+
);
|
|
2248
|
+
const payNow = useCallback(
|
|
2249
|
+
async (id) => {
|
|
2250
|
+
try {
|
|
2251
|
+
await api.payNow(id);
|
|
2252
|
+
} catch (failure) {
|
|
2253
|
+
const wait = busyFor(failure);
|
|
2254
|
+
return {
|
|
2255
|
+
ok: false,
|
|
2256
|
+
message: payNowRefusal(failure),
|
|
2257
|
+
...wait === null ? {} : { retryAfter: wait }
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
void reload();
|
|
2261
|
+
return { ok: true, message: PAY_NOW_SENT };
|
|
2262
|
+
},
|
|
2263
|
+
[api, reload]
|
|
2264
|
+
);
|
|
2265
|
+
return { state, subscriptions, error, reload, payNow, manage };
|
|
2266
|
+
}
|
|
2267
|
+
var STATUS = {
|
|
2268
|
+
active: "Active",
|
|
2269
|
+
unpaid: "Payment late",
|
|
2270
|
+
cancelled: "Cancelled",
|
|
2271
|
+
stopped: "Stopped",
|
|
2272
|
+
ended: "Ended"
|
|
2273
|
+
};
|
|
2274
|
+
var ACTION = { cancel: "Cancel", resume: "Resume", close: "Close" };
|
|
2275
|
+
function fact(row) {
|
|
2276
|
+
const pick = (term, iso) => {
|
|
2277
|
+
const date = moment(iso);
|
|
2278
|
+
return date ? { term, date } : null;
|
|
2279
|
+
};
|
|
2280
|
+
if (row.status === "cancelled") {
|
|
2281
|
+
return pick("Access until", row.access_until ?? row.current_period_end);
|
|
2282
|
+
}
|
|
2283
|
+
if (row.status === "unpaid") {
|
|
2284
|
+
return pick("Next try", row.next_retry_at) ?? pick("Pay by", row.retry_deadline);
|
|
2285
|
+
}
|
|
2286
|
+
if (row.status === "active") {
|
|
2287
|
+
return pick("Next charge", row.next_charge_at) ?? pick("Access until", row.access_until);
|
|
2288
|
+
}
|
|
2289
|
+
return null;
|
|
2290
|
+
}
|
|
2291
|
+
function closeFrom(row) {
|
|
2292
|
+
if (row.closable !== false || row.action === "close") return null;
|
|
2293
|
+
return moment(row.closable_from);
|
|
2294
|
+
}
|
|
2295
|
+
function stoppedNote(row) {
|
|
2296
|
+
const open = "Its subscription account stays open on your wallet, so the plan's owner could still charge it: cancel it if you are not coming back.";
|
|
2297
|
+
switch (row.stop_reason) {
|
|
2298
|
+
case "out_of_retries":
|
|
2299
|
+
return `Stopped: every retry of the missed payment failed, and your access has ended. ${open}`;
|
|
2300
|
+
case "retry_deadline_passed":
|
|
2301
|
+
return `Stopped: the missed payment was not paid by the date to pay by, and your access has ended. ${open}`;
|
|
2302
|
+
case "authority_closed":
|
|
2303
|
+
return `Stopped: your wallet's approval for this token was closed for good, so no payment can go through any more, and your access has ended.${row.action === "close" ? " Close it to get its deposit back." : ""}`;
|
|
2304
|
+
default:
|
|
2305
|
+
return "Stopped after missed payments. Nothing more is charged.";
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
function noteOf(row) {
|
|
2309
|
+
if (row.paused) return "Parked: nothing is charged while it is.";
|
|
2310
|
+
if (row.blocked_since && (row.status === "active" || row.status === "unpaid")) {
|
|
2311
|
+
const until = moment(row.access_until);
|
|
2312
|
+
return `Payments are on hold for now. There is nothing for you to do${until ? `, and your access runs until ${until}` : ""}.`;
|
|
2313
|
+
}
|
|
2314
|
+
if (row.status === "unpaid") {
|
|
2315
|
+
if (row.late_reason === "approval_revoked") {
|
|
2316
|
+
return "The last payment did not go through. This wallet no longer lets Mesub charge it: adding funds will not fix it.";
|
|
2317
|
+
}
|
|
2318
|
+
if (row.late_reason === "authority_closed") {
|
|
2319
|
+
return "The last payment did not go through. This wallet closed its authorisation: it can no longer be charged.";
|
|
2320
|
+
}
|
|
2321
|
+
if (row.next_retry_at) {
|
|
2322
|
+
return "The last payment did not go through. Add funds to the wallet, then pay now, with nothing to sign; otherwise Mesub tries again at the next try.";
|
|
2323
|
+
}
|
|
2324
|
+
return row.retry_deadline ? "The last payment did not go through, so access is off. Add funds to the wallet, then pay now, with nothing to sign, before the date to pay by." : "The last payment did not go through. Add funds to the wallet, then pay now.";
|
|
2325
|
+
}
|
|
2326
|
+
if (row.status === "cancelled") {
|
|
2327
|
+
if (row.action === "close") return "Cancelled, and over. Close it to get its deposit back.";
|
|
2328
|
+
if (!row.access) {
|
|
2329
|
+
return "Cancelled. Your last payment was missed, so you have no access, and Mesub will not collect it. Close it once it ends to get its deposit back.";
|
|
2330
|
+
}
|
|
2331
|
+
return "Cancelled. Resume before the end to keep it.";
|
|
2332
|
+
}
|
|
2333
|
+
if (row.status === "stopped") return stoppedNote(row);
|
|
2334
|
+
if (row.status === "ended") {
|
|
2335
|
+
return row.action === "close" ? "Ended. Close it to get its deposit back." : "Ended. Nothing more is charged.";
|
|
2336
|
+
}
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
function usePlans(slugs) {
|
|
2340
|
+
const { plan } = useMesubInternal();
|
|
2341
|
+
const [plans, setPlans] = useState4({});
|
|
2342
|
+
const key = [...new Set(slugs)].sort().join(",");
|
|
2343
|
+
useEffect4(() => {
|
|
2344
|
+
let live = true;
|
|
2345
|
+
for (const slug of key ? key.split(",") : []) {
|
|
2346
|
+
plan(slug).then(
|
|
2347
|
+
(served) => live && setPlans((known) => ({ ...known, [slug]: served })),
|
|
2348
|
+
() => void 0
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
return () => {
|
|
2352
|
+
live = false;
|
|
2353
|
+
};
|
|
2354
|
+
}, [key, plan]);
|
|
2355
|
+
return plans;
|
|
2356
|
+
}
|
|
2357
|
+
function PayNow({ pay }) {
|
|
2358
|
+
const [busy, setBusy] = useState4(false);
|
|
2359
|
+
const [said2, setSaid] = useState4(null);
|
|
2360
|
+
const [wait, setWait] = useState4(0);
|
|
2361
|
+
useEffect4(() => {
|
|
2362
|
+
if (wait <= 0) return;
|
|
2363
|
+
const timer = setTimeout(() => {
|
|
2364
|
+
setWait(wait - 1);
|
|
2365
|
+
if (wait === 1) setSaid(null);
|
|
2366
|
+
}, 1e3);
|
|
2367
|
+
return () => clearTimeout(timer);
|
|
2368
|
+
}, [wait]);
|
|
2369
|
+
async function click() {
|
|
2370
|
+
setBusy(true);
|
|
2371
|
+
try {
|
|
2372
|
+
const result = await pay();
|
|
2373
|
+
setSaid(result);
|
|
2374
|
+
setWait(result.retryAfter ?? 0);
|
|
2375
|
+
} finally {
|
|
2376
|
+
setBusy(false);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
const tone = said2?.ok ? "sent" : wait > 0 ? "wait" : "refused";
|
|
2380
|
+
return /* @__PURE__ */ jsxs5("div", { "data-mesub-pay-now": "", children: [
|
|
2381
|
+
/* @__PURE__ */ jsx6(
|
|
2382
|
+
"button",
|
|
2383
|
+
{
|
|
2384
|
+
type: "button",
|
|
2385
|
+
"data-mesub-action": "pay-now",
|
|
2386
|
+
disabled: busy || wait > 0,
|
|
2387
|
+
"aria-busy": busy || void 0,
|
|
2388
|
+
onClick: () => void click(),
|
|
2389
|
+
children: wait > 0 ? `Pay again in ${wait} s` : "Pay now"
|
|
2390
|
+
}
|
|
2391
|
+
),
|
|
2392
|
+
said2 ? /* @__PURE__ */ jsx6("p", { role: said2.ok ? "status" : "alert", "data-mesub-pay-now-said": tone, children: said2.message }) : null
|
|
2393
|
+
] });
|
|
2394
|
+
}
|
|
2395
|
+
function ManageSubscriptions({ onChanged, ...rest }) {
|
|
2396
|
+
const { theme } = useMesubInternal();
|
|
2397
|
+
const { state, subscriptions, error, reload, payNow, manage } = useSubscriptions();
|
|
2398
|
+
const plans = usePlans(subscriptions.flatMap((row) => row.plan ? [row.plan] : []));
|
|
2399
|
+
const [retrying, setRetrying] = useState4(false);
|
|
2400
|
+
async function act(id) {
|
|
2401
|
+
const changed = await manage(id);
|
|
2402
|
+
if (changed) onChanged?.(changed);
|
|
2403
|
+
}
|
|
2404
|
+
async function retry() {
|
|
2405
|
+
setRetrying(true);
|
|
2406
|
+
try {
|
|
2407
|
+
await reload();
|
|
2408
|
+
} finally {
|
|
2409
|
+
setRetrying(false);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
let body;
|
|
2413
|
+
if (state === "loading") {
|
|
2414
|
+
body = /* @__PURE__ */ jsx6("p", { "data-mesub-notice": "", role: "status", "aria-busy": "true", children: "Loading your subscriptions" });
|
|
2415
|
+
} else if (state === "signed-out") {
|
|
2416
|
+
body = /* @__PURE__ */ jsx6("p", { "data-mesub-empty": "signed-out", children: "Sign in on this site to see your subscriptions." });
|
|
2417
|
+
} else if (state === "error") {
|
|
2418
|
+
body = /* @__PURE__ */ jsxs5(Fragment5, { children: [
|
|
2419
|
+
/* @__PURE__ */ jsx6("p", { role: "alert", "data-mesub-error": "", children: error }),
|
|
2420
|
+
/* @__PURE__ */ jsx6(
|
|
2421
|
+
"button",
|
|
2422
|
+
{
|
|
2423
|
+
type: "button",
|
|
2424
|
+
"data-mesub-retry": "",
|
|
2425
|
+
disabled: retrying,
|
|
2426
|
+
"aria-busy": retrying || void 0,
|
|
2427
|
+
onClick: () => void retry(),
|
|
2428
|
+
children: "Try again"
|
|
2429
|
+
}
|
|
2430
|
+
)
|
|
2431
|
+
] });
|
|
2432
|
+
} else if (subscriptions.length === 0) {
|
|
2433
|
+
body = /* @__PURE__ */ jsx6("p", { "data-mesub-empty": "none", children: "You have no subscription yet." });
|
|
2434
|
+
} else {
|
|
2435
|
+
body = /* @__PURE__ */ jsx6("ul", { "data-mesub-subscription-list": "", "aria-label": "Your subscriptions", children: subscriptions.map((row) => {
|
|
2436
|
+
const plan = row.plan ? plans[row.plan] : void 0;
|
|
2437
|
+
const dated = fact(row);
|
|
2438
|
+
const name = plan?.name ?? row.plan ?? "Subscription";
|
|
2439
|
+
const since = day(row.confirmed_at);
|
|
2440
|
+
const note = noteOf(row);
|
|
2441
|
+
const price = plan ? briefPrice(plan) : null;
|
|
2442
|
+
return /* @__PURE__ */ jsxs5("li", { "data-mesub-subscription": row.id, children: [
|
|
2443
|
+
/* @__PURE__ */ jsxs5("div", { "data-mesub-subscription-head": "", children: [
|
|
2444
|
+
plan?.logo_url ? /* @__PURE__ */ jsx6("img", { "data-mesub-logo": "", src: plan.logo_url, alt: "" }) : /* @__PURE__ */ jsx6("span", { "data-mesub-logo": "", "aria-hidden": "true", children: name.slice(0, 1).toUpperCase() }),
|
|
2445
|
+
/* @__PURE__ */ jsxs5("div", { "data-mesub-subscription-name": "", children: [
|
|
2446
|
+
/* @__PURE__ */ jsx6("strong", { children: name }),
|
|
2447
|
+
plan ? /* @__PURE__ */ jsxs5("span", { children: [
|
|
2448
|
+
"by ",
|
|
2449
|
+
plan.project_name
|
|
2450
|
+
] }) : null
|
|
2451
|
+
] }),
|
|
2452
|
+
/* @__PURE__ */ jsx6("span", { "data-mesub-status": row.paused ? "paused" : row.status, children: row.paused ? "Parked" : STATUS[row.status] ?? row.status })
|
|
2453
|
+
] }),
|
|
2454
|
+
plan && price ? /* @__PURE__ */ jsxs5("p", { "data-mesub-subscription-price": "", children: [
|
|
2455
|
+
/* @__PURE__ */ jsxs5("strong", { title: price.title, "data-mesub-fit": fit(price.text), children: [
|
|
2456
|
+
/* @__PURE__ */ jsx6(TokenLogo, { plan }),
|
|
2457
|
+
price.text
|
|
2458
|
+
] }),
|
|
2459
|
+
" ",
|
|
2460
|
+
cadence(plan.period_hours)
|
|
2461
|
+
] }) : null,
|
|
2462
|
+
/* @__PURE__ */ jsxs5("dl", { "data-mesub-summary": "", children: [
|
|
2463
|
+
dated ? /* @__PURE__ */ jsxs5("div", { "data-mesub-row": "", children: [
|
|
2464
|
+
/* @__PURE__ */ jsx6("dt", { children: dated.term }),
|
|
2465
|
+
/* @__PURE__ */ jsx6("dd", { children: dated.date })
|
|
2466
|
+
] }) : null,
|
|
2467
|
+
since ? /* @__PURE__ */ jsxs5("div", { "data-mesub-row": "", children: [
|
|
2468
|
+
/* @__PURE__ */ jsx6("dt", { children: "Since" }),
|
|
2469
|
+
/* @__PURE__ */ jsx6("dd", { children: since })
|
|
2470
|
+
] }) : null,
|
|
2471
|
+
closeFrom(row) ? /* @__PURE__ */ jsxs5("div", { "data-mesub-row": "", children: [
|
|
2472
|
+
/* @__PURE__ */ jsx6("dt", { children: "Close from" }),
|
|
2473
|
+
/* @__PURE__ */ jsx6("dd", { children: closeFrom(row) })
|
|
2474
|
+
] }) : null,
|
|
2475
|
+
/* @__PURE__ */ jsxs5("div", { "data-mesub-row": "", children: [
|
|
2476
|
+
/* @__PURE__ */ jsx6("dt", { children: "Paid from" }),
|
|
2477
|
+
/* @__PURE__ */ jsx6("dd", { title: row.wallet, children: shortAddress(row.wallet) })
|
|
2478
|
+
] })
|
|
2479
|
+
] }),
|
|
2480
|
+
note ? /* @__PURE__ */ jsx6("p", { "data-mesub-subscription-note": "", children: note }) : null,
|
|
2481
|
+
row.payable ? /* @__PURE__ */ jsx6(PayNow, { pay: () => payNow(row.id) }) : null,
|
|
2482
|
+
row.action ? /* @__PURE__ */ jsx6(
|
|
2483
|
+
"button",
|
|
2484
|
+
{
|
|
2485
|
+
type: "button",
|
|
2486
|
+
"data-mesub-action": row.action,
|
|
2487
|
+
onClick: () => void act(row.id),
|
|
2488
|
+
children: ACTION[row.action]
|
|
2489
|
+
}
|
|
2490
|
+
) : null
|
|
2491
|
+
] }, row.id);
|
|
2492
|
+
}) });
|
|
2493
|
+
}
|
|
2494
|
+
return /* @__PURE__ */ jsx6(
|
|
2495
|
+
"div",
|
|
2496
|
+
{
|
|
2497
|
+
...rest,
|
|
2498
|
+
"data-mesub-subscriptions": "",
|
|
2499
|
+
"data-mesub-state": state,
|
|
2500
|
+
"data-mesub-theme": theme,
|
|
2501
|
+
children: body
|
|
2502
|
+
}
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
var ManageButton = forwardRef(function ManageButton2({ plan, children, onClick, ...rest }, ref) {
|
|
2506
|
+
const { openSubscription, theme } = useMesubInternal();
|
|
2507
|
+
return /* @__PURE__ */ jsx6(
|
|
2508
|
+
"button",
|
|
2509
|
+
{
|
|
2510
|
+
type: "button",
|
|
2511
|
+
...rest,
|
|
2512
|
+
ref,
|
|
2513
|
+
"data-mesub-manage-button": "",
|
|
2514
|
+
"data-mesub-theme": theme,
|
|
2515
|
+
onClick: (event) => {
|
|
2516
|
+
onClick?.(event);
|
|
2517
|
+
if (!event.defaultPrevented) openSubscription(plan);
|
|
2518
|
+
},
|
|
2519
|
+
children: children ?? "Manage subscription"
|
|
2520
|
+
}
|
|
2521
|
+
);
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2524
|
+
// src/detail.tsx
|
|
2525
|
+
import { Fragment as Fragment6, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2526
|
+
var ACTION2 = {
|
|
2527
|
+
cancel: "Cancel subscription",
|
|
2528
|
+
resume: "Resume subscription",
|
|
2529
|
+
close: "Close and get the deposit back"
|
|
2530
|
+
};
|
|
2531
|
+
var UPCOMING = { charge: "Next charge", retry: "Next try" };
|
|
2532
|
+
function usePlan(slug) {
|
|
2533
|
+
const { plan } = useMesubInternal();
|
|
2534
|
+
const [read, setRead] = useState5(void 0);
|
|
2535
|
+
useEffect5(() => {
|
|
2536
|
+
if (!slug) return;
|
|
2537
|
+
let live = true;
|
|
2538
|
+
plan(slug).then(
|
|
2539
|
+
(served) => live && setRead(served),
|
|
2540
|
+
() => void 0
|
|
2541
|
+
);
|
|
2542
|
+
return () => {
|
|
2543
|
+
live = false;
|
|
2544
|
+
};
|
|
2545
|
+
}, [plan, slug]);
|
|
2546
|
+
return slug && read?.slug === slug ? read : void 0;
|
|
2547
|
+
}
|
|
2548
|
+
function useDetail(row) {
|
|
2549
|
+
const { api, revision } = useMesubInternal();
|
|
2550
|
+
const [detail, setDetail] = useState5(null);
|
|
2551
|
+
const id = row?.id;
|
|
2552
|
+
useEffect5(() => {
|
|
2553
|
+
if (!id) return;
|
|
2554
|
+
let live = true;
|
|
2555
|
+
api.subscription(id).then(
|
|
2556
|
+
(served) => live && setDetail(served),
|
|
2557
|
+
// An older server has no such route: the view stands without its payments.
|
|
2558
|
+
() => void 0
|
|
2559
|
+
);
|
|
2560
|
+
return () => {
|
|
2561
|
+
live = false;
|
|
2562
|
+
};
|
|
2563
|
+
}, [api, id, revision]);
|
|
2564
|
+
return detail?.subscription.id === id ? detail : null;
|
|
2565
|
+
}
|
|
2566
|
+
var PAST_SHOWN = 5;
|
|
2567
|
+
var PAST_STEP = 10;
|
|
2568
|
+
function SubscriptionDialog({
|
|
2569
|
+
plan: slug,
|
|
2570
|
+
onClose
|
|
2571
|
+
}) {
|
|
2572
|
+
const [shown, setShown] = useState5(PAST_SHOWN);
|
|
2573
|
+
const titleId = useId3();
|
|
2574
|
+
const { chain } = useMesubInternal();
|
|
2575
|
+
const { state, subscriptions, error, reload, payNow, manage } = useSubscriptions();
|
|
2576
|
+
const row = pickSubscription(subscriptions, slug) ?? null;
|
|
2577
|
+
const plan = usePlan(row?.plan);
|
|
2578
|
+
const detail = useDetail(row);
|
|
2579
|
+
let body;
|
|
2580
|
+
if (state === "loading") {
|
|
2581
|
+
body = /* @__PURE__ */ jsxs6(Fragment6, { children: [
|
|
2582
|
+
/* @__PURE__ */ jsx7("h2", { id: titleId, children: "Your subscription" }),
|
|
2583
|
+
/* @__PURE__ */ jsx7("p", { "data-mesub-notice": "", role: "status", "aria-busy": "true", children: "Reading your subscription" })
|
|
2584
|
+
] });
|
|
2585
|
+
} else if (state === "signed-out") {
|
|
2586
|
+
body = /* @__PURE__ */ jsxs6(Fragment6, { children: [
|
|
2587
|
+
/* @__PURE__ */ jsx7("h2", { id: titleId, children: "Sign in first" }),
|
|
2588
|
+
/* @__PURE__ */ jsx7("p", { children: "Sign in on this site to see your subscription." })
|
|
2589
|
+
] });
|
|
2590
|
+
} else if (state === "error") {
|
|
2591
|
+
body = /* @__PURE__ */ jsxs6(Fragment6, { children: [
|
|
2592
|
+
/* @__PURE__ */ jsx7("h2", { id: titleId, children: "Your subscription" }),
|
|
2593
|
+
/* @__PURE__ */ jsx7("p", { role: "alert", "data-mesub-error": "", children: error }),
|
|
2594
|
+
/* @__PURE__ */ jsx7("button", { type: "button", "data-mesub-retry": "", onClick: () => void reload(), children: "Try again" })
|
|
2595
|
+
] });
|
|
2596
|
+
} else if (!row) {
|
|
2597
|
+
body = /* @__PURE__ */ jsxs6(Fragment6, { children: [
|
|
2598
|
+
/* @__PURE__ */ jsx7("h2", { id: titleId, children: "No subscription yet" }),
|
|
2599
|
+
/* @__PURE__ */ jsx7("p", { children: "You are not subscribed to this plan." })
|
|
2600
|
+
] });
|
|
2601
|
+
} else {
|
|
2602
|
+
const name = plan?.name ?? row.plan ?? "Subscription";
|
|
2603
|
+
const dated = fact(row);
|
|
2604
|
+
const since = day(row.confirmed_at);
|
|
2605
|
+
const note = noteOf(row);
|
|
2606
|
+
const upcoming = detail?.upcoming ?? [];
|
|
2607
|
+
const past = detail?.payments ?? [];
|
|
2608
|
+
const total = detail?.paid?.amount ? paidBrief(detail.paid.amount, plan, formatAmount) : null;
|
|
2609
|
+
const rate = plan ? briefPrice(plan) : null;
|
|
2610
|
+
const due = (display) => display ? `${grouped(display)}${symbolOf(plan)}` : "";
|
|
2611
|
+
const facts = upcoming.length > 0 && dated?.term !== "Access until" ? null : dated;
|
|
2612
|
+
body = /* @__PURE__ */ jsxs6(Fragment6, { children: [
|
|
2613
|
+
/* @__PURE__ */ jsxs6("div", { "data-mesub-identity": "", children: [
|
|
2614
|
+
plan?.logo_url ? /* @__PURE__ */ jsx7("img", { "data-mesub-logo": "", src: plan.logo_url, alt: "" }) : /* @__PURE__ */ jsx7("span", { "data-mesub-logo": "", "aria-hidden": "true", children: name.slice(0, 1).toUpperCase() }),
|
|
2615
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
2616
|
+
/* @__PURE__ */ jsx7("h2", { id: titleId, children: name }),
|
|
2617
|
+
plan ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
2618
|
+
"by ",
|
|
2619
|
+
plan.project_name
|
|
2620
|
+
] }) : null
|
|
2621
|
+
] }),
|
|
2622
|
+
/* @__PURE__ */ jsx7("span", { "data-mesub-status": row.paused ? "paused" : row.status, children: row.paused ? "Parked" : STATUS[row.status] ?? row.status })
|
|
2623
|
+
] }),
|
|
2624
|
+
plan && rate ? /* @__PURE__ */ jsxs6("p", { "data-mesub-rate": "", children: [
|
|
2625
|
+
/* @__PURE__ */ jsxs6("strong", { title: rate.title, "data-mesub-fit": fit(rate.text), children: [
|
|
2626
|
+
/* @__PURE__ */ jsx7(TokenLogo, { plan }),
|
|
2627
|
+
rate.text
|
|
2628
|
+
] }),
|
|
2629
|
+
" ",
|
|
2630
|
+
cadence(plan.period_hours)
|
|
2631
|
+
] }) : null,
|
|
2632
|
+
note ? /* @__PURE__ */ jsx7("p", { "data-mesub-state-note": "", children: note }) : null,
|
|
2633
|
+
/* @__PURE__ */ jsxs6("dl", { "data-mesub-summary": "", children: [
|
|
2634
|
+
facts ? /* @__PURE__ */ jsxs6("div", { "data-mesub-row": "", children: [
|
|
2635
|
+
/* @__PURE__ */ jsx7("dt", { children: facts.term }),
|
|
2636
|
+
/* @__PURE__ */ jsx7("dd", { children: facts.date })
|
|
2637
|
+
] }) : null,
|
|
2638
|
+
since ? /* @__PURE__ */ jsxs6("div", { "data-mesub-row": "", children: [
|
|
2639
|
+
/* @__PURE__ */ jsx7("dt", { children: "Since" }),
|
|
2640
|
+
/* @__PURE__ */ jsx7("dd", { children: since })
|
|
2641
|
+
] }) : null,
|
|
2642
|
+
total ? /* @__PURE__ */ jsxs6("div", { "data-mesub-row": "", children: [
|
|
2643
|
+
/* @__PURE__ */ jsx7("dt", { children: "Total paid" }),
|
|
2644
|
+
/* @__PURE__ */ jsx7("dd", { title: total.title, children: total.text })
|
|
2645
|
+
] }) : null,
|
|
2646
|
+
planEnd(plan?.ends_at) ? /* @__PURE__ */ jsxs6("div", { "data-mesub-row": "", children: [
|
|
2647
|
+
/* @__PURE__ */ jsx7("dt", { children: "Plan ends" }),
|
|
2648
|
+
/* @__PURE__ */ jsx7("dd", { children: planEnd(plan?.ends_at) })
|
|
2649
|
+
] }) : null,
|
|
2650
|
+
closeFrom(row) ? /* @__PURE__ */ jsxs6("div", { "data-mesub-row": "", children: [
|
|
2651
|
+
/* @__PURE__ */ jsx7("dt", { children: "Close from" }),
|
|
2652
|
+
/* @__PURE__ */ jsx7("dd", { children: closeFrom(row) })
|
|
2653
|
+
] }) : null,
|
|
2654
|
+
/* @__PURE__ */ jsxs6("div", { "data-mesub-row": "", children: [
|
|
2655
|
+
/* @__PURE__ */ jsx7("dt", { children: "Pays from" }),
|
|
2656
|
+
/* @__PURE__ */ jsx7("dd", { title: row.wallet, children: shortAddress(row.wallet) })
|
|
2657
|
+
] })
|
|
2658
|
+
] }),
|
|
2659
|
+
upcoming.length > 0 || past.length > 0 ? (
|
|
2660
|
+
// Closed until asked for, as the terms are on the checkout: same toggle.
|
|
2661
|
+
/* @__PURE__ */ jsxs6("details", { "data-mesub-terms": "", "data-mesub-payments": "", children: [
|
|
2662
|
+
/* @__PURE__ */ jsx7("summary", { children: "Payments" }),
|
|
2663
|
+
upcoming.length > 0 ? /* @__PURE__ */ jsxs6("section", { "aria-label": "Incoming payments", children: [
|
|
2664
|
+
/* @__PURE__ */ jsx7("h3", { children: "Incoming" }),
|
|
2665
|
+
/* @__PURE__ */ jsx7("ul", { children: upcoming.map((next) => /* @__PURE__ */ jsxs6(
|
|
2666
|
+
"li",
|
|
2667
|
+
{
|
|
2668
|
+
"data-mesub-payment": "upcoming",
|
|
2669
|
+
children: [
|
|
2670
|
+
/* @__PURE__ */ jsx7("span", { children: shortMoment(next.due_at) }),
|
|
2671
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
2672
|
+
UPCOMING[next.kind] ?? next.kind,
|
|
2673
|
+
retryOf(next) ? /* @__PURE__ */ jsx7("small", { title: "Retry", children: retryOf(next) }) : null
|
|
2674
|
+
] }),
|
|
2675
|
+
/* @__PURE__ */ jsx7("span", { "data-mesub-fit": fit(due(next.amount_display)), children: due(next.amount_display) })
|
|
2676
|
+
]
|
|
2677
|
+
},
|
|
2678
|
+
`next-${next.due_at}`
|
|
2679
|
+
)) })
|
|
2680
|
+
] }) : null,
|
|
2681
|
+
past.length > 0 ? /* @__PURE__ */ jsxs6("section", { "aria-label": "Past payments", children: [
|
|
2682
|
+
/* @__PURE__ */ jsx7("h3", { children: "Past" }),
|
|
2683
|
+
/* @__PURE__ */ jsx7("ul", { children: past.slice(0, shown).map((payment) => {
|
|
2684
|
+
const said2 = paymentSaid(payment);
|
|
2685
|
+
const amount = paidIn(payment.amount, plan, formatAmount);
|
|
2686
|
+
const usd = plan && amount && !tokenOf(plan).vouched ? formatUsd(payment.amount_usd) : null;
|
|
2687
|
+
return /* @__PURE__ */ jsxs6(
|
|
2688
|
+
"li",
|
|
2689
|
+
{
|
|
2690
|
+
"data-mesub-payment": said2.tone,
|
|
2691
|
+
children: [
|
|
2692
|
+
/* @__PURE__ */ jsx7("span", { children: shortMoment(payment.attempted_at) }),
|
|
2693
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
2694
|
+
said2.label,
|
|
2695
|
+
retryOf(payment) ? /* @__PURE__ */ jsx7("small", { title: "Retry", children: retryOf(payment) }) : null
|
|
2696
|
+
] }),
|
|
2697
|
+
/* @__PURE__ */ jsxs6("span", { "data-mesub-fit": fit(amount ?? ""), children: [
|
|
2698
|
+
payment.signature && payment.outcome === "paid" ? /* @__PURE__ */ jsx7(
|
|
2699
|
+
"a",
|
|
2700
|
+
{
|
|
2701
|
+
href: explorerUrl(
|
|
2702
|
+
payment.signature,
|
|
2703
|
+
chain
|
|
2704
|
+
),
|
|
2705
|
+
target: "_blank",
|
|
2706
|
+
rel: "noopener noreferrer",
|
|
2707
|
+
title: "See the transaction",
|
|
2708
|
+
children: amount ?? "Receipt"
|
|
2709
|
+
}
|
|
2710
|
+
) : amount,
|
|
2711
|
+
usd ? /* @__PURE__ */ jsx7("small", { children: usd }) : null
|
|
2712
|
+
] })
|
|
2713
|
+
]
|
|
2714
|
+
},
|
|
2715
|
+
`${payment.attempted_at}-${payment.signature ?? ""}`
|
|
2716
|
+
);
|
|
2717
|
+
}) }),
|
|
2718
|
+
past.length > shown ? /* @__PURE__ */ jsx7(
|
|
2719
|
+
"button",
|
|
2720
|
+
{
|
|
2721
|
+
type: "button",
|
|
2722
|
+
"data-mesub-more": "",
|
|
2723
|
+
onClick: () => setShown(shown + PAST_STEP),
|
|
2724
|
+
children: "Show more"
|
|
2725
|
+
}
|
|
2726
|
+
) : null
|
|
2727
|
+
] }) : null
|
|
2728
|
+
] })
|
|
2729
|
+
) : null,
|
|
2730
|
+
row.payable ? /* @__PURE__ */ jsx7(PayNow, { pay: () => payNow(row.id) }) : null,
|
|
2731
|
+
row.action ? /* @__PURE__ */ jsx7(
|
|
2732
|
+
"button",
|
|
2733
|
+
{
|
|
2734
|
+
type: "button",
|
|
2735
|
+
"data-mesub-action": row.action,
|
|
2736
|
+
onClick: () => void manage(row.id),
|
|
2737
|
+
children: ACTION2[row.action]
|
|
2738
|
+
}
|
|
2739
|
+
) : null
|
|
2740
|
+
] });
|
|
2741
|
+
}
|
|
2742
|
+
return /* @__PURE__ */ jsx7(
|
|
2743
|
+
MesubDialog,
|
|
2744
|
+
{
|
|
2745
|
+
titleId,
|
|
2746
|
+
onClose,
|
|
2747
|
+
screen: {
|
|
2748
|
+
step: "subscription",
|
|
2749
|
+
view: `subscription-${state}-${row?.id ?? "none"}`,
|
|
2750
|
+
body: /* @__PURE__ */ jsxs6(Fragment6, { children: [
|
|
2751
|
+
body,
|
|
2752
|
+
/* @__PURE__ */ jsx7("button", { type: "button", "data-mesub-cancel": "", onClick: onClose, children: "Done" })
|
|
2753
|
+
] })
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// src/provider.tsx
|
|
2760
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2761
|
+
function MesubProvider({
|
|
2762
|
+
endpoint,
|
|
2763
|
+
chain = "solana:mainnet",
|
|
2764
|
+
fetch,
|
|
2765
|
+
theme,
|
|
2766
|
+
manageUrl,
|
|
2767
|
+
children
|
|
2768
|
+
}) {
|
|
2769
|
+
const api = useMemo(() => createApi({ endpoint, fetch }), [endpoint, fetch]);
|
|
2770
|
+
const plans = useMemo(() => /* @__PURE__ */ new Map(), [api]);
|
|
2771
|
+
const [open, setOpen] = useState6(null);
|
|
2772
|
+
const [revision, setRevision] = useState6(0);
|
|
2773
|
+
const pending = useRef5(null);
|
|
2774
|
+
const [managed, setManaged] = useState6(null);
|
|
2775
|
+
const openSubscription = useCallback2((plan2) => setManaged({ plan: plan2 }), []);
|
|
2776
|
+
useEffect6(
|
|
2777
|
+
() => () => {
|
|
2778
|
+
pending.current?.resolve(null);
|
|
2779
|
+
pending.current = null;
|
|
2780
|
+
},
|
|
2781
|
+
[]
|
|
2782
|
+
);
|
|
2783
|
+
const plan = useCallback2(
|
|
2784
|
+
(slug) => {
|
|
2785
|
+
let read = plans.get(slug);
|
|
2786
|
+
if (!read) {
|
|
2787
|
+
read = api.plan(slug);
|
|
2788
|
+
plans.set(slug, read);
|
|
2789
|
+
read.catch(() => plans.delete(slug));
|
|
2790
|
+
}
|
|
2791
|
+
return read;
|
|
2792
|
+
},
|
|
2793
|
+
[api, plans]
|
|
2794
|
+
);
|
|
2795
|
+
const start = useCallback2((next) => {
|
|
2796
|
+
if (pending.current) return pending.current.promise;
|
|
2797
|
+
let resolve;
|
|
2798
|
+
const promise = new Promise((done) => {
|
|
2799
|
+
resolve = done;
|
|
2800
|
+
});
|
|
2801
|
+
pending.current = { promise, resolve, result: null };
|
|
2802
|
+
setOpen(next);
|
|
2803
|
+
return promise;
|
|
2804
|
+
}, []);
|
|
2805
|
+
const subscribe = useCallback2(
|
|
2806
|
+
(request) => start({ kind: "subscribe", request }),
|
|
2807
|
+
[start]
|
|
2808
|
+
);
|
|
2809
|
+
const manage = useCallback2(
|
|
2810
|
+
(request) => start({ kind: "manage", request }),
|
|
2811
|
+
[start]
|
|
2812
|
+
);
|
|
2813
|
+
const settled = useCallback2((subscription) => {
|
|
2814
|
+
if (pending.current && subscription) pending.current.result = subscription;
|
|
2815
|
+
setRevision((current) => current + 1);
|
|
2816
|
+
}, []);
|
|
2817
|
+
const close = useCallback2(() => {
|
|
2818
|
+
pending.current?.resolve(pending.current.result);
|
|
2819
|
+
pending.current = null;
|
|
2820
|
+
setOpen(null);
|
|
2821
|
+
}, []);
|
|
2822
|
+
const value = useMemo(
|
|
2823
|
+
() => ({
|
|
2824
|
+
api,
|
|
2825
|
+
chain,
|
|
2826
|
+
theme,
|
|
2827
|
+
manageUrl,
|
|
2828
|
+
plan,
|
|
2829
|
+
revision,
|
|
2830
|
+
subscribe,
|
|
2831
|
+
manage,
|
|
2832
|
+
openSubscription
|
|
2833
|
+
}),
|
|
2834
|
+
[api, chain, theme, manageUrl, plan, revision, subscribe, manage, openSubscription]
|
|
2835
|
+
);
|
|
2836
|
+
return /* @__PURE__ */ jsxs7(MesubContext.Provider, { value, children: [
|
|
2837
|
+
children,
|
|
2838
|
+
open?.kind === "subscribe" ? /* @__PURE__ */ jsx8(
|
|
2839
|
+
Checkout,
|
|
2840
|
+
{
|
|
2841
|
+
plan: open.request.plan,
|
|
2842
|
+
onState: open.request.onState,
|
|
2843
|
+
onSubscribed: (subscription, signature) => {
|
|
2844
|
+
settled(subscription);
|
|
2845
|
+
open.request.onSubscribed(subscription, signature);
|
|
2846
|
+
},
|
|
2847
|
+
onClose: close
|
|
2848
|
+
}
|
|
2849
|
+
) : null,
|
|
2850
|
+
managed && !open ? /* @__PURE__ */ jsx8(SubscriptionDialog, { plan: managed.plan, onClose: () => setManaged(null) }) : null,
|
|
2851
|
+
open?.kind === "manage" ? /* @__PURE__ */ jsx8(
|
|
2852
|
+
Manage,
|
|
2853
|
+
{
|
|
2854
|
+
subscription: open.request.subscription,
|
|
2855
|
+
action: open.request.action,
|
|
2856
|
+
onChanged: settled,
|
|
2857
|
+
onClose: close
|
|
2858
|
+
}
|
|
2859
|
+
) : null
|
|
2860
|
+
] });
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// src/subscribe-button.tsx
|
|
2864
|
+
import {
|
|
2865
|
+
forwardRef as forwardRef2,
|
|
2866
|
+
useCallback as useCallback3,
|
|
2867
|
+
useEffect as useEffect7,
|
|
2868
|
+
useRef as useRef6,
|
|
2869
|
+
useState as useState7
|
|
2870
|
+
} from "react";
|
|
2871
|
+
import { Fragment as Fragment7, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2872
|
+
function useSubscribe(plan, options = {}) {
|
|
2873
|
+
const { subscribe: open } = useMesubInternal();
|
|
2874
|
+
const [state, setState] = useState7("idle");
|
|
2875
|
+
const [subscription, setSubscription] = useState7(null);
|
|
2876
|
+
const [signature, setSignature] = useState7(null);
|
|
2877
|
+
const latest = useRef6(options);
|
|
2878
|
+
latest.current = options;
|
|
2879
|
+
const alive = useRef6(true);
|
|
2880
|
+
useEffect7(() => {
|
|
2881
|
+
alive.current = true;
|
|
2882
|
+
return () => {
|
|
2883
|
+
alive.current = false;
|
|
2884
|
+
};
|
|
2885
|
+
}, []);
|
|
2886
|
+
const subscribe = useCallback3(async () => {
|
|
2887
|
+
let done = false;
|
|
2888
|
+
const result = await open({
|
|
2889
|
+
plan,
|
|
2890
|
+
onState: (next) => alive.current && setState(next),
|
|
2891
|
+
onSubscribed: (confirmed, paid) => {
|
|
2892
|
+
done = true;
|
|
2893
|
+
if (alive.current) {
|
|
2894
|
+
setSubscription(confirmed);
|
|
2895
|
+
setSignature(paid);
|
|
2896
|
+
setState("subscribed");
|
|
2897
|
+
}
|
|
2898
|
+
latest.current.onSubscribed?.(confirmed);
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
if (alive.current) setState(done ? "subscribed" : "idle");
|
|
2902
|
+
return result;
|
|
2903
|
+
}, [open, plan]);
|
|
2904
|
+
return { state, subscription, signature, subscribe };
|
|
2905
|
+
}
|
|
2906
|
+
var LABELS = {
|
|
2907
|
+
signing: "Approve in your wallet",
|
|
2908
|
+
confirming: "Confirming",
|
|
2909
|
+
subscribed: "Subscribed"
|
|
2910
|
+
};
|
|
2911
|
+
var SubscribeButton = forwardRef2(
|
|
2912
|
+
function SubscribeButton2({ plan, onSubscribed, children, onClick, disabled, type, ...rest }, ref) {
|
|
2913
|
+
const { chain, theme } = useMesubInternal();
|
|
2914
|
+
const { state, subscription, signature, subscribe } = useSubscribe(plan, { onSubscribed });
|
|
2915
|
+
const busy = state === "signing" || state === "confirming";
|
|
2916
|
+
const due = moment(subscription?.next_charge_at);
|
|
2917
|
+
function click(event) {
|
|
2918
|
+
if (busy) return;
|
|
2919
|
+
onClick?.(event);
|
|
2920
|
+
if (!event.defaultPrevented) void subscribe();
|
|
2921
|
+
}
|
|
2922
|
+
return /* @__PURE__ */ jsxs8(Fragment7, { children: [
|
|
2923
|
+
/* @__PURE__ */ jsx9(
|
|
2924
|
+
"button",
|
|
2925
|
+
{
|
|
2926
|
+
...rest,
|
|
2927
|
+
ref,
|
|
2928
|
+
type: type ?? "button",
|
|
2929
|
+
disabled: Boolean(disabled) || state === "subscribed",
|
|
2930
|
+
"aria-busy": busy || void 0,
|
|
2931
|
+
"aria-disabled": busy || void 0,
|
|
2932
|
+
onClick: click,
|
|
2933
|
+
"data-mesub-subscribe": "",
|
|
2934
|
+
"data-mesub-state": state,
|
|
2935
|
+
"data-mesub-theme": theme,
|
|
2936
|
+
children: LABELS[state] ?? children ?? "Subscribe"
|
|
2937
|
+
}
|
|
2938
|
+
),
|
|
2939
|
+
state === "subscribed" && (due || signature) ? /* @__PURE__ */ jsxs8("span", { "data-mesub-receipt": "", "data-mesub-theme": theme, children: [
|
|
2940
|
+
due ? `Next charge ${due}` : null,
|
|
2941
|
+
due && signature ? " \xB7 " : null,
|
|
2942
|
+
signature ? /* @__PURE__ */ jsx9("a", { href: explorerUrl(signature, chain), target: "_blank", rel: "noopener", children: "Receipt" }) : null
|
|
2943
|
+
] }) : null
|
|
2944
|
+
] });
|
|
2945
|
+
}
|
|
2946
|
+
);
|
|
2947
|
+
export {
|
|
2948
|
+
ManageButton,
|
|
2949
|
+
ManageSubscriptions,
|
|
2950
|
+
MesubProvider,
|
|
2951
|
+
SubscribeButton,
|
|
2952
|
+
useSubscribe,
|
|
2953
|
+
useSubscriptions
|
|
2954
|
+
};
|
|
2955
|
+
//# sourceMappingURL=index.js.map
|