@micha.bigler/ui-core-micha 4.4.0 → 4.5.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/CHANGELOG.md +11 -0
- package/dist/billing/SubscriptionSection.js +8 -2
- package/dist/billing/WithdrawalDialog.js +95 -0
- package/dist/billing/WithdrawalSection.js +59 -0
- package/dist/billing/api.js +3 -0
- package/dist/billing/format.js +26 -4
- package/dist/billing/wire.js +3 -0
- package/dist/i18n/billingTranslations.js +20 -0
- package/dist/index.js +2 -0
- package/dist/theme/kitSxRegistry.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.0 - UCM-BILL-3
|
|
4
|
+
|
|
5
|
+
"Withdraw contract here" for EU/EEA consumers, against the `django-core-micha` 3.5.0 withdrawal API
|
|
6
|
+
(`withdrawal/`, `withdrawal/<id>/preview/`, `POST withdrawal/<id>/`). `WithdrawalSection` lists only the purchases the
|
|
7
|
+
server calls eligible, with the 14-day deadline (purchase date plus 14 days) and a button; `WithdrawalDialog` shows the
|
|
8
|
+
server's refund amount and a calculation composed from the server's numbers (`describeCalculation`), confirms exactly
|
|
9
|
+
once and shows the result. The kit never decides eligibility or the amount itself; a 409 shows "not possible". The
|
|
10
|
+
section is part of `SubscriptionSection` (prop `withdrawal`, default `true`, `false` hides it). `createBillingApi`
|
|
11
|
+
gets `listWithdrawals`, `previewWithdrawal`, `confirmWithdrawal`; `formatPrice` takes an optional currency and
|
|
12
|
+
`formatDateTime` is new. New keys `Billing.WITHDRAWAL_*` (de formal, de_informal, en, fr, sw).
|
|
13
|
+
|
|
3
14
|
## 4.4.0 - UCM-BILL-1
|
|
4
15
|
|
|
5
16
|
Subscription and token surfaces in the main entry, against the `django-core-micha` billing API (DCM-BILL-1, `me/`,
|
|
@@ -9,15 +9,17 @@ import { formatDate, formatPrice, formatTokens } from './format';
|
|
|
9
9
|
import { PlanPicker } from './PlanPicker';
|
|
10
10
|
import { TokenBalanceBadge } from './TokenBalanceBadge';
|
|
11
11
|
import { AccessEndedBanner } from './AccessEndedBanner';
|
|
12
|
+
import { WithdrawalSection } from './WithdrawalSection';
|
|
12
13
|
export const SUBSCRIPTION_SECTION_ALERT_SX = { width: '100%' };
|
|
13
14
|
function isStatus(error, status) { var _a; return ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === status; }
|
|
14
15
|
function activeUntil(access, t, language) {
|
|
15
16
|
const date = formatDate(access === null || access === void 0 ? void 0 : access.ends_at, language);
|
|
16
17
|
return date ? t('Billing.ACTIVE_UNTIL', { date }) : null;
|
|
17
18
|
}
|
|
18
|
-
export function SubscriptionSection({ api = defaultBillingApi, redirect = (url) => window.location.assign(url), allowedHosts = DEFAULT_PAYMENT_HOSTS, onRenew, describePlan, sx }) {
|
|
19
|
+
export function SubscriptionSection({ api = defaultBillingApi, redirect = (url) => window.location.assign(url), allowedHosts = DEFAULT_PAYMENT_HOSTS, onRenew, describePlan, withdrawal = true, sx }) {
|
|
19
20
|
const { t, i18n } = useTranslation();
|
|
20
21
|
const { state, me, reload } = useBilling(api);
|
|
22
|
+
const [withdrawnPurchases, setWithdrawnPurchases] = useState({});
|
|
21
23
|
const [seats, setSeats] = useState(null);
|
|
22
24
|
const [seatError, setSeatError] = useState(null);
|
|
23
25
|
const [seatInput, setSeatInput] = useState('');
|
|
@@ -90,6 +92,10 @@ export function SubscriptionSection({ api = defaultBillingApi, redirect = (url)
|
|
|
90
92
|
var _a, _b, _c, _d;
|
|
91
93
|
(_b = (_a = renewalPickerRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, { behavior: 'smooth', block: 'start' });
|
|
92
94
|
(_d = (_c = renewalPickerRef.current) === null || _c === void 0 ? void 0 : _c.querySelector('input[type="radio"]')) === null || _d === void 0 ? void 0 : _d.focus();
|
|
93
|
-
}) }), _jsx("div", { ref: renewalPickerRef, children: _jsx(PlanPicker, { plans: me.plans || [], api: api, redirect: redirect, allowedHosts: allowedHosts, describePlan: describePlan }) })] }) : _jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h5", children: t('Billing.HEADING') }), _jsx(Typography, { children: sourceLine }), access.source !== 'free' && dateLine && _jsx(Typography, { children: dateLine }), access.source === 'subscription' && _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.openPortal(), 'Billing.ERROR_PORTAL'), children: t('Billing.MANAGE') }), _jsx(Typography, { children: t('Billing.MANAGE_DETAILS') })] }), access.source === 'team' && seatError && !seats && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t('Billing.ERROR_LOAD') }), access.source === 'team' && seats && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { children: t('Billing.TEAM_SEATS') }), seats.map((seat) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: String(seat.user_id) }), _jsx(Button, { disabled: busy, onClick: () => updateSeat(() => api.removeSeat({ userId: seat.user_id })), children: t('Billing.SEAT_REMOVE') })] }, seat.user_id)), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { size: "small", label: t('Billing.SEAT_USER'), value: seatInput, onChange: (event) => setSeatInput(event.target.value) }), _jsx(Button, { disabled: busy || !seatInput.trim(), onClick: () => updateSeat(() => seatInput.includes('@') ? api.addSeat({ email: seatInput.trim() }) : api.addSeat({ userId: seatInput.trim() })), children: t('Billing.SEATS_ADD') })] }), seatError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: typeof seatError === 'string' ? t(seatError) : t('Billing.ERROR_LOAD') })] }), actionError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t(actionError) })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: t('Billing.BALANCE_LABEL') }), _jsx(TokenBalanceBadge, { balance: tokens.balance })] }), _jsx(Typography, { children: t('Billing.BALANCE', { tokens: formatTokens(tokens.balance, i18n.language) }) }), packagePlans.length > 0 && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('Billing.BUY_PACKAGE') }), packagePlans.map((plan) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsxs(Typography, { children: [formatPrice(plan.amount_cents, i18n.language), " \u00B7 ", t('Billing.PACKAGE_TOKENS', { tokens: formatTokens(plan.tokens, i18n.language) })] }), _jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.startCheckout(plan.key), 'Billing.ERROR_CHECKOUT'), children: t('Billing.PACKAGE_BUY') })] }, plan.key))] })
|
|
95
|
+
}) }), _jsx("div", { ref: renewalPickerRef, children: _jsx(PlanPicker, { plans: me.plans || [], api: api, redirect: redirect, allowedHosts: allowedHosts, describePlan: describePlan }) })] }) : _jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h5", children: t('Billing.HEADING') }), _jsx(Typography, { children: sourceLine }), access.source !== 'free' && dateLine && _jsx(Typography, { children: dateLine }), access.source === 'subscription' && _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.openPortal(), 'Billing.ERROR_PORTAL'), children: t('Billing.MANAGE') }), _jsx(Typography, { children: t('Billing.MANAGE_DETAILS') })] }), access.source === 'team' && seatError && !seats && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t('Billing.ERROR_LOAD') }), access.source === 'team' && seats && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { children: t('Billing.TEAM_SEATS') }), seats.map((seat) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: String(seat.user_id) }), _jsx(Button, { disabled: busy, onClick: () => updateSeat(() => api.removeSeat({ userId: seat.user_id })), children: t('Billing.SEAT_REMOVE') })] }, seat.user_id)), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { size: "small", label: t('Billing.SEAT_USER'), value: seatInput, onChange: (event) => setSeatInput(event.target.value) }), _jsx(Button, { disabled: busy || !seatInput.trim(), onClick: () => updateSeat(() => seatInput.includes('@') ? api.addSeat({ email: seatInput.trim() }) : api.addSeat({ userId: seatInput.trim() })), children: t('Billing.SEATS_ADD') })] }), seatError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: typeof seatError === 'string' ? t(seatError) : t('Billing.ERROR_LOAD') })] }), actionError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t(actionError) })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: t('Billing.BALANCE_LABEL') }), _jsx(TokenBalanceBadge, { balance: tokens.balance })] }), _jsx(Typography, { children: t('Billing.BALANCE', { tokens: formatTokens(tokens.balance, i18n.language) }) }), packagePlans.length > 0 && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('Billing.BUY_PACKAGE') }), packagePlans.map((plan) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsxs(Typography, { children: [formatPrice(plan.amount_cents, i18n.language), " \u00B7 ", t('Billing.PACKAGE_TOKENS', { tokens: formatTokens(plan.tokens, i18n.language) })] }), _jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.startCheckout(plan.key), 'Billing.ERROR_CHECKOUT'), children: t('Billing.PACKAGE_BUY') })] }, plan.key))] }), withdrawal && _jsx(WithdrawalSection, { api: api, plans: me.plans, sessionWithdrawals: withdrawnPurchases, onWithdrawn: (result, purchase) => {
|
|
96
|
+
if ((purchase === null || purchase === void 0 ? void 0 : purchase.id) != null)
|
|
97
|
+
setWithdrawnPurchases((current) => (Object.assign(Object.assign({}, current), { [purchase.id]: { result, purchase } })));
|
|
98
|
+
reload();
|
|
99
|
+
} })] });
|
|
94
100
|
}
|
|
95
101
|
export default SubscriptionSection;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Alert, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { formatDateTime, formatPrice } from './format';
|
|
6
|
+
export const WITHDRAWAL_DIALOG_ALERT_SX = { width: '100%' };
|
|
7
|
+
export function describeCalculation(preview, t, language) {
|
|
8
|
+
const currency = (preview === null || preview === void 0 ? void 0 : preview.currency) || 'USD';
|
|
9
|
+
const price = (value) => formatPrice(value, language, currency);
|
|
10
|
+
const items = [t('Billing.WITHDRAWAL_PAID', { amount: price(preview === null || preview === void 0 ? void 0 : preview.amount_paid_cents) })];
|
|
11
|
+
const time = Number(preview === null || preview === void 0 ? void 0 : preview.time_share_cents) || 0;
|
|
12
|
+
const token = Number(preview === null || preview === void 0 ? void 0 : preview.token_share_cents) || 0;
|
|
13
|
+
if (time !== 0 || ((preview === null || preview === void 0 ? void 0 : preview.kind) !== 'token_package' && token === 0))
|
|
14
|
+
items.push(t('Billing.WITHDRAWAL_TIME_SHARE', { amount: price(time) }));
|
|
15
|
+
if (token !== 0 || (preview === null || preview === void 0 ? void 0 : preview.kind) === 'token_package')
|
|
16
|
+
items.push(t('Billing.WITHDRAWAL_TOKEN_SHARE', { amount: price(token) }));
|
|
17
|
+
const used = Number.isFinite(preview === null || preview === void 0 ? void 0 : preview.used_cents) ? preview.used_cents : Math.max(time, token);
|
|
18
|
+
items.push(t('Billing.WITHDRAWAL_USED', { amount: price(used) }));
|
|
19
|
+
items.push(t('Billing.WITHDRAWAL_REFUND', { amount: price(preview === null || preview === void 0 ? void 0 : preview.refund_cents) }));
|
|
20
|
+
return items.join(' · ');
|
|
21
|
+
}
|
|
22
|
+
export function WithdrawalDialog({ open, purchase, api, onClose, onWithdrawn }) {
|
|
23
|
+
const { t, i18n } = useTranslation();
|
|
24
|
+
const [state, setState] = useState('loading');
|
|
25
|
+
const [preview, setPreview] = useState(null);
|
|
26
|
+
const [result, setResult] = useState(null);
|
|
27
|
+
const [errorStatus, setErrorStatus] = useState(null);
|
|
28
|
+
const [attempt, setAttempt] = useState(0);
|
|
29
|
+
const confirmGuard = useRef(false);
|
|
30
|
+
const notified = useRef(false);
|
|
31
|
+
const previewRequest = useRef(null);
|
|
32
|
+
const titleId = `withdrawal-title-${purchase === null || purchase === void 0 ? void 0 : purchase.id}`;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
var _a;
|
|
35
|
+
if (!open || !purchase || typeof (api === null || api === void 0 ? void 0 : api.previewWithdrawal) !== 'function') {
|
|
36
|
+
if (!open)
|
|
37
|
+
previewRequest.current = null;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const requestKey = String(purchase.id) + ':' + attempt;
|
|
41
|
+
let live = true;
|
|
42
|
+
setState('loading');
|
|
43
|
+
setPreview(null);
|
|
44
|
+
setResult(null);
|
|
45
|
+
setErrorStatus(null);
|
|
46
|
+
confirmGuard.current = false;
|
|
47
|
+
notified.current = false;
|
|
48
|
+
let request = ((_a = previewRequest.current) === null || _a === void 0 ? void 0 : _a.key) === requestKey ? previewRequest.current.promise : null;
|
|
49
|
+
if (!request) {
|
|
50
|
+
request = Promise.resolve().then(() => api.previewWithdrawal(purchase.id));
|
|
51
|
+
previewRequest.current = { key: requestKey, promise: request };
|
|
52
|
+
}
|
|
53
|
+
request.then((data) => {
|
|
54
|
+
if (!live)
|
|
55
|
+
return;
|
|
56
|
+
setPreview(data);
|
|
57
|
+
setState((data === null || data === void 0 ? void 0 : data.eligible) === true ? 'ready' : 'not_possible');
|
|
58
|
+
}).catch((error) => {
|
|
59
|
+
var _a;
|
|
60
|
+
if (!live)
|
|
61
|
+
return;
|
|
62
|
+
setErrorStatus((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status);
|
|
63
|
+
setState('preview_error');
|
|
64
|
+
});
|
|
65
|
+
return () => { live = false; };
|
|
66
|
+
}, [open, purchase === null || purchase === void 0 ? void 0 : purchase.id, api, attempt]);
|
|
67
|
+
const confirm = async () => {
|
|
68
|
+
var _a, _b;
|
|
69
|
+
if (confirmGuard.current || !['ready', 'confirm_error'].includes(state) || typeof (api === null || api === void 0 ? void 0 : api.confirmWithdrawal) !== 'function')
|
|
70
|
+
return;
|
|
71
|
+
confirmGuard.current = true;
|
|
72
|
+
setState('submitting');
|
|
73
|
+
setErrorStatus(null);
|
|
74
|
+
try {
|
|
75
|
+
const value = await api.confirmWithdrawal(purchase.id);
|
|
76
|
+
setResult(value);
|
|
77
|
+
setState('success');
|
|
78
|
+
if (!notified.current) {
|
|
79
|
+
notified.current = true;
|
|
80
|
+
onWithdrawn === null || onWithdrawn === void 0 ? void 0 : onWithdrawn(value, purchase);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
setErrorStatus((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status);
|
|
85
|
+
setState(((_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status) === 409 ? 'not_possible' : 'confirm_error');
|
|
86
|
+
confirmGuard.current = false;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const close = () => { if (state !== 'submitting')
|
|
90
|
+
onClose === null || onClose === void 0 ? void 0 : onClose(); };
|
|
91
|
+
const amount = formatPrice(preview === null || preview === void 0 ? void 0 : preview.refund_cents, i18n.language, preview === null || preview === void 0 ? void 0 : preview.currency);
|
|
92
|
+
const none = !(Number(preview === null || preview === void 0 ? void 0 : preview.refund_cents) > 0);
|
|
93
|
+
return _jsxs(Dialog, { open: open, onClose: close, disableEscapeKeyDown: state === 'submitting', fullWidth: true, maxWidth: "sm", "aria-labelledby": titleId, children: [_jsx(DialogTitle, { id: titleId, children: t(state === 'success' ? 'Billing.WITHDRAWAL_DONE' : 'Billing.WITHDRAWAL_TITLE') }), _jsxs(DialogContent, { children: [state === 'loading' && _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", role: "status", children: [_jsx(CircularProgress, { size: 20 }), _jsx(Typography, { children: t('Billing.LOADING') })] }), state === 'preview_error' && _jsx(Alert, { severity: "error", sx: WITHDRAWAL_DIALOG_ALERT_SX, children: t('Billing.WITHDRAWAL_PREVIEW_ERROR') }), (state === 'not_possible') && _jsx(Alert, { severity: "info", children: t('Billing.WITHDRAWAL_NOT_POSSIBLE') }), (state === 'ready' || state === 'submitting' || state === 'confirm_error') && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { children: t('Billing.WITHDRAWAL_EFFECT') }), _jsx(Typography, { children: none ? t('Billing.WITHDRAWAL_REFUND_NONE') : t('Billing.WITHDRAWAL_REFUND', { amount }) }), _jsx(Typography, { children: t('Billing.WITHDRAWAL_CALCULATION', { explanation: describeCalculation(Object.assign(Object.assign({}, preview), { kind: purchase === null || purchase === void 0 ? void 0 : purchase.kind }), t, i18n.language) }) }), state === 'confirm_error' && _jsx(Alert, { severity: "error", sx: WITHDRAWAL_DIALOG_ALERT_SX, children: t('Billing.WITHDRAWAL_ERROR') }), state === 'submitting' && _jsxs(Typography, { role: "status", children: [_jsx(CircularProgress, { size: 16 }), " ", t('Billing.LOADING')] })] }), state === 'success' && _jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE_DETAIL', { date: formatDateTime(result === null || result === void 0 ? void 0 : result.withdrawn_at, i18n.language) }) })] }), _jsx(DialogActions, { children: state === 'ready' || state === 'confirm_error' ? _jsxs(_Fragment, { children: [_jsx(Button, { onClick: close, disabled: state === 'submitting', children: t('Billing.WITHDRAWAL_CANCEL') }), _jsx(Button, { variant: "contained", onClick: confirm, disabled: state === 'submitting', children: t('Billing.WITHDRAWAL_CONFIRM') })] }) : state === 'preview_error' ? _jsxs(_Fragment, { children: [_jsx(Button, { onClick: close, children: t('Billing.WITHDRAWAL_CLOSE') }), _jsx(Button, { onClick: () => setAttempt((n) => n + 1), children: t('Billing.RETRY') })] }) : (state === 'success' || state === 'not_possible') && _jsx(Button, { onClick: close, children: t('Billing.WITHDRAWAL_CLOSE') }) })] });
|
|
94
|
+
}
|
|
95
|
+
export default WithdrawalDialog;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Alert, Button, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { formatDate, formatDateTime } from './format';
|
|
6
|
+
import { WithdrawalDialog } from './WithdrawalDialog';
|
|
7
|
+
export const WITHDRAWAL_SECTION_ALERT_SX = { width: '100%' };
|
|
8
|
+
const is404 = (error) => { var _a; return ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === 404; };
|
|
9
|
+
const deadline = (purchase) => {
|
|
10
|
+
const date = new Date(purchase.purchased_at);
|
|
11
|
+
if (!Number.isFinite(date.getTime()))
|
|
12
|
+
return '';
|
|
13
|
+
date.setUTCDate(date.getUTCDate() + 14);
|
|
14
|
+
return date.toISOString();
|
|
15
|
+
};
|
|
16
|
+
export function WithdrawalSection({ api, plans = [], onWithdrawn, sessionWithdrawals = {}, sx }) {
|
|
17
|
+
const { t, i18n } = useTranslation();
|
|
18
|
+
const [purchases, setPurchases] = useState([]);
|
|
19
|
+
const [withdrawn, setWithdrawn] = useState({});
|
|
20
|
+
const [selected, setSelected] = useState(null);
|
|
21
|
+
const [loadError, setLoadError] = useState(false);
|
|
22
|
+
const [reload, setReload] = useState(0);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (typeof (api === null || api === void 0 ? void 0 : api.listWithdrawals) !== 'function')
|
|
25
|
+
return undefined;
|
|
26
|
+
let live = true;
|
|
27
|
+
Promise.resolve().then(() => api.listWithdrawals()).then((data) => {
|
|
28
|
+
if (live) {
|
|
29
|
+
setPurchases(Array.isArray(data === null || data === void 0 ? void 0 : data.purchases) ? data.purchases : []);
|
|
30
|
+
setLoadError(false);
|
|
31
|
+
}
|
|
32
|
+
}).catch((error) => {
|
|
33
|
+
if (!live)
|
|
34
|
+
return;
|
|
35
|
+
if (is404(error)) {
|
|
36
|
+
setPurchases([]);
|
|
37
|
+
setLoadError(false);
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
setLoadError(true);
|
|
41
|
+
});
|
|
42
|
+
return () => { live = false; };
|
|
43
|
+
}, [api, reload]);
|
|
44
|
+
const handleWithdrawn = (purchaseId, result) => {
|
|
45
|
+
setWithdrawn((current) => (Object.assign(Object.assign({}, current), { [purchaseId]: { purchase: purchases.find((item) => String(item.id) === String(purchaseId)), result } })));
|
|
46
|
+
setSelected(null);
|
|
47
|
+
onWithdrawn === null || onWithdrawn === void 0 ? void 0 : onWithdrawn(result, purchases.find((item) => String(item.id) === String(purchaseId)));
|
|
48
|
+
setReload((count) => count + 1);
|
|
49
|
+
};
|
|
50
|
+
const successful = Object.assign(Object.assign({}, sessionWithdrawals), withdrawn);
|
|
51
|
+
const eligible = purchases.filter((item) => (item === null || item === void 0 ? void 0 : item.eligible) === true && item.withdrawn_at == null && !successful[item.id]);
|
|
52
|
+
if (loadError)
|
|
53
|
+
return _jsxs(Stack, { sx: sx, spacing: 1, children: [_jsx(Alert, { severity: "error", sx: WITHDRAWAL_SECTION_ALERT_SX, children: t('Billing.WITHDRAWAL_LOAD_ERROR') }), _jsx(Button, { onClick: () => setReload((count) => count + 1), children: t('Billing.RETRY') }), Object.entries(successful).map(([id, entry]) => { var _a; return _jsxs(Stack, { spacing: 0.5, role: "status", children: [_jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE') }), _jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE_DETAIL', { date: formatDateTime((_a = ((entry === null || entry === void 0 ? void 0 : entry.result) || entry)) === null || _a === void 0 ? void 0 : _a.withdrawn_at, i18n.language) }) })] }, id); })] });
|
|
54
|
+
if (!eligible.length && !Object.keys(successful).length)
|
|
55
|
+
return null;
|
|
56
|
+
const findPlan = (key) => { var _a; return ((_a = plans.find((plan) => (plan === null || plan === void 0 ? void 0 : plan.key) === key)) === null || _a === void 0 ? void 0 : _a.name) || key; };
|
|
57
|
+
return _jsxs(Stack, { sx: sx, spacing: 1, children: [eligible.map((purchase) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [_jsx(Typography, { children: findPlan(purchase.plan_key) }), _jsx(Typography, { children: formatDate(purchase.purchased_at, i18n.language) }), _jsx(Typography, { children: t('Billing.WITHDRAWAL_UNTIL', { date: formatDate(deadline(purchase), i18n.language) }) }), _jsx(Button, { onClick: () => setSelected(purchase), children: t('Billing.WITHDRAWAL_BUTTON') })] }, purchase.id)), Object.entries(successful).map(([id, entry]) => { var _a; return _jsxs(Stack, { spacing: 0.5, role: "status", children: [_jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE') }), _jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE_DETAIL', { date: formatDateTime((_a = ((entry === null || entry === void 0 ? void 0 : entry.result) || entry)) === null || _a === void 0 ? void 0 : _a.withdrawn_at, i18n.language) }) })] }, id); }), _jsx(WithdrawalDialog, { open: Boolean(selected), purchase: selected, api: api, onClose: () => setSelected(null), onWithdrawn: (result) => selected && handleWithdrawn(selected.id, result) })] });
|
|
58
|
+
}
|
|
59
|
+
export default WithdrawalSection;
|
package/dist/billing/api.js
CHANGED
|
@@ -10,6 +10,9 @@ export function createBillingApi(overrides = {}, { apiBase = '/api/billing/' } =
|
|
|
10
10
|
listSeats: () => data(apiClient.get(urls.seats)),
|
|
11
11
|
addSeat: ({ userId, email } = {}) => data(apiClient.post(urls.seats, userId !== undefined ? { user_id: userId } : { email })),
|
|
12
12
|
removeSeat: ({ userId, email } = {}) => data(apiClient.delete(urls.seats, { data: userId !== undefined ? { user_id: userId } : { email } })),
|
|
13
|
+
listWithdrawals: () => data(apiClient.get(urls.withdrawals)),
|
|
14
|
+
previewWithdrawal: (id) => data(apiClient.get(urls.withdrawalPreview(id))),
|
|
15
|
+
confirmWithdrawal: (id) => data(apiClient.post(urls.withdrawalConfirm(id), { confirm: true })),
|
|
13
16
|
};
|
|
14
17
|
return Object.assign(Object.assign({}, defaults), overrides);
|
|
15
18
|
}
|
package/dist/billing/format.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
// The kit's own language codes (for example de_informal) are not valid Intl locales; use the base language.
|
|
2
|
+
export function intlLocale(language) {
|
|
3
|
+
return typeof language === 'string' && language ? language.split('_')[0] : undefined;
|
|
4
|
+
}
|
|
5
|
+
export function formatPrice(amountCents, language, currency = 'USD') {
|
|
2
6
|
if (!Number.isFinite(amountCents))
|
|
3
7
|
return '';
|
|
4
|
-
|
|
8
|
+
const candidate = typeof currency === 'string' ? currency.toUpperCase() : '';
|
|
9
|
+
let validCurrency = 'USD';
|
|
10
|
+
try {
|
|
11
|
+
const supported = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('currency') : null;
|
|
12
|
+
if (/^[A-Z]{3}$/.test(candidate) && (!supported || supported.includes(candidate))) {
|
|
13
|
+
new Intl.NumberFormat(intlLocale(language), { style: 'currency', currency: candidate });
|
|
14
|
+
validCurrency = candidate;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (_a) { /* use USD for unknown currency codes */ }
|
|
18
|
+
return new Intl.NumberFormat(intlLocale(language), { style: 'currency', currency: validCurrency }).format(amountCents / 100);
|
|
5
19
|
}
|
|
6
20
|
export function formatTokens(count, language) {
|
|
7
21
|
if (!Number.isFinite(count))
|
|
8
22
|
return '';
|
|
9
|
-
return new Intl.NumberFormat(language, { maximumFractionDigits: 0 }).format(count);
|
|
23
|
+
return new Intl.NumberFormat(intlLocale(language), { maximumFractionDigits: 0 }).format(count);
|
|
10
24
|
}
|
|
11
25
|
export function formatDate(iso, language) {
|
|
12
26
|
if (typeof iso !== 'string' || !iso)
|
|
@@ -14,5 +28,13 @@ export function formatDate(iso, language) {
|
|
|
14
28
|
const date = new Date(iso);
|
|
15
29
|
if (!Number.isFinite(date.getTime()))
|
|
16
30
|
return '';
|
|
17
|
-
return new Intl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(date);
|
|
31
|
+
return new Intl.DateTimeFormat(intlLocale(language), { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(date);
|
|
32
|
+
}
|
|
33
|
+
export function formatDateTime(iso, language) {
|
|
34
|
+
if (typeof iso !== 'string' || !iso)
|
|
35
|
+
return '';
|
|
36
|
+
const date = new Date(iso);
|
|
37
|
+
if (!Number.isFinite(date.getTime()))
|
|
38
|
+
return '';
|
|
39
|
+
return new Intl.DateTimeFormat(intlLocale(language), { dateStyle: 'long', timeStyle: 'short' }).format(date);
|
|
18
40
|
}
|
package/dist/billing/wire.js
CHANGED
|
@@ -9,5 +9,8 @@ export const billingApiUrls = (base = '/api/billing/') => {
|
|
|
9
9
|
checkout: root + 'checkout/',
|
|
10
10
|
portal: root + 'portal/',
|
|
11
11
|
seats: root + 'team/seats/',
|
|
12
|
+
withdrawals: root + 'withdrawal/',
|
|
13
|
+
withdrawalPreview: (id) => root + `withdrawal/${encodeURIComponent(id)}/preview/`,
|
|
14
|
+
withdrawalConfirm: (id) => root + `withdrawal/${encodeURIComponent(id)}/`,
|
|
12
15
|
};
|
|
13
16
|
};
|
|
@@ -40,4 +40,24 @@ export const billingTranslations = {
|
|
|
40
40
|
'Billing.TEAM_SEATS': { de: 'Sitzplätze im Team-Paket', en: 'Seats in the team package', fr: 'Places dans le forfait d’équipe', sw: 'Nafasi katika kifurushi cha timu' },
|
|
41
41
|
'Billing.PACKAGE_TOKENS': { de: '{{tokens}} Token', en: '{{tokens}} tokens', fr: '{{tokens}} jetons', sw: 'Tokeni {{tokens}}' },
|
|
42
42
|
'Billing.PLANS': { de: 'Pläne', en: 'Plans', fr: 'Offres', sw: 'Mipango' },
|
|
43
|
+
'Billing.WITHDRAWAL_BUTTON': { de: 'Vertrag hier widerrufen', de_informal: 'Vertrag hier widerrufen', en: 'Withdraw this contract', fr: 'Révoquer ce contrat ici', sw: 'Ondoa mkataba huu hapa' },
|
|
44
|
+
'Billing.WITHDRAWAL_UNTIL': { de: 'bis {{date}}', de_informal: 'bis {{date}}', en: 'until {{date}}', fr: 'jusqu’au {{date}}', sw: 'hadi {{date}}' },
|
|
45
|
+
'Billing.WITHDRAWAL_TITLE': { de: 'Vertrag widerrufen?', de_informal: 'Vertrag widerrufen?', en: 'Withdraw this contract?', fr: 'Révoquer ce contrat ?', sw: 'Uondoe mkataba huu?' },
|
|
46
|
+
'Billing.WITHDRAWAL_EFFECT': { de: 'Mit dem Widerruf endet der Zugang aus diesem Kauf sofort, und das Abo verlängert sich nicht.', de_informal: 'Mit dem Widerruf endet der Zugang aus diesem Kauf sofort, und das Abo verlängert sich nicht.', en: 'With the withdrawal, access from this purchase ends immediately and the subscription will not renew.', fr: 'La révocation met immédiatement fin à l’accès lié à cet achat et l’abonnement ne sera pas renouvelé.', sw: 'Ukiondoa, ufikiaji wa ununuzi huu utaisha mara moja na usajili hautasasishwa.' },
|
|
47
|
+
'Billing.WITHDRAWAL_REFUND': { de: 'Sie erhalten {{amount}} zurück.', de_informal: 'Du erhältst {{amount}} zurück.', en: 'You will receive {{amount}} back.', fr: 'Vous recevrez {{amount}} en remboursement.', sw: 'Utarejeshewa {{amount}}.' },
|
|
48
|
+
'Billing.WITHDRAWAL_REFUND_NONE': { de: 'Sie erhalten keine Rückzahlung.', de_informal: 'Du erhältst keine Rückzahlung.', en: 'You will receive no refund.', fr: 'Vous ne recevrez aucun remboursement.', sw: 'Hutapokea marejesho ya pesa.' },
|
|
49
|
+
'Billing.WITHDRAWAL_CALCULATION': { de: 'Berechnung: {{explanation}}', de_informal: 'Berechnung: {{explanation}}', en: 'Calculation: {{explanation}}', fr: 'Calcul : {{explanation}}', sw: 'Hesabu: {{explanation}}' },
|
|
50
|
+
'Billing.WITHDRAWAL_CONFIRM': { de: 'Widerruf bestätigen', de_informal: 'Widerruf bestätigen', en: 'Confirm withdrawal', fr: 'Confirmer la révocation', sw: 'Thibitisha uondoaji' },
|
|
51
|
+
'Billing.WITHDRAWAL_CANCEL': { de: 'Abbrechen', de_informal: 'Abbrechen', en: 'Cancel', fr: 'Annuler', sw: 'Ghairi' },
|
|
52
|
+
'Billing.WITHDRAWAL_DONE': { de: 'Widerruf eingegangen', de_informal: 'Widerruf eingegangen', en: 'Withdrawal received', fr: 'Révocation reçue', sw: 'Uondoaji umepokelewa' },
|
|
53
|
+
'Billing.WITHDRAWAL_DONE_DETAIL': { de: 'Eingegangen am {{date}}. Eine Bestätigung ist per E-Mail unterwegs; die Rückzahlung folgt innerhalb von 14 Tagen.', de_informal: 'Eingegangen am {{date}}. Eine Bestätigung ist per E-Mail unterwegs; die Rückzahlung folgt innert 14 Tagen.', en: 'Received on {{date}}. A confirmation email is on its way; the refund will follow within 14 days.', fr: 'Reçu le {{date}}. Un e-mail de confirmation est en route ; le remboursement suivra sous 14 jours.', sw: 'Imepokelewa {{date}}. Barua pepe ya uthibitisho inatumwa; marejesho yatafuata ndani ya siku 14.' },
|
|
54
|
+
'Billing.WITHDRAWAL_PREVIEW_ERROR': { de: 'Die Vorschau konnte nicht geladen werden.', de_informal: 'Die Vorschau konnte nicht geladen werden.', en: 'The preview could not be loaded.', fr: 'Impossible de charger l’aperçu.', sw: 'Muhtasari haukuweza kupakiwa.' },
|
|
55
|
+
'Billing.WITHDRAWAL_NOT_POSSIBLE': { de: 'Dieser Vertrag kann derzeit nicht widerrufen werden.', de_informal: 'Dieser Vertrag kann derzeit nicht widerrufen werden.', en: 'This contract cannot be withdrawn right now.', fr: 'Ce contrat ne peut pas être révoqué pour le moment.', sw: 'Mkataba huu hauwezi kuondolewa kwa sasa.' },
|
|
56
|
+
'Billing.WITHDRAWAL_ERROR': { de: 'Der Widerruf konnte nicht übermittelt werden. Bitte versuchen Sie es erneut.', de_informal: 'Der Widerruf konnte nicht übermittelt werden. Bitte versuche es erneut.', en: 'The withdrawal could not be submitted. Please try again.', fr: 'La révocation n’a pas pu être envoyée. Veuillez réessayer.', sw: 'Uondoaji haukuweza kutumwa. Tafadhali jaribu tena.' },
|
|
57
|
+
'Billing.WITHDRAWAL_LOAD_ERROR': { de: 'Widerrufsmöglichkeiten konnten nicht geladen werden.', de_informal: 'Widerrufsmöglichkeiten konnten nicht geladen werden.', en: 'Withdrawal options could not be loaded.', fr: 'Impossible de charger les possibilités de révocation.', sw: 'Chaguo za uondoaji hazikuweza kupakiwa.' },
|
|
58
|
+
'Billing.WITHDRAWAL_PAID': { de: 'Gezahlt: {{amount}}', de_informal: 'Gezahlt: {{amount}}', en: 'Paid: {{amount}}', fr: 'Payé : {{amount}}', sw: 'Imelipwa: {{amount}}' },
|
|
59
|
+
'Billing.WITHDRAWAL_TIME_SHARE': { de: 'Zeitanteil: {{amount}}', de_informal: 'Zeitanteil: {{amount}}', en: 'Time share: {{amount}}', fr: 'Part temporelle : {{amount}}', sw: 'Sehemu ya muda: {{amount}}' },
|
|
60
|
+
'Billing.WITHDRAWAL_TOKEN_SHARE': { de: 'Tokenanteil: {{amount}}', de_informal: 'Tokenanteil: {{amount}}', en: 'Token share: {{amount}}', fr: 'Part des jetons : {{amount}}', sw: 'Sehemu ya tokeni: {{amount}}' },
|
|
61
|
+
'Billing.WITHDRAWAL_USED': { de: 'Genutzt: {{amount}}', de_informal: 'Genutzt: {{amount}}', en: 'Used: {{amount}}', fr: 'Utilisé : {{amount}}', sw: 'Imetumika: {{amount}}' },
|
|
62
|
+
'Billing.WITHDRAWAL_CLOSE': { de: 'Schließen', de_informal: 'Schließen', en: 'Close', fr: 'Fermer', sw: 'Funga' },
|
|
43
63
|
};
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,8 @@ export { TokenCostPreview } from './billing/TokenCostPreview';
|
|
|
48
48
|
export { AccessEndedBanner } from './billing/AccessEndedBanner';
|
|
49
49
|
export { PlanPicker } from './billing/PlanPicker';
|
|
50
50
|
export { SubscriptionSection } from './billing/SubscriptionSection';
|
|
51
|
+
export { WithdrawalSection } from './billing/WithdrawalSection';
|
|
52
|
+
export { WithdrawalDialog } from './billing/WithdrawalDialog';
|
|
51
53
|
export { useBilling } from './billing/useBilling';
|
|
52
54
|
export { createBillingApi, defaultBillingApi } from './billing/api';
|
|
53
55
|
// --- 6. Charts ---
|
|
@@ -37,6 +37,8 @@ import { ACCESS_ENDED_BANNER_ALERT_SX } from '../billing/AccessEndedBanner';
|
|
|
37
37
|
import { TOKEN_BALANCE_BADGE_SX } from '../billing/TokenBalanceBadge';
|
|
38
38
|
import { PLAN_PICKER_ALERT_SX, PLAN_PICKER_CARD_SX, PLAN_PICKER_LABEL_SX, PLAN_PICKER_SELECTED_CARD_SX } from '../billing/PlanPicker';
|
|
39
39
|
import { SUBSCRIPTION_SECTION_ALERT_SX } from '../billing/SubscriptionSection';
|
|
40
|
+
import { WITHDRAWAL_DIALOG_ALERT_SX } from '../billing/WithdrawalDialog';
|
|
41
|
+
import { WITHDRAWAL_SECTION_ALERT_SX } from '../billing/WithdrawalSection';
|
|
40
42
|
const SX_EXPORTS = {
|
|
41
43
|
ACCESS_CODE_ACTION_SX,
|
|
42
44
|
ACCESS_ENDED_BANNER_ALERT_SX,
|
|
@@ -112,6 +114,8 @@ const SX_EXPORTS = {
|
|
|
112
114
|
PLAN_PICKER_LABEL_SX,
|
|
113
115
|
PLAN_PICKER_SELECTED_CARD_SX,
|
|
114
116
|
SUBSCRIPTION_SECTION_ALERT_SX,
|
|
117
|
+
WITHDRAWAL_DIALOG_ALERT_SX,
|
|
118
|
+
WITHDRAWAL_SECTION_ALERT_SX,
|
|
115
119
|
USER_INVITE_ACTION_SX,
|
|
116
120
|
USER_INVITE_ALERT_SX,
|
|
117
121
|
USER_LIST_ACTION_SX,
|
|
@@ -217,4 +221,6 @@ export const KIT_COMPONENT_SX_REGISTRY = [
|
|
|
217
221
|
entry('PlanPicker.label', 'MuiFormControlLabel', PLAN_PICKER_LABEL_SX),
|
|
218
222
|
entry('PlanPicker.alert', 'MuiAlert', PLAN_PICKER_ALERT_SX),
|
|
219
223
|
entry('SubscriptionSection.alert', 'MuiAlert', SUBSCRIPTION_SECTION_ALERT_SX),
|
|
224
|
+
entry('WithdrawalDialog.alert', 'MuiAlert', WITHDRAWAL_DIALOG_ALERT_SX),
|
|
225
|
+
entry('WithdrawalSection.alert', 'MuiAlert', WITHDRAWAL_SECTION_ALERT_SX),
|
|
220
226
|
];
|