@blocklet/payment-react 1.20.13 → 1.20.14

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.
@@ -0,0 +1,23 @@
1
+ type LocalizedText = {
2
+ zh: string;
3
+ en: string;
4
+ };
5
+ type SimpleSourceData = Record<string, string>;
6
+ type StructuredSourceDataField = {
7
+ key: string;
8
+ label: string | LocalizedText;
9
+ value: string;
10
+ type?: 'text' | 'image' | 'url';
11
+ url?: string;
12
+ group?: string;
13
+ };
14
+ type SourceData = SimpleSourceData | StructuredSourceDataField[];
15
+ interface SourceDataViewerProps {
16
+ data: SourceData;
17
+ compact?: boolean;
18
+ maxItems?: number;
19
+ locale?: 'en' | 'zh';
20
+ showGroups?: boolean;
21
+ }
22
+ declare function SourceDataViewer({ data, compact, maxItems, locale: propLocale, showGroups, }: SourceDataViewerProps): import("react").JSX.Element | null;
23
+ export default SourceDataViewer;
@@ -0,0 +1,228 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import Empty from "@arcblock/ux/lib/Empty";
3
+ import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
4
+ import { Box, Chip, Link, Stack, Typography } from "@mui/material";
5
+ function SourceDataViewer({
6
+ data,
7
+ compact = false,
8
+ maxItems = void 0,
9
+ locale: propLocale = void 0,
10
+ showGroups = false
11
+ }) {
12
+ const { locale: contextLocale, t } = useLocaleContext();
13
+ const currentLocale = propLocale || contextLocale || "en";
14
+ if (!data || Array.isArray(data) && data.length === 0 || typeof data === "object" && Object.keys(data).length === 0) {
15
+ return /* @__PURE__ */ jsx(Empty, { children: t("common.none") });
16
+ }
17
+ const getLocalizedText = (text) => {
18
+ if (typeof text === "string") {
19
+ return text;
20
+ }
21
+ return text[currentLocale] || text.en || "";
22
+ };
23
+ const isSimpleSourceData = (sourceData) => {
24
+ return typeof sourceData === "object" && !Array.isArray(sourceData) && sourceData !== null;
25
+ };
26
+ const isUrlLike = (str) => {
27
+ try {
28
+ new URL(str);
29
+ return true;
30
+ } catch {
31
+ return /^https?:\/\/.+/.test(str) || /^www\..+/.test(str);
32
+ }
33
+ };
34
+ const normalizeData = (inputData) => {
35
+ if (isSimpleSourceData(inputData)) {
36
+ return Object.entries(inputData).map(([key, value]) => {
37
+ const stringValue = String(value);
38
+ const isUrl = isUrlLike(stringValue);
39
+ return {
40
+ key,
41
+ label: key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
42
+ value: stringValue,
43
+ type: isUrl ? "url" : "text",
44
+ url: isUrl ? stringValue : void 0
45
+ };
46
+ });
47
+ }
48
+ return inputData;
49
+ };
50
+ const renderFieldValue = (field) => {
51
+ const displayValue = field.value;
52
+ if (field.type === "url") {
53
+ return /* @__PURE__ */ jsx(
54
+ Link,
55
+ {
56
+ href: field.url || field.value,
57
+ target: "_blank",
58
+ rel: "noopener noreferrer",
59
+ sx: {
60
+ color: "primary.main",
61
+ textDecoration: "none",
62
+ fontSize: "0.85rem",
63
+ fontWeight: 600,
64
+ lineHeight: 1.4,
65
+ "&:hover": {
66
+ textDecoration: "underline",
67
+ color: "primary.dark"
68
+ },
69
+ "&:visited": {
70
+ color: "primary.main"
71
+ }
72
+ },
73
+ children: displayValue
74
+ }
75
+ );
76
+ }
77
+ if (field.type === "image") {
78
+ return /* @__PURE__ */ jsxs(
79
+ Box,
80
+ {
81
+ sx: {
82
+ display: "flex",
83
+ alignItems: "center",
84
+ gap: 1
85
+ },
86
+ children: [
87
+ /* @__PURE__ */ jsx(
88
+ Box,
89
+ {
90
+ component: "img",
91
+ src: field.url || field.value,
92
+ alt: displayValue,
93
+ sx: {
94
+ width: 24,
95
+ height: 24,
96
+ borderRadius: 1,
97
+ objectFit: "cover"
98
+ }
99
+ }
100
+ ),
101
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { fontWeight: 500 }, children: displayValue })
102
+ ]
103
+ }
104
+ );
105
+ }
106
+ return /* @__PURE__ */ jsx(
107
+ Typography,
108
+ {
109
+ variant: "body2",
110
+ sx: {
111
+ fontWeight: 500,
112
+ color: "text.primary"
113
+ },
114
+ children: displayValue
115
+ }
116
+ );
117
+ };
118
+ if (!data || Array.isArray(data) && data.length === 0 || typeof data === "object" && Object.keys(data).length === 0) {
119
+ return null;
120
+ }
121
+ const normalizedData = normalizeData(data);
122
+ const displayItems = maxItems ? normalizedData.slice(0, maxItems) : normalizedData;
123
+ if (compact) {
124
+ return /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, flexWrap: "wrap", useFlexGap: true, children: [
125
+ displayItems.map((field) => /* @__PURE__ */ jsx(
126
+ Chip,
127
+ {
128
+ label: `${getLocalizedText(field.label)}: ${field.value}`,
129
+ size: "small",
130
+ variant: "outlined",
131
+ sx: { maxWidth: 200 }
132
+ },
133
+ field.key
134
+ )),
135
+ maxItems && normalizedData.length > maxItems && /* @__PURE__ */ jsx(Chip, { label: `+${normalizedData.length - maxItems} more`, size: "small", color: "primary", variant: "outlined" })
136
+ ] });
137
+ }
138
+ if (showGroups) {
139
+ const groupedData = displayItems.reduce(
140
+ (acc, field) => {
141
+ const group = field.group || "default";
142
+ if (!acc[group]) acc[group] = [];
143
+ acc[group].push(field);
144
+ return acc;
145
+ },
146
+ {}
147
+ );
148
+ return /* @__PURE__ */ jsx(Stack, { spacing: 2.5, children: Object.entries(groupedData).map(([group, fields]) => /* @__PURE__ */ jsxs(Box, { children: [
149
+ group !== "default" && /* @__PURE__ */ jsx(
150
+ Typography,
151
+ {
152
+ variant: "subtitle2",
153
+ sx: {
154
+ mb: 1.5,
155
+ fontWeight: 700,
156
+ color: "text.primary",
157
+ letterSpacing: "0.5px",
158
+ borderBottom: "1px solid",
159
+ borderColor: "divider",
160
+ pb: 0.5
161
+ },
162
+ children: group.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())
163
+ }
164
+ ),
165
+ /* @__PURE__ */ jsx(Stack, { spacing: 1.2, sx: { pl: group !== "default" ? 1 : 0 }, children: fields.map((field) => /* @__PURE__ */ jsxs(
166
+ Box,
167
+ {
168
+ sx: {
169
+ display: "flex",
170
+ flexDirection: "row",
171
+ alignItems: "flex-start",
172
+ gap: 3,
173
+ minHeight: 20
174
+ },
175
+ children: [
176
+ /* @__PURE__ */ jsx(
177
+ Typography,
178
+ {
179
+ variant: "body2",
180
+ color: "text.secondary",
181
+ sx: {
182
+ fontSize: "0.8rem",
183
+ minWidth: 100,
184
+ fontWeight: 600,
185
+ lineHeight: 1.4
186
+ },
187
+ children: getLocalizedText(field.label)
188
+ }
189
+ ),
190
+ /* @__PURE__ */ jsx(Box, { sx: { flex: 1, minWidth: 0, wordBreak: "break-all", whiteSpace: "wrap" }, children: renderFieldValue(field) })
191
+ ]
192
+ },
193
+ field.key
194
+ )) })
195
+ ] }, group)) });
196
+ }
197
+ return /* @__PURE__ */ jsx(Stack, { spacing: 1.5, children: displayItems.map((field) => /* @__PURE__ */ jsxs(
198
+ Box,
199
+ {
200
+ sx: {
201
+ display: "flex",
202
+ flexDirection: "row",
203
+ alignItems: "flex-start",
204
+ gap: 3,
205
+ minHeight: 20
206
+ },
207
+ children: [
208
+ /* @__PURE__ */ jsx(
209
+ Typography,
210
+ {
211
+ variant: "body2",
212
+ color: "text.secondary",
213
+ sx: {
214
+ fontSize: "0.8rem",
215
+ minWidth: 100,
216
+ fontWeight: 600,
217
+ lineHeight: 1.4
218
+ },
219
+ children: getLocalizedText(field.label)
220
+ }
221
+ ),
222
+ /* @__PURE__ */ jsx(Box, { sx: { flex: 1, minWidth: 0 }, children: renderFieldValue(field) })
223
+ ]
224
+ },
225
+ field.key
226
+ )) });
227
+ }
228
+ export default SourceDataViewer;
@@ -6,6 +6,7 @@ type Props = {
6
6
  onTableDataChange?: Function;
7
7
  showAdminColumns?: boolean;
8
8
  showTimeFilter?: boolean;
9
+ includeGrants?: boolean;
9
10
  source?: string;
10
11
  mode?: 'dashboard' | 'portal';
11
12
  };
@@ -1,17 +1,18 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
3
- import { Box, Typography, Stack, Link, Grid } from "@mui/material";
3
+ import { Box, Typography, Grid, Stack, Link, Button, Popover } from "@mui/material";
4
4
  import { useRequest } from "ahooks";
5
5
  import { useNavigate } from "react-router-dom";
6
6
  import React, { useCallback, useEffect, useRef, useState } from "react";
7
- import { joinURL } from "ufo";
8
7
  import { styled } from "@mui/system";
8
+ import { joinURL } from "ufo";
9
9
  import DateRangePicker from "../../components/date-range-picker.js";
10
10
  import { formatBNStr, formatToDate, getPrefix } from "../../libs/util.js";
11
11
  import { usePaymentContext } from "../../contexts/payment.js";
12
12
  import api from "../../libs/api.js";
13
13
  import Table from "../../components/table.js";
14
14
  import { createLink, handleNavigation } from "../../libs/navigation.js";
15
+ import SourceDataViewer from "../../components/source-data-viewer.js";
15
16
  const fetchData = (params = {}) => {
16
17
  const search = new URLSearchParams();
17
18
  Object.keys(params).forEach((key) => {
@@ -31,6 +32,16 @@ const getGrantDetailLink = (grantId, inDashboard) => {
31
32
  connect: false
32
33
  };
33
34
  };
35
+ const getInvoiceDetailLink = (invoiceId, inDashboard) => {
36
+ let path = `/customer/invoice/${invoiceId}`;
37
+ if (inDashboard) {
38
+ path = `/admin/billing/${invoiceId}`;
39
+ }
40
+ return {
41
+ link: createLink(path),
42
+ connect: false
43
+ };
44
+ };
34
45
  const TransactionsTable = React.memo((props) => {
35
46
  const {
36
47
  pageSize,
@@ -40,6 +51,7 @@ const TransactionsTable = React.memo((props) => {
40
51
  onTableDataChange,
41
52
  showAdminColumns = false,
42
53
  showTimeFilter = false,
54
+ includeGrants = false,
43
55
  source,
44
56
  mode = "portal"
45
57
  } = props;
@@ -57,6 +69,7 @@ const TransactionsTable = React.memo((props) => {
57
69
  start: void 0,
58
70
  end: void 0
59
71
  });
72
+ const [sourceDataPopover, setSourceDataPopover] = useState({ anchorEl: null, data: null });
60
73
  const handleDateRangeChange = useCallback((newValue) => {
61
74
  setFilters(newValue);
62
75
  setSearch((prev) => ({
@@ -72,10 +85,11 @@ const TransactionsTable = React.memo((props) => {
72
85
  customer_id: effectiveCustomerId,
73
86
  subscription_id,
74
87
  credit_grant_id,
75
- source
88
+ source,
89
+ include_grants: includeGrants
76
90
  }),
77
91
  {
78
- refreshDeps: [search, effectiveCustomerId, subscription_id, credit_grant_id, source]
92
+ refreshDeps: [search, effectiveCustomerId, subscription_id, credit_grant_id, source, includeGrants]
79
93
  }
80
94
  );
81
95
  useEffect(() => {
@@ -97,35 +111,59 @@ const TransactionsTable = React.memo((props) => {
97
111
  align: "right",
98
112
  options: {
99
113
  customBodyRenderLite: (_, index) => {
100
- const transaction = data?.list[index];
101
- const unit = transaction.meter?.unit || transaction.paymentCurrency.symbol;
102
- return /* @__PURE__ */ jsxs(Typography, { children: [
103
- formatBNStr(transaction.credit_amount, transaction.paymentCurrency.decimal),
104
- " ",
105
- unit
106
- ] });
114
+ const item = data?.list[index];
115
+ const isGrant = item.activity_type === "grant";
116
+ const amount = isGrant ? item.amount : item.credit_amount;
117
+ const currency = item.paymentCurrency || item.currency;
118
+ const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
119
+ const displayAmount = formatBNStr(amount, currency?.decimal || 0);
120
+ if (!includeGrants) {
121
+ return /* @__PURE__ */ jsxs(Typography, { children: [
122
+ displayAmount,
123
+ " ",
124
+ unit
125
+ ] });
126
+ }
127
+ return /* @__PURE__ */ jsxs(
128
+ Typography,
129
+ {
130
+ sx: {
131
+ color: isGrant ? "success.main" : "error.main"
132
+ },
133
+ children: [
134
+ isGrant ? "+" : "-",
135
+ " ",
136
+ displayAmount,
137
+ " ",
138
+ unit
139
+ ]
140
+ }
141
+ );
107
142
  }
108
143
  }
109
144
  },
110
- !credit_grant_id && {
145
+ {
111
146
  label: t("common.creditGrant"),
112
147
  name: "credit_grant",
113
148
  options: {
114
149
  customBodyRenderLite: (_, index) => {
115
- const transaction = data?.list[index];
150
+ const item = data?.list[index];
151
+ const isGrant = item.activity_type === "grant";
152
+ const grantName = isGrant ? item.name : item.creditGrant.name;
153
+ const grantId = isGrant ? item.id : item.credit_grant_id;
116
154
  return /* @__PURE__ */ jsx(
117
155
  Stack,
118
156
  {
119
157
  direction: "row",
120
158
  spacing: 1,
121
159
  onClick: (e) => {
122
- const link = getGrantDetailLink(transaction.credit_grant_id, isAdmin && mode === "dashboard");
160
+ const link = getGrantDetailLink(grantId, isAdmin && mode === "dashboard");
123
161
  handleNavigation(e, link.link, navigate);
124
162
  },
125
163
  sx: {
126
164
  alignItems: "center"
127
165
  },
128
- children: /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.link", cursor: "pointer" }, children: transaction.creditGrant.name || `Grant ${transaction.credit_grant_id.slice(-6)}` })
166
+ children: /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { cursor: "pointer" }, children: grantName || `Grant ${grantId.slice(-6)}` })
129
167
  }
130
168
  );
131
169
  }
@@ -133,11 +171,13 @@ const TransactionsTable = React.memo((props) => {
133
171
  },
134
172
  {
135
173
  label: t("common.description"),
136
- name: "subscription",
174
+ name: "description",
137
175
  options: {
138
176
  customBodyRenderLite: (_, index) => {
139
- const transaction = data?.list[index];
140
- return /* @__PURE__ */ jsx(Typography, { variant: "body2", children: transaction.subscription?.description || transaction.description });
177
+ const item = data?.list[index];
178
+ const isGrant = item.activity_type === "grant";
179
+ const description = isGrant ? item.name || item.description || "Credit Granted" : item.subscription?.description || item.description || `${item.meter_event_name} usage`;
180
+ return /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { fontWeight: 400 }, children: description });
141
181
  }
142
182
  }
143
183
  },
@@ -161,8 +201,52 @@ const TransactionsTable = React.memo((props) => {
161
201
  name: "created_at",
162
202
  options: {
163
203
  customBodyRenderLite: (_, index) => {
164
- const transaction = data?.list[index];
165
- return /* @__PURE__ */ jsx(Typography, { variant: "body2", children: formatToDate(transaction.created_at, locale, "YYYY-MM-DD HH:mm:ss") });
204
+ const item = data?.list[index];
205
+ return /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", sx: { fontSize: "0.875rem" }, children: formatToDate(item.created_at, locale, "YYYY-MM-DD HH:mm") });
206
+ }
207
+ }
208
+ },
209
+ {
210
+ label: t("common.actions"),
211
+ name: "actions",
212
+ options: {
213
+ customBodyRenderLite: (_, index) => {
214
+ const item = data?.list[index];
215
+ const isGrant = item.activity_type === "grant";
216
+ const invoiceId = isGrant ? item.metadata?.invoice_id : null;
217
+ const sourceData = !isGrant && item.meterEvent?.source_data;
218
+ return /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", gap: 1, alignItems: "center" }, children: [
219
+ isGrant && invoiceId && /* @__PURE__ */ jsx(
220
+ Button,
221
+ {
222
+ variant: "text",
223
+ size: "small",
224
+ color: "primary",
225
+ onClick: (e) => {
226
+ e.preventDefault();
227
+ const link = getInvoiceDetailLink(invoiceId, isAdmin && mode === "dashboard");
228
+ handleNavigation(e, link.link, navigate);
229
+ },
230
+ children: t("common.viewInvoice")
231
+ }
232
+ ),
233
+ sourceData && /* @__PURE__ */ jsx(
234
+ Button,
235
+ {
236
+ variant: "text",
237
+ size: "small",
238
+ color: "primary",
239
+ onClick: (e) => {
240
+ e.preventDefault();
241
+ setSourceDataPopover({
242
+ anchorEl: e.currentTarget,
243
+ data: sourceData
244
+ });
245
+ },
246
+ children: t("common.viewSourceData")
247
+ }
248
+ )
249
+ ] });
166
250
  }
167
251
  }
168
252
  }
@@ -217,6 +301,38 @@ const TransactionsTable = React.memo((props) => {
217
301
  mobileTDFlexDirection: "row",
218
302
  emptyNodeText: t("admin.creditTransactions.noTransactions")
219
303
  }
304
+ ),
305
+ /* @__PURE__ */ jsx(
306
+ Popover,
307
+ {
308
+ open: Boolean(sourceDataPopover.anchorEl),
309
+ anchorEl: sourceDataPopover.anchorEl,
310
+ onClose: () => setSourceDataPopover({ anchorEl: null, data: null }),
311
+ anchorOrigin: {
312
+ vertical: "bottom",
313
+ horizontal: "left"
314
+ },
315
+ transformOrigin: {
316
+ vertical: "top",
317
+ horizontal: "left"
318
+ },
319
+ slotProps: {
320
+ paper: {
321
+ sx: {
322
+ minWidth: {
323
+ xs: 0,
324
+ md: 320
325
+ },
326
+ maxHeight: 450,
327
+ p: {
328
+ xs: 1,
329
+ md: 3
330
+ }
331
+ }
332
+ }
333
+ },
334
+ children: sourceDataPopover.data && /* @__PURE__ */ jsx(SourceDataViewer, { data: sourceDataPopover.data, showGroups: true })
335
+ }
220
336
  )
221
337
  ] });
222
338
  });
@@ -246,6 +362,7 @@ export default function CreditTransactionsList(rawProps) {
246
362
  },
247
363
  showAdminColumns: false,
248
364
  showTimeFilter: false,
365
+ includeGrants: false,
249
366
  mode: "portal"
250
367
  },
251
368
  rawProps
package/es/index.d.ts CHANGED
@@ -40,6 +40,7 @@ import AutoTopupModal from './components/auto-topup/modal';
40
40
  import AutoTopup from './components/auto-topup';
41
41
  import Collapse from './components/collapse';
42
42
  import PromotionCode from './components/promotion-code';
43
+ import SourceDataViewer from './components/source-data-viewer';
43
44
  export { PaymentThemeProvider } from './theme';
44
45
  export * from './libs/util';
45
46
  export * from './libs/connect';
@@ -54,4 +55,4 @@ export * from './hooks/scroll';
54
55
  export * from './hooks/keyboard';
55
56
  export * from './libs/validator';
56
57
  export { translations, createTranslator } from './locales';
57
- export { createLazyComponent, api, dayjs, FormInput, FormLabel, PhoneInput, AddressForm, StripeForm, Status, Livemode, Switch, ConfirmDialog, CheckoutForm, CheckoutTable, CheckoutDonate, CurrencySelector, Payment, PaymentSummary, PricingTable, ProductSkeleton, Amount, CustomerInvoiceList, CustomerPaymentList, TxLink, TxGas, SafeGuard, PricingItem, CountrySelect, Table, TruncatedText, Link, OverdueInvoicePayment, PaymentBeneficiaries, LoadingButton, DonateDetails, ResumeSubscription, CreditGrantsList, CreditTransactionsList, DateRangePicker, CreditStatusChip, AutoTopupModal, AutoTopup, Collapse, PromotionCode, };
58
+ export { createLazyComponent, api, dayjs, FormInput, FormLabel, PhoneInput, AddressForm, StripeForm, Status, Livemode, Switch, ConfirmDialog, CheckoutForm, CheckoutTable, CheckoutDonate, CurrencySelector, Payment, PaymentSummary, PricingTable, ProductSkeleton, Amount, CustomerInvoiceList, CustomerPaymentList, TxLink, TxGas, SafeGuard, PricingItem, CountrySelect, Table, TruncatedText, Link, OverdueInvoicePayment, PaymentBeneficiaries, LoadingButton, DonateDetails, ResumeSubscription, CreditGrantsList, CreditTransactionsList, DateRangePicker, CreditStatusChip, AutoTopupModal, AutoTopup, Collapse, PromotionCode, SourceDataViewer, };
package/es/index.js CHANGED
@@ -40,6 +40,7 @@ import AutoTopupModal from "./components/auto-topup/modal.js";
40
40
  import AutoTopup from "./components/auto-topup/index.js";
41
41
  import Collapse from "./components/collapse.js";
42
42
  import PromotionCode from "./components/promotion-code.js";
43
+ import SourceDataViewer from "./components/source-data-viewer.js";
43
44
  export { PaymentThemeProvider } from "./theme/index.js";
44
45
  export * from "./libs/util.js";
45
46
  export * from "./libs/connect.js";
@@ -98,5 +99,6 @@ export {
98
99
  AutoTopupModal,
99
100
  AutoTopup,
100
101
  Collapse,
101
- PromotionCode
102
+ PromotionCode,
103
+ SourceDataViewer
102
104
  };
package/es/locales/en.js CHANGED
@@ -107,7 +107,8 @@ export default flat({
107
107
  confirm: "Confirm",
108
108
  cancel: "Cancel"
109
109
  },
110
- paymentMethod: "Payment Method"
110
+ paymentMethod: "Payment Method",
111
+ viewInvoice: "View Invoice"
111
112
  },
112
113
  payment: {
113
114
  checkout: {
package/es/locales/zh.js CHANGED
@@ -107,7 +107,8 @@ export default flat({
107
107
  confirm: "\u786E\u8BA4",
108
108
  cancel: "\u53D6\u6D88"
109
109
  },
110
- paymentMethod: "\u652F\u4ED8\u65B9\u5F0F"
110
+ paymentMethod: "\u652F\u4ED8\u65B9\u5F0F",
111
+ viewInvoice: "\u67E5\u770B\u8D26\u5355"
111
112
  },
112
113
  payment: {
113
114
  checkout: {
@@ -135,10 +135,7 @@ function PaymentInner({
135
135
  const currencyId = useWatch({ control: methods.control, name: "payment_currency", defaultValue: defaultCurrencyId });
136
136
  const currency = findCurrency(paymentMethods, currencyId) || settings.baseCurrency;
137
137
  const method = paymentMethods.find((x) => x.id === currency.payment_method_id);
138
- useEffect(() => {
139
- if (onChange) {
140
- onChange(methods.getValues());
141
- }
138
+ const recalculatePromotion = () => {
142
139
  if (state.checkoutSession?.discounts?.length) {
143
140
  api.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
144
141
  currency_id: currencyId
@@ -146,10 +143,20 @@ function PaymentInner({
146
143
  onPromotionUpdate();
147
144
  });
148
145
  }
146
+ };
147
+ useEffect(() => {
148
+ if (onChange) {
149
+ onChange(methods.getValues());
150
+ }
151
+ recalculatePromotion();
149
152
  }, [currencyId]);
150
153
  const onUpsell = async (from, to) => {
151
154
  try {
152
155
  const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/upsell`, { from, to });
156
+ if (data.discounts?.length) {
157
+ recalculatePromotion();
158
+ return;
159
+ }
153
160
  setState({ checkoutSession: data });
154
161
  } catch (err) {
155
162
  console.error(err);
@@ -159,6 +166,10 @@ function PaymentInner({
159
166
  const onDownsell = async (from) => {
160
167
  try {
161
168
  const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/downsell`, { from });
169
+ if (data.discounts?.length) {
170
+ recalculatePromotion();
171
+ return;
172
+ }
162
173
  setState({ checkoutSession: data });
163
174
  } catch (err) {
164
175
  console.error(err);
@@ -168,6 +179,10 @@ function PaymentInner({
168
179
  const onApplyCrossSell = async (to) => {
169
180
  try {
170
181
  const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`, { to });
182
+ if (data.discounts?.length) {
183
+ recalculatePromotion();
184
+ return;
185
+ }
171
186
  setState({ checkoutSession: data });
172
187
  } catch (err) {
173
188
  console.error(err);
@@ -180,6 +195,10 @@ function PaymentInner({
180
195
  itemId,
181
196
  quantity
182
197
  });
198
+ if (data.discounts?.length) {
199
+ recalculatePromotion();
200
+ return;
201
+ }
183
202
  setState({ checkoutSession: data });
184
203
  } catch (err) {
185
204
  console.error(err);
@@ -189,6 +208,10 @@ function PaymentInner({
189
208
  const onCancelCrossSell = async () => {
190
209
  try {
191
210
  const { data } = await api.delete(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`);
211
+ if (data.discounts?.length) {
212
+ recalculatePromotion();
213
+ return;
214
+ }
192
215
  setState({ checkoutSession: data });
193
216
  } catch (err) {
194
217
  console.error(err);
@@ -201,6 +224,10 @@ function PaymentInner({
201
224
  priceId,
202
225
  amount: fromTokenToUnit(amount, currency.decimal).toString()
203
226
  });
227
+ if (data.discounts?.length) {
228
+ recalculatePromotion();
229
+ return;
230
+ }
204
231
  setState({ checkoutSession: data });
205
232
  } catch (err) {
206
233
  console.error(err);