@blocklet/payment-react 1.18.47 → 1.18.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -321,6 +321,56 @@ function DonationPage() {
321
321
 
322
322
  ### Subscription Management Example
323
323
 
324
+ - `ResumeSubscription` component
325
+ - Resume subscription, with support for re-stake if needed
326
+ - Props:
327
+ - `subscriptionId`: [Required] The subscription ID to resume
328
+ - `onResumed`: [Optional] Callback function called after successful resume, receives `(subscription)`
329
+ - `dialogProps`: [Optional] Dialog properties, default is `{ open: true }`
330
+ - `successToast`: [Optional] Whether to show success toast, default is `true`
331
+ - `authToken`: [Optional] Authentication token for API requests
332
+
333
+ ```tsx
334
+ import {
335
+ PaymentProvider,
336
+ ResumeSubscription,
337
+ CustomerInvoiceList,
338
+ Amount
339
+ } from '@blocklet/payment-react';
340
+
341
+ function SubscriptionPage({ subscriptionId }) {
342
+ return (
343
+ <PaymentProvider session={session}>
344
+ <ResumeSubscription
345
+ subscriptionId={subscriptionId}
346
+ onResumed={(subscription) => {
347
+ // Refresh subscription status
348
+ refetchSubscription();
349
+ }}
350
+ />
351
+
352
+ {/* Custom dialog props */}
353
+ <ResumeSubscription
354
+ subscriptionId={subscriptionId}
355
+ dialogProps={{
356
+ open: true,
357
+ title: 'Resume Your Subscription',
358
+ onClose: () => {
359
+ // Handle dialog close
360
+ }
361
+ }}
362
+ />
363
+
364
+ {/* With auth token */}
365
+ <ResumeSubscription
366
+ subscriptionId={subscriptionId}
367
+ authToken="your-auth-token"
368
+ />
369
+ </PaymentProvider>
370
+ );
371
+ }
372
+ ```
373
+
324
374
  - `OverdueInvoicePayment` component
325
375
  - Display overdue invoices for a subscription, and support batch payment
326
376
  - Props:
@@ -44,7 +44,7 @@ const LoadingButton = forwardRef(
44
44
  Button,
45
45
  {
46
46
  ref,
47
- disabled: props.disabled,
47
+ disabled: props.disabled || loading,
48
48
  onClick: handleClick,
49
49
  sx: {
50
50
  position: "relative",
@@ -0,0 +1,25 @@
1
+ import type { Subscription, TSubscriptionExpanded } from '@blocklet/payment-types';
2
+ type DialogProps = {
3
+ open?: boolean;
4
+ onClose?: () => void;
5
+ title?: string;
6
+ };
7
+ type Props = {
8
+ subscriptionId: string;
9
+ onResumed?: (subscription: Subscription | TSubscriptionExpanded) => void;
10
+ dialogProps?: DialogProps;
11
+ successToast?: boolean;
12
+ authToken?: string;
13
+ };
14
+ declare function RecoverSubscription({ subscriptionId, dialogProps, onResumed, successToast, authToken, }: Props): import("react").JSX.Element | null;
15
+ declare namespace RecoverSubscription {
16
+ var defaultProps: {
17
+ onResumed: () => void;
18
+ dialogProps: {
19
+ open: boolean;
20
+ };
21
+ successToast: boolean;
22
+ authToken: undefined;
23
+ };
24
+ }
25
+ export default RecoverSubscription;
@@ -0,0 +1,158 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Button, Typography, Stack, Alert } from "@mui/material";
4
+ import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
5
+ import Toast from "@arcblock/ux/lib/Toast";
6
+ import { joinURL } from "ufo";
7
+ import { useRequest } from "ahooks";
8
+ import { Dialog } from "@arcblock/ux";
9
+ import { usePaymentContext } from "../contexts/payment.js";
10
+ import { formatError, formatToDate, getPrefix, isCrossOrigin } from "../libs/util.js";
11
+ import api from "../libs/api.js";
12
+ import LoadingButton from "./loading-button.js";
13
+ const fetchSubscriptionDetail = async (params) => {
14
+ const url = `/api/subscriptions/${params.subscriptionId}`;
15
+ const res = await api.get(params.authToken ? joinURL(url, `?authToken=${params.authToken}`) : url);
16
+ return res.data;
17
+ };
18
+ const fetchRecoverInfo = async (params) => {
19
+ const url = `/api/subscriptions/${params.subscriptionId}/recover-info`;
20
+ const res = await api.get(params.authToken ? joinURL(url, `?authToken=${params.authToken}`) : url);
21
+ return res.data;
22
+ };
23
+ const recoverSubscription = async (params) => {
24
+ const url = `/api/subscriptions/${params.subscriptionId}/recover`;
25
+ const res = await api.put(params.authToken ? joinURL(url, `?authToken=${params.authToken}`) : url);
26
+ return res.data;
27
+ };
28
+ function RecoverSubscription({
29
+ subscriptionId,
30
+ dialogProps = {},
31
+ onResumed = () => {
32
+ },
33
+ successToast = true,
34
+ authToken
35
+ }) {
36
+ const { t, locale } = useLocaleContext();
37
+ const { connect } = usePaymentContext();
38
+ const [recoverLoading, setRecoverLoading] = useState(false);
39
+ const [dialogOpen, setDialogOpen] = useState(dialogProps.open || false);
40
+ const { data, error, loading } = useRequest(() => fetchRecoverInfo({ subscriptionId, authToken }), {
41
+ ready: !!subscriptionId
42
+ });
43
+ const isCrossOriginRequest = isCrossOrigin();
44
+ const needStake = data?.needStake ?? false;
45
+ const handleSuccess = async () => {
46
+ try {
47
+ const subscription = await fetchSubscriptionDetail({ subscriptionId, authToken });
48
+ if (successToast) {
49
+ Toast.success(t("payment.customer.recover.success"));
50
+ }
51
+ setDialogOpen(false);
52
+ onResumed(subscription);
53
+ setRecoverLoading(false);
54
+ } catch (err) {
55
+ console.error("Failed to fetch updated subscription:", err);
56
+ Toast.error(formatError(err));
57
+ setRecoverLoading(false);
58
+ }
59
+ };
60
+ const handleRecoverWithStake = () => {
61
+ setRecoverLoading(true);
62
+ try {
63
+ connect.open({
64
+ locale,
65
+ containerEl: void 0,
66
+ saveConnect: false,
67
+ action: "re-stake",
68
+ prefix: joinURL(getPrefix(), "/api/did"),
69
+ useSocket: !isCrossOriginRequest,
70
+ extraParams: { subscriptionId },
71
+ messages: {
72
+ scan: t("common.connect.defaultScan"),
73
+ title: t("payment.customer.recover.reStakeTitle"),
74
+ confirm: t("common.connect.confirm")
75
+ },
76
+ onSuccess: handleSuccess,
77
+ onClose: () => {
78
+ connect.close();
79
+ setRecoverLoading(false);
80
+ },
81
+ onError: (err) => {
82
+ Toast.error(formatError(err));
83
+ setRecoverLoading(false);
84
+ }
85
+ });
86
+ } catch (err) {
87
+ Toast.error(formatError(err));
88
+ setRecoverLoading(false);
89
+ }
90
+ };
91
+ const handleDirectRecover = async () => {
92
+ setRecoverLoading(true);
93
+ try {
94
+ const result = await recoverSubscription({ subscriptionId, authToken });
95
+ if (result.needStake) {
96
+ handleRecoverWithStake();
97
+ return;
98
+ }
99
+ const { subscription } = result;
100
+ if (successToast) {
101
+ Toast.success(t("payment.customer.recover.success"));
102
+ }
103
+ setDialogOpen(false);
104
+ onResumed(subscription);
105
+ setRecoverLoading(false);
106
+ } catch (err) {
107
+ Toast.error(formatError(err));
108
+ setRecoverLoading(false);
109
+ }
110
+ };
111
+ const handleRecover = () => {
112
+ if (needStake) {
113
+ handleRecoverWithStake();
114
+ } else {
115
+ handleDirectRecover();
116
+ }
117
+ };
118
+ const handleClose = () => {
119
+ setDialogOpen(false);
120
+ dialogProps.onClose?.();
121
+ };
122
+ if (loading) {
123
+ return null;
124
+ }
125
+ return /* @__PURE__ */ jsx(
126
+ Dialog,
127
+ {
128
+ PaperProps: {
129
+ style: { minHeight: "auto" }
130
+ },
131
+ ...dialogProps || {},
132
+ open: dialogOpen,
133
+ title: dialogProps?.title || t("payment.customer.recover.title"),
134
+ sx: { "& .MuiDialogContent-root": { pt: 0 } },
135
+ onClose: handleClose,
136
+ children: error ? /* @__PURE__ */ jsx(Alert, { severity: "error", children: error.message }) : /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
137
+ /* @__PURE__ */ jsx(Typography, { color: "text.secondary", variant: "body2", children: t("payment.customer.recover.description", {
138
+ date: formatToDate(Number(data?.subscription?.current_period_end || "0") * 1e3)
139
+ }) }),
140
+ needStake && /* @__PURE__ */ jsx(Alert, { severity: "warning", children: t("payment.customer.recover.stakeRequiredDescription") }),
141
+ /* @__PURE__ */ jsxs(Stack, { direction: "row", justifyContent: "flex-end", gap: 2, mt: 2, children: [
142
+ /* @__PURE__ */ jsx(Button, { variant: "outlined", color: "primary", onClick: handleClose, children: t("common.cancel") }),
143
+ /* @__PURE__ */ jsx(LoadingButton, { variant: "contained", size: "small", loading: recoverLoading, onClick: handleRecover, children: t("common.confirm") })
144
+ ] })
145
+ ] })
146
+ }
147
+ );
148
+ }
149
+ RecoverSubscription.defaultProps = {
150
+ onResumed: () => {
151
+ },
152
+ dialogProps: {
153
+ open: true
154
+ },
155
+ successToast: true,
156
+ authToken: void 0
157
+ };
158
+ export default RecoverSubscription;
package/es/index.d.ts CHANGED
@@ -31,6 +31,7 @@ import { createLazyComponent } from './components/lazy-loader';
31
31
  import OverdueInvoicePayment from './components/over-due-invoice-payment';
32
32
  import PaymentBeneficiaries from './components/payment-beneficiaries';
33
33
  import LoadingButton from './components/loading-button';
34
+ import ResumeSubscription from './components/resume-subscription';
34
35
  export { PaymentThemeProvider } from './theme';
35
36
  export * from './libs/util';
36
37
  export * from './libs/connect';
@@ -45,4 +46,4 @@ export * from './hooks/scroll';
45
46
  export * from './hooks/keyboard';
46
47
  export * from './libs/validator';
47
48
  export { translations, createTranslator } from './locales';
48
- export { createLazyComponent, api, dayjs, FormInput, PhoneInput, AddressForm, StripeForm, Status, Livemode, Switch, ConfirmDialog, CheckoutForm, CheckoutTable, CheckoutDonate, CurrencySelector, Payment, PaymentSummary, PricingTable, ProductSkeleton, Amount, CustomerInvoiceList, CustomerPaymentList, TxLink, TxGas, SafeGuard, PricingItem, CountrySelect, Table, TruncatedText, Link, OverdueInvoicePayment, PaymentBeneficiaries, LoadingButton, DonateDetails, };
49
+ export { createLazyComponent, api, dayjs, FormInput, PhoneInput, AddressForm, StripeForm, Status, Livemode, Switch, ConfirmDialog, CheckoutForm, CheckoutTable, CheckoutDonate, CurrencySelector, Payment, PaymentSummary, PricingTable, ProductSkeleton, Amount, CustomerInvoiceList, CustomerPaymentList, TxLink, TxGas, SafeGuard, PricingItem, CountrySelect, Table, TruncatedText, Link, OverdueInvoicePayment, PaymentBeneficiaries, LoadingButton, DonateDetails, ResumeSubscription, };
package/es/index.js CHANGED
@@ -31,6 +31,7 @@ import { createLazyComponent } from "./components/lazy-loader.js";
31
31
  import OverdueInvoicePayment from "./components/over-due-invoice-payment.js";
32
32
  import PaymentBeneficiaries from "./components/payment-beneficiaries.js";
33
33
  import LoadingButton from "./components/loading-button.js";
34
+ import ResumeSubscription from "./components/resume-subscription.js";
34
35
  export { PaymentThemeProvider } from "./theme/index.js";
35
36
  export * from "./libs/util.js";
36
37
  export * from "./libs/connect.js";
@@ -79,5 +80,6 @@ export {
79
80
  OverdueInvoicePayment,
80
81
  PaymentBeneficiaries,
81
82
  LoadingButton,
82
- DonateDetails
83
+ DonateDetails,
84
+ ResumeSubscription
83
85
  };
package/es/libs/util.js CHANGED
@@ -943,7 +943,8 @@ export function getInvoiceDescriptionAndReason(invoice, locale = "en") {
943
943
  "Stake for subscription overdraft protection": t(
944
944
  "payment.invoice.reason.stakeForSubscriptionOverdraftProtection",
945
945
  locale
946
- )
946
+ ),
947
+ "Re-stake to resume subscription": t("payment.invoice.reason.reStakeToResumeSubscription", locale)
947
948
  };
948
949
  return {
949
950
  description: descMap[description] || description,
package/es/locales/en.js CHANGED
@@ -248,8 +248,8 @@ export default flat({
248
248
  button: "Unsubscribe",
249
249
  title: "Cancel your subscription",
250
250
  comment: "Any additional feedback?",
251
- description: "Your subscription will be canceled, but it is still available until the end of your current billing period on {date}",
252
- trialDescription: "Free trial subscriptions will be canceled immediately and no longer available, confirm to continue",
251
+ description: "Your subscription will be canceled, but it is still available until the end of your current billing period on {date}.",
252
+ trialDescription: "Free trial subscriptions will be canceled immediately and no longer available, confirm to continue.",
253
253
  feedback: {
254
254
  tip: "We would love your feedback, it will help us improve our service",
255
255
  too_expensive: "The service is too expensive",
@@ -276,7 +276,11 @@ export default flat({
276
276
  recover: {
277
277
  button: "Resume Subscription",
278
278
  title: "Resume Your Subscription",
279
- description: "Your subscription will not be canceled and will be automatically renewed on {date}, please confirm to continue"
279
+ description: "Your subscription will not be canceled and will be automatically renewed on {date}, please confirm to continue.",
280
+ success: "Subscription resumed successfully",
281
+ reStakeTitle: "Re-stake to resume subscription",
282
+ stakeRequiredDescription: "Your previous stake has been revoked. You need to re-stake to resume your subscription.",
283
+ stakeWarning: "Please ensure you have sufficient balance for staking before proceeding."
280
284
  },
281
285
  changePlan: {
282
286
  button: "Change Plan",
@@ -373,6 +377,7 @@ export default flat({
373
377
  rechargeForSubscription: "Add funds for subscription",
374
378
  overdraftProtection: "SubGuard\u2122 Fee",
375
379
  stakeForSubscriptionOverdraftProtection: "SubGuard\u2122",
380
+ reStakeToResumeSubscription: "Subscription resume",
376
381
  gas: "Gas",
377
382
  fee: "Fee"
378
383
  }
package/es/locales/zh.js CHANGED
@@ -248,8 +248,8 @@ export default flat({
248
248
  button: "\u53D6\u6D88\u8BA2\u9605",
249
249
  title: "\u53D6\u6D88\u60A8\u7684\u8BA2\u9605",
250
250
  comment: "\u4F60\u8FD8\u6709\u5176\u4ED6\u53CD\u9988\u4E48\uFF1F",
251
- description: "\u60A8\u7684\u8BA2\u9605\u5C06\u88AB\u53D6\u6D88\uFF0C\u4F46\u4ECD\u7136\u53EF\u7528\u76F4\u5230\u60A8\u5F53\u524D\u8BA1\u8D39\u5468\u671F\u7ED3\u675F\u4E8E{date}",
252
- trialDescription: "\u514D\u8D39\u8BD5\u7528\u7684\u8BA2\u9605\u5C06\u88AB\u7ACB\u5373\u53D6\u6D88\uFF0C\u4E0D\u518D\u53EF\u7528\uFF0C\u786E\u8BA4\u662F\u5426\u7EE7\u7EED",
251
+ description: "\u60A8\u7684\u8BA2\u9605\u5C06\u88AB\u53D6\u6D88\uFF0C\u4F46\u4ECD\u7136\u53EF\u7528\u76F4\u5230\u60A8\u5F53\u524D\u8BA1\u8D39\u5468\u671F\u7ED3\u675F\u4E8E{date}\u3002",
252
+ trialDescription: "\u514D\u8D39\u8BD5\u7528\u7684\u8BA2\u9605\u5C06\u88AB\u7ACB\u5373\u53D6\u6D88\uFF0C\u4E0D\u518D\u53EF\u7528\uFF0C\u786E\u8BA4\u662F\u5426\u7EE7\u7EED\u3002",
253
253
  feedback: {
254
254
  tip: "\u6211\u4EEC\u5E0C\u671B\u542C\u5230\u60A8\u7684\u53CD\u9988\uFF0C\u8FD9\u5C06\u5E2E\u52A9\u6211\u4EEC\u6539\u8FDB\u6211\u4EEC\u7684\u670D\u52A1",
255
255
  too_expensive: "\u670D\u52A1\u8D39\u7528\u592A\u9AD8",
@@ -277,7 +277,10 @@ export default flat({
277
277
  recover: {
278
278
  button: "\u6062\u590D\u8BA2\u9605",
279
279
  title: "\u6062\u590D\u60A8\u7684\u8BA2\u9605",
280
- description: "\u60A8\u7684\u8BA2\u9605\u5C06\u4E0D\u4F1A\u88AB\u53D6\u6D88\uFF0C\u5E76\u5C06\u5728{date}\u81EA\u52A8\u7EED\u8BA2\uFF0C\u8BF7\u786E\u8BA4\u662F\u5426\u7EE7\u7EED"
280
+ description: "\u60A8\u7684\u8BA2\u9605\u5C06\u4E0D\u4F1A\u88AB\u53D6\u6D88\uFF0C\u5E76\u5C06\u5728{date}\u81EA\u52A8\u7EED\u8BA2\uFF0C\u8BF7\u786E\u8BA4\u662F\u5426\u7EE7\u7EED\u3002",
281
+ success: "\u8BA2\u9605\u6062\u590D\u6210\u529F",
282
+ reStakeTitle: "\u91CD\u65B0\u8D28\u62BC\u4EE5\u6062\u590D\u8BA2\u9605",
283
+ stakeRequiredDescription: "\u60A8\u4E4B\u524D\u7684\u8D28\u62BC\u5DF2\u88AB\u7533\u8BF7\u53D6\u56DE\uFF0C\u9700\u8981\u91CD\u65B0\u8D28\u62BC\u624D\u80FD\u6062\u590D\u8BA2\u9605\u3002"
281
284
  },
282
285
  changePlan: {
283
286
  button: "\u5207\u6362\u5957\u9910",
@@ -374,6 +377,7 @@ export default flat({
374
377
  rechargeForSubscription: "\u8BA2\u9605\u5145\u503C",
375
378
  overdraftProtection: "\u8BA2\u9605\u5B88\u62A4\u670D\u52A1\u8D39",
376
379
  stakeForSubscriptionOverdraftProtection: "\u8BA2\u9605\u5B88\u62A4\u670D\u52A1",
380
+ reStakeToResumeSubscription: "\u8BA2\u9605\u6062\u590D",
377
381
  gas: "\u624B\u7EED\u8D39",
378
382
  fee: "\u670D\u52A1\u8D39"
379
383
  }
@@ -2,7 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import Center from "@arcblock/ux/lib/Center";
3
3
  import Dialog from "@arcblock/ux/lib/Dialog";
4
4
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
5
- import { CircularProgress, Typography } from "@mui/material";
5
+ import { CircularProgress, Typography, useTheme } from "@mui/material";
6
6
  import { styled } from "@mui/system";
7
7
  import { useSetState } from "ahooks";
8
8
  import { useEffect, useCallback } from "react";
@@ -28,6 +28,7 @@ function StripeCheckoutForm({
28
28
  const stripe = useStripe();
29
29
  const elements = useElements();
30
30
  const { t } = useLocaleContext();
31
+ const theme = useTheme();
31
32
  const [state, setState] = useSetState({
32
33
  message: "",
33
34
  confirming: false,
@@ -171,6 +172,16 @@ function StripeCheckoutForm({
171
172
  email: customer.email,
172
173
  address: customer.address
173
174
  }
175
+ },
176
+ appearance: {
177
+ theme: theme.palette.mode,
178
+ variables: {
179
+ colorPrimary: theme.palette.primary.main,
180
+ colorBackground: theme.palette.background.paper,
181
+ colorText: theme.palette.text.primary,
182
+ colorDanger: theme.palette.error.main,
183
+ borderRadius: "4px"
184
+ }
174
185
  }
175
186
  },
176
187
  onChange: handlePaymentMethodChange,
@@ -217,6 +228,7 @@ export default function StripeCheckout({
217
228
  const stripePromise = loadStripe(publicKey);
218
229
  const { isMobile } = useMobile();
219
230
  const { t, locale } = useLocaleContext();
231
+ const theme = useTheme();
220
232
  const [state, setState] = useSetState({
221
233
  open: true,
222
234
  closable: true
@@ -243,6 +255,15 @@ export default function StripeCheckout({
243
255
  },
244
256
  form: {
245
257
  justifyContent: "flex-start"
258
+ },
259
+ ".StripeElement--focus": {
260
+ borderColor: theme.palette.primary.main
261
+ },
262
+ ".StripeElement--invalid": {
263
+ borderColor: theme.palette.error.main
264
+ },
265
+ ".StripeElement--complete": {
266
+ borderColor: theme.palette.success.main
246
267
  }
247
268
  },
248
269
  PaperProps: {
@@ -255,7 +276,16 @@ export default function StripeCheckout({
255
276
  {
256
277
  options: {
257
278
  clientSecret,
258
- locale: locale === "zh" ? "zh-CN" : "en"
279
+ locale: locale === "zh" ? "zh-CN" : "en",
280
+ appearance: {
281
+ theme: theme.palette.mode,
282
+ variables: {
283
+ colorPrimary: theme.palette.primary.main,
284
+ colorBackground: theme.palette.background.paper,
285
+ colorText: theme.palette.text.primary,
286
+ colorDanger: theme.palette.error.main
287
+ }
288
+ }
259
289
  },
260
290
  stripe: stripePromise,
261
291
  children: /* @__PURE__ */ jsx(
@@ -57,7 +57,7 @@ const LoadingButton = (0, _react.forwardRef)(({
57
57
  });
58
58
  return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Button, {
59
59
  ref,
60
- disabled: props.disabled,
60
+ disabled: props.disabled || loading,
61
61
  onClick: handleClick,
62
62
  sx: {
63
63
  position: "relative",
@@ -0,0 +1,25 @@
1
+ import type { Subscription, TSubscriptionExpanded } from '@blocklet/payment-types';
2
+ type DialogProps = {
3
+ open?: boolean;
4
+ onClose?: () => void;
5
+ title?: string;
6
+ };
7
+ type Props = {
8
+ subscriptionId: string;
9
+ onResumed?: (subscription: Subscription | TSubscriptionExpanded) => void;
10
+ dialogProps?: DialogProps;
11
+ successToast?: boolean;
12
+ authToken?: string;
13
+ };
14
+ declare function RecoverSubscription({ subscriptionId, dialogProps, onResumed, successToast, authToken, }: Props): import("react").JSX.Element | null;
15
+ declare namespace RecoverSubscription {
16
+ var defaultProps: {
17
+ onResumed: () => void;
18
+ dialogProps: {
19
+ open: boolean;
20
+ };
21
+ successToast: boolean;
22
+ authToken: undefined;
23
+ };
24
+ }
25
+ export default RecoverSubscription;
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+
7
+ var _jsxRuntime = require("react/jsx-runtime");
8
+ var _react = require("react");
9
+ var _material = require("@mui/material");
10
+ var _context = require("@arcblock/ux/lib/Locale/context");
11
+ var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast"));
12
+ var _ufo = require("ufo");
13
+ var _ahooks = require("ahooks");
14
+ var _ux = require("@arcblock/ux");
15
+ var _payment = require("../contexts/payment");
16
+ var _util = require("../libs/util");
17
+ var _api = _interopRequireDefault(require("../libs/api"));
18
+ var _loadingButton = _interopRequireDefault(require("./loading-button"));
19
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20
+ const fetchSubscriptionDetail = async params => {
21
+ const url = `/api/subscriptions/${params.subscriptionId}`;
22
+ const res = await _api.default.get(params.authToken ? (0, _ufo.joinURL)(url, `?authToken=${params.authToken}`) : url);
23
+ return res.data;
24
+ };
25
+ const fetchRecoverInfo = async params => {
26
+ const url = `/api/subscriptions/${params.subscriptionId}/recover-info`;
27
+ const res = await _api.default.get(params.authToken ? (0, _ufo.joinURL)(url, `?authToken=${params.authToken}`) : url);
28
+ return res.data;
29
+ };
30
+ const recoverSubscription = async params => {
31
+ const url = `/api/subscriptions/${params.subscriptionId}/recover`;
32
+ const res = await _api.default.put(params.authToken ? (0, _ufo.joinURL)(url, `?authToken=${params.authToken}`) : url);
33
+ return res.data;
34
+ };
35
+ function RecoverSubscription({
36
+ subscriptionId,
37
+ dialogProps = {},
38
+ onResumed = () => {},
39
+ successToast = true,
40
+ authToken
41
+ }) {
42
+ const {
43
+ t,
44
+ locale
45
+ } = (0, _context.useLocaleContext)();
46
+ const {
47
+ connect
48
+ } = (0, _payment.usePaymentContext)();
49
+ const [recoverLoading, setRecoverLoading] = (0, _react.useState)(false);
50
+ const [dialogOpen, setDialogOpen] = (0, _react.useState)(dialogProps.open || false);
51
+ const {
52
+ data,
53
+ error,
54
+ loading
55
+ } = (0, _ahooks.useRequest)(() => fetchRecoverInfo({
56
+ subscriptionId,
57
+ authToken
58
+ }), {
59
+ ready: !!subscriptionId
60
+ });
61
+ const isCrossOriginRequest = (0, _util.isCrossOrigin)();
62
+ const needStake = data?.needStake ?? false;
63
+ const handleSuccess = async () => {
64
+ try {
65
+ const subscription = await fetchSubscriptionDetail({
66
+ subscriptionId,
67
+ authToken
68
+ });
69
+ if (successToast) {
70
+ _Toast.default.success(t("payment.customer.recover.success"));
71
+ }
72
+ setDialogOpen(false);
73
+ onResumed(subscription);
74
+ setRecoverLoading(false);
75
+ } catch (err) {
76
+ console.error("Failed to fetch updated subscription:", err);
77
+ _Toast.default.error((0, _util.formatError)(err));
78
+ setRecoverLoading(false);
79
+ }
80
+ };
81
+ const handleRecoverWithStake = () => {
82
+ setRecoverLoading(true);
83
+ try {
84
+ connect.open({
85
+ locale,
86
+ containerEl: void 0,
87
+ saveConnect: false,
88
+ action: "re-stake",
89
+ prefix: (0, _ufo.joinURL)((0, _util.getPrefix)(), "/api/did"),
90
+ useSocket: !isCrossOriginRequest,
91
+ extraParams: {
92
+ subscriptionId
93
+ },
94
+ messages: {
95
+ scan: t("common.connect.defaultScan"),
96
+ title: t("payment.customer.recover.reStakeTitle"),
97
+ confirm: t("common.connect.confirm")
98
+ },
99
+ onSuccess: handleSuccess,
100
+ onClose: () => {
101
+ connect.close();
102
+ setRecoverLoading(false);
103
+ },
104
+ onError: err => {
105
+ _Toast.default.error((0, _util.formatError)(err));
106
+ setRecoverLoading(false);
107
+ }
108
+ });
109
+ } catch (err) {
110
+ _Toast.default.error((0, _util.formatError)(err));
111
+ setRecoverLoading(false);
112
+ }
113
+ };
114
+ const handleDirectRecover = async () => {
115
+ setRecoverLoading(true);
116
+ try {
117
+ const result = await recoverSubscription({
118
+ subscriptionId,
119
+ authToken
120
+ });
121
+ if (result.needStake) {
122
+ handleRecoverWithStake();
123
+ return;
124
+ }
125
+ const {
126
+ subscription
127
+ } = result;
128
+ if (successToast) {
129
+ _Toast.default.success(t("payment.customer.recover.success"));
130
+ }
131
+ setDialogOpen(false);
132
+ onResumed(subscription);
133
+ setRecoverLoading(false);
134
+ } catch (err) {
135
+ _Toast.default.error((0, _util.formatError)(err));
136
+ setRecoverLoading(false);
137
+ }
138
+ };
139
+ const handleRecover = () => {
140
+ if (needStake) {
141
+ handleRecoverWithStake();
142
+ } else {
143
+ handleDirectRecover();
144
+ }
145
+ };
146
+ const handleClose = () => {
147
+ setDialogOpen(false);
148
+ dialogProps.onClose?.();
149
+ };
150
+ if (loading) {
151
+ return null;
152
+ }
153
+ return /* @__PURE__ */(0, _jsxRuntime.jsx)(_ux.Dialog, {
154
+ PaperProps: {
155
+ style: {
156
+ minHeight: "auto"
157
+ }
158
+ },
159
+ ...(dialogProps || {}),
160
+ open: dialogOpen,
161
+ title: dialogProps?.title || t("payment.customer.recover.title"),
162
+ sx: {
163
+ "& .MuiDialogContent-root": {
164
+ pt: 0
165
+ }
166
+ },
167
+ onClose: handleClose,
168
+ children: error ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Alert, {
169
+ severity: "error",
170
+ children: error.message
171
+ }) : /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
172
+ gap: 2,
173
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
174
+ color: "text.secondary",
175
+ variant: "body2",
176
+ children: t("payment.customer.recover.description", {
177
+ date: (0, _util.formatToDate)(Number(data?.subscription?.current_period_end || "0") * 1e3)
178
+ })
179
+ }), needStake && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Alert, {
180
+ severity: "warning",
181
+ children: t("payment.customer.recover.stakeRequiredDescription")
182
+ }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
183
+ direction: "row",
184
+ justifyContent: "flex-end",
185
+ gap: 2,
186
+ mt: 2,
187
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, {
188
+ variant: "outlined",
189
+ color: "primary",
190
+ onClick: handleClose,
191
+ children: t("common.cancel")
192
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_loadingButton.default, {
193
+ variant: "contained",
194
+ size: "small",
195
+ loading: recoverLoading,
196
+ onClick: handleRecover,
197
+ children: t("common.confirm")
198
+ })]
199
+ })]
200
+ })
201
+ });
202
+ }
203
+ RecoverSubscription.defaultProps = {
204
+ onResumed: () => {},
205
+ dialogProps: {
206
+ open: true
207
+ },
208
+ successToast: true,
209
+ authToken: void 0
210
+ };
211
+ module.exports = RecoverSubscription;
package/lib/index.d.ts CHANGED
@@ -31,6 +31,7 @@ import { createLazyComponent } from './components/lazy-loader';
31
31
  import OverdueInvoicePayment from './components/over-due-invoice-payment';
32
32
  import PaymentBeneficiaries from './components/payment-beneficiaries';
33
33
  import LoadingButton from './components/loading-button';
34
+ import ResumeSubscription from './components/resume-subscription';
34
35
  export { PaymentThemeProvider } from './theme';
35
36
  export * from './libs/util';
36
37
  export * from './libs/connect';
@@ -45,4 +46,4 @@ export * from './hooks/scroll';
45
46
  export * from './hooks/keyboard';
46
47
  export * from './libs/validator';
47
48
  export { translations, createTranslator } from './locales';
48
- export { createLazyComponent, api, dayjs, FormInput, PhoneInput, AddressForm, StripeForm, Status, Livemode, Switch, ConfirmDialog, CheckoutForm, CheckoutTable, CheckoutDonate, CurrencySelector, Payment, PaymentSummary, PricingTable, ProductSkeleton, Amount, CustomerInvoiceList, CustomerPaymentList, TxLink, TxGas, SafeGuard, PricingItem, CountrySelect, Table, TruncatedText, Link, OverdueInvoicePayment, PaymentBeneficiaries, LoadingButton, DonateDetails, };
49
+ export { createLazyComponent, api, dayjs, FormInput, PhoneInput, AddressForm, StripeForm, Status, Livemode, Switch, ConfirmDialog, CheckoutForm, CheckoutTable, CheckoutDonate, CurrencySelector, Payment, PaymentSummary, PricingTable, ProductSkeleton, Amount, CustomerInvoiceList, CustomerPaymentList, TxLink, TxGas, SafeGuard, PricingItem, CountrySelect, Table, TruncatedText, Link, OverdueInvoicePayment, PaymentBeneficiaries, LoadingButton, DonateDetails, ResumeSubscription, };
package/lib/index.js CHANGED
@@ -38,6 +38,7 @@ var _exportNames = {
38
38
  OverdueInvoicePayment: true,
39
39
  PaymentBeneficiaries: true,
40
40
  LoadingButton: true,
41
+ ResumeSubscription: true,
41
42
  PaymentThemeProvider: true,
42
43
  translations: true,
43
44
  createTranslator: true
@@ -186,6 +187,12 @@ Object.defineProperty(exports, "ProductSkeleton", {
186
187
  return _productSkeleton.default;
187
188
  }
188
189
  });
190
+ Object.defineProperty(exports, "ResumeSubscription", {
191
+ enumerable: true,
192
+ get: function () {
193
+ return _resumeSubscription.default;
194
+ }
195
+ });
189
196
  Object.defineProperty(exports, "SafeGuard", {
190
197
  enumerable: true,
191
198
  get: function () {
@@ -297,6 +304,7 @@ var _lazyLoader = require("./components/lazy-loader");
297
304
  var _overDueInvoicePayment = _interopRequireDefault(require("./components/over-due-invoice-payment"));
298
305
  var _paymentBeneficiaries = _interopRequireDefault(require("./components/payment-beneficiaries"));
299
306
  var _loadingButton = _interopRequireDefault(require("./components/loading-button"));
307
+ var _resumeSubscription = _interopRequireDefault(require("./components/resume-subscription"));
300
308
  var _theme = require("./theme");
301
309
  var _util = require("./libs/util");
302
310
  Object.keys(_util).forEach(function (key) {
package/lib/libs/util.js CHANGED
@@ -1172,7 +1172,8 @@ function getInvoiceDescriptionAndReason(invoice, locale = "en") {
1172
1172
  "Recharge for subscription": (0, _locales.t)("payment.invoice.reason.rechargeForSubscription", locale),
1173
1173
  "Add funds for subscription": (0, _locales.t)("payment.invoice.reason.rechargeForSubscription", locale),
1174
1174
  "Overdraft protection": (0, _locales.t)("payment.invoice.reason.overdraftProtection", locale),
1175
- "Stake for subscription overdraft protection": (0, _locales.t)("payment.invoice.reason.stakeForSubscriptionOverdraftProtection", locale)
1175
+ "Stake for subscription overdraft protection": (0, _locales.t)("payment.invoice.reason.stakeForSubscriptionOverdraftProtection", locale),
1176
+ "Re-stake to resume subscription": (0, _locales.t)("payment.invoice.reason.reStakeToResumeSubscription", locale)
1176
1177
  };
1177
1178
  return {
1178
1179
  description: descMap[description] || description,
package/lib/locales/en.js CHANGED
@@ -255,8 +255,8 @@ module.exports = (0, _flat.default)({
255
255
  button: "Unsubscribe",
256
256
  title: "Cancel your subscription",
257
257
  comment: "Any additional feedback?",
258
- description: "Your subscription will be canceled, but it is still available until the end of your current billing period on {date}",
259
- trialDescription: "Free trial subscriptions will be canceled immediately and no longer available, confirm to continue",
258
+ description: "Your subscription will be canceled, but it is still available until the end of your current billing period on {date}.",
259
+ trialDescription: "Free trial subscriptions will be canceled immediately and no longer available, confirm to continue.",
260
260
  feedback: {
261
261
  tip: "We would love your feedback, it will help us improve our service",
262
262
  too_expensive: "The service is too expensive",
@@ -283,7 +283,11 @@ module.exports = (0, _flat.default)({
283
283
  recover: {
284
284
  button: "Resume Subscription",
285
285
  title: "Resume Your Subscription",
286
- description: "Your subscription will not be canceled and will be automatically renewed on {date}, please confirm to continue"
286
+ description: "Your subscription will not be canceled and will be automatically renewed on {date}, please confirm to continue.",
287
+ success: "Subscription resumed successfully",
288
+ reStakeTitle: "Re-stake to resume subscription",
289
+ stakeRequiredDescription: "Your previous stake has been revoked. You need to re-stake to resume your subscription.",
290
+ stakeWarning: "Please ensure you have sufficient balance for staking before proceeding."
287
291
  },
288
292
  changePlan: {
289
293
  button: "Change Plan",
@@ -380,6 +384,7 @@ module.exports = (0, _flat.default)({
380
384
  rechargeForSubscription: "Add funds for subscription",
381
385
  overdraftProtection: "SubGuard\u2122 Fee",
382
386
  stakeForSubscriptionOverdraftProtection: "SubGuard\u2122",
387
+ reStakeToResumeSubscription: "Subscription resume",
383
388
  gas: "Gas",
384
389
  fee: "Fee"
385
390
  }
package/lib/locales/zh.js CHANGED
@@ -255,8 +255,8 @@ module.exports = (0, _flat.default)({
255
255
  button: "\u53D6\u6D88\u8BA2\u9605",
256
256
  title: "\u53D6\u6D88\u60A8\u7684\u8BA2\u9605",
257
257
  comment: "\u4F60\u8FD8\u6709\u5176\u4ED6\u53CD\u9988\u4E48\uFF1F",
258
- description: "\u60A8\u7684\u8BA2\u9605\u5C06\u88AB\u53D6\u6D88\uFF0C\u4F46\u4ECD\u7136\u53EF\u7528\u76F4\u5230\u60A8\u5F53\u524D\u8BA1\u8D39\u5468\u671F\u7ED3\u675F\u4E8E{date}",
259
- trialDescription: "\u514D\u8D39\u8BD5\u7528\u7684\u8BA2\u9605\u5C06\u88AB\u7ACB\u5373\u53D6\u6D88\uFF0C\u4E0D\u518D\u53EF\u7528\uFF0C\u786E\u8BA4\u662F\u5426\u7EE7\u7EED",
258
+ description: "\u60A8\u7684\u8BA2\u9605\u5C06\u88AB\u53D6\u6D88\uFF0C\u4F46\u4ECD\u7136\u53EF\u7528\u76F4\u5230\u60A8\u5F53\u524D\u8BA1\u8D39\u5468\u671F\u7ED3\u675F\u4E8E{date}\u3002",
259
+ trialDescription: "\u514D\u8D39\u8BD5\u7528\u7684\u8BA2\u9605\u5C06\u88AB\u7ACB\u5373\u53D6\u6D88\uFF0C\u4E0D\u518D\u53EF\u7528\uFF0C\u786E\u8BA4\u662F\u5426\u7EE7\u7EED\u3002",
260
260
  feedback: {
261
261
  tip: "\u6211\u4EEC\u5E0C\u671B\u542C\u5230\u60A8\u7684\u53CD\u9988\uFF0C\u8FD9\u5C06\u5E2E\u52A9\u6211\u4EEC\u6539\u8FDB\u6211\u4EEC\u7684\u670D\u52A1",
262
262
  too_expensive: "\u670D\u52A1\u8D39\u7528\u592A\u9AD8",
@@ -284,7 +284,10 @@ module.exports = (0, _flat.default)({
284
284
  recover: {
285
285
  button: "\u6062\u590D\u8BA2\u9605",
286
286
  title: "\u6062\u590D\u60A8\u7684\u8BA2\u9605",
287
- description: "\u60A8\u7684\u8BA2\u9605\u5C06\u4E0D\u4F1A\u88AB\u53D6\u6D88\uFF0C\u5E76\u5C06\u5728{date}\u81EA\u52A8\u7EED\u8BA2\uFF0C\u8BF7\u786E\u8BA4\u662F\u5426\u7EE7\u7EED"
287
+ description: "\u60A8\u7684\u8BA2\u9605\u5C06\u4E0D\u4F1A\u88AB\u53D6\u6D88\uFF0C\u5E76\u5C06\u5728{date}\u81EA\u52A8\u7EED\u8BA2\uFF0C\u8BF7\u786E\u8BA4\u662F\u5426\u7EE7\u7EED\u3002",
288
+ success: "\u8BA2\u9605\u6062\u590D\u6210\u529F",
289
+ reStakeTitle: "\u91CD\u65B0\u8D28\u62BC\u4EE5\u6062\u590D\u8BA2\u9605",
290
+ stakeRequiredDescription: "\u60A8\u4E4B\u524D\u7684\u8D28\u62BC\u5DF2\u88AB\u7533\u8BF7\u53D6\u56DE\uFF0C\u9700\u8981\u91CD\u65B0\u8D28\u62BC\u624D\u80FD\u6062\u590D\u8BA2\u9605\u3002"
288
291
  },
289
292
  changePlan: {
290
293
  button: "\u5207\u6362\u5957\u9910",
@@ -381,6 +384,7 @@ module.exports = (0, _flat.default)({
381
384
  rechargeForSubscription: "\u8BA2\u9605\u5145\u503C",
382
385
  overdraftProtection: "\u8BA2\u9605\u5B88\u62A4\u670D\u52A1\u8D39",
383
386
  stakeForSubscriptionOverdraftProtection: "\u8BA2\u9605\u5B88\u62A4\u670D\u52A1",
387
+ reStakeToResumeSubscription: "\u8BA2\u9605\u6062\u590D",
384
388
  gas: "\u624B\u7EED\u8D39",
385
389
  fee: "\u670D\u52A1\u8D39"
386
390
  }
@@ -44,6 +44,7 @@ function StripeCheckoutForm({
44
44
  const {
45
45
  t
46
46
  } = (0, _context.useLocaleContext)();
47
+ const theme = (0, _material.useTheme)();
47
48
  const [state, setState] = (0, _ahooks.useSetState)({
48
49
  message: "",
49
50
  confirming: false,
@@ -212,6 +213,16 @@ function StripeCheckoutForm({
212
213
  email: customer.email,
213
214
  address: customer.address
214
215
  }
216
+ },
217
+ appearance: {
218
+ theme: theme.palette.mode,
219
+ variables: {
220
+ colorPrimary: theme.palette.primary.main,
221
+ colorBackground: theme.palette.background.paper,
222
+ colorText: theme.palette.text.primary,
223
+ colorDanger: theme.palette.error.main,
224
+ borderRadius: "4px"
225
+ }
215
226
  }
216
227
  },
217
228
  onChange: handlePaymentMethodChange,
@@ -275,6 +286,7 @@ function StripeCheckout({
275
286
  t,
276
287
  locale
277
288
  } = (0, _context.useLocaleContext)();
289
+ const theme = (0, _material.useTheme)();
278
290
  const [state, setState] = (0, _ahooks.useSetState)({
279
291
  open: true,
280
292
  closable: true
@@ -303,6 +315,15 @@ function StripeCheckout({
303
315
  },
304
316
  form: {
305
317
  justifyContent: "flex-start"
318
+ },
319
+ ".StripeElement--focus": {
320
+ borderColor: theme.palette.primary.main
321
+ },
322
+ ".StripeElement--invalid": {
323
+ borderColor: theme.palette.error.main
324
+ },
325
+ ".StripeElement--complete": {
326
+ borderColor: theme.palette.success.main
306
327
  }
307
328
  },
308
329
  PaperProps: {
@@ -313,7 +334,16 @@ function StripeCheckout({
313
334
  children: /* @__PURE__ */(0, _jsxRuntime.jsx)(Elements, {
314
335
  options: {
315
336
  clientSecret,
316
- locale: locale === "zh" ? "zh-CN" : "en"
337
+ locale: locale === "zh" ? "zh-CN" : "en",
338
+ appearance: {
339
+ theme: theme.palette.mode,
340
+ variables: {
341
+ colorPrimary: theme.palette.primary.main,
342
+ colorBackground: theme.palette.background.paper,
343
+ colorText: theme.palette.text.primary,
344
+ colorDanger: theme.palette.error.main
345
+ }
346
+ }
317
347
  },
318
348
  stripe: stripePromise,
319
349
  children: /* @__PURE__ */(0, _jsxRuntime.jsx)(StripeCheckoutForm, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.18.47",
3
+ "version": "1.18.49",
4
4
  "description": "Reusable react components for payment kit v2",
5
5
  "keywords": [
6
6
  "react",
@@ -54,11 +54,11 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@arcblock/did-connect": "^2.13.47",
58
- "@arcblock/ux": "^2.13.47",
57
+ "@arcblock/did-connect": "^2.13.54",
58
+ "@arcblock/ux": "^2.13.54",
59
59
  "@arcblock/ws": "^1.20.11",
60
- "@blocklet/theme": "^2.13.47",
61
- "@blocklet/ui-react": "^2.13.47",
60
+ "@blocklet/theme": "^2.13.54",
61
+ "@blocklet/ui-react": "^2.13.54",
62
62
  "@mui/icons-material": "^5.16.6",
63
63
  "@mui/lab": "^5.0.0-alpha.173",
64
64
  "@mui/material": "^5.16.6",
@@ -94,7 +94,7 @@
94
94
  "@babel/core": "^7.25.2",
95
95
  "@babel/preset-env": "^7.25.2",
96
96
  "@babel/preset-react": "^7.24.7",
97
- "@blocklet/payment-types": "1.18.47",
97
+ "@blocklet/payment-types": "1.18.49",
98
98
  "@storybook/addon-essentials": "^7.6.20",
99
99
  "@storybook/addon-interactions": "^7.6.20",
100
100
  "@storybook/addon-links": "^7.6.20",
@@ -125,5 +125,5 @@
125
125
  "vite-plugin-babel": "^1.2.0",
126
126
  "vite-plugin-node-polyfills": "^0.21.0"
127
127
  },
128
- "gitHead": "e781286c5a20e54ef0eb7e029505f32c00cb33e2"
128
+ "gitHead": "05028e0b605756db083baa56b1faa36ef9431042"
129
129
  }
@@ -68,7 +68,7 @@ const LoadingButton = forwardRef<HTMLButtonElement, LoadingButtonProps>(
68
68
  return (
69
69
  <Button
70
70
  ref={ref}
71
- disabled={props.disabled}
71
+ disabled={props.disabled || loading}
72
72
  onClick={handleClick}
73
73
  sx={{
74
74
  position: 'relative',
@@ -0,0 +1,217 @@
1
+ import { useState } from 'react';
2
+ import { Button, Typography, Stack, Alert } from '@mui/material';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import Toast from '@arcblock/ux/lib/Toast';
5
+ import { joinURL } from 'ufo';
6
+ import type { Subscription, TSubscriptionExpanded } from '@blocklet/payment-types';
7
+ import { useRequest } from 'ahooks';
8
+ import { Dialog } from '@arcblock/ux';
9
+ import { usePaymentContext } from '../contexts/payment';
10
+ import { formatError, formatToDate, getPrefix, isCrossOrigin } from '../libs/util';
11
+ import api from '../libs/api';
12
+ import LoadingButton from './loading-button';
13
+
14
+ type DialogProps = {
15
+ open?: boolean;
16
+ onClose?: () => void;
17
+ title?: string;
18
+ };
19
+
20
+ type Props = {
21
+ subscriptionId: string;
22
+ onResumed?: (subscription: Subscription | TSubscriptionExpanded) => void;
23
+ dialogProps?: DialogProps;
24
+ successToast?: boolean;
25
+ authToken?: string;
26
+ };
27
+
28
+ type RecoverInfo = {
29
+ subscription: Subscription;
30
+ needStake?: boolean;
31
+ };
32
+
33
+ const fetchSubscriptionDetail = async (params: {
34
+ subscriptionId: string;
35
+ authToken?: string;
36
+ }): Promise<TSubscriptionExpanded> => {
37
+ const url = `/api/subscriptions/${params.subscriptionId}`;
38
+ const res = await api.get(params.authToken ? joinURL(url, `?authToken=${params.authToken}`) : url);
39
+ return res.data;
40
+ };
41
+
42
+ const fetchRecoverInfo = async (params: { subscriptionId: string; authToken?: string }): Promise<RecoverInfo> => {
43
+ const url = `/api/subscriptions/${params.subscriptionId}/recover-info`;
44
+ const res = await api.get(params.authToken ? joinURL(url, `?authToken=${params.authToken}`) : url);
45
+ return res.data;
46
+ };
47
+
48
+ const recoverSubscription = async (params: {
49
+ subscriptionId: string;
50
+ authToken?: string;
51
+ }): Promise<{
52
+ subscription: Subscription;
53
+ needStake?: boolean;
54
+ }> => {
55
+ const url = `/api/subscriptions/${params.subscriptionId}/recover`;
56
+ const res = await api.put(params.authToken ? joinURL(url, `?authToken=${params.authToken}`) : url);
57
+ return res.data;
58
+ };
59
+
60
+ function RecoverSubscription({
61
+ subscriptionId,
62
+ dialogProps = {},
63
+ onResumed = () => {},
64
+ successToast = true,
65
+ authToken,
66
+ }: Props) {
67
+ const { t, locale } = useLocaleContext();
68
+ const { connect } = usePaymentContext();
69
+ const [recoverLoading, setRecoverLoading] = useState(false);
70
+ const [dialogOpen, setDialogOpen] = useState(dialogProps.open || false);
71
+
72
+ const { data, error, loading } = useRequest(() => fetchRecoverInfo({ subscriptionId, authToken }), {
73
+ ready: !!subscriptionId,
74
+ });
75
+
76
+ const isCrossOriginRequest = isCrossOrigin();
77
+ const needStake = data?.needStake ?? false;
78
+
79
+ const handleSuccess = async () => {
80
+ try {
81
+ // 获取最新的订阅数据
82
+ const subscription = await fetchSubscriptionDetail({ subscriptionId, authToken });
83
+
84
+ if (successToast) {
85
+ Toast.success(t('payment.customer.recover.success'));
86
+ }
87
+
88
+ setDialogOpen(false);
89
+ onResumed(subscription);
90
+ setRecoverLoading(false);
91
+ } catch (err) {
92
+ console.error('Failed to fetch updated subscription:', err);
93
+ Toast.error(formatError(err));
94
+ setRecoverLoading(false);
95
+ }
96
+ };
97
+
98
+ const handleRecoverWithStake = () => {
99
+ setRecoverLoading(true);
100
+
101
+ try {
102
+ connect.open({
103
+ locale: locale as 'en' | 'zh',
104
+ containerEl: undefined as unknown as Element,
105
+ saveConnect: false,
106
+ action: 're-stake',
107
+ prefix: joinURL(getPrefix(), '/api/did'),
108
+ useSocket: !isCrossOriginRequest,
109
+ extraParams: { subscriptionId },
110
+ messages: {
111
+ scan: t('common.connect.defaultScan'),
112
+ title: t('payment.customer.recover.reStakeTitle'),
113
+ confirm: t('common.connect.confirm'),
114
+ } as any,
115
+ onSuccess: handleSuccess,
116
+ onClose: () => {
117
+ connect.close();
118
+ setRecoverLoading(false);
119
+ },
120
+ onError: (err: any) => {
121
+ Toast.error(formatError(err));
122
+ setRecoverLoading(false);
123
+ },
124
+ });
125
+ } catch (err) {
126
+ Toast.error(formatError(err));
127
+ setRecoverLoading(false);
128
+ }
129
+ };
130
+
131
+ const handleDirectRecover = async () => {
132
+ setRecoverLoading(true);
133
+
134
+ try {
135
+ const result = await recoverSubscription({ subscriptionId, authToken });
136
+
137
+ if (result.needStake) {
138
+ handleRecoverWithStake();
139
+ return;
140
+ }
141
+ const { subscription } = result;
142
+ if (successToast) {
143
+ Toast.success(t('payment.customer.recover.success'));
144
+ }
145
+
146
+ setDialogOpen(false);
147
+ onResumed(subscription);
148
+ setRecoverLoading(false);
149
+ } catch (err: any) {
150
+ Toast.error(formatError(err));
151
+ setRecoverLoading(false);
152
+ }
153
+ };
154
+
155
+ const handleRecover = () => {
156
+ if (needStake) {
157
+ handleRecoverWithStake();
158
+ } else {
159
+ handleDirectRecover();
160
+ }
161
+ };
162
+
163
+ const handleClose = () => {
164
+ setDialogOpen(false);
165
+ dialogProps.onClose?.();
166
+ };
167
+
168
+ if (loading) {
169
+ return null;
170
+ }
171
+
172
+ return (
173
+ <Dialog
174
+ PaperProps={{
175
+ style: { minHeight: 'auto' },
176
+ }}
177
+ {...(dialogProps || {})}
178
+ open={dialogOpen}
179
+ title={dialogProps?.title || t('payment.customer.recover.title')}
180
+ sx={{ '& .MuiDialogContent-root': { pt: 0 } }}
181
+ onClose={handleClose}>
182
+ {error ? (
183
+ <Alert severity="error">{error.message}</Alert>
184
+ ) : (
185
+ <Stack gap={2}>
186
+ <Typography color="text.secondary" variant="body2">
187
+ {t('payment.customer.recover.description', {
188
+ date: formatToDate(Number(data?.subscription?.current_period_end || '0') * 1000),
189
+ })}
190
+ </Typography>
191
+
192
+ {needStake && <Alert severity="warning">{t('payment.customer.recover.stakeRequiredDescription')}</Alert>}
193
+
194
+ <Stack direction="row" justifyContent="flex-end" gap={2} mt={2}>
195
+ <Button variant="outlined" color="primary" onClick={handleClose}>
196
+ {t('common.cancel')}
197
+ </Button>
198
+ <LoadingButton variant="contained" size="small" loading={recoverLoading} onClick={handleRecover}>
199
+ {t('common.confirm')}
200
+ </LoadingButton>
201
+ </Stack>
202
+ </Stack>
203
+ )}
204
+ </Dialog>
205
+ );
206
+ }
207
+
208
+ RecoverSubscription.defaultProps = {
209
+ onResumed: () => {},
210
+ dialogProps: {
211
+ open: true,
212
+ },
213
+ successToast: true,
214
+ authToken: undefined,
215
+ };
216
+
217
+ export default RecoverSubscription;
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ import { createLazyComponent } from './components/lazy-loader';
31
31
  import OverdueInvoicePayment from './components/over-due-invoice-payment';
32
32
  import PaymentBeneficiaries from './components/payment-beneficiaries';
33
33
  import LoadingButton from './components/loading-button';
34
+ import ResumeSubscription from './components/resume-subscription';
34
35
 
35
36
  export { PaymentThemeProvider } from './theme';
36
37
 
@@ -84,4 +85,5 @@ export {
84
85
  PaymentBeneficiaries,
85
86
  LoadingButton,
86
87
  DonateDetails,
88
+ ResumeSubscription,
87
89
  };
package/src/libs/util.ts CHANGED
@@ -1214,6 +1214,7 @@ export function getInvoiceDescriptionAndReason(invoice: TInvoiceExpanded, locale
1214
1214
  'payment.invoice.reason.stakeForSubscriptionOverdraftProtection',
1215
1215
  locale
1216
1216
  ),
1217
+ 'Re-stake to resume subscription': t('payment.invoice.reason.reStakeToResumeSubscription', locale),
1217
1218
  };
1218
1219
 
1219
1220
  return {
@@ -257,9 +257,9 @@ export default flat({
257
257
  title: 'Cancel your subscription',
258
258
  comment: 'Any additional feedback?',
259
259
  description:
260
- 'Your subscription will be canceled, but it is still available until the end of your current billing period on {date}',
260
+ 'Your subscription will be canceled, but it is still available until the end of your current billing period on {date}.',
261
261
  trialDescription:
262
- 'Free trial subscriptions will be canceled immediately and no longer available, confirm to continue',
262
+ 'Free trial subscriptions will be canceled immediately and no longer available, confirm to continue.',
263
263
  feedback: {
264
264
  tip: 'We would love your feedback, it will help us improve our service',
265
265
  too_expensive: 'The service is too expensive',
@@ -287,7 +287,12 @@ export default flat({
287
287
  button: 'Resume Subscription',
288
288
  title: 'Resume Your Subscription',
289
289
  description:
290
- 'Your subscription will not be canceled and will be automatically renewed on {date}, please confirm to continue',
290
+ 'Your subscription will not be canceled and will be automatically renewed on {date}, please confirm to continue.',
291
+ success: 'Subscription resumed successfully',
292
+ reStakeTitle: 'Re-stake to resume subscription',
293
+ stakeRequiredDescription:
294
+ 'Your previous stake has been revoked. You need to re-stake to resume your subscription.',
295
+ stakeWarning: 'Please ensure you have sufficient balance for staking before proceeding.',
291
296
  },
292
297
  changePlan: {
293
298
  button: 'Change Plan',
@@ -388,6 +393,7 @@ export default flat({
388
393
  rechargeForSubscription: 'Add funds for subscription',
389
394
  overdraftProtection: 'SubGuard™ Fee',
390
395
  stakeForSubscriptionOverdraftProtection: 'SubGuard™',
396
+ reStakeToResumeSubscription: 'Subscription resume',
391
397
  gas: 'Gas',
392
398
  fee: 'Fee',
393
399
  },
@@ -251,8 +251,8 @@ export default flat({
251
251
  button: '取消订阅',
252
252
  title: '取消您的订阅',
253
253
  comment: '你还有其他反馈么?',
254
- description: '您的订阅将被取消,但仍然可用直到您当前计费周期结束于{date}',
255
- trialDescription: '免费试用的订阅将被立即取消,不再可用,确认是否继续',
254
+ description: '您的订阅将被取消,但仍然可用直到您当前计费周期结束于{date}',
255
+ trialDescription: '免费试用的订阅将被立即取消,不再可用,确认是否继续。',
256
256
  feedback: {
257
257
  tip: '我们希望听到您的反馈,这将帮助我们改进我们的服务',
258
258
  too_expensive: '服务费用太高',
@@ -280,7 +280,10 @@ export default flat({
280
280
  recover: {
281
281
  button: '恢复订阅',
282
282
  title: '恢复您的订阅',
283
- description: '您的订阅将不会被取消,并将在{date}自动续订,请确认是否继续',
283
+ description: '您的订阅将不会被取消,并将在{date}自动续订,请确认是否继续。',
284
+ success: '订阅恢复成功',
285
+ reStakeTitle: '重新质押以恢复订阅',
286
+ stakeRequiredDescription: '您之前的质押已被申请取回,需要重新质押才能恢复订阅。',
284
287
  },
285
288
  changePlan: {
286
289
  button: '切换套餐',
@@ -378,6 +381,7 @@ export default flat({
378
381
  rechargeForSubscription: '订阅充值',
379
382
  overdraftProtection: '订阅守护服务费',
380
383
  stakeForSubscriptionOverdraftProtection: '订阅守护服务',
384
+ reStakeToResumeSubscription: '订阅恢复',
381
385
  gas: '手续费',
382
386
  fee: '服务费',
383
387
  },
@@ -3,7 +3,7 @@ import Center from '@arcblock/ux/lib/Center';
3
3
  import Dialog from '@arcblock/ux/lib/Dialog';
4
4
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
5
  import type { TCustomer } from '@blocklet/payment-types';
6
- import { CircularProgress, Typography } from '@mui/material';
6
+ import { CircularProgress, Typography, useTheme } from '@mui/material';
7
7
  import { styled } from '@mui/system';
8
8
  import { useSetState } from 'ahooks';
9
9
  import { useEffect, useCallback } from 'react';
@@ -43,6 +43,7 @@ function StripeCheckoutForm({
43
43
  const stripe = useStripe();
44
44
  const elements = useElements();
45
45
  const { t } = useLocaleContext();
46
+ const theme = useTheme();
46
47
 
47
48
  const [state, setState] = useSetState({
48
49
  message: '',
@@ -204,6 +205,16 @@ function StripeCheckoutForm({
204
205
  address: customer.address,
205
206
  },
206
207
  },
208
+ appearance: {
209
+ theme: theme.palette.mode,
210
+ variables: {
211
+ colorPrimary: theme.palette.primary.main,
212
+ colorBackground: theme.palette.background.paper,
213
+ colorText: theme.palette.text.primary,
214
+ colorDanger: theme.palette.error.main,
215
+ borderRadius: '4px',
216
+ },
217
+ },
207
218
  }}
208
219
  onChange={handlePaymentMethodChange}
209
220
  onReady={() => setState({ loaded: true })}
@@ -266,6 +277,7 @@ export default function StripeCheckout({
266
277
  const stripePromise = loadStripe(publicKey);
267
278
  const { isMobile } = useMobile();
268
279
  const { t, locale } = useLocaleContext();
280
+ const theme = useTheme();
269
281
  const [state, setState] = useSetState({
270
282
  open: true,
271
283
  closable: true,
@@ -295,6 +307,15 @@ export default function StripeCheckout({
295
307
  form: {
296
308
  justifyContent: 'flex-start',
297
309
  },
310
+ '.StripeElement--focus': {
311
+ borderColor: theme.palette.primary.main,
312
+ },
313
+ '.StripeElement--invalid': {
314
+ borderColor: theme.palette.error.main,
315
+ },
316
+ '.StripeElement--complete': {
317
+ borderColor: theme.palette.success.main,
318
+ },
298
319
  }}
299
320
  PaperProps={{
300
321
  style: {
@@ -305,6 +326,15 @@ export default function StripeCheckout({
305
326
  options={{
306
327
  clientSecret,
307
328
  locale: locale === 'zh' ? 'zh-CN' : 'en',
329
+ appearance: {
330
+ theme: theme.palette.mode,
331
+ variables: {
332
+ colorPrimary: theme.palette.primary.main,
333
+ colorBackground: theme.palette.background.paper,
334
+ colorText: theme.palette.text.primary,
335
+ colorDanger: theme.palette.error.main,
336
+ },
337
+ },
308
338
  }}
309
339
  stripe={stripePromise}>
310
340
  <StripeCheckoutForm