payment-kit 1.24.3 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/crons/overdue-detection.ts +10 -1
- package/api/src/index.ts +3 -0
- package/api/src/libs/credit-utils.ts +21 -0
- package/api/src/libs/discount/discount.ts +13 -0
- package/api/src/libs/env.ts +5 -0
- package/api/src/libs/error.ts +14 -0
- package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
- package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
- package/api/src/libs/exchange-rate/index.ts +5 -0
- package/api/src/libs/exchange-rate/service.ts +583 -0
- package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
- package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
- package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
- package/api/src/libs/exchange-rate/types.ts +114 -0
- package/api/src/libs/exchange-rate/validator.ts +319 -0
- package/api/src/libs/invoice-quote.ts +158 -0
- package/api/src/libs/invoice.ts +143 -7
- package/api/src/libs/math-utils.ts +46 -0
- package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
- package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
- package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
- package/api/src/libs/payment.ts +1 -1
- package/api/src/libs/price.ts +4 -1
- package/api/src/libs/queue/index.ts +8 -0
- package/api/src/libs/quote-service.ts +1132 -0
- package/api/src/libs/quote-validation.ts +388 -0
- package/api/src/libs/session.ts +686 -39
- package/api/src/libs/slippage.ts +135 -0
- package/api/src/libs/subscription.ts +185 -15
- package/api/src/libs/util.ts +64 -3
- package/api/src/locales/en.ts +50 -0
- package/api/src/locales/zh.ts +48 -0
- package/api/src/queues/auto-recharge.ts +295 -21
- package/api/src/queues/exchange-rate-health.ts +242 -0
- package/api/src/queues/invoice.ts +48 -1
- package/api/src/queues/notification.ts +190 -3
- package/api/src/queues/payment.ts +177 -7
- package/api/src/queues/subscription.ts +436 -6
- package/api/src/routes/auto-recharge-configs.ts +71 -6
- package/api/src/routes/checkout-sessions.ts +1730 -81
- package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
- package/api/src/routes/connect/change-payer.ts +2 -0
- package/api/src/routes/connect/change-payment.ts +61 -8
- package/api/src/routes/connect/change-plan.ts +161 -17
- package/api/src/routes/connect/collect.ts +9 -6
- package/api/src/routes/connect/delegation.ts +1 -0
- package/api/src/routes/connect/pay.ts +157 -0
- package/api/src/routes/connect/setup.ts +32 -10
- package/api/src/routes/connect/shared.ts +159 -13
- package/api/src/routes/connect/subscribe.ts +32 -9
- package/api/src/routes/credit-grants.ts +99 -0
- package/api/src/routes/exchange-rate-providers.ts +248 -0
- package/api/src/routes/exchange-rates.ts +87 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +280 -2
- package/api/src/routes/meter-events.ts +3 -0
- package/api/src/routes/payment-links.ts +13 -0
- package/api/src/routes/prices.ts +84 -2
- package/api/src/routes/subscriptions.ts +526 -15
- package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
- package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
- package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
- package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
- package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
- package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
- package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
- package/api/src/store/models/auto-recharge-config.ts +12 -0
- package/api/src/store/models/checkout-session.ts +7 -0
- package/api/src/store/models/exchange-rate-provider.ts +225 -0
- package/api/src/store/models/index.ts +6 -0
- package/api/src/store/models/payment-intent.ts +6 -0
- package/api/src/store/models/price-quote.ts +284 -0
- package/api/src/store/models/price.ts +53 -5
- package/api/src/store/models/subscription.ts +11 -0
- package/api/src/store/models/types.ts +61 -1
- package/api/tests/libs/change-payment-plan.spec.ts +282 -0
- package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
- package/api/tests/libs/quote-service.spec.ts +199 -0
- package/api/tests/libs/session.spec.ts +464 -0
- package/api/tests/libs/slippage.spec.ts +109 -0
- package/api/tests/libs/token-data-provider.spec.ts +267 -0
- package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
- package/api/tests/models/price-dynamic.spec.ts +100 -0
- package/api/tests/models/price-quote.spec.ts +112 -0
- package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
- package/api/tests/routes/subscription-slippage.spec.ts +254 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/components/customer/credit-overview.tsx +14 -0
- package/src/components/discount/discount-info.tsx +8 -2
- package/src/components/invoice/list.tsx +146 -16
- package/src/components/invoice/table.tsx +276 -71
- package/src/components/invoice-pdf/template.tsx +3 -7
- package/src/components/metadata/form.tsx +6 -8
- package/src/components/price/form.tsx +519 -149
- package/src/components/promotion/active-redemptions.tsx +5 -3
- package/src/components/quote/info.tsx +234 -0
- package/src/hooks/subscription.ts +132 -2
- package/src/locales/en.tsx +145 -0
- package/src/locales/zh.tsx +143 -1
- package/src/pages/admin/billing/invoices/detail.tsx +41 -4
- package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
- package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
- package/src/pages/admin/products/index.tsx +12 -1
- package/src/pages/customer/invoice/detail.tsx +36 -12
- package/src/pages/customer/subscription/change-payment.tsx +65 -3
- package/src/pages/customer/subscription/change-plan.tsx +207 -38
- package/src/pages/customer/subscription/detail.tsx +599 -419
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable react/display-name, react/no-unstable-nested-components */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
-
import { api, formatTime, Link, Table } from '@blocklet/payment-react';
|
|
3
|
+
import { api, formatTime, formatAmount, Link, Table } from '@blocklet/payment-react';
|
|
4
4
|
import { Box, Tab, Tabs, Typography, Chip } from '@mui/material';
|
|
5
5
|
import { useRequest } from 'ahooks';
|
|
6
6
|
import { useState } from 'react';
|
|
@@ -210,10 +210,11 @@ export default function ActiveRedemptions({ couponId = '', promotionCodeId = ''
|
|
|
210
210
|
if (!value) {
|
|
211
211
|
return null;
|
|
212
212
|
}
|
|
213
|
+
const v = value as any;
|
|
213
214
|
return (
|
|
214
215
|
<Typography key={currencyId} variant="body2" sx={{ display: 'inline-flex' }}>
|
|
215
216
|
{i > 0 && '、'}
|
|
216
|
-
{
|
|
217
|
+
{formatAmount(v.amount, v.currency?.decimal || 18)} {v.currency?.symbol || ''}
|
|
217
218
|
</Typography>
|
|
218
219
|
);
|
|
219
220
|
})}
|
|
@@ -371,10 +372,11 @@ export default function ActiveRedemptions({ couponId = '', promotionCodeId = ''
|
|
|
371
372
|
if (!value) {
|
|
372
373
|
return null;
|
|
373
374
|
}
|
|
375
|
+
const v = value as any;
|
|
374
376
|
return (
|
|
375
377
|
<Typography key={currencyId} variant="body2" sx={{ lineHeight: 1.2 }}>
|
|
376
378
|
{i > 0 && ', '}
|
|
377
|
-
{
|
|
379
|
+
{formatAmount(v.amount, v.currency?.decimal || 18)} {v.currency?.symbol || ''}
|
|
378
380
|
</Typography>
|
|
379
381
|
);
|
|
380
382
|
})}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { formatTime } from '@blocklet/payment-react';
|
|
3
|
+
import { LockClockOutlined, TrendingUpOutlined } from '@mui/icons-material';
|
|
4
|
+
import { Box, Chip, Paper, Stack, Tooltip, Typography } from '@mui/material';
|
|
5
|
+
|
|
6
|
+
import Copyable from '../copyable';
|
|
7
|
+
import InfoRow from '../info-row';
|
|
8
|
+
|
|
9
|
+
interface QuoteMetadata {
|
|
10
|
+
calculation?: {
|
|
11
|
+
token_amount_raw?: string;
|
|
12
|
+
unit_amount_raw?: string;
|
|
13
|
+
};
|
|
14
|
+
rounding?: {
|
|
15
|
+
mode?: string;
|
|
16
|
+
token_decimals?: number;
|
|
17
|
+
};
|
|
18
|
+
risk?: {
|
|
19
|
+
anomaly_detected?: boolean;
|
|
20
|
+
deviation_percent?: number;
|
|
21
|
+
degraded?: boolean;
|
|
22
|
+
degraded_reason?: string | null;
|
|
23
|
+
};
|
|
24
|
+
context?: Record<string, any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface PriceQuote {
|
|
28
|
+
id: string;
|
|
29
|
+
price_id: string;
|
|
30
|
+
base_currency: string;
|
|
31
|
+
base_amount: string;
|
|
32
|
+
target_currency_id: string;
|
|
33
|
+
rate_currency_symbol: string;
|
|
34
|
+
exchange_rate: string;
|
|
35
|
+
quoted_amount: string;
|
|
36
|
+
rate_provider_id: string;
|
|
37
|
+
rate_provider_name: string;
|
|
38
|
+
rate_timestamp_ms: number;
|
|
39
|
+
expires_at: number;
|
|
40
|
+
status: 'active' | 'used' | 'paid' | 'expired' | 'cancelled' | 'failed';
|
|
41
|
+
metadata: QuoteMetadata | null;
|
|
42
|
+
created_at: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface QuoteInfoProps {
|
|
46
|
+
quotes: PriceQuote[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getStatusColor = (status: string) => {
|
|
50
|
+
switch (status) {
|
|
51
|
+
case 'paid':
|
|
52
|
+
return 'success';
|
|
53
|
+
case 'used':
|
|
54
|
+
return 'primary';
|
|
55
|
+
case 'active':
|
|
56
|
+
return 'info';
|
|
57
|
+
case 'expired':
|
|
58
|
+
case 'cancelled':
|
|
59
|
+
case 'failed':
|
|
60
|
+
return 'error';
|
|
61
|
+
default:
|
|
62
|
+
return 'default';
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default function QuoteInfo({ quotes }: QuoteInfoProps) {
|
|
67
|
+
const { t } = useLocaleContext();
|
|
68
|
+
|
|
69
|
+
if (!quotes || quotes.length === 0) {
|
|
70
|
+
return (
|
|
71
|
+
<Typography variant="body2" color="text.secondary">
|
|
72
|
+
{t('admin.quote.noQuotes')}
|
|
73
|
+
</Typography>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Stack spacing={2}>
|
|
79
|
+
{quotes.map((quote) => (
|
|
80
|
+
<Paper key={quote.id} variant="outlined" sx={{ p: 2 }}>
|
|
81
|
+
<Stack spacing={2}>
|
|
82
|
+
{/* Header */}
|
|
83
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
84
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
85
|
+
<LockClockOutlined fontSize="small" color="primary" />
|
|
86
|
+
<Typography variant="subtitle2" fontWeight={600}>
|
|
87
|
+
{t('admin.quote.title')}
|
|
88
|
+
</Typography>
|
|
89
|
+
</Stack>
|
|
90
|
+
<Chip label={t(`admin.quote.status.${quote.status}`)} color={getStatusColor(quote.status)} size="small" />
|
|
91
|
+
</Stack>
|
|
92
|
+
|
|
93
|
+
{/* Quote ID */}
|
|
94
|
+
<InfoRow
|
|
95
|
+
label={t('admin.quote.id')}
|
|
96
|
+
value={<Copyable text={quote.id} />}
|
|
97
|
+
direction="row"
|
|
98
|
+
alignItems="center"
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
{/* Pricing Details */}
|
|
102
|
+
<Box>
|
|
103
|
+
<Typography variant="caption" color="text.secondary" gutterBottom>
|
|
104
|
+
{t('admin.quote.pricingDetails')}
|
|
105
|
+
</Typography>
|
|
106
|
+
<Stack spacing={1} sx={{ mt: 1 }}>
|
|
107
|
+
<InfoRow
|
|
108
|
+
label={t('admin.quote.baseAmount')}
|
|
109
|
+
value={
|
|
110
|
+
<Typography variant="body2">
|
|
111
|
+
{quote.base_amount} {quote.base_currency}
|
|
112
|
+
</Typography>
|
|
113
|
+
}
|
|
114
|
+
direction="row"
|
|
115
|
+
alignItems="center"
|
|
116
|
+
/>
|
|
117
|
+
<InfoRow
|
|
118
|
+
label={t('admin.quote.exchangeRate')}
|
|
119
|
+
value={
|
|
120
|
+
<Stack direction="row" spacing={0.5} alignItems="center">
|
|
121
|
+
<Typography variant="body2">
|
|
122
|
+
1 {quote.rate_currency_symbol} = {quote.exchange_rate} {quote.base_currency}
|
|
123
|
+
</Typography>
|
|
124
|
+
{quote.metadata?.risk?.degraded && (
|
|
125
|
+
<Tooltip title={quote.metadata.risk.degraded_reason || ''}>
|
|
126
|
+
<Chip label={t('admin.quote.degraded')} color="warning" size="small" variant="outlined" />
|
|
127
|
+
</Tooltip>
|
|
128
|
+
)}
|
|
129
|
+
</Stack>
|
|
130
|
+
}
|
|
131
|
+
direction="row"
|
|
132
|
+
alignItems="center"
|
|
133
|
+
/>
|
|
134
|
+
<InfoRow
|
|
135
|
+
label={t('admin.quote.quotedAmount')}
|
|
136
|
+
value={
|
|
137
|
+
<Typography variant="body2" fontWeight={500}>
|
|
138
|
+
{quote.metadata?.calculation?.token_amount_raw || '-'} {quote.rate_currency_symbol}
|
|
139
|
+
</Typography>
|
|
140
|
+
}
|
|
141
|
+
direction="row"
|
|
142
|
+
alignItems="center"
|
|
143
|
+
/>
|
|
144
|
+
</Stack>
|
|
145
|
+
</Box>
|
|
146
|
+
|
|
147
|
+
{/* Rate Provider Info */}
|
|
148
|
+
<Box>
|
|
149
|
+
<Typography variant="caption" color="text.secondary" gutterBottom>
|
|
150
|
+
{t('admin.quote.providerInfo')}
|
|
151
|
+
</Typography>
|
|
152
|
+
<Stack spacing={1} sx={{ mt: 1 }}>
|
|
153
|
+
<InfoRow
|
|
154
|
+
label={t('admin.quote.provider')}
|
|
155
|
+
value={<Typography variant="body2">{quote.rate_provider_name}</Typography>}
|
|
156
|
+
direction="row"
|
|
157
|
+
alignItems="center"
|
|
158
|
+
/>
|
|
159
|
+
<InfoRow
|
|
160
|
+
label={t('admin.quote.rateTimestamp')}
|
|
161
|
+
value={
|
|
162
|
+
<Tooltip title={formatTime(quote.rate_timestamp_ms)}>
|
|
163
|
+
<Typography variant="body2" color="text.secondary">
|
|
164
|
+
{formatTime(quote.rate_timestamp_ms, 'relative')}
|
|
165
|
+
</Typography>
|
|
166
|
+
</Tooltip>
|
|
167
|
+
}
|
|
168
|
+
direction="row"
|
|
169
|
+
alignItems="center"
|
|
170
|
+
/>
|
|
171
|
+
</Stack>
|
|
172
|
+
</Box>
|
|
173
|
+
|
|
174
|
+
{/* Lifecycle */}
|
|
175
|
+
<Box>
|
|
176
|
+
<Typography variant="caption" color="text.secondary" gutterBottom>
|
|
177
|
+
{t('admin.quote.lifecycle')}
|
|
178
|
+
</Typography>
|
|
179
|
+
<Stack spacing={1} sx={{ mt: 1 }}>
|
|
180
|
+
<InfoRow
|
|
181
|
+
label={t('common.createdAt')}
|
|
182
|
+
value={
|
|
183
|
+
<Typography variant="body2" color="text.secondary">
|
|
184
|
+
{formatTime(quote.created_at)}
|
|
185
|
+
</Typography>
|
|
186
|
+
}
|
|
187
|
+
direction="row"
|
|
188
|
+
alignItems="center"
|
|
189
|
+
/>
|
|
190
|
+
<InfoRow
|
|
191
|
+
label={t('admin.quote.expiresAt')}
|
|
192
|
+
value={
|
|
193
|
+
<Typography variant="body2" color="text.secondary">
|
|
194
|
+
{formatTime(quote.expires_at * 1000)}
|
|
195
|
+
</Typography>
|
|
196
|
+
}
|
|
197
|
+
direction="row"
|
|
198
|
+
alignItems="center"
|
|
199
|
+
/>
|
|
200
|
+
</Stack>
|
|
201
|
+
</Box>
|
|
202
|
+
|
|
203
|
+
{/* Risk Information */}
|
|
204
|
+
{quote.metadata?.risk && (
|
|
205
|
+
<Box>
|
|
206
|
+
<Typography variant="caption" color="text.secondary" gutterBottom>
|
|
207
|
+
{t('admin.quote.riskInfo')}
|
|
208
|
+
</Typography>
|
|
209
|
+
<Stack spacing={1} sx={{ mt: 1 }}>
|
|
210
|
+
{quote.metadata.risk.deviation_percent !== undefined && (
|
|
211
|
+
<InfoRow
|
|
212
|
+
label={t('admin.quote.deviation')}
|
|
213
|
+
value={
|
|
214
|
+
<Stack direction="row" spacing={0.5} alignItems="center">
|
|
215
|
+
<TrendingUpOutlined fontSize="small" />
|
|
216
|
+
<Typography variant="body2">{quote.metadata.risk.deviation_percent.toFixed(2)}%</Typography>
|
|
217
|
+
</Stack>
|
|
218
|
+
}
|
|
219
|
+
direction="row"
|
|
220
|
+
alignItems="center"
|
|
221
|
+
/>
|
|
222
|
+
)}
|
|
223
|
+
{quote.metadata.risk.anomaly_detected && (
|
|
224
|
+
<Chip label={t('admin.quote.anomalyDetected')} color="warning" size="small" variant="outlined" />
|
|
225
|
+
)}
|
|
226
|
+
</Stack>
|
|
227
|
+
</Box>
|
|
228
|
+
)}
|
|
229
|
+
</Stack>
|
|
230
|
+
</Paper>
|
|
231
|
+
))}
|
|
232
|
+
</Stack>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
|
-
import { useRequest } from 'ahooks';
|
|
4
|
+
import { useRequest, useSetState } from 'ahooks';
|
|
5
5
|
import { api } from '@blocklet/payment-react';
|
|
6
6
|
import type { TPaymentCurrency } from '@blocklet/payment-types';
|
|
7
7
|
|
|
@@ -62,3 +62,133 @@ export function usePendingAmountForSubscription(subscriptionId: string, paymentC
|
|
|
62
62
|
checkPendingAmount,
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
type SubscriptionExchangeRateInfo = {
|
|
67
|
+
rate?: string;
|
|
68
|
+
provider_id?: string;
|
|
69
|
+
provider_name?: string;
|
|
70
|
+
base_currency?: string;
|
|
71
|
+
timestamp_ms?: number;
|
|
72
|
+
fetched_at?: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type UseSubscriptionExchangeRateOptions = {
|
|
76
|
+
subscriptionId?: string;
|
|
77
|
+
currencyId?: string;
|
|
78
|
+
enabled?: boolean;
|
|
79
|
+
pollingInterval?: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function useSubscriptionExchangeRate({
|
|
83
|
+
subscriptionId,
|
|
84
|
+
currencyId,
|
|
85
|
+
enabled = false,
|
|
86
|
+
pollingInterval = 30000,
|
|
87
|
+
}: UseSubscriptionExchangeRateOptions) {
|
|
88
|
+
const [state, setState] = useSetState<{
|
|
89
|
+
liveRateInfo?: SubscriptionExchangeRateInfo;
|
|
90
|
+
liveRateUnavailable: boolean;
|
|
91
|
+
liveRateError?: string;
|
|
92
|
+
}>({
|
|
93
|
+
liveRateInfo: undefined,
|
|
94
|
+
liveRateUnavailable: false,
|
|
95
|
+
liveRateError: undefined,
|
|
96
|
+
});
|
|
97
|
+
const liveRateRefreshRef = useRef(false);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!enabled || !subscriptionId || !currencyId) {
|
|
101
|
+
setState({
|
|
102
|
+
liveRateInfo: undefined,
|
|
103
|
+
liveRateUnavailable: false,
|
|
104
|
+
liveRateError: undefined,
|
|
105
|
+
});
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let cancelled = false;
|
|
110
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
111
|
+
|
|
112
|
+
const fetchRate = async (isManualRetry = false) => {
|
|
113
|
+
if (typeof document !== 'undefined' && document.hidden && !isManualRetry) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (liveRateRefreshRef.current) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
liveRateRefreshRef.current = true;
|
|
123
|
+
try {
|
|
124
|
+
const { data } = await api.get(`/api/subscriptions/${subscriptionId}/exchange-rate`, {
|
|
125
|
+
params: { currency_id: currencyId },
|
|
126
|
+
});
|
|
127
|
+
if (cancelled) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
setState({
|
|
131
|
+
liveRateInfo: data,
|
|
132
|
+
liveRateUnavailable: false,
|
|
133
|
+
liveRateError: undefined,
|
|
134
|
+
});
|
|
135
|
+
scheduleNext();
|
|
136
|
+
} catch (err: any) {
|
|
137
|
+
if (cancelled) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
console.error('[Subscription Exchange Rate Fetch Error]', {
|
|
141
|
+
subscriptionId,
|
|
142
|
+
currencyId,
|
|
143
|
+
message: err?.message,
|
|
144
|
+
});
|
|
145
|
+
setState({
|
|
146
|
+
liveRateUnavailable: true,
|
|
147
|
+
liveRateError: err?.response?.data?.error || err?.message,
|
|
148
|
+
});
|
|
149
|
+
} finally {
|
|
150
|
+
liveRateRefreshRef.current = false;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const scheduleNext = () => {
|
|
155
|
+
if (!timer) {
|
|
156
|
+
// no existing timer
|
|
157
|
+
} else {
|
|
158
|
+
clearInterval(timer);
|
|
159
|
+
}
|
|
160
|
+
if (typeof window === 'undefined') {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
timer = setInterval(() => {
|
|
164
|
+
fetchRate(false);
|
|
165
|
+
}, pollingInterval);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
fetchRate(false);
|
|
169
|
+
const handleVisibilityChange = () => {
|
|
170
|
+
if (typeof document !== 'undefined' && !document.hidden) {
|
|
171
|
+
fetchRate(false);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
if (typeof document !== 'undefined') {
|
|
175
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
176
|
+
}
|
|
177
|
+
return () => {
|
|
178
|
+
cancelled = true;
|
|
179
|
+
if (timer) {
|
|
180
|
+
clearInterval(timer);
|
|
181
|
+
}
|
|
182
|
+
if (typeof document !== 'undefined') {
|
|
183
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
187
|
+
}, [subscriptionId, currencyId, enabled, pollingInterval]);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
liveRateInfo: state.liveRateInfo,
|
|
191
|
+
liveRateUnavailable: state.liveRateUnavailable,
|
|
192
|
+
liveRateError: state.liveRateError,
|
|
193
|
+
};
|
|
194
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -9,6 +9,8 @@ export default flat({
|
|
|
9
9
|
active: 'Active',
|
|
10
10
|
every: 'Every',
|
|
11
11
|
inactive: 'Inactive',
|
|
12
|
+
enabled: 'Enabled',
|
|
13
|
+
disabled: 'Disabled',
|
|
12
14
|
metadata: {
|
|
13
15
|
label: 'Metadata',
|
|
14
16
|
description: 'Add custom key-value pairs to store additional information about this meter.',
|
|
@@ -40,6 +42,10 @@ export default flat({
|
|
|
40
42
|
estimatedDuration: '{duration} est.',
|
|
41
43
|
detail: 'Detail',
|
|
42
44
|
setting: 'Setting',
|
|
45
|
+
slippage: 'Slippage Limit',
|
|
46
|
+
slippageMinRate: 'Min acceptable rate {rate} {currency}',
|
|
47
|
+
slippageTooltip:
|
|
48
|
+
'The minimum acceptable exchange rate for automatic payments. If the rate drops below this, payment will be paused.',
|
|
43
49
|
welcome: 'Welcome to Payment Kit',
|
|
44
50
|
welcomeDesc: 'Start accepting payments in minutes with Payment Kit. Choose a feature to get started.',
|
|
45
51
|
quickStart: 'Quick Start Guides',
|
|
@@ -55,10 +61,14 @@ export default flat({
|
|
|
55
61
|
copyTip: 'Please copy manually',
|
|
56
62
|
save: 'Save',
|
|
57
63
|
saving: 'Saving...',
|
|
64
|
+
saved: 'Saved successfully',
|
|
65
|
+
refresh: 'Refresh',
|
|
58
66
|
cancel: 'Cancel',
|
|
59
67
|
back: 'Back',
|
|
60
68
|
know: 'I Know',
|
|
61
69
|
confirm: 'Confirm',
|
|
70
|
+
increased: 'increased',
|
|
71
|
+
decreased: 'decreased',
|
|
62
72
|
edit: 'Edit',
|
|
63
73
|
view: 'View',
|
|
64
74
|
select: 'Select',
|
|
@@ -804,6 +814,33 @@ export default flat({
|
|
|
804
814
|
description: 'Enter the number of units that can be purchased in a single checkout, 0 means unlimited',
|
|
805
815
|
},
|
|
806
816
|
inventory: 'Inventory Settings',
|
|
817
|
+
dynamicPricing: {
|
|
818
|
+
label: 'Enable Dynamic Pricing',
|
|
819
|
+
description: 'Price fluctuates based on real-time exchange rates',
|
|
820
|
+
config: {
|
|
821
|
+
title: 'Dynamic Pricing Configuration',
|
|
822
|
+
baseAmount: {
|
|
823
|
+
label: 'Base Price',
|
|
824
|
+
description: 'The base price in fiat currency',
|
|
825
|
+
required: 'Base price is required',
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
validation: {
|
|
829
|
+
checking: 'Checking exchange rate availability...',
|
|
830
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
831
|
+
rateLine: 'Current rate: 1 {currency} ≈ ${rate}',
|
|
832
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
833
|
+
usdLine: 'USD estimate: ≈ ${amount}',
|
|
834
|
+
error: 'Failed to fetch exchange rate, currency not supported or data source exception',
|
|
835
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
836
|
+
estimatedLine: 'Estimated amount: ≈ {amount} {currency}',
|
|
837
|
+
useAmount: 'Fill amount',
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
referencePricing: {
|
|
841
|
+
label: 'Reference Exchange Rate Pricing',
|
|
842
|
+
description: 'Use current exchange rates to estimate token amounts without enabling dynamic pricing',
|
|
843
|
+
},
|
|
807
844
|
},
|
|
808
845
|
coupon: {
|
|
809
846
|
create: 'Create Coupon',
|
|
@@ -1324,6 +1361,8 @@ export default flat({
|
|
|
1324
1361
|
finalizedAt: 'Finalized At',
|
|
1325
1362
|
paidAt: 'Payment Date',
|
|
1326
1363
|
summary: 'Summary',
|
|
1364
|
+
billingContextNote: 'Invoice amounts are calculated based on real-time prices at the time of billing.',
|
|
1365
|
+
dynamicPricingNote: 'This invoice uses dynamic pricing with locked exchange rates.',
|
|
1327
1366
|
billTo: 'Billed to',
|
|
1328
1367
|
billing: 'Billing Method',
|
|
1329
1368
|
download: 'Download PDF',
|
|
@@ -1816,6 +1855,71 @@ export default flat({
|
|
|
1816
1855
|
totalCreditUsed: 'Total Credit Used',
|
|
1817
1856
|
transactionDate: 'Transaction Date',
|
|
1818
1857
|
},
|
|
1858
|
+
exchangeRateProvider: {
|
|
1859
|
+
title: 'Exchange Rate Providers',
|
|
1860
|
+
subtitle: 'Manage exchange rate data sources for dynamic pricing',
|
|
1861
|
+
medianStrategyNote:
|
|
1862
|
+
'Exchange rates are calculated automatically using multiple data sources. You only need to manage this list when a provider is consistently failing or needs to be temporarily disabled.',
|
|
1863
|
+
disableConfirmTitle: 'Disable Provider?',
|
|
1864
|
+
disableConfirmMessage: 'Disabling this provider may affect dynamic pricing payments. Are you sure?',
|
|
1865
|
+
table: {
|
|
1866
|
+
name: 'Name',
|
|
1867
|
+
participation: 'Participation',
|
|
1868
|
+
health: 'Health',
|
|
1869
|
+
recentActivity: 'Recent Activity',
|
|
1870
|
+
trustLevel: 'Trust Level',
|
|
1871
|
+
trustLevelTip:
|
|
1872
|
+
'Indicates confidence and eligibility for rate calculation. Higher values do not mean preferred or primary source.',
|
|
1873
|
+
enabled: 'Enabled',
|
|
1874
|
+
lastUpdate: 'Last update: {time}',
|
|
1875
|
+
failures24h: 'Failures (24h): {count}',
|
|
1876
|
+
},
|
|
1877
|
+
participation: {
|
|
1878
|
+
included: 'Included',
|
|
1879
|
+
excluded: 'Excluded',
|
|
1880
|
+
},
|
|
1881
|
+
health: {
|
|
1882
|
+
active: 'Healthy',
|
|
1883
|
+
degraded: 'Unstable',
|
|
1884
|
+
paused: 'Outlier',
|
|
1885
|
+
inactive: 'Outlier',
|
|
1886
|
+
},
|
|
1887
|
+
status: {
|
|
1888
|
+
active: 'Active',
|
|
1889
|
+
degraded: 'Degraded',
|
|
1890
|
+
paused: 'Paused',
|
|
1891
|
+
inactive: 'Inactive',
|
|
1892
|
+
},
|
|
1893
|
+
create: {
|
|
1894
|
+
title: 'Add Provider',
|
|
1895
|
+
primaryAction: 'Add Exchange Rate Provider',
|
|
1896
|
+
},
|
|
1897
|
+
edit: {
|
|
1898
|
+
title: 'Edit {name}',
|
|
1899
|
+
name: 'Name',
|
|
1900
|
+
nameHelp: 'Unique identifier for this data source',
|
|
1901
|
+
nameRequired: 'Name is required',
|
|
1902
|
+
type: 'Provider Type',
|
|
1903
|
+
typeHelp: 'Data source type. Cannot be changed after creation.',
|
|
1904
|
+
baseUrl: 'Base URL (Optional)',
|
|
1905
|
+
baseUrlHelp: 'Custom base URL for proxy or self-hosted instance. Default: {defaultUrl}',
|
|
1906
|
+
apiKey: 'API Key',
|
|
1907
|
+
apiKeyHelp: 'API key for accessing the data source.',
|
|
1908
|
+
apiKeyLinkText: 'Get your API key at',
|
|
1909
|
+
apiKeyLinkAction: 'CoinMarketCap account',
|
|
1910
|
+
testConnection: 'Test Connection',
|
|
1911
|
+
testing: 'Testing...',
|
|
1912
|
+
testSuccess: 'Connection successful ({symbol}/USD: {rate}, {time}ms)',
|
|
1913
|
+
testFailed: 'Connection failed: {error}',
|
|
1914
|
+
enabled: 'Enabled',
|
|
1915
|
+
priority: 'Priority',
|
|
1916
|
+
priorityHelp: 'Lower number = higher priority. Providers are used in priority order.',
|
|
1917
|
+
status: 'Status',
|
|
1918
|
+
statusHelp: 'Set to "paused" to temporarily disable this provider.',
|
|
1919
|
+
pausedReason: 'Paused Reason',
|
|
1920
|
+
pausedReasonHelp: 'Explain why this provider is paused (optional).',
|
|
1921
|
+
},
|
|
1922
|
+
},
|
|
1819
1923
|
},
|
|
1820
1924
|
empty: {
|
|
1821
1925
|
image: 'No Image',
|
|
@@ -1924,6 +2028,17 @@ export default flat({
|
|
|
1924
2028
|
donation: 'Donation',
|
|
1925
2029
|
creditsInfo: 'Total {amount} included',
|
|
1926
2030
|
appliedDiscounts: 'Applied Discounts',
|
|
2031
|
+
priceChanged:
|
|
2032
|
+
'The exchange rate has changed by {percent}%. The payment amount will be updated. Do you want to continue?',
|
|
2033
|
+
paymentCancelled: 'Payment cancelled',
|
|
2034
|
+
paymentMethodChanged: 'Payment method changed',
|
|
2035
|
+
priceChangeTitle: 'Price Changed',
|
|
2036
|
+
priceChangeDescription: 'The exchange rate has {direction} by {percent}%. Your payment amount will be updated.',
|
|
2037
|
+
currentPaymentMethod: 'Current Payment Method',
|
|
2038
|
+
otherPaymentMethods: 'Or pay with another method',
|
|
2039
|
+
confirmAndPay: 'Confirm & Pay',
|
|
2040
|
+
switchAndPay: 'Switch & Pay',
|
|
2041
|
+
current: 'current',
|
|
1927
2042
|
},
|
|
1928
2043
|
payout: {
|
|
1929
2044
|
empty: 'No Revenues',
|
|
@@ -1940,6 +2055,36 @@ export default flat({
|
|
|
1940
2055
|
alert: 'You have due invoices. Please pay them promptly to avoid service interruption.',
|
|
1941
2056
|
title: 'Settle Due Invoices',
|
|
1942
2057
|
},
|
|
2058
|
+
quote: {
|
|
2059
|
+
title: 'Price Quotes',
|
|
2060
|
+
noQuotes: 'No price quotes for this invoice',
|
|
2061
|
+
id: 'Quote ID',
|
|
2062
|
+
pricingDetails: 'Pricing Details',
|
|
2063
|
+
baseAmount: 'Base Amount',
|
|
2064
|
+
exchangeRate: 'Exchange Rate',
|
|
2065
|
+
consensusMethod: 'Consensus',
|
|
2066
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
2067
|
+
referenceRate: 'Reference Rate: 1 {symbol} ≈ ${rate}',
|
|
2068
|
+
quotedAmount: 'Quoted Amount',
|
|
2069
|
+
providerInfo: 'Rate Provider',
|
|
2070
|
+
provider: 'Provider',
|
|
2071
|
+
rateTimestamp: 'Rate Timestamp',
|
|
2072
|
+
slippage: 'Slippage',
|
|
2073
|
+
lifecycle: 'Lifecycle',
|
|
2074
|
+
expiresAt: 'Expires At',
|
|
2075
|
+
riskInfo: 'Risk Information',
|
|
2076
|
+
deviation: 'Price Deviation',
|
|
2077
|
+
anomalyDetected: 'Anomaly Detected',
|
|
2078
|
+
degraded: 'Degraded',
|
|
2079
|
+
status: {
|
|
2080
|
+
active: 'Active',
|
|
2081
|
+
used: 'Used',
|
|
2082
|
+
paid: 'Paid',
|
|
2083
|
+
expired: 'Expired',
|
|
2084
|
+
cancelled: 'Cancelled',
|
|
2085
|
+
failed: 'Failed',
|
|
2086
|
+
},
|
|
2087
|
+
},
|
|
1943
2088
|
},
|
|
1944
2089
|
integrations: {
|
|
1945
2090
|
description: 'Configure and manage how Payment Kit integrates with your application.',
|