@blocklet/payment-react 1.20.14 → 1.20.16
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/locales/en.js +13 -3
- package/es/locales/zh.js +13 -3
- package/es/payment/product-item.js +123 -20
- package/es/payment/success.js +3 -3
- package/lib/locales/en.js +13 -3
- package/lib/locales/zh.js +13 -3
- package/lib/payment/product-item.js +136 -32
- package/lib/payment/success.js +3 -3
- package/package.json +5 -5
- package/src/locales/en.tsx +18 -3
- package/src/locales/zh.tsx +17 -3
- package/src/payment/product-item.tsx +140 -19
- package/src/payment/success.tsx +3 -3
package/es/locales/en.js
CHANGED
|
@@ -220,9 +220,19 @@ export default flat({
|
|
|
220
220
|
remove: "Remove from order"
|
|
221
221
|
},
|
|
222
222
|
credit: {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
normal: {
|
|
224
|
+
oneTime: "You will receive {amount} {symbol} after payment.",
|
|
225
|
+
oneTimeWithExpiry: "You will receive {amount} {symbol} after payment, valid for {duration} {unit}.",
|
|
226
|
+
recurring: "You will receive {amount} {symbol} {period}.",
|
|
227
|
+
recurringWithExpiry: "You will receive {amount} {symbol} {period}, valid for {duration} {unit}."
|
|
228
|
+
},
|
|
229
|
+
pending: {
|
|
230
|
+
notEnough: "Your outstanding balance is {amount} {symbol}. To settle it, a minimum purchase of {quantity} units is required.",
|
|
231
|
+
oneTimeEnough: "Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol}, resulting in a net balance of {availableAmount} {symbol} after the deduction.",
|
|
232
|
+
oneTimeEnoughWithExpiry: "Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol} (valid for {duration} {unit}), resulting in a net balance of {availableAmount} {symbol} after the deduction.",
|
|
233
|
+
recurringEnough: "Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol} {period}, resulting in a net balance of {availableAmount} {symbol} after the deduction.",
|
|
234
|
+
recurringEnoughWithExpiry: "Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol} {period} (valid for {duration} {unit}), resulting in a net balance of {availableAmount} {symbol} after the deduction."
|
|
235
|
+
}
|
|
226
236
|
},
|
|
227
237
|
expired: {
|
|
228
238
|
title: "Expired Link",
|
package/es/locales/zh.js
CHANGED
|
@@ -257,9 +257,19 @@ export default flat({
|
|
|
257
257
|
}
|
|
258
258
|
},
|
|
259
259
|
credit: {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
normal: {
|
|
261
|
+
oneTime: "\u4ED8\u6B3E\u5B8C\u6210\u540E\u60A8\u5C06\u83B7\u5F97 {amount} {symbol} \u989D\u5EA6\u3002",
|
|
262
|
+
oneTimeWithExpiry: "\u4ED8\u6B3E\u5B8C\u6210\u540E\u60A8\u5C06\u83B7\u5F97 {amount} {symbol} \u989D\u5EA6\uFF0C\u6709\u6548\u671F {duration} {unit}\u3002",
|
|
263
|
+
recurring: "\u4ED8\u6B3E\u5B8C\u6210\u540E\uFF0C{period}\u60A8\u5C06\u83B7\u5F97 {amount} {symbol} \u989D\u5EA6\u3002",
|
|
264
|
+
recurringWithExpiry: "\u4ED8\u6B3E\u5B8C\u6210\u540E\uFF0C{period}\u60A8\u5C06\u83B7\u5F97 {amount} {symbol} \u989D\u5EA6\uFF0C\u6709\u6548\u671F {duration} {unit}\u3002"
|
|
265
|
+
},
|
|
266
|
+
pending: {
|
|
267
|
+
notEnough: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u81F3\u5C11\u9700\u8981\u8D2D\u4E70 {quantity} \u6570\u91CF\u3002",
|
|
268
|
+
oneTimeEnough: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u4ED8\u6B3E\u540E\u5C06\u83B7\u5F97 {totalAmount} {symbol} \u989D\u5EA6\uFF0C\u6263\u9664\u6B20\u8D39\u540E\u51C0\u4F59\u989D\u4E3A {availableAmount} {symbol}\u3002",
|
|
269
|
+
oneTimeEnoughWithExpiry: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u4ED8\u6B3E\u540E\u5C06\u83B7\u5F97 {totalAmount} {symbol} \u989D\u5EA6\uFF08\u6709\u6548\u671F {duration} {unit}\uFF09\uFF0C\u6263\u9664\u6B20\u8D39\u540E\u51C0\u4F59\u989D\u4E3A {availableAmount} {symbol}\u3002",
|
|
270
|
+
recurringEnough: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u4ED8\u6B3E\u540E{period}\u5C06\u83B7\u5F97 {totalAmount} {symbol} \u989D\u5EA6\uFF0C\u6263\u9664\u6B20\u8D39\u540E\u51C0\u4F59\u989D\u4E3A {availableAmount} {symbol}\u3002",
|
|
271
|
+
recurringEnoughWithExpiry: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u4ED8\u6B3E\u540E{period}\u5C06\u83B7\u5F97 {totalAmount} {symbol} \u989D\u5EA6\uFF08\u6709\u6548\u671F {duration} {unit}\uFF09\uFF0C\u6263\u9664\u6B20\u8D39\u540E\u51C0\u4F59\u989D\u4E3A {availableAmount} {symbol}\u3002"
|
|
272
|
+
}
|
|
263
273
|
},
|
|
264
274
|
emptyItems: {
|
|
265
275
|
title: "\u6CA1\u6709\u4EFB\u4F55\u8D2D\u4E70\u9879\u76EE",
|
|
@@ -2,7 +2,9 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
|
|
3
3
|
import { Box, Stack, Typography, IconButton, TextField, Alert, Chip } from "@mui/material";
|
|
4
4
|
import { Add, Remove, LocalOffer } from "@mui/icons-material";
|
|
5
|
-
import { useMemo, useState } from "react";
|
|
5
|
+
import { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { useRequest } from "ahooks";
|
|
7
|
+
import { BN, fromTokenToUnit } from "@ocap/util";
|
|
6
8
|
import Status from "../components/status.js";
|
|
7
9
|
import Switch from "../components/switch-button.js";
|
|
8
10
|
import {
|
|
@@ -13,11 +15,37 @@ import {
|
|
|
13
15
|
formatQuantityInventory,
|
|
14
16
|
formatRecurring,
|
|
15
17
|
formatUpsellSaving,
|
|
16
|
-
formatAmount
|
|
18
|
+
formatAmount,
|
|
19
|
+
formatBNStr
|
|
17
20
|
} from "../libs/util.js";
|
|
18
21
|
import ProductCard from "./product-card.js";
|
|
19
22
|
import dayjs from "../libs/dayjs.js";
|
|
20
23
|
import { usePaymentContext } from "../contexts/payment.js";
|
|
24
|
+
const getRecommendedQuantityFromUrl = (priceId) => {
|
|
25
|
+
try {
|
|
26
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
27
|
+
const recommendedQuantity = urlParams.get(`qty_${priceId}`) || urlParams.get("qty");
|
|
28
|
+
return recommendedQuantity ? Math.max(1, parseInt(recommendedQuantity, 10)) : void 0;
|
|
29
|
+
} catch {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const getUserQuantityPreference = (userDid, priceId) => {
|
|
34
|
+
try {
|
|
35
|
+
const key = `quantity_preference_${userDid}_${priceId}`;
|
|
36
|
+
const stored = localStorage.getItem(key);
|
|
37
|
+
return stored ? Math.max(1, parseInt(stored, 10)) : void 0;
|
|
38
|
+
} catch {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const saveUserQuantityPreference = (userDid, priceId, quantity) => {
|
|
43
|
+
try {
|
|
44
|
+
const key = `quantity_preference_${userDid}_${priceId}`;
|
|
45
|
+
localStorage.setItem(key, quantity.toString());
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
};
|
|
21
49
|
export default function ProductItem({
|
|
22
50
|
item,
|
|
23
51
|
items,
|
|
@@ -35,7 +63,7 @@ export default function ProductItem({
|
|
|
35
63
|
showFeatures = false
|
|
36
64
|
}) {
|
|
37
65
|
const { t, locale } = useLocaleContext();
|
|
38
|
-
const { settings, setPayable } = usePaymentContext();
|
|
66
|
+
const { settings, setPayable, session, api } = usePaymentContext();
|
|
39
67
|
const pricing = formatLineItemPricing(item, currency, { trialEnd, trialInDays }, locale);
|
|
40
68
|
const saving = formatUpsellSaving(items, currency);
|
|
41
69
|
const metered = item.price?.recurring?.usage_type === "metered" ? t("common.metered") : "";
|
|
@@ -45,12 +73,52 @@ export default function ProductItem({
|
|
|
45
73
|
const creditCurrency = isCreditProduct ? findCurrency(settings.paymentMethods, item.price.metadata?.credit_config?.currency_id ?? "") : null;
|
|
46
74
|
const validDuration = item.price.metadata?.credit_config?.valid_duration_value;
|
|
47
75
|
const validDurationUnit = item.price.metadata?.credit_config?.valid_duration_unit || "days";
|
|
48
|
-
const
|
|
76
|
+
const userDid = session?.user?.did;
|
|
77
|
+
const { data: pendingAmount } = useRequest(
|
|
78
|
+
async () => {
|
|
79
|
+
if (!isCreditProduct || !userDid) return null;
|
|
80
|
+
try {
|
|
81
|
+
const { data } = await api.get("/api/meter-events/pending-amount", {
|
|
82
|
+
params: {
|
|
83
|
+
customer_id: userDid,
|
|
84
|
+
currency_id: creditCurrency?.id
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return data?.[creditCurrency?.id || ""];
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn("Failed to fetch pending amount:", error);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
refreshDeps: [isCreditProduct, userDid, creditCurrency?.id]
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
const getInitialQuantity = () => {
|
|
98
|
+
const urlQuantity = getRecommendedQuantityFromUrl(item.price.id);
|
|
99
|
+
if (urlQuantity && urlQuantity > 0) {
|
|
100
|
+
return urlQuantity;
|
|
101
|
+
}
|
|
102
|
+
if (userDid) {
|
|
103
|
+
const preferredQuantity = getUserQuantityPreference(userDid, item.price.id);
|
|
104
|
+
if (preferredQuantity && preferredQuantity > 0) {
|
|
105
|
+
return preferredQuantity;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return item.quantity;
|
|
109
|
+
};
|
|
110
|
+
const [localQuantity, setLocalQuantity] = useState(getInitialQuantity());
|
|
49
111
|
const canAdjustQuantity = adjustableQuantity.enabled && mode === "normal";
|
|
50
112
|
const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
|
|
51
113
|
const quantityAvailable = Math.min(item.price.quantity_limit_per_checkout, item.price.quantity_available);
|
|
52
114
|
const maxQuantity = quantityAvailable ? Math.min(adjustableQuantity.maximum || Infinity, quantityAvailable) : adjustableQuantity.maximum || Infinity;
|
|
53
115
|
const localQuantityNum = localQuantity || 0;
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const initialQuantity = getInitialQuantity();
|
|
118
|
+
if (initialQuantity !== item.quantity && initialQuantity && initialQuantity > 1) {
|
|
119
|
+
onQuantityChange(item.price_id, initialQuantity);
|
|
120
|
+
}
|
|
121
|
+
}, []);
|
|
54
122
|
const handleQuantityChange = (newQuantity) => {
|
|
55
123
|
if (!newQuantity) {
|
|
56
124
|
setLocalQuantity(void 0);
|
|
@@ -64,6 +132,9 @@ export default function ProductItem({
|
|
|
64
132
|
}
|
|
65
133
|
setLocalQuantity(newQuantity);
|
|
66
134
|
onQuantityChange(item.price_id, newQuantity);
|
|
135
|
+
if (userDid && newQuantity > 0) {
|
|
136
|
+
saveUserQuantityPreference(userDid, item.price.id, newQuantity);
|
|
137
|
+
}
|
|
67
138
|
}
|
|
68
139
|
};
|
|
69
140
|
const handleQuantityIncrease = () => {
|
|
@@ -84,27 +155,59 @@ export default function ProductItem({
|
|
|
84
155
|
};
|
|
85
156
|
const formatCreditInfo = () => {
|
|
86
157
|
if (!isCreditProduct) return null;
|
|
158
|
+
const totalCreditStr = formatNumber(creditAmount * (localQuantity || 0));
|
|
159
|
+
const currencySymbol = creditCurrency?.symbol || "Credits";
|
|
160
|
+
const hasPendingAmount = pendingAmount && new BN(pendingAmount || "0").gt(new BN(0));
|
|
87
161
|
const isRecurring = item.price.type === "recurring";
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
162
|
+
const hasExpiry = validDuration && validDuration > 0;
|
|
163
|
+
const buildBaseParams = () => ({
|
|
164
|
+
amount: totalCreditStr,
|
|
165
|
+
symbol: currencySymbol,
|
|
166
|
+
...hasExpiry && {
|
|
167
|
+
duration: validDuration,
|
|
168
|
+
unit: t(`common.${validDurationUnit}`)
|
|
169
|
+
},
|
|
170
|
+
...isRecurring && {
|
|
93
171
|
period: formatRecurring(item.price.recurring, true, "per", locale)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
const buildPendingParams = (pendingBN, availableAmount) => ({
|
|
175
|
+
amount: formatBNStr(pendingBN.toString(), creditCurrency?.decimal || 2),
|
|
176
|
+
symbol: currencySymbol,
|
|
177
|
+
totalAmount: totalCreditStr,
|
|
178
|
+
...availableAmount && {
|
|
179
|
+
availableAmount: formatBNStr(availableAmount, creditCurrency?.decimal || 2)
|
|
180
|
+
},
|
|
181
|
+
...hasExpiry && {
|
|
103
182
|
duration: validDuration,
|
|
104
183
|
unit: t(`common.${validDurationUnit}`)
|
|
105
|
-
}
|
|
184
|
+
},
|
|
185
|
+
...isRecurring && {
|
|
186
|
+
period: formatRecurring(item.price.recurring, true, "per", locale)
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const getLocaleKey = (category, type2) => {
|
|
190
|
+
const suffix = hasExpiry ? "WithExpiry" : "";
|
|
191
|
+
return `payment.checkout.credit.${category}.${type2}${suffix}`;
|
|
192
|
+
};
|
|
193
|
+
if (!hasPendingAmount) {
|
|
194
|
+
const type2 = isRecurring ? "recurring" : "oneTime";
|
|
195
|
+
return t(getLocaleKey("normal", type2), buildBaseParams());
|
|
196
|
+
}
|
|
197
|
+
const pendingAmountBN = new BN(pendingAmount || "0");
|
|
198
|
+
const creditAmountBN = fromTokenToUnit(new BN(creditAmount), creditCurrency?.decimal || 2);
|
|
199
|
+
const minQuantityNeeded = Math.ceil(pendingAmountBN.mul(new BN(100)).div(creditAmountBN).toNumber() / 100);
|
|
200
|
+
const currentPurchaseCreditBN = creditAmountBN.mul(new BN(localQuantity || 0));
|
|
201
|
+
const actualAvailable = currentPurchaseCreditBN.sub(pendingAmountBN).toString();
|
|
202
|
+
if (!new BN(actualAvailable).gt(new BN(0))) {
|
|
203
|
+
return t("payment.checkout.credit.pending.notEnough", {
|
|
204
|
+
amount: formatBNStr(pendingAmountBN.toString(), creditCurrency?.decimal || 2),
|
|
205
|
+
symbol: currencySymbol,
|
|
206
|
+
quantity: formatNumber(minQuantityNeeded)
|
|
207
|
+
});
|
|
106
208
|
}
|
|
107
|
-
|
|
209
|
+
const type = isRecurring ? "recurringEnough" : "oneTimeEnough";
|
|
210
|
+
return t(getLocaleKey("pending", type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
108
211
|
};
|
|
109
212
|
const primaryText = useMemo(() => {
|
|
110
213
|
const price = item.upsell_price || item.price || {};
|
package/es/payment/success.js
CHANGED
|
@@ -33,7 +33,7 @@ export default function PaymentSuccess({
|
|
|
33
33
|
const needCheckError = Date.now() - timerRef.current > 6 * 1e3;
|
|
34
34
|
const allCompleted = response.data?.vendors?.every((vendor) => vendor.progress >= 100);
|
|
35
35
|
const hasAnyFailed = response.data?.vendors?.some(
|
|
36
|
-
(vendor) => vendor.status === "failed" || needCheckError && !!vendor.error
|
|
36
|
+
(vendor) => vendor.status === "failed" || needCheckError && !!vendor.error && !!vendor.error_message
|
|
37
37
|
);
|
|
38
38
|
if (hasAnyFailed || allCompleted) {
|
|
39
39
|
clearInterval(interval2);
|
|
@@ -228,7 +228,7 @@ const Div = styled("div")`
|
|
|
228
228
|
content: '';
|
|
229
229
|
height: 100px;
|
|
230
230
|
position: absolute;
|
|
231
|
-
background: ${(props) => props.theme.palette.background.
|
|
231
|
+
background: ${(props) => props.theme.palette.background.default};
|
|
232
232
|
transform: rotate(-45deg);
|
|
233
233
|
}
|
|
234
234
|
.check-icon .icon-line {
|
|
@@ -272,7 +272,7 @@ const Div = styled("div")`
|
|
|
272
272
|
height: 85px;
|
|
273
273
|
position: absolute;
|
|
274
274
|
transform: rotate(-45deg);
|
|
275
|
-
background-color: ${(props) => props.theme.palette.background.
|
|
275
|
+
background-color: ${(props) => props.theme.palette.background.default};
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
@keyframes rotate-circle {
|
package/lib/locales/en.js
CHANGED
|
@@ -227,9 +227,19 @@ module.exports = (0, _flat.default)({
|
|
|
227
227
|
remove: "Remove from order"
|
|
228
228
|
},
|
|
229
229
|
credit: {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
normal: {
|
|
231
|
+
oneTime: "You will receive {amount} {symbol} after payment.",
|
|
232
|
+
oneTimeWithExpiry: "You will receive {amount} {symbol} after payment, valid for {duration} {unit}.",
|
|
233
|
+
recurring: "You will receive {amount} {symbol} {period}.",
|
|
234
|
+
recurringWithExpiry: "You will receive {amount} {symbol} {period}, valid for {duration} {unit}."
|
|
235
|
+
},
|
|
236
|
+
pending: {
|
|
237
|
+
notEnough: "Your outstanding balance is {amount} {symbol}. To settle it, a minimum purchase of {quantity} units is required.",
|
|
238
|
+
oneTimeEnough: "Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol}, resulting in a net balance of {availableAmount} {symbol} after the deduction.",
|
|
239
|
+
oneTimeEnoughWithExpiry: "Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol} (valid for {duration} {unit}), resulting in a net balance of {availableAmount} {symbol} after the deduction.",
|
|
240
|
+
recurringEnough: "Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol} {period}, resulting in a net balance of {availableAmount} {symbol} after the deduction.",
|
|
241
|
+
recurringEnoughWithExpiry: "Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol} {period} (valid for {duration} {unit}), resulting in a net balance of {availableAmount} {symbol} after the deduction."
|
|
242
|
+
}
|
|
233
243
|
},
|
|
234
244
|
expired: {
|
|
235
245
|
title: "Expired Link",
|
package/lib/locales/zh.js
CHANGED
|
@@ -264,9 +264,19 @@ module.exports = (0, _flat.default)({
|
|
|
264
264
|
}
|
|
265
265
|
},
|
|
266
266
|
credit: {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
normal: {
|
|
268
|
+
oneTime: "\u4ED8\u6B3E\u5B8C\u6210\u540E\u60A8\u5C06\u83B7\u5F97 {amount} {symbol} \u989D\u5EA6\u3002",
|
|
269
|
+
oneTimeWithExpiry: "\u4ED8\u6B3E\u5B8C\u6210\u540E\u60A8\u5C06\u83B7\u5F97 {amount} {symbol} \u989D\u5EA6\uFF0C\u6709\u6548\u671F {duration} {unit}\u3002",
|
|
270
|
+
recurring: "\u4ED8\u6B3E\u5B8C\u6210\u540E\uFF0C{period}\u60A8\u5C06\u83B7\u5F97 {amount} {symbol} \u989D\u5EA6\u3002",
|
|
271
|
+
recurringWithExpiry: "\u4ED8\u6B3E\u5B8C\u6210\u540E\uFF0C{period}\u60A8\u5C06\u83B7\u5F97 {amount} {symbol} \u989D\u5EA6\uFF0C\u6709\u6548\u671F {duration} {unit}\u3002"
|
|
272
|
+
},
|
|
273
|
+
pending: {
|
|
274
|
+
notEnough: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u81F3\u5C11\u9700\u8981\u8D2D\u4E70 {quantity} \u6570\u91CF\u3002",
|
|
275
|
+
oneTimeEnough: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u4ED8\u6B3E\u540E\u5C06\u83B7\u5F97 {totalAmount} {symbol} \u989D\u5EA6\uFF0C\u6263\u9664\u6B20\u8D39\u540E\u51C0\u4F59\u989D\u4E3A {availableAmount} {symbol}\u3002",
|
|
276
|
+
oneTimeEnoughWithExpiry: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u4ED8\u6B3E\u540E\u5C06\u83B7\u5F97 {totalAmount} {symbol} \u989D\u5EA6\uFF08\u6709\u6548\u671F {duration} {unit}\uFF09\uFF0C\u6263\u9664\u6B20\u8D39\u540E\u51C0\u4F59\u989D\u4E3A {availableAmount} {symbol}\u3002",
|
|
277
|
+
recurringEnough: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u4ED8\u6B3E\u540E{period}\u5C06\u83B7\u5F97 {totalAmount} {symbol} \u989D\u5EA6\uFF0C\u6263\u9664\u6B20\u8D39\u540E\u51C0\u4F59\u989D\u4E3A {availableAmount} {symbol}\u3002",
|
|
278
|
+
recurringEnoughWithExpiry: "\u60A8\u5F53\u524D\u6B20\u8D39 {amount} {symbol}\uFF0C\u4ED8\u6B3E\u540E{period}\u5C06\u83B7\u5F97 {totalAmount} {symbol} \u989D\u5EA6\uFF08\u6709\u6548\u671F {duration} {unit}\uFF09\uFF0C\u6263\u9664\u6B20\u8D39\u540E\u51C0\u4F59\u989D\u4E3A {availableAmount} {symbol}\u3002"
|
|
279
|
+
}
|
|
270
280
|
},
|
|
271
281
|
emptyItems: {
|
|
272
282
|
title: "\u6CA1\u6709\u4EFB\u4F55\u8D2D\u4E70\u9879\u76EE",
|
|
@@ -9,13 +9,39 @@ var _context = require("@arcblock/ux/lib/Locale/context");
|
|
|
9
9
|
var _material = require("@mui/material");
|
|
10
10
|
var _iconsMaterial = require("@mui/icons-material");
|
|
11
11
|
var _react = require("react");
|
|
12
|
+
var _ahooks = require("ahooks");
|
|
13
|
+
var _util = require("@ocap/util");
|
|
12
14
|
var _status = _interopRequireDefault(require("../components/status"));
|
|
13
15
|
var _switchButton = _interopRequireDefault(require("../components/switch-button"));
|
|
14
|
-
var
|
|
16
|
+
var _util2 = require("../libs/util");
|
|
15
17
|
var _productCard = _interopRequireDefault(require("./product-card"));
|
|
16
18
|
var _dayjs = _interopRequireDefault(require("../libs/dayjs"));
|
|
17
19
|
var _payment = require("../contexts/payment");
|
|
18
20
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
21
|
+
const getRecommendedQuantityFromUrl = priceId => {
|
|
22
|
+
try {
|
|
23
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
24
|
+
const recommendedQuantity = urlParams.get(`qty_${priceId}`) || urlParams.get("qty");
|
|
25
|
+
return recommendedQuantity ? Math.max(1, parseInt(recommendedQuantity, 10)) : void 0;
|
|
26
|
+
} catch {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const getUserQuantityPreference = (userDid, priceId) => {
|
|
31
|
+
try {
|
|
32
|
+
const key = `quantity_preference_${userDid}_${priceId}`;
|
|
33
|
+
const stored = localStorage.getItem(key);
|
|
34
|
+
return stored ? Math.max(1, parseInt(stored, 10)) : void 0;
|
|
35
|
+
} catch {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const saveUserQuantityPreference = (userDid, priceId, quantity) => {
|
|
40
|
+
try {
|
|
41
|
+
const key = `quantity_preference_${userDid}_${priceId}`;
|
|
42
|
+
localStorage.setItem(key, quantity.toString());
|
|
43
|
+
} catch {}
|
|
44
|
+
};
|
|
19
45
|
function ProductItem({
|
|
20
46
|
item,
|
|
21
47
|
items,
|
|
@@ -39,26 +65,69 @@ function ProductItem({
|
|
|
39
65
|
} = (0, _context.useLocaleContext)();
|
|
40
66
|
const {
|
|
41
67
|
settings,
|
|
42
|
-
setPayable
|
|
68
|
+
setPayable,
|
|
69
|
+
session,
|
|
70
|
+
api
|
|
43
71
|
} = (0, _payment.usePaymentContext)();
|
|
44
|
-
const pricing = (0,
|
|
72
|
+
const pricing = (0, _util2.formatLineItemPricing)(item, currency, {
|
|
45
73
|
trialEnd,
|
|
46
74
|
trialInDays
|
|
47
75
|
}, locale);
|
|
48
|
-
const saving = (0,
|
|
76
|
+
const saving = (0, _util2.formatUpsellSaving)(items, currency);
|
|
49
77
|
const metered = item.price?.recurring?.usage_type === "metered" ? t("common.metered") : "";
|
|
50
78
|
const canUpsell = mode === "normal" && items.length === 1;
|
|
51
79
|
const isCreditProduct = item.price.product?.type === "credit" && item.price.metadata?.credit_config?.credit_amount;
|
|
52
80
|
const creditAmount = isCreditProduct ? Number(item.price.metadata.credit_config.credit_amount) : 0;
|
|
53
|
-
const creditCurrency = isCreditProduct ? (0,
|
|
81
|
+
const creditCurrency = isCreditProduct ? (0, _util2.findCurrency)(settings.paymentMethods, item.price.metadata?.credit_config?.currency_id ?? "") : null;
|
|
54
82
|
const validDuration = item.price.metadata?.credit_config?.valid_duration_value;
|
|
55
83
|
const validDurationUnit = item.price.metadata?.credit_config?.valid_duration_unit || "days";
|
|
56
|
-
const
|
|
84
|
+
const userDid = session?.user?.did;
|
|
85
|
+
const {
|
|
86
|
+
data: pendingAmount
|
|
87
|
+
} = (0, _ahooks.useRequest)(async () => {
|
|
88
|
+
if (!isCreditProduct || !userDid) return null;
|
|
89
|
+
try {
|
|
90
|
+
const {
|
|
91
|
+
data
|
|
92
|
+
} = await api.get("/api/meter-events/pending-amount", {
|
|
93
|
+
params: {
|
|
94
|
+
customer_id: userDid,
|
|
95
|
+
currency_id: creditCurrency?.id
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return data?.[creditCurrency?.id || ""];
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.warn("Failed to fetch pending amount:", error);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}, {
|
|
104
|
+
refreshDeps: [isCreditProduct, userDid, creditCurrency?.id]
|
|
105
|
+
});
|
|
106
|
+
const getInitialQuantity = () => {
|
|
107
|
+
const urlQuantity = getRecommendedQuantityFromUrl(item.price.id);
|
|
108
|
+
if (urlQuantity && urlQuantity > 0) {
|
|
109
|
+
return urlQuantity;
|
|
110
|
+
}
|
|
111
|
+
if (userDid) {
|
|
112
|
+
const preferredQuantity = getUserQuantityPreference(userDid, item.price.id);
|
|
113
|
+
if (preferredQuantity && preferredQuantity > 0) {
|
|
114
|
+
return preferredQuantity;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return item.quantity;
|
|
118
|
+
};
|
|
119
|
+
const [localQuantity, setLocalQuantity] = (0, _react.useState)(getInitialQuantity());
|
|
57
120
|
const canAdjustQuantity = adjustableQuantity.enabled && mode === "normal";
|
|
58
121
|
const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
|
|
59
122
|
const quantityAvailable = Math.min(item.price.quantity_limit_per_checkout, item.price.quantity_available);
|
|
60
123
|
const maxQuantity = quantityAvailable ? Math.min(adjustableQuantity.maximum || Infinity, quantityAvailable) : adjustableQuantity.maximum || Infinity;
|
|
61
124
|
const localQuantityNum = localQuantity || 0;
|
|
125
|
+
(0, _react.useEffect)(() => {
|
|
126
|
+
const initialQuantity = getInitialQuantity();
|
|
127
|
+
if (initialQuantity !== item.quantity && initialQuantity && initialQuantity > 1) {
|
|
128
|
+
onQuantityChange(item.price_id, initialQuantity);
|
|
129
|
+
}
|
|
130
|
+
}, []);
|
|
62
131
|
const handleQuantityChange = newQuantity => {
|
|
63
132
|
if (!newQuantity) {
|
|
64
133
|
setLocalQuantity(void 0);
|
|
@@ -67,11 +136,14 @@ function ProductItem({
|
|
|
67
136
|
}
|
|
68
137
|
setPayable(true);
|
|
69
138
|
if (newQuantity >= minQuantity && newQuantity <= maxQuantity) {
|
|
70
|
-
if ((0,
|
|
139
|
+
if ((0, _util2.formatQuantityInventory)(item.price, newQuantity, locale)) {
|
|
71
140
|
return;
|
|
72
141
|
}
|
|
73
142
|
setLocalQuantity(newQuantity);
|
|
74
143
|
onQuantityChange(item.price_id, newQuantity);
|
|
144
|
+
if (userDid && newQuantity > 0) {
|
|
145
|
+
saveUserQuantityPreference(userDid, item.price.id, newQuantity);
|
|
146
|
+
}
|
|
75
147
|
}
|
|
76
148
|
};
|
|
77
149
|
const handleQuantityIncrease = () => {
|
|
@@ -92,38 +164,70 @@ function ProductItem({
|
|
|
92
164
|
};
|
|
93
165
|
const formatCreditInfo = () => {
|
|
94
166
|
if (!isCreditProduct) return null;
|
|
167
|
+
const totalCreditStr = (0, _util2.formatNumber)(creditAmount * (localQuantity || 0));
|
|
168
|
+
const currencySymbol = creditCurrency?.symbol || "Credits";
|
|
169
|
+
const hasPendingAmount = pendingAmount && new _util.BN(pendingAmount || "0").gt(new _util.BN(0));
|
|
95
170
|
const isRecurring = item.price.type === "recurring";
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
period: (0, _util.formatRecurring)(item.price.recurring, true, "per", locale)
|
|
102
|
-
});
|
|
103
|
-
} else {
|
|
104
|
-
message = t("payment.checkout.credit.oneTimeInfo", {
|
|
105
|
-
amount: totalCredit,
|
|
106
|
-
symbol: creditCurrency?.symbol || "Credits"
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
if (validDuration && validDuration > 0) {
|
|
110
|
-
message += `\uFF0C${t("payment.checkout.credit.expiresIn", {
|
|
171
|
+
const hasExpiry = validDuration && validDuration > 0;
|
|
172
|
+
const buildBaseParams = () => ({
|
|
173
|
+
amount: totalCreditStr,
|
|
174
|
+
symbol: currencySymbol,
|
|
175
|
+
...(hasExpiry && {
|
|
111
176
|
duration: validDuration,
|
|
112
177
|
unit: t(`common.${validDurationUnit}`)
|
|
113
|
-
})
|
|
178
|
+
}),
|
|
179
|
+
...(isRecurring && {
|
|
180
|
+
period: (0, _util2.formatRecurring)(item.price.recurring, true, "per", locale)
|
|
181
|
+
})
|
|
182
|
+
});
|
|
183
|
+
const buildPendingParams = (pendingBN, availableAmount) => ({
|
|
184
|
+
amount: (0, _util2.formatBNStr)(pendingBN.toString(), creditCurrency?.decimal || 2),
|
|
185
|
+
symbol: currencySymbol,
|
|
186
|
+
totalAmount: totalCreditStr,
|
|
187
|
+
...(availableAmount && {
|
|
188
|
+
availableAmount: (0, _util2.formatBNStr)(availableAmount, creditCurrency?.decimal || 2)
|
|
189
|
+
}),
|
|
190
|
+
...(hasExpiry && {
|
|
191
|
+
duration: validDuration,
|
|
192
|
+
unit: t(`common.${validDurationUnit}`)
|
|
193
|
+
}),
|
|
194
|
+
...(isRecurring && {
|
|
195
|
+
period: (0, _util2.formatRecurring)(item.price.recurring, true, "per", locale)
|
|
196
|
+
})
|
|
197
|
+
});
|
|
198
|
+
const getLocaleKey = (category, type2) => {
|
|
199
|
+
const suffix = hasExpiry ? "WithExpiry" : "";
|
|
200
|
+
return `payment.checkout.credit.${category}.${type2}${suffix}`;
|
|
201
|
+
};
|
|
202
|
+
if (!hasPendingAmount) {
|
|
203
|
+
const type2 = isRecurring ? "recurring" : "oneTime";
|
|
204
|
+
return t(getLocaleKey("normal", type2), buildBaseParams());
|
|
205
|
+
}
|
|
206
|
+
const pendingAmountBN = new _util.BN(pendingAmount || "0");
|
|
207
|
+
const creditAmountBN = (0, _util.fromTokenToUnit)(new _util.BN(creditAmount), creditCurrency?.decimal || 2);
|
|
208
|
+
const minQuantityNeeded = Math.ceil(pendingAmountBN.mul(new _util.BN(100)).div(creditAmountBN).toNumber() / 100);
|
|
209
|
+
const currentPurchaseCreditBN = creditAmountBN.mul(new _util.BN(localQuantity || 0));
|
|
210
|
+
const actualAvailable = currentPurchaseCreditBN.sub(pendingAmountBN).toString();
|
|
211
|
+
if (!new _util.BN(actualAvailable).gt(new _util.BN(0))) {
|
|
212
|
+
return t("payment.checkout.credit.pending.notEnough", {
|
|
213
|
+
amount: (0, _util2.formatBNStr)(pendingAmountBN.toString(), creditCurrency?.decimal || 2),
|
|
214
|
+
symbol: currencySymbol,
|
|
215
|
+
quantity: (0, _util2.formatNumber)(minQuantityNeeded)
|
|
216
|
+
});
|
|
114
217
|
}
|
|
115
|
-
|
|
218
|
+
const type = isRecurring ? "recurringEnough" : "oneTimeEnough";
|
|
219
|
+
return t(getLocaleKey("pending", type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
116
220
|
};
|
|
117
221
|
const primaryText = (0, _react.useMemo)(() => {
|
|
118
222
|
const price = item.upsell_price || item.price || {};
|
|
119
223
|
const isRecurring = price?.type === "recurring" && price?.recurring;
|
|
120
224
|
const trial = trialInDays > 0 || trialEnd > (0, _dayjs.default)().unix();
|
|
121
225
|
if (isRecurring && !trial && price?.recurring?.usage_type !== "metered") {
|
|
122
|
-
return `${pricing.primary} ${price.recurring ? (0,
|
|
226
|
+
return `${pricing.primary} ${price.recurring ? (0, _util2.formatRecurring)(price.recurring, false, "slash", locale) : ""}`;
|
|
123
227
|
}
|
|
124
228
|
return pricing.primary;
|
|
125
229
|
}, [trialInDays, trialEnd, pricing, item, locale]);
|
|
126
|
-
const quantityInventoryError = (0,
|
|
230
|
+
const quantityInventoryError = (0, _util2.formatQuantityInventory)(item.price, localQuantityNum, locale);
|
|
127
231
|
const features = item.price.product?.features || [];
|
|
128
232
|
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
129
233
|
direction: "column",
|
|
@@ -158,7 +262,7 @@ function ProductItem({
|
|
|
158
262
|
alignItems: "center"
|
|
159
263
|
},
|
|
160
264
|
children: item.price.type === "recurring" && item.price.recurring ? [pricing.quantity, t("common.billed", {
|
|
161
|
-
rule: `${(0,
|
|
265
|
+
rule: `${(0, _util2.formatRecurring)(item.upsell_price?.recurring || item.price.recurring, true, "per", locale)} ${metered}`
|
|
162
266
|
})].filter(Boolean).join(", ") : pricing.quantity
|
|
163
267
|
})
|
|
164
268
|
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
@@ -214,7 +318,7 @@ function ProductItem({
|
|
|
214
318
|
sx: {
|
|
215
319
|
fontSize: "0.75rem"
|
|
216
320
|
},
|
|
217
|
-
children: ["(-", (0,
|
|
321
|
+
children: ["(-", (0, _util2.formatAmount)(discountAmount.amount || "0", currency.decimal), " ", currency.symbol, ")"]
|
|
218
322
|
})]
|
|
219
323
|
}),
|
|
220
324
|
size: "small",
|
|
@@ -375,7 +479,7 @@ function ProductItem({
|
|
|
375
479
|
checked: false,
|
|
376
480
|
onChange: () => onUpsell(item.price_id, item.price.upsell?.upsells_to_id)
|
|
377
481
|
}), t("payment.checkout.upsell.save", {
|
|
378
|
-
recurring: (0,
|
|
482
|
+
recurring: (0, _util2.formatRecurring)(item.price.upsell.upsells_to.recurring, true, "per", locale)
|
|
379
483
|
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_status.default, {
|
|
380
484
|
label: t("payment.checkout.upsell.off", {
|
|
381
485
|
saving
|
|
@@ -393,7 +497,7 @@ function ProductItem({
|
|
|
393
497
|
sx: {
|
|
394
498
|
fontSize: 12
|
|
395
499
|
},
|
|
396
|
-
children: (0,
|
|
500
|
+
children: (0, _util2.formatPrice)(item.price.upsell.upsells_to, currency, item.price.product?.unit_label, 1, true, locale)
|
|
397
501
|
})]
|
|
398
502
|
}), canUpsell && item.upsell_price_id && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
399
503
|
direction: "row",
|
|
@@ -420,14 +524,14 @@ function ProductItem({
|
|
|
420
524
|
checked: true,
|
|
421
525
|
onChange: () => onDownsell(item.upsell_price_id)
|
|
422
526
|
}), t("payment.checkout.upsell.revert", {
|
|
423
|
-
recurring: t(`common.${(0,
|
|
527
|
+
recurring: t(`common.${(0, _util2.formatRecurring)(item.price.recurring)}`)
|
|
424
528
|
})]
|
|
425
529
|
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
426
530
|
component: "span",
|
|
427
531
|
sx: {
|
|
428
532
|
fontSize: 12
|
|
429
533
|
},
|
|
430
|
-
children: (0,
|
|
534
|
+
children: (0, _util2.formatPrice)(item.price, currency, item.price.product?.unit_label, 1, true, locale)
|
|
431
535
|
})]
|
|
432
536
|
})]
|
|
433
537
|
});
|
package/lib/payment/success.js
CHANGED
|
@@ -44,7 +44,7 @@ function PaymentSuccess({
|
|
|
44
44
|
const response = await api.get((0, _ufo.joinURL)(prefix, `/api/vendors/order/${sessionId}/status`), {});
|
|
45
45
|
const needCheckError = Date.now() - timerRef.current > 6 * 1e3;
|
|
46
46
|
const allCompleted = response.data?.vendors?.every(vendor => vendor.progress >= 100);
|
|
47
|
-
const hasAnyFailed = response.data?.vendors?.some(vendor => vendor.status === "failed" || needCheckError && !!vendor.error);
|
|
47
|
+
const hasAnyFailed = response.data?.vendors?.some(vendor => vendor.status === "failed" || needCheckError && !!vendor.error && !!vendor.error_message);
|
|
48
48
|
if (hasAnyFailed || allCompleted) {
|
|
49
49
|
clearInterval(interval2);
|
|
50
50
|
}
|
|
@@ -246,7 +246,7 @@ const Div = (0, _material.styled)("div")`
|
|
|
246
246
|
content: '';
|
|
247
247
|
height: 100px;
|
|
248
248
|
position: absolute;
|
|
249
|
-
background: ${props => props.theme.palette.background.
|
|
249
|
+
background: ${props => props.theme.palette.background.default};
|
|
250
250
|
transform: rotate(-45deg);
|
|
251
251
|
}
|
|
252
252
|
.check-icon .icon-line {
|
|
@@ -290,7 +290,7 @@ const Div = (0, _material.styled)("div")`
|
|
|
290
290
|
height: 85px;
|
|
291
291
|
position: absolute;
|
|
292
292
|
transform: rotate(-45deg);
|
|
293
|
-
background-color: ${props => props.theme.palette.background.
|
|
293
|
+
background-color: ${props => props.theme.palette.background.default};
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
@keyframes rotate-circle {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/payment-react",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.16",
|
|
4
4
|
"description": "Reusable react components for payment kit v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@arcblock/did-connect-react": "^3.1.41",
|
|
58
58
|
"@arcblock/ux": "^3.1.41",
|
|
59
|
-
"@arcblock/ws": "^1.25.
|
|
59
|
+
"@arcblock/ws": "^1.25.3",
|
|
60
60
|
"@blocklet/theme": "^3.1.41",
|
|
61
61
|
"@blocklet/ui-react": "^3.1.41",
|
|
62
62
|
"@mui/icons-material": "^7.1.2",
|
|
63
63
|
"@mui/lab": "7.0.0-beta.14",
|
|
64
64
|
"@mui/material": "^7.1.2",
|
|
65
65
|
"@mui/system": "^7.1.1",
|
|
66
|
-
"@ocap/util": "^1.25.
|
|
66
|
+
"@ocap/util": "^1.25.3",
|
|
67
67
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
68
68
|
"@stripe/stripe-js": "^2.4.0",
|
|
69
69
|
"@vitejs/plugin-legacy": "^7.0.0",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"@babel/core": "^7.27.4",
|
|
95
95
|
"@babel/preset-env": "^7.27.2",
|
|
96
96
|
"@babel/preset-react": "^7.27.1",
|
|
97
|
-
"@blocklet/payment-types": "1.20.
|
|
97
|
+
"@blocklet/payment-types": "1.20.16",
|
|
98
98
|
"@storybook/addon-essentials": "^7.6.20",
|
|
99
99
|
"@storybook/addon-interactions": "^7.6.20",
|
|
100
100
|
"@storybook/addon-links": "^7.6.20",
|
|
@@ -125,5 +125,5 @@
|
|
|
125
125
|
"vite-plugin-babel": "^1.3.1",
|
|
126
126
|
"vite-plugin-node-polyfills": "^0.23.0"
|
|
127
127
|
},
|
|
128
|
-
"gitHead": "
|
|
128
|
+
"gitHead": "ebf0677dd4414d4abc4129d6663a7e9ddb415281"
|
|
129
129
|
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -226,9 +226,24 @@ export default flat({
|
|
|
226
226
|
remove: 'Remove from order',
|
|
227
227
|
},
|
|
228
228
|
credit: {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
normal: {
|
|
230
|
+
oneTime: 'You will receive {amount} {symbol} after payment.',
|
|
231
|
+
oneTimeWithExpiry: 'You will receive {amount} {symbol} after payment, valid for {duration} {unit}.',
|
|
232
|
+
recurring: 'You will receive {amount} {symbol} {period}.',
|
|
233
|
+
recurringWithExpiry: 'You will receive {amount} {symbol} {period}, valid for {duration} {unit}.',
|
|
234
|
+
},
|
|
235
|
+
pending: {
|
|
236
|
+
notEnough:
|
|
237
|
+
'Your outstanding balance is {amount} {symbol}. To settle it, a minimum purchase of {quantity} units is required.',
|
|
238
|
+
oneTimeEnough:
|
|
239
|
+
'Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol}, resulting in a net balance of {availableAmount} {symbol} after the deduction.',
|
|
240
|
+
oneTimeEnoughWithExpiry:
|
|
241
|
+
'Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol} (valid for {duration} {unit}), resulting in a net balance of {availableAmount} {symbol} after the deduction.',
|
|
242
|
+
recurringEnough:
|
|
243
|
+
'Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol} {period}, resulting in a net balance of {availableAmount} {symbol} after the deduction.',
|
|
244
|
+
recurringEnoughWithExpiry:
|
|
245
|
+
'Your outstanding balance is {amount} {symbol}. Upon payment, you will receive {totalAmount} {symbol} {period} (valid for {duration} {unit}), resulting in a net balance of {availableAmount} {symbol} after the deduction.',
|
|
246
|
+
},
|
|
232
247
|
},
|
|
233
248
|
expired: {
|
|
234
249
|
title: 'Expired Link',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -260,9 +260,23 @@ export default flat({
|
|
|
260
260
|
},
|
|
261
261
|
},
|
|
262
262
|
credit: {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
normal: {
|
|
264
|
+
oneTime: '付款完成后您将获得 {amount} {symbol} 额度。',
|
|
265
|
+
oneTimeWithExpiry: '付款完成后您将获得 {amount} {symbol} 额度,有效期 {duration} {unit}。',
|
|
266
|
+
recurring: '付款完成后,{period}您将获得 {amount} {symbol} 额度。',
|
|
267
|
+
recurringWithExpiry: '付款完成后,{period}您将获得 {amount} {symbol} 额度,有效期 {duration} {unit}。',
|
|
268
|
+
},
|
|
269
|
+
pending: {
|
|
270
|
+
notEnough: '您当前欠费 {amount} {symbol},至少需要购买 {quantity} 数量。',
|
|
271
|
+
oneTimeEnough:
|
|
272
|
+
'您当前欠费 {amount} {symbol},付款后将获得 {totalAmount} {symbol} 额度,扣除欠费后净余额为 {availableAmount} {symbol}。',
|
|
273
|
+
oneTimeEnoughWithExpiry:
|
|
274
|
+
'您当前欠费 {amount} {symbol},付款后将获得 {totalAmount} {symbol} 额度(有效期 {duration} {unit}),扣除欠费后净余额为 {availableAmount} {symbol}。',
|
|
275
|
+
recurringEnough:
|
|
276
|
+
'您当前欠费 {amount} {symbol},付款后{period}将获得 {totalAmount} {symbol} 额度,扣除欠费后净余额为 {availableAmount} {symbol}。',
|
|
277
|
+
recurringEnoughWithExpiry:
|
|
278
|
+
'您当前欠费 {amount} {symbol},付款后{period}将获得 {totalAmount} {symbol} 额度(有效期 {duration} {unit}),扣除欠费后净余额为 {availableAmount} {symbol}。',
|
|
279
|
+
},
|
|
266
280
|
},
|
|
267
281
|
emptyItems: {
|
|
268
282
|
title: '没有任何购买项目',
|
|
@@ -3,7 +3,9 @@ import type { PriceRecurring, TLineItemExpanded, TPaymentCurrency } from '@block
|
|
|
3
3
|
import { Box, Stack, Typography, IconButton, TextField, Alert, Chip } from '@mui/material';
|
|
4
4
|
import { Add, Remove, LocalOffer } from '@mui/icons-material';
|
|
5
5
|
|
|
6
|
-
import React, { useMemo, useState } from 'react';
|
|
6
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
7
|
+
import { useRequest } from 'ahooks';
|
|
8
|
+
import { BN, fromTokenToUnit } from '@ocap/util';
|
|
7
9
|
import Status from '../components/status';
|
|
8
10
|
import Switch from '../components/switch-button';
|
|
9
11
|
import {
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
formatRecurring,
|
|
16
18
|
formatUpsellSaving,
|
|
17
19
|
formatAmount,
|
|
20
|
+
formatBNStr,
|
|
18
21
|
} from '../libs/util';
|
|
19
22
|
import ProductCard from './product-card';
|
|
20
23
|
import dayjs from '../libs/dayjs';
|
|
@@ -41,6 +44,35 @@ type Props = {
|
|
|
41
44
|
showFeatures?: boolean;
|
|
42
45
|
};
|
|
43
46
|
|
|
47
|
+
const getRecommendedQuantityFromUrl = (priceId: string): number | undefined => {
|
|
48
|
+
try {
|
|
49
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
50
|
+
const recommendedQuantity = urlParams.get(`qty_${priceId}`) || urlParams.get('qty');
|
|
51
|
+
return recommendedQuantity ? Math.max(1, parseInt(recommendedQuantity, 10)) : undefined;
|
|
52
|
+
} catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getUserQuantityPreference = (userDid: string, priceId: string): number | undefined => {
|
|
58
|
+
try {
|
|
59
|
+
const key = `quantity_preference_${userDid}_${priceId}`;
|
|
60
|
+
const stored = localStorage.getItem(key);
|
|
61
|
+
return stored ? Math.max(1, parseInt(stored, 10)) : undefined;
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const saveUserQuantityPreference = (userDid: string, priceId: string, quantity: number): void => {
|
|
68
|
+
try {
|
|
69
|
+
const key = `quantity_preference_${userDid}_${priceId}`;
|
|
70
|
+
localStorage.setItem(key, quantity.toString());
|
|
71
|
+
} catch {
|
|
72
|
+
// Silently fail if localStorage is not available
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
44
76
|
export default function ProductItem({
|
|
45
77
|
item,
|
|
46
78
|
items,
|
|
@@ -57,7 +89,7 @@ export default function ProductItem({
|
|
|
57
89
|
showFeatures = false,
|
|
58
90
|
}: Props) {
|
|
59
91
|
const { t, locale } = useLocaleContext();
|
|
60
|
-
const { settings, setPayable } = usePaymentContext();
|
|
92
|
+
const { settings, setPayable, session, api } = usePaymentContext();
|
|
61
93
|
const pricing = formatLineItemPricing(item, currency, { trialEnd, trialInDays }, locale);
|
|
62
94
|
const saving = formatUpsellSaving(items, currency);
|
|
63
95
|
const metered = item.price?.recurring?.usage_type === 'metered' ? t('common.metered') : '';
|
|
@@ -71,7 +103,47 @@ export default function ProductItem({
|
|
|
71
103
|
const validDuration = item.price.metadata?.credit_config?.valid_duration_value;
|
|
72
104
|
const validDurationUnit = item.price.metadata?.credit_config?.valid_duration_unit || 'days';
|
|
73
105
|
|
|
74
|
-
const
|
|
106
|
+
const userDid = session?.user?.did;
|
|
107
|
+
|
|
108
|
+
const { data: pendingAmount } = useRequest(
|
|
109
|
+
async () => {
|
|
110
|
+
if (!isCreditProduct || !userDid) return null;
|
|
111
|
+
try {
|
|
112
|
+
const { data } = await api.get('/api/meter-events/pending-amount', {
|
|
113
|
+
params: {
|
|
114
|
+
customer_id: userDid,
|
|
115
|
+
currency_id: creditCurrency?.id,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
return data?.[creditCurrency?.id || ''];
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn('Failed to fetch pending amount:', error);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
refreshDeps: [isCreditProduct, userDid, creditCurrency?.id],
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// calculate initial quantity: priority URL recommendation > user preference > item.quantity
|
|
130
|
+
const getInitialQuantity = (): number | undefined => {
|
|
131
|
+
const urlQuantity = getRecommendedQuantityFromUrl(item.price.id);
|
|
132
|
+
if (urlQuantity && urlQuantity > 0) {
|
|
133
|
+
return urlQuantity;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (userDid) {
|
|
137
|
+
const preferredQuantity = getUserQuantityPreference(userDid, item.price.id);
|
|
138
|
+
if (preferredQuantity && preferredQuantity > 0) {
|
|
139
|
+
return preferredQuantity;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return item.quantity;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const [localQuantity, setLocalQuantity] = useState<number | undefined>(getInitialQuantity());
|
|
75
147
|
const canAdjustQuantity = adjustableQuantity.enabled && mode === 'normal';
|
|
76
148
|
const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
|
|
77
149
|
const quantityAvailable = Math.min(item.price.quantity_limit_per_checkout, item.price.quantity_available);
|
|
@@ -80,6 +152,15 @@ export default function ProductItem({
|
|
|
80
152
|
: adjustableQuantity.maximum || Infinity;
|
|
81
153
|
const localQuantityNum = localQuantity || 0;
|
|
82
154
|
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const initialQuantity = getInitialQuantity();
|
|
157
|
+
if (initialQuantity !== item.quantity && initialQuantity && initialQuantity > 1) {
|
|
158
|
+
// need update checkout session
|
|
159
|
+
onQuantityChange(item.price_id, initialQuantity);
|
|
160
|
+
}
|
|
161
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
83
164
|
const handleQuantityChange = (newQuantity: number) => {
|
|
84
165
|
if (!newQuantity) {
|
|
85
166
|
setLocalQuantity(undefined);
|
|
@@ -93,6 +174,10 @@ export default function ProductItem({
|
|
|
93
174
|
}
|
|
94
175
|
setLocalQuantity(newQuantity);
|
|
95
176
|
onQuantityChange(item.price_id, newQuantity);
|
|
177
|
+
|
|
178
|
+
if (userDid && newQuantity > 0) {
|
|
179
|
+
saveUserQuantityPreference(userDid, item.price.id, newQuantity);
|
|
180
|
+
}
|
|
96
181
|
}
|
|
97
182
|
};
|
|
98
183
|
|
|
@@ -119,30 +204,66 @@ export default function ProductItem({
|
|
|
119
204
|
const formatCreditInfo = () => {
|
|
120
205
|
if (!isCreditProduct) return null;
|
|
121
206
|
|
|
207
|
+
const totalCreditStr = formatNumber(creditAmount * (localQuantity || 0));
|
|
208
|
+
const currencySymbol = creditCurrency?.symbol || 'Credits';
|
|
209
|
+
const hasPendingAmount = pendingAmount && new BN(pendingAmount || '0').gt(new BN(0));
|
|
122
210
|
const isRecurring = item.price.type === 'recurring';
|
|
123
|
-
const
|
|
211
|
+
const hasExpiry = validDuration && validDuration > 0;
|
|
124
212
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
213
|
+
const buildBaseParams = () => ({
|
|
214
|
+
amount: totalCreditStr,
|
|
215
|
+
symbol: currencySymbol,
|
|
216
|
+
...(hasExpiry && {
|
|
217
|
+
duration: validDuration,
|
|
218
|
+
unit: t(`common.${validDurationUnit}`),
|
|
219
|
+
}),
|
|
220
|
+
...(isRecurring && {
|
|
129
221
|
period: formatRecurring(item.price.recurring!, true, 'per', locale),
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
message = t('payment.checkout.credit.oneTimeInfo', {
|
|
133
|
-
amount: totalCredit,
|
|
134
|
-
symbol: creditCurrency?.symbol || 'Credits',
|
|
135
|
-
});
|
|
136
|
-
}
|
|
222
|
+
}),
|
|
223
|
+
});
|
|
137
224
|
|
|
138
|
-
|
|
139
|
-
|
|
225
|
+
const buildPendingParams = (pendingBN: BN, availableAmount?: string) => ({
|
|
226
|
+
amount: formatBNStr(pendingBN.toString(), creditCurrency?.decimal || 2),
|
|
227
|
+
symbol: currencySymbol,
|
|
228
|
+
totalAmount: totalCreditStr,
|
|
229
|
+
...(availableAmount && {
|
|
230
|
+
availableAmount: formatBNStr(availableAmount, creditCurrency?.decimal || 2),
|
|
231
|
+
}),
|
|
232
|
+
...(hasExpiry && {
|
|
140
233
|
duration: validDuration,
|
|
141
234
|
unit: t(`common.${validDurationUnit}`),
|
|
142
|
-
})
|
|
235
|
+
}),
|
|
236
|
+
...(isRecurring && {
|
|
237
|
+
period: formatRecurring(item.price.recurring!, true, 'per', locale),
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const getLocaleKey = (category: 'normal' | 'pending', type: string) => {
|
|
242
|
+
const suffix = hasExpiry ? 'WithExpiry' : '';
|
|
243
|
+
return `payment.checkout.credit.${category}.${type}${suffix}`;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (!hasPendingAmount) {
|
|
247
|
+
const type = isRecurring ? 'recurring' : 'oneTime';
|
|
248
|
+
return t(getLocaleKey('normal', type), buildBaseParams());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const pendingAmountBN = new BN(pendingAmount || '0');
|
|
252
|
+
const creditAmountBN = fromTokenToUnit(new BN(creditAmount), creditCurrency?.decimal || 2);
|
|
253
|
+
const minQuantityNeeded = Math.ceil(pendingAmountBN.mul(new BN(100)).div(creditAmountBN).toNumber() / 100);
|
|
254
|
+
const currentPurchaseCreditBN = creditAmountBN.mul(new BN(localQuantity || 0));
|
|
255
|
+
const actualAvailable = currentPurchaseCreditBN.sub(pendingAmountBN).toString();
|
|
256
|
+
|
|
257
|
+
if (!new BN(actualAvailable).gt(new BN(0))) {
|
|
258
|
+
return t('payment.checkout.credit.pending.notEnough', {
|
|
259
|
+
amount: formatBNStr(pendingAmountBN.toString(), creditCurrency?.decimal || 2),
|
|
260
|
+
symbol: currencySymbol,
|
|
261
|
+
quantity: formatNumber(minQuantityNeeded),
|
|
262
|
+
});
|
|
143
263
|
}
|
|
144
264
|
|
|
145
|
-
|
|
265
|
+
const type = isRecurring ? 'recurringEnough' : 'oneTimeEnough';
|
|
266
|
+
return t(getLocaleKey('pending', type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
146
267
|
};
|
|
147
268
|
|
|
148
269
|
const primaryText = useMemo(() => {
|
package/src/payment/success.tsx
CHANGED
|
@@ -67,7 +67,7 @@ export default function PaymentSuccess({
|
|
|
67
67
|
|
|
68
68
|
const allCompleted = response.data?.vendors?.every((vendor: any) => vendor.progress >= 100);
|
|
69
69
|
const hasAnyFailed = response.data?.vendors?.some(
|
|
70
|
-
(vendor: any) => vendor.status === 'failed' || (needCheckError && !!vendor.error)
|
|
70
|
+
(vendor: any) => vendor.status === 'failed' || (needCheckError && !!vendor.error && !!vendor.error_message)
|
|
71
71
|
);
|
|
72
72
|
if (hasAnyFailed || allCompleted) {
|
|
73
73
|
clearInterval(interval);
|
|
@@ -271,7 +271,7 @@ const Div = styled('div')`
|
|
|
271
271
|
content: '';
|
|
272
272
|
height: 100px;
|
|
273
273
|
position: absolute;
|
|
274
|
-
background: ${(props) => props.theme.palette.background.
|
|
274
|
+
background: ${(props) => props.theme.palette.background.default};
|
|
275
275
|
transform: rotate(-45deg);
|
|
276
276
|
}
|
|
277
277
|
.check-icon .icon-line {
|
|
@@ -315,7 +315,7 @@ const Div = styled('div')`
|
|
|
315
315
|
height: 85px;
|
|
316
316
|
position: absolute;
|
|
317
317
|
transform: rotate(-45deg);
|
|
318
|
-
background-color: ${(props) => props.theme.palette.background.
|
|
318
|
+
background-color: ${(props) => props.theme.palette.background.default};
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
@keyframes rotate-circle {
|