@blocklet/payment-react 1.13.301 → 1.13.302

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.
@@ -47,6 +47,9 @@ const fetchSupporters = (target, livemode = true) => {
47
47
  }
48
48
  return supporterCache[target];
49
49
  };
50
+ const emojiFont = {
51
+ fontFamily: 'Avenir, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'
52
+ };
50
53
  function SupporterAvatar({ supporters = [], totalAmount = "0", currency, method }) {
51
54
  const { t } = useLocaleContext();
52
55
  const customers = uniqBy(supporters, "customer_did");
@@ -68,7 +71,7 @@ function SupporterAvatar({ supporters = [], totalAmount = "0", currency, method
68
71
  sm: 1
69
72
  },
70
73
  children: [
71
- /* @__PURE__ */ jsx(Typography, { component: "p", color: "text.secondary", children: customersNum === 0 ? t("payment.checkout.donation.empty") : t("payment.checkout.donation.summary", {
74
+ /* @__PURE__ */ jsx(Typography, { component: "p", color: "text.secondary", children: customersNum === 0 ? /* @__PURE__ */ jsx("span", { style: emojiFont, children: t("payment.checkout.donation.empty") }) : t("payment.checkout.donation.summary", {
72
75
  total: customersNum,
73
76
  symbol: currency.symbol,
74
77
  totalAmount: formatAmount(totalAmount || "0", currency.decimal)
@@ -92,7 +95,7 @@ function SupporterTable({ supporters = [], totalAmount = "0", currency, method }
92
95
  const customers = uniqBy(supporters, "customer_did");
93
96
  const customersNum = customers.length;
94
97
  return /* @__PURE__ */ jsxs(Box, { display: "flex", flexDirection: "column", alignItems: "center", gap: { xs: 0.5, sm: 1 }, children: [
95
- /* @__PURE__ */ jsx(Typography, { component: "p", color: "text.secondary", children: customersNum === 0 ? t("payment.checkout.donation.empty") : t("payment.checkout.donation.summary", {
98
+ /* @__PURE__ */ jsx(Typography, { component: "p", color: "text.secondary", children: customersNum === 0 ? /* @__PURE__ */ jsx("span", { style: emojiFont, children: t("payment.checkout.donation.empty") }) : t("payment.checkout.donation.summary", {
96
99
  total: customersNum,
97
100
  symbol: currency.symbol,
98
101
  totalAmount: formatAmount(totalAmount || "0", currency.decimal)
@@ -150,7 +153,7 @@ function SupporterSimple({ supporters = [], totalAmount = "0", currency, method
150
153
  sm: 1
151
154
  },
152
155
  children: [
153
- /* @__PURE__ */ jsx(Typography, { component: "p", color: "text.secondary", children: customersNum === 0 ? t("payment.checkout.donation.empty") : t("payment.checkout.donation.summary", {
156
+ /* @__PURE__ */ jsx(Typography, { component: "p", color: "text.secondary", children: customersNum === 0 ? /* @__PURE__ */ jsx("span", { style: emojiFont, children: t("payment.checkout.donation.empty") }) : t("payment.checkout.donation.summary", {
154
157
  total: customersNum,
155
158
  symbol: currency.symbol,
156
159
  totalAmount: formatAmount(totalAmount || "0", currency.decimal)
package/es/libs/util.d.ts CHANGED
@@ -76,3 +76,4 @@ export declare function formatTotalPrice({ product, quantity, priceId, locale, }
76
76
  priceId?: string;
77
77
  locale: string;
78
78
  }): PricingRenderProps;
79
+ export declare function formatQuantityInventory(price: TPrice, quantity: string | number, locale?: string): string;
package/es/libs/util.js CHANGED
@@ -672,3 +672,18 @@ export function formatTotalPrice({
672
672
  totalAmount: unitValue.mul(new BN(quantity)).toString()
673
673
  };
674
674
  }
675
+ export function formatQuantityInventory(price, quantity, locale = "en") {
676
+ const q = Number(quantity);
677
+ const {
678
+ quantity_available: quantityAvailable = 0,
679
+ quantity_sold: quantitySold = 0,
680
+ quantity_limit_per_checkout: quantityLimitPerCheckout = 0
681
+ } = price || {};
682
+ if (quantityAvailable > 0 && quantitySold + q > quantityAvailable) {
683
+ return t("common.quantityNotEnough", locale);
684
+ }
685
+ if (quantityLimitPerCheckout > 0 && quantityLimitPerCheckout < q) {
686
+ return t("common.quantityLimitPerCheckout", locale);
687
+ }
688
+ return "";
689
+ }
package/es/locales/en.js CHANGED
@@ -82,7 +82,9 @@ export default flat({
82
82
  months: "months",
83
83
  years: "years",
84
84
  type: "type",
85
- donation: "Donation"
85
+ donation: "Donation",
86
+ quantityLimitPerCheckout: "Exceed purchase limit",
87
+ quantityNotEnough: "Exceed inventory"
86
88
  },
87
89
  payment: {
88
90
  checkout: {
package/es/locales/zh.js CHANGED
@@ -82,7 +82,9 @@ export default flat({
82
82
  months: "\u6708",
83
83
  years: "\u5E74",
84
84
  type: "\u7C7B\u578B",
85
- donation: "\u6253\u8D4F"
85
+ donation: "\u6253\u8D4F",
86
+ quantityLimitPerCheckout: "\u8D85\u51FA\u8D2D\u4E70\u9650\u5236",
87
+ quantityNotEnough: "\u5E93\u5B58\u4E0D\u8DB3"
86
88
  },
87
89
  payment: {
88
90
  checkout: {
@@ -8,7 +8,7 @@ import { Fade, InputAdornment, Stack, Typography } from "@mui/material";
8
8
  import { useCreation, useMemoizedFn, useSetState, useSize } from "ahooks";
9
9
  import { PhoneNumberUtil } from "google-libphonenumber";
10
10
  import pWaitFor from "p-wait-for";
11
- import { useEffect, useState } from "react";
11
+ import { useEffect, useMemo, useState } from "react";
12
12
  import { Controller, useFormContext, useWatch } from "react-hook-form";
13
13
  import { joinURL } from "ufo";
14
14
  import { dispatch } from "use-bus";
@@ -18,7 +18,14 @@ import FormInput from "../../components/input.js";
18
18
  import { usePaymentContext } from "../../contexts/payment.js";
19
19
  import { useSubscription } from "../../hooks/subscription.js";
20
20
  import api from "../../libs/api.js";
21
- import { flattenPaymentMethods, formatError, getPrefix, getQueryParams, getStatementDescriptor } from "../../libs/util.js";
21
+ import {
22
+ flattenPaymentMethods,
23
+ formatError,
24
+ formatQuantityInventory,
25
+ getPrefix,
26
+ getQueryParams,
27
+ getStatementDescriptor
28
+ } from "../../libs/util.js";
22
29
  import UserButtons from "./addon.js";
23
30
  import AddressForm from "./address.js";
24
31
  import CurrencySelector from "./currency.js";
@@ -61,6 +68,16 @@ export default function PaymentForm({
61
68
  const { session, connect } = usePaymentContext();
62
69
  const subscription = useSubscription("events");
63
70
  const { control, getValues, setValue, handleSubmit } = useFormContext();
71
+ const quantityInventoryStatus = useMemo(() => {
72
+ let status = true;
73
+ for (const item of checkoutSession.line_items) {
74
+ if (formatQuantityInventory(item.price, item.quantity)) {
75
+ status = false;
76
+ break;
77
+ }
78
+ }
79
+ return status;
80
+ }, [checkoutSession]);
64
81
  const [state, setState] = useSetState({
65
82
  submitting: false,
66
83
  paying: false,
@@ -386,7 +403,7 @@ export default function PaymentForm({
386
403
  className: "cko-submit-button",
387
404
  onClick: onAction,
388
405
  fullWidth: true,
389
- disabled: state.submitting || state.paying || state.stripePaying,
406
+ disabled: state.submitting || state.paying || state.stripePaying || !quantityInventoryStatus,
390
407
  loading: state.submitting || state.paying,
391
408
  children: state.submitting || state.paying ? t("payment.checkout.processing") : buttonText
392
409
  }
@@ -1,9 +1,15 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
3
- import { Stack, Typography } from "@mui/material";
3
+ import { Box, Stack, Typography } from "@mui/material";
4
4
  import Status from "../components/status.js";
5
5
  import Switch from "../components/switch-button.js";
6
- import { formatLineItemPricing, formatPrice, formatRecurring, formatUpsellSaving } from "../libs/util.js";
6
+ import {
7
+ formatLineItemPricing,
8
+ formatPrice,
9
+ formatQuantityInventory,
10
+ formatRecurring,
11
+ formatUpsellSaving
12
+ } from "../libs/util.js";
7
13
  import ProductCard from "./product-card.js";
8
14
  ProductItem.defaultProps = {
9
15
  mode: "normal",
@@ -41,7 +47,14 @@ export default function ProductItem({
41
47
  logo: item.price.product?.images[0],
42
48
  name: item.price.product?.name,
43
49
  description: item.price.product?.description,
44
- extra: item.price.type === "recurring" && item.price.recurring ? [pricing.quantity, t("common.billed", { rule: `${formatRecurring(item.upsell_price?.recurring || item.price.recurring, true, "per", locale)} ${metered}` })].filter(Boolean).join(", ") : pricing.quantity
50
+ extra: /* @__PURE__ */ jsxs(Box, { display: "flex", alignItems: "center", children: [
51
+ item.price.type === "recurring" && item.price.recurring ? [pricing.quantity, t("common.billed", { rule: `${formatRecurring(item.upsell_price?.recurring || item.price.recurring, true, "per", locale)} ${metered}` })].filter(Boolean).join(", ") : pricing.quantity,
52
+ formatQuantityInventory(item.price, item.quantity, locale) ? /* @__PURE__ */ jsxs(Typography, { sx: { fontSize: "0.85rem", color: "red" }, children: [
53
+ "\uFF08",
54
+ formatQuantityInventory(item.price, item.quantity, locale),
55
+ "\uFF09"
56
+ ] }) : ""
57
+ ] })
45
58
  }
46
59
  ),
47
60
  /* @__PURE__ */ jsxs(Stack, { direction: "column", alignItems: "flex-end", flex: 1, children: [
@@ -44,6 +44,9 @@ const fetchSupporters = (target, livemode = true) => {
44
44
  }
45
45
  return supporterCache[target];
46
46
  };
47
+ const emojiFont = {
48
+ fontFamily: 'Avenir, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'
49
+ };
47
50
  function SupporterAvatar({
48
51
  supporters = [],
49
52
  totalAmount = "0",
@@ -72,7 +75,10 @@ function SupporterAvatar({
72
75
  children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
73
76
  component: "p",
74
77
  color: "text.secondary",
75
- children: customersNum === 0 ? t("payment.checkout.donation.empty") : t("payment.checkout.donation.summary", {
78
+ children: customersNum === 0 ? /* @__PURE__ */(0, _jsxRuntime.jsx)("span", {
79
+ style: emojiFont,
80
+ children: t("payment.checkout.donation.empty")
81
+ }) : t("payment.checkout.donation.summary", {
76
82
  total: customersNum,
77
83
  symbol: currency.symbol,
78
84
  totalAmount: (0, _util.formatAmount)(totalAmount || "0", currency.decimal)
@@ -114,7 +120,10 @@ function SupporterTable({
114
120
  children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
115
121
  component: "p",
116
122
  color: "text.secondary",
117
- children: customersNum === 0 ? t("payment.checkout.donation.empty") : t("payment.checkout.donation.summary", {
123
+ children: customersNum === 0 ? /* @__PURE__ */(0, _jsxRuntime.jsx)("span", {
124
+ style: emojiFont,
125
+ children: t("payment.checkout.donation.empty")
126
+ }) : t("payment.checkout.donation.summary", {
118
127
  total: customersNum,
119
128
  symbol: currency.symbol,
120
129
  totalAmount: (0, _util.formatAmount)(totalAmount || "0", currency.decimal)
@@ -216,7 +225,10 @@ function SupporterSimple({
216
225
  children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
217
226
  component: "p",
218
227
  color: "text.secondary",
219
- children: customersNum === 0 ? t("payment.checkout.donation.empty") : t("payment.checkout.donation.summary", {
228
+ children: customersNum === 0 ? /* @__PURE__ */(0, _jsxRuntime.jsx)("span", {
229
+ style: emojiFont,
230
+ children: t("payment.checkout.donation.empty")
231
+ }) : t("payment.checkout.donation.summary", {
220
232
  total: customersNum,
221
233
  symbol: currency.symbol,
222
234
  totalAmount: (0, _util.formatAmount)(totalAmount || "0", currency.decimal)
@@ -76,3 +76,4 @@ export declare function formatTotalPrice({ product, quantity, priceId, locale, }
76
76
  priceId?: string;
77
77
  locale: string;
78
78
  }): PricingRenderProps;
79
+ export declare function formatQuantityInventory(price: TPrice, quantity: string | number, locale?: string): string;
package/lib/libs/util.js CHANGED
@@ -15,6 +15,7 @@ exports.formatLineItemPricing = formatLineItemPricing;
15
15
  exports.formatLocale = void 0;
16
16
  exports.formatNumber = formatNumber;
17
17
  exports.formatPriceAmount = exports.formatPrice = exports.formatPrettyMsLocale = void 0;
18
+ exports.formatQuantityInventory = formatQuantityInventory;
18
19
  exports.formatRecurring = formatRecurring;
19
20
  exports.formatSubscriptionProduct = formatSubscriptionProduct;
20
21
  exports.formatTime = formatTime;
@@ -805,4 +806,19 @@ function formatTotalPrice({
805
806
  }),
806
807
  totalAmount: unitValue.mul(new _util.BN(quantity)).toString()
807
808
  };
809
+ }
810
+ function formatQuantityInventory(price, quantity, locale = "en") {
811
+ const q = Number(quantity);
812
+ const {
813
+ quantity_available: quantityAvailable = 0,
814
+ quantity_sold: quantitySold = 0,
815
+ quantity_limit_per_checkout: quantityLimitPerCheckout = 0
816
+ } = price || {};
817
+ if (quantityAvailable > 0 && quantitySold + q > quantityAvailable) {
818
+ return (0, _locales.t)("common.quantityNotEnough", locale);
819
+ }
820
+ if (quantityLimitPerCheckout > 0 && quantityLimitPerCheckout < q) {
821
+ return (0, _locales.t)("common.quantityLimitPerCheckout", locale);
822
+ }
823
+ return "";
808
824
  }
package/lib/locales/en.js CHANGED
@@ -89,7 +89,9 @@ module.exports = (0, _flat.default)({
89
89
  months: "months",
90
90
  years: "years",
91
91
  type: "type",
92
- donation: "Donation"
92
+ donation: "Donation",
93
+ quantityLimitPerCheckout: "Exceed purchase limit",
94
+ quantityNotEnough: "Exceed inventory"
93
95
  },
94
96
  payment: {
95
97
  checkout: {
package/lib/locales/zh.js CHANGED
@@ -89,7 +89,9 @@ module.exports = (0, _flat.default)({
89
89
  months: "\u6708",
90
90
  years: "\u5E74",
91
91
  type: "\u7C7B\u578B",
92
- donation: "\u6253\u8D4F"
92
+ donation: "\u6253\u8D4F",
93
+ quantityLimitPerCheckout: "\u8D85\u51FA\u8D2D\u4E70\u9650\u5236",
94
+ quantityNotEnough: "\u5E93\u5B58\u4E0D\u8DB3"
93
95
  },
94
96
  payment: {
95
97
  checkout: {
@@ -80,6 +80,16 @@ function PaymentForm({
80
80
  setValue,
81
81
  handleSubmit
82
82
  } = (0, _reactHookForm.useFormContext)();
83
+ const quantityInventoryStatus = (0, _react.useMemo)(() => {
84
+ let status = true;
85
+ for (const item of checkoutSession.line_items) {
86
+ if ((0, _util.formatQuantityInventory)(item.price, item.quantity)) {
87
+ status = false;
88
+ break;
89
+ }
90
+ }
91
+ return status;
92
+ }, [checkoutSession]);
83
93
  const [state, setState] = (0, _ahooks.useSetState)({
84
94
  submitting: false,
85
95
  paying: false,
@@ -472,7 +482,7 @@ function PaymentForm({
472
482
  className: "cko-submit-button",
473
483
  onClick: onAction,
474
484
  fullWidth: true,
475
- disabled: state.submitting || state.paying || state.stripePaying,
485
+ disabled: state.submitting || state.paying || state.stripePaying || !quantityInventoryStatus,
476
486
  loading: state.submitting || state.paying,
477
487
  children: state.submitting || state.paying ? t("payment.checkout.processing") : buttonText
478
488
  }), ["subscription", "setup"].includes(checkoutSession.mode) && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
@@ -59,9 +59,19 @@ function ProductItem({
59
59
  logo: item.price.product?.images[0],
60
60
  name: item.price.product?.name,
61
61
  description: item.price.product?.description,
62
- extra: item.price.type === "recurring" && item.price.recurring ? [pricing.quantity, t("common.billed", {
63
- rule: `${(0, _util.formatRecurring)(item.upsell_price?.recurring || item.price.recurring, true, "per", locale)} ${metered}`
64
- })].filter(Boolean).join(", ") : pricing.quantity
62
+ extra: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
63
+ display: "flex",
64
+ alignItems: "center",
65
+ children: [item.price.type === "recurring" && item.price.recurring ? [pricing.quantity, t("common.billed", {
66
+ rule: `${(0, _util.formatRecurring)(item.upsell_price?.recurring || item.price.recurring, true, "per", locale)} ${metered}`
67
+ })].filter(Boolean).join(", ") : pricing.quantity, (0, _util.formatQuantityInventory)(item.price, item.quantity, locale) ? /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
68
+ sx: {
69
+ fontSize: "0.85rem",
70
+ color: "red"
71
+ },
72
+ children: ["\uFF08", (0, _util.formatQuantityInventory)(item.price, item.quantity, locale), "\uFF09"]
73
+ }) : ""]
74
+ })
65
75
  }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
66
76
  direction: "column",
67
77
  alignItems: "flex-end",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.13.301",
3
+ "version": "1.13.302",
4
4
  "description": "Reusable react components for payment kit v2",
5
5
  "keywords": [
6
6
  "react",
@@ -91,7 +91,7 @@
91
91
  "@babel/core": "^7.24.7",
92
92
  "@babel/preset-env": "^7.24.7",
93
93
  "@babel/preset-react": "^7.24.7",
94
- "@blocklet/payment-types": "1.13.301",
94
+ "@blocklet/payment-types": "1.13.302",
95
95
  "@storybook/addon-essentials": "^7.6.19",
96
96
  "@storybook/addon-interactions": "^7.6.19",
97
97
  "@storybook/addon-links": "^7.6.19",
@@ -120,5 +120,5 @@
120
120
  "vite-plugin-babel": "^1.2.0",
121
121
  "vite-plugin-node-polyfills": "^0.21.0"
122
122
  },
123
- "gitHead": "51232b3901a744f8340c345dc738552f9df899a2"
123
+ "gitHead": "366468bdbb1c5e8837a3baff852067fa041609a1"
124
124
  }
@@ -92,6 +92,11 @@ const fetchSupporters = (target: string, livemode: boolean = true): Promise<Dona
92
92
  return supporterCache[target];
93
93
  };
94
94
 
95
+ const emojiFont = {
96
+ fontFamily:
97
+ 'Avenir, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
98
+ };
99
+
95
100
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
96
101
  function SupporterAvatar({ supporters = [], totalAmount = '0', currency, method }: DonateHistory) {
97
102
  const { t } = useLocaleContext();
@@ -113,13 +118,15 @@ function SupporterAvatar({ supporters = [], totalAmount = '0', currency, method
113
118
  sm: 1,
114
119
  }}>
115
120
  <Typography component="p" color="text.secondary">
116
- {customersNum === 0
117
- ? t('payment.checkout.donation.empty')
118
- : t('payment.checkout.donation.summary', {
119
- total: customersNum,
120
- symbol: currency.symbol,
121
- totalAmount: formatAmount(totalAmount || '0', currency.decimal),
122
- })}
121
+ {customersNum === 0 ? (
122
+ <span style={emojiFont}>{t('payment.checkout.donation.empty')}</span>
123
+ ) : (
124
+ t('payment.checkout.donation.summary', {
125
+ total: customersNum,
126
+ symbol: currency.symbol,
127
+ totalAmount: formatAmount(totalAmount || '0', currency.decimal),
128
+ })
129
+ )}
123
130
  </Typography>
124
131
  <AvatarGroup total={customersNum} max={20}>
125
132
  {customers.map((x) => (
@@ -144,13 +151,15 @@ function SupporterTable({ supporters = [], totalAmount = '0', currency, method }
144
151
  return (
145
152
  <Box display="flex" flexDirection="column" alignItems="center" gap={{ xs: 0.5, sm: 1 }}>
146
153
  <Typography component="p" color="text.secondary">
147
- {customersNum === 0
148
- ? t('payment.checkout.donation.empty')
149
- : t('payment.checkout.donation.summary', {
150
- total: customersNum,
151
- symbol: currency.symbol,
152
- totalAmount: formatAmount(totalAmount || '0', currency.decimal),
153
- })}
154
+ {customersNum === 0 ? (
155
+ <span style={emojiFont}>{t('payment.checkout.donation.empty')}</span>
156
+ ) : (
157
+ t('payment.checkout.donation.summary', {
158
+ total: customersNum,
159
+ symbol: currency.symbol,
160
+ totalAmount: formatAmount(totalAmount || '0', currency.decimal),
161
+ })
162
+ )}
154
163
  </Typography>
155
164
  <Table size="small" sx={{ width: '100%', overflow: 'hidden' }}>
156
165
  <TableBody>
@@ -220,13 +229,15 @@ function SupporterSimple({ supporters = [], totalAmount = '0', currency, method
220
229
  sm: 1,
221
230
  }}>
222
231
  <Typography component="p" color="text.secondary">
223
- {customersNum === 0
224
- ? t('payment.checkout.donation.empty')
225
- : t('payment.checkout.donation.summary', {
226
- total: customersNum,
227
- symbol: currency.symbol,
228
- totalAmount: formatAmount(totalAmount || '0', currency.decimal),
229
- })}
232
+ {customersNum === 0 ? (
233
+ <span style={emojiFont}>{t('payment.checkout.donation.empty')}</span>
234
+ ) : (
235
+ t('payment.checkout.donation.summary', {
236
+ total: customersNum,
237
+ symbol: currency.symbol,
238
+ totalAmount: formatAmount(totalAmount || '0', currency.decimal),
239
+ })
240
+ )}
230
241
  </Typography>
231
242
  <AvatarGroup total={customersNum} max={10}>
232
243
  {customers.map((x) => (
package/src/libs/util.ts CHANGED
@@ -872,3 +872,19 @@ export function formatTotalPrice({
872
872
  totalAmount: unitValue.mul(new BN(quantity)).toString(),
873
873
  };
874
874
  }
875
+
876
+ export function formatQuantityInventory(price: TPrice, quantity: string | number, locale = 'en') {
877
+ const q = Number(quantity);
878
+ const {
879
+ quantity_available: quantityAvailable = 0,
880
+ quantity_sold: quantitySold = 0,
881
+ quantity_limit_per_checkout: quantityLimitPerCheckout = 0,
882
+ } = price || {};
883
+ if (quantityAvailable > 0 && quantitySold + q > quantityAvailable) {
884
+ return t('common.quantityNotEnough', locale);
885
+ }
886
+ if (quantityLimitPerCheckout > 0 && quantityLimitPerCheckout < q) {
887
+ return t('common.quantityLimitPerCheckout', locale);
888
+ }
889
+ return '';
890
+ }
@@ -85,6 +85,8 @@ export default flat({
85
85
  years: 'years',
86
86
  type: 'type',
87
87
  donation: 'Donation',
88
+ quantityLimitPerCheckout: 'Exceed purchase limit',
89
+ quantityNotEnough: 'Exceed inventory',
88
90
  },
89
91
  payment: {
90
92
  checkout: {
@@ -85,6 +85,8 @@ export default flat({
85
85
  years: '年',
86
86
  type: '类型',
87
87
  donation: '打赏',
88
+ quantityLimitPerCheckout: '超出购买限制',
89
+ quantityNotEnough: '库存不足',
88
90
  },
89
91
  payment: {
90
92
  checkout: {
@@ -15,7 +15,7 @@ import { Fade, InputAdornment, Stack, Typography } from '@mui/material';
15
15
  import { useCreation, useMemoizedFn, useSetState, useSize } from 'ahooks';
16
16
  import { PhoneNumberUtil } from 'google-libphonenumber';
17
17
  import pWaitFor from 'p-wait-for';
18
- import { useEffect, useState } from 'react';
18
+ import { useEffect, useMemo, useState } from 'react';
19
19
  import { Controller, useFormContext, useWatch } from 'react-hook-form';
20
20
  import { joinURL } from 'ufo';
21
21
  import { dispatch } from 'use-bus';
@@ -26,7 +26,14 @@ import FormInput from '../../components/input';
26
26
  import { usePaymentContext } from '../../contexts/payment';
27
27
  import { useSubscription } from '../../hooks/subscription';
28
28
  import api from '../../libs/api';
29
- import { flattenPaymentMethods, formatError, getPrefix, getQueryParams, getStatementDescriptor } from '../../libs/util';
29
+ import {
30
+ flattenPaymentMethods,
31
+ formatError,
32
+ formatQuantityInventory,
33
+ getPrefix,
34
+ getQueryParams,
35
+ getStatementDescriptor,
36
+ } from '../../libs/util';
30
37
  import { CheckoutCallbacks, CheckoutContext } from '../../types';
31
38
  import UserButtons from './addon';
32
39
  import AddressForm from './address';
@@ -94,6 +101,16 @@ export default function PaymentForm({
94
101
  const { session, connect } = usePaymentContext();
95
102
  const subscription = useSubscription('events');
96
103
  const { control, getValues, setValue, handleSubmit } = useFormContext();
104
+ const quantityInventoryStatus = useMemo(() => {
105
+ let status = true;
106
+ for (const item of checkoutSession.line_items) {
107
+ if (formatQuantityInventory(item.price, item.quantity)) {
108
+ status = false;
109
+ break;
110
+ }
111
+ }
112
+ return status;
113
+ }, [checkoutSession]);
97
114
  const [state, setState] = useSetState<{
98
115
  submitting: boolean;
99
116
  paying: boolean;
@@ -455,7 +472,7 @@ export default function PaymentForm({
455
472
  className="cko-submit-button"
456
473
  onClick={onAction}
457
474
  fullWidth
458
- disabled={state.submitting || state.paying || state.stripePaying}
475
+ disabled={state.submitting || state.paying || state.stripePaying || !quantityInventoryStatus}
459
476
  loading={state.submitting || state.paying}>
460
477
  {state.submitting || state.paying ? t('payment.checkout.processing') : buttonText}
461
478
  </LoadingButton>
@@ -1,10 +1,16 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import type { PriceRecurring, TLineItemExpanded, TPaymentCurrency } from '@blocklet/payment-types';
3
- import { Stack, Typography } from '@mui/material';
3
+ import { Box, Stack, Typography } from '@mui/material';
4
4
 
5
5
  import Status from '../components/status';
6
6
  import Switch from '../components/switch-button';
7
- import { formatLineItemPricing, formatPrice, formatRecurring, formatUpsellSaving } from '../libs/util';
7
+ import {
8
+ formatLineItemPricing,
9
+ formatPrice,
10
+ formatQuantityInventory,
11
+ formatRecurring,
12
+ formatUpsellSaving,
13
+ } from '../libs/util';
8
14
  import ProductCard from './product-card';
9
15
 
10
16
  type Props = {
@@ -52,9 +58,19 @@ export default function ProductItem({
52
58
  name={item.price.product?.name}
53
59
  description={item.price.product?.description}
54
60
  extra={
55
- item.price.type === 'recurring' && item.price.recurring
56
- ? [pricing.quantity, t('common.billed', { rule: `${formatRecurring(item.upsell_price?.recurring || item.price.recurring, true, 'per', locale)} ${metered}` })].filter(Boolean).join(', ') // prettier-ignore
57
- : pricing.quantity
61
+ <Box display="flex" alignItems="center">
62
+ {item.price.type === 'recurring' && item.price.recurring
63
+ ? [pricing.quantity, t('common.billed', { rule: `${formatRecurring(item.upsell_price?.recurring || item.price.recurring, true, 'per', locale)} ${metered}` })].filter(Boolean).join(', ') // prettier-ignore
64
+ : pricing.quantity}
65
+
66
+ {formatQuantityInventory(item.price, item.quantity, locale) ? (
67
+ <Typography sx={{ fontSize: '0.85rem', color: 'red' }}>
68
+ ({formatQuantityInventory(item.price, item.quantity, locale)})
69
+ </Typography>
70
+ ) : (
71
+ ''
72
+ )}
73
+ </Box>
58
74
  }
59
75
  />
60
76
  <Stack direction="column" alignItems="flex-end" flex={1}>