@blocklet/payment-react 1.23.10 → 1.24.0
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/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 +93 -18
- 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 +104 -18
- package/package.json +3 -3
- 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 +133 -24
|
@@ -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 () => {
|
|
@@ -95,42 +98,76 @@ export default function ProductItem({
|
|
|
95
98
|
refreshDeps: [isCreditProduct, userDid, creditCurrency?.id]
|
|
96
99
|
}
|
|
97
100
|
);
|
|
98
|
-
const
|
|
101
|
+
const canAdjustQuantity = adjustableQuantity.enabled && mode === "normal";
|
|
102
|
+
const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
|
|
103
|
+
const quantityAvailable = Math.min(item.price.quantity_limit_per_checkout, item.price.quantity_available);
|
|
104
|
+
const maxQuantity = quantityAvailable ? Math.min(adjustableQuantity.maximum || Infinity, quantityAvailable) : adjustableQuantity.maximum || Infinity;
|
|
105
|
+
const getMinQuantityForPending = useMemo(() => {
|
|
106
|
+
if (!isCreditProduct || !pendingAmount) return null;
|
|
107
|
+
const pendingAmountBN = new BN(pendingAmount || "0");
|
|
108
|
+
if (!pendingAmountBN.gt(new BN(0))) return null;
|
|
109
|
+
const creditAmountBN = fromTokenToUnit(creditAmount, creditCurrency?.decimal || 2);
|
|
110
|
+
return Math.ceil(pendingAmountBN.mul(new BN(100)).div(creditAmountBN).toNumber() / 100);
|
|
111
|
+
}, [isCreditProduct, pendingAmount, creditAmount, creditCurrency?.decimal]);
|
|
112
|
+
const initialQuantity = useMemo(() => {
|
|
99
113
|
const urlQuantity = getRecommendedQuantityFromUrl(item.price.id);
|
|
100
114
|
if (urlQuantity && urlQuantity > 0) {
|
|
115
|
+
if (canAdjustQuantity && getMinQuantityForPending) {
|
|
116
|
+
return Math.max(urlQuantity, getMinQuantityForPending, minQuantity);
|
|
117
|
+
}
|
|
101
118
|
return urlQuantity;
|
|
102
119
|
}
|
|
103
120
|
if (userDid) {
|
|
104
121
|
const preferredQuantity = getUserQuantityPreference(userDid, item.price.id);
|
|
105
122
|
if (preferredQuantity && preferredQuantity > 0) {
|
|
123
|
+
if (canAdjustQuantity && getMinQuantityForPending) {
|
|
124
|
+
return Math.max(preferredQuantity, getMinQuantityForPending, minQuantity);
|
|
125
|
+
}
|
|
106
126
|
return preferredQuantity;
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
129
|
+
let baseQuantity = item.quantity;
|
|
130
|
+
if (canAdjustQuantity && getMinQuantityForPending) {
|
|
131
|
+
baseQuantity = Math.max(baseQuantity, getMinQuantityForPending, minQuantity);
|
|
132
|
+
}
|
|
133
|
+
return baseQuantity;
|
|
134
|
+
}, [item.quantity, item.price.id, userDid, getMinQuantityForPending, canAdjustQuantity, minQuantity]);
|
|
135
|
+
const [localQuantity, setLocalQuantity] = useState(initialQuantity);
|
|
116
136
|
const localQuantityNum = localQuantity || 0;
|
|
117
137
|
useEffect(() => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
if (initialQuantity && initialQuantity > 0) {
|
|
139
|
+
if (initialQuantity !== localQuantity) {
|
|
140
|
+
setLocalQuantity(initialQuantity);
|
|
141
|
+
}
|
|
142
|
+
if (initialQuantity !== item.quantity) {
|
|
143
|
+
onQuantityChange(item.price_id, initialQuantity);
|
|
144
|
+
}
|
|
145
|
+
if (isCreditProduct && pendingAmount && getMinQuantityForPending) {
|
|
146
|
+
setPayable(initialQuantity >= getMinQuantityForPending);
|
|
147
|
+
} else {
|
|
148
|
+
setPayable(true);
|
|
149
|
+
}
|
|
121
150
|
}
|
|
122
|
-
}, []);
|
|
151
|
+
}, [initialQuantity, isCreditProduct, pendingAmount, getMinQuantityForPending]);
|
|
123
152
|
const handleQuantityChange = (newQuantity) => {
|
|
124
153
|
if (!newQuantity) {
|
|
125
154
|
setLocalQuantity(void 0);
|
|
126
155
|
setPayable(false);
|
|
127
156
|
return;
|
|
128
157
|
}
|
|
129
|
-
setPayable(true);
|
|
130
158
|
if (newQuantity >= minQuantity && newQuantity <= maxQuantity) {
|
|
131
159
|
if (formatQuantityInventory(item.price, newQuantity, locale)) {
|
|
132
160
|
return;
|
|
133
161
|
}
|
|
162
|
+
if (isCreditProduct && pendingAmount && getMinQuantityForPending) {
|
|
163
|
+
if (newQuantity < getMinQuantityForPending) {
|
|
164
|
+
setPayable(false);
|
|
165
|
+
setLocalQuantity(newQuantity);
|
|
166
|
+
onQuantityChange(item.price_id, newQuantity);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
setPayable(true);
|
|
134
171
|
setLocalQuantity(newQuantity);
|
|
135
172
|
onQuantityChange(item.price_id, newQuantity);
|
|
136
173
|
if (userDid && newQuantity > 0) {
|
|
@@ -204,7 +241,7 @@ export default function ProductItem({
|
|
|
204
241
|
}
|
|
205
242
|
const pendingAmountBN = new BN(pendingAmount || "0");
|
|
206
243
|
const creditAmountBN = fromTokenToUnit(creditAmount, creditCurrency?.decimal || 2);
|
|
207
|
-
const minQuantityNeeded =
|
|
244
|
+
const minQuantityNeeded = getMinQuantityForPending || 0;
|
|
208
245
|
const currentPurchaseCreditBN = creditAmountBN.mul(new BN(localQuantity || 0));
|
|
209
246
|
const actualAvailable = currentPurchaseCreditBN.sub(pendingAmountBN).toString();
|
|
210
247
|
if (!new BN(actualAvailable).gt(new BN(0))) {
|
|
@@ -220,6 +257,44 @@ export default function ProductItem({
|
|
|
220
257
|
const type = isRecurring ? "recurringEnough" : "oneTimeEnough";
|
|
221
258
|
return t(getLocaleKey("pending", type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
222
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
|
+
};
|
|
223
298
|
const primaryText = useMemo(() => {
|
|
224
299
|
const price = item.upsell_price || item.price || {};
|
|
225
300
|
const isRecurring = price?.type === "recurring" && price?.recurring;
|
|
@@ -461,7 +536,7 @@ export default function ProductItem({
|
|
|
461
536
|
]
|
|
462
537
|
}
|
|
463
538
|
) }),
|
|
464
|
-
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() }) }),
|
|
465
540
|
children
|
|
466
541
|
]
|
|
467
542
|
}
|
|
@@ -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
|
|
@@ -103,42 +105,76 @@ function ProductItem({
|
|
|
103
105
|
}, {
|
|
104
106
|
refreshDeps: [isCreditProduct, userDid, creditCurrency?.id]
|
|
105
107
|
});
|
|
106
|
-
const
|
|
108
|
+
const canAdjustQuantity = adjustableQuantity.enabled && mode === "normal";
|
|
109
|
+
const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
|
|
110
|
+
const quantityAvailable = Math.min(item.price.quantity_limit_per_checkout, item.price.quantity_available);
|
|
111
|
+
const maxQuantity = quantityAvailable ? Math.min(adjustableQuantity.maximum || Infinity, quantityAvailable) : adjustableQuantity.maximum || Infinity;
|
|
112
|
+
const getMinQuantityForPending = (0, _react.useMemo)(() => {
|
|
113
|
+
if (!isCreditProduct || !pendingAmount) return null;
|
|
114
|
+
const pendingAmountBN = new _util.BN(pendingAmount || "0");
|
|
115
|
+
if (!pendingAmountBN.gt(new _util.BN(0))) return null;
|
|
116
|
+
const creditAmountBN = (0, _util.fromTokenToUnit)(creditAmount, creditCurrency?.decimal || 2);
|
|
117
|
+
return Math.ceil(pendingAmountBN.mul(new _util.BN(100)).div(creditAmountBN).toNumber() / 100);
|
|
118
|
+
}, [isCreditProduct, pendingAmount, creditAmount, creditCurrency?.decimal]);
|
|
119
|
+
const initialQuantity = (0, _react.useMemo)(() => {
|
|
107
120
|
const urlQuantity = getRecommendedQuantityFromUrl(item.price.id);
|
|
108
121
|
if (urlQuantity && urlQuantity > 0) {
|
|
122
|
+
if (canAdjustQuantity && getMinQuantityForPending) {
|
|
123
|
+
return Math.max(urlQuantity, getMinQuantityForPending, minQuantity);
|
|
124
|
+
}
|
|
109
125
|
return urlQuantity;
|
|
110
126
|
}
|
|
111
127
|
if (userDid) {
|
|
112
128
|
const preferredQuantity = getUserQuantityPreference(userDid, item.price.id);
|
|
113
129
|
if (preferredQuantity && preferredQuantity > 0) {
|
|
130
|
+
if (canAdjustQuantity && getMinQuantityForPending) {
|
|
131
|
+
return Math.max(preferredQuantity, getMinQuantityForPending, minQuantity);
|
|
132
|
+
}
|
|
114
133
|
return preferredQuantity;
|
|
115
134
|
}
|
|
116
135
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
136
|
+
let baseQuantity = item.quantity;
|
|
137
|
+
if (canAdjustQuantity && getMinQuantityForPending) {
|
|
138
|
+
baseQuantity = Math.max(baseQuantity, getMinQuantityForPending, minQuantity);
|
|
139
|
+
}
|
|
140
|
+
return baseQuantity;
|
|
141
|
+
}, [item.quantity, item.price.id, userDid, getMinQuantityForPending, canAdjustQuantity, minQuantity]);
|
|
142
|
+
const [localQuantity, setLocalQuantity] = (0, _react.useState)(initialQuantity);
|
|
124
143
|
const localQuantityNum = localQuantity || 0;
|
|
125
144
|
(0, _react.useEffect)(() => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
if (initialQuantity && initialQuantity > 0) {
|
|
146
|
+
if (initialQuantity !== localQuantity) {
|
|
147
|
+
setLocalQuantity(initialQuantity);
|
|
148
|
+
}
|
|
149
|
+
if (initialQuantity !== item.quantity) {
|
|
150
|
+
onQuantityChange(item.price_id, initialQuantity);
|
|
151
|
+
}
|
|
152
|
+
if (isCreditProduct && pendingAmount && getMinQuantityForPending) {
|
|
153
|
+
setPayable(initialQuantity >= getMinQuantityForPending);
|
|
154
|
+
} else {
|
|
155
|
+
setPayable(true);
|
|
156
|
+
}
|
|
129
157
|
}
|
|
130
|
-
}, []);
|
|
158
|
+
}, [initialQuantity, isCreditProduct, pendingAmount, getMinQuantityForPending]);
|
|
131
159
|
const handleQuantityChange = newQuantity => {
|
|
132
160
|
if (!newQuantity) {
|
|
133
161
|
setLocalQuantity(void 0);
|
|
134
162
|
setPayable(false);
|
|
135
163
|
return;
|
|
136
164
|
}
|
|
137
|
-
setPayable(true);
|
|
138
165
|
if (newQuantity >= minQuantity && newQuantity <= maxQuantity) {
|
|
139
166
|
if ((0, _util2.formatQuantityInventory)(item.price, newQuantity, locale)) {
|
|
140
167
|
return;
|
|
141
168
|
}
|
|
169
|
+
if (isCreditProduct && pendingAmount && getMinQuantityForPending) {
|
|
170
|
+
if (newQuantity < getMinQuantityForPending) {
|
|
171
|
+
setPayable(false);
|
|
172
|
+
setLocalQuantity(newQuantity);
|
|
173
|
+
onQuantityChange(item.price_id, newQuantity);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
setPayable(true);
|
|
142
178
|
setLocalQuantity(newQuantity);
|
|
143
179
|
onQuantityChange(item.price_id, newQuantity);
|
|
144
180
|
if (userDid && newQuantity > 0) {
|
|
@@ -204,7 +240,7 @@ function ProductItem({
|
|
|
204
240
|
}
|
|
205
241
|
const pendingAmountBN = new _util.BN(pendingAmount || "0");
|
|
206
242
|
const creditAmountBN = (0, _util.fromTokenToUnit)(creditAmount, creditCurrency?.decimal || 2);
|
|
207
|
-
const minQuantityNeeded =
|
|
243
|
+
const minQuantityNeeded = getMinQuantityForPending || 0;
|
|
208
244
|
const currentPurchaseCreditBN = creditAmountBN.mul(new _util.BN(localQuantity || 0));
|
|
209
245
|
const actualAvailable = currentPurchaseCreditBN.sub(pendingAmountBN).toString();
|
|
210
246
|
if (!new _util.BN(actualAvailable).gt(new _util.BN(0))) {
|
|
@@ -216,6 +252,44 @@ function ProductItem({
|
|
|
216
252
|
const type = isRecurring ? "recurringEnough" : "oneTimeEnough";
|
|
217
253
|
return t(getLocaleKey("pending", type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
218
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
|
+
};
|
|
219
293
|
const primaryText = (0, _react.useMemo)(() => {
|
|
220
294
|
const price = item.upsell_price || item.price || {};
|
|
221
295
|
const isRecurring = price?.type === "recurring" && price?.recurring;
|
|
@@ -450,7 +524,19 @@ function ProductItem({
|
|
|
450
524
|
fontSize: "0.875rem"
|
|
451
525
|
},
|
|
452
526
|
icon: false,
|
|
453
|
-
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
|
+
})
|
|
454
540
|
}), children]
|
|
455
541
|
}), canUpsell && !item.upsell_price_id && item.price.upsell?.upsells_to && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
456
542
|
direction: "row",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/payment-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "Reusable react components for payment kit v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -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.0",
|
|
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": "f3fad0829a1377422d51f9b6445a3502d0e94719"
|
|
131
131
|
}
|
|
@@ -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
|
|
|
@@ -127,40 +136,69 @@ export default function ProductItem({
|
|
|
127
136
|
}
|
|
128
137
|
);
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
const
|
|
139
|
+
const canAdjustQuantity = adjustableQuantity.enabled && mode === 'normal';
|
|
140
|
+
const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
|
|
141
|
+
const quantityAvailable = Math.min(item.price.quantity_limit_per_checkout, item.price.quantity_available);
|
|
142
|
+
const maxQuantity = quantityAvailable
|
|
143
|
+
? Math.min(adjustableQuantity.maximum || Infinity, quantityAvailable)
|
|
144
|
+
: adjustableQuantity.maximum || Infinity;
|
|
145
|
+
|
|
146
|
+
const getMinQuantityForPending = useMemo(() => {
|
|
147
|
+
if (!isCreditProduct || !pendingAmount) return null;
|
|
148
|
+
const pendingAmountBN = new BN(pendingAmount || '0');
|
|
149
|
+
if (!pendingAmountBN.gt(new BN(0))) return null;
|
|
150
|
+
const creditAmountBN = fromTokenToUnit(creditAmount, creditCurrency?.decimal || 2);
|
|
151
|
+
return Math.ceil(pendingAmountBN.mul(new BN(100)).div(creditAmountBN).toNumber() / 100);
|
|
152
|
+
}, [isCreditProduct, pendingAmount, creditAmount, creditCurrency?.decimal]);
|
|
153
|
+
|
|
154
|
+
const initialQuantity = useMemo(() => {
|
|
132
155
|
const urlQuantity = getRecommendedQuantityFromUrl(item.price.id);
|
|
133
156
|
if (urlQuantity && urlQuantity > 0) {
|
|
157
|
+
if (canAdjustQuantity && getMinQuantityForPending) {
|
|
158
|
+
return Math.max(urlQuantity, getMinQuantityForPending, minQuantity);
|
|
159
|
+
}
|
|
134
160
|
return urlQuantity;
|
|
135
161
|
}
|
|
136
162
|
|
|
137
163
|
if (userDid) {
|
|
138
164
|
const preferredQuantity = getUserQuantityPreference(userDid, item.price.id);
|
|
139
165
|
if (preferredQuantity && preferredQuantity > 0) {
|
|
166
|
+
if (canAdjustQuantity && getMinQuantityForPending) {
|
|
167
|
+
return Math.max(preferredQuantity, getMinQuantityForPending, minQuantity);
|
|
168
|
+
}
|
|
140
169
|
return preferredQuantity;
|
|
141
170
|
}
|
|
142
171
|
}
|
|
143
172
|
|
|
144
|
-
|
|
145
|
-
};
|
|
173
|
+
let baseQuantity = item.quantity;
|
|
146
174
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
if (canAdjustQuantity && getMinQuantityForPending) {
|
|
176
|
+
baseQuantity = Math.max(baseQuantity, getMinQuantityForPending, minQuantity);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return baseQuantity;
|
|
180
|
+
}, [item.quantity, item.price.id, userDid, getMinQuantityForPending, canAdjustQuantity, minQuantity]);
|
|
181
|
+
|
|
182
|
+
const [localQuantity, setLocalQuantity] = useState<number | undefined>(initialQuantity);
|
|
154
183
|
const localQuantityNum = localQuantity || 0;
|
|
155
184
|
|
|
156
185
|
useEffect(() => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
186
|
+
if (initialQuantity && initialQuantity > 0) {
|
|
187
|
+
if (initialQuantity !== localQuantity) {
|
|
188
|
+
setLocalQuantity(initialQuantity);
|
|
189
|
+
}
|
|
190
|
+
if (initialQuantity !== item.quantity) {
|
|
191
|
+
onQuantityChange(item.price_id, initialQuantity);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (isCreditProduct && pendingAmount && getMinQuantityForPending) {
|
|
195
|
+
setPayable(initialQuantity >= getMinQuantityForPending);
|
|
196
|
+
} else {
|
|
197
|
+
setPayable(true);
|
|
198
|
+
}
|
|
161
199
|
}
|
|
162
200
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
-
}, []);
|
|
201
|
+
}, [initialQuantity, isCreditProduct, pendingAmount, getMinQuantityForPending]);
|
|
164
202
|
|
|
165
203
|
const handleQuantityChange = (newQuantity: number) => {
|
|
166
204
|
if (!newQuantity) {
|
|
@@ -168,11 +206,22 @@ export default function ProductItem({
|
|
|
168
206
|
setPayable(false);
|
|
169
207
|
return;
|
|
170
208
|
}
|
|
171
|
-
|
|
209
|
+
|
|
172
210
|
if (newQuantity >= minQuantity && newQuantity <= maxQuantity) {
|
|
173
211
|
if (formatQuantityInventory(item.price, newQuantity, locale)) {
|
|
174
212
|
return;
|
|
175
213
|
}
|
|
214
|
+
|
|
215
|
+
if (isCreditProduct && pendingAmount && getMinQuantityForPending) {
|
|
216
|
+
if (newQuantity < getMinQuantityForPending) {
|
|
217
|
+
setPayable(false);
|
|
218
|
+
setLocalQuantity(newQuantity);
|
|
219
|
+
onQuantityChange(item.price_id, newQuantity);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setPayable(true);
|
|
176
225
|
setLocalQuantity(newQuantity);
|
|
177
226
|
onQuantityChange(item.price_id, newQuantity);
|
|
178
227
|
|
|
@@ -259,7 +308,7 @@ export default function ProductItem({
|
|
|
259
308
|
|
|
260
309
|
const pendingAmountBN = new BN(pendingAmount || '0');
|
|
261
310
|
const creditAmountBN = fromTokenToUnit(creditAmount, creditCurrency?.decimal || 2);
|
|
262
|
-
const minQuantityNeeded =
|
|
311
|
+
const minQuantityNeeded = getMinQuantityForPending || 0;
|
|
263
312
|
const currentPurchaseCreditBN = creditAmountBN.mul(new BN(localQuantity || 0));
|
|
264
313
|
const actualAvailable = currentPurchaseCreditBN.sub(pendingAmountBN).toString();
|
|
265
314
|
|
|
@@ -278,6 +327,58 @@ export default function ProductItem({
|
|
|
278
327
|
return t(getLocaleKey('pending', type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
279
328
|
};
|
|
280
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
|
+
|
|
281
382
|
const primaryText = useMemo(() => {
|
|
282
383
|
const price = item.upsell_price || item.price || {};
|
|
283
384
|
const isRecurring = price?.type === 'recurring' && price?.recurring;
|
|
@@ -494,7 +595,15 @@ export default function ProductItem({
|
|
|
494
595
|
{/* Credit 信息展示 */}
|
|
495
596
|
{isCreditProduct && (
|
|
496
597
|
<Alert severity="info" sx={{ mt: 1, fontSize: '0.875rem' }} icon={false}>
|
|
497
|
-
{
|
|
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
|
+
)}
|
|
498
607
|
</Alert>
|
|
499
608
|
)}
|
|
500
609
|
|