refactor: project architecture refactor, container splitting
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
import { writable, get } from "svelte/store";
|
||||
import { emailError, buildTelegramOAuthStartUrl } from "../authHelpers.js";
|
||||
|
||||
export function createAccountStore({
|
||||
api,
|
||||
publicApi,
|
||||
setToken,
|
||||
loadData,
|
||||
t,
|
||||
showToast,
|
||||
clearToken,
|
||||
markManualLogout,
|
||||
showLogin,
|
||||
telegramSdk,
|
||||
getTg,
|
||||
telegramOAuthClientId,
|
||||
currentLang,
|
||||
normalizeLangCode,
|
||||
updateLocalData,
|
||||
}) {
|
||||
const state = writable({
|
||||
linkEmailOpen: false,
|
||||
linkEmailBusy: false,
|
||||
linkTelegramBusy: false,
|
||||
linkEmailValue: "",
|
||||
linkEmailPending: "",
|
||||
linkEmailCode: "",
|
||||
linkEmailStatus: "",
|
||||
linkEmailIsError: false,
|
||||
linkEmailFieldError: "",
|
||||
linkEmailResendCooldown: 0,
|
||||
languageBusy: false,
|
||||
});
|
||||
|
||||
let linkEmailResendTimer = null;
|
||||
|
||||
function setLinkEmailStatus(message, isError = false) {
|
||||
state.update((s) => ({ ...s, linkEmailStatus: message, linkEmailIsError: isError }));
|
||||
}
|
||||
|
||||
function clearCooldownTimer() {
|
||||
if (linkEmailResendTimer) {
|
||||
window.clearInterval(linkEmailResendTimer);
|
||||
linkEmailResendTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startCooldownTimer(seconds = 60) {
|
||||
clearCooldownTimer();
|
||||
state.update((s) => ({ ...s, linkEmailResendCooldown: Math.max(0, Number(seconds || 60)) }));
|
||||
linkEmailResendTimer = window.setInterval(() => {
|
||||
const s = get(state);
|
||||
if (s.linkEmailResendCooldown <= 1) {
|
||||
state.update((s) => ({ ...s, linkEmailResendCooldown: 0 }));
|
||||
clearCooldownTimer();
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, linkEmailResendCooldown: s.linkEmailResendCooldown - 1 }));
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function openLinkEmailDialog(email) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
linkEmailOpen: true,
|
||||
linkEmailBusy: false,
|
||||
linkEmailCode: "",
|
||||
linkEmailPending: "",
|
||||
linkEmailStatus: "",
|
||||
linkEmailIsError: false,
|
||||
linkEmailFieldError: "",
|
||||
linkEmailValue: email || "",
|
||||
linkEmailResendCooldown: 0,
|
||||
}));
|
||||
clearCooldownTimer();
|
||||
}
|
||||
|
||||
function closeLinkEmailDialog() {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
linkEmailOpen: false,
|
||||
linkEmailBusy: false,
|
||||
linkEmailCode: "",
|
||||
linkEmailPending: "",
|
||||
linkEmailStatus: "",
|
||||
linkEmailIsError: false,
|
||||
linkEmailFieldError: "",
|
||||
linkEmailResendCooldown: 0,
|
||||
}));
|
||||
clearCooldownTimer();
|
||||
}
|
||||
|
||||
async function requestLinkEmailCode() {
|
||||
const s = get(state);
|
||||
if (s.linkEmailPending && s.linkEmailResendCooldown > 0) return;
|
||||
const normalized = String(s.linkEmailValue || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!normalized || !normalized.includes("@")) {
|
||||
state.update((s) => ({ ...s, linkEmailFieldError: t("wa_auth_invalid_email") }));
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, linkEmailFieldError: "", linkEmailBusy: true }));
|
||||
setLinkEmailStatus(t("wa_auth_sending_code"));
|
||||
try {
|
||||
const response = await api("/account/email/request", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: normalized }),
|
||||
});
|
||||
if (!response?.ok) throw response;
|
||||
state.update((s) => ({ ...s, linkEmailPending: normalized, linkEmailCode: "" }));
|
||||
setLinkEmailStatus("");
|
||||
startCooldownTimer(60);
|
||||
} catch (error) {
|
||||
setLinkEmailStatus(emailError(error, t("wa_auth_send_code_failed"), t), true);
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, linkEmailBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyLinkEmailCode() {
|
||||
const s = get(state);
|
||||
const code = String(s.linkEmailCode || "")
|
||||
.replace(/\\D/g, "")
|
||||
.slice(0, 6);
|
||||
if (!s.linkEmailPending) {
|
||||
setLinkEmailStatus(t("wa_auth_send_code_failed"), true);
|
||||
return;
|
||||
}
|
||||
if (code.length !== 6) {
|
||||
setLinkEmailStatus(t("wa_auth_enter_code_6digits"), true);
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, linkEmailBusy: true }));
|
||||
setLinkEmailStatus(t("wa_auth_checking_code"));
|
||||
try {
|
||||
const response = await api("/account/email/verify", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: s.linkEmailPending, code }),
|
||||
});
|
||||
if (!response?.ok) throw response;
|
||||
if (response?.csrf_token) setToken("", response.csrf_token);
|
||||
await loadData();
|
||||
closeLinkEmailDialog();
|
||||
showToast(t("wa_settings_linked"));
|
||||
} catch (error) {
|
||||
setLinkEmailStatus(emailError(error, t("wa_auth_invalid_code"), t), true);
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, linkEmailBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function linkTelegramAccountWithPayload(payload) {
|
||||
state.update((s) => ({ ...s, linkTelegramBusy: true }));
|
||||
try {
|
||||
const response = await api("/account/telegram/link", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response?.ok) throw response;
|
||||
if (response?.csrf_token) setToken("", response.csrf_token);
|
||||
await loadData();
|
||||
showToast(t("wa_settings_linked"));
|
||||
} catch (error) {
|
||||
showToast(error?.message || t("wa_auth_telegram_not_confirmed"));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, linkTelegramBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function linkTelegramAccount(getTelegramMiniAppInitData) {
|
||||
const s = get(state);
|
||||
if (s.linkTelegramBusy) return;
|
||||
const isTelegramMiniAppAttempt = telegramSdk.hasLaunchParams();
|
||||
if (isTelegramMiniAppAttempt) {
|
||||
await telegramSdk.ensureForAction();
|
||||
}
|
||||
const initData = getTelegramMiniAppInitData();
|
||||
if (initData) {
|
||||
await linkTelegramAccountWithPayload({ init_data: initData });
|
||||
return;
|
||||
}
|
||||
if (!telegramOAuthClientId) {
|
||||
showToast(t("wa_auth_telegram_not_configured"));
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, linkTelegramBusy: true }));
|
||||
window.location.assign(buildTelegramOAuthStartUrl("link", getTg()));
|
||||
}
|
||||
|
||||
async function updateAccountLanguage(nextValue) {
|
||||
const s = get(state);
|
||||
const normalize = typeof normalizeLangCode === "function" ? normalizeLangCode : (v) => v;
|
||||
const language = normalize(nextValue);
|
||||
if (!language || s.languageBusy || language === currentLang()) return;
|
||||
state.update((s) => ({ ...s, languageBusy: true }));
|
||||
try {
|
||||
const response = await api("/account/language", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ language }),
|
||||
});
|
||||
if (!response?.ok) throw response;
|
||||
if (typeof updateLocalData === "function") {
|
||||
updateLocalData(normalize(response.language || language));
|
||||
}
|
||||
await loadData();
|
||||
} catch {
|
||||
showToast(t("wa_settings_language_update_failed"));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, languageBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
markManualLogout();
|
||||
clearToken();
|
||||
try {
|
||||
await publicApi("/auth/logout", { keepalive: true });
|
||||
} catch (_error) {
|
||||
void _error;
|
||||
}
|
||||
showLogin();
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: state.subscribe,
|
||||
set: state.set,
|
||||
update: state.update,
|
||||
openLinkEmailDialog,
|
||||
closeLinkEmailDialog,
|
||||
requestLinkEmailCode,
|
||||
verifyLinkEmailCode,
|
||||
linkTelegramAccount,
|
||||
updateAccountLanguage,
|
||||
logout,
|
||||
clearLinkEmailResendTimer: clearCooldownTimer,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import { writable, get } from "svelte/store";
|
||||
import {
|
||||
readReferralParam,
|
||||
clearAuthQuery,
|
||||
buildTelegramOAuthStartUrl,
|
||||
emailError,
|
||||
} from "../authHelpers.js";
|
||||
|
||||
export function createAuthStore({
|
||||
publicApi,
|
||||
setToken,
|
||||
loadData,
|
||||
telegramSdk,
|
||||
getTg,
|
||||
t,
|
||||
currentLang,
|
||||
}) {
|
||||
const state = writable({
|
||||
authStatus: "",
|
||||
authIsError: false,
|
||||
authBusy: false,
|
||||
telegramLoginBusy: false,
|
||||
telegramLoginAttemptId: 0,
|
||||
loginEmailFieldError: "",
|
||||
loginEmailTooltipOpen: false,
|
||||
authResendCooldown: 0,
|
||||
email: "",
|
||||
pendingEmail: "",
|
||||
emailCode: "",
|
||||
});
|
||||
|
||||
let authResendTimer = null;
|
||||
let telegramLoginWatchdogTimer = null;
|
||||
|
||||
function setAuthStatus(message, isError = false) {
|
||||
state.update((s) => ({ ...s, authStatus: message, authIsError: isError }));
|
||||
}
|
||||
|
||||
function clearCooldownTimer() {
|
||||
if (authResendTimer) {
|
||||
window.clearInterval(authResendTimer);
|
||||
authResendTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startCooldownTimer(seconds = 60) {
|
||||
clearCooldownTimer();
|
||||
state.update((s) => ({ ...s, authResendCooldown: Math.max(0, Number(seconds || 60)) }));
|
||||
authResendTimer = window.setInterval(() => {
|
||||
const { authResendCooldown } = get(state);
|
||||
if (authResendCooldown <= 1) {
|
||||
state.update((s) => ({ ...s, authResendCooldown: 0 }));
|
||||
clearCooldownTimer();
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, authResendCooldown: authResendCooldown - 1 }));
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function startTelegramLoginWatchdog() {
|
||||
const TELEGRAM_MINI_APP_AUTH_TIMEOUT_MS = 6000;
|
||||
stopTelegramLoginWatchdog();
|
||||
state.update((s) => ({ ...s, telegramLoginAttemptId: s.telegramLoginAttemptId + 1 }));
|
||||
const { telegramLoginAttemptId } = get(state);
|
||||
|
||||
telegramLoginWatchdogTimer = window.setTimeout(() => {
|
||||
if (get(state).telegramLoginAttemptId !== telegramLoginAttemptId) return;
|
||||
telegramLoginWatchdogTimer = null;
|
||||
state.update((s) => ({ ...s, telegramLoginBusy: false, authBusy: false }));
|
||||
setAuthStatus(t("wa_auth_telegram_timeout"), true);
|
||||
}, TELEGRAM_MINI_APP_AUTH_TIMEOUT_MS);
|
||||
|
||||
return telegramLoginAttemptId;
|
||||
}
|
||||
|
||||
function stopTelegramLoginWatchdog(attemptId = null) {
|
||||
if (attemptId !== null && attemptId !== get(state).telegramLoginAttemptId) return;
|
||||
if (telegramLoginWatchdogTimer) {
|
||||
window.clearTimeout(telegramLoginWatchdogTimer);
|
||||
telegramLoginWatchdogTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function isActiveTelegramLoginAttempt(attemptId) {
|
||||
const s = get(state);
|
||||
return attemptId === s.telegramLoginAttemptId && s.telegramLoginBusy;
|
||||
}
|
||||
|
||||
async function finalizeMagicLogin(loginToken) {
|
||||
const s = get(state);
|
||||
if (s.authBusy) return false;
|
||||
state.update((s) => ({ ...s, authBusy: true }));
|
||||
setAuthStatus(t("wa_auth_checking_login"));
|
||||
try {
|
||||
const payload = { token: loginToken };
|
||||
const referralParam = readReferralParam(getTg());
|
||||
if (referralParam) payload.referral_code = referralParam;
|
||||
const response = await publicApi("/auth/email/magic", payload);
|
||||
if (response.ok && response.csrf_token) {
|
||||
setToken("", response.csrf_token);
|
||||
clearAuthQuery();
|
||||
await loadData();
|
||||
return true;
|
||||
}
|
||||
setAuthStatus(t("wa_auth_login_confirm_failed"), true);
|
||||
} catch {
|
||||
setAuthStatus(t("wa_auth_login_confirm_failed"), true);
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, authBusy: false }));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function finalizeTelegramAuth(authData, source = "auth_data", options = {}) {
|
||||
const s = get(state);
|
||||
if (s.authBusy) return false;
|
||||
state.update((s) => ({ ...s, authBusy: true }));
|
||||
setAuthStatus(t("wa_auth_checking_telegram"));
|
||||
try {
|
||||
const payload =
|
||||
source === "init_data"
|
||||
? { init_data: authData }
|
||||
: source === "id_token"
|
||||
? { id_token: authData.id_token, nonce: authData.nonce }
|
||||
: { auth_data: authData };
|
||||
const referralParam = readReferralParam(getTg());
|
||||
if (referralParam) payload.referral_code = referralParam;
|
||||
const response = await publicApi("/auth/token", payload, { signal: options.signal });
|
||||
if (response.ok && response.csrf_token) {
|
||||
setToken("", response.csrf_token);
|
||||
clearAuthQuery();
|
||||
setAuthStatus("");
|
||||
await loadData();
|
||||
return true;
|
||||
}
|
||||
setAuthStatus(
|
||||
response.error === "banned"
|
||||
? t("wa_auth_access_denied")
|
||||
: t("wa_auth_telegram_not_confirmed"),
|
||||
true
|
||||
);
|
||||
} catch (error) {
|
||||
setAuthStatus(
|
||||
error?.name === "AbortError"
|
||||
? t("wa_auth_telegram_timeout")
|
||||
: t("wa_auth_telegram_unavailable"),
|
||||
true
|
||||
);
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, authBusy: false }));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function requestEmailCode(changeScreen) {
|
||||
const s = get(state);
|
||||
if (s.authResendCooldown > 0 && s.pendingEmail) return;
|
||||
const normalized = s.email.trim().toLowerCase();
|
||||
if (!normalized || !normalized.includes("@")) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
loginEmailFieldError: t("wa_auth_invalid_email"),
|
||||
loginEmailTooltipOpen: true,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
loginEmailFieldError: "",
|
||||
loginEmailTooltipOpen: false,
|
||||
authBusy: true,
|
||||
}));
|
||||
setAuthStatus(t("wa_auth_sending_code"));
|
||||
try {
|
||||
const payload = { email: normalized, language: currentLang() };
|
||||
const referralParam = readReferralParam(getTg());
|
||||
if (referralParam) payload.referral_code = referralParam;
|
||||
const response = await publicApi("/auth/email/request", payload);
|
||||
if (!response.ok) throw response;
|
||||
state.update((s) => ({ ...s, pendingEmail: normalized, emailCode: "" }));
|
||||
changeScreen("code");
|
||||
setAuthStatus("");
|
||||
startCooldownTimer(60);
|
||||
} catch (error) {
|
||||
setAuthStatus(emailError(error, t("wa_auth_send_code_failed"), t), true);
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, authBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyEmailCode() {
|
||||
const s = get(state);
|
||||
const code = s.emailCode.replace(/\\D/g, "").slice(0, 6);
|
||||
if (code.length !== 6) {
|
||||
setAuthStatus(t("wa_auth_enter_code_6digits"), true);
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, authBusy: true }));
|
||||
setAuthStatus(t("wa_auth_checking_code"));
|
||||
try {
|
||||
const payload = { email: s.pendingEmail, code };
|
||||
const referralParam = readReferralParam(getTg());
|
||||
if (referralParam) payload.referral_code = referralParam;
|
||||
const response = await publicApi("/auth/email/verify", payload);
|
||||
if (!response.ok || !response.csrf_token) throw response;
|
||||
setToken("", response.csrf_token);
|
||||
await loadData();
|
||||
setAuthStatus("");
|
||||
} catch (error) {
|
||||
setAuthStatus(emailError(error, t("wa_auth_invalid_code"), t), true);
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, authBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function openTelegramLogin(telegramOAuthClientId, getTelegramMiniAppInitData) {
|
||||
const s = get(state);
|
||||
if (s.authBusy || s.telegramLoginBusy) return;
|
||||
setAuthStatus("");
|
||||
|
||||
const isTelegramMiniAppAttempt = telegramSdk.hasLaunchParams();
|
||||
if (!isTelegramMiniAppAttempt && telegramOAuthClientId) {
|
||||
state.update((s) => ({ ...s, telegramLoginBusy: true }));
|
||||
window.location.assign(buildTelegramOAuthStartUrl("login", getTg()));
|
||||
window.setTimeout(() => {
|
||||
state.update((s) => ({ ...s, telegramLoginBusy: false }));
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
state.update((s) => ({ ...s, telegramLoginBusy: true }));
|
||||
const attemptId = startTelegramLoginWatchdog();
|
||||
const loginTimeout = telegramSdk.createMiniAppAuthTimeout();
|
||||
try {
|
||||
await Promise.race([
|
||||
(async () => {
|
||||
await telegramSdk.ensureForAction();
|
||||
if (!isActiveTelegramLoginAttempt(attemptId)) return;
|
||||
const initData = getTelegramMiniAppInitData();
|
||||
if (initData) {
|
||||
await finalizeTelegramAuth(initData, "init_data", { signal: loginTimeout.signal });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!telegramOAuthClientId) {
|
||||
setAuthStatus(t("wa_auth_telegram_not_configured"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.assign(buildTelegramOAuthStartUrl("login", getTg()));
|
||||
})(),
|
||||
loginTimeout.promise,
|
||||
]);
|
||||
} catch (error) {
|
||||
if (!isActiveTelegramLoginAttempt(attemptId)) return;
|
||||
if (error?.name === "AbortError") {
|
||||
setAuthStatus(t("wa_auth_telegram_timeout"), true);
|
||||
} else {
|
||||
setAuthStatus(t("wa_auth_telegram_unavailable"), true);
|
||||
}
|
||||
} finally {
|
||||
loginTimeout.clear();
|
||||
if (loginTimeout.timedOut) {
|
||||
setAuthStatus(t("wa_auth_telegram_timeout"), true);
|
||||
state.update((s) => ({ ...s, authBusy: false }));
|
||||
}
|
||||
if (isActiveTelegramLoginAttempt(attemptId)) {
|
||||
stopTelegramLoginWatchdog(attemptId);
|
||||
state.update((s) => ({ ...s, telegramLoginBusy: false }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: state.subscribe,
|
||||
set: state.set,
|
||||
update: state.update,
|
||||
finalizeMagicLogin,
|
||||
finalizeTelegramAuth,
|
||||
requestEmailCode,
|
||||
verifyEmailCode,
|
||||
openTelegramLogin,
|
||||
clearCooldownTimer,
|
||||
stopTelegramLoginWatchdog,
|
||||
setAuthStatus,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
import { writable, get } from "svelte/store";
|
||||
|
||||
export function createBillingStore({ billing, loadData, t, showToast, openExternalLink, tg }) {
|
||||
const state = writable({
|
||||
paymentModalOpen: false,
|
||||
paymentStep: "tariff",
|
||||
selectedTariffKey: "",
|
||||
selectedPlan: null,
|
||||
selectedMethod: "",
|
||||
topupModalOpen: false,
|
||||
topupKind: "regular",
|
||||
deviceTopupModalOpen: false,
|
||||
changeModalOpen: false,
|
||||
topupOptions: null,
|
||||
deviceTopupOptions: null,
|
||||
changeOptions: null,
|
||||
selectedTopupPlan: null,
|
||||
selectedDeviceTopupPlan: null,
|
||||
selectedChangeTarget: null,
|
||||
selectedChangeAction: null,
|
||||
changeConfirmOpen: false,
|
||||
tariffActionBusy: false,
|
||||
payBusy: false,
|
||||
});
|
||||
|
||||
let topupOptionsRequestId = 0;
|
||||
|
||||
function openPaymentModal(
|
||||
tariffMode,
|
||||
singleTariffMode,
|
||||
tariffCatalog,
|
||||
subscription,
|
||||
plans,
|
||||
defaultMethod = ""
|
||||
) {
|
||||
state.update((s) => {
|
||||
let step;
|
||||
let plan = s.selectedPlan;
|
||||
let tariffKey = s.selectedTariffKey;
|
||||
|
||||
if (tariffMode) {
|
||||
if (singleTariffMode && tariffCatalog[0]?.key) {
|
||||
tariffKey = tariffCatalog[0].key;
|
||||
plan = plans.find((p) => p?.tariff_key === tariffKey) || null;
|
||||
step = "checkout";
|
||||
} else if (
|
||||
subscription?.active &&
|
||||
subscription?.tariff_key &&
|
||||
tariffCatalog.some((t) => t.key === subscription.tariff_key)
|
||||
) {
|
||||
tariffKey = subscription.tariff_key;
|
||||
plan = plans.find((p) => p?.tariff_key === tariffKey) || null;
|
||||
step = "checkout";
|
||||
} else {
|
||||
step = "tariff";
|
||||
tariffKey = "";
|
||||
plan = null;
|
||||
}
|
||||
} else {
|
||||
step = "checkout";
|
||||
}
|
||||
return {
|
||||
...s,
|
||||
paymentModalOpen: true,
|
||||
paymentStep: step,
|
||||
selectedTariffKey: tariffKey,
|
||||
selectedPlan: plan,
|
||||
selectedMethod: s.selectedMethod || defaultMethod,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function closePaymentModal() {
|
||||
state.update((s) => ({ ...s, paymentModalOpen: false }));
|
||||
}
|
||||
|
||||
function selectTariff(tariff, plans = []) {
|
||||
const key = String(tariff?.key || "").trim();
|
||||
if (!key) return;
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
selectedTariffKey: key,
|
||||
selectedPlan: plans.find((plan) => plan?.tariff_key === key) || null,
|
||||
}));
|
||||
}
|
||||
|
||||
function continueWithSelectedTariff(selectedTariffPlans = []) {
|
||||
state.update((s) => {
|
||||
if (!s.selectedTariffKey) return s;
|
||||
return {
|
||||
...s,
|
||||
selectedPlan: s.selectedPlan || selectedTariffPlans[0] || null,
|
||||
paymentStep: "checkout",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function backToTariffList(subscription, tariffCatalog = []) {
|
||||
if (
|
||||
subscription?.active &&
|
||||
subscription?.tariff_key &&
|
||||
tariffCatalog.some((t) => t.key === subscription.tariff_key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, paymentStep: "tariff" }));
|
||||
}
|
||||
|
||||
function openTopupModal(kind = "regular", defaultMethod = "") {
|
||||
const normalizedKind = kind === "premium" ? "premium" : "regular";
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
topupKind: normalizedKind,
|
||||
topupModalOpen: true,
|
||||
topupOptions: s.topupOptions?.topup_kind === normalizedKind ? s.topupOptions : null,
|
||||
selectedTopupPlan: s.topupOptions?.topup_kind === normalizedKind ? s.selectedTopupPlan : null,
|
||||
selectedMethod: s.selectedMethod || defaultMethod,
|
||||
}));
|
||||
loadTopupOptions(normalizedKind);
|
||||
}
|
||||
|
||||
function closeTopupModal() {
|
||||
state.update((s) => ({ ...s, topupModalOpen: false }));
|
||||
}
|
||||
|
||||
function openDeviceTopupModal(defaultMethod = "") {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
deviceTopupModalOpen: true,
|
||||
selectedMethod: s.selectedMethod || defaultMethod,
|
||||
}));
|
||||
loadDeviceTopupOptions();
|
||||
}
|
||||
|
||||
function closeDeviceTopupModal() {
|
||||
state.update((s) => ({ ...s, deviceTopupModalOpen: false }));
|
||||
}
|
||||
|
||||
function openTariffChangeModal(defaultMethod = "") {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
changeModalOpen: true,
|
||||
selectedMethod: s.selectedMethod || defaultMethod,
|
||||
}));
|
||||
loadTariffChangeOptions();
|
||||
}
|
||||
|
||||
function closeTariffChangeModal() {
|
||||
state.update((s) => ({ ...s, changeModalOpen: false }));
|
||||
}
|
||||
|
||||
function openTariffChangeConfirm() {
|
||||
const s = get(state);
|
||||
if (!s.selectedChangeTarget || !s.selectedChangeAction) return;
|
||||
state.update((s) => ({ ...s, changeConfirmOpen: true }));
|
||||
}
|
||||
|
||||
function closeTariffChangeConfirm() {
|
||||
state.update((s) => ({ ...s, changeConfirmOpen: false }));
|
||||
}
|
||||
|
||||
function openTelegramInvoice(url) {
|
||||
if (!url) return;
|
||||
if (tg?.openInvoice) {
|
||||
tg.openInvoice(url, (status) => {
|
||||
if (status === "paid") {
|
||||
showToast(t("wa_payment_success", {}, "Payment successful"));
|
||||
loadData();
|
||||
} else if (status === "failed") {
|
||||
showToast(t("wa_payment_create_failed"));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
openExternalLink(url);
|
||||
}
|
||||
|
||||
async function createPayment() {
|
||||
const s = get(state);
|
||||
if (!s.selectedPlan || !s.selectedMethod || s.payBusy) return;
|
||||
state.update((s) => ({ ...s, payBusy: true }));
|
||||
try {
|
||||
const response = await billing.postPayment(
|
||||
billing.planPaymentBody(s.selectedPlan, s.selectedMethod)
|
||||
);
|
||||
if (!response.ok) throw response;
|
||||
showToast(t("wa_payment_created"));
|
||||
if (response.action === "open_invoice") {
|
||||
if (!response.payment_url) throw response;
|
||||
openTelegramInvoice(response.payment_url);
|
||||
} else if (response.action === "invoice_sent") {
|
||||
state.update((s) => ({ ...s, paymentModalOpen: false }));
|
||||
return;
|
||||
} else {
|
||||
if (!response.payment_url) throw response;
|
||||
openExternalLink(response.payment_url);
|
||||
}
|
||||
state.update((s) => ({ ...s, paymentModalOpen: false }));
|
||||
} catch (error) {
|
||||
showToast(error?.message || t("wa_payment_create_failed"));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, payBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTopupOptions(kind) {
|
||||
const s = get(state);
|
||||
if (s.topupOptions?.topup_kind === kind) return;
|
||||
const requestId = ++topupOptionsRequestId;
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
tariffActionBusy: true,
|
||||
topupOptions: null,
|
||||
selectedTopupPlan: null,
|
||||
}));
|
||||
try {
|
||||
const response = await billing.fetchTopupOptions(kind);
|
||||
if (requestId !== topupOptionsRequestId || kind !== get(state).topupKind) return;
|
||||
if (!response?.ok) throw response;
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
topupOptions: response,
|
||||
selectedTopupPlan: response.plans?.[0] || null,
|
||||
}));
|
||||
} catch (error) {
|
||||
if (requestId !== topupOptionsRequestId || kind !== get(state).topupKind) return;
|
||||
showToast(error?.message || t("wa_tariff_options_failed"));
|
||||
state.update((s) => ({ ...s, topupModalOpen: false }));
|
||||
} finally {
|
||||
if (requestId === topupOptionsRequestId) {
|
||||
state.update((s) => ({ ...s, tariffActionBusy: false }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createTopupPayment() {
|
||||
const s = get(state);
|
||||
if (!s.selectedTopupPlan || !s.selectedMethod || s.payBusy) return;
|
||||
state.update((s) => ({ ...s, payBusy: true }));
|
||||
try {
|
||||
const response = await billing.postPayment(
|
||||
billing.topupPaymentBody(s.selectedTopupPlan, s.selectedMethod, s.topupOptions?.tariff_key)
|
||||
);
|
||||
if (!response.ok || !response.payment_url) throw response;
|
||||
showToast(t("wa_payment_created"));
|
||||
openExternalLink(response.payment_url);
|
||||
state.update((s) => ({ ...s, topupModalOpen: false }));
|
||||
} catch (error) {
|
||||
showToast(error?.message || t("wa_payment_create_failed"));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, payBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTariffChangeOptions() {
|
||||
const s = get(state);
|
||||
if (s.changeOptions || s.tariffActionBusy) return;
|
||||
state.update((s) => ({ ...s, tariffActionBusy: true }));
|
||||
try {
|
||||
const response = await billing.fetchTariffChangeOptions();
|
||||
if (!response?.ok) throw response;
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
changeOptions: response,
|
||||
selectedChangeTarget: response.targets?.[0] || null,
|
||||
selectedChangeAction: response.targets?.[0]?.actions?.[0] || null,
|
||||
}));
|
||||
} catch (error) {
|
||||
showToast(error?.message || t("wa_tariff_options_failed"));
|
||||
state.update((s) => ({ ...s, changeModalOpen: false }));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, tariffActionBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function applyTariffChange() {
|
||||
const s = get(state);
|
||||
if (!s.selectedChangeTarget || !s.selectedChangeAction || s.tariffActionBusy) return;
|
||||
if (s.selectedChangeAction.kind === "payment") {
|
||||
await createTariffChangePayment();
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, tariffActionBusy: true }));
|
||||
try {
|
||||
const response = await billing.postTariffChange({
|
||||
tariff_key: s.selectedChangeTarget.tariff_key,
|
||||
mode: s.selectedChangeAction.mode,
|
||||
});
|
||||
if (!response?.ok) throw response;
|
||||
showToast(t("wa_tariff_change_applied"));
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
changeConfirmOpen: false,
|
||||
changeModalOpen: false,
|
||||
changeOptions: null,
|
||||
}));
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
showToast(error?.message || t("wa_tariff_change_failed"));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, tariffActionBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function createTariffChangePayment() {
|
||||
const s = get(state);
|
||||
if (!s.selectedChangeTarget || !s.selectedChangeAction || !s.selectedMethod || s.payBusy)
|
||||
return;
|
||||
state.update((s) => ({ ...s, payBusy: true }));
|
||||
try {
|
||||
const body = billing.changePaymentBody(
|
||||
s.selectedChangeAction,
|
||||
s.selectedChangeTarget,
|
||||
s.selectedMethod
|
||||
);
|
||||
const response =
|
||||
s.selectedChangeAction.mode === "buy_package" ||
|
||||
s.selectedChangeAction.mode === "buy_period"
|
||||
? await billing.postPayment(body)
|
||||
: await billing.postTariffChangePayment(body);
|
||||
if (!response.ok || !response.payment_url) throw response;
|
||||
showToast(t("wa_payment_created"));
|
||||
openExternalLink(response.payment_url);
|
||||
state.update((s) => ({ ...s, changeConfirmOpen: false, changeModalOpen: false }));
|
||||
} catch (error) {
|
||||
showToast(error?.message || t("wa_payment_create_failed"));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, payBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDeviceTopupOptions() {
|
||||
const s = get(state);
|
||||
if (s.deviceTopupOptions || s.tariffActionBusy) return;
|
||||
state.update((s) => ({ ...s, tariffActionBusy: true }));
|
||||
try {
|
||||
const response = await billing.fetchDeviceTopupOptions();
|
||||
if (!response?.ok) throw response;
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
deviceTopupOptions: response,
|
||||
selectedDeviceTopupPlan: response.plans?.[0] || null,
|
||||
}));
|
||||
} catch (error) {
|
||||
showToast(error?.message || t("wa_device_topup_options_failed"));
|
||||
state.update((s) => ({ ...s, deviceTopupModalOpen: false }));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, tariffActionBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function createDeviceTopupPayment() {
|
||||
const s = get(state);
|
||||
if (!s.selectedDeviceTopupPlan || !s.selectedMethod || s.payBusy) return;
|
||||
state.update((s) => ({ ...s, payBusy: true }));
|
||||
try {
|
||||
const response = await billing.postPayment(
|
||||
billing.deviceTopupPaymentBody(
|
||||
s.selectedDeviceTopupPlan,
|
||||
s.selectedMethod,
|
||||
s.deviceTopupOptions?.tariff_key
|
||||
)
|
||||
);
|
||||
if (!response.ok || !response.payment_url) throw response;
|
||||
showToast(t("wa_payment_created"));
|
||||
openExternalLink(response.payment_url);
|
||||
state.update((s) => ({ ...s, deviceTopupModalOpen: false }));
|
||||
} catch (error) {
|
||||
showToast(error?.message || t("wa_payment_create_failed"));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, payBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: state.subscribe,
|
||||
set: state.set,
|
||||
update: state.update,
|
||||
openPaymentModal,
|
||||
closePaymentModal,
|
||||
selectTariff,
|
||||
continueWithSelectedTariff,
|
||||
backToTariffList,
|
||||
createPayment,
|
||||
openTopupModal,
|
||||
closeTopupModal,
|
||||
loadTopupOptions,
|
||||
createTopupPayment,
|
||||
openTariffChangeModal,
|
||||
closeTariffChangeModal,
|
||||
openTariffChangeConfirm,
|
||||
closeTariffChangeConfirm,
|
||||
loadTariffChangeOptions,
|
||||
applyTariffChange,
|
||||
createTariffChangePayment,
|
||||
openDeviceTopupModal,
|
||||
closeDeviceTopupModal,
|
||||
loadDeviceTopupOptions,
|
||||
createDeviceTopupPayment,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { writable, get } from "svelte/store";
|
||||
|
||||
export function createDevicesStore({ api, t, showToast }) {
|
||||
const state = writable({
|
||||
devicesData: null,
|
||||
devicesLoaded: false,
|
||||
devicesBusy: false,
|
||||
devicesStatus: "",
|
||||
devicesIsError: false,
|
||||
deviceConfirmOpen: false,
|
||||
deviceToDisconnect: null,
|
||||
deviceDisconnectBusy: false,
|
||||
});
|
||||
|
||||
async function loadDevices(devicesEnabled, force = false) {
|
||||
const s = get(state);
|
||||
if (!devicesEnabled || s.devicesBusy || (s.devicesLoaded && !force)) return;
|
||||
state.update((s) => ({ ...s, devicesBusy: true, devicesStatus: "", devicesIsError: false }));
|
||||
try {
|
||||
const response = await api("/devices");
|
||||
if (!response?.ok) throw response;
|
||||
state.update((s) => ({ ...s, devicesData: response, devicesLoaded: true }));
|
||||
} catch (error) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
devicesStatus: error?.message || t("wa_devices_load_failed"),
|
||||
devicesIsError: true,
|
||||
devicesLoaded: true,
|
||||
}));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, devicesBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
function openDeviceDisconnectDialog(device) {
|
||||
state.update((s) => ({ ...s, deviceToDisconnect: device, deviceConfirmOpen: true }));
|
||||
}
|
||||
|
||||
function closeDeviceDisconnectDialog() {
|
||||
const s = get(state);
|
||||
if (s.deviceDisconnectBusy) return;
|
||||
state.update((s) => ({ ...s, deviceConfirmOpen: false, deviceToDisconnect: null }));
|
||||
}
|
||||
|
||||
async function disconnectDevice(devicesEnabled) {
|
||||
const s = get(state);
|
||||
const token = String(s.deviceToDisconnect?.token || "").trim();
|
||||
if (!token || s.deviceDisconnectBusy) return;
|
||||
state.update((s) => ({ ...s, deviceDisconnectBusy: true }));
|
||||
try {
|
||||
const response = await api("/devices/disconnect", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
if (!response?.ok) throw response;
|
||||
showToast(t("wa_device_disconnected"));
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
deviceConfirmOpen: false,
|
||||
deviceToDisconnect: null,
|
||||
devicesLoaded: false,
|
||||
}));
|
||||
await loadDevices(devicesEnabled, true);
|
||||
} catch (error) {
|
||||
showToast(error?.message || t("wa_device_disconnect_failed"));
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, deviceDisconnectBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: state.subscribe,
|
||||
set: state.set,
|
||||
update: state.update,
|
||||
loadDevices,
|
||||
openDeviceDisconnectDialog,
|
||||
closeDeviceDisconnectDialog,
|
||||
disconnectDevice,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user