mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat(legal): wire recurrent-payments document + restore footer link
Backend now serves a dedicated recurring-payments legal document, so: - Add adminLegalPagesApi.get/updateRecurrentPayments and infoApi.getRecurrentPayments (+ InfoVisibility.recurrent). - AdminLegalPages: new 'Рекуррентные платежи' editor tab (DocumentEditor now driven by a DOCUMENT_API dispatch map instead of privacy/offer ternaries). - PublicLegal: add 'recurrent' doc; add public /recurrent-payments route. - Re-add the recurrent link to the login LegalFooter (was removed pending backend). - i18n: admin.legalPages.tabs.recurrent for ru/en/fa/zh (footer.recurrent already existed). type-check, eslint, prettier and build all pass.
This commit is contained in:
@@ -269,6 +269,7 @@ function App() {
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route path="/offer" element={<PublicLegal doc="offer" />} />
|
||||
<Route path="/privacy" element={<PublicLegal doc="privacy" />} />
|
||||
<Route path="/recurrent-payments" element={<PublicLegal doc="recurrent" />} />
|
||||
<Route
|
||||
path="/merge/:mergeToken"
|
||||
element={
|
||||
|
||||
@@ -111,6 +111,23 @@ export const adminLegalPagesApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getRecurrentPayments: async (): Promise<LegalDocumentResponse> => {
|
||||
const response = await apiClient.get<LegalDocumentResponse>(
|
||||
'/cabinet/admin/legal-pages/recurrent-payments',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateRecurrentPayments: async (
|
||||
data: LegalDocumentUpdateRequest,
|
||||
): Promise<LegalDocumentResponse> => {
|
||||
const response = await apiClient.put<LegalDocumentResponse>(
|
||||
'/cabinet/admin/legal-pages/recurrent-payments',
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getRules: async (): Promise<AdminRulesResponse> => {
|
||||
const response = await apiClient.get<AdminRulesResponse>('/cabinet/admin/legal-pages/rules');
|
||||
return response.data;
|
||||
|
||||
@@ -23,6 +23,11 @@ export interface PublicOfferResponse {
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface RecurrentPaymentsResponse {
|
||||
content: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface ServiceInfo {
|
||||
name: string;
|
||||
description: string | null;
|
||||
@@ -42,6 +47,7 @@ export interface InfoVisibility {
|
||||
rules: boolean;
|
||||
privacy: boolean;
|
||||
offer: boolean;
|
||||
recurrent: boolean;
|
||||
}
|
||||
|
||||
export const infoApi = {
|
||||
@@ -75,6 +81,14 @@ export const infoApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get recurring-payments document
|
||||
getRecurrentPayments: async (): Promise<RecurrentPaymentsResponse> => {
|
||||
const response = await apiClient.get<RecurrentPaymentsResponse>(
|
||||
'/cabinet/info/recurrent-payments',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get service info
|
||||
getServiceInfo: async (): Promise<ServiceInfo> => {
|
||||
const response = await apiClient.get<ServiceInfo>('/cabinet/info/service');
|
||||
|
||||
@@ -7,12 +7,10 @@ interface LegalLink {
|
||||
fallback: string;
|
||||
}
|
||||
|
||||
// Recurring-payments has no dedicated legal document in the backend yet, so it is
|
||||
// intentionally omitted rather than pointed at unrelated content. Re-add once a
|
||||
// `recurrent-payments` document + public endpoint exist.
|
||||
const LINKS: LegalLink[] = [
|
||||
{ href: '/offer', labelKey: 'footer.offer', fallback: 'Публичная оферта' },
|
||||
{ href: '/privacy', labelKey: 'footer.privacy', fallback: 'Политика конфиденциальности' },
|
||||
{ href: '/recurrent-payments', labelKey: 'footer.recurrent', fallback: 'Рекуррентные платежи' },
|
||||
];
|
||||
|
||||
interface LegalFooterProps {
|
||||
|
||||
@@ -4181,6 +4181,7 @@
|
||||
"tabs": {
|
||||
"privacy": "Privacy",
|
||||
"offer": "Offer",
|
||||
"recurrent": "Recurring Payments",
|
||||
"rules": "Rules",
|
||||
"faq": "FAQ"
|
||||
},
|
||||
|
||||
@@ -4006,6 +4006,7 @@
|
||||
"tabs": {
|
||||
"privacy": "حریم خصوصی",
|
||||
"offer": "پیشنهاد",
|
||||
"recurrent": "پرداختهای تکراری",
|
||||
"rules": "قوانین",
|
||||
"faq": "FAQ"
|
||||
},
|
||||
|
||||
@@ -4726,6 +4726,7 @@
|
||||
"tabs": {
|
||||
"privacy": "Политика конф.",
|
||||
"offer": "Оферта",
|
||||
"recurrent": "Рекуррентные платежи",
|
||||
"rules": "Правила",
|
||||
"faq": "FAQ"
|
||||
},
|
||||
|
||||
@@ -4005,6 +4005,7 @@
|
||||
"tabs": {
|
||||
"privacy": "隐私",
|
||||
"offer": "条款",
|
||||
"recurrent": "定期付款",
|
||||
"rules": "规则",
|
||||
"faq": "FAQ"
|
||||
},
|
||||
|
||||
@@ -14,10 +14,35 @@ import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
||||
import { cn } from '../lib/utils';
|
||||
import { ChevronDownIcon, ChevronUpIcon, PlusIcon, TrashIcon } from '@/components/icons';
|
||||
|
||||
type LegalTab = 'privacy' | 'offer' | 'rules' | 'faq';
|
||||
type LegalTab = 'privacy' | 'offer' | 'recurrent' | 'rules' | 'faq';
|
||||
|
||||
const DISPLAY_MODES: LegalDisplayMode[] = ['bot', 'web', 'both'];
|
||||
|
||||
type DocumentKind = 'privacy-policy' | 'public-offer' | 'recurrent-payments';
|
||||
|
||||
const DOCUMENT_API: Record<
|
||||
DocumentKind,
|
||||
{
|
||||
get: () => ReturnType<typeof adminLegalPagesApi.getPrivacyPolicy>;
|
||||
update: (
|
||||
data: Parameters<typeof adminLegalPagesApi.updatePrivacyPolicy>[0],
|
||||
) => ReturnType<typeof adminLegalPagesApi.updatePrivacyPolicy>;
|
||||
}
|
||||
> = {
|
||||
'privacy-policy': {
|
||||
get: adminLegalPagesApi.getPrivacyPolicy,
|
||||
update: adminLegalPagesApi.updatePrivacyPolicy,
|
||||
},
|
||||
'public-offer': {
|
||||
get: adminLegalPagesApi.getPublicOffer,
|
||||
update: adminLegalPagesApi.updatePublicOffer,
|
||||
},
|
||||
'recurrent-payments': {
|
||||
get: adminLegalPagesApi.getRecurrentPayments,
|
||||
update: adminLegalPagesApi.updateRecurrentPayments,
|
||||
},
|
||||
};
|
||||
|
||||
function extractErrorDetail(err: unknown): string | null {
|
||||
const error = err as { response?: { data?: { detail?: unknown } } };
|
||||
const detail = error.response?.data?.detail;
|
||||
@@ -103,7 +128,7 @@ function DocumentEditor({
|
||||
kind,
|
||||
onDirtyChange,
|
||||
}: {
|
||||
kind: 'privacy-policy' | 'public-offer';
|
||||
kind: DocumentKind;
|
||||
onDirtyChange: (dirty: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -118,10 +143,7 @@ function DocumentEditor({
|
||||
|
||||
const { data, isLoading, isFetching } = useQuery({
|
||||
queryKey: ['admin', 'legal-pages', kind],
|
||||
queryFn: () =>
|
||||
kind === 'privacy-policy'
|
||||
? adminLegalPagesApi.getPrivacyPolicy()
|
||||
: adminLegalPagesApi.getPublicOffer(),
|
||||
queryFn: () => DOCUMENT_API[kind].get(),
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
});
|
||||
@@ -167,9 +189,7 @@ function DocumentEditor({
|
||||
is_enabled: enabled[language] ?? false,
|
||||
})),
|
||||
};
|
||||
return kind === 'privacy-policy'
|
||||
? adminLegalPagesApi.updatePrivacyPolicy(payload)
|
||||
: adminLegalPagesApi.updatePublicOffer(payload);
|
||||
return DOCUMENT_API[kind].update(payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
haptic.success();
|
||||
@@ -661,7 +681,7 @@ export default function AdminLegalPages() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(['privacy', 'offer', 'rules', 'faq'] as const).map((tab) => (
|
||||
{(['privacy', 'offer', 'recurrent', 'rules', 'faq'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
@@ -684,6 +704,9 @@ export default function AdminLegalPages() {
|
||||
{activeTab === 'offer' && (
|
||||
<DocumentEditor key="offer" kind="public-offer" onDirtyChange={setDirty} />
|
||||
)}
|
||||
{activeTab === 'recurrent' && (
|
||||
<DocumentEditor key="recurrent" kind="recurrent-payments" onDirtyChange={setDirty} />
|
||||
)}
|
||||
{activeTab === 'rules' && <RulesEditor onDirtyChange={setDirty} />}
|
||||
{activeTab === 'faq' && <FaqEditor onDirtyChange={setDirty} />}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { infoApi } from '../api/info';
|
||||
import { formatContent } from '../utils/legalContent';
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher';
|
||||
|
||||
export type PublicLegalDoc = 'offer' | 'privacy';
|
||||
export type PublicLegalDoc = 'offer' | 'privacy' | 'recurrent';
|
||||
|
||||
interface PublicLegalProps {
|
||||
doc: PublicLegalDoc;
|
||||
@@ -32,6 +32,12 @@ const DOC_CONFIG: Record<
|
||||
titleFallback: 'Политика конфиденциальности',
|
||||
fetch: infoApi.getPrivacyPolicy,
|
||||
},
|
||||
recurrent: {
|
||||
queryKey: 'recurrent-payments',
|
||||
titleKey: 'footer.recurrent',
|
||||
titleFallback: 'Рекуррентные платежи',
|
||||
fetch: infoApi.getRecurrentPayments,
|
||||
},
|
||||
};
|
||||
|
||||
// Public, unauthenticated viewer for the legal documents linked from the login
|
||||
|
||||
Reference in New Issue
Block a user