@blocklet/payment-react 1.23.11 → 1.24.1
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/es/components/over-due-invoice-payment.js +1 -1
- package/es/components/resume-subscription.js +1 -1
- package/es/components/stripe-payment-action.js +1 -1
- package/es/history/credit/transactions-list.js +29 -6
- package/es/locales/en.js +4 -0
- package/es/locales/zh.js +4 -0
- package/es/payment/product-item.js +45 -4
- package/es/payment/success.js +1 -1
- package/lib/components/over-due-invoice-payment.js +2 -2
- package/lib/components/resume-subscription.js +2 -2
- package/lib/components/stripe-payment-action.js +2 -2
- package/lib/history/credit/transactions-list.js +45 -17
- package/lib/locales/en.js +4 -0
- package/lib/locales/zh.js +4 -0
- package/lib/payment/product-item.js +56 -4
- package/lib/payment/success.js +3 -3
- package/package.json +11 -11
- package/src/components/over-due-invoice-payment.tsx +1 -1
- package/src/components/resume-subscription.tsx +1 -1
- package/src/components/stripe-payment-action.tsx +1 -1
- package/src/history/credit/transactions-list.tsx +42 -16
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +4 -0
- package/src/payment/product-item.tsx +75 -6
- package/src/payment/success.tsx +1 -1
|
@@ -7,7 +7,7 @@ import Toast from "@arcblock/ux/lib/Toast";
|
|
|
7
7
|
import { joinURL } from "ufo";
|
|
8
8
|
import { useRequest } from "ahooks";
|
|
9
9
|
import pWaitFor from "p-wait-for";
|
|
10
|
-
import
|
|
10
|
+
import Dialog from "@arcblock/ux/lib/Dialog/dialog";
|
|
11
11
|
import { CheckCircle as CheckCircleIcon } from "@mui/icons-material";
|
|
12
12
|
import debounce from "lodash/debounce";
|
|
13
13
|
import { usePaymentContext } from "../contexts/payment.js";
|
|
@@ -5,7 +5,7 @@ import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
|
|
|
5
5
|
import Toast from "@arcblock/ux/lib/Toast";
|
|
6
6
|
import { joinURL } from "ufo";
|
|
7
7
|
import { useRequest } from "ahooks";
|
|
8
|
-
import
|
|
8
|
+
import Dialog from "@arcblock/ux/lib/Dialog/dialog";
|
|
9
9
|
import { usePaymentContext } from "../contexts/payment.js";
|
|
10
10
|
import { formatError, formatToDate, getPrefix, isCrossOrigin } from "../libs/util.js";
|
|
11
11
|
import api from "../libs/api.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
|
|
3
3
|
import Toast from "@arcblock/ux/lib/Toast";
|
|
4
|
-
import
|
|
4
|
+
import Dialog from "@arcblock/ux/lib/Dialog/dialog";
|
|
5
5
|
import { Button, Typography } from "@mui/material";
|
|
6
6
|
import { useSetState } from "ahooks";
|
|
7
7
|
import { useEffect, useRef } from "react";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
|
|
3
|
-
import { Box, Typography, Grid, Stack, Link, Button } from "@mui/material";
|
|
3
|
+
import { Box, Typography, Grid, Stack, Link, Button, Chip } from "@mui/material";
|
|
4
4
|
import { useRequest } from "ahooks";
|
|
5
5
|
import { useNavigate } from "react-router-dom";
|
|
6
6
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -130,6 +130,7 @@ const TransactionsTable = React.memo((props) => {
|
|
|
130
130
|
customBodyRenderLite: (_, index) => {
|
|
131
131
|
const item = data?.list[index];
|
|
132
132
|
const isGrant = item.activity_type === "grant";
|
|
133
|
+
const isExpiredGrant = isGrant && item.status === "expired";
|
|
133
134
|
const amount = isGrant ? item.amount : item.credit_amount;
|
|
134
135
|
const currency = item.paymentCurrency || item.currency;
|
|
135
136
|
const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
|
|
@@ -137,11 +138,11 @@ const TransactionsTable = React.memo((props) => {
|
|
|
137
138
|
if (!includeGrants) {
|
|
138
139
|
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleTransactionClick(e, item), children: /* @__PURE__ */ jsx(Typography, { children: displayAmount }) });
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
+
const amountNode = /* @__PURE__ */ jsxs(
|
|
141
142
|
Typography,
|
|
142
143
|
{
|
|
143
144
|
sx: {
|
|
144
|
-
color: isGrant ? "success.main" : "error.main"
|
|
145
|
+
color: isGrant ? isExpiredGrant ? "text.disabled" : "success.main" : "error.main"
|
|
145
146
|
},
|
|
146
147
|
children: [
|
|
147
148
|
isGrant ? "+" : "-",
|
|
@@ -149,7 +150,25 @@ const TransactionsTable = React.memo((props) => {
|
|
|
149
150
|
displayAmount
|
|
150
151
|
]
|
|
151
152
|
}
|
|
152
|
-
)
|
|
153
|
+
);
|
|
154
|
+
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleTransactionClick(e, item), children: /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", justifyContent: "flex-end", sx: { width: "100%" }, children: [
|
|
155
|
+
isExpiredGrant && /* @__PURE__ */ jsx(
|
|
156
|
+
Chip,
|
|
157
|
+
{
|
|
158
|
+
label: t("admin.creditGrants.status.expired"),
|
|
159
|
+
size: "small",
|
|
160
|
+
variant: "outlined",
|
|
161
|
+
sx: {
|
|
162
|
+
mr: 2,
|
|
163
|
+
height: 18,
|
|
164
|
+
fontSize: "12px",
|
|
165
|
+
color: "text.disabled",
|
|
166
|
+
borderColor: "text.disabled"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
),
|
|
170
|
+
amountNode
|
|
171
|
+
] }) });
|
|
153
172
|
}
|
|
154
173
|
}
|
|
155
174
|
},
|
|
@@ -160,8 +179,10 @@ const TransactionsTable = React.memo((props) => {
|
|
|
160
179
|
customBodyRenderLite: (_, index) => {
|
|
161
180
|
const item = data?.list[index];
|
|
162
181
|
const isGrant = item.activity_type === "grant";
|
|
182
|
+
const isExpiredGrant = isGrant && item.status === "expired";
|
|
163
183
|
const grantName = isGrant ? item.name : item.creditGrant.name;
|
|
164
184
|
const grantId = isGrant ? item.id : item.credit_grant_id;
|
|
185
|
+
const nameNode = /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { cursor: "pointer", color: isExpiredGrant ? "text.disabled" : void 0 }, children: grantName || `Grant ${grantId.slice(-6)}` });
|
|
165
186
|
return /* @__PURE__ */ jsx(
|
|
166
187
|
Stack,
|
|
167
188
|
{
|
|
@@ -174,7 +195,7 @@ const TransactionsTable = React.memo((props) => {
|
|
|
174
195
|
sx: {
|
|
175
196
|
alignItems: "center"
|
|
176
197
|
},
|
|
177
|
-
children:
|
|
198
|
+
children: nameNode
|
|
178
199
|
}
|
|
179
200
|
);
|
|
180
201
|
}
|
|
@@ -187,8 +208,10 @@ const TransactionsTable = React.memo((props) => {
|
|
|
187
208
|
customBodyRenderLite: (_, index) => {
|
|
188
209
|
const item = data?.list[index];
|
|
189
210
|
const isGrant = item.activity_type === "grant";
|
|
211
|
+
const isExpiredGrant = isGrant && item.status === "expired";
|
|
190
212
|
const description = isGrant ? item.name || item.description || "Credit Granted" : item.subscription?.description || item.description || `${item.meter_event_name} usage`;
|
|
191
|
-
|
|
213
|
+
const descriptionNode = /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { fontWeight: 400, color: isExpiredGrant ? "text.disabled" : void 0 }, children: description });
|
|
214
|
+
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleTransactionClick(e, item), children: descriptionNode });
|
|
192
215
|
}
|
|
193
216
|
}
|
|
194
217
|
},
|
package/es/locales/en.js
CHANGED
|
@@ -264,6 +264,10 @@ export default flat({
|
|
|
264
264
|
oneTimeEnoughWithExpiry: "You have a usage overage of {amount}. This purchase adds {totalAmount} (valid for {duration} {unit}). After covering the overage, your new available balance will be {availableAmount}.",
|
|
265
265
|
recurringEnough: "You have a usage overage of {amount}. This subscription adds {totalAmount} {period}. After covering the overage, your new available balance will be {availableAmount}.",
|
|
266
266
|
recurringEnoughWithExpiry: "You have a usage overage of {amount}. This subscription adds {totalAmount} {period} (valid for {duration} {unit}). After covering the overage, your new available balance will be {availableAmount}."
|
|
267
|
+
},
|
|
268
|
+
schedule: {
|
|
269
|
+
periodic: "Grant {amount} every {interval}.",
|
|
270
|
+
withRefresh: "Grant {amount} every {interval}; unused credits expire with the next grant."
|
|
267
271
|
}
|
|
268
272
|
},
|
|
269
273
|
expired: {
|
package/es/locales/zh.js
CHANGED
|
@@ -301,6 +301,10 @@ export default flat({
|
|
|
301
301
|
oneTimeEnoughWithExpiry: "\u60A8\u6709 {amount} \u7684\u4F7F\u7528\u8D85\u989D\u3002\u672C\u6B21\u8D2D\u4E70\u5C06\u589E\u52A0 {totalAmount}\uFF08\u6709\u6548\u671F {duration} {unit}\uFF09\u3002\u6263\u9664\u8D85\u989D\u540E\uFF0C\u60A8\u7684\u65B0\u53EF\u7528\u4F59\u989D\u5C06\u4E3A {availableAmount}\u3002",
|
|
302
302
|
recurringEnough: "\u60A8\u6709 {amount} \u7684\u4F7F\u7528\u8D85\u989D\u3002\u672C\u8BA2\u9605{period}\u5C06\u589E\u52A0 {totalAmount}\u3002\u6263\u9664\u8D85\u989D\u540E\uFF0C\u60A8\u7684\u65B0\u53EF\u7528\u4F59\u989D\u5C06\u4E3A {availableAmount}\u3002",
|
|
303
303
|
recurringEnoughWithExpiry: "\u60A8\u6709 {amount} \u7684\u4F7F\u7528\u8D85\u989D\u3002\u672C\u8BA2\u9605{period}\u5C06\u589E\u52A0 {totalAmount}\uFF08\u6709\u6548\u671F {duration} {unit}\uFF09\u3002\u6263\u9664\u8D85\u989D\u540E\uFF0C\u60A8\u7684\u65B0\u53EF\u7528\u4F59\u989D\u5C06\u4E3A {availableAmount}\u3002"
|
|
304
|
+
},
|
|
305
|
+
schedule: {
|
|
306
|
+
periodic: "\u6BCF{interval}\u53D1\u653E {amount}\u3002",
|
|
307
|
+
withRefresh: "\u6BCF{interval}\u53D1\u653E {amount}\uFF0C\u672A\u4F7F\u7528\u989D\u5EA6\u5C06\u5728\u4E0B\u6B21\u53D1\u653E\u65F6\u8FC7\u671F\u3002"
|
|
304
308
|
}
|
|
305
309
|
},
|
|
306
310
|
emptyItems: {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
formatUpsellSaving,
|
|
18
18
|
formatAmount,
|
|
19
19
|
formatCreditForCheckout,
|
|
20
|
+
formatCreditAmount,
|
|
20
21
|
formatBNStr
|
|
21
22
|
} from "../libs/util.js";
|
|
22
23
|
import ProductCard from "./product-card.js";
|
|
@@ -69,11 +70,13 @@ export default function ProductItem({
|
|
|
69
70
|
const saving = formatUpsellSaving(items, currency);
|
|
70
71
|
const metered = item.price?.recurring?.usage_type === "metered" ? t("common.metered") : "";
|
|
71
72
|
const canUpsell = mode === "normal" && items.length === 1;
|
|
72
|
-
const isCreditProduct = item.price.product?.type === "credit" && item.price.metadata?.credit_config
|
|
73
|
-
const creditAmount = isCreditProduct ? Number(item.price.metadata.credit_config.credit_amount) : 0;
|
|
74
|
-
const creditCurrency = isCreditProduct ? findCurrency(settings.paymentMethods, item.price.metadata
|
|
73
|
+
const isCreditProduct = item.price.product?.type === "credit" && item.price.metadata?.credit_config;
|
|
74
|
+
const creditAmount = isCreditProduct && item.price.metadata?.credit_config?.credit_amount ? Number(item.price.metadata.credit_config.credit_amount) : 0;
|
|
75
|
+
const creditCurrency = isCreditProduct && item.price.metadata?.credit_config?.currency_id ? findCurrency(settings.paymentMethods, item.price.metadata.credit_config.currency_id) : null;
|
|
75
76
|
const validDuration = item.price.metadata?.credit_config?.valid_duration_value;
|
|
76
77
|
const validDurationUnit = item.price.metadata?.credit_config?.valid_duration_unit || "days";
|
|
78
|
+
const scheduleConfig = item.price.metadata?.credit_config?.schedule;
|
|
79
|
+
const hasSchedule = scheduleConfig?.enabled && scheduleConfig?.delivery_mode && scheduleConfig.delivery_mode !== "invoice";
|
|
77
80
|
const userDid = session?.user?.did;
|
|
78
81
|
const { data: pendingAmount } = useRequest(
|
|
79
82
|
async () => {
|
|
@@ -254,6 +257,44 @@ export default function ProductItem({
|
|
|
254
257
|
const type = isRecurring ? "recurringEnough" : "oneTimeEnough";
|
|
255
258
|
return t(getLocaleKey("pending", type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
256
259
|
};
|
|
260
|
+
const formatScheduleInfo = () => {
|
|
261
|
+
if (!hasSchedule || !scheduleConfig) return null;
|
|
262
|
+
const totalCredit = creditAmount * (localQuantity || 0);
|
|
263
|
+
const currencySymbol = creditCurrency?.symbol || "Credits";
|
|
264
|
+
const intervalUnit = scheduleConfig.interval_unit;
|
|
265
|
+
const intervalValue = scheduleConfig.interval_value;
|
|
266
|
+
let amountPerGrant;
|
|
267
|
+
if (scheduleConfig.amount_per_grant) {
|
|
268
|
+
amountPerGrant = Number(scheduleConfig.amount_per_grant) * (localQuantity || 1);
|
|
269
|
+
} else {
|
|
270
|
+
const billingIntervalUnit = item.price.recurring?.interval || "month";
|
|
271
|
+
const billingIntervalCount = item.price.recurring?.interval_count || 1;
|
|
272
|
+
const unitToHours = {
|
|
273
|
+
hour: 1,
|
|
274
|
+
day: 24,
|
|
275
|
+
week: 168,
|
|
276
|
+
month: 720
|
|
277
|
+
// ~30 days
|
|
278
|
+
};
|
|
279
|
+
const billingHours = unitToHours[billingIntervalUnit] * billingIntervalCount;
|
|
280
|
+
const scheduleHours = unitToHours[intervalUnit] * intervalValue;
|
|
281
|
+
const grantsPerPeriod = Math.floor(billingHours / scheduleHours);
|
|
282
|
+
amountPerGrant = grantsPerPeriod > 0 ? totalCredit / grantsPerPeriod : totalCredit;
|
|
283
|
+
}
|
|
284
|
+
const formattedAmount = formatCreditAmount(formatNumber(amountPerGrant), currencySymbol);
|
|
285
|
+
const intervalDisplay = intervalValue === 1 ? t(`common.${intervalUnit}`) : ` ${intervalValue} ${t(`common.${intervalUnit}s`)} `;
|
|
286
|
+
const expireWithNext = scheduleConfig.expire_with_next_grant;
|
|
287
|
+
if (expireWithNext) {
|
|
288
|
+
return t("payment.checkout.credit.schedule.withRefresh", {
|
|
289
|
+
amount: formattedAmount,
|
|
290
|
+
interval: intervalDisplay
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return t("payment.checkout.credit.schedule.periodic", {
|
|
294
|
+
amount: formattedAmount,
|
|
295
|
+
interval: intervalDisplay
|
|
296
|
+
});
|
|
297
|
+
};
|
|
257
298
|
const primaryText = useMemo(() => {
|
|
258
299
|
const price = item.upsell_price || item.price || {};
|
|
259
300
|
const isRecurring = price?.type === "recurring" && price?.recurring;
|
|
@@ -495,7 +536,7 @@ export default function ProductItem({
|
|
|
495
536
|
]
|
|
496
537
|
}
|
|
497
538
|
) }),
|
|
498
|
-
isCreditProduct && /* @__PURE__ */ jsx(Alert, { severity: "info", sx: { mt: 1, fontSize: "0.875rem" }, icon: false, children: formatCreditInfo() }),
|
|
539
|
+
isCreditProduct && /* @__PURE__ */ jsx(Alert, { severity: "info", sx: { mt: 1, fontSize: "0.875rem" }, icon: false, children: hasSchedule ? /* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: "inherit" }, children: formatScheduleInfo() }) : /* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: "inherit" }, children: formatCreditInfo() }) }),
|
|
499
540
|
children
|
|
500
541
|
]
|
|
501
542
|
}
|
package/es/payment/success.js
CHANGED
|
@@ -5,7 +5,7 @@ import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
|
|
|
5
5
|
import { Box, Grow, Link, Paper, Stack, styled, Typography } from "@mui/material";
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { joinURL } from "ufo";
|
|
8
|
-
import
|
|
8
|
+
import Button from "@arcblock/ux/lib/Button";
|
|
9
9
|
import { usePaymentContext } from "../contexts/payment.js";
|
|
10
10
|
import { VendorPlaceholder, VendorProgressItem } from "./progress-item.js";
|
|
11
11
|
export default function PaymentSuccess({
|
|
@@ -13,7 +13,7 @@ var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast"));
|
|
|
13
13
|
var _ufo = require("ufo");
|
|
14
14
|
var _ahooks = require("ahooks");
|
|
15
15
|
var _pWaitFor = _interopRequireDefault(require("p-wait-for"));
|
|
16
|
-
var
|
|
16
|
+
var _dialog = _interopRequireDefault(require("@arcblock/ux/lib/Dialog/dialog"));
|
|
17
17
|
var _iconsMaterial = require("@mui/icons-material");
|
|
18
18
|
var _debounce = _interopRequireDefault(require("lodash/debounce"));
|
|
19
19
|
var _payment = require("../contexts/payment");
|
|
@@ -457,7 +457,7 @@ function OverdueInvoicePayment({
|
|
|
457
457
|
})
|
|
458
458
|
});
|
|
459
459
|
}
|
|
460
|
-
return /* @__PURE__ */(0, _jsxRuntime.jsx)(
|
|
460
|
+
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_dialog.default, {
|
|
461
461
|
PaperProps: {
|
|
462
462
|
style: {
|
|
463
463
|
minHeight: "auto"
|
|
@@ -11,7 +11,7 @@ var _context = require("@arcblock/ux/lib/Locale/context");
|
|
|
11
11
|
var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast"));
|
|
12
12
|
var _ufo = require("ufo");
|
|
13
13
|
var _ahooks = require("ahooks");
|
|
14
|
-
var
|
|
14
|
+
var _dialog = _interopRequireDefault(require("@arcblock/ux/lib/Dialog/dialog"));
|
|
15
15
|
var _payment = require("../contexts/payment");
|
|
16
16
|
var _util = require("../libs/util");
|
|
17
17
|
var _api = _interopRequireDefault(require("../libs/api"));
|
|
@@ -152,7 +152,7 @@ function RecoverSubscription({
|
|
|
152
152
|
if (loading) {
|
|
153
153
|
return null;
|
|
154
154
|
}
|
|
155
|
-
return /* @__PURE__ */(0, _jsxRuntime.jsx)(
|
|
155
|
+
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_dialog.default, {
|
|
156
156
|
PaperProps: {
|
|
157
157
|
style: {
|
|
158
158
|
minHeight: "auto"
|
|
@@ -7,7 +7,7 @@ module.exports = StripePaymentAction;
|
|
|
7
7
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
8
8
|
var _context = require("@arcblock/ux/lib/Locale/context");
|
|
9
9
|
var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast"));
|
|
10
|
-
var
|
|
10
|
+
var _dialog = _interopRequireDefault(require("@arcblock/ux/lib/Dialog/dialog"));
|
|
11
11
|
var _material = require("@mui/material");
|
|
12
12
|
var _ahooks = require("ahooks");
|
|
13
13
|
var _react = require("react");
|
|
@@ -148,7 +148,7 @@ function StripePaymentAction(props) {
|
|
|
148
148
|
onClose?.();
|
|
149
149
|
};
|
|
150
150
|
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
151
|
-
children: [children?.(handlePay, state.paying), state.confirmDialog && /* @__PURE__ */(0, _jsxRuntime.jsx)(
|
|
151
|
+
children: [children?.(handlePay, state.paying), state.confirmDialog && /* @__PURE__ */(0, _jsxRuntime.jsx)(_dialog.default, {
|
|
152
152
|
open: state.confirmDialog,
|
|
153
153
|
title: t("payment.customer.invoice.paymentConfirmTitle"),
|
|
154
154
|
onClose: handleConfirmCancel,
|
|
@@ -154,6 +154,7 @@ const TransactionsTable = _react.default.memo(props => {
|
|
|
154
154
|
customBodyRenderLite: (_, index) => {
|
|
155
155
|
const item = data?.list[index];
|
|
156
156
|
const isGrant = item.activity_type === "grant";
|
|
157
|
+
const isExpiredGrant = isGrant && item.status === "expired";
|
|
157
158
|
const amount = isGrant ? item.amount : item.credit_amount;
|
|
158
159
|
const currency = item.paymentCurrency || item.currency;
|
|
159
160
|
const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
|
|
@@ -166,13 +167,34 @@ const TransactionsTable = _react.default.memo(props => {
|
|
|
166
167
|
})
|
|
167
168
|
});
|
|
168
169
|
}
|
|
170
|
+
const amountNode = /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
|
|
171
|
+
sx: {
|
|
172
|
+
color: isGrant ? isExpiredGrant ? "text.disabled" : "success.main" : "error.main"
|
|
173
|
+
},
|
|
174
|
+
children: [isGrant ? "+" : "-", " ", displayAmount]
|
|
175
|
+
});
|
|
169
176
|
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
170
177
|
onClick: e => handleTransactionClick(e, item),
|
|
171
|
-
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.
|
|
178
|
+
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
179
|
+
direction: "row",
|
|
180
|
+
spacing: 1,
|
|
181
|
+
alignItems: "center",
|
|
182
|
+
justifyContent: "flex-end",
|
|
172
183
|
sx: {
|
|
173
|
-
|
|
184
|
+
width: "100%"
|
|
174
185
|
},
|
|
175
|
-
children: [
|
|
186
|
+
children: [isExpiredGrant && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Chip, {
|
|
187
|
+
label: t("admin.creditGrants.status.expired"),
|
|
188
|
+
size: "small",
|
|
189
|
+
variant: "outlined",
|
|
190
|
+
sx: {
|
|
191
|
+
mr: 2,
|
|
192
|
+
height: 18,
|
|
193
|
+
fontSize: "12px",
|
|
194
|
+
color: "text.disabled",
|
|
195
|
+
borderColor: "text.disabled"
|
|
196
|
+
}
|
|
197
|
+
}), amountNode]
|
|
176
198
|
})
|
|
177
199
|
});
|
|
178
200
|
}
|
|
@@ -184,8 +206,17 @@ const TransactionsTable = _react.default.memo(props => {
|
|
|
184
206
|
customBodyRenderLite: (_, index) => {
|
|
185
207
|
const item = data?.list[index];
|
|
186
208
|
const isGrant = item.activity_type === "grant";
|
|
209
|
+
const isExpiredGrant = isGrant && item.status === "expired";
|
|
187
210
|
const grantName = isGrant ? item.name : item.creditGrant.name;
|
|
188
211
|
const grantId = isGrant ? item.id : item.credit_grant_id;
|
|
212
|
+
const nameNode = /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
213
|
+
variant: "body2",
|
|
214
|
+
sx: {
|
|
215
|
+
cursor: "pointer",
|
|
216
|
+
color: isExpiredGrant ? "text.disabled" : void 0
|
|
217
|
+
},
|
|
218
|
+
children: grantName || `Grant ${grantId.slice(-6)}`
|
|
219
|
+
});
|
|
189
220
|
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, {
|
|
190
221
|
direction: "row",
|
|
191
222
|
spacing: 1,
|
|
@@ -196,13 +227,7 @@ const TransactionsTable = _react.default.memo(props => {
|
|
|
196
227
|
sx: {
|
|
197
228
|
alignItems: "center"
|
|
198
229
|
},
|
|
199
|
-
children:
|
|
200
|
-
variant: "body2",
|
|
201
|
-
sx: {
|
|
202
|
-
cursor: "pointer"
|
|
203
|
-
},
|
|
204
|
-
children: grantName || `Grant ${grantId.slice(-6)}`
|
|
205
|
-
})
|
|
230
|
+
children: nameNode
|
|
206
231
|
});
|
|
207
232
|
}
|
|
208
233
|
}
|
|
@@ -213,16 +238,19 @@ const TransactionsTable = _react.default.memo(props => {
|
|
|
213
238
|
customBodyRenderLite: (_, index) => {
|
|
214
239
|
const item = data?.list[index];
|
|
215
240
|
const isGrant = item.activity_type === "grant";
|
|
241
|
+
const isExpiredGrant = isGrant && item.status === "expired";
|
|
216
242
|
const description = isGrant ? item.name || item.description || "Credit Granted" : item.subscription?.description || item.description || `${item.meter_event_name} usage`;
|
|
243
|
+
const descriptionNode = /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
244
|
+
variant: "body2",
|
|
245
|
+
sx: {
|
|
246
|
+
fontWeight: 400,
|
|
247
|
+
color: isExpiredGrant ? "text.disabled" : void 0
|
|
248
|
+
},
|
|
249
|
+
children: description
|
|
250
|
+
});
|
|
217
251
|
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
218
252
|
onClick: e => handleTransactionClick(e, item),
|
|
219
|
-
children:
|
|
220
|
-
variant: "body2",
|
|
221
|
-
sx: {
|
|
222
|
-
fontWeight: 400
|
|
223
|
-
},
|
|
224
|
-
children: description
|
|
225
|
-
})
|
|
253
|
+
children: descriptionNode
|
|
226
254
|
});
|
|
227
255
|
}
|
|
228
256
|
}
|
package/lib/locales/en.js
CHANGED
|
@@ -271,6 +271,10 @@ module.exports = (0, _flat.default)({
|
|
|
271
271
|
oneTimeEnoughWithExpiry: "You have a usage overage of {amount}. This purchase adds {totalAmount} (valid for {duration} {unit}). After covering the overage, your new available balance will be {availableAmount}.",
|
|
272
272
|
recurringEnough: "You have a usage overage of {amount}. This subscription adds {totalAmount} {period}. After covering the overage, your new available balance will be {availableAmount}.",
|
|
273
273
|
recurringEnoughWithExpiry: "You have a usage overage of {amount}. This subscription adds {totalAmount} {period} (valid for {duration} {unit}). After covering the overage, your new available balance will be {availableAmount}."
|
|
274
|
+
},
|
|
275
|
+
schedule: {
|
|
276
|
+
periodic: "Grant {amount} every {interval}.",
|
|
277
|
+
withRefresh: "Grant {amount} every {interval}; unused credits expire with the next grant."
|
|
274
278
|
}
|
|
275
279
|
},
|
|
276
280
|
expired: {
|
package/lib/locales/zh.js
CHANGED
|
@@ -308,6 +308,10 @@ module.exports = (0, _flat.default)({
|
|
|
308
308
|
oneTimeEnoughWithExpiry: "\u60A8\u6709 {amount} \u7684\u4F7F\u7528\u8D85\u989D\u3002\u672C\u6B21\u8D2D\u4E70\u5C06\u589E\u52A0 {totalAmount}\uFF08\u6709\u6548\u671F {duration} {unit}\uFF09\u3002\u6263\u9664\u8D85\u989D\u540E\uFF0C\u60A8\u7684\u65B0\u53EF\u7528\u4F59\u989D\u5C06\u4E3A {availableAmount}\u3002",
|
|
309
309
|
recurringEnough: "\u60A8\u6709 {amount} \u7684\u4F7F\u7528\u8D85\u989D\u3002\u672C\u8BA2\u9605{period}\u5C06\u589E\u52A0 {totalAmount}\u3002\u6263\u9664\u8D85\u989D\u540E\uFF0C\u60A8\u7684\u65B0\u53EF\u7528\u4F59\u989D\u5C06\u4E3A {availableAmount}\u3002",
|
|
310
310
|
recurringEnoughWithExpiry: "\u60A8\u6709 {amount} \u7684\u4F7F\u7528\u8D85\u989D\u3002\u672C\u8BA2\u9605{period}\u5C06\u589E\u52A0 {totalAmount}\uFF08\u6709\u6548\u671F {duration} {unit}\uFF09\u3002\u6263\u9664\u8D85\u989D\u540E\uFF0C\u60A8\u7684\u65B0\u53EF\u7528\u4F59\u989D\u5C06\u4E3A {availableAmount}\u3002"
|
|
311
|
+
},
|
|
312
|
+
schedule: {
|
|
313
|
+
periodic: "\u6BCF{interval}\u53D1\u653E {amount}\u3002",
|
|
314
|
+
withRefresh: "\u6BCF{interval}\u53D1\u653E {amount}\uFF0C\u672A\u4F7F\u7528\u989D\u5EA6\u5C06\u5728\u4E0B\u6B21\u53D1\u653E\u65F6\u8FC7\u671F\u3002"
|
|
311
315
|
}
|
|
312
316
|
},
|
|
313
317
|
emptyItems: {
|
|
@@ -76,11 +76,13 @@ function ProductItem({
|
|
|
76
76
|
const saving = (0, _util2.formatUpsellSaving)(items, currency);
|
|
77
77
|
const metered = item.price?.recurring?.usage_type === "metered" ? t("common.metered") : "";
|
|
78
78
|
const canUpsell = mode === "normal" && items.length === 1;
|
|
79
|
-
const isCreditProduct = item.price.product?.type === "credit" && item.price.metadata?.credit_config
|
|
80
|
-
const creditAmount = isCreditProduct ? Number(item.price.metadata.credit_config.credit_amount) : 0;
|
|
81
|
-
const creditCurrency = isCreditProduct ? (0, _util2.findCurrency)(settings.paymentMethods, item.price.metadata
|
|
79
|
+
const isCreditProduct = item.price.product?.type === "credit" && item.price.metadata?.credit_config;
|
|
80
|
+
const creditAmount = isCreditProduct && item.price.metadata?.credit_config?.credit_amount ? Number(item.price.metadata.credit_config.credit_amount) : 0;
|
|
81
|
+
const creditCurrency = isCreditProduct && item.price.metadata?.credit_config?.currency_id ? (0, _util2.findCurrency)(settings.paymentMethods, item.price.metadata.credit_config.currency_id) : null;
|
|
82
82
|
const validDuration = item.price.metadata?.credit_config?.valid_duration_value;
|
|
83
83
|
const validDurationUnit = item.price.metadata?.credit_config?.valid_duration_unit || "days";
|
|
84
|
+
const scheduleConfig = item.price.metadata?.credit_config?.schedule;
|
|
85
|
+
const hasSchedule = scheduleConfig?.enabled && scheduleConfig?.delivery_mode && scheduleConfig.delivery_mode !== "invoice";
|
|
84
86
|
const userDid = session?.user?.did;
|
|
85
87
|
const {
|
|
86
88
|
data: pendingAmount
|
|
@@ -250,6 +252,44 @@ function ProductItem({
|
|
|
250
252
|
const type = isRecurring ? "recurringEnough" : "oneTimeEnough";
|
|
251
253
|
return t(getLocaleKey("pending", type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
252
254
|
};
|
|
255
|
+
const formatScheduleInfo = () => {
|
|
256
|
+
if (!hasSchedule || !scheduleConfig) return null;
|
|
257
|
+
const totalCredit = creditAmount * (localQuantity || 0);
|
|
258
|
+
const currencySymbol = creditCurrency?.symbol || "Credits";
|
|
259
|
+
const intervalUnit = scheduleConfig.interval_unit;
|
|
260
|
+
const intervalValue = scheduleConfig.interval_value;
|
|
261
|
+
let amountPerGrant;
|
|
262
|
+
if (scheduleConfig.amount_per_grant) {
|
|
263
|
+
amountPerGrant = Number(scheduleConfig.amount_per_grant) * (localQuantity || 1);
|
|
264
|
+
} else {
|
|
265
|
+
const billingIntervalUnit = item.price.recurring?.interval || "month";
|
|
266
|
+
const billingIntervalCount = item.price.recurring?.interval_count || 1;
|
|
267
|
+
const unitToHours = {
|
|
268
|
+
hour: 1,
|
|
269
|
+
day: 24,
|
|
270
|
+
week: 168,
|
|
271
|
+
month: 720
|
|
272
|
+
// ~30 days
|
|
273
|
+
};
|
|
274
|
+
const billingHours = unitToHours[billingIntervalUnit] * billingIntervalCount;
|
|
275
|
+
const scheduleHours = unitToHours[intervalUnit] * intervalValue;
|
|
276
|
+
const grantsPerPeriod = Math.floor(billingHours / scheduleHours);
|
|
277
|
+
amountPerGrant = grantsPerPeriod > 0 ? totalCredit / grantsPerPeriod : totalCredit;
|
|
278
|
+
}
|
|
279
|
+
const formattedAmount = (0, _util2.formatCreditAmount)((0, _util2.formatNumber)(amountPerGrant), currencySymbol);
|
|
280
|
+
const intervalDisplay = intervalValue === 1 ? t(`common.${intervalUnit}`) : ` ${intervalValue} ${t(`common.${intervalUnit}s`)} `;
|
|
281
|
+
const expireWithNext = scheduleConfig.expire_with_next_grant;
|
|
282
|
+
if (expireWithNext) {
|
|
283
|
+
return t("payment.checkout.credit.schedule.withRefresh", {
|
|
284
|
+
amount: formattedAmount,
|
|
285
|
+
interval: intervalDisplay
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return t("payment.checkout.credit.schedule.periodic", {
|
|
289
|
+
amount: formattedAmount,
|
|
290
|
+
interval: intervalDisplay
|
|
291
|
+
});
|
|
292
|
+
};
|
|
253
293
|
const primaryText = (0, _react.useMemo)(() => {
|
|
254
294
|
const price = item.upsell_price || item.price || {};
|
|
255
295
|
const isRecurring = price?.type === "recurring" && price?.recurring;
|
|
@@ -484,7 +524,19 @@ function ProductItem({
|
|
|
484
524
|
fontSize: "0.875rem"
|
|
485
525
|
},
|
|
486
526
|
icon: false,
|
|
487
|
-
children:
|
|
527
|
+
children: hasSchedule ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
528
|
+
component: "span",
|
|
529
|
+
sx: {
|
|
530
|
+
fontSize: "inherit"
|
|
531
|
+
},
|
|
532
|
+
children: formatScheduleInfo()
|
|
533
|
+
}) : /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
534
|
+
component: "span",
|
|
535
|
+
sx: {
|
|
536
|
+
fontSize: "inherit"
|
|
537
|
+
},
|
|
538
|
+
children: formatCreditInfo()
|
|
539
|
+
})
|
|
488
540
|
}), children]
|
|
489
541
|
}), canUpsell && !item.upsell_price_id && item.price.upsell?.upsells_to && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
490
542
|
direction: "row",
|
package/lib/payment/success.js
CHANGED
|
@@ -11,7 +11,7 @@ var _context = require("@arcblock/ux/lib/Locale/context");
|
|
|
11
11
|
var _material = require("@mui/material");
|
|
12
12
|
var _react = require("react");
|
|
13
13
|
var _ufo = require("ufo");
|
|
14
|
-
var
|
|
14
|
+
var _Button = _interopRequireDefault(require("@arcblock/ux/lib/Button"));
|
|
15
15
|
var _payment = require("../contexts/payment");
|
|
16
16
|
var _progressItem = require("./progress-item");
|
|
17
17
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -160,7 +160,7 @@ function PaymentSuccess({
|
|
|
160
160
|
borderColor: "grey.300",
|
|
161
161
|
mx: 2
|
|
162
162
|
}
|
|
163
|
-
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(
|
|
163
|
+
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_Button.default, {
|
|
164
164
|
variant: "text",
|
|
165
165
|
color: "primary",
|
|
166
166
|
size: "small",
|
|
@@ -175,7 +175,7 @@ function PaymentSuccess({
|
|
|
175
175
|
}, subscription.id))
|
|
176
176
|
});
|
|
177
177
|
} else if (subscriptionId) {
|
|
178
|
-
next = /* @__PURE__ */(0, _jsxRuntime.jsx)(
|
|
178
|
+
next = /* @__PURE__ */(0, _jsxRuntime.jsx)(_Button.default, {
|
|
179
179
|
variant: "outlined",
|
|
180
180
|
color: "primary",
|
|
181
181
|
sx: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/payment-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.1",
|
|
4
4
|
"description": "Reusable react components for payment kit v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -54,18 +54,18 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@arcblock/bridge": "^3.
|
|
58
|
-
"@arcblock/did-connect-react": "^3.
|
|
59
|
-
"@arcblock/react-hooks": "^3.
|
|
60
|
-
"@arcblock/ux": "^3.
|
|
61
|
-
"@arcblock/ws": "^1.28.
|
|
62
|
-
"@blocklet/theme": "^3.
|
|
63
|
-
"@blocklet/ui-react": "^3.
|
|
57
|
+
"@arcblock/bridge": "^3.4.7",
|
|
58
|
+
"@arcblock/did-connect-react": "^3.4.7",
|
|
59
|
+
"@arcblock/react-hooks": "^3.4.7",
|
|
60
|
+
"@arcblock/ux": "^3.4.7",
|
|
61
|
+
"@arcblock/ws": "^1.28.5",
|
|
62
|
+
"@blocklet/theme": "^3.4.7",
|
|
63
|
+
"@blocklet/ui-react": "^3.4.7",
|
|
64
64
|
"@mui/icons-material": "^7.1.2",
|
|
65
65
|
"@mui/lab": "7.0.0-beta.14",
|
|
66
66
|
"@mui/material": "^7.1.2",
|
|
67
67
|
"@mui/system": "^7.1.1",
|
|
68
|
-
"@ocap/util": "^1.28.
|
|
68
|
+
"@ocap/util": "^1.28.5",
|
|
69
69
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
70
70
|
"@stripe/stripe-js": "^2.4.0",
|
|
71
71
|
"@vitejs/plugin-legacy": "^7.0.0",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"@babel/core": "^7.27.4",
|
|
97
97
|
"@babel/preset-env": "^7.27.2",
|
|
98
98
|
"@babel/preset-react": "^7.27.1",
|
|
99
|
-
"@blocklet/payment-types": "1.
|
|
99
|
+
"@blocklet/payment-types": "1.24.1",
|
|
100
100
|
"@storybook/addon-essentials": "^7.6.20",
|
|
101
101
|
"@storybook/addon-interactions": "^7.6.20",
|
|
102
102
|
"@storybook/addon-links": "^7.6.20",
|
|
@@ -127,5 +127,5 @@
|
|
|
127
127
|
"vite-plugin-babel": "^1.3.1",
|
|
128
128
|
"vite-plugin-node-polyfills": "^0.23.0"
|
|
129
129
|
},
|
|
130
|
-
"gitHead": "
|
|
130
|
+
"gitHead": "37207c8206743f75ad682d9e020fa04ab7b93e7b"
|
|
131
131
|
}
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
} from '@blocklet/payment-types';
|
|
16
16
|
import { useRequest } from 'ahooks';
|
|
17
17
|
import pWaitFor from 'p-wait-for';
|
|
18
|
-
import
|
|
18
|
+
import Dialog from '@arcblock/ux/lib/Dialog/dialog';
|
|
19
19
|
import { CheckCircle as CheckCircleIcon } from '@mui/icons-material';
|
|
20
20
|
import debounce from 'lodash/debounce';
|
|
21
21
|
import { usePaymentContext } from '../contexts/payment';
|
|
@@ -5,7 +5,7 @@ import Toast from '@arcblock/ux/lib/Toast';
|
|
|
5
5
|
import { joinURL } from 'ufo';
|
|
6
6
|
import type { Subscription, TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
7
7
|
import { useRequest } from 'ahooks';
|
|
8
|
-
import
|
|
8
|
+
import Dialog from '@arcblock/ux/lib/Dialog/dialog';
|
|
9
9
|
import { usePaymentContext } from '../contexts/payment';
|
|
10
10
|
import { formatError, formatToDate, getPrefix, isCrossOrigin } from '../libs/util';
|
|
11
11
|
import api from '../libs/api';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable react/require-default-props */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
-
import
|
|
4
|
+
import Dialog from '@arcblock/ux/lib/Dialog/dialog';
|
|
5
5
|
import type { Customer, TPaymentMethod, TInvoiceExpanded } from '@blocklet/payment-types';
|
|
6
6
|
import { Button, Typography } from '@mui/material';
|
|
7
7
|
import { useSetState } from 'ahooks';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/* eslint-disable react/no-unstable-nested-components */
|
|
7
7
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
8
8
|
import type { Paginated, TCreditTransactionExpanded } from '@blocklet/payment-types';
|
|
9
|
-
import { Box, Typography, Grid, Stack, Link, Button } from '@mui/material';
|
|
9
|
+
import { Box, Typography, Grid, Stack, Link, Button, Chip } from '@mui/material';
|
|
10
10
|
import { useRequest } from 'ahooks';
|
|
11
11
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
12
12
|
import { useNavigate } from 'react-router-dom';
|
|
@@ -181,6 +181,7 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
181
181
|
customBodyRenderLite: (_: string, index: number) => {
|
|
182
182
|
const item = data?.list[index] as any;
|
|
183
183
|
const isGrant = item.activity_type === 'grant';
|
|
184
|
+
const isExpiredGrant = isGrant && item.status === 'expired';
|
|
184
185
|
const amount = isGrant ? item.amount : item.credit_amount;
|
|
185
186
|
const currency = item.paymentCurrency || item.currency;
|
|
186
187
|
const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
|
|
@@ -194,14 +195,34 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
194
195
|
);
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
const amountNode = (
|
|
199
|
+
<Typography
|
|
200
|
+
sx={{
|
|
201
|
+
color: isGrant ? (isExpiredGrant ? 'text.disabled' : 'success.main') : 'error.main',
|
|
202
|
+
}}>
|
|
203
|
+
{isGrant ? '+' : '-'} {displayAmount}
|
|
204
|
+
</Typography>
|
|
205
|
+
);
|
|
206
|
+
|
|
197
207
|
return (
|
|
198
208
|
<Box onClick={(e) => handleTransactionClick(e, item)}>
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
<Stack direction="row" spacing={1} alignItems="center" justifyContent="flex-end" sx={{ width: '100%' }}>
|
|
210
|
+
{isExpiredGrant && (
|
|
211
|
+
<Chip
|
|
212
|
+
label={t('admin.creditGrants.status.expired')}
|
|
213
|
+
size="small"
|
|
214
|
+
variant="outlined"
|
|
215
|
+
sx={{
|
|
216
|
+
mr: 2,
|
|
217
|
+
height: 18,
|
|
218
|
+
fontSize: '12px',
|
|
219
|
+
color: 'text.disabled',
|
|
220
|
+
borderColor: 'text.disabled',
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
{amountNode}
|
|
225
|
+
</Stack>
|
|
205
226
|
</Box>
|
|
206
227
|
);
|
|
207
228
|
},
|
|
@@ -214,9 +235,15 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
214
235
|
customBodyRenderLite: (_: string, index: number) => {
|
|
215
236
|
const item = data?.list[index] as any;
|
|
216
237
|
const isGrant = item.activity_type === 'grant';
|
|
238
|
+
const isExpiredGrant = isGrant && item.status === 'expired';
|
|
217
239
|
|
|
218
240
|
const grantName = isGrant ? item.name : item.creditGrant.name;
|
|
219
241
|
const grantId = isGrant ? item.id : item.credit_grant_id;
|
|
242
|
+
const nameNode = (
|
|
243
|
+
<Typography variant="body2" sx={{ cursor: 'pointer', color: isExpiredGrant ? 'text.disabled' : undefined }}>
|
|
244
|
+
{grantName || `Grant ${grantId.slice(-6)}`}
|
|
245
|
+
</Typography>
|
|
246
|
+
);
|
|
220
247
|
return (
|
|
221
248
|
<Stack
|
|
222
249
|
direction="row"
|
|
@@ -228,9 +255,7 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
228
255
|
sx={{
|
|
229
256
|
alignItems: 'center',
|
|
230
257
|
}}>
|
|
231
|
-
|
|
232
|
-
{grantName || `Grant ${grantId.slice(-6)}`}
|
|
233
|
-
</Typography>
|
|
258
|
+
{nameNode}
|
|
234
259
|
</Stack>
|
|
235
260
|
);
|
|
236
261
|
},
|
|
@@ -243,17 +268,18 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
243
268
|
customBodyRenderLite: (_: string, index: number) => {
|
|
244
269
|
const item = data?.list[index] as any;
|
|
245
270
|
const isGrant = item.activity_type === 'grant';
|
|
271
|
+
const isExpiredGrant = isGrant && item.status === 'expired';
|
|
246
272
|
const description = isGrant
|
|
247
273
|
? item.name || item.description || 'Credit Granted'
|
|
248
274
|
: item.subscription?.description || item.description || `${item.meter_event_name} usage`;
|
|
249
275
|
|
|
250
|
-
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
</Typography>
|
|
255
|
-
</Box>
|
|
276
|
+
const descriptionNode = (
|
|
277
|
+
<Typography variant="body2" sx={{ fontWeight: 400, color: isExpiredGrant ? 'text.disabled' : undefined }}>
|
|
278
|
+
{description}
|
|
279
|
+
</Typography>
|
|
256
280
|
);
|
|
281
|
+
|
|
282
|
+
return <Box onClick={(e) => handleTransactionClick(e, item)}>{descriptionNode}</Box>;
|
|
257
283
|
},
|
|
258
284
|
},
|
|
259
285
|
},
|
package/src/locales/en.tsx
CHANGED
|
@@ -276,6 +276,10 @@ export default flat({
|
|
|
276
276
|
recurringEnoughWithExpiry:
|
|
277
277
|
'You have a usage overage of {amount}. This subscription adds {totalAmount} {period} (valid for {duration} {unit}). After covering the overage, your new available balance will be {availableAmount}.',
|
|
278
278
|
},
|
|
279
|
+
schedule: {
|
|
280
|
+
periodic: 'Grant {amount} every {interval}.',
|
|
281
|
+
withRefresh: 'Grant {amount} every {interval}; unused credits expire with the next grant.',
|
|
282
|
+
},
|
|
279
283
|
},
|
|
280
284
|
expired: {
|
|
281
285
|
title: 'Expired Link',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -309,6 +309,10 @@ export default flat({
|
|
|
309
309
|
recurringEnoughWithExpiry:
|
|
310
310
|
'您有 {amount} 的使用超额。本订阅{period}将增加 {totalAmount}(有效期 {duration} {unit})。扣除超额后,您的新可用余额将为 {availableAmount}。',
|
|
311
311
|
},
|
|
312
|
+
schedule: {
|
|
313
|
+
periodic: '每{interval}发放 {amount}。',
|
|
314
|
+
withRefresh: '每{interval}发放 {amount},未使用额度将在下次发放时过期。',
|
|
315
|
+
},
|
|
312
316
|
},
|
|
313
317
|
emptyItems: {
|
|
314
318
|
title: '没有任何购买项目',
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
formatUpsellSaving,
|
|
19
19
|
formatAmount,
|
|
20
20
|
formatCreditForCheckout,
|
|
21
|
+
formatCreditAmount,
|
|
21
22
|
formatBNStr,
|
|
22
23
|
} from '../libs/util';
|
|
23
24
|
import ProductCard from './product-card';
|
|
@@ -96,13 +97,21 @@ export default function ProductItem({
|
|
|
96
97
|
const metered = item.price?.recurring?.usage_type === 'metered' ? t('common.metered') : '';
|
|
97
98
|
const canUpsell = mode === 'normal' && items.length === 1;
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
// Check if this is a credit product - be more lenient in detection
|
|
101
|
+
const isCreditProduct = item.price.product?.type === 'credit' && item.price.metadata?.credit_config;
|
|
102
|
+
const creditAmount =
|
|
103
|
+
isCreditProduct && item.price.metadata?.credit_config?.credit_amount
|
|
104
|
+
? Number(item.price.metadata.credit_config.credit_amount)
|
|
105
|
+
: 0;
|
|
106
|
+
const creditCurrency =
|
|
107
|
+
isCreditProduct && item.price.metadata?.credit_config?.currency_id
|
|
108
|
+
? findCurrency(settings.paymentMethods, item.price.metadata.credit_config.currency_id)
|
|
109
|
+
: null;
|
|
104
110
|
const validDuration = item.price.metadata?.credit_config?.valid_duration_value;
|
|
105
111
|
const validDurationUnit = item.price.metadata?.credit_config?.valid_duration_unit || 'days';
|
|
112
|
+
const scheduleConfig = item.price.metadata?.credit_config?.schedule;
|
|
113
|
+
const hasSchedule =
|
|
114
|
+
scheduleConfig?.enabled && scheduleConfig?.delivery_mode && scheduleConfig.delivery_mode !== 'invoice';
|
|
106
115
|
|
|
107
116
|
const userDid = session?.user?.did;
|
|
108
117
|
|
|
@@ -318,6 +327,58 @@ export default function ProductItem({
|
|
|
318
327
|
return t(getLocaleKey('pending', type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
319
328
|
};
|
|
320
329
|
|
|
330
|
+
// Format credit schedule info for display
|
|
331
|
+
const formatScheduleInfo = () => {
|
|
332
|
+
if (!hasSchedule || !scheduleConfig) return null;
|
|
333
|
+
|
|
334
|
+
const totalCredit = creditAmount * (localQuantity || 0);
|
|
335
|
+
const currencySymbol = creditCurrency?.symbol || 'Credits';
|
|
336
|
+
const intervalUnit = scheduleConfig.interval_unit;
|
|
337
|
+
const intervalValue = scheduleConfig.interval_value;
|
|
338
|
+
|
|
339
|
+
// Calculate amount per grant
|
|
340
|
+
let amountPerGrant: number;
|
|
341
|
+
if (scheduleConfig.amount_per_grant) {
|
|
342
|
+
amountPerGrant = Number(scheduleConfig.amount_per_grant) * (localQuantity || 1);
|
|
343
|
+
} else {
|
|
344
|
+
// Divide total by period intervals
|
|
345
|
+
const billingIntervalUnit = item.price.recurring?.interval || 'month';
|
|
346
|
+
const billingIntervalCount = item.price.recurring?.interval_count || 1;
|
|
347
|
+
|
|
348
|
+
// Calculate how many schedule intervals fit in billing period
|
|
349
|
+
const unitToHours: Record<string, number> = {
|
|
350
|
+
hour: 1,
|
|
351
|
+
day: 24,
|
|
352
|
+
week: 168,
|
|
353
|
+
month: 720, // ~30 days
|
|
354
|
+
};
|
|
355
|
+
const billingHours = unitToHours[billingIntervalUnit] * billingIntervalCount;
|
|
356
|
+
const scheduleHours = unitToHours[intervalUnit] * intervalValue;
|
|
357
|
+
const grantsPerPeriod = Math.floor(billingHours / scheduleHours);
|
|
358
|
+
amountPerGrant = grantsPerPeriod > 0 ? totalCredit / grantsPerPeriod : totalCredit;
|
|
359
|
+
}
|
|
360
|
+
const formattedAmount = formatCreditAmount(formatNumber(amountPerGrant), currencySymbol);
|
|
361
|
+
|
|
362
|
+
const intervalDisplay =
|
|
363
|
+
intervalValue === 1
|
|
364
|
+
? t(`common.${intervalUnit}`)
|
|
365
|
+
: ` ${intervalValue} ${t(`common.${intervalUnit}s` as 'common.hours' | 'common.days' | 'common.weeks' | 'common.months')} `;
|
|
366
|
+
|
|
367
|
+
const expireWithNext = scheduleConfig.expire_with_next_grant;
|
|
368
|
+
|
|
369
|
+
if (expireWithNext) {
|
|
370
|
+
return t('payment.checkout.credit.schedule.withRefresh', {
|
|
371
|
+
amount: formattedAmount,
|
|
372
|
+
interval: intervalDisplay,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return t('payment.checkout.credit.schedule.periodic', {
|
|
377
|
+
amount: formattedAmount,
|
|
378
|
+
interval: intervalDisplay,
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
|
|
321
382
|
const primaryText = useMemo(() => {
|
|
322
383
|
const price = item.upsell_price || item.price || {};
|
|
323
384
|
const isRecurring = price?.type === 'recurring' && price?.recurring;
|
|
@@ -534,7 +595,15 @@ export default function ProductItem({
|
|
|
534
595
|
{/* Credit 信息展示 */}
|
|
535
596
|
{isCreditProduct && (
|
|
536
597
|
<Alert severity="info" sx={{ mt: 1, fontSize: '0.875rem' }} icon={false}>
|
|
537
|
-
{
|
|
598
|
+
{hasSchedule ? (
|
|
599
|
+
<Typography component="span" sx={{ fontSize: 'inherit' }}>
|
|
600
|
+
{formatScheduleInfo()}
|
|
601
|
+
</Typography>
|
|
602
|
+
) : (
|
|
603
|
+
<Typography component="span" sx={{ fontSize: 'inherit' }}>
|
|
604
|
+
{formatCreditInfo()}
|
|
605
|
+
</Typography>
|
|
606
|
+
)}
|
|
538
607
|
</Alert>
|
|
539
608
|
)}
|
|
540
609
|
|
package/src/payment/success.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { Box, Grow, Link, Paper, Stack, styled, Typography } from '@mui/material
|
|
|
5
5
|
import { useEffect, useRef, useState } from 'react';
|
|
6
6
|
import { joinURL } from 'ufo';
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import Button from '@arcblock/ux/lib/Button';
|
|
9
9
|
import { usePaymentContext } from '../contexts/payment';
|
|
10
10
|
import { VendorPlaceholder, VendorProgressItem } from './progress-item';
|
|
11
11
|
|