@blocklet/payment-react 1.27.2 → 1.29.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/checkout-v2/components/left/product-item-card.js +33 -5
- package/es/checkout-v2/panels/left/credit-topup-panel.js +13 -1
- package/es/checkout-v2/panels/right/payment-panel.js +15 -3
- package/es/checkout-v2/views/success-view.js +1 -1
- package/es/history/invoice/list.js +17 -8
- package/es/libs/util.js +1 -0
- package/es/locales/en.js +1 -0
- package/es/locales/zh.js +1 -0
- package/lib/checkout-v2/components/left/product-item-card.js +26 -4
- package/lib/checkout-v2/panels/left/credit-topup-panel.js +10 -1
- package/lib/checkout-v2/panels/right/payment-panel.js +15 -3
- package/lib/checkout-v2/views/success-view.js +1 -1
- package/lib/history/invoice/list.js +22 -11
- package/lib/libs/util.js +1 -0
- package/lib/locales/en.js +1 -0
- package/lib/locales/zh.js +1 -0
- package/package.json +12 -12
- package/src/checkout-v2/components/left/product-item-card.tsx +35 -5
- package/src/checkout-v2/panels/left/credit-topup-panel.tsx +16 -2
- package/src/checkout-v2/panels/right/payment-panel.tsx +28 -3
- package/src/checkout-v2/views/success-view.tsx +1 -1
- package/src/history/invoice/list.tsx +29 -14
- package/src/hooks/subscription.ts +1 -1
- package/src/libs/util.ts +1 -0
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from "react";
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
3
3
|
import AddIcon from "@mui/icons-material/Add";
|
|
4
4
|
import CheckIcon from "@mui/icons-material/Check";
|
|
5
5
|
import LocalOfferIcon from "@mui/icons-material/LocalOffer";
|
|
@@ -129,6 +129,22 @@ export default function ProductItemCard({
|
|
|
129
129
|
useEffect(() => {
|
|
130
130
|
if (!isEditing) setQtyInput(String(quantity));
|
|
131
131
|
}, [quantity, isEditing]);
|
|
132
|
+
const debounceRef = useRef(null);
|
|
133
|
+
const debouncedQuantityChange = useCallback(
|
|
134
|
+
(priceId, qty) => {
|
|
135
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
136
|
+
debounceRef.current = setTimeout(() => {
|
|
137
|
+
onQuantityChange(priceId, qty);
|
|
138
|
+
}, 400);
|
|
139
|
+
},
|
|
140
|
+
[onQuantityChange]
|
|
141
|
+
);
|
|
142
|
+
useEffect(
|
|
143
|
+
() => () => {
|
|
144
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
145
|
+
},
|
|
146
|
+
[]
|
|
147
|
+
);
|
|
132
148
|
const canUpsell = !!item.price?.upsell?.upsells_to;
|
|
133
149
|
const isUpselled = !!item.upsell_price;
|
|
134
150
|
const upsellTo = item.price?.upsell?.upsells_to;
|
|
@@ -409,8 +425,14 @@ export default function ProductItemCard({
|
|
|
409
425
|
IconButton,
|
|
410
426
|
{
|
|
411
427
|
size: "small",
|
|
412
|
-
onClick: () =>
|
|
413
|
-
|
|
428
|
+
onClick: () => {
|
|
429
|
+
const cur = parseInt(qtyInput, 10) || quantity;
|
|
430
|
+
const newQty = cur - 1;
|
|
431
|
+
if (newQty < min) return;
|
|
432
|
+
setQtyInput(String(newQty));
|
|
433
|
+
debouncedQuantityChange(item.price_id, newQty);
|
|
434
|
+
},
|
|
435
|
+
disabled: (parseInt(qtyInput, 10) || quantity) <= min,
|
|
414
436
|
sx: {
|
|
415
437
|
width: 36,
|
|
416
438
|
height: 36,
|
|
@@ -477,8 +499,14 @@ export default function ProductItemCard({
|
|
|
477
499
|
IconButton,
|
|
478
500
|
{
|
|
479
501
|
size: "small",
|
|
480
|
-
onClick: () =>
|
|
481
|
-
|
|
502
|
+
onClick: () => {
|
|
503
|
+
const cur = parseInt(qtyInput, 10) || quantity;
|
|
504
|
+
const newQty = cur + 1;
|
|
505
|
+
if (max && newQty > max) return;
|
|
506
|
+
setQtyInput(String(newQty));
|
|
507
|
+
debouncedQuantityChange(item.price_id, newQty);
|
|
508
|
+
},
|
|
509
|
+
disabled: max ? (parseInt(qtyInput, 10) || quantity) >= max : false,
|
|
482
510
|
sx: {
|
|
483
511
|
width: 36,
|
|
484
512
|
height: 36,
|
|
@@ -153,6 +153,13 @@ export default function CreditTopupPanel() {
|
|
|
153
153
|
}, [minQtyForPending]);
|
|
154
154
|
const maxCredits = maxQuantity * step;
|
|
155
155
|
const minCredits = minQuantity * step;
|
|
156
|
+
const debounceRef = useRef(null);
|
|
157
|
+
useEffect(
|
|
158
|
+
() => () => {
|
|
159
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
160
|
+
},
|
|
161
|
+
[]
|
|
162
|
+
);
|
|
156
163
|
const commitCredits = useCallback(
|
|
157
164
|
(credits) => {
|
|
158
165
|
if (credits <= 0) return;
|
|
@@ -161,7 +168,12 @@ export default function CreditTopupPanel() {
|
|
|
161
168
|
const clampedPacks = Math.max(minQuantity, Math.min(maxQuantity, packs));
|
|
162
169
|
setLocalQty(clampedPacks);
|
|
163
170
|
setDesiredCredits(clamped);
|
|
164
|
-
if (item)
|
|
171
|
+
if (item) {
|
|
172
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
173
|
+
debounceRef.current = setTimeout(() => {
|
|
174
|
+
lineItems.updateQuantity(item.price_id, clampedPacks);
|
|
175
|
+
}, 400);
|
|
176
|
+
}
|
|
165
177
|
},
|
|
166
178
|
[step, minQuantity, maxQuantity, minCredits, maxCredits, item, lineItems]
|
|
167
179
|
);
|
|
@@ -122,31 +122,43 @@ export default function PaymentPanel() {
|
|
|
122
122
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
123
123
|
}, [canSubmit, submit.status, handleAction]);
|
|
124
124
|
const didConnectOpenedRef = useRef(false);
|
|
125
|
+
const didConnectSucceededRef = useRef(false);
|
|
126
|
+
const submitStatusRef = useRef(submit.status);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
submitStatusRef.current = submit.status;
|
|
129
|
+
}, [submit.status]);
|
|
125
130
|
useEffect(() => {
|
|
126
131
|
if (submit.status !== "waiting_did") {
|
|
127
132
|
didConnectOpenedRef.current = false;
|
|
133
|
+
didConnectSucceededRef.current = false;
|
|
128
134
|
return;
|
|
129
135
|
}
|
|
130
136
|
const ctx = submit.context;
|
|
131
137
|
if (ctx?.type !== "did_connect" || !connect) return;
|
|
132
138
|
if (didConnectOpenedRef.current) return;
|
|
133
139
|
didConnectOpenedRef.current = true;
|
|
134
|
-
const didPrefix = `${paymentKitPrefix}/api/did`.replace(/([^:])\/\//g, "$1/");
|
|
140
|
+
const didPrefix = `${paymentKitPrefix || window.location.origin}/api/did`.replace(/([^:])\/\//g, "$1/");
|
|
135
141
|
connect.open({
|
|
136
142
|
locale,
|
|
137
143
|
action: ctx.action,
|
|
138
144
|
prefix: didPrefix,
|
|
139
145
|
saveConnect: false,
|
|
140
146
|
extraParams: ctx.extraParams,
|
|
141
|
-
onSuccess: () => {
|
|
147
|
+
onSuccess: async () => {
|
|
148
|
+
didConnectSucceededRef.current = true;
|
|
142
149
|
connect.close();
|
|
150
|
+
await submit.didConnectComplete();
|
|
143
151
|
},
|
|
144
152
|
onClose: () => {
|
|
145
153
|
connect.close();
|
|
146
|
-
|
|
154
|
+
if (submitStatusRef.current !== "waiting_did") return;
|
|
155
|
+
if (!didConnectSucceededRef.current) {
|
|
156
|
+
submit.reset();
|
|
157
|
+
}
|
|
147
158
|
},
|
|
148
159
|
onError: (err) => {
|
|
149
160
|
console.error("DID Connect error:", err);
|
|
161
|
+
if (submitStatusRef.current !== "waiting_did") return;
|
|
150
162
|
submit.reset();
|
|
151
163
|
},
|
|
152
164
|
messages: {
|
|
@@ -365,7 +365,7 @@ function VendorProgressPanel({
|
|
|
365
365
|
animation: `${fadeUp} 0.5s ease 0.3s both`
|
|
366
366
|
},
|
|
367
367
|
children: [
|
|
368
|
-
vendorStatus.vendors.map((vendor, idx) => /* @__PURE__ */ jsx(VendorProgressItemV2, { vendor, t }, vendor.title || `vendor-${idx}`)),
|
|
368
|
+
(vendorStatus.vendors || []).map((vendor, idx) => /* @__PURE__ */ jsx(VendorProgressItemV2, { vendor, t }, vendor.title || `vendor-${idx}`)),
|
|
369
369
|
vendorStatus.hasFailed && /* @__PURE__ */ jsx(Typography, { sx: { fontSize: 13, fontWeight: 600, color: "warning.main", mt: 1 }, children: t("payment.checkout.vendor.failedMsg") }),
|
|
370
370
|
vendorStatus.isAllCompleted && pageInfo?.success_message?.[locale] && /* @__PURE__ */ jsx(Typography, { sx: { fontSize: 14, fontWeight: 600, color: "text.primary", mt: 1 }, children: pageInfo.success_message[locale] })
|
|
371
371
|
]
|
|
@@ -410,11 +410,12 @@ const InvoiceTable = React.memo((props) => {
|
|
|
410
410
|
options: {
|
|
411
411
|
customBodyRenderLite: (val, index) => {
|
|
412
412
|
const invoice = data?.list[index];
|
|
413
|
-
|
|
413
|
+
const periodTooltip = invoice.subscription_id && invoice.period_start && invoice.period_end ? `${t("common.billingPeriod")}: ${formatToDate(invoice.period_start * 1e3, locale, "YYYY-MM-DD HH:mm")} ~ ${formatToDate(invoice.period_end * 1e3, locale, "YYYY-MM-DD HH:mm")}` : "";
|
|
414
|
+
return /* @__PURE__ */ jsx(Tooltip, { title: periodTooltip, arrow: true, placement: "top-start", children: /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: linkStyle, children: formatToDate(
|
|
414
415
|
invoice.created_at,
|
|
415
416
|
locale,
|
|
416
417
|
relatedSubscription ? "YYYY-MM-DD HH:mm" : "YYYY-MM-DD HH:mm:ss"
|
|
417
|
-
) });
|
|
418
|
+
) }) });
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
421
|
},
|
|
@@ -700,13 +701,21 @@ const InvoiceList = React.memo((props) => {
|
|
|
700
701
|
}
|
|
701
702
|
),
|
|
702
703
|
/* @__PURE__ */ jsx(
|
|
703
|
-
|
|
704
|
+
Tooltip,
|
|
704
705
|
{
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
706
|
+
title: invoice.subscription_id && invoice.period_start && invoice.period_end ? `${t("common.billingPeriod")}: ${formatToDate(invoice.period_start * 1e3, locale, "YYYY-MM-DD HH:mm")} ~ ${formatToDate(invoice.period_end * 1e3, locale, "YYYY-MM-DD HH:mm")}` : "",
|
|
707
|
+
arrow: true,
|
|
708
|
+
placement: "top-start",
|
|
709
|
+
children: /* @__PURE__ */ jsx(
|
|
710
|
+
Box,
|
|
711
|
+
{
|
|
712
|
+
sx: {
|
|
713
|
+
flex: 1,
|
|
714
|
+
textAlign: "right"
|
|
715
|
+
},
|
|
716
|
+
children: /* @__PURE__ */ jsx(Typography, { children: formatToDate(invoice.created_at, locale, "HH:mm:ss") })
|
|
717
|
+
}
|
|
718
|
+
)
|
|
710
719
|
}
|
|
711
720
|
),
|
|
712
721
|
!action && /* @__PURE__ */ jsx(
|
package/es/libs/util.js
CHANGED
|
@@ -1225,6 +1225,7 @@ export function truncateText(text, maxLength, useWidth = false) {
|
|
|
1225
1225
|
return `${truncated}...`;
|
|
1226
1226
|
}
|
|
1227
1227
|
export function getCustomerAvatar(did, updated_at, imageSize = 48) {
|
|
1228
|
+
if (!did) return "";
|
|
1228
1229
|
const updated = typeof updated_at === "number" ? updated_at : dayjs(updated_at).unix();
|
|
1229
1230
|
return `/.well-known/service/user/avatar/${did}?imageFilter=resize&w=${imageSize}&h=${imageSize}&updateAt=${updated || dayjs().unix()}`;
|
|
1230
1231
|
}
|
package/es/locales/en.js
CHANGED
package/es/locales/zh.js
CHANGED
|
@@ -126,6 +126,16 @@ function ProductItemCard({
|
|
|
126
126
|
(0, _react.useEffect)(() => {
|
|
127
127
|
if (!isEditing) setQtyInput(String(quantity));
|
|
128
128
|
}, [quantity, isEditing]);
|
|
129
|
+
const debounceRef = (0, _react.useRef)(null);
|
|
130
|
+
const debouncedQuantityChange = (0, _react.useCallback)((priceId, qty) => {
|
|
131
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
132
|
+
debounceRef.current = setTimeout(() => {
|
|
133
|
+
onQuantityChange(priceId, qty);
|
|
134
|
+
}, 400);
|
|
135
|
+
}, [onQuantityChange]);
|
|
136
|
+
(0, _react.useEffect)(() => () => {
|
|
137
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
138
|
+
}, []);
|
|
129
139
|
const canUpsell = !!item.price?.upsell?.upsells_to;
|
|
130
140
|
const isUpselled = !!item.upsell_price;
|
|
131
141
|
const upsellTo = item.price?.upsell?.upsells_to;
|
|
@@ -554,8 +564,14 @@ function ProductItemCard({
|
|
|
554
564
|
children: t("common.quantity")
|
|
555
565
|
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.IconButton, {
|
|
556
566
|
size: "small",
|
|
557
|
-
onClick: () =>
|
|
558
|
-
|
|
567
|
+
onClick: () => {
|
|
568
|
+
const cur = parseInt(qtyInput, 10) || quantity;
|
|
569
|
+
const newQty = cur - 1;
|
|
570
|
+
if (newQty < min) return;
|
|
571
|
+
setQtyInput(String(newQty));
|
|
572
|
+
debouncedQuantityChange(item.price_id, newQty);
|
|
573
|
+
},
|
|
574
|
+
disabled: (parseInt(qtyInput, 10) || quantity) <= min,
|
|
559
575
|
sx: {
|
|
560
576
|
width: 36,
|
|
561
577
|
height: 36,
|
|
@@ -624,8 +640,14 @@ function ProductItemCard({
|
|
|
624
640
|
}
|
|
625
641
|
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.IconButton, {
|
|
626
642
|
size: "small",
|
|
627
|
-
onClick: () =>
|
|
628
|
-
|
|
643
|
+
onClick: () => {
|
|
644
|
+
const cur = parseInt(qtyInput, 10) || quantity;
|
|
645
|
+
const newQty = cur + 1;
|
|
646
|
+
if (max && newQty > max) return;
|
|
647
|
+
setQtyInput(String(newQty));
|
|
648
|
+
debouncedQuantityChange(item.price_id, newQty);
|
|
649
|
+
},
|
|
650
|
+
disabled: max ? (parseInt(qtyInput, 10) || quantity) >= max : false,
|
|
629
651
|
sx: {
|
|
630
652
|
width: 36,
|
|
631
653
|
height: 36,
|
|
@@ -181,6 +181,10 @@ function CreditTopupPanel() {
|
|
|
181
181
|
}, [minQtyForPending]);
|
|
182
182
|
const maxCredits = maxQuantity * step;
|
|
183
183
|
const minCredits = minQuantity * step;
|
|
184
|
+
const debounceRef = (0, _react.useRef)(null);
|
|
185
|
+
(0, _react.useEffect)(() => () => {
|
|
186
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
187
|
+
}, []);
|
|
184
188
|
const commitCredits = (0, _react.useCallback)(credits => {
|
|
185
189
|
if (credits <= 0) return;
|
|
186
190
|
const clamped = Math.max(minCredits, Math.min(maxCredits, credits));
|
|
@@ -188,7 +192,12 @@ function CreditTopupPanel() {
|
|
|
188
192
|
const clampedPacks = Math.max(minQuantity, Math.min(maxQuantity, packs));
|
|
189
193
|
setLocalQty(clampedPacks);
|
|
190
194
|
setDesiredCredits(clamped);
|
|
191
|
-
if (item)
|
|
195
|
+
if (item) {
|
|
196
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
197
|
+
debounceRef.current = setTimeout(() => {
|
|
198
|
+
lineItems.updateQuantity(item.price_id, clampedPacks);
|
|
199
|
+
}, 400);
|
|
200
|
+
}
|
|
192
201
|
}, [step, minQuantity, maxQuantity, minCredits, maxCredits, item, lineItems]);
|
|
193
202
|
const handleStep = (0, _react.useCallback)(delta => {
|
|
194
203
|
const newCredits = desiredCredits + delta * step;
|
|
@@ -125,31 +125,43 @@ function PaymentPanel() {
|
|
|
125
125
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
126
126
|
}, [canSubmit, submit.status, handleAction]);
|
|
127
127
|
const didConnectOpenedRef = (0, _react.useRef)(false);
|
|
128
|
+
const didConnectSucceededRef = (0, _react.useRef)(false);
|
|
129
|
+
const submitStatusRef = (0, _react.useRef)(submit.status);
|
|
130
|
+
(0, _react.useEffect)(() => {
|
|
131
|
+
submitStatusRef.current = submit.status;
|
|
132
|
+
}, [submit.status]);
|
|
128
133
|
(0, _react.useEffect)(() => {
|
|
129
134
|
if (submit.status !== "waiting_did") {
|
|
130
135
|
didConnectOpenedRef.current = false;
|
|
136
|
+
didConnectSucceededRef.current = false;
|
|
131
137
|
return;
|
|
132
138
|
}
|
|
133
139
|
const ctx = submit.context;
|
|
134
140
|
if (ctx?.type !== "did_connect" || !connect) return;
|
|
135
141
|
if (didConnectOpenedRef.current) return;
|
|
136
142
|
didConnectOpenedRef.current = true;
|
|
137
|
-
const didPrefix = `${paymentKitPrefix}/api/did`.replace(/([^:])\/\//g, "$1/");
|
|
143
|
+
const didPrefix = `${paymentKitPrefix || window.location.origin}/api/did`.replace(/([^:])\/\//g, "$1/");
|
|
138
144
|
connect.open({
|
|
139
145
|
locale,
|
|
140
146
|
action: ctx.action,
|
|
141
147
|
prefix: didPrefix,
|
|
142
148
|
saveConnect: false,
|
|
143
149
|
extraParams: ctx.extraParams,
|
|
144
|
-
onSuccess: () => {
|
|
150
|
+
onSuccess: async () => {
|
|
151
|
+
didConnectSucceededRef.current = true;
|
|
145
152
|
connect.close();
|
|
153
|
+
await submit.didConnectComplete();
|
|
146
154
|
},
|
|
147
155
|
onClose: () => {
|
|
148
156
|
connect.close();
|
|
149
|
-
|
|
157
|
+
if (submitStatusRef.current !== "waiting_did") return;
|
|
158
|
+
if (!didConnectSucceededRef.current) {
|
|
159
|
+
submit.reset();
|
|
160
|
+
}
|
|
150
161
|
},
|
|
151
162
|
onError: err => {
|
|
152
163
|
console.error("DID Connect error:", err);
|
|
164
|
+
if (submitStatusRef.current !== "waiting_did") return;
|
|
153
165
|
submit.reset();
|
|
154
166
|
},
|
|
155
167
|
messages: {
|
|
@@ -424,7 +424,7 @@ function VendorProgressPanel({
|
|
|
424
424
|
borderColor: "divider",
|
|
425
425
|
animation: `${fadeUp} 0.5s ease 0.3s both`
|
|
426
426
|
},
|
|
427
|
-
children: [vendorStatus.vendors.map((vendor, idx) => /* @__PURE__ */(0, _jsxRuntime.jsx)(VendorProgressItemV2, {
|
|
427
|
+
children: [(vendorStatus.vendors || []).map((vendor, idx) => /* @__PURE__ */(0, _jsxRuntime.jsx)(VendorProgressItemV2, {
|
|
428
428
|
vendor,
|
|
429
429
|
t
|
|
430
430
|
}, vendor.title || `vendor-${idx}`)), vendorStatus.hasFailed && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
@@ -479,10 +479,16 @@ const InvoiceTable = _react.default.memo(props => {
|
|
|
479
479
|
options: {
|
|
480
480
|
customBodyRenderLite: (val, index) => {
|
|
481
481
|
const invoice = data?.list[index];
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
482
|
+
const periodTooltip = invoice.subscription_id && invoice.period_start && invoice.period_end ? `${t("common.billingPeriod")}: ${(0, _util2.formatToDate)(invoice.period_start * 1e3, locale, "YYYY-MM-DD HH:mm")} ~ ${(0, _util2.formatToDate)(invoice.period_end * 1e3, locale, "YYYY-MM-DD HH:mm")}` : "";
|
|
483
|
+
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Tooltip, {
|
|
484
|
+
title: periodTooltip,
|
|
485
|
+
arrow: true,
|
|
486
|
+
placement: "top-start",
|
|
487
|
+
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
488
|
+
onClick: e => handleLinkClick(e, invoice),
|
|
489
|
+
sx: linkStyle,
|
|
490
|
+
children: (0, _util2.formatToDate)(invoice.created_at, locale, relatedSubscription ? "YYYY-MM-DD HH:mm" : "YYYY-MM-DD HH:mm:ss")
|
|
491
|
+
})
|
|
486
492
|
});
|
|
487
493
|
}
|
|
488
494
|
}
|
|
@@ -806,13 +812,18 @@ const InvoiceList = _react.default.memo(props => {
|
|
|
806
812
|
},
|
|
807
813
|
children: rateLine
|
|
808
814
|
})]
|
|
809
|
-
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
815
|
+
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Tooltip, {
|
|
816
|
+
title: invoice.subscription_id && invoice.period_start && invoice.period_end ? `${t("common.billingPeriod")}: ${(0, _util2.formatToDate)(invoice.period_start * 1e3, locale, "YYYY-MM-DD HH:mm")} ~ ${(0, _util2.formatToDate)(invoice.period_end * 1e3, locale, "YYYY-MM-DD HH:mm")}` : "",
|
|
817
|
+
arrow: true,
|
|
818
|
+
placement: "top-start",
|
|
819
|
+
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
820
|
+
sx: {
|
|
821
|
+
flex: 1,
|
|
822
|
+
textAlign: "right"
|
|
823
|
+
},
|
|
824
|
+
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
825
|
+
children: (0, _util2.formatToDate)(invoice.created_at, locale, "HH:mm:ss")
|
|
826
|
+
})
|
|
816
827
|
})
|
|
817
828
|
}), !action && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
818
829
|
className: "invoice-description",
|
package/lib/libs/util.js
CHANGED
|
@@ -1510,6 +1510,7 @@ function truncateText(text, maxLength, useWidth = false) {
|
|
|
1510
1510
|
return `${truncated}...`;
|
|
1511
1511
|
}
|
|
1512
1512
|
function getCustomerAvatar(did, updated_at, imageSize = 48) {
|
|
1513
|
+
if (!did) return "";
|
|
1513
1514
|
const updated = typeof updated_at === "number" ? updated_at : (0, _dayjs.default)(updated_at).unix();
|
|
1514
1515
|
return `/.well-known/service/user/avatar/${did}?imageFilter=resize&w=${imageSize}&h=${imageSize}&updateAt=${updated || (0, _dayjs.default)().unix()}`;
|
|
1515
1516
|
}
|
package/lib/locales/en.js
CHANGED
package/lib/locales/zh.js
CHANGED
|
@@ -14,6 +14,7 @@ module.exports = (0, _flat.default)({
|
|
|
14
14
|
none: "\u65E0",
|
|
15
15
|
createdAt: "\u521B\u5EFA\u65F6\u95F4",
|
|
16
16
|
updatedAt: "\u66F4\u65B0\u65F6\u95F4",
|
|
17
|
+
billingPeriod: "\u8D26\u5355\u5468\u671F",
|
|
17
18
|
resumesAt: "\u6062\u590D\u65F6\u95F4",
|
|
18
19
|
actions: "\u64CD\u4F5C",
|
|
19
20
|
options: "\u9009\u9879",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/payment-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "Reusable react components for payment kit v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -54,19 +54,19 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@arcblock/bridge": "^3.5.
|
|
58
|
-
"@arcblock/did-connect-react": "^3.5.
|
|
59
|
-
"@arcblock/react-hooks": "^3.5.
|
|
60
|
-
"@arcblock/ux": "^3.5.
|
|
61
|
-
"@arcblock/ws": "^1.
|
|
62
|
-
"@blocklet/payment-react-headless": "1.
|
|
63
|
-
"@blocklet/theme": "^3.5.
|
|
64
|
-
"@blocklet/ui-react": "^3.5.
|
|
57
|
+
"@arcblock/bridge": "^3.5.2",
|
|
58
|
+
"@arcblock/did-connect-react": "^3.5.2",
|
|
59
|
+
"@arcblock/react-hooks": "^3.5.2",
|
|
60
|
+
"@arcblock/ux": "^3.5.2",
|
|
61
|
+
"@arcblock/ws": "^1.30.9",
|
|
62
|
+
"@blocklet/payment-react-headless": "1.29.0",
|
|
63
|
+
"@blocklet/theme": "^3.5.2",
|
|
64
|
+
"@blocklet/ui-react": "^3.5.2",
|
|
65
65
|
"@mui/icons-material": "^7.1.2",
|
|
66
66
|
"@mui/lab": "7.0.0-beta.14",
|
|
67
67
|
"@mui/material": "^7.1.2",
|
|
68
68
|
"@mui/system": "^7.1.1",
|
|
69
|
-
"@ocap/util": "^1.
|
|
69
|
+
"@ocap/util": "^1.30.9",
|
|
70
70
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
71
71
|
"@stripe/stripe-js": "^2.4.0",
|
|
72
72
|
"@vitejs/plugin-legacy": "^7.0.0",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"@babel/core": "^7.27.4",
|
|
98
98
|
"@babel/preset-env": "^7.27.2",
|
|
99
99
|
"@babel/preset-react": "^7.27.1",
|
|
100
|
-
"@blocklet/payment-types": "1.
|
|
100
|
+
"@blocklet/payment-types": "1.29.0",
|
|
101
101
|
"@storybook/addon-essentials": "^7.6.20",
|
|
102
102
|
"@storybook/addon-interactions": "^7.6.20",
|
|
103
103
|
"@storybook/addon-links": "^7.6.20",
|
|
@@ -128,5 +128,5 @@
|
|
|
128
128
|
"vite-plugin-babel": "^1.3.1",
|
|
129
129
|
"vite-plugin-node-polyfills": "^0.23.0"
|
|
130
130
|
},
|
|
131
|
-
"gitHead": "
|
|
131
|
+
"gitHead": "02334964fbf505ea2fd27081039542d1f9868d57"
|
|
132
132
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import AddIcon from '@mui/icons-material/Add';
|
|
3
3
|
import CheckIcon from '@mui/icons-material/Check';
|
|
4
4
|
import LocalOfferIcon from '@mui/icons-material/LocalOffer';
|
|
@@ -182,6 +182,24 @@ export default function ProductItemCard({
|
|
|
182
182
|
if (!isEditing) setQtyInput(String(quantity));
|
|
183
183
|
}, [quantity, isEditing]);
|
|
184
184
|
|
|
185
|
+
// Debounce quantity API calls — UI updates immediately, API fires after 400ms idle
|
|
186
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
187
|
+
const debouncedQuantityChange = useCallback(
|
|
188
|
+
(priceId: string, qty: number) => {
|
|
189
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
190
|
+
debounceRef.current = setTimeout(() => {
|
|
191
|
+
onQuantityChange(priceId, qty);
|
|
192
|
+
}, 400);
|
|
193
|
+
},
|
|
194
|
+
[onQuantityChange]
|
|
195
|
+
);
|
|
196
|
+
useEffect(
|
|
197
|
+
() => () => {
|
|
198
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
199
|
+
},
|
|
200
|
+
[]
|
|
201
|
+
);
|
|
202
|
+
|
|
185
203
|
// Upsell info
|
|
186
204
|
const canUpsell = !!(item.price as any)?.upsell?.upsells_to;
|
|
187
205
|
const isUpselled = !!(item as any).upsell_price;
|
|
@@ -494,8 +512,14 @@ export default function ProductItemCard({
|
|
|
494
512
|
</Typography>
|
|
495
513
|
<IconButton
|
|
496
514
|
size="small"
|
|
497
|
-
onClick={() =>
|
|
498
|
-
|
|
515
|
+
onClick={() => {
|
|
516
|
+
const cur = parseInt(qtyInput, 10) || quantity;
|
|
517
|
+
const newQty = cur - 1;
|
|
518
|
+
if (newQty < min) return;
|
|
519
|
+
setQtyInput(String(newQty));
|
|
520
|
+
debouncedQuantityChange(item.price_id, newQty);
|
|
521
|
+
}}
|
|
522
|
+
disabled={(parseInt(qtyInput, 10) || quantity) <= min}
|
|
499
523
|
sx={{
|
|
500
524
|
width: 36,
|
|
501
525
|
height: 36,
|
|
@@ -556,8 +580,14 @@ export default function ProductItemCard({
|
|
|
556
580
|
/>
|
|
557
581
|
<IconButton
|
|
558
582
|
size="small"
|
|
559
|
-
onClick={() =>
|
|
560
|
-
|
|
583
|
+
onClick={() => {
|
|
584
|
+
const cur = parseInt(qtyInput, 10) || quantity;
|
|
585
|
+
const newQty = cur + 1;
|
|
586
|
+
if (max && newQty > max) return;
|
|
587
|
+
setQtyInput(String(newQty));
|
|
588
|
+
debouncedQuantityChange(item.price_id, newQty);
|
|
589
|
+
}}
|
|
590
|
+
disabled={max ? (parseInt(qtyInput, 10) || quantity) >= max : false}
|
|
561
591
|
sx={{
|
|
562
592
|
width: 36,
|
|
563
593
|
height: 36,
|
|
@@ -204,7 +204,16 @@ export default function CreditTopupPanel() {
|
|
|
204
204
|
const maxCredits = maxQuantity * step;
|
|
205
205
|
const minCredits = minQuantity * step;
|
|
206
206
|
|
|
207
|
-
//
|
|
207
|
+
// Debounce API calls — UI updates immediately, API fires after 400ms idle
|
|
208
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
209
|
+
useEffect(
|
|
210
|
+
() => () => {
|
|
211
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
212
|
+
},
|
|
213
|
+
[]
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Commit desired credits → compute packs → update API (debounced)
|
|
208
217
|
const commitCredits = useCallback(
|
|
209
218
|
(credits: number) => {
|
|
210
219
|
if (credits <= 0) return;
|
|
@@ -214,7 +223,12 @@ export default function CreditTopupPanel() {
|
|
|
214
223
|
const clampedPacks = Math.max(minQuantity, Math.min(maxQuantity, packs));
|
|
215
224
|
setLocalQty(clampedPacks);
|
|
216
225
|
setDesiredCredits(clamped);
|
|
217
|
-
if (item)
|
|
226
|
+
if (item) {
|
|
227
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
228
|
+
debounceRef.current = setTimeout(() => {
|
|
229
|
+
lineItems.updateQuantity(item.price_id, clampedPacks);
|
|
230
|
+
}, 400);
|
|
231
|
+
}
|
|
218
232
|
},
|
|
219
233
|
[step, minQuantity, maxQuantity, minCredits, maxCredits, item, lineItems]
|
|
220
234
|
);
|
|
@@ -151,9 +151,18 @@ export default function PaymentPanel() {
|
|
|
151
151
|
|
|
152
152
|
// DID Connect
|
|
153
153
|
const didConnectOpenedRef = useRef(false);
|
|
154
|
+
const didConnectSucceededRef = useRef(false);
|
|
155
|
+
// Track latest submit.status for use inside DID Connect SDK callbacks (onClose/onError),
|
|
156
|
+
// which fire asynchronously and may run after status has already advanced past waiting_did.
|
|
157
|
+
// Reading submit.status directly would capture a stale closure.
|
|
158
|
+
const submitStatusRef = useRef(submit.status);
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
submitStatusRef.current = submit.status;
|
|
161
|
+
}, [submit.status]);
|
|
154
162
|
useEffect(() => {
|
|
155
163
|
if (submit.status !== 'waiting_did') {
|
|
156
164
|
didConnectOpenedRef.current = false;
|
|
165
|
+
didConnectSucceededRef.current = false;
|
|
157
166
|
return;
|
|
158
167
|
}
|
|
159
168
|
const ctx = submit.context;
|
|
@@ -161,22 +170,36 @@ export default function PaymentPanel() {
|
|
|
161
170
|
if (didConnectOpenedRef.current) return;
|
|
162
171
|
didConnectOpenedRef.current = true;
|
|
163
172
|
|
|
164
|
-
|
|
173
|
+
// Use absolute URL for DID Connect prefix — DID Connect SDK needs full URL
|
|
174
|
+
// for status polling. paymentKitPrefix may be relative ('/' or ''), which can
|
|
175
|
+
// cause polling to fail. getPrefix() from util.ts returns the full origin URL.
|
|
176
|
+
const didPrefix = `${paymentKitPrefix || window.location.origin}/api/did`.replace(/([^:])\/\//g, '$1/');
|
|
165
177
|
connect.open({
|
|
166
178
|
locale: locale as any,
|
|
167
179
|
action: ctx.action,
|
|
168
180
|
prefix: didPrefix,
|
|
169
181
|
saveConnect: false,
|
|
170
182
|
extraParams: ctx.extraParams,
|
|
171
|
-
onSuccess: () => {
|
|
183
|
+
onSuccess: async () => {
|
|
184
|
+
didConnectSucceededRef.current = true;
|
|
172
185
|
connect.close();
|
|
186
|
+
await submit.didConnectComplete();
|
|
173
187
|
},
|
|
174
188
|
onClose: () => {
|
|
175
189
|
connect.close();
|
|
176
|
-
submit.
|
|
190
|
+
// If submit has already advanced past waiting_did (e.g. didConnectComplete is
|
|
191
|
+
// polling, or completion/failure resolved), the modal close is a follow-up to a
|
|
192
|
+
// successful flow — do not reset. Without this guard, the effect re-run after
|
|
193
|
+
// setStatus('submitting') clears didConnectSucceededRef, and a late onClose call
|
|
194
|
+
// would incorrectly tear down the in-flight polling.
|
|
195
|
+
if (submitStatusRef.current !== 'waiting_did') return;
|
|
196
|
+
if (!didConnectSucceededRef.current) {
|
|
197
|
+
submit.reset();
|
|
198
|
+
}
|
|
177
199
|
},
|
|
178
200
|
onError: (err: any) => {
|
|
179
201
|
console.error('DID Connect error:', err);
|
|
202
|
+
if (submitStatusRef.current !== 'waiting_did') return;
|
|
180
203
|
submit.reset();
|
|
181
204
|
},
|
|
182
205
|
messages: {
|
|
@@ -185,6 +208,8 @@ export default function PaymentPanel() {
|
|
|
185
208
|
confirm: t('payment.checkout.connectModal.confirm'),
|
|
186
209
|
} as any,
|
|
187
210
|
});
|
|
211
|
+
|
|
212
|
+
// No cleanup needed — connect modal manages its own lifecycle.
|
|
188
213
|
}, [submit.status, submit.context, connect, locale, t, buttonLabel]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
189
214
|
|
|
190
215
|
// Active payment type
|
|
@@ -471,7 +471,7 @@ function VendorProgressPanel({
|
|
|
471
471
|
borderColor: 'divider',
|
|
472
472
|
animation: `${fadeUp} 0.5s ease 0.3s both`,
|
|
473
473
|
}}>
|
|
474
|
-
{vendorStatus.vendors.map((vendor, idx) => (
|
|
474
|
+
{(vendorStatus.vendors || []).map((vendor, idx) => (
|
|
475
475
|
<VendorProgressItemV2 key={vendor.title || `vendor-${idx}`} vendor={vendor} t={t} />
|
|
476
476
|
))}
|
|
477
477
|
{vendorStatus.hasFailed && (
|
|
@@ -510,15 +510,21 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
|
|
|
510
510
|
options: {
|
|
511
511
|
customBodyRenderLite: (val: string, index: number) => {
|
|
512
512
|
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
513
|
+
const periodTooltip =
|
|
514
|
+
invoice.subscription_id && invoice.period_start && invoice.period_end
|
|
515
|
+
? `${t('common.billingPeriod')}: ${formatToDate(invoice.period_start * 1000, locale, 'YYYY-MM-DD HH:mm')} ~ ${formatToDate(invoice.period_end * 1000, locale, 'YYYY-MM-DD HH:mm')}`
|
|
516
|
+
: '';
|
|
513
517
|
|
|
514
518
|
return (
|
|
515
|
-
<
|
|
516
|
-
{
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
519
|
+
<Tooltip title={periodTooltip} arrow placement="top-start">
|
|
520
|
+
<Box onClick={(e) => handleLinkClick(e, invoice)} sx={linkStyle}>
|
|
521
|
+
{formatToDate(
|
|
522
|
+
invoice.created_at,
|
|
523
|
+
locale,
|
|
524
|
+
relatedSubscription ? 'YYYY-MM-DD HH:mm' : 'YYYY-MM-DD HH:mm:ss'
|
|
525
|
+
)}
|
|
526
|
+
</Box>
|
|
527
|
+
</Tooltip>
|
|
522
528
|
);
|
|
523
529
|
},
|
|
524
530
|
},
|
|
@@ -825,13 +831,22 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
|
|
|
825
831
|
</Typography>
|
|
826
832
|
)}
|
|
827
833
|
</Box>
|
|
828
|
-
<
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
834
|
+
<Tooltip
|
|
835
|
+
title={
|
|
836
|
+
invoice.subscription_id && invoice.period_start && invoice.period_end
|
|
837
|
+
? `${t('common.billingPeriod')}: ${formatToDate(invoice.period_start * 1000, locale, 'YYYY-MM-DD HH:mm')} ~ ${formatToDate(invoice.period_end * 1000, locale, 'YYYY-MM-DD HH:mm')}`
|
|
838
|
+
: ''
|
|
839
|
+
}
|
|
840
|
+
arrow
|
|
841
|
+
placement="top-start">
|
|
842
|
+
<Box
|
|
843
|
+
sx={{
|
|
844
|
+
flex: 1,
|
|
845
|
+
textAlign: 'right',
|
|
846
|
+
}}>
|
|
847
|
+
<Typography>{formatToDate(invoice.created_at, locale, 'HH:mm:ss')}</Typography>
|
|
848
|
+
</Box>
|
|
849
|
+
</Tooltip>
|
|
835
850
|
{!action && (
|
|
836
851
|
<Box
|
|
837
852
|
className="invoice-description"
|
|
@@ -15,7 +15,7 @@ const getSocketHost = () => new URL(window.location.href).host;
|
|
|
15
15
|
* @return {*}
|
|
16
16
|
*/
|
|
17
17
|
export function useSubscription(channel: string) {
|
|
18
|
-
const socket = useRef<typeof WsClient>(null);
|
|
18
|
+
const socket = useRef<InstanceType<typeof WsClient> | null>(null);
|
|
19
19
|
const subscription = useRef<any>(null);
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
package/src/libs/util.ts
CHANGED
|
@@ -1654,6 +1654,7 @@ export function getCustomerAvatar(
|
|
|
1654
1654
|
updated_at: string | number | undefined,
|
|
1655
1655
|
imageSize: number = 48
|
|
1656
1656
|
): string {
|
|
1657
|
+
if (!did) return '';
|
|
1657
1658
|
const updated = typeof updated_at === 'number' ? updated_at : dayjs(updated_at).unix();
|
|
1658
1659
|
return `/.well-known/service/user/avatar/${did}?imageFilter=resize&w=${imageSize}&h=${imageSize}&updateAt=${updated || dayjs().unix()}`;
|
|
1659
1660
|
}
|
package/src/locales/en.tsx
CHANGED