@blocklet/payment-react 1.20.13 → 1.20.15

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.
@@ -145,10 +145,7 @@ function PaymentInner({
145
145
  });
146
146
  const currency = (0, _util2.findCurrency)(paymentMethods, currencyId) || settings.baseCurrency;
147
147
  const method = paymentMethods.find(x => x.id === currency.payment_method_id);
148
- (0, _react.useEffect)(() => {
149
- if (onChange) {
150
- onChange(methods.getValues());
151
- }
148
+ const recalculatePromotion = () => {
152
149
  if (state.checkoutSession?.discounts?.length) {
153
150
  _api.default.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
154
151
  currency_id: currencyId
@@ -156,6 +153,12 @@ function PaymentInner({
156
153
  onPromotionUpdate();
157
154
  });
158
155
  }
156
+ };
157
+ (0, _react.useEffect)(() => {
158
+ if (onChange) {
159
+ onChange(methods.getValues());
160
+ }
161
+ recalculatePromotion();
159
162
  }, [currencyId]);
160
163
  const onUpsell = async (from, to) => {
161
164
  try {
@@ -165,6 +168,10 @@ function PaymentInner({
165
168
  from,
166
169
  to
167
170
  });
171
+ if (data.discounts?.length) {
172
+ recalculatePromotion();
173
+ return;
174
+ }
168
175
  setState({
169
176
  checkoutSession: data
170
177
  });
@@ -180,6 +187,10 @@ function PaymentInner({
180
187
  } = await _api.default.put(`/api/checkout-sessions/${state.checkoutSession.id}/downsell`, {
181
188
  from
182
189
  });
190
+ if (data.discounts?.length) {
191
+ recalculatePromotion();
192
+ return;
193
+ }
183
194
  setState({
184
195
  checkoutSession: data
185
196
  });
@@ -195,6 +206,10 @@ function PaymentInner({
195
206
  } = await _api.default.put(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`, {
196
207
  to
197
208
  });
209
+ if (data.discounts?.length) {
210
+ recalculatePromotion();
211
+ return;
212
+ }
198
213
  setState({
199
214
  checkoutSession: data
200
215
  });
@@ -211,6 +226,10 @@ function PaymentInner({
211
226
  itemId,
212
227
  quantity
213
228
  });
229
+ if (data.discounts?.length) {
230
+ recalculatePromotion();
231
+ return;
232
+ }
214
233
  setState({
215
234
  checkoutSession: data
216
235
  });
@@ -224,6 +243,10 @@ function PaymentInner({
224
243
  const {
225
244
  data
226
245
  } = await _api.default.delete(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`);
246
+ if (data.discounts?.length) {
247
+ recalculatePromotion();
248
+ return;
249
+ }
227
250
  setState({
228
251
  checkoutSession: data
229
252
  });
@@ -243,6 +266,10 @@ function PaymentInner({
243
266
  priceId,
244
267
  amount: (0, _util.fromTokenToUnit)(amount, currency.decimal).toString()
245
268
  });
269
+ if (data.discounts?.length) {
270
+ recalculatePromotion();
271
+ return;
272
+ }
246
273
  setState({
247
274
  checkoutSession: data
248
275
  });
@@ -44,7 +44,7 @@ function PaymentSuccess({
44
44
  const response = await api.get((0, _ufo.joinURL)(prefix, `/api/vendors/order/${sessionId}/status`), {});
45
45
  const needCheckError = Date.now() - timerRef.current > 6 * 1e3;
46
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);
47
+ const hasAnyFailed = response.data?.vendors?.some(vendor => vendor.status === "failed" || needCheckError && !!vendor.error && !!vendor.error_message);
48
48
  if (hasAnyFailed || allCompleted) {
49
49
  clearInterval(interval2);
50
50
  }
@@ -44,11 +44,11 @@ const ExpandMore = (0, _styles.styled)(props => {
44
44
  duration: theme.transitions.duration.shortest
45
45
  })
46
46
  }));
47
- async function fetchCrossSell(id) {
47
+ async function fetchCrossSell(id, skipError = true) {
48
48
  try {
49
49
  const {
50
50
  data
51
- } = await _api.default.get(`/api/checkout-sessions/${id}/cross-sell`);
51
+ } = await _api.default.get(`/api/checkout-sessions/${id}/cross-sell?skipError=${skipError}`);
52
52
  if (!data.error) {
53
53
  return data;
54
54
  }
@@ -127,7 +127,7 @@ function PaymentSummary({
127
127
  const {
128
128
  data,
129
129
  runAsync
130
- } = (0, _ahooks.useRequest)(() => checkoutSessionId ? fetchCrossSell(checkoutSessionId) : Promise.resolve(null));
130
+ } = (0, _ahooks.useRequest)(skipError => checkoutSessionId ? fetchCrossSell(checkoutSessionId, skipError) : Promise.resolve(null));
131
131
  const sessionDiscounts = checkoutSession?.discounts || [];
132
132
  const allowPromotionCodes = !!checkoutSession?.allow_promotion_codes;
133
133
  const hasDiscounts = sessionDiscounts?.length > 0;
@@ -174,15 +174,15 @@ function PaymentSummary({
174
174
  }, []);
175
175
  const handleUpsell = async (from, to) => {
176
176
  await onUpsell(from, to);
177
- runAsync();
177
+ runAsync(false);
178
178
  };
179
179
  const handleQuantityChange = async (itemId, quantity) => {
180
180
  await onQuantityChange(itemId, quantity);
181
- runAsync();
181
+ runAsync(false);
182
182
  };
183
183
  const handleDownsell = async from => {
184
184
  await onDownsell(from);
185
- runAsync();
185
+ runAsync(false);
186
186
  };
187
187
  const handleApplyCrossSell = async () => {
188
188
  if (data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.20.13",
3
+ "version": "1.20.15",
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.13",
97
+ "@blocklet/payment-types": "1.20.15",
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": "0cbe918549f7d06561b5307fb947bfbbd2250984"
128
+ "gitHead": "d205c3b1ec7d2b819e375ed2eb8b70c9d48f0bcb"
129
129
  }
@@ -0,0 +1,295 @@
1
+ // React not needed for this component
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
+
6
+ // 定义本地类型,避免依赖问题
7
+ type LocalizedText = {
8
+ zh: string;
9
+ en: string;
10
+ };
11
+
12
+ // 简单格式适合大部分业务场景
13
+ type SimpleSourceData = Record<string, string>;
14
+
15
+ // 结构化格式适合复杂场景
16
+ type StructuredSourceDataField = {
17
+ key: string;
18
+ label: string | LocalizedText;
19
+ value: string;
20
+ type?: 'text' | 'image' | 'url';
21
+ url?: string;
22
+ group?: string;
23
+ };
24
+
25
+ // 混合类型:支持渐进式接入
26
+ type SourceData = SimpleSourceData | StructuredSourceDataField[];
27
+
28
+ interface SourceDataViewerProps {
29
+ data: SourceData;
30
+ compact?: boolean;
31
+ maxItems?: number;
32
+ locale?: 'en' | 'zh';
33
+ showGroups?: boolean; // 是否显示分组
34
+ }
35
+
36
+ function SourceDataViewer({
37
+ data,
38
+ compact = false,
39
+ maxItems = undefined,
40
+ locale: propLocale = undefined,
41
+ showGroups = false,
42
+ }: SourceDataViewerProps) {
43
+ const { locale: contextLocale, t } = useLocaleContext();
44
+ const currentLocale = propLocale || (contextLocale as 'en' | 'zh') || 'en';
45
+
46
+ if (
47
+ !data ||
48
+ (Array.isArray(data) && data.length === 0) ||
49
+ (typeof data === 'object' && Object.keys(data).length === 0)
50
+ ) {
51
+ return <Empty>{t('common.none')}</Empty>;
52
+ }
53
+
54
+ const getLocalizedText = (text: string | LocalizedText): string => {
55
+ if (typeof text === 'string') {
56
+ return text;
57
+ }
58
+ return text[currentLocale] || text.en || '';
59
+ };
60
+
61
+ /**
62
+ * Type guards
63
+ */
64
+ const isSimpleSourceData = (sourceData: SourceData): sourceData is SimpleSourceData => {
65
+ return typeof sourceData === 'object' && !Array.isArray(sourceData) && sourceData !== null;
66
+ };
67
+
68
+ /**
69
+ * Auto detect URL pattern
70
+ */
71
+ const isUrlLike = (str: string): boolean => {
72
+ try {
73
+ // eslint-disable-next-line no-new
74
+ new URL(str);
75
+ return true;
76
+ } catch {
77
+ return /^https?:\/\/.+/.test(str) || /^www\..+/.test(str);
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Convert any format to normalized structured format with auto URL detection
83
+ */
84
+ const normalizeData = (inputData: SourceData): StructuredSourceDataField[] => {
85
+ if (isSimpleSourceData(inputData)) {
86
+ return Object.entries(inputData).map(([key, value]) => {
87
+ const stringValue = String(value);
88
+ const isUrl = isUrlLike(stringValue);
89
+ return {
90
+ key,
91
+ label: key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()),
92
+ value: stringValue,
93
+ type: isUrl ? ('url' as const) : ('text' as const),
94
+ url: isUrl ? stringValue : undefined,
95
+ };
96
+ });
97
+ }
98
+ return inputData;
99
+ };
100
+
101
+ /**
102
+ * Render field value based on type
103
+ */
104
+ const renderFieldValue = (field: StructuredSourceDataField) => {
105
+ const displayValue = field.value;
106
+
107
+ if (field.type === 'url') {
108
+ return (
109
+ <Link
110
+ href={field.url || field.value}
111
+ target="_blank"
112
+ rel="noopener noreferrer"
113
+ sx={{
114
+ color: 'primary.main',
115
+ textDecoration: 'none',
116
+ fontSize: '0.85rem',
117
+ fontWeight: 600,
118
+ lineHeight: 1.4,
119
+ '&:hover': {
120
+ textDecoration: 'underline',
121
+ color: 'primary.dark',
122
+ },
123
+ '&:visited': {
124
+ color: 'primary.main',
125
+ },
126
+ }}>
127
+ {displayValue}
128
+ </Link>
129
+ );
130
+ }
131
+
132
+ if (field.type === 'image') {
133
+ return (
134
+ <Box
135
+ sx={{
136
+ display: 'flex',
137
+ alignItems: 'center',
138
+ gap: 1,
139
+ }}>
140
+ <Box
141
+ component="img"
142
+ src={field.url || field.value}
143
+ alt={displayValue}
144
+ sx={{
145
+ width: 24,
146
+ height: 24,
147
+ borderRadius: 1,
148
+ objectFit: 'cover',
149
+ }}
150
+ />
151
+ <Typography variant="body2" sx={{ fontWeight: 500 }}>
152
+ {displayValue}
153
+ </Typography>
154
+ </Box>
155
+ );
156
+ }
157
+
158
+ return (
159
+ <Typography
160
+ variant="body2"
161
+ sx={{
162
+ fontWeight: 500,
163
+ color: 'text.primary',
164
+ }}>
165
+ {displayValue}
166
+ </Typography>
167
+ );
168
+ };
169
+
170
+ if (
171
+ !data ||
172
+ (Array.isArray(data) && data.length === 0) ||
173
+ (typeof data === 'object' && Object.keys(data).length === 0)
174
+ ) {
175
+ return null;
176
+ }
177
+
178
+ const normalizedData = normalizeData(data);
179
+ const displayItems = maxItems ? normalizedData.slice(0, maxItems) : normalizedData;
180
+
181
+ if (compact) {
182
+ return (
183
+ <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
184
+ {displayItems.map((field) => (
185
+ <Chip
186
+ key={field.key}
187
+ label={`${getLocalizedText(field.label)}: ${field.value}`}
188
+ size="small"
189
+ variant="outlined"
190
+ sx={{ maxWidth: 200 }}
191
+ />
192
+ ))}
193
+ {maxItems && normalizedData.length > maxItems && (
194
+ <Chip label={`+${normalizedData.length - maxItems} more`} size="small" color="primary" variant="outlined" />
195
+ )}
196
+ </Stack>
197
+ );
198
+ }
199
+
200
+ if (showGroups) {
201
+ // Group by group field
202
+ const groupedData = displayItems.reduce(
203
+ (acc, field) => {
204
+ const group = field.group || 'default';
205
+ if (!acc[group]) acc[group] = [];
206
+ acc[group].push(field);
207
+ return acc;
208
+ },
209
+ {} as Record<string, StructuredSourceDataField[]>
210
+ );
211
+
212
+ return (
213
+ <Stack spacing={2.5}>
214
+ {Object.entries(groupedData).map(([group, fields]) => (
215
+ <Box key={group}>
216
+ {group !== 'default' && (
217
+ <Typography
218
+ variant="subtitle2"
219
+ sx={{
220
+ mb: 1.5,
221
+ fontWeight: 700,
222
+ color: 'text.primary',
223
+ letterSpacing: '0.5px',
224
+ borderBottom: '1px solid',
225
+ borderColor: 'divider',
226
+ pb: 0.5,
227
+ }}>
228
+ {group.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())}
229
+ </Typography>
230
+ )}
231
+ <Stack spacing={1.2} sx={{ pl: group !== 'default' ? 1 : 0 }}>
232
+ {fields.map((field) => (
233
+ <Box
234
+ key={field.key}
235
+ sx={{
236
+ display: 'flex',
237
+ flexDirection: 'row',
238
+ alignItems: 'flex-start',
239
+ gap: 3,
240
+ minHeight: 20,
241
+ }}>
242
+ <Typography
243
+ variant="body2"
244
+ color="text.secondary"
245
+ sx={{
246
+ fontSize: '0.8rem',
247
+ minWidth: 100,
248
+ fontWeight: 600,
249
+ lineHeight: 1.4,
250
+ }}>
251
+ {getLocalizedText(field.label)}
252
+ </Typography>
253
+ <Box sx={{ flex: 1, minWidth: 0, wordBreak: 'break-all', whiteSpace: 'wrap' }}>
254
+ {renderFieldValue(field)}
255
+ </Box>
256
+ </Box>
257
+ ))}
258
+ </Stack>
259
+ </Box>
260
+ ))}
261
+ </Stack>
262
+ );
263
+ }
264
+
265
+ return (
266
+ <Stack spacing={1.5}>
267
+ {displayItems.map((field) => (
268
+ <Box
269
+ key={field.key}
270
+ sx={{
271
+ display: 'flex',
272
+ flexDirection: 'row',
273
+ alignItems: 'flex-start',
274
+ gap: 3,
275
+ minHeight: 20,
276
+ }}>
277
+ <Typography
278
+ variant="body2"
279
+ color="text.secondary"
280
+ sx={{
281
+ fontSize: '0.8rem',
282
+ minWidth: 100,
283
+ fontWeight: 600,
284
+ lineHeight: 1.4,
285
+ }}>
286
+ {getLocalizedText(field.label)}
287
+ </Typography>
288
+ <Box sx={{ flex: 1, minWidth: 0 }}>{renderFieldValue(field)}</Box>
289
+ </Box>
290
+ ))}
291
+ </Stack>
292
+ );
293
+ }
294
+
295
+ export default SourceDataViewer;