diff --git a/src/api/adminBroadcasts.ts b/src/api/adminBroadcasts.ts index 4e6fb0a..dfc77ff 100644 --- a/src/api/adminBroadcasts.ts +++ b/src/api/adminBroadcasts.ts @@ -76,6 +76,8 @@ export interface EmailBroadcastCreateRequest { export interface CombinedBroadcastCreateRequest { channel: BroadcastChannel; target: string; + // Broadcast category for user notification preference filtering + category?: 'system' | 'news' | 'promo'; // Telegram fields message_text?: string; selected_buttons?: string[]; @@ -111,6 +113,8 @@ export interface Broadcast { created_at: string; completed_at: string | null; progress_percent: number; + // Broadcast category + category?: 'system' | 'news' | 'promo'; // New fields for channel support channel?: BroadcastChannel; email_subject?: string | null; diff --git a/src/api/auth.ts b/src/api/auth.ts index 4eae2be..7bfd3fa 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -97,6 +97,7 @@ export const authApi = { first_name?: string; language?: string; referral_code?: string; + campaign_slug?: string; }): Promise => { const response = await apiClient.post( '/cabinet/auth/email/register/standalone', diff --git a/src/components/admin/MenuEditorTab.tsx b/src/components/admin/MenuEditorTab.tsx index 15baf1e..3548762 100644 --- a/src/components/admin/MenuEditorTab.tsx +++ b/src/components/admin/MenuEditorTab.tsx @@ -573,8 +573,10 @@ export function MenuEditorTab() { setDraftConfig(data); queryClient.setQueryData(['menu-layout'], data); }, - onError: () => { - notify.error(t('common.error')); + onError: (err: unknown) => { + const error = err as { response?: { data?: { detail?: string } } }; + const detail = error.response?.data?.detail; + notify.error(detail || t('common.error')); }, }); @@ -748,7 +750,12 @@ export function MenuEditorTab() { for (const row of currentDraft.rows) { for (const btn of row.buttons) { if (btn.type === 'custom') { - if (!btn.url || (!btn.url.startsWith('http://') && !btn.url.startsWith('https://'))) { + if ( + !btn.url || + (!btn.url.startsWith('http://') && + !btn.url.startsWith('https://') && + !btn.url.startsWith('tg://')) + ) { notify.error(t('admin.menuEditor.invalidUrl')); return; } diff --git a/src/main.tsx b/src/main.tsx index 1619030..2b9b765 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -27,6 +27,16 @@ import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegra import './i18n'; import './styles/globals.css'; +// Polyfill Object.hasOwn for older iOS/Android WebViews (Safari < 15.4, old Chrome). +// @telegram-apps/sdk v3 depends on valibot which uses Object.hasOwn internally. +// Without this, init() throws LaunchParamsRetrieveError on affected devices. +// See: https://github.com/Telegram-Mini-Apps/tma.js/issues/683 +if (typeof (Object as { hasOwn?: unknown }).hasOwn !== 'function') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Object as any).hasOwn = (obj: object, prop: PropertyKey): boolean => + Object.prototype.hasOwnProperty.call(obj, prop); +} + // Only initialize Telegram SDK when running inside Telegram const isTelegramEnv = !!(window as unknown as Record).TelegramWebviewProxy || diff --git a/src/pages/AdminBanSystem.tsx b/src/pages/AdminBanSystem.tsx index 5a7ee2d..edfe8c0 100644 --- a/src/pages/AdminBanSystem.tsx +++ b/src/pages/AdminBanSystem.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { AdminBackButton } from '../components/admin/AdminBackButton'; import { banSystemApi, type BanSystemStatus, @@ -557,6 +558,7 @@ export default function AdminBanSystem() { {/* Header */}
+
diff --git a/src/pages/AdminBroadcastCreate.tsx b/src/pages/AdminBroadcastCreate.tsx index 1709aab..de04726 100644 --- a/src/pages/AdminBroadcastCreate.tsx +++ b/src/pages/AdminBroadcastCreate.tsx @@ -128,6 +128,9 @@ export default function AdminBroadcastCreate() { const [showTelegramFilters, setShowTelegramFilters] = useState(false); const [showEmailFilters, setShowEmailFilters] = useState(false); + // Broadcast category (system/news/promo) + const [category, setCategory] = useState<'system' | 'news' | 'promo'>('system'); + // Telegram-specific state const [messageText, setMessageText] = useState(''); const [selectedButtons, setSelectedButtons] = useState(['home']); @@ -395,6 +398,7 @@ export default function AdminBroadcastCreate() { message_text: messageText, selected_buttons: selectedButtons, custom_buttons: customButtons.length > 0 ? customButtons : undefined, + category, }; if (uploadedFileId) { data.media = { type: mediaType, file_id: uploadedFileId }; @@ -409,6 +413,7 @@ export default function AdminBroadcastCreate() { target: emailTarget, email_subject: emailSubject, email_html_content: emailContent, + category, }; createMutation.mutate(data); return; @@ -423,6 +428,7 @@ export default function AdminBroadcastCreate() { message_text: messageText, selected_buttons: selectedButtons, custom_buttons: customButtons.length > 0 ? customButtons : undefined, + category, }; if (uploadedFileId) { telegramData.media = { type: mediaType, file_id: uploadedFileId }; @@ -433,6 +439,7 @@ export default function AdminBroadcastCreate() { target: emailTarget, email_subject: emailSubject, email_html_content: emailContent, + category, }; await adminBroadcastsApi.createCombined(telegramData); @@ -583,6 +590,36 @@ export default function AdminBroadcastCreate() { )}
+ {/* Broadcast category */} +
+ +

+ {t( + 'admin.broadcasts.categoryDesc', + 'Пользователи могут отключить получение новостей и промо в настройках профиля. Системные рассылки доставляются всем.', + )} +

+
+ {(['system', 'news', 'promo'] as const).map((cat) => ( + + ))} +
+
+ {/* Telegram section */} {telegramEnabled && (
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index e437ca3..a9a7b41 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -720,6 +720,16 @@ export default function Login() { value={password} onChange={(e) => setPassword(e.target.value)} /> + {authMode === 'register' && + password.length > 0 && + password.length < 8 && ( +

+ {t( + 'auth.passwordTooShort', + 'Password must be at least 8 characters', + )} +

+ )}
{authMode === 'register' && ( diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index d73674a..9f3a14e 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -584,7 +584,7 @@ export default function Subscription() { {/* Status badge */} ()( registerWithEmail: async (email, password, firstName, referralCode) => { const code = referralCode || getPendingReferralCode() || undefined; + const campaignSlug = getPendingCampaignSlug() || undefined; const response = await authApi.registerEmailStandalone({ email, password, first_name: firstName, language: navigator.language.split('-')[0] || 'ru', referral_code: code, + campaign_slug: campaignSlug, }); consumeReferralCode(); return response;