2372 lines
80 KiB
Svelte
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>
|