Files
remnawave-minishop/frontend/src/App.svelte
T

2372 lines
80 KiB
Svelte

<script>
import { onMount, setContext, tick } from "svelte";
import { createAuthStore } from "./lib/webapp/stores/authStore.js";
import { createBillingStore } from "./lib/webapp/stores/billingStore.js";
import { createDevicesStore } from "./lib/webapp/stores/devicesStore.js";
import { createInstallGuidesStore } from "./lib/webapp/stores/installGuidesStore.js";
import { createSupportStore } from "./lib/webapp/stores/supportStore.js";
import { createAccountStore } from "./lib/webapp/stores/accountStore.js";
import { Tooltip } from "$components/ui/primitives.js";
import { CheckCircle2 } from "$components/ui/icons.js";
import BrandMark from "$lib/webapp/BrandMark.svelte";
import Button from "$components/ui/button.svelte";
import Dialog from "$components/ui/dialog.svelte";
import WebAppShell from "./webapp/WebAppShell.svelte";
import AuthScreen from "./webapp/auth/AuthScreen.svelte";
import PaymentDialogs from "./webapp/PaymentDialogs.svelte";
import TariffDialogs from "./webapp/TariffDialogs.svelte";
import AppLaunchScreen from "./webapp/screens/AppLaunchScreen.svelte";
import DevicesScreen from "./webapp/screens/DevicesScreen.svelte";
import HomeScreen from "./webapp/screens/HomeScreen.svelte";
import InstallGuideScreen from "./webapp/screens/InstallGuideScreen.svelte";
import InviteScreen from "./webapp/screens/InviteScreen.svelte";
import SettingsScreen from "./webapp/screens/SettingsScreen.svelte";
import SupportScreen from "./webapp/screens/SupportScreen.svelte";
import SupportTicketScreen from "./webapp/screens/SupportTicketScreen.svelte";
import TrialActivationScreen from "./webapp/screens/TrialActivationScreen.svelte";
import {
LANGUAGE_FLAGS,
LANGUAGE_LABELS,
MANUAL_LOGOUT_FLAG_KEY,
TELEGRAM_MINI_APP_AUTH_TIMEOUT_MS,
TELEGRAM_SDK_ACTION_TIMEOUT_MS,
TELEGRAM_SDK_BOOT_TIMEOUT_MS,
TELEGRAM_WEBAPP_SCRIPT_URL,
uniqueLanguageCodes,
WEBAPP_LANGUAGE_ORDER,
} from "./lib/webapp/constants.js";
import {
applyFavicon,
applyDocumentTitle,
normalizeBrand,
readJsonScript,
structuredCloneSafe,
} from "./lib/webapp/browser.js";
import {
buildExternalAppLaunchUrl,
hasControlChars,
isExternalAppLaunchPath,
isHttpUrl,
openUrlWithHiddenAnchor,
readExternalAppLaunchTarget,
} from "./lib/webapp/appLinks.js";
import { createApiClient } from "./lib/webapp/publicApi.js";
import { createI18n } from "./lib/webapp/i18n.js";
import { normalizedEmail, telegramName } from "./lib/webapp/formatters.js";
import { activeTariffName, buildTariffCatalog } from "./lib/webapp/tariffs.js";
import { premiumTrafficPercent, trafficPercent } from "./lib/webapp/traffic.js";
import {
findThemeEntry,
resolveEffectiveThemeKey,
themeCssHref,
themeEntryToInlineStyle,
themeRootClass,
} from "./lib/webapp/themeStyle.js";
/** Used-traffic percent from which top-up modals and CTAs unlock in the web app home screen */
const TRAFFIC_TOPUP_UNLOCK_PERCENT = 80;
const ACTIVATION_HANDOFF_STORAGE_KEY = "rw_webapp_activation_handoff_v1";
const ACTIVATION_HANDOFF_TTL_MS = 48 * 60 * 60 * 1000;
const ACTIVATION_PENDING_WATCH_INTERVAL_MS = 2000;
const ACTIVATION_PENDING_WATCH_MAX_ATTEMPTS = 45;
const ACTIVATION_RESUME_CHECK_COOLDOWN_MS = 1500;
import {
activationPaymentFailed,
createActivationHandoff,
} from "./lib/webapp/activationHandoff.js";
import { buildGravatarUrl } from "./lib/webapp/gravatar.js";
import { createBillingActions } from "./lib/webapp/billingActions.js";
import { invalidateWebappTariffOptionCaches } from "./lib/webapp/billingOptionCache.js";
import { runWebappBoot } from "./lib/webapp/webappBoot.js";
import {
clearManualLogoutFlag as clearManualLogoutFlagInStorage,
clearStoredToken,
CSRF_COOKIE_NAME,
isManuallyLoggedOut as readManualLogoutFlag,
markManualLogout as markManualLogoutInStorage,
readCookie,
} from "./lib/webapp/session.js";
import { createTelegramSdk } from "./lib/webapp/telegramSdk.js";
import {
adminPaymentIdFromPath,
adminPaymentsUserIdFromPath,
adminSectionFromPath,
adminUserIdFromPath,
normalizeAdminSection,
normalizeSection,
publicInstallTokenFromPath,
sectionFromPath,
supportTicketIdFromPath,
syncSectionPath,
withRoutePrefix,
} from "./lib/webapp/routes.js";
export let mockRuntime = null;
const EMPTY_MOCK = {
config: {
title: "/minishop",
primaryColor: "#00fe7a",
apiBase: "/api",
language: "ru",
languages: [],
},
data: {
plans: [],
payment_methods: [],
subscription: {},
settings: {},
referral: {},
themes_catalog: { default_theme: "dark", themes: [] },
},
};
const MOCK_SOURCE = mockRuntime?.source || EMPTY_MOCK;
const previewBoardComponent = mockRuntime?.PreviewBoard || null;
const isDocsDemo = mockRuntime?.docsDemo === true;
const routePrefix = isDocsDemo ? "/demo/runtime" : "";
let docsDemoParentRouteConsumed = false;
const query = new URLSearchParams(window.location.search);
const isAppLaunchRoute = isExternalAppLaunchPath(window.location.pathname);
mockRuntime?.applyPreviewMock?.(query.get("mock"));
const isPreviewBoard = Boolean(previewBoardComponent) && query.get("preview") === "all";
const injectedConfig = readJsonScript("webapp-config");
const injectedI18n = readJsonScript("i18n");
const isLocalShell =
window.location.protocol === "file:" ||
["", "localhost", "127.0.0.1"].includes(window.location.hostname);
const MOCK =
mockRuntime?.mockApi && !injectedConfig && (isLocalShell || isDocsDemo) ? MOCK_SOURCE : null;
const CFG = {
...MOCK_SOURCE.config,
...(MOCK ? MOCK.config : {}),
...(injectedConfig || {}),
};
const themePreviewKey = String(CFG.themePreviewKey || query.get("theme_preview") || "").trim();
const I18N = injectedI18n || {};
let telegramSdkStatus = "idle";
let telegramMiniAppInitData = "";
let mode = isAppLaunchRoute ? "appLaunch" : isPreviewBoard ? "preview" : "loading";
let activeTab = "home";
let screen = "home";
let data = isPreviewBoard ? structuredCloneSafe(MOCK_SOURCE.data) : null;
let appLaunchTarget = isAppLaunchRoute ? readExternalAppLaunchTarget() : "";
let publicInstallSubscription = null;
let publicInstallToken = "";
let trialBusy = false;
let trialActivationResult = null;
let trialActivationError = "";
let activationSuccessDialogOpen = false;
let activationSuccessUseInstallGuides = false;
let activationPendingWatchTimer = null;
let activationPendingWatchAttempts = 0;
let activationPendingWatchBusy = false;
let activationResumeRefreshBusy = false;
let activationResumeLastCheckAt = 0;
let promoCode = "";
let promoBusy = false;
let promoStatus = "";
let promoIsError = false;
let promoFieldError = "";
let toastText = "";
let toastTimer = null;
let languageMenuOpen = false;
let languageClickGuard = false;
let languageClickGuardArmed = false;
let languageClickGuardTimer = null;
let languageClickGuardArmTimer = null;
let emailAvatarUrl = "";
let avatarHashToken = "";
let token = MOCK ? "local-preview" : "";
let csrfToken = MOCK ? "" : readCookie(CSRF_COOKIE_NAME) || "";
let scrollLockApplied = false;
let adminI18nLoaded = false;
let adminI18nPromise = null;
let adminBundleApi = null;
let adminBundlePromise = null;
let adminBundleError = "";
let adminMountTarget = null;
let adminMountHandle = null;
let adminMountedTarget = null;
let adminPanelProps = {};
let adminActiveSection = "stats";
let tg = null;
const telegramSdk = createTelegramSdk({
scriptUrl: TELEGRAM_WEBAPP_SCRIPT_URL,
bootTimeoutMs: TELEGRAM_SDK_BOOT_TIMEOUT_MS,
actionTimeoutMs: TELEGRAM_SDK_ACTION_TIMEOUT_MS,
miniAppAuthTimeoutMs: TELEGRAM_MINI_APP_AUTH_TIMEOUT_MS,
onStatusChange: (status) => (telegramSdkStatus = status),
onInitDataChange: (initData) => (telegramMiniAppInitData = initData || ""),
});
tg = telegramSdk.refresh();
telegramSdkStatus = tg ? "ready" : "idle";
telegramMiniAppInitData = telegramSdk.initData;
const i18n = createI18n({
messages: I18N,
defaultLang: "ru",
getLang: () => user?.language_code || CFG.language || "ru",
});
const normalizeLangCode = i18n.normalizeLangCode;
const t = i18n.t;
const termUnitLabel = i18n.termUnitLabel;
const languageName = i18n.languageName;
const apiClient = createApiClient({
apiBase: CFG.apiBase,
csrfCookieName: CSRF_COOKIE_NAME,
getCsrfToken: () => csrfToken,
onUnauthorized: () => {
clearToken();
showLogin();
},
mockApi:
MOCK && mockRuntime?.mockApi
? (path, options, context) => mockRuntime.mockApi(path, options, context)
: null,
getMockContext: () => ({ currentLang, normalizeLangCode, clone: structuredCloneSafe }),
});
const billing = createBillingActions({
api: (path, options) => apiClient.api(path, options),
t: (...args) => t(...args),
});
const activationHandoff = createActivationHandoff({
storageKey: ACTIVATION_HANDOFF_STORAGE_KEY,
ttlMs: ACTIVATION_HANDOFF_TTL_MS,
});
const authStore = createAuthStore({
publicApi,
setToken,
loadData,
telegramSdk,
getTg: () => tg,
t,
currentLang: () => currentLang,
clearManualLogoutFlag,
});
const billingStore = createBillingStore({
billing,
loadData,
t,
showToast,
openExternalLink,
onSubscriptionActivationPending: rememberActivationPending,
onSubscriptionActivated: handleSubscriptionActivated,
tg,
getTg: () => tg || telegramSdk.refresh(),
telegramSdk,
});
const devicesStore = createDevicesStore({ api, t, showToast });
const supportStore = createSupportStore({ api, t, showToast, routePrefix });
const installGuidesStore = createInstallGuidesStore({ api, t, showToast });
const accountStore = createAccountStore({
api,
publicApi,
setToken,
loadData,
t,
showToast,
clearToken,
markManualLogout,
showLogin,
telegramSdk,
getTg: () => tg,
telegramOAuthClientId: () => telegramOAuthClientId,
currentLang: () => currentLang,
normalizeLangCode,
updateLocalData: (updatedLanguage) => {
if (!data?.user) return;
data = { ...data, user: { ...data.user, language_code: updatedLanguage } };
},
});
setContext("authStore", authStore);
setContext("billingStore", billingStore);
setContext("devicesStore", devicesStore);
setContext("supportStore", supportStore);
setContext("installGuidesStore", installGuidesStore);
setContext("accountStore", accountStore);
$: ({
authStatus,
authIsError,
authBusy,
telegramLoginBusy,
loginEmailFieldError,
loginEmailTooltipOpen,
passwordLoginFallback,
passwordLoginMode,
authResendCooldown,
pendingEmail,
} = $authStore);
$: ({
paymentModalOpen,
selectedTariffKey,
selectedPlan,
topupModalOpen,
topupKind,
deviceTopupModalOpen,
changeModalOpen,
topupOptions,
deviceTopupOptions,
changeOptions,
changeConfirmOpen,
tariffActionBusy,
payBusy,
} = $billingStore);
$: ({
devicesData,
devicesLoaded,
devicesBusy,
devicesStatus,
devicesIsError,
deviceConfirmOpen,
deviceToDisconnect,
deviceDisconnectBusy,
} = $devicesStore);
$: ({
unreadCount: supportUnreadCount,
unreadLoading: supportUnreadLoading,
unreadLoaded: supportUnreadLoaded,
} = $supportStore);
$: ({
linkEmailOpen,
linkEmailBusy,
linkTelegramBusy,
linkEmailPending,
linkEmailStatus,
linkEmailIsError,
linkEmailResendCooldown,
setPasswordBusy,
setPasswordIsError,
setPasswordOpen,
setPasswordPending,
setPasswordResendCooldown,
setPasswordStatus,
languageBusy,
} = $accountStore);
$: brandTitle = CFG.title || "/minishop";
$: brandEmoji = CFG.logoEmoji || "🫥";
$: brandEmojiFont = CFG.logoEmojiFont || "system";
$: brand = normalizeBrand({
title: brandTitle,
logoUrl: CFG.logoUseEmoji ? "" : CFG.logoUrl,
emoji: brandEmoji,
emojiFont: brandEmojiFont,
});
$: faviconBrand = normalizeBrand({
...brand,
logoUrl: String(CFG.faviconUrl || "").trim() || brand.logoUrl,
});
$: plans = data?.plans?.length ? data.plans : MOCK_SOURCE.data.plans;
$: methods = data?.payment_methods?.length ? data.payment_methods : [];
$: appSettings = data?.settings || MOCK_SOURCE.data.settings;
$: subscriptionPurchaseDescription = String(
appSettings?.subscription_purchase_description || ""
).trim();
$: trafficMode = Boolean(appSettings?.traffic_mode);
$: tariffMode = plans.some((plan) => plan?.tariff_key);
$: tariffCatalog = buildTariffCatalog(plans);
$: singleTariffMode = tariffMode && tariffCatalog.length === 1;
$: hasMultipleTariffs = tariffCatalog.length > 1;
$: selectedTariff = tariffCatalog.find((tariff) => tariff.key === selectedTariffKey) || null;
$: selectedTariffPlans = tariffMode
? selectedTariffKey
? plans.filter((plan) => plan?.tariff_key === selectedTariffKey)
: []
: plans;
$: devicesEnabled = Boolean(appSettings?.my_devices_enabled);
$: supportEnabled = Boolean(appSettings?.support_tickets_enabled ?? true);
$: installGuidesEnabled = Boolean(appSettings?.subscription_guides_enabled);
$: supportStore.setActive(Boolean(mode === "app" && screen === "support" && supportEnabled));
$: subscription = data?.subscription || MOCK_SOURCE.data.subscription;
$: hasActiveTariffSubscription = Boolean(
tariffMode && subscription?.active && subscription?.tariff_key
);
$: canChangeTariff = Boolean(hasActiveTariffSubscription && hasMultipleTariffs);
$: currentTariffName = activeTariffName(subscription, plans);
$: canOpenRegularTopupModal = Boolean(
hasActiveTariffSubscription &&
(subscription?.can_topup_regular_traffic ?? subscription?.can_topup_traffic) &&
Number(subscription?.traffic_limit_bytes || 0) > 0
);
$: canOpenPremiumTopupModal = Boolean(
hasActiveTariffSubscription &&
(subscription?.can_topup_premium_traffic ?? subscription?.can_topup_traffic) &&
Number(subscription?.premium_limit_bytes || 0) > 0
);
$: activeTariffCatalogEntry =
tariffCatalog.find((entry) => entry.key === String(subscription?.tariff_key || "").trim()) ||
null;
$: subscriptionIsTrafficTariff = Boolean(
String(
subscription?.billing_model || activeTariffCatalogEntry?.billing_model || ""
).toLowerCase() === "traffic"
);
$: regularTrafficTopupUnlocked = Boolean(
canOpenRegularTopupModal && trafficPercent(subscription) >= TRAFFIC_TOPUP_UNLOCK_PERCENT
);
$: premiumTrafficTopupUnlocked = Boolean(
canOpenPremiumTopupModal && premiumTrafficPercent(subscription) >= TRAFFIC_TOPUP_UNLOCK_PERCENT
);
/** Progress-bar card opens top-up immediately on traffic-only tariffs; period tariffs still need 80% usage */
$: regularTrafficTopupBarClickable = Boolean(
canOpenRegularTopupModal &&
(subscriptionIsTrafficTariff || trafficPercent(subscription) >= TRAFFIC_TOPUP_UNLOCK_PERCENT)
);
$: premiumTrafficTopupBarClickable = Boolean(
canOpenPremiumTopupModal &&
(subscriptionIsTrafficTariff ||
premiumTrafficPercent(subscription) >= TRAFFIC_TOPUP_UNLOCK_PERCENT)
);
$: user = data?.user || {};
$: themesCatalog = data?.themes_catalog ||
CFG.themesCatalog || { default_theme: "dark", themes: [] };
$: previewThemeAllowed = Boolean(themePreviewKey && (!data?.user || user?.is_admin));
$: previewThemeEntry = previewThemeAllowed
? findThemeEntry(themesCatalog, themePreviewKey)
: null;
$: resolvedThemeKey = previewThemeEntry?.key || resolveEffectiveThemeKey(themesCatalog);
$: activeThemeEntry = findThemeEntry(themesCatalog, resolvedThemeKey);
$: darkThemeEntry = findThemeEntry(themesCatalog, "dark");
$: effectiveThemeEntry =
screen === "admin" && activeThemeEntry?.use_in_admin === false
? darkThemeEntry || activeThemeEntry
: activeThemeEntry;
$: shellStyle = themeEntryToInlineStyle(effectiveThemeEntry, CFG.primaryColor);
$: shellToneClass =
effectiveThemeEntry?.tokens?.color_scheme === "light" ? "theme-light" : "theme-dark";
$: shellThemeClass = themeRootClass(effectiveThemeEntry);
$: shellThemeCssHref = themeCssHref(effectiveThemeEntry);
$: if (typeof document !== "undefined" && effectiveThemeEntry?.tokens) {
const scheme = effectiveThemeEntry.tokens.color_scheme || "dark";
document.documentElement.style.colorScheme = scheme;
const bg = effectiveThemeEntry.tokens.bg;
if (bg) document.body.style.backgroundColor = bg;
}
$: isAdmin = Boolean(user?.is_admin);
$: if (screen === "admin" && !isAdmin) {
screen = "settings";
activeTab = "settings";
}
$: referral = data?.referral || MOCK_SOURCE.data.referral;
$: currentLang = normalizeLangCode(user?.language_code || CFG.language || "ru");
$: languageCodes = uniqueLanguageCodes(
WEBAPP_LANGUAGE_ORDER,
CFG.languages,
Object.keys(I18N || {}),
[currentLang]
);
$: languageOptions = languageCodes.map((code) => {
const serverLanguage = (CFG.languages || []).find((language) => language.code === code);
return {
value: code,
label: serverLanguage?.label || LANGUAGE_LABELS[code] || code.toUpperCase(),
flag: serverLanguage?.flag || LANGUAGE_FLAGS[code] || "🏳️",
};
});
$: currentLanguageOption =
languageOptions.find((option) => option.value === currentLang) || languageOptions[0];
$: userLanguage = languageName(currentLang);
$: emailLinkStatus = user?.email ? t("wa_settings_linked") : t("wa_settings_email_not_linked");
$: hasUnlinkedIdentity = !user?.telegram_linked || !user?.email;
$: referralBonusDetails = Array.isArray(referral?.bonus_details) ? referral.bonus_details : [];
$: referralWelcomeBonusDays = Math.max(0, Number(referral?.welcome_bonus_days || 0));
$: referralOneBonusPerReferee = Boolean(referral?.one_bonus_per_referee);
$: telegramProfileName = telegramName(user);
$: profileEmail = user?.email || t("wa_settings_email_not_linked");
$: profileTelegramId = user?.telegram_id ? `TG ID ${user.telegram_id}` : t("wa_tg_id_not_linked");
$: profileAvatarUrl = user?.telegram_photo_url || emailAvatarUrl || "";
$: privacyPolicyUrl = String(CFG.privacyPolicyUrl || "").trim();
$: userAgreementUrl = String(CFG.userAgreementUrl || "").trim();
$: supportUrl = String(appSettings?.support_url || CFG.supportUrl || "").trim();
$: telegramLoginBotId = Number(CFG.telegramLoginBotId || 0);
$: telegramOAuthClientId = Number(CFG.telegramOAuthClientId || telegramLoginBotId || 0);
$: telegramMiniAppInitData = tg?.initData || readTelegramMiniAppInitDataFromLocation();
$: telegramMiniAppAuthAvailable = Boolean(telegramMiniAppInitData);
$: telegramMiniAppContext = hasTelegramLaunchParams();
$: demoAuthLogin = MOCK && isDemoAuthMock();
$: telegramLoginUnavailable =
!demoAuthLogin &&
!telegramMiniAppAuthAvailable &&
!telegramOAuthClientId &&
telegramSdkStatus !== "loading";
$: telegramLoginChecking =
telegramLoginBusy || (authBusy && authStatus === t("wa_auth_checking_telegram"));
$: telegramLoginLabel = telegramLoginUnavailable
? t("wa_login_telegram_unavailable_button")
: telegramLoginChecking
? t("wa_auth_checking_telegram")
: t("wa_login_telegram_button");
$: telegramLoginUnavailableMessage = demoAuthLogin
? ""
: telegramLoginUnavailable && telegramSdkStatus === "unavailable"
? t("wa_auth_telegram_unavailable")
: telegramLoginUnavailable
? t("wa_auth_telegram_not_configured")
: "";
$: applyFavicon(faviconBrand);
$: applyDocumentTitle(brandTitle);
$: syncBodyScrollLock(
paymentModalOpen ||
changeModalOpen ||
changeConfirmOpen ||
topupModalOpen ||
deviceTopupModalOpen ||
linkEmailOpen ||
setPasswordOpen
);
$: if (!tariffMode && !$billingStore.selectedPlan && plans.length) {
billingStore.update((s) => ({ ...s, selectedPlan: plans[Math.min(1, plans.length - 1)] }));
}
$: if (singleTariffMode && tariffCatalog[0]?.key && selectedTariffKey !== tariffCatalog[0].key) {
const tariffKey = tariffCatalog[0].key;
billingStore.update((s) => ({
...s,
selectedTariffKey: tariffKey,
selectedPlan: plans.find((plan) => plan?.tariff_key === tariffKey) || null,
paymentStep: s.paymentStep === "tariff" ? "checkout" : s.paymentStep,
}));
}
$: if (
tariffMode &&
selectedTariffKey &&
!tariffCatalog.some((tariff) => tariff.key === selectedTariffKey)
) {
billingStore.update((s) => ({
...s,
selectedTariffKey: "",
selectedPlan: null,
paymentStep: singleTariffMode ? "checkout" : "tariff",
}));
}
$: if (
tariffMode &&
selectedTariffKey &&
(!selectedPlan || selectedPlan.tariff_key !== selectedTariffKey)
) {
billingStore.update((s) => ({ ...s, selectedPlan: selectedTariffPlans[0] || null }));
}
$: if (methods.length) {
const selectedMethodAvailable = methods.some(
(method) => method.id === $billingStore.selectedMethod
);
if (!$billingStore.selectedMethod || !selectedMethodAvailable) {
billingStore.update((s) => ({ ...s, selectedMethod: methods[0].id }));
}
} else if ($billingStore.selectedMethod) {
billingStore.update((s) => ({ ...s, selectedMethod: "" }));
}
$: {
const emailKey = normalizedEmail(user?.email);
if (!emailKey) {
avatarHashToken = "";
emailAvatarUrl = "";
} else if (avatarHashToken !== emailKey) {
avatarHashToken = emailKey;
buildGravatarUrl(emailKey).then((url) => {
if (avatarHashToken === emailKey) emailAvatarUrl = url;
});
}
}
function canUseInstallGuides(settings = appSettings, sub = subscription) {
const enabled =
settings === appSettings
? installGuidesEnabled
: Boolean(settings?.subscription_guides_enabled);
return Boolean(enabled && sub?.active);
}
function hasPendingActivationHandoff(payload = data) {
return activationHandoff.hasPending(payload);
}
function rememberActivationPending(context = {}) {
activationHandoff.rememberPending(context, data);
}
function clearPendingActivationHandoff() {
activationHandoff.clearPending();
}
async function maybeShowActivationSuccessDialog(context = {}) {
if (activationSuccessDialogOpen) return false;
await tick();
const payload = context.payload || data;
const subscriptionKey = activationHandoff.subscriptionKey(payload);
if (!subscriptionKey) return false;
const state = activationHandoff.read();
const pending = state.pending;
if (!context.force && activationHandoff.isAcknowledged(subscriptionKey, state)) {
if (pending && activationHandoff.pendingMatchesUser(pending, payload)) {
activationHandoff.write({ ...state, pending: null });
}
return false;
}
if (
!context.force &&
(!pending ||
!activationHandoff.isPendingFresh(pending) ||
!activationHandoff.pendingMatchesUser(pending, payload))
) {
return false;
}
activationHandoff.acknowledge(subscriptionKey, context, payload, state);
stopPendingActivationWatch();
activationSuccessUseInstallGuides = canUseInstallGuides();
billingStore.closePaymentModal();
activeTab = "home";
if (!activationSuccessUseInstallGuides) {
screen = "home";
syncAppSectionPath("home", true);
}
activationSuccessDialogOpen = true;
return true;
}
function stopPendingActivationWatch() {
if (activationPendingWatchTimer) {
window.clearTimeout(activationPendingWatchTimer);
activationPendingWatchTimer = null;
}
activationPendingWatchAttempts = 0;
activationPendingWatchBusy = false;
}
function schedulePendingActivationWatch() {
if (activationPendingWatchTimer || !hasPendingActivationHandoff()) return;
activationPendingWatchTimer = window.setTimeout(() => {
activationPendingWatchTimer = null;
void checkPendingActivationWatch();
}, ACTIVATION_PENDING_WATCH_INTERVAL_MS);
}
function startPendingActivationWatch() {
if (
mode !== "app" ||
!hasPendingActivationHandoff() ||
activationSuccessDialogOpen ||
screen === "admin"
) {
stopPendingActivationWatch();
return;
}
if (activationPendingWatchTimer || activationPendingWatchBusy) return;
schedulePendingActivationWatch();
}
async function checkPendingActivationWatch() {
if (activationPendingWatchBusy) return;
if (
mode !== "app" ||
!hasPendingActivationHandoff() ||
activationSuccessDialogOpen ||
screen === "admin"
) {
stopPendingActivationWatch();
return;
}
if (activationPendingWatchAttempts >= ACTIVATION_PENDING_WATCH_MAX_ATTEMPTS) {
stopPendingActivationWatch();
return;
}
const state = activationHandoff.read();
const pending = state.pending;
activationPendingWatchAttempts += 1;
activationPendingWatchBusy = true;
try {
let shouldRefreshProfile = !pending?.paymentId;
if (pending?.paymentId && billing.fetchPaymentStatus) {
const paymentStatus = await billing.fetchPaymentStatus(pending.paymentId);
if (paymentStatus?.paid || paymentStatus?.status === "succeeded") {
shouldRefreshProfile = true;
} else if (activationPaymentFailed(paymentStatus)) {
clearPendingActivationHandoff();
stopPendingActivationWatch();
return;
}
}
if (shouldRefreshProfile) {
await loadData({ fresh: true });
const shown = await maybeShowActivationSuccessDialog({
source: "watch",
paymentId: pending?.paymentId,
});
if (shown || !hasPendingActivationHandoff()) {
stopPendingActivationWatch();
return;
}
}
} catch (_error) {
void _error;
} finally {
activationPendingWatchBusy = false;
}
schedulePendingActivationWatch();
}
function canRefreshPendingActivationOnResume() {
return Boolean(
mode === "app" &&
screen !== "admin" &&
!activationSuccessDialogOpen &&
!paymentModalOpen &&
!topupModalOpen &&
!deviceTopupModalOpen &&
!changeModalOpen &&
!changeConfirmOpen &&
hasPendingActivationHandoff()
);
}
async function refreshPendingActivationOnResume() {
if (!canRefreshPendingActivationOnResume()) return;
const now = Date.now();
if (
activationResumeRefreshBusy ||
now - activationResumeLastCheckAt < ACTIVATION_RESUME_CHECK_COOLDOWN_MS
) {
return;
}
activationResumeLastCheckAt = now;
activationResumeRefreshBusy = true;
try {
await loadData({ fresh: true });
const shown = await maybeShowActivationSuccessDialog({ source: "resume" });
if (!shown) startPendingActivationWatch();
} catch (_error) {
void _error;
} finally {
activationResumeRefreshBusy = false;
}
}
function refreshAppLaunchTarget() {
appLaunchTarget = readExternalAppLaunchTarget();
return appLaunchTarget;
}
function openAppLaunchTarget(nextTarget = "") {
const target = String(nextTarget || refreshAppLaunchTarget() || "").trim();
if (!target) return false;
appLaunchTarget = target;
openUrlWithHiddenAnchor(target);
return true;
}
onMount(() => {
if (isPreviewBoard) return;
if (isAppLaunchRoute) return;
const onAnyPointerDown = () => {
if (mode === "login") loginEmailTooltipOpen = false;
};
const onActivationResume = () => {
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
void refreshPendingActivationOnResume();
};
const onVisibilityChange = () => {
if (document.visibilityState !== "hidden") onActivationResume();
};
const onPopState = () => {
const shareToken = publicInstallTokenFromPath(window.location.pathname);
if (shareToken) {
void loadPublicInstall(shareToken);
return;
}
if (mode === "publicInstall") {
void boot();
return;
}
const currentQuery = currentSearchParams();
const section =
isDocsDemo && currentQuery.get("screen")
? normalizeSection(currentQuery.get("screen"))
: sectionFromPath(routePathnameFromLocation(), routePrefix);
if (mode === "login") {
setPasswordLoginMode(isPasswordLoginPath(), true);
screen = "login";
return;
}
if (mode === "app") {
if (section === "admin" && isAdmin) {
adminActiveSection = isDocsDemo
? initialAdminSectionFromLocation()
: adminSectionFromPath(routePathnameFromLocation(), routePrefix);
const pathAtStart = window.location.pathname;
void Promise.all([ensureI18nScope("admin"), ensureAdminBundle()])
.then(() => {
if (sectionFromPath(routePathnameFromLocation(), routePrefix) !== "admin") return;
if (window.location.pathname !== pathAtStart) return;
activeTab = "settings";
screen = "admin";
})
.catch(() => {
showToast(t("wa_unavailable"));
});
return;
}
const nextSection =
section === "devices" && !devicesEnabled
? "home"
: section === "support" && !supportEnabled
? "home"
: section === "install" && !canUseInstallGuides()
? "home"
: section;
activeTab = nextSection === "install" || nextSection === "trial" ? "home" : nextSection;
screen = nextSection;
if (nextSection === "devices") devicesStore.loadDevices(devicesEnabled);
if (nextSection === "support") {
supportStore.loadList();
supportStore.startPolling({ includeList: true });
}
if (nextSection === "install") installGuidesStore.load(true);
}
};
window.addEventListener("popstate", onPopState);
window.addEventListener("pointerdown", onAnyPointerDown);
window.addEventListener("focus", onActivationResume);
window.addEventListener("pageshow", onActivationResume);
document.addEventListener("visibilitychange", onVisibilityChange);
boot();
return () => {
window.removeEventListener("popstate", onPopState);
window.removeEventListener("pointerdown", onAnyPointerDown);
window.removeEventListener("focus", onActivationResume);
window.removeEventListener("pageshow", onActivationResume);
document.removeEventListener("visibilitychange", onVisibilityChange);
authStore.stopTelegramLoginWatchdog();
authStore.clearCooldownTimer();
accountStore.clearLinkEmailResendTimer();
accountStore.clearSetPasswordResendTimer();
supportStore.closePolling();
stopPendingActivationWatch();
clearLanguageClickGuard();
syncBodyScrollLock(false);
destroyAdminMount();
};
});
function syncBodyScrollLock(locked) {
if (typeof document === "undefined") return;
if (locked && !scrollLockApplied) {
document.body.style.overflow = "hidden";
scrollLockApplied = true;
return;
}
if (!locked && scrollLockApplied) {
document.body.style.overflow = "";
scrollLockApplied = false;
}
}
function clearLanguageClickGuard() {
if (languageClickGuardTimer) {
window.clearTimeout(languageClickGuardTimer);
languageClickGuardTimer = null;
}
if (languageClickGuardArmTimer) {
window.clearTimeout(languageClickGuardArmTimer);
languageClickGuardArmTimer = null;
}
languageClickGuard = false;
languageClickGuardArmed = false;
}
function setLanguageMenuOpen(open) {
languageMenuOpen = Boolean(open);
clearLanguageClickGuard();
if (languageMenuOpen) {
languageClickGuard = true;
languageClickGuardArmTimer = window.setTimeout(() => {
languageClickGuardArmed = true;
languageClickGuardArmTimer = null;
}, 220);
return;
}
languageClickGuard = true;
languageClickGuardArmed = false;
languageClickGuardTimer = window.setTimeout(() => {
languageClickGuard = false;
languageClickGuardTimer = null;
}, 260);
}
function readTelegramMiniAppInitDataFromLocation() {
return telegramSdk.readInitDataFromLocation();
}
function hasTelegramLaunchParams() {
return telegramSdk.hasLaunchParams();
}
function loadTelegramSdk(timeoutMs = TELEGRAM_SDK_BOOT_TIMEOUT_MS) {
return telegramSdk.load(timeoutMs).then((value) => {
tg = value;
telegramMiniAppInitData = telegramSdk.initData;
return value;
});
}
async function ensureI18nScope(scope) {
if (MOCK || scope !== "admin" || adminI18nLoaded) return;
if (adminI18nPromise) return adminI18nPromise;
const apiBase = String(CFG.apiBase || "/api").replace(/\/+$/, "");
adminI18nPromise = fetch(`${apiBase}/i18n?scope=admin`, {
credentials: "same-origin",
headers: { Accept: "application/json" },
})
.then((response) => (response.ok ? response.json() : null))
.then((payload) => {
if (!payload?.ok || !payload.i18n) return;
i18n.mergeMessages(payload.i18n);
adminI18nLoaded = true;
})
.catch((_error) => {
void _error;
})
.finally(() => {
adminI18nPromise = null;
});
return adminI18nPromise;
}
function resolveWebappAssetPath(configValue, fallbackName) {
const raw = String(configValue || "").trim() || fallbackName;
if (/^(?:https?:)?\/\//i.test(raw) || raw.startsWith("data:")) return fallbackName;
if (window.location.protocol === "file:" && raw.startsWith("/")) return raw.slice(1);
return raw.startsWith("/") ? raw : `/${raw}`;
}
function appendStylesheetOnce(id, href) {
if (!href || document.getElementById(id)) return Promise.resolve();
return new Promise((resolve, reject) => {
const link = document.createElement("link");
link.id = id;
link.rel = "stylesheet";
link.href = href;
link.onload = () => resolve();
link.onerror = () => {
link.remove();
reject(new Error(`stylesheet_load_failed:${href}`));
};
document.head.appendChild(link);
});
}
function appendScriptOnce(id, src) {
if (!src || document.getElementById(id)) return Promise.resolve();
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.id = id;
script.src = src;
script.async = true;
script.onload = () => resolve();
script.onerror = () => {
script.remove();
reject(new Error(`script_load_failed:${src}`));
};
document.head.appendChild(script);
});
}
async function appendStylesheetWithFallback(id, href, fallbackName) {
const fallbackHref = resolveWebappAssetPath("", fallbackName);
try {
await appendStylesheetOnce(id, href);
} catch (error) {
if (!fallbackHref || href === fallbackHref) throw error;
await appendStylesheetOnce(id, fallbackHref);
}
}
async function appendScriptWithFallback(id, src, fallbackName) {
const fallbackSrc = resolveWebappAssetPath("", fallbackName);
try {
await appendScriptOnce(id, src);
} catch (error) {
if (!fallbackSrc || src === fallbackSrc) throw error;
await appendScriptOnce(id, fallbackSrc);
}
}
function readAdminBundleApi() {
const bundle = window.SubscriptionWebAppAdmin;
return bundle?.mount ? bundle : null;
}
async function ensureAdminBundle() {
if (adminBundleApi) return true;
if (adminBundlePromise) return adminBundlePromise;
const existing = readAdminBundleApi();
if (existing) {
adminBundleApi = existing;
return true;
}
adminBundleError = "";
adminBundlePromise = (async () => {
const cssHref = resolveWebappAssetPath(CFG.adminCssAsset, "subscription_webapp_admin.css");
const jsSrc = resolveWebappAssetPath(CFG.adminJsAsset, "subscription_webapp_admin.js");
await appendStylesheetWithFallback(
"subscription-webapp-admin-css",
cssHref,
"subscription_webapp_admin.css"
);
await appendScriptWithFallback(
"subscription-webapp-admin-js",
jsSrc,
"subscription_webapp_admin.js"
);
const loaded = readAdminBundleApi();
if (!loaded) throw new Error("admin_bundle_missing_mount");
adminBundleApi = loaded;
return true;
})()
.catch((error) => {
adminBundleError = error?.message || "admin_bundle_load_failed";
throw error;
})
.finally(() => {
adminBundlePromise = null;
});
return adminBundlePromise;
}
function destroyAdminMount() {
if (!adminMountHandle) return;
adminMountHandle.destroy?.();
adminMountHandle = null;
adminMountedTarget = null;
}
function currentMockMode() {
if (!MOCK) return "";
const currentMock = currentSearchParams().get("mock");
if (currentMock) return String(currentMock).trim().toLowerCase();
const parentMock = docsDemoParentSearchParams()?.get("mock");
return String(parentMock || "")
.trim()
.toLowerCase();
}
function isDemoAuthMock() {
return ["auth", "login", "register"].includes(currentMockMode());
}
function prepareDemoAuthState() {
const authDemo = MOCK_SOURCE.data?.auth_demo || {};
const email = String(authDemo.email || "3252a8@proton.me").trim();
authStore.update((s) => ({
...s,
authStatus: "",
authIsError: false,
authBusy: false,
authResendCooldown: 0,
email,
emailPassword: String(authDemo.password || ""),
pendingEmail: "",
emailCode: "",
passwordLoginMode: false,
passwordLoginFallback: false,
loginEmailFieldError: "",
loginEmailTooltipOpen: false,
telegramLoginBusy: false,
}));
}
async function openLoginTelegram() {
if (demoAuthLogin) {
const authDemo = MOCK_SOURCE.data?.auth_demo || {};
await authStore.finalizeTelegramAuth(
{
id: Number(authDemo.telegram_id || 7410865527),
username: authDemo.telegram_username || "u3252a8",
first_name: authDemo.telegram_first_name || "3252a8",
last_name: authDemo.telegram_last_name || "",
},
"auth_data"
);
return;
}
await authStore.openTelegramLogin(telegramOAuthClientId, () => telegramMiniAppInitData);
}
function demoTelegramAuthPayload() {
const authDemo = MOCK_SOURCE.data?.auth_demo || {};
return {
id: Number(authDemo.telegram_id || 7410865527),
username: authDemo.telegram_username || "u3252a8",
first_name: authDemo.telegram_first_name || "3252a8",
last_name: authDemo.telegram_last_name || "",
};
}
function openSettingsLinkEmailDialog() {
const authDemo = MOCK_SOURCE.data?.auth_demo || {};
accountStore.openLinkEmailDialog(demoAuthLogin ? authDemo.email || "3252a8@proton.me" : "");
}
async function linkTelegramFromSettings() {
if (!demoAuthLogin) {
await accountStore.linkTelegramAccount(() => telegramMiniAppInitData);
return;
}
accountStore.update((s) => ({ ...s, linkTelegramBusy: true }));
try {
const response = await api("/account/telegram/link", {
method: "POST",
body: JSON.stringify({ auth_data: demoTelegramAuthPayload() }),
});
if (!response?.ok) throw response;
if (response?.csrf_token) setToken("", response.csrf_token);
await loadData({ fresh: true, preserveView: true });
showToast(t("wa_settings_linked"));
} catch (error) {
showToast(error?.message || t("wa_auth_telegram_not_confirmed"));
} finally {
accountStore.update((s) => ({ ...s, linkTelegramBusy: false }));
}
}
function currentSearchParams() {
return new URLSearchParams(window.location.search);
}
function docsDemoParentSearchParams() {
if (!isDocsDemo) return null;
try {
if (window.parent === window) return null;
return new URLSearchParams(window.parent.location.search);
} catch (_error) {
return null;
}
}
function normalizeDemoRoutePath(value) {
const raw = String(value || "").trim();
if (!raw) return "";
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
return withSlash.replace(/\/{2,}/g, "/").replace(/\/+$/, "") || "/";
}
function docsDemoRouteParams() {
if (!isDocsDemo) return null;
const currentQuery = currentSearchParams();
const currentParams = {
path: currentQuery.get("path") || "",
screen: currentQuery.get("screen") || "",
adminSection: currentQuery.get("admin_section") || "",
};
if (currentParams.path || currentParams.screen || currentParams.adminSection) {
return currentParams;
}
if (docsDemoParentRouteConsumed) return currentParams;
const parentQuery = docsDemoParentSearchParams();
return {
path: parentQuery?.get("path") || "",
screen: parentQuery?.get("screen") || "",
adminSection: parentQuery?.get("admin_section") || "",
};
}
function docsDemoRoutePathFromParams() {
const params = docsDemoRouteParams();
if (!params) return "";
const explicitPath = normalizeDemoRoutePath(params.path);
if (explicitPath) return explicitPath;
const section = normalizeSection(params.screen);
if (section === "admin") {
return `/admin/${normalizeAdminSection(params.adminSection || "stats")}`;
}
return params.screen ? `/${section}` : "";
}
function routePathnameFromLocation() {
return docsDemoRoutePathFromParams() || window.location.pathname;
}
function cleanDocsDemoRouteQuery() {
if (!isDocsDemo || window.location.protocol === "file:") return;
const url = new URL(window.location.href);
const routeKeys = ["path", "screen", "admin_section"];
const changed = routeKeys.some((key) => url.searchParams.has(key));
if (!changed) return;
for (const key of routeKeys) url.searchParams.delete(key);
const search = url.searchParams.toString();
window.history.replaceState(
null,
"",
`${url.pathname}${search ? `?${search}` : ""}${url.hash}`
);
}
function initialAdminSectionFromLocation() {
const currentQuery = currentSearchParams();
if (MOCK && currentQuery.get("admin_section")) {
return normalizeAdminSection(currentQuery.get("admin_section"));
}
const demoRouteParams = docsDemoRouteParams();
if (MOCK && demoRouteParams?.adminSection) {
return normalizeAdminSection(demoRouteParams.adminSection);
}
return adminSectionFromPath(routePathnameFromLocation(), routePrefix);
}
function syncDocsDemoSection(section, replace = false, adminSection = null, adminUserId = null) {
if (!isDocsDemo || window.location.protocol === "file:") return false;
syncSectionPath(section, replace, adminSection, adminUserId, routePrefix);
cleanDocsDemoRouteQuery();
return true;
}
function syncAppSectionPath(section, replace = false, adminSection = null, adminUserId = null) {
if (syncDocsDemoSection(section, replace, adminSection, adminUserId)) return;
syncSectionPath(section, replace, adminSection, adminUserId);
}
$: adminPanelProps = {
api,
onClose: closeAdminPanel,
onToast: (text) => showToast(text),
initialSection: screen === "admin" ? adminActiveSection : initialAdminSectionFromLocation(),
initialPaymentId: adminPaymentIdFromPath(routePathnameFromLocation(), routePrefix),
initialPaymentUserId: adminPaymentsUserIdFromPath(routePathnameFromLocation(), routePrefix),
initialUserId: adminUserIdFromPath(routePathnameFromLocation(), routePrefix),
onSectionChange: handleAdminSectionChange,
onSettingsSaved: handleAdminPersistedSaved,
onTariffsSaved: handleAdminPersistedSaved,
onThemesSaved: handleAdminPersistedSaved,
onTranslationsSaved: handleAdminTranslationsSaved,
routePrefix,
brandTitle,
brand,
appFaviconUrl: CFG.faviconUrl,
appFaviconUseCustom: CFG.faviconUseCustom,
appVersion: CFG.appVersion,
appRepositoryUrl: CFG.appRepositoryUrl,
currentLang,
languageOptions,
languageBusy,
onLanguageChange: accountStore.updateAccountLanguage,
t,
};
$: {
const shouldMountAdmin = screen === "admin" && isAdmin && adminBundleApi && adminMountTarget;
const props = adminPanelProps;
if (shouldMountAdmin) {
try {
if (adminMountHandle && adminMountedTarget === adminMountTarget) {
adminMountHandle.update?.(props);
} else {
destroyAdminMount();
adminMountTarget.replaceChildren();
adminMountHandle = adminBundleApi.mount(adminMountTarget, props);
adminMountedTarget = adminMountTarget;
}
} catch (error) {
adminBundleError = error?.message || "admin_bundle_mount_failed";
adminBundleApi = null;
destroyAdminMount();
}
} else {
destroyAdminMount();
}
}
async function boot() {
const shareToken = publicInstallTokenFromPath(window.location.pathname);
if (shareToken) {
await loadPublicInstall(shareToken);
return;
}
if (MOCK && isDemoAuthMock()) {
prepareDemoAuthState();
showLogin();
return;
}
await runWebappBoot({
MOCK,
setMode: (next) => {
mode = next;
},
hasTelegramLaunchParams,
loadTelegramSdk,
prepareTelegramMiniApp: () => {
if (!tg) return;
try {
tg.ready();
tg.expand();
} catch (_error) {
void _error;
}
},
loadData,
showLogin,
clearToken,
clearManualLogoutFlag,
isManuallyLoggedOut,
finalizeMagicLogin: (loginToken) => authStore.finalizeMagicLogin(loginToken),
finalizeTelegramAuth: (authData, source) => authStore.finalizeTelegramAuth(authData, source),
setAuthStatus: (message, isError) => authStore.setAuthStatus(message, isError),
t,
getInitDataForBoot: () =>
telegramMiniAppInitData || tg?.initData || readTelegramMiniAppInitDataFromLocation(),
getToken: () => token,
getCsrfToken: () => csrfToken,
});
if (mode === "app" && screen !== "admin") {
if (hasPendingActivationHandoff()) await loadData({ fresh: true });
const shown = await maybeShowActivationSuccessDialog({ source: "boot" });
if (!shown) startPendingActivationWatch();
}
}
function stripTopupQueryFromUrl() {
if (typeof window === "undefined") return;
const u = new URL(window.location.href);
if (!u.searchParams.has("topup")) return;
u.searchParams.delete("topup");
const search = u.searchParams.toString();
const qs = search ? `?${search}` : "";
window.history.replaceState(null, "", `${u.pathname}${qs}${u.hash}`);
}
function isPasswordLoginPath(pathname = routePathnameFromLocation()) {
return (
String(pathname || "")
.replace(/\/+$/, "")
.toLowerCase() === "/login/password"
);
}
function syncPasswordLoginPath(enabled, replace = false) {
if (typeof window === "undefined" || window.location.protocol === "file:") return;
const targetPath = enabled ? "/login/password" : isDocsDemo ? "/login" : "/";
if (isDocsDemo) {
const targetRuntimePath = withRoutePrefix(targetPath, routePrefix);
if (window.location.pathname === targetRuntimePath) return;
const nextUrl = `${targetRuntimePath}${window.location.search}${window.location.hash}`;
window.history[replace ? "replaceState" : "pushState"](null, "", nextUrl);
cleanDocsDemoRouteQuery();
return;
}
if (window.location.pathname === targetPath) return;
const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`;
window.history[replace ? "replaceState" : "pushState"](null, "", nextUrl);
}
function setPasswordLoginMode(enabled, replace = false) {
const nextEnabled = Boolean(enabled);
authStore.update((s) => ({
...s,
passwordLoginMode: nextEnabled,
passwordLoginFallback: false,
authStatus: "",
authIsError: false,
}));
syncPasswordLoginPath(nextEnabled, replace);
}
async function loadData(options = {}) {
const preserveView = options?.preserveView === true;
const preservedSection = preserveView
? normalizeSection(options?.section || screen || activeTab)
: null;
const preservedAdminSection =
preserveView && preservedSection === "admin"
? normalizeAdminSection(
options?.adminSection || adminActiveSection || initialAdminSectionFromLocation()
)
: null;
const payload = await api(options?.fresh ? "/me?fresh=1" : "/me");
if (!payload.ok) throw new Error(payload.error || "load_failed");
data = payload;
billingStore.update((s) => ({
...s,
selectedPlan: null,
selectedTariffKey: "",
paymentStep: "tariff",
selectedMethod: payload.payment_methods?.[0]?.id || "",
}));
const currentQuery = currentSearchParams();
let section = preserveView
? preservedSection
: MOCK && currentQuery.get("screen")
? normalizeSection(currentQuery.get("screen"))
: sectionFromPath(routePathnameFromLocation(), routePrefix);
if (section === "admin" && !payload.user?.is_admin) section = "settings";
if (section === "devices" && !payload.settings?.my_devices_enabled) section = "home";
if (section === "support" && payload.settings?.support_tickets_enabled === false) {
section = "home";
}
if (
section === "install" &&
!(payload.settings?.subscription_guides_enabled && payload.subscription?.active)
) {
section = "home";
}
const initialAdminSection =
section === "admin" ? preservedAdminSection || initialAdminSectionFromLocation() : null;
if (section === "admin" && payload.user?.is_admin) {
try {
await ensureI18nScope("admin");
await ensureAdminBundle();
adminActiveSection = initialAdminSection || "stats";
} catch (_error) {
void _error;
section = "settings";
activeTab = "settings";
showToast(t("wa_unavailable"));
}
}
const initialSupportTicketId =
section === "support"
? supportTicketIdFromPath(routePathnameFromLocation(), routePrefix)
: null;
if (isDocsDemo) docsDemoParentRouteConsumed = true;
activeTab =
section === "admin"
? "settings"
: section === "install" || section === "trial"
? "home"
: section;
screen = section;
mode = "app";
if (payload.settings?.support_tickets_enabled !== false) {
if (typeof payload.support_unread_count !== "undefined") {
supportStore.hydrateUnread(payload.support_unread_count);
} else {
void supportStore.refreshUnread();
}
supportStore.startPolling({ includeList: false });
}
if (section === "support" && initialSupportTicketId) {
const targetPath = withRoutePrefix(`/support/${initialSupportTicketId}`, routePrefix);
if (window.location.protocol !== "file:" && window.location.pathname !== targetPath) {
window.history.replaceState(
null,
"",
`${targetPath}${window.location.search}${window.location.hash}`
);
}
cleanDocsDemoRouteQuery();
} else {
syncAppSectionPath(section, true, initialAdminSection);
}
if (section === "devices" && payload.settings?.my_devices_enabled) {
await devicesStore.loadDevices(true, true);
}
if (section === "install") {
await installGuidesStore.load(true);
}
if (section === "support") {
if (initialSupportTicketId)
await supportStore.openTicket(initialSupportTicketId, { skipPush: true });
else await supportStore.loadList();
supportStore.startPolling({ includeList: true });
}
if (topupModalOpen) await billingStore.loadTopupOptions(topupKind);
if (deviceTopupModalOpen) await billingStore.loadDeviceTopupOptions();
if (changeModalOpen) await billingStore.loadTariffChangeOptions();
const topupDeep = new URLSearchParams(window.location.search).get("topup");
if (topupDeep === "regular" || topupDeep === "premium") {
const plansList = payload.plans?.length ? payload.plans : [];
const tariffCatalogLocal = buildTariffCatalog(plansList);
const sub = payload.subscription || {};
const tariffModeLocal = plansList.some((plan) => plan?.tariff_key);
const hasTariffSub = Boolean(
tariffModeLocal &&
sub?.active &&
sub?.tariff_key &&
tariffCatalogLocal.some((t) => t.key === sub.tariff_key)
);
const canRegular =
hasTariffSub &&
(sub?.can_topup_regular_traffic ?? sub?.can_topup_traffic) &&
Number(sub?.traffic_limit_bytes || 0) > 0;
const canPremium =
hasTariffSub &&
(sub?.can_topup_premium_traffic ?? sub?.can_topup_traffic) &&
Number(sub?.premium_limit_bytes || 0) > 0;
if (topupDeep === "regular" && canRegular) {
billingStore.openTopupModal("regular", payload.payment_methods?.[0]?.id || "");
stripTopupQueryFromUrl();
} else if (topupDeep === "premium" && canPremium) {
billingStore.openTopupModal("premium", payload.payment_methods?.[0]?.id || "");
stripTopupQueryFromUrl();
}
}
return payload;
}
async function loadPublicInstall(shareToken) {
mode = "publicInstall";
screen = "install";
activeTab = "home";
publicInstallToken = shareToken;
publicInstallSubscription = {
install_share_token: shareToken,
share_url: typeof window !== "undefined" ? `${window.location.origin}/s/${shareToken}` : "",
};
const response = await installGuidesStore.loadPublic(shareToken, true);
publicInstallSubscription = response?.subscription || publicInstallSubscription;
}
function showLogin() {
mode = "login";
screen = "login";
activeTab = "home";
setPasswordLoginMode(isPasswordLoginPath(), true);
}
async function api(path, options = {}) {
return apiClient.api(path, options);
}
async function publicApi(path, payload = {}, options = {}) {
return apiClient.publicApi(path, payload, options);
}
function setToken(nextToken, nextCsrf = "") {
clearManualLogoutFlag();
token = nextToken || "";
csrfToken = nextCsrf || readCookie(CSRF_COOKIE_NAME) || "";
if (!MOCK) clearStoredToken();
}
function clearToken() {
token = "";
csrfToken = "";
clearStoredToken();
}
function markManualLogout() {
markManualLogoutInStorage(MANUAL_LOGOUT_FLAG_KEY);
}
function clearManualLogoutFlag() {
clearManualLogoutFlagInStorage(MANUAL_LOGOUT_FLAG_KEY);
}
function isManuallyLoggedOut() {
return readManualLogoutFlag(MANUAL_LOGOUT_FLAG_KEY);
}
function submitEmailOnEnter(event) {
if (event.key !== "Enter") return;
event.preventDefault();
authStore.requestEmailCode((s) => (screen = s));
}
function openExternalLink(url) {
if (!url) return;
if (tg?.openLink) {
tg.openLink(url, { try_instant_view: false });
return;
}
window.location.assign(url);
}
function openAppLink(url) {
const raw = String(url || "").trim();
if (!raw || hasControlChars(raw) || /^(javascript|data|vbscript):/i.test(raw)) {
return;
}
if (isHttpUrl(raw)) {
openExternalLink(raw);
return;
}
const isTelegramMiniApp = hasTelegramLaunchParams();
const currentTg = tg || telegramSdk.refresh();
const gatewayUrl = isTelegramMiniApp ? buildExternalAppLaunchUrl(raw, null, currentLang) : "";
if (gatewayUrl) {
if (currentTg?.openLink) {
try {
tg = currentTg;
currentTg.openLink(gatewayUrl);
return;
} catch {
// Fall back to regular browser navigation below.
}
}
window.location.assign(gatewayUrl);
return;
}
if (/^tg:\/\//i.test(raw) && currentTg?.openTelegramLink) {
try {
tg = currentTg;
currentTg.openTelegramLink(raw);
return;
} catch {
// Fall back to the generic deeplink path below.
}
}
openUrlWithHiddenAnchor(raw);
}
function openConnectLink() {
const url = subscription?.connect_url || subscription?.config_link;
if (!url) {
showToast(t("wa_connect_link_unavailable"));
return;
}
openExternalLink(url);
}
function openPublicConnectLink() {
const url = publicInstallSubscription?.connect_url || publicInstallSubscription?.config_link;
if (!url) {
showToast(t("wa_connect_link_unavailable"));
return;
}
openExternalLink(url);
}
function openInstallOrConnect() {
if (canUseInstallGuides()) {
goInstall();
return;
}
openConnectLink();
}
function openTrialInstallOrConnect() {
if (canUseInstallGuides()) {
goInstall();
return;
}
const url = trialActivationResult?.connect_url || trialActivationResult?.config_link;
if (url) {
openExternalLink(url);
return;
}
openConnectLink();
}
function openActivationConnectLink() {
const url =
subscription?.connect_url ||
subscription?.config_link ||
trialActivationResult?.connect_url ||
trialActivationResult?.config_link;
if (!url) {
showToast(t("wa_connect_link_unavailable"));
return;
}
openExternalLink(url);
}
function navigateToActivationTarget({ replace = true } = {}) {
const useInstallGuides = canUseInstallGuides();
activationSuccessUseInstallGuides = useInstallGuides;
billingStore.closePaymentModal();
activeTab = "home";
if (useInstallGuides) {
screen = "install";
syncAppSectionPath("install", replace);
installGuidesStore.load(true);
return;
}
screen = "home";
syncAppSectionPath("home", replace);
}
async function handleSubscriptionActivated(context = {}) {
await tick();
if (!subscription?.active) return;
await maybeShowActivationSuccessDialog({ ...context, force: true, source: "payment" });
}
function closeActivationSuccessDialog() {
const shouldOpenConnect = !activationSuccessUseInstallGuides;
activationSuccessDialogOpen = false;
if (activationSuccessUseInstallGuides) {
navigateToActivationTarget({ replace: true });
return;
}
if (shouldOpenConnect) openActivationConnectLink();
}
async function copyText(value, success = t("wa_copied")) {
if (!value) {
showToast(t("wa_unavailable"));
return;
}
try {
await navigator.clipboard.writeText(value);
} catch {
const area = document.createElement("textarea");
area.value = value;
document.body.appendChild(area);
area.select();
document.execCommand("copy");
area.remove();
}
showToast(success);
}
async function applyPromo() {
const code = promoCode.trim();
if (!code) {
promoFieldError = t("wa_promo_enter");
return;
}
promoFieldError = "";
promoBusy = true;
promoStatus = "";
try {
const response = await api("/promo/apply", {
method: "POST",
body: JSON.stringify({ code }),
});
if (!response.ok) throw response;
promoCode = "";
promoStatus = response.end_date_text
? t("wa_promo_activated_until", { date: response.end_date_text })
: t("wa_promo_activated");
promoIsError = false;
await loadData({ fresh: true });
} catch (error) {
promoStatus = error?.message || t("wa_promo_activation_failed");
promoIsError = true;
promoFieldError = promoStatus;
} finally {
promoBusy = false;
}
}
async function activateTrial() {
if (trialBusy) return;
trialBusy = true;
trialActivationResult = null;
trialActivationError = "";
try {
const response = await api("/trial/activate", {
method: "POST",
body: JSON.stringify({}),
});
if (!response.ok) throw response;
trialActivationResult = response;
showToast(t("wa_trial_activated"));
await loadData({ fresh: true });
await maybeShowActivationSuccessDialog({ source: "trial", force: true });
} catch (error) {
const message = error?.message || t("wa_trial_activation_failed");
trialActivationError = message;
showToast(message);
} finally {
trialBusy = false;
}
}
function showToast(message) {
toastText = message;
if (toastTimer) window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(() => {
toastText = "";
}, 2400);
}
function goHome() {
billingStore.closePaymentModal();
activeTab = "home";
screen = "home";
syncAppSectionPath("home");
}
function goInstall() {
if (!canUseInstallGuides()) {
openConnectLink();
return;
}
billingStore.closePaymentModal();
activeTab = "home";
screen = "install";
syncAppSectionPath("install");
installGuidesStore.load(true);
}
function goInvite() {
billingStore.closePaymentModal();
activeTab = "invite";
screen = "invite";
syncAppSectionPath("invite");
}
function goDevices() {
if (!devicesEnabled) return;
billingStore.closePaymentModal();
activeTab = "devices";
screen = "devices";
syncAppSectionPath("devices");
devicesStore.loadDevices(devicesEnabled);
}
function goSupport() {
if (!supportEnabled) return;
billingStore.closePaymentModal();
activeTab = "support";
screen = "support";
syncAppSectionPath("support");
supportStore.loadList();
supportStore.startPolling({ includeList: true });
}
function defaultPaymentMethod() {
return methods[0]?.id || "";
}
function openPaymentModal() {
billingStore.openPaymentModal(
tariffMode,
singleTariffMode,
tariffCatalog,
subscription,
plans,
defaultPaymentMethod()
);
}
function openTopupModal(kind) {
billingStore.openTopupModal(kind, defaultPaymentMethod());
}
function openRegularTopupModal() {
openTopupModal("regular");
}
function openPremiumTopupModal() {
openTopupModal("premium");
}
function openTariffChangeModal() {
billingStore.openTariffChangeModal(defaultPaymentMethod());
}
function openDeviceTopupModal() {
billingStore.openDeviceTopupModal(defaultPaymentMethod());
}
function closeDeviceTopupModal() {
billingStore.closeDeviceTopupModal();
}
function loadDevices(force = false) {
return devicesStore.loadDevices(devicesEnabled, force);
}
function disconnectDevice() {
return devicesStore.disconnectDevice(devicesEnabled);
}
function goSettings() {
billingStore.closePaymentModal();
activeTab = "settings";
screen = "settings";
syncAppSectionPath("settings");
}
async function openAdminPanel() {
if (!isAdmin) return;
clearLanguageClickGuard();
billingStore.closePaymentModal();
const nextAdminSection = normalizeAdminSection(
adminActiveSection || adminSectionFromPath(routePathnameFromLocation(), routePrefix)
);
try {
await ensureI18nScope("admin");
await ensureAdminBundle();
} catch (_error) {
void _error;
showToast(t("wa_unavailable"));
return;
}
activeTab = "settings";
screen = "admin";
adminActiveSection = nextAdminSection;
syncAppSectionPath("admin", false, adminActiveSection);
}
function closeAdminPanel() {
screen = "settings";
activeTab = "settings";
syncAppSectionPath("settings");
}
function handleAdminSectionChange(adminSection, adminUserId = null) {
if (screen !== "admin") return;
const nextAdminSection = normalizeAdminSection(adminSection);
adminActiveSection = nextAdminSection;
if (window.location.protocol === "file:") return;
syncAppSectionPath("admin", false, nextAdminSection, adminUserId);
}
function adminPayloadHasLogoChange(options = {}) {
const keys = new Set([
...Object.keys(options.updates || {}),
...(Array.isArray(options.deletes) ? options.deletes : []),
]);
return [
"WEBAPP_LOGO_URL",
"WEBAPP_LOGO_USE_EMOJI",
"WEBAPP_LOGO_EMOJI",
"WEBAPP_LOGO_EMOJI_FONT",
"WEBAPP_FAVICON_URL",
"WEBAPP_FAVICON_USE_CUSTOM",
"WEBAPP_LOGO_FAVICON_URL",
].some((key) => keys.has(key));
}
async function handleAdminPersistedSaved(options = {}) {
invalidateWebappTariffOptionCaches(billingStore);
installGuidesStore.reset();
try {
await loadData();
} catch {
// Admin save already succeeded; a later full refresh will pick up new settings or catalog.
}
const shouldReloadFrontend =
options?.reloadFrontend === true ||
(!options?.deferFrontendReload && adminPayloadHasLogoChange(options));
if (shouldReloadFrontend && typeof window !== "undefined") {
window.location.reload();
}
}
async function refreshI18nScope(scope) {
if (MOCK) return;
const apiBase = String(CFG.apiBase || "/api").replace(/\/+$/, "");
try {
const response = await fetch(`${apiBase}/i18n?scope=${encodeURIComponent(scope)}`, {
credentials: "same-origin",
headers: { Accept: "application/json" },
});
if (!response.ok) return;
const payload = await response.json();
if (payload?.ok && payload.i18n) i18n.mergeMessages(payload.i18n);
if (scope === "admin") adminI18nLoaded = true;
} catch (_error) {
void _error;
}
}
async function handleAdminTranslationsSaved(options = {}) {
adminI18nLoaded = false;
await Promise.all([refreshI18nScope("webapp"), refreshI18nScope("admin")]);
await handleAdminPersistedSaved({ ...options, deferFrontendReload: true });
}
function selectTariff(tariff) {
billingStore.selectTariff(tariff, plans);
}
function continueWithSelectedTariff() {
billingStore.continueWithSelectedTariff(selectedTariffPlans);
}
function backToTariffList() {
billingStore.backToTariffList(subscription, tariffCatalog);
}
function primaryPayActionLabel() {
if (!subscription.active && appSettings?.trial_enabled && appSettings?.trial_available) {
return t("wa_pay_full_subscription", {}, "Оплатить полную подписку");
}
if (trafficMode || selectedPlan?.sale_mode === "traffic_package") return t("wa_buy_traffic");
return subscription.active
? t("wa_renew_subscription", {}, "Продлить подписку")
: t("wa_pay_subscription");
}
</script>
<svelte:head>
<title>{brandTitle}</title>
{#if shellThemeCssHref}
<link rel="stylesheet" href={shellThemeCssHref} data-theme-css={resolvedThemeKey} />
{/if}
</svelte:head>
<Tooltip.Provider>
{#key currentLang}
{#if isPreviewBoard}
<svelte:component this={previewBoardComponent} config={CFG} mockData={MOCK_SOURCE.data} />
{:else}
<div class="app-shell {shellToneClass} {shellThemeClass}" style={shellStyle}>
{#if mode === "loading"}
<div class="loader">
<BrandMark {brand} size="md" />
<div>{t("wa_loading")}</div>
</div>
{:else if mode === "appLaunch"}
<AppLaunchScreen
{brand}
{appLaunchTarget}
{refreshAppLaunchTarget}
{openAppLaunchTarget}
{t}
/>
{:else if mode === "publicInstall"}
<div class="public-install-shell">
<a class="public-install-brand" href="/" aria-label={brandTitle}>
<BrandMark {brand} />
<strong>{brandTitle}</strong>
</a>
<InstallGuideScreen
{currentLang}
telegramPlatform={tg?.platform || ""}
user={{}}
subscription={publicInstallSubscription || {
install_share_token: publicInstallToken,
}}
{goHome}
openConnectLink={openPublicConnectLink}
{openExternalLink}
{openAppLink}
{copyText}
{t}
publicMode
/>
</div>
{:else if mode === "login"}
<AuthScreen
{screen}
{CFG}
{brandTitle}
{brand}
bind:email={$authStore.email}
bind:emailPassword={$authStore.emailPassword}
bind:emailCode={$authStore.emailCode}
{pendingEmail}
{authStatus}
{authIsError}
{authBusy}
{authResendCooldown}
{loginEmailFieldError}
{loginEmailTooltipOpen}
{passwordLoginFallback}
{passwordLoginMode}
{telegramLoginBusy}
{telegramLoginUnavailable}
{telegramLoginChecking}
{telegramLoginLabel}
{telegramLoginUnavailableMessage}
{privacyPolicyUrl}
{userAgreementUrl}
{t}
requestEmailCode={() => authStore.requestEmailCode((s) => (screen = s))}
loginWithEmailPassword={authStore.loginWithEmailPassword}
verifyEmailCode={authStore.verifyEmailCode}
openTelegramLogin={openLoginTelegram}
{openExternalLink}
{submitEmailOnEnter}
onBackToLogin={() => (screen = "login")}
clearLoginEmailError={() => {
loginEmailFieldError = "";
loginEmailTooltipOpen = false;
}}
setPasswordLoginMode={(enabled) => setPasswordLoginMode(enabled)}
/>
{:else if screen === "admin" && isAdmin}
{#if adminBundleApi}
<div class="admin-mount" bind:this={adminMountTarget}></div>
{:else}
<div class="loader">
<BrandMark {brand} size="md" />
<div>{adminBundleError ? t("wa_unavailable") : t("wa_loading")}</div>
</div>
{/if}
{:else}
<WebAppShell
{screen}
{activeTab}
{brandTitle}
{brand}
{devicesEnabled}
{supportEnabled}
{supportUnreadCount}
{supportUnreadLoading}
{supportUnreadLoaded}
{hasUnlinkedIdentity}
{isAdmin}
{openAdminPanel}
{goDevices}
{goHome}
{goInvite}
{goSupport}
{goSettings}
{t}
>
{#if screen === "home"}
<HomeScreen
{appSettings}
{brand}
{brandTitle}
{canChangeTariff}
{currentTariffName}
{hasActiveTariffSubscription}
{hasMultipleTariffs}
{premiumTrafficTopupBarClickable}
{premiumTrafficTopupUnlocked}
{regularTrafficTopupBarClickable}
{regularTrafficTopupUnlocked}
{subscription}
{termUnitLabel}
{trafficMode}
{trialBusy}
{activateTrial}
openConnectLink={openInstallOrConnect}
{openPaymentModal}
{openRegularTopupModal}
{openPremiumTopupModal}
{openTariffChangeModal}
{primaryPayActionLabel}
{t}
/>
{:else if screen === "install"}
<InstallGuideScreen
{currentLang}
telegramPlatform={tg?.platform || ""}
{user}
{subscription}
{goHome}
{openConnectLink}
{openExternalLink}
{openAppLink}
{copyText}
{t}
/>
{:else if screen === "trial"}
<TrialActivationScreen
{appSettings}
{brand}
{brandTitle}
{subscription}
{trialBusy}
trialResult={trialActivationResult}
trialError={trialActivationError}
{activateTrial}
openInstallOrConnect={openTrialInstallOrConnect}
{goHome}
{t}
/>
{:else if screen === "invite"}
<InviteScreen
{referral}
{referralBonusDetails}
{referralOneBonusPerReferee}
{referralWelcomeBonusDays}
bind:promoCode
bind:promoFieldError
{promoBusy}
{promoIsError}
{promoStatus}
{applyPromo}
clearPromoFieldError={() => (promoFieldError = "")}
{copyText}
{t}
/>
{:else if screen === "devices"}
<DevicesScreen
{devicesBusy}
{devicesData}
{devicesIsError}
{devicesLoaded}
{devicesStatus}
{subscription}
{loadDevices}
openDeviceDisconnectDialog={devicesStore.openDeviceDisconnectDialog}
{openDeviceTopupModal}
{t}
/>
{:else if screen === "support"}
{#if $supportStore.openedTicketId}
<SupportTicketScreen
maxBodyLength={appSettings?.support_ticket_max_body_length || 4000}
{brand}
{user}
userAvatarUrl={profileAvatarUrl}
userInitials={telegramProfileName
? telegramProfileName.slice(0, 2).toUpperCase()
: "U"}
{t}
/>
{:else}
<SupportScreen
maxSubjectLength={appSettings?.support_ticket_max_subject_length || 160}
maxBodyLength={appSettings?.support_ticket_max_body_length || 4000}
{user}
{t}
/>
{/if}
{:else if screen === "settings"}
<SettingsScreen
{currentLang}
{currentLanguageOption}
{emailLinkStatus}
{isAdmin}
{languageBusy}
{languageClickGuard}
{languageClickGuardArmed}
bind:languageMenuOpen
{languageOptions}
{linkEmailBusy}
{linkTelegramBusy}
{privacyPolicyUrl}
{profileAvatarUrl}
{profileEmail}
{profileTelegramId}
{supportUrl}
{telegramProfileName}
{user}
{userAgreementUrl}
{userLanguage}
showLogout={!telegramMiniAppContext}
linkTelegramAccount={linkTelegramFromSettings}
logout={accountStore.logout}
{openAdminPanel}
{openExternalLink}
openLinkEmailDialog={openSettingsLinkEmailDialog}
openSetPasswordDialog={accountStore.openSetPasswordDialog}
{setLanguageMenuOpen}
{t}
updateAccountLanguage={accountStore.updateAccountLanguage}
/>
{/if}
</WebAppShell>
<PaymentDialogs
bind:linkEmailCode={$accountStore.linkEmailCode}
bind:linkEmailFieldError={$accountStore.linkEmailFieldError}
bind:linkEmailValue={$accountStore.linkEmailValue}
bind:paymentModalOpen={$billingStore.paymentModalOpen}
bind:paymentStep={$billingStore.paymentStep}
bind:selectedMethod={$billingStore.selectedMethod}
bind:selectedPlan={$billingStore.selectedPlan}
bind:selectedTariffKey={$billingStore.selectedTariffKey}
bind:setPasswordCode={$accountStore.setPasswordCode}
bind:setPasswordConfirm={$accountStore.setPasswordConfirm}
bind:setPasswordValue={$accountStore.setPasswordValue}
setPasswordEmail={user?.email || ""}
createPayment={billingStore.createPayment}
{deviceConfirmOpen}
{deviceDisconnectBusy}
{deviceToDisconnect}
{disconnectDevice}
{linkEmailBusy}
{linkEmailIsError}
{linkEmailOpen}
{linkEmailPending}
{linkEmailResendCooldown}
{linkEmailStatus}
{setPasswordBusy}
{setPasswordIsError}
{setPasswordOpen}
{setPasswordPending}
{setPasswordResendCooldown}
{setPasswordStatus}
{hasMultipleTariffs}
{methods}
{payBusy}
{plans}
{selectedTariff}
{selectedTariffPlans}
{singleTariffMode}
{subscription}
{subscriptionPurchaseDescription}
{tariffCatalog}
{tariffMode}
closeDeviceDisconnectDialog={devicesStore.closeDeviceDisconnectDialog}
closeLinkEmailDialog={accountStore.closeLinkEmailDialog}
closePaymentModal={billingStore.closePaymentModal}
closeSetPasswordDialog={accountStore.closeSetPasswordDialog}
{backToTariffList}
{continueWithSelectedTariff}
requestLinkEmailCode={accountStore.requestLinkEmailCode}
requestSetPasswordCode={accountStore.requestSetPasswordCode}
{selectTariff}
{t}
{termUnitLabel}
verifyLinkEmailCode={accountStore.verifyLinkEmailCode}
confirmSetPassword={accountStore.confirmSetPassword}
/>
<TariffDialogs
bind:changeConfirmOpen={$billingStore.changeConfirmOpen}
bind:changeModalOpen={$billingStore.changeModalOpen}
bind:deviceTopupModalOpen={$billingStore.deviceTopupModalOpen}
bind:selectedChangeAction={$billingStore.selectedChangeAction}
bind:selectedChangeTarget={$billingStore.selectedChangeTarget}
bind:selectedDeviceTopupPlan={$billingStore.selectedDeviceTopupPlan}
bind:selectedMethod={$billingStore.selectedMethod}
bind:selectedTopupPlan={$billingStore.selectedTopupPlan}
bind:topupModalOpen={$billingStore.topupModalOpen}
applyTariffChange={billingStore.applyTariffChange}
{changeOptions}
{closeDeviceTopupModal}
closeTariffChangeConfirm={billingStore.closeTariffChangeConfirm}
closeTariffChangeModal={billingStore.closeTariffChangeModal}
closeTopupModal={billingStore.closeTopupModal}
createDeviceTopupPayment={billingStore.createDeviceTopupPayment}
createTopupPayment={billingStore.createTopupPayment}
{deviceTopupOptions}
{methods}
openTariffChangeConfirm={billingStore.openTariffChangeConfirm}
{payBusy}
{singleTariffMode}
{subscription}
{tariffActionBusy}
{topupKind}
{topupOptions}
{trafficMode}
{t}
/>
<Dialog
open={activationSuccessDialogOpen}
title={t("wa_activation_success_title", {}, "Everything is successfully activated")}
description={activationSuccessUseInstallGuides
? t(
"wa_activation_success_install_hint",
{},
"Press OK and follow the setup instructions for your device."
)
: t(
"wa_activation_success_connect_hint",
{},
"Press OK and we will open the Remnawave subscription page for setup."
)}
closeLabel={t("wa_close")}
onclose={closeActivationSuccessDialog}
class="activation-success-dialog"
>
<CheckCircle2 slot="titleIcon" size={23} />
<div class="activation-success-dialog-body">
<Button class="wide" onclick={closeActivationSuccessDialog}>
{t("wa_ok", {}, "OK")}
</Button>
</div>
</Dialog>
{/if}
{#if toastText}
<div class="toast" role="status">{toastText}</div>
{/if}
</div>
{/if}
{/key}
</Tooltip.Provider>