@blocklet/payment-react 1.20.8 → 1.20.10

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/libs/util.js CHANGED
@@ -311,6 +311,8 @@ export function getPayoutStatusColor(status) {
311
311
  switch (status) {
312
312
  case "paid":
313
313
  return "success";
314
+ case "deferred":
315
+ return "warning";
314
316
  case "failed":
315
317
  return "warning";
316
318
  case "canceled":
package/es/locales/en.js CHANGED
@@ -184,7 +184,7 @@ export default flat({
184
184
  progress: "Progress {progress}%",
185
185
  delivered: "Installation completed",
186
186
  failed: "Processing failed",
187
- pending: "Waiting for processing"
187
+ failedMsg: "An exception occurred during installation. We will automatically process a refund for you. We apologize for the inconvenience caused. Thank you for your understanding!"
188
188
  },
189
189
  confirm: {
190
190
  withStake: "By confirming, you allow {payee} to charge your account for future payments and, if necessary, slash your stake. You can cancel your subscription or withdraw your stake at any time.",
package/es/locales/zh.js CHANGED
@@ -184,7 +184,7 @@ export default flat({
184
184
  progress: "\u8FDB\u5EA6 {progress}%",
185
185
  delivered: "\u5B89\u88C5\u6210\u529F",
186
186
  failed: "\u5B89\u88C5\u5931\u8D25",
187
- pending: "\u51C6\u5907\u5B89\u88C5"
187
+ failedMsg: "\u5F88\u62B1\u6B49\uFF0C\u5B89\u88C5\u8FC7\u7A0B\u4E2D\u9047\u5230\u4E86\u4E00\u4E9B\u95EE\u9898\u3002\u6211\u4EEC\u6B63\u5728\u5904\u7406\u8FD9\u4E2A\u95EE\u9898\uFF0C\u7A0D\u540E\u4F1A\u81EA\u52A8\u4E3A\u60A8\u9000\u6B3E\u3002\u611F\u8C22\u60A8\u7684\u8010\u5FC3\u7B49\u5F85\uFF01"
188
188
  },
189
189
  confirm: {
190
190
  withStake: "\u786E\u8BA4\u8BA2\u9605\uFF0C\u5373\u8868\u793A\u60A8\u6388\u6743 {payee} \u4ECE\u60A8\u7684\u8D26\u6237\u6263\u53D6\u672A\u6765\u6B3E\u9879\uFF0C\u5E76\u5728\u5FC5\u8981\u65F6\u7F5A\u6CA1\u8D28\u62BC\u3002\u60A8\u53EF\u968F\u65F6\u53D6\u6D88\u8BA2\u9605\u6216\u64A4\u9500\u8D28\u62BC\u3002",
@@ -211,6 +211,7 @@ function PaymentInner({
211
211
  trialInDays = 0;
212
212
  trialEnd = 0;
213
213
  }
214
+ const showFeatures = `${paymentLink?.metadata?.show_product_features}` === "true";
214
215
  return /* @__PURE__ */ jsx(FormProvider, { ...methods, children: /* @__PURE__ */ jsxs(Stack, { className: "cko-container", sx: { gap: { sm: mode === "standalone" ? 0 : mode === "inline" ? 4 : 8 } }, children: [
215
216
  !hideSummaryCard && /* @__PURE__ */ jsx(Fade, { in: true, children: /* @__PURE__ */ jsxs(Stack, { className: "base-card cko-overview", direction: "column", children: [
216
217
  /* @__PURE__ */ jsx(
@@ -236,7 +237,8 @@ function PaymentInner({
236
237
  crossSellBehavior: state.checkoutSession.cross_sell_behavior,
237
238
  donationSettings: paymentLink?.donation_settings,
238
239
  action,
239
- completed
240
+ completed,
241
+ showFeatures
240
242
  }
241
243
  ),
242
244
  mode === "standalone" && !isMobile && /* @__PURE__ */ jsx(CheckoutFooter, { className: "cko-footer", sx: { color: "text.lighter" } })
@@ -17,6 +17,7 @@ type Props = {
17
17
  };
18
18
  onQuantityChange?: (itemId: string, quantity: number) => void;
19
19
  completed?: boolean;
20
+ showFeatures?: boolean;
20
21
  };
21
- export default function ProductItem({ item, items, trialInDays, trialEnd, currency, mode, children, onUpsell, onDownsell, completed, adjustableQuantity, onQuantityChange, }: Props): JSX.Element;
22
+ export default function ProductItem({ item, items, trialInDays, trialEnd, currency, mode, children, onUpsell, onDownsell, completed, adjustableQuantity, onQuantityChange, showFeatures, }: Props): JSX.Element;
22
23
  export {};
@@ -30,7 +30,8 @@ export default function ProductItem({
30
30
  completed = false,
31
31
  adjustableQuantity = { enabled: false },
32
32
  onQuantityChange = () => {
33
- }
33
+ },
34
+ showFeatures = false
34
35
  }) {
35
36
  const { t, locale } = useLocaleContext();
36
37
  const { settings, setPayable } = usePaymentContext();
@@ -114,6 +115,7 @@ export default function ProductItem({
114
115
  return pricing.primary;
115
116
  }, [trialInDays, trialEnd, pricing, item, locale]);
116
117
  const quantityInventoryError = formatQuantityInventory(item.price, localQuantityNum, locale);
118
+ const features = item.price.product?.features || [];
117
119
  return /* @__PURE__ */ jsxs(
118
120
  Stack,
119
121
  {
@@ -181,6 +183,66 @@ export default function ProductItem({
181
183
  ]
182
184
  }
183
185
  ),
186
+ showFeatures && features.length > 0 && /* @__PURE__ */ jsx(
187
+ Box,
188
+ {
189
+ sx: {
190
+ display: "flex",
191
+ alignItems: "center",
192
+ justifyContent: "flex-start",
193
+ flexWrap: "wrap",
194
+ gap: 1.5,
195
+ width: "100%",
196
+ mt: 1.5
197
+ },
198
+ children: features.map((feature) => /* @__PURE__ */ jsxs(
199
+ Box,
200
+ {
201
+ sx: {
202
+ display: "flex",
203
+ alignItems: "center",
204
+ justifyContent: "center",
205
+ gap: 0.5,
206
+ px: 1,
207
+ py: 0.5,
208
+ bgcolor: "action.hover",
209
+ borderRadius: 1,
210
+ border: "1px solid",
211
+ borderColor: "divider"
212
+ },
213
+ children: [
214
+ /* @__PURE__ */ jsx(
215
+ Box,
216
+ {
217
+ sx: {
218
+ bgcolor: "success.main",
219
+ width: 4,
220
+ height: 4,
221
+ borderRadius: "50%"
222
+ }
223
+ }
224
+ ),
225
+ /* @__PURE__ */ jsx(
226
+ Typography,
227
+ {
228
+ component: "span",
229
+ variant: "caption",
230
+ sx: {
231
+ color: "text.primary",
232
+ fontSize: "0.75rem",
233
+ fontWeight: 500,
234
+ textTransform: "capitalize",
235
+ whiteSpace: "nowrap"
236
+ },
237
+ children: feature.name
238
+ }
239
+ )
240
+ ]
241
+ },
242
+ feature.name
243
+ ))
244
+ }
245
+ ),
184
246
  quantityInventoryError ? /* @__PURE__ */ jsx(
185
247
  Status,
186
248
  {
@@ -1,5 +1,7 @@
1
1
  interface VendorStatus {
2
2
  success: boolean;
3
+ name?: string;
4
+ key?: string;
3
5
  status: 'delivered' | 'pending' | 'failed';
4
6
  progress: number;
5
7
  message: string;
@@ -50,11 +50,37 @@ export function VendorProgressItem({ vendor }) {
50
50
  }
51
51
  };
52
52
  }, [startAnimation]);
53
- const isCompleted = displayProgress >= 80;
53
+ const isCompleted = displayProgress >= 100;
54
+ const isFailed = vendor.status === "failed";
55
+ const nameText = vendor.name || vendor.title;
56
+ if (isFailed) {
57
+ return /* @__PURE__ */ jsxs(Box, { sx: { mb: 2 }, children: [
58
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }, children: [
59
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.secondary" }, children: nameText ? `${nameText} - ${t("payment.checkout.vendor.failed")}` : t("payment.checkout.vendor.failed") }),
60
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "error.main", fontWeight: 500 }, children: t("payment.checkout.vendor.progress", { progress: 0 }) })
61
+ ] }),
62
+ /* @__PURE__ */ jsx(
63
+ LinearProgress,
64
+ {
65
+ variant: "determinate",
66
+ value: 100,
67
+ sx: {
68
+ height: 8,
69
+ borderRadius: 4,
70
+ backgroundColor: "grey.200",
71
+ "& .MuiLinearProgress-bar": {
72
+ borderRadius: 4,
73
+ backgroundColor: "error.main"
74
+ }
75
+ }
76
+ }
77
+ )
78
+ ] });
79
+ }
54
80
  const statusText = isCompleted ? t("payment.checkout.vendor.delivered") : t("payment.checkout.vendor.processing");
55
81
  return /* @__PURE__ */ jsxs(Box, { sx: { mb: 2 }, children: [
56
82
  /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }, children: [
57
- /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.secondary" }, children: vendor.title ? `${vendor.title} - ${statusText}` : statusText }),
83
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.secondary" }, children: nameText ? `${nameText} - ${statusText}` : statusText }),
58
84
  /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.secondary", fontWeight: 500 }, children: t("payment.checkout.vendor.progress", { progress: displayProgress }) })
59
85
  ] }),
60
86
  /* @__PURE__ */ jsx(
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
3
3
  import { Box, Grow, Link, Paper, Stack, styled, Typography } from "@mui/material";
4
- import { useEffect, useState } from "react";
4
+ import { useEffect, useRef, useState } from "react";
5
5
  import { joinURL } from "ufo";
6
6
  import { Button } from "@arcblock/ux";
7
7
  import { usePaymentContext } from "../contexts/payment.js";
@@ -22,17 +22,24 @@ export default function PaymentSuccess({
22
22
  const { prefix, api } = usePaymentContext();
23
23
  const [vendorStatus, setVendorStatus] = useState(null);
24
24
  const [isAllCompleted, setIsAllCompleted] = useState(false);
25
+ const [hasFailed, setHasFailed] = useState(false);
26
+ const timerRef = useRef(Date.now());
25
27
  let next = null;
26
28
  useEffect(() => {
27
29
  if (!hasVendor || !sessionId) return void 0;
28
30
  const fetchVendorStatus = async (interval2) => {
29
31
  try {
30
32
  const response = await api.get(joinURL(prefix, `/api/vendors/order/${sessionId}/status`), {});
31
- const allCompleted = response.data?.vendors?.every((vendor) => vendor.progress >= 80);
32
- if (allCompleted) {
33
+ const needCheckError = Date.now() - timerRef.current > 6 * 1e3;
34
+ const allCompleted = response.data?.vendors?.every((vendor) => vendor.progress >= 100);
35
+ const hasAnyFailed = response.data?.vendors?.some(
36
+ (vendor) => vendor.status === "failed" || needCheckError && !!vendor.error
37
+ );
38
+ if (hasAnyFailed || allCompleted) {
33
39
  clearInterval(interval2);
34
40
  }
35
- setIsAllCompleted(allCompleted);
41
+ setHasFailed(hasAnyFailed);
42
+ setIsAllCompleted(!hasAnyFailed && allCompleted);
36
43
  setVendorStatus(response.data);
37
44
  } catch (error) {
38
45
  console.error("Failed to fetch vendor status:", error);
@@ -62,6 +69,7 @@ export default function PaymentSuccess({
62
69
  },
63
70
  children: [
64
71
  vendorStatus.vendors?.map((vendor, index) => /* @__PURE__ */ jsx(VendorProgressItem, { vendor }, vendor.title || `vendor-${index}`)),
72
+ hasFailed ? /* @__PURE__ */ jsx(Typography, { variant: "h6", sx: { color: "warning.main", mb: 1 }, children: t("payment.checkout.vendor.failedMsg") }) : null,
65
73
  pageInfo?.success_message?.[locale] && isAllCompleted ? /* @__PURE__ */ jsx(Typography, { variant: "h6", sx: { color: "text.primary", mb: 1 }, children: pageInfo?.success_message?.[locale] }) : null
66
74
  ]
67
75
  }
@@ -17,6 +17,7 @@ type Props = {
17
17
  donationSettings?: DonationSettings;
18
18
  action?: string;
19
19
  completed?: boolean;
20
+ showFeatures?: boolean;
20
21
  };
21
- export default function PaymentSummary({ items, currency, trialInDays, billingThreshold, onUpsell, onDownsell, onQuantityChange, onApplyCrossSell, onCancelCrossSell, onChangeAmount, checkoutSessionId, crossSellBehavior, showStaking, donationSettings, action, trialEnd, completed, ...rest }: Props): import("react").JSX.Element;
22
+ export default function PaymentSummary({ items, currency, trialInDays, billingThreshold, onUpsell, onDownsell, onQuantityChange, onApplyCrossSell, onCancelCrossSell, onChangeAmount, checkoutSessionId, crossSellBehavior, showStaking, donationSettings, action, trialEnd, completed, showFeatures, ...rest }: Props): import("react").JSX.Element;
22
23
  export {};
@@ -84,6 +84,7 @@ export default function PaymentSummary({
84
84
  action = "",
85
85
  trialEnd = 0,
86
86
  completed = false,
87
+ showFeatures = false,
87
88
  ...rest
88
89
  }) {
89
90
  const { t, locale } = useLocaleContext();
@@ -174,6 +175,7 @@ export default function PaymentSummary({
174
175
  onDownsell: handleDownsell,
175
176
  adjustableQuantity: x.adjustable_quantity,
176
177
  completed,
178
+ showFeatures,
177
179
  onQuantityChange: handleQuantityChange,
178
180
  children: x.cross_sell && /* @__PURE__ */ jsxs(
179
181
  Stack,
package/lib/libs/util.js CHANGED
@@ -410,6 +410,8 @@ function getPayoutStatusColor(status) {
410
410
  switch (status) {
411
411
  case "paid":
412
412
  return "success";
413
+ case "deferred":
414
+ return "warning";
413
415
  case "failed":
414
416
  return "warning";
415
417
  case "canceled":
package/lib/locales/en.js CHANGED
@@ -191,7 +191,7 @@ module.exports = (0, _flat.default)({
191
191
  progress: "Progress {progress}%",
192
192
  delivered: "Installation completed",
193
193
  failed: "Processing failed",
194
- pending: "Waiting for processing"
194
+ failedMsg: "An exception occurred during installation. We will automatically process a refund for you. We apologize for the inconvenience caused. Thank you for your understanding!"
195
195
  },
196
196
  confirm: {
197
197
  withStake: "By confirming, you allow {payee} to charge your account for future payments and, if necessary, slash your stake. You can cancel your subscription or withdraw your stake at any time.",
package/lib/locales/zh.js CHANGED
@@ -191,7 +191,7 @@ module.exports = (0, _flat.default)({
191
191
  progress: "\u8FDB\u5EA6 {progress}%",
192
192
  delivered: "\u5B89\u88C5\u6210\u529F",
193
193
  failed: "\u5B89\u88C5\u5931\u8D25",
194
- pending: "\u51C6\u5907\u5B89\u88C5"
194
+ failedMsg: "\u5F88\u62B1\u6B49\uFF0C\u5B89\u88C5\u8FC7\u7A0B\u4E2D\u9047\u5230\u4E86\u4E00\u4E9B\u95EE\u9898\u3002\u6211\u4EEC\u6B63\u5728\u5904\u7406\u8FD9\u4E2A\u95EE\u9898\uFF0C\u7A0D\u540E\u4F1A\u81EA\u52A8\u4E3A\u60A8\u9000\u6B3E\u3002\u611F\u8C22\u60A8\u7684\u8010\u5FC3\u7B49\u5F85\uFF01"
195
195
  },
196
196
  confirm: {
197
197
  withStake: "\u786E\u8BA4\u8BA2\u9605\uFF0C\u5373\u8868\u793A\u60A8\u6388\u6743 {payee} \u4ECE\u60A8\u7684\u8D26\u6237\u6263\u53D6\u672A\u6765\u6B3E\u9879\uFF0C\u5E76\u5728\u5FC5\u8981\u65F6\u7F5A\u6CA1\u8D28\u62BC\u3002\u60A8\u53EF\u968F\u65F6\u53D6\u6D88\u8BA2\u9605\u6216\u64A4\u9500\u8D28\u62BC\u3002",
@@ -257,6 +257,7 @@ function PaymentInner({
257
257
  trialInDays = 0;
258
258
  trialEnd = 0;
259
259
  }
260
+ const showFeatures = `${paymentLink?.metadata?.show_product_features}` === "true";
260
261
  return /* @__PURE__ */(0, _jsxRuntime.jsx)(_reactHookForm.FormProvider, {
261
262
  ...methods,
262
263
  children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
@@ -290,7 +291,8 @@ function PaymentInner({
290
291
  crossSellBehavior: state.checkoutSession.cross_sell_behavior,
291
292
  donationSettings: paymentLink?.donation_settings,
292
293
  action,
293
- completed
294
+ completed,
295
+ showFeatures
294
296
  }), mode === "standalone" && !isMobile && /* @__PURE__ */(0, _jsxRuntime.jsx)(_footer.default, {
295
297
  className: "cko-footer",
296
298
  sx: {
@@ -17,6 +17,7 @@ type Props = {
17
17
  };
18
18
  onQuantityChange?: (itemId: string, quantity: number) => void;
19
19
  completed?: boolean;
20
+ showFeatures?: boolean;
20
21
  };
21
- export default function ProductItem({ item, items, trialInDays, trialEnd, currency, mode, children, onUpsell, onDownsell, completed, adjustableQuantity, onQuantityChange, }: Props): JSX.Element;
22
+ export default function ProductItem({ item, items, trialInDays, trialEnd, currency, mode, children, onUpsell, onDownsell, completed, adjustableQuantity, onQuantityChange, showFeatures, }: Props): JSX.Element;
22
23
  export {};
@@ -30,7 +30,8 @@ function ProductItem({
30
30
  adjustableQuantity = {
31
31
  enabled: false
32
32
  },
33
- onQuantityChange = () => {}
33
+ onQuantityChange = () => {},
34
+ showFeatures = false
34
35
  }) {
35
36
  const {
36
37
  t,
@@ -123,6 +124,7 @@ function ProductItem({
123
124
  return pricing.primary;
124
125
  }, [trialInDays, trialEnd, pricing, item, locale]);
125
126
  const quantityInventoryError = (0, _util.formatQuantityInventory)(item.price, localQuantityNum, locale);
127
+ const features = item.price.product?.features || [];
126
128
  return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
127
129
  direction: "column",
128
130
  spacing: 1,
@@ -181,6 +183,49 @@ function ProductItem({
181
183
  children: pricing.secondary
182
184
  })]
183
185
  })]
186
+ }), showFeatures && features.length > 0 && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
187
+ sx: {
188
+ display: "flex",
189
+ alignItems: "center",
190
+ justifyContent: "flex-start",
191
+ flexWrap: "wrap",
192
+ gap: 1.5,
193
+ width: "100%",
194
+ mt: 1.5
195
+ },
196
+ children: features.map(feature => /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
197
+ sx: {
198
+ display: "flex",
199
+ alignItems: "center",
200
+ justifyContent: "center",
201
+ gap: 0.5,
202
+ px: 1,
203
+ py: 0.5,
204
+ bgcolor: "action.hover",
205
+ borderRadius: 1,
206
+ border: "1px solid",
207
+ borderColor: "divider"
208
+ },
209
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
210
+ sx: {
211
+ bgcolor: "success.main",
212
+ width: 4,
213
+ height: 4,
214
+ borderRadius: "50%"
215
+ }
216
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
217
+ component: "span",
218
+ variant: "caption",
219
+ sx: {
220
+ color: "text.primary",
221
+ fontSize: "0.75rem",
222
+ fontWeight: 500,
223
+ textTransform: "capitalize",
224
+ whiteSpace: "nowrap"
225
+ },
226
+ children: feature.name
227
+ })]
228
+ }, feature.name))
184
229
  }), quantityInventoryError ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_status.default, {
185
230
  label: quantityInventoryError,
186
231
  variant: "outlined",
@@ -1,5 +1,7 @@
1
1
  interface VendorStatus {
2
2
  success: boolean;
3
+ name?: string;
4
+ key?: string;
3
5
  status: 'delivered' | 'pending' | 'failed';
4
6
  progress: number;
5
7
  message: string;
@@ -60,7 +60,52 @@ function VendorProgressItem({
60
60
  }
61
61
  };
62
62
  }, [startAnimation]);
63
- const isCompleted = displayProgress >= 80;
63
+ const isCompleted = displayProgress >= 100;
64
+ const isFailed = vendor.status === "failed";
65
+ const nameText = vendor.name || vendor.title;
66
+ if (isFailed) {
67
+ return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
68
+ sx: {
69
+ mb: 2
70
+ },
71
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
72
+ sx: {
73
+ display: "flex",
74
+ justifyContent: "space-between",
75
+ alignItems: "center",
76
+ mb: 1
77
+ },
78
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
79
+ variant: "body2",
80
+ sx: {
81
+ color: "text.secondary"
82
+ },
83
+ children: nameText ? `${nameText} - ${t("payment.checkout.vendor.failed")}` : t("payment.checkout.vendor.failed")
84
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
85
+ variant: "body2",
86
+ sx: {
87
+ color: "error.main",
88
+ fontWeight: 500
89
+ },
90
+ children: t("payment.checkout.vendor.progress", {
91
+ progress: 0
92
+ })
93
+ })]
94
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.LinearProgress, {
95
+ variant: "determinate",
96
+ value: 100,
97
+ sx: {
98
+ height: 8,
99
+ borderRadius: 4,
100
+ backgroundColor: "grey.200",
101
+ "& .MuiLinearProgress-bar": {
102
+ borderRadius: 4,
103
+ backgroundColor: "error.main"
104
+ }
105
+ }
106
+ })]
107
+ });
108
+ }
64
109
  const statusText = isCompleted ? t("payment.checkout.vendor.delivered") : t("payment.checkout.vendor.processing");
65
110
  return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
66
111
  sx: {
@@ -78,7 +123,7 @@ function VendorProgressItem({
78
123
  sx: {
79
124
  color: "text.secondary"
80
125
  },
81
- children: vendor.title ? `${vendor.title} - ${statusText}` : statusText
126
+ children: nameText ? `${nameText} - ${statusText}` : statusText
82
127
  }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
83
128
  variant: "body2",
84
129
  sx: {
@@ -34,17 +34,22 @@ function PaymentSuccess({
34
34
  } = (0, _payment.usePaymentContext)();
35
35
  const [vendorStatus, setVendorStatus] = (0, _react.useState)(null);
36
36
  const [isAllCompleted, setIsAllCompleted] = (0, _react.useState)(false);
37
+ const [hasFailed, setHasFailed] = (0, _react.useState)(false);
38
+ const timerRef = (0, _react.useRef)(Date.now());
37
39
  let next = null;
38
40
  (0, _react.useEffect)(() => {
39
41
  if (!hasVendor || !sessionId) return void 0;
40
42
  const fetchVendorStatus = async interval2 => {
41
43
  try {
42
44
  const response = await api.get((0, _ufo.joinURL)(prefix, `/api/vendors/order/${sessionId}/status`), {});
43
- const allCompleted = response.data?.vendors?.every(vendor => vendor.progress >= 80);
44
- if (allCompleted) {
45
+ const needCheckError = Date.now() - timerRef.current > 6 * 1e3;
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);
48
+ if (hasAnyFailed || allCompleted) {
45
49
  clearInterval(interval2);
46
50
  }
47
- setIsAllCompleted(allCompleted);
51
+ setHasFailed(hasAnyFailed);
52
+ setIsAllCompleted(!hasAnyFailed && allCompleted);
48
53
  setVendorStatus(response.data);
49
54
  } catch (error) {
50
55
  console.error("Failed to fetch vendor status:", error);
@@ -72,7 +77,14 @@ function PaymentSuccess({
72
77
  },
73
78
  children: [vendorStatus.vendors?.map((vendor, index) => /* @__PURE__ */(0, _jsxRuntime.jsx)(_progressItem.VendorProgressItem, {
74
79
  vendor
75
- }, vendor.title || `vendor-${index}`)), pageInfo?.success_message?.[locale] && isAllCompleted ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
80
+ }, vendor.title || `vendor-${index}`)), hasFailed ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
81
+ variant: "h6",
82
+ sx: {
83
+ color: "warning.main",
84
+ mb: 1
85
+ },
86
+ children: t("payment.checkout.vendor.failedMsg")
87
+ }) : null, pageInfo?.success_message?.[locale] && isAllCompleted ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
76
88
  variant: "h6",
77
89
  sx: {
78
90
  color: "text.primary",
@@ -17,6 +17,7 @@ type Props = {
17
17
  donationSettings?: DonationSettings;
18
18
  action?: string;
19
19
  completed?: boolean;
20
+ showFeatures?: boolean;
20
21
  };
21
- export default function PaymentSummary({ items, currency, trialInDays, billingThreshold, onUpsell, onDownsell, onQuantityChange, onApplyCrossSell, onCancelCrossSell, onChangeAmount, checkoutSessionId, crossSellBehavior, showStaking, donationSettings, action, trialEnd, completed, ...rest }: Props): import("react").JSX.Element;
22
+ export default function PaymentSummary({ items, currency, trialInDays, billingThreshold, onUpsell, onDownsell, onQuantityChange, onApplyCrossSell, onCancelCrossSell, onChangeAmount, checkoutSessionId, crossSellBehavior, showStaking, donationSettings, action, trialEnd, completed, showFeatures, ...rest }: Props): import("react").JSX.Element;
22
23
  export {};
@@ -101,6 +101,7 @@ function PaymentSummary({
101
101
  action = "",
102
102
  trialEnd = 0,
103
103
  completed = false,
104
+ showFeatures = false,
104
105
  ...rest
105
106
  }) {
106
107
  const {
@@ -204,6 +205,7 @@ function PaymentSummary({
204
205
  onDownsell: handleDownsell,
205
206
  adjustableQuantity: x.adjustable_quantity,
206
207
  completed,
208
+ showFeatures,
207
209
  onQuantityChange: handleQuantityChange,
208
210
  children: x.cross_sell && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
209
211
  direction: "row",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.20.8",
3
+ "version": "1.20.10",
4
4
  "description": "Reusable react components for payment kit v2",
5
5
  "keywords": [
6
6
  "react",
@@ -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.8",
97
+ "@blocklet/payment-types": "1.20.10",
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": "804fbe22b7f12af2fc58134679161be52a464582"
128
+ "gitHead": "1659d63120ced92167ec8681e51db96801b910ec"
129
129
  }
package/src/libs/util.ts CHANGED
@@ -432,6 +432,8 @@ export function getPayoutStatusColor(status: string) {
432
432
  switch (status) {
433
433
  case 'paid':
434
434
  return 'success';
435
+ case 'deferred':
436
+ return 'warning';
435
437
  case 'failed':
436
438
  return 'warning';
437
439
  case 'canceled':
@@ -187,7 +187,8 @@ export default flat({
187
187
  progress: 'Progress {progress}%',
188
188
  delivered: 'Installation completed',
189
189
  failed: 'Processing failed',
190
- pending: 'Waiting for processing',
190
+ failedMsg:
191
+ 'An exception occurred during installation. We will automatically process a refund for you. We apologize for the inconvenience caused. Thank you for your understanding!',
191
192
  },
192
193
  confirm: {
193
194
  withStake:
@@ -186,7 +186,7 @@ export default flat({
186
186
  progress: '进度 {progress}%',
187
187
  delivered: '安装成功',
188
188
  failed: '安装失败',
189
- pending: '准备安装',
189
+ failedMsg: '很抱歉,安装过程中遇到了一些问题。我们正在处理这个问题,稍后会自动为您退款。感谢您的耐心等待!',
190
190
  },
191
191
  confirm: {
192
192
  withStake:
@@ -262,6 +262,8 @@ function PaymentInner({
262
262
  trialEnd = 0;
263
263
  }
264
264
 
265
+ const showFeatures = `${paymentLink?.metadata?.show_product_features}` === 'true';
266
+
265
267
  return (
266
268
  <FormProvider {...methods}>
267
269
  <Stack className="cko-container" sx={{ gap: { sm: mode === 'standalone' ? 0 : mode === 'inline' ? 4 : 8 } }}>
@@ -290,6 +292,7 @@ function PaymentInner({
290
292
  donationSettings={paymentLink?.donation_settings}
291
293
  action={action}
292
294
  completed={completed}
295
+ showFeatures={showFeatures}
293
296
  />
294
297
  {mode === 'standalone' && !isMobile && (
295
298
  <CheckoutFooter className="cko-footer" sx={{ color: 'text.lighter' }} />
@@ -37,6 +37,7 @@ type Props = {
37
37
  };
38
38
  onQuantityChange?: (itemId: string, quantity: number) => void;
39
39
  completed?: boolean;
40
+ showFeatures?: boolean;
40
41
  };
41
42
 
42
43
  export default function ProductItem({
@@ -52,6 +53,7 @@ export default function ProductItem({
52
53
  completed = false,
53
54
  adjustableQuantity = { enabled: false },
54
55
  onQuantityChange = () => {},
56
+ showFeatures = false,
55
57
  }: Props) {
56
58
  const { t, locale } = useLocaleContext();
57
59
  const { settings, setPayable } = usePaymentContext();
@@ -153,6 +155,7 @@ export default function ProductItem({
153
155
  }, [trialInDays, trialEnd, pricing, item, locale]);
154
156
 
155
157
  const quantityInventoryError = formatQuantityInventory(item.price, localQuantityNum, locale);
158
+ const features = item.price.product?.features || [];
156
159
 
157
160
  return (
158
161
  <Stack
@@ -182,7 +185,7 @@ export default function ProductItem({
182
185
  <ProductCard
183
186
  logo={item.price.product?.images[0]}
184
187
  name={item.price.product?.name}
185
- // description={item.price.product?.description}
188
+ // description={showDescription ? item.price.product?.description : undefined}
186
189
  extra={
187
190
  <Box
188
191
  sx={{
@@ -209,6 +212,56 @@ export default function ProductItem({
209
212
  )}
210
213
  </Stack>
211
214
  </Stack>
215
+ {showFeatures && features.length > 0 && (
216
+ <Box
217
+ sx={{
218
+ display: 'flex',
219
+ alignItems: 'center',
220
+ justifyContent: 'flex-start',
221
+ flexWrap: 'wrap',
222
+ gap: 1.5,
223
+ width: '100%',
224
+ mt: 1.5,
225
+ }}>
226
+ {features.map((feature) => (
227
+ <Box
228
+ key={feature.name}
229
+ sx={{
230
+ display: 'flex',
231
+ alignItems: 'center',
232
+ justifyContent: 'center',
233
+ gap: 0.5,
234
+ px: 1,
235
+ py: 0.5,
236
+ bgcolor: 'action.hover',
237
+ borderRadius: 1,
238
+ border: '1px solid',
239
+ borderColor: 'divider',
240
+ }}>
241
+ <Box
242
+ sx={{
243
+ bgcolor: 'success.main',
244
+ width: 4,
245
+ height: 4,
246
+ borderRadius: '50%',
247
+ }}
248
+ />
249
+ <Typography
250
+ component="span"
251
+ variant="caption"
252
+ sx={{
253
+ color: 'text.primary',
254
+ fontSize: '0.75rem',
255
+ fontWeight: 500,
256
+ textTransform: 'capitalize',
257
+ whiteSpace: 'nowrap',
258
+ }}>
259
+ {feature.name}
260
+ </Typography>
261
+ </Box>
262
+ ))}
263
+ </Box>
264
+ )}
212
265
  {quantityInventoryError ? (
213
266
  <Status
214
267
  label={quantityInventoryError}
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
4
4
 
5
5
  interface VendorStatus {
6
6
  success: boolean;
7
+ name?: string;
8
+ key?: string;
7
9
  status: 'delivered' | 'pending' | 'failed';
8
10
  progress: number;
9
11
  message: string;
@@ -75,14 +77,46 @@ export function VendorProgressItem({ vendor }: { vendor: VendorStatus }) {
75
77
  };
76
78
  }, [startAnimation]);
77
79
 
78
- const isCompleted = displayProgress >= 80;
79
- const statusText = isCompleted ? t('payment.checkout.vendor.delivered') : t('payment.checkout.vendor.processing');
80
+ const isCompleted = displayProgress >= 100;
81
+ const isFailed = vendor.status === 'failed';
82
+
83
+ const nameText = vendor.name || vendor.title;
84
+
85
+ // 如果是失败状态,显示错误 UI
86
+ if (isFailed) {
87
+ return (
88
+ <Box sx={{ mb: 2 }}>
89
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
90
+ <Typography variant="body2" sx={{ color: 'text.secondary' }}>
91
+ {nameText ? `${nameText} - ${t('payment.checkout.vendor.failed')}` : t('payment.checkout.vendor.failed')}
92
+ </Typography>
93
+ <Typography variant="body2" sx={{ color: 'error.main', fontWeight: 500 }}>
94
+ {t('payment.checkout.vendor.progress', { progress: 0 })}
95
+ </Typography>
96
+ </Box>
97
+ <LinearProgress
98
+ variant="determinate"
99
+ value={100}
100
+ sx={{
101
+ height: 8,
102
+ borderRadius: 4,
103
+ backgroundColor: 'grey.200',
104
+ '& .MuiLinearProgress-bar': {
105
+ borderRadius: 4,
106
+ backgroundColor: 'error.main',
107
+ },
108
+ }}
109
+ />
110
+ </Box>
111
+ );
112
+ }
80
113
 
114
+ const statusText = isCompleted ? t('payment.checkout.vendor.delivered') : t('payment.checkout.vendor.processing');
81
115
  return (
82
116
  <Box sx={{ mb: 2 }}>
83
117
  <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
84
118
  <Typography variant="body2" sx={{ color: 'text.secondary' }}>
85
- {vendor.title ? `${vendor.title} - ${statusText}` : statusText}
119
+ {nameText ? `${nameText} - ${statusText}` : statusText}
86
120
  </Typography>
87
121
  <Typography variant="body2" sx={{ color: 'text.secondary', fontWeight: 500 }}>
88
122
  {t('payment.checkout.vendor.progress', { progress: displayProgress })}
@@ -1,6 +1,6 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { Box, Grow, Link, Paper, Stack, styled, Typography } from '@mui/material';
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useRef, useState } from 'react';
4
4
  import { joinURL } from 'ufo';
5
5
 
6
6
  import { Button } from '@arcblock/ux';
@@ -52,6 +52,8 @@ export default function PaymentSuccess({
52
52
  const { prefix, api } = usePaymentContext();
53
53
  const [vendorStatus, setVendorStatus] = useState<VendorResponse | null>(null);
54
54
  const [isAllCompleted, setIsAllCompleted] = useState(false);
55
+ const [hasFailed, setHasFailed] = useState(false);
56
+ const timerRef = useRef(Date.now());
55
57
  let next: any = null;
56
58
 
57
59
  // Fetch vendor status when hasVendor is true
@@ -61,11 +63,18 @@ export default function PaymentSuccess({
61
63
  const fetchVendorStatus = async (interval?: NodeJS.Timeout) => {
62
64
  try {
63
65
  const response = await api.get(joinURL(prefix, `/api/vendors/order/${sessionId}/status`), {});
64
- const allCompleted = response.data?.vendors?.every((vendor: any) => vendor.progress >= 80);
65
- if (allCompleted) {
66
+ const needCheckError = Date.now() - timerRef.current > 6 * 1000;
67
+
68
+ const allCompleted = response.data?.vendors?.every((vendor: any) => vendor.progress >= 100);
69
+ const hasAnyFailed = response.data?.vendors?.some(
70
+ (vendor: any) => vendor.status === 'failed' || (needCheckError && !!vendor.error)
71
+ );
72
+ if (hasAnyFailed || allCompleted) {
66
73
  clearInterval(interval);
67
74
  }
68
- setIsAllCompleted(allCompleted);
75
+
76
+ setHasFailed(hasAnyFailed);
77
+ setIsAllCompleted(!hasAnyFailed && allCompleted);
69
78
  setVendorStatus(response.data);
70
79
  } catch (error) {
71
80
  console.error('Failed to fetch vendor status:', error);
@@ -102,6 +111,11 @@ export default function PaymentSuccess({
102
111
  {vendorStatus.vendors?.map((vendor, index) => (
103
112
  <VendorProgressItem key={vendor.title || `vendor-${index}`} vendor={vendor} />
104
113
  ))}
114
+ {hasFailed ? (
115
+ <Typography variant="h6" sx={{ color: 'warning.main', mb: 1 }}>
116
+ {t('payment.checkout.vendor.failedMsg')}
117
+ </Typography>
118
+ ) : null}
105
119
  {pageInfo?.success_message?.[locale] && isAllCompleted ? (
106
120
  <Typography variant="h6" sx={{ color: 'text.primary', mb: 1 }}>
107
121
  {pageInfo?.success_message?.[locale]}
@@ -71,6 +71,7 @@ type Props = {
71
71
  donationSettings?: DonationSettings; // only include backend part
72
72
  action?: string;
73
73
  completed?: boolean;
74
+ showFeatures?: boolean;
74
75
  };
75
76
 
76
77
  async function fetchCrossSell(id: string) {
@@ -138,6 +139,7 @@ export default function PaymentSummary({
138
139
  action = '',
139
140
  trialEnd = 0,
140
141
  completed = false,
142
+ showFeatures = false,
141
143
  ...rest
142
144
  }: Props) {
143
145
  const { t, locale } = useLocaleContext();
@@ -233,6 +235,7 @@ export default function PaymentSummary({
233
235
  onDownsell={handleDownsell}
234
236
  adjustableQuantity={x.adjustable_quantity}
235
237
  completed={completed}
238
+ showFeatures={showFeatures}
236
239
  onQuantityChange={handleQuantityChange}>
237
240
  {x.cross_sell && (
238
241
  <Stack