@blocklet/payment-react 1.13.282 → 1.13.283

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.
@@ -31,10 +31,12 @@ export default function CheckoutTable({ id, mode, onPaid, onError, onChange, ext
31
31
  if (data.items.length === 0) {
32
32
  return /* @__PURE__ */ jsx(Alert, { severity: "warning", children: t("payment.checkout.noPricing") });
33
33
  }
34
- const handleSelect = (priceId) => {
34
+ const handleSelect = (priceId, currencyId) => {
35
35
  api.post(`/api/pricing-tables/${data.id}/checkout/${priceId}?${mergeExtraParams(extraParams)}`).then((res) => {
36
36
  if (mode === "standalone") {
37
- window.location.replace(res.data.url);
37
+ let { url } = res.data;
38
+ url = url.indexOf("?") > -1 ? `${url}&currencyId=${currencyId}` : `${url}?currencyId=${currencyId}`;
39
+ window.location.replace(url);
38
40
  } else {
39
41
  window.location.hash = res.data.id;
40
42
  setSessionId(res.data.id);
@@ -2,7 +2,7 @@
2
2
  import type { TPricingTableExpanded } from '@blocklet/payment-types';
3
3
  type Props = {
4
4
  table: TPricingTableExpanded;
5
- onSelect: (priceId: string) => void;
5
+ onSelect: (priceId: string, currencyId: string) => void;
6
6
  alignItems?: 'center' | 'left';
7
7
  mode?: 'checkout' | 'select';
8
8
  interval?: string;
@@ -10,6 +10,8 @@ import {
10
10
  ListItem,
11
11
  ListItemIcon,
12
12
  ListItemText,
13
+ MenuItem,
14
+ Select,
13
15
  Stack,
14
16
  ToggleButton,
15
17
  ToggleButtonGroup,
@@ -17,7 +19,8 @@ import {
17
19
  } from "@mui/material";
18
20
  import { styled } from "@mui/system";
19
21
  import { useSetState } from "ahooks";
20
- import { useEffect, useState } from "react";
22
+ import { useEffect, useMemo, useState } from "react";
23
+ import { usePaymentContext } from "../contexts/payment.js";
21
24
  import { formatError, formatPriceAmount, formatRecurring } from "../libs/util.js";
22
25
  import Amount from "../payment/amount.js";
23
26
  const groupItemsByRecurring = (items) => {
@@ -40,8 +43,31 @@ PricingTable.defaultProps = {
40
43
  };
41
44
  export default function PricingTable({ table, alignItems, interval, mode, onSelect }) {
42
45
  const { t, locale } = useLocaleContext();
46
+ const {
47
+ settings: { paymentMethods = [] }
48
+ } = usePaymentContext();
49
+ const [currency, setCurrency] = useState(table.currency);
43
50
  const { recurring, grouped } = groupItemsByRecurring(table.items);
44
51
  const [state, setState] = useSetState({ interval });
52
+ const currencyMap = useMemo(() => {
53
+ const { payment_currencies: paymentCurrencies = [] } = paymentMethods[0];
54
+ return paymentCurrencies.reduce((acc, x) => {
55
+ acc[x.id] = x;
56
+ return acc;
57
+ }, {});
58
+ }, [paymentMethods]);
59
+ const currencyList = useMemo(() => {
60
+ const visited = {};
61
+ if (!state.interval) {
62
+ return [];
63
+ }
64
+ grouped[state.interval].forEach((x) => {
65
+ x.price.currency_options.forEach((c) => {
66
+ visited[c?.currency_id] = true;
67
+ });
68
+ });
69
+ return Object.keys(visited).map((x) => currencyMap[x]);
70
+ }, [currencyMap, grouped, state.interval]);
45
71
  useEffect(() => {
46
72
  if (table) {
47
73
  if (!state.interval || !grouped[state.interval]) {
@@ -53,10 +79,21 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
53
79
  }
54
80
  }, [table]);
55
81
  const Root = styled(Box)`
82
+ .btn-row {
83
+ display: flex;
84
+ flex-wrap: wrap;
85
+ justify-content: space-between;
86
+ align-items: center;
87
+ width: 100%;
88
+ gap: 20px;
89
+ }
56
90
  @media (max-width: ${({ theme }) => theme.breakpoints.values.sm}px) {
57
91
  .price-table-item {
58
92
  width: 90% !important;
59
93
  }
94
+ .btn-row {
95
+ padding: 0 20px;
96
+ }
60
97
  }
61
98
  @media (min-width: ${({ theme }) => theme.breakpoints.values.md}px) {
62
99
  .price-table-wrap:has(> div:nth-child(1)) {
@@ -82,47 +119,59 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
82
119
  }
83
120
  },
84
121
  children: [
85
- Object.keys(recurring).length > 1 && /* @__PURE__ */ jsx(
86
- ToggleButtonGroup,
87
- {
88
- size: "small",
89
- value: state.interval,
90
- sx: {
91
- padding: "4px",
92
- borderRadius: "36px",
93
- height: "40px",
94
- boxSizing: "border-box",
95
- backgroundColor: "#f1f3f5",
96
- border: 0
97
- },
98
- onChange: (_, value) => {
99
- if (value !== null) {
100
- setState({ interval: value });
101
- }
102
- },
103
- exclusive: true,
104
- children: Object.keys(recurring).map((x) => /* @__PURE__ */ jsx(
105
- ToggleButton,
106
- {
107
- size: "small",
108
- value: x,
109
- sx: {
110
- textTransform: "capitalize",
111
- padding: "5px 12px",
112
- fontSize: "13px",
113
- backgroundColor: x === state.interval ? "#fff !important" : "#f1f3f5 !important",
114
- border: "0px",
115
- "&.Mui-selected": {
116
- borderRadius: "9999px !important",
117
- border: "1px solid #e5e7eb"
118
- }
119
- },
120
- children: formatRecurring(recurring[x], true, "", locale)
122
+ /* @__PURE__ */ jsxs("div", { className: "btn-row", children: [
123
+ Object.keys(recurring).length > 1 && /* @__PURE__ */ jsx(
124
+ ToggleButtonGroup,
125
+ {
126
+ size: "small",
127
+ value: state.interval,
128
+ sx: {
129
+ padding: "4px",
130
+ borderRadius: "36px",
131
+ height: "40px",
132
+ boxSizing: "border-box",
133
+ backgroundColor: "#f1f3f5",
134
+ border: 0
121
135
  },
122
- x
123
- ))
124
- }
125
- ),
136
+ onChange: (_, value) => {
137
+ if (value !== null) {
138
+ setState({ interval: value });
139
+ }
140
+ },
141
+ exclusive: true,
142
+ children: Object.keys(recurring).map((x) => /* @__PURE__ */ jsx(
143
+ ToggleButton,
144
+ {
145
+ size: "small",
146
+ value: x,
147
+ sx: {
148
+ textTransform: "capitalize",
149
+ padding: "5px 12px",
150
+ fontSize: "13px",
151
+ backgroundColor: x === state.interval ? "#fff !important" : "#f1f3f5 !important",
152
+ border: "0px",
153
+ "&.Mui-selected": {
154
+ borderRadius: "9999px !important",
155
+ border: "1px solid #e5e7eb"
156
+ }
157
+ },
158
+ children: formatRecurring(recurring[x], true, "", locale)
159
+ },
160
+ x
161
+ ))
162
+ }
163
+ ),
164
+ currencyList.length > 1 && /* @__PURE__ */ jsx(
165
+ Select,
166
+ {
167
+ value: currency.id,
168
+ onChange: (e) => setCurrency(currencyList.find((v) => v.id === e.target.value)),
169
+ size: "small",
170
+ sx: { m: 1, width: 120 },
171
+ children: currencyList.map((x) => /* @__PURE__ */ jsx(MenuItem, { value: x.id, children: /* @__PURE__ */ jsx(Typography, { color: x.id === currency.id ? "text.primary" : "text.secondary", children: x.symbol }) }, x.id))
172
+ }
173
+ )
174
+ ] }),
126
175
  /* @__PURE__ */ jsx(
127
176
  Stack,
128
177
  {
@@ -204,7 +253,7 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
204
253
  /* @__PURE__ */ jsx(
205
254
  Amount,
206
255
  {
207
- amount: formatPriceAmount(x.price, table.currency, x.product.unit_label),
256
+ amount: formatPriceAmount(x.price, currency, x.product.unit_label),
208
257
  sx: { my: 0, marginTop: "0px !important", fontSize: "48px", fontWeight: "bold" }
209
258
  }
210
259
  ),
@@ -265,7 +314,7 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
265
314
  )
266
315
  ] }, f.name))
267
316
  ] }) }),
268
- /* @__PURE__ */ jsx(Subscribe, { x, action, onSelect })
317
+ /* @__PURE__ */ jsx(Subscribe, { x, action, onSelect, currencyId: currency.id })
269
318
  ]
270
319
  },
271
320
  x?.price_id
@@ -278,12 +327,12 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
278
327
  }
279
328
  ) });
280
329
  }
281
- function Subscribe({ x, action, onSelect }) {
330
+ function Subscribe({ x, action, onSelect, currencyId }) {
282
331
  const [state, setState] = useState({ loading: "", loaded: false });
283
332
  const handleSelect = async (priceId) => {
284
333
  try {
285
334
  setState({ loading: priceId, loaded: true });
286
- await onSelect(priceId);
335
+ await onSelect(priceId, currencyId);
287
336
  } catch (err) {
288
337
  console.error(err);
289
338
  Toast.error(formatError(err));
package/es/libs/util.d.ts CHANGED
@@ -67,3 +67,4 @@ export declare const getTxLink: (method: TPaymentMethod, details: PaymentDetails
67
67
  text: string;
68
68
  gas: any;
69
69
  };
70
+ export declare function getQueryParams(url: string): Record<string, string>;
package/es/libs/util.js CHANGED
@@ -609,3 +609,11 @@ export const getTxLink = (method, details) => {
609
609
  }
610
610
  return { text: "N/A", link: "", gas: "" };
611
611
  };
612
+ export function getQueryParams(url) {
613
+ const queryParams = {};
614
+ const urlObj = new URL(url);
615
+ urlObj.searchParams.forEach((value, key) => {
616
+ queryParams[key] = value;
617
+ });
618
+ return queryParams;
619
+ }
@@ -18,7 +18,7 @@ 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, getStatementDescriptor } from "../../libs/util.js";
21
+ import { flattenPaymentMethods, formatError, getPrefix, getQueryParams, getStatementDescriptor } from "../../libs/util.js";
22
22
  import UserButtons from "./addon.js";
23
23
  import AddressForm from "./address.js";
24
24
  import CurrencySelector from "./currency.js";
@@ -72,7 +72,11 @@ export default function PaymentForm({
72
72
  stripePaying: false
73
73
  });
74
74
  const currencies = flattenPaymentMethods(paymentMethods);
75
- const [paymentCurrencyIndex, setPaymentCurrencyIndex] = useState(0);
75
+ const [paymentCurrencyIndex, setPaymentCurrencyIndex] = useState(() => {
76
+ const query = getQueryParams(window.location.href);
77
+ const index = currencies.findIndex((x) => x.id === query.currencyId);
78
+ return index >= 0 ? index : 0;
79
+ });
76
80
  const onCheckoutComplete = useMemoizedFn(async ({ response }) => {
77
81
  if (response.id === checkoutSession.id && state.paid === false) {
78
82
  await handleConnected();
@@ -60,10 +60,14 @@ function CheckoutTable({
60
60
  children: t("payment.checkout.noPricing")
61
61
  });
62
62
  }
63
- const handleSelect = priceId => {
63
+ const handleSelect = (priceId, currencyId) => {
64
64
  _api.default.post(`/api/pricing-tables/${data.id}/checkout/${priceId}?${(0, _util.mergeExtraParams)(extraParams)}`).then(res => {
65
65
  if (mode === "standalone") {
66
- window.location.replace(res.data.url);
66
+ let {
67
+ url
68
+ } = res.data;
69
+ url = url.indexOf("?") > -1 ? `${url}&currencyId=${currencyId}` : `${url}?currencyId=${currencyId}`;
70
+ window.location.replace(url);
67
71
  } else {
68
72
  window.location.hash = res.data.id;
69
73
  setSessionId(res.data.id);
@@ -2,7 +2,7 @@
2
2
  import type { TPricingTableExpanded } from '@blocklet/payment-types';
3
3
  type Props = {
4
4
  table: TPricingTableExpanded;
5
- onSelect: (priceId: string) => void;
5
+ onSelect: (priceId: string, currencyId: string) => void;
6
6
  alignItems?: 'center' | 'left';
7
7
  mode?: 'checkout' | 'select';
8
8
  interval?: string;
@@ -13,6 +13,7 @@ var _material = require("@mui/material");
13
13
  var _system = require("@mui/system");
14
14
  var _ahooks = require("ahooks");
15
15
  var _react = require("react");
16
+ var _payment = require("../contexts/payment");
16
17
  var _util = require("../libs/util");
17
18
  var _amount = _interopRequireDefault(require("../payment/amount"));
18
19
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -48,6 +49,12 @@ function PricingTable({
48
49
  t,
49
50
  locale
50
51
  } = (0, _context.useLocaleContext)();
52
+ const {
53
+ settings: {
54
+ paymentMethods = []
55
+ }
56
+ } = (0, _payment.usePaymentContext)();
57
+ const [currency, setCurrency] = (0, _react.useState)(table.currency);
51
58
  const {
52
59
  recurring,
53
60
  grouped
@@ -55,6 +62,27 @@ function PricingTable({
55
62
  const [state, setState] = (0, _ahooks.useSetState)({
56
63
  interval
57
64
  });
65
+ const currencyMap = (0, _react.useMemo)(() => {
66
+ const {
67
+ payment_currencies: paymentCurrencies = []
68
+ } = paymentMethods[0];
69
+ return paymentCurrencies.reduce((acc, x) => {
70
+ acc[x.id] = x;
71
+ return acc;
72
+ }, {});
73
+ }, [paymentMethods]);
74
+ const currencyList = (0, _react.useMemo)(() => {
75
+ const visited = {};
76
+ if (!state.interval) {
77
+ return [];
78
+ }
79
+ grouped[state.interval].forEach(x => {
80
+ x.price.currency_options.forEach(c => {
81
+ visited[c?.currency_id] = true;
82
+ });
83
+ });
84
+ return Object.keys(visited).map(x => currencyMap[x]);
85
+ }, [currencyMap, grouped, state.interval]);
58
86
  (0, _react.useEffect)(() => {
59
87
  if (table) {
60
88
  if (!state.interval || !grouped[state.interval]) {
@@ -68,12 +96,23 @@ function PricingTable({
68
96
  }
69
97
  }, [table]);
70
98
  const Root = (0, _system.styled)(_material.Box)`
99
+ .btn-row {
100
+ display: flex;
101
+ flex-wrap: wrap;
102
+ justify-content: space-between;
103
+ align-items: center;
104
+ width: 100%;
105
+ gap: 20px;
106
+ }
71
107
  @media (max-width: ${({
72
108
  theme
73
109
  }) => theme.breakpoints.values.sm}px) {
74
110
  .price-table-item {
75
111
  width: 90% !important;
76
112
  }
113
+ .btn-row {
114
+ padding: 0 20px;
115
+ }
77
116
  }
78
117
  @media (min-width: ${({
79
118
  theme
@@ -99,41 +138,59 @@ function PricingTable({
99
138
  sm: mode === "select" ? 3 : 5
100
139
  }
101
140
  },
102
- children: [Object.keys(recurring).length > 1 && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.ToggleButtonGroup, {
103
- size: "small",
104
- value: state.interval,
105
- sx: {
106
- padding: "4px",
107
- borderRadius: "36px",
108
- height: "40px",
109
- boxSizing: "border-box",
110
- backgroundColor: "#f1f3f5",
111
- border: 0
112
- },
113
- onChange: (_, value) => {
114
- if (value !== null) {
115
- setState({
116
- interval: value
117
- });
118
- }
119
- },
120
- exclusive: true,
121
- children: Object.keys(recurring).map(x => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.ToggleButton, {
141
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)("div", {
142
+ className: "btn-row",
143
+ children: [Object.keys(recurring).length > 1 && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.ToggleButtonGroup, {
122
144
  size: "small",
123
- value: x,
145
+ value: state.interval,
124
146
  sx: {
125
- textTransform: "capitalize",
126
- padding: "5px 12px",
127
- fontSize: "13px",
128
- backgroundColor: x === state.interval ? "#fff !important" : "#f1f3f5 !important",
129
- border: "0px",
130
- "&.Mui-selected": {
131
- borderRadius: "9999px !important",
132
- border: "1px solid #e5e7eb"
147
+ padding: "4px",
148
+ borderRadius: "36px",
149
+ height: "40px",
150
+ boxSizing: "border-box",
151
+ backgroundColor: "#f1f3f5",
152
+ border: 0
153
+ },
154
+ onChange: (_, value) => {
155
+ if (value !== null) {
156
+ setState({
157
+ interval: value
158
+ });
133
159
  }
134
160
  },
135
- children: (0, _util.formatRecurring)(recurring[x], true, "", locale)
136
- }, x))
161
+ exclusive: true,
162
+ children: Object.keys(recurring).map(x => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.ToggleButton, {
163
+ size: "small",
164
+ value: x,
165
+ sx: {
166
+ textTransform: "capitalize",
167
+ padding: "5px 12px",
168
+ fontSize: "13px",
169
+ backgroundColor: x === state.interval ? "#fff !important" : "#f1f3f5 !important",
170
+ border: "0px",
171
+ "&.Mui-selected": {
172
+ borderRadius: "9999px !important",
173
+ border: "1px solid #e5e7eb"
174
+ }
175
+ },
176
+ children: (0, _util.formatRecurring)(recurring[x], true, "", locale)
177
+ }, x))
178
+ }), currencyList.length > 1 && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Select, {
179
+ value: currency.id,
180
+ onChange: e => setCurrency(currencyList.find(v => v.id === e.target.value)),
181
+ size: "small",
182
+ sx: {
183
+ m: 1,
184
+ width: 120
185
+ },
186
+ children: currencyList.map(x => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.MenuItem, {
187
+ value: x.id,
188
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
189
+ color: x.id === currency.id ? "text.primary" : "text.secondary",
190
+ children: x.symbol
191
+ })
192
+ }, x.id))
193
+ })]
137
194
  }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, {
138
195
  flexWrap: "wrap",
139
196
  direction: "row",
@@ -205,7 +262,7 @@ function PricingTable({
205
262
  }
206
263
  })]
207
264
  }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_amount.default, {
208
- amount: (0, _util.formatPriceAmount)(x.price, table.currency, x.product.unit_label),
265
+ amount: (0, _util.formatPriceAmount)(x.price, currency, x.product.unit_label),
209
266
  sx: {
210
267
  my: 0,
211
268
  marginTop: "0px !important",
@@ -279,7 +336,8 @@ function PricingTable({
279
336
  }), /* @__PURE__ */(0, _jsxRuntime.jsx)(Subscribe, {
280
337
  x,
281
338
  action,
282
- onSelect
339
+ onSelect,
340
+ currencyId: currency.id
283
341
  })]
284
342
  }, x?.price_id);
285
343
  })
@@ -290,7 +348,8 @@ function PricingTable({
290
348
  function Subscribe({
291
349
  x,
292
350
  action,
293
- onSelect
351
+ onSelect,
352
+ currencyId
294
353
  }) {
295
354
  const [state, setState] = (0, _react.useState)({
296
355
  loading: "",
@@ -302,7 +361,7 @@ function Subscribe({
302
361
  loading: priceId,
303
362
  loaded: true
304
363
  });
305
- await onSelect(priceId);
364
+ await onSelect(priceId, currencyId);
306
365
  } catch (err) {
307
366
  console.error(err);
308
367
  _Toast.default.error((0, _util.formatError)(err));
@@ -67,3 +67,4 @@ export declare const getTxLink: (method: TPaymentMethod, details: PaymentDetails
67
67
  text: string;
68
68
  gas: any;
69
69
  };
70
+ export declare function getQueryParams(url: string): Record<string, string>;
package/lib/libs/util.js CHANGED
@@ -28,6 +28,7 @@ exports.getPayoutStatusColor = getPayoutStatusColor;
28
28
  exports.getPrefix = void 0;
29
29
  exports.getPriceCurrencyOptions = getPriceCurrencyOptions;
30
30
  exports.getPriceUintAmountByCurrency = getPriceUintAmountByCurrency;
31
+ exports.getQueryParams = getQueryParams;
31
32
  exports.getRecurringPeriod = getRecurringPeriod;
32
33
  exports.getRefundStatusColor = getRefundStatusColor;
33
34
  exports.getStatementDescriptor = getStatementDescriptor;
@@ -724,4 +725,12 @@ const getTxLink = (method, details) => {
724
725
  gas: ""
725
726
  };
726
727
  };
727
- exports.getTxLink = getTxLink;
728
+ exports.getTxLink = getTxLink;
729
+ function getQueryParams(url) {
730
+ const queryParams = {};
731
+ const urlObj = new URL(url);
732
+ urlObj.searchParams.forEach((value, key) => {
733
+ queryParams[key] = value;
734
+ });
735
+ return queryParams;
736
+ }
@@ -91,7 +91,11 @@ function PaymentForm({
91
91
  stripePaying: false
92
92
  });
93
93
  const currencies = (0, _util.flattenPaymentMethods)(paymentMethods);
94
- const [paymentCurrencyIndex, setPaymentCurrencyIndex] = (0, _react.useState)(0);
94
+ const [paymentCurrencyIndex, setPaymentCurrencyIndex] = (0, _react.useState)(() => {
95
+ const query = (0, _util.getQueryParams)(window.location.href);
96
+ const index = currencies.findIndex(x => x.id === query.currencyId);
97
+ return index >= 0 ? index : 0;
98
+ });
95
99
  const onCheckoutComplete = (0, _ahooks.useMemoizedFn)(async ({
96
100
  response
97
101
  }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.13.282",
3
+ "version": "1.13.283",
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.282",
94
+ "@blocklet/payment-types": "1.13.283",
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": "38f01328afa9704e945163ce3f3006cb4e4d5e54"
123
+ "gitHead": "6c88365a990a15695b990346d88289372d0700cc"
124
124
  }
@@ -41,12 +41,14 @@ export default function CheckoutTable({ id, mode, onPaid, onError, onChange, ext
41
41
  return <Alert severity="warning">{t('payment.checkout.noPricing')}</Alert>;
42
42
  }
43
43
 
44
- const handleSelect = (priceId: string) => {
44
+ const handleSelect = (priceId: string, currencyId: string) => {
45
45
  api
46
46
  .post(`/api/pricing-tables/${data.id}/checkout/${priceId}?${mergeExtraParams(extraParams)}`)
47
47
  .then((res) => {
48
48
  if (mode === 'standalone') {
49
- window.location.replace(res.data.url);
49
+ let { url } = res.data;
50
+ url = url.indexOf('?') > -1 ? `${url}&currencyId=${currencyId}` : `${url}?currencyId=${currencyId}`;
51
+ window.location.replace(url);
50
52
  } else {
51
53
  window.location.hash = res.data.id;
52
54
  setSessionId(res.data.id);
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import type { PriceRecurring, TPricingTableExpanded, TPricingTableItem } from '@blocklet/payment-types';
4
+ import type { PriceCurrency, PriceRecurring, TPricingTableExpanded, TPricingTableItem } from '@blocklet/payment-types';
5
5
  import { CheckOutlined } from '@mui/icons-material';
6
6
  import { LoadingButton } from '@mui/lab';
7
7
  import {
@@ -11,6 +11,8 @@ import {
11
11
  ListItem,
12
12
  ListItemIcon,
13
13
  ListItemText,
14
+ MenuItem,
15
+ Select,
14
16
  Stack,
15
17
  ToggleButton,
16
18
  ToggleButtonGroup,
@@ -18,8 +20,9 @@ import {
18
20
  } from '@mui/material';
19
21
  import { styled } from '@mui/system';
20
22
  import { useSetState } from 'ahooks';
21
- import { useEffect, useState } from 'react';
23
+ import { useEffect, useMemo, useState } from 'react';
22
24
 
25
+ import { usePaymentContext } from '../contexts/payment';
23
26
  import { formatError, formatPriceAmount, formatRecurring } from '../libs/util';
24
27
  import Amount from '../payment/amount';
25
28
 
@@ -44,7 +47,7 @@ const groupItemsByRecurring = (items: TPricingTableItem[]) => {
44
47
 
45
48
  type Props = {
46
49
  table: TPricingTableExpanded;
47
- onSelect: (priceId: string) => void;
50
+ onSelect: (priceId: string, currencyId: string) => void;
48
51
  alignItems?: 'center' | 'left';
49
52
  mode?: 'checkout' | 'select';
50
53
  interval?: string;
@@ -58,8 +61,32 @@ PricingTable.defaultProps = {
58
61
 
59
62
  export default function PricingTable({ table, alignItems, interval, mode, onSelect }: Props) {
60
63
  const { t, locale } = useLocaleContext();
64
+ const {
65
+ settings: { paymentMethods = [] },
66
+ } = usePaymentContext();
67
+ const [currency, setCurrency] = useState(table.currency);
61
68
  const { recurring, grouped } = groupItemsByRecurring(table.items);
62
69
  const [state, setState] = useSetState({ interval });
70
+ const currencyMap = useMemo(() => {
71
+ const { payment_currencies: paymentCurrencies = [] } = paymentMethods[0];
72
+ return paymentCurrencies.reduce((acc: any, x: { id: string; symbol: string }) => {
73
+ acc[x.id] = x;
74
+ return acc;
75
+ }, {});
76
+ }, [paymentMethods]);
77
+
78
+ const currencyList = useMemo(() => {
79
+ const visited: { [key: string]: boolean } = {};
80
+ if (!state.interval) {
81
+ return [];
82
+ }
83
+ grouped[state.interval].forEach((x: TPricingTableItem) => {
84
+ x.price.currency_options.forEach((c: PriceCurrency) => {
85
+ visited[c?.currency_id] = true;
86
+ });
87
+ });
88
+ return Object.keys(visited).map((x) => currencyMap[x]);
89
+ }, [currencyMap, grouped, state.interval]);
63
90
 
64
91
  useEffect(() => {
65
92
  if (table) {
@@ -74,10 +101,21 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
74
101
  }, [table]);
75
102
 
76
103
  const Root = styled(Box)`
104
+ .btn-row {
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ justify-content: space-between;
108
+ align-items: center;
109
+ width: 100%;
110
+ gap: 20px;
111
+ }
77
112
  @media (max-width: ${({ theme }) => theme.breakpoints.values.sm}px) {
78
113
  .price-table-item {
79
114
  width: 90% !important;
80
115
  }
116
+ .btn-row {
117
+ padding: 0 20px;
118
+ }
81
119
  }
82
120
  @media (min-width: ${({ theme }) => theme.breakpoints.values.md}px) {
83
121
  .price-table-wrap:has(> div:nth-child(1)) {
@@ -102,45 +140,60 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
102
140
  sm: mode === 'select' ? 3 : 5,
103
141
  },
104
142
  }}>
105
- {Object.keys(recurring).length > 1 && (
106
- <ToggleButtonGroup
107
- size="small"
108
- value={state.interval}
109
- sx={{
110
- padding: '4px',
111
- borderRadius: '36px',
112
- height: '40px',
113
- boxSizing: 'border-box',
114
- backgroundColor: '#f1f3f5',
115
- border: 0,
116
- }}
117
- onChange={(_, value) => {
118
- if (value !== null) {
119
- setState({ interval: value });
120
- }
121
- }}
122
- exclusive>
123
- {Object.keys(recurring).map((x) => (
124
- <ToggleButton
125
- size="small"
126
- key={x}
127
- value={x}
128
- sx={{
129
- textTransform: 'capitalize',
130
- padding: '5px 12px',
131
- fontSize: '13px',
132
- backgroundColor: x === state.interval ? '#fff !important' : '#f1f3f5 !important',
133
- border: '0px',
134
- '&.Mui-selected': {
135
- borderRadius: '9999px !important',
136
- border: '1px solid #e5e7eb',
137
- },
138
- }}>
139
- {formatRecurring(recurring[x] as PriceRecurring, true, '', locale)}
140
- </ToggleButton>
141
- ))}
142
- </ToggleButtonGroup>
143
- )}
143
+ <div className="btn-row">
144
+ {Object.keys(recurring).length > 1 && (
145
+ <ToggleButtonGroup
146
+ size="small"
147
+ value={state.interval}
148
+ sx={{
149
+ padding: '4px',
150
+ borderRadius: '36px',
151
+ height: '40px',
152
+ boxSizing: 'border-box',
153
+ backgroundColor: '#f1f3f5',
154
+ border: 0,
155
+ }}
156
+ onChange={(_, value) => {
157
+ if (value !== null) {
158
+ setState({ interval: value });
159
+ }
160
+ }}
161
+ exclusive>
162
+ {Object.keys(recurring).map((x) => (
163
+ <ToggleButton
164
+ size="small"
165
+ key={x}
166
+ value={x}
167
+ sx={{
168
+ textTransform: 'capitalize',
169
+ padding: '5px 12px',
170
+ fontSize: '13px',
171
+ backgroundColor: x === state.interval ? '#fff !important' : '#f1f3f5 !important',
172
+ border: '0px',
173
+ '&.Mui-selected': {
174
+ borderRadius: '9999px !important',
175
+ border: '1px solid #e5e7eb',
176
+ },
177
+ }}>
178
+ {formatRecurring(recurring[x] as PriceRecurring, true, '', locale)}
179
+ </ToggleButton>
180
+ ))}
181
+ </ToggleButtonGroup>
182
+ )}
183
+ {currencyList.length > 1 && (
184
+ <Select
185
+ value={currency.id}
186
+ onChange={(e) => setCurrency(currencyList.find((v) => v.id === e.target.value))}
187
+ size="small"
188
+ sx={{ m: 1, width: 120 }}>
189
+ {currencyList.map((x) => (
190
+ <MenuItem key={x.id} value={x.id}>
191
+ <Typography color={x.id === currency.id ? 'text.primary' : 'text.secondary'}>{x.symbol}</Typography>
192
+ </MenuItem>
193
+ ))}
194
+ </Select>
195
+ )}
196
+ </div>
144
197
  <Stack
145
198
  flexWrap="wrap"
146
199
  direction="row"
@@ -216,7 +269,7 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
216
269
  )}
217
270
  </Box>
218
271
  <Amount
219
- amount={formatPriceAmount(x.price, table.currency, x.product.unit_label)}
272
+ amount={formatPriceAmount(x.price, currency, x.product.unit_label)}
220
273
  sx={{ my: 0, marginTop: '0px !important', fontSize: '48px', fontWeight: 'bold' }}
221
274
  />
222
275
  <Typography
@@ -271,7 +324,7 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
271
324
  </List>
272
325
  </Box>
273
326
  )}
274
- <Subscribe x={x} action={action} onSelect={onSelect} />
327
+ <Subscribe x={x} action={action} onSelect={onSelect} currencyId={currency.id} />
275
328
  </Stack>
276
329
  );
277
330
  }
@@ -282,13 +335,13 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
282
335
  );
283
336
  }
284
337
 
285
- function Subscribe({ x, action, onSelect }: any) {
338
+ function Subscribe({ x, action, onSelect, currencyId }: any) {
286
339
  const [state, setState] = useState({ loading: '', loaded: false });
287
340
 
288
341
  const handleSelect = async (priceId: string) => {
289
342
  try {
290
343
  setState({ loading: priceId, loaded: true });
291
- await onSelect(priceId);
344
+ await onSelect(priceId, currencyId);
292
345
  } catch (err) {
293
346
  console.error(err);
294
347
  Toast.error(formatError(err));
package/src/libs/util.ts CHANGED
@@ -793,3 +793,12 @@ export const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
793
793
 
794
794
  return { text: 'N/A', link: '', gas: '' };
795
795
  };
796
+
797
+ export function getQueryParams(url: string): Record<string, string> {
798
+ const queryParams: Record<string, string> = {};
799
+ const urlObj = new URL(url);
800
+ urlObj.searchParams.forEach((value, key) => {
801
+ queryParams[key] = value;
802
+ });
803
+ return queryParams;
804
+ }
@@ -26,7 +26,7 @@ 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, getStatementDescriptor } from '../../libs/util';
29
+ import { flattenPaymentMethods, formatError, getPrefix, getQueryParams, getStatementDescriptor } from '../../libs/util';
30
30
  import { CheckoutCallbacks, CheckoutContext } from '../../types';
31
31
  import UserButtons from './addon';
32
32
  import AddressForm from './address';
@@ -119,7 +119,12 @@ export default function PaymentForm({
119
119
  });
120
120
 
121
121
  const currencies = flattenPaymentMethods(paymentMethods);
122
- const [paymentCurrencyIndex, setPaymentCurrencyIndex] = useState(0);
122
+
123
+ const [paymentCurrencyIndex, setPaymentCurrencyIndex] = useState(() => {
124
+ const query = getQueryParams(window.location.href);
125
+ const index = currencies.findIndex((x) => x.id === query.currencyId);
126
+ return index >= 0 ? index : 0;
127
+ });
123
128
 
124
129
  const onCheckoutComplete = useMemoizedFn(async ({ response }: { response: TCheckoutSession }) => {
125
130
  if (response.id === checkoutSession.id && state.paid === false) {