feat: install instruction inside web app

This commit is contained in:
3252a8
2026-05-22 13:58:34 +03:00
parent 8a38524774
commit 9e61a3d8a8
41 changed files with 8216 additions and 72 deletions
+2
View File
@@ -38,6 +38,7 @@ export {
Menu,
MessageSquare,
MessageSquarePlus,
Monitor,
MousePointerClick,
Paintbrush,
Plus,
@@ -50,6 +51,7 @@ export {
Send,
Server,
Settings,
Share2,
Shield,
Sliders,
Smartphone,
+1
View File
@@ -20,6 +20,7 @@ export const LANGUAGE_FLAGS = {
export const WEBAPP_LANGUAGE_ORDER = ["ru", "en"];
export const APP_SECTION_PATHS = {
home: "/home",
install: "/install",
invite: "/invite",
devices: "/devices",
support: "/support",
+7
View File
@@ -604,6 +604,13 @@ export async function mockApi(path, options = {}, context = {}) {
}
if (cleanPath === "/support/unread") return { ok: true, unread: 1 };
if (path === "/me") return clone(DEV_MOCK.data);
if (path === "/subscription-guides") return clone(DEV_MOCK.data.subscription_guides);
if (cleanPath.startsWith("/subscription-guides/public/")) {
return {
...clone(DEV_MOCK.data.subscription_guides),
subscription: clone(DEV_MOCK.data.subscription),
};
}
if (path === "/auth/email/request") return { ok: true };
if (path === "/auth/email/verify" || path === "/auth/email/magic") {
return { ok: true, csrf_token: "local-preview-csrf" };
+191
View File
@@ -23,6 +23,178 @@ const ASCII_THEME = {
},
};
const INSTALL_GUIDES_CONFIG = {
version: "1",
locales: ["ru", "en"],
brandingSettings: {
title: "/minishop",
logoUrl: "https://example.com/logo.svg",
supportUrl: "https://t.me/support",
},
uiConfig: {
subscriptionInfoBlockType: "collapsed",
installationGuidesBlockType: "cards",
},
baseSettings: {
metaTitle: "Subscription",
metaDescription: "Subscription",
showConnectionKeys: false,
hideGetLinkButton: false,
},
baseTranslations: Object.fromEntries(
[
"active",
"bandwidth",
"connectionKeysHeader",
"copyLink",
"expired",
"expires",
"expiresIn",
"getLink",
"inactive",
"indefinitely",
"installationGuideHeader",
"linkCopied",
"linkCopiedToClipboard",
"name",
"scanQrCode",
"scanQrCodeDescription",
"scanToImport",
"status",
"unknown",
].map((key) => [
key,
{
ru: key === "installationGuideHeader" ? "Установка и настройка" : key,
en: key === "installationGuideHeader" ? "Install and configure" : key,
},
])
),
svgLibrary: {
App: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="5" y="3" width="14" height="18" rx="3"/><path d="M9 7h6M9 17h6"/></svg>',
Copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="8" y="8" width="10" height="10" rx="2"/><path d="M6 16H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
Desktop: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg>',
Download: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/></svg>',
Phone: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="7" y="2" width="10" height="20" rx="2"/><path d="M11 18h2"/></svg>',
},
platforms: {
ios: {
displayName: "iOS",
svgIconKey: "Phone",
apps: [
{
name: "Streisand",
svgIconKey: "App",
featured: true,
blocks: [
{
svgIconKey: "Download",
svgIconColor: "green",
title: { ru: "Установите приложение", en: "Install the app" },
description: {
ru: "Откройте App Store и установите клиент.",
en: "Open the App Store and install the client.",
},
buttons: [
{
type: "external",
link: "https://apps.apple.com/app/streisand/id6450534064",
text: { ru: "Открыть App Store", en: "Open App Store" },
svgIconKey: "Download",
},
{
type: "subscriptionLink",
link: "streisand://import/{{SUBSCRIPTION_LINK}}",
text: { ru: "Импортировать", en: "Import" },
svgIconKey: "App",
},
{
type: "copyButton",
link: "{{SUBSCRIPTION_LINK}}",
text: { ru: "Скопировать ссылку", en: "Copy link" },
svgIconKey: "Copy",
},
],
},
],
},
],
},
android: {
displayName: "Android",
svgIconKey: "Phone",
apps: [
{
name: "Happ",
svgIconKey: "App",
featured: true,
blocks: [
{
svgIconKey: "Download",
svgIconColor: "emerald",
title: { ru: "Установите Happ", en: "Install Happ" },
description: {
ru: "Загрузите приложение и добавьте подписку по ссылке.",
en: "Install the app and add the subscription link.",
},
buttons: [
{
type: "external",
link: "https://play.google.com/store/apps/details?id=com.happproxy",
text: { ru: "Открыть Google Play", en: "Open Google Play" },
svgIconKey: "Download",
},
{
type: "copyButton",
link: "{{SUBSCRIPTION_LINK}}",
text: { ru: "Скопировать ссылку", en: "Copy link" },
svgIconKey: "Copy",
},
],
},
],
},
],
},
windows: {
displayName: "Windows",
svgIconKey: "Desktop",
apps: [
{
name: "Hiddify",
svgIconKey: "Desktop",
featured: true,
blocks: [
{
svgIconKey: "Download",
svgIconColor: "sky",
title: { ru: "Установите клиент", en: "Install the client" },
description: {
ru: "Скачайте приложение и импортируйте ссылку подписки.",
en: "Download the client and import the subscription link.",
},
buttons: [
{
type: "external",
link: "https://github.com/hiddify/hiddify-app/releases",
text: { ru: "Открыть релизы", en: "Open releases" },
svgIconKey: "Download",
},
{
type: "copyButton",
link: "{{SUBSCRIPTION_LINK}}",
text: { ru: "Скопировать ссылку", en: "Copy link" },
svgIconKey: "Copy",
},
],
},
],
},
],
},
},
};
export const DEV_MOCK = {
config: {
title: "/minishop",
@@ -103,6 +275,8 @@ export const DEV_MOCK = {
days_left: 25,
config_link: "https://sub.example.com/sub/preview-token",
connect_url: "https://sub.example.com/connect/preview-token",
panel_short_uuid: "preview-token",
install_share_url: "https://app.example.com/install/share/preview-token",
traffic_used: "18.4 GB",
traffic_limit: "100 GB",
traffic_used_bytes: 19756849561,
@@ -120,6 +294,12 @@ export const DEV_MOCK = {
can_topup_premium_traffic: true,
max_devices: 5,
},
subscription_guides: {
ok: true,
enabled: true,
config: INSTALL_GUIDES_CONFIG,
source: "mock",
},
devices: {
ok: true,
enabled: true,
@@ -227,6 +407,7 @@ export const DEV_MOCK = {
trial_traffic_strategy: "NO_RESET",
subscription_purchase_description:
"Покупая или продлевая подписку, вы получаете доступ к VPN/прокси-сервису, который помогает защищать ваше соединение и поддерживать стабильный доступ к сети.",
subscription_guides_enabled: true,
email_auth_enabled: true,
},
},
@@ -250,6 +431,16 @@ export function applyPreviewMock(kind) {
return;
}
if (mode === "guides" || mode === "install") {
DEV_MOCK.data.settings.subscription_guides_enabled = true;
DEV_MOCK.data.subscription_guides = {
...DEV_MOCK.data.subscription_guides,
enabled: true,
config: INSTALL_GUIDES_CONFIG,
};
return;
}
if (mode === "traffic") {
DEV_MOCK.data.settings.traffic_mode = true;
DEV_MOCK.data.settings.trial_available = false;
+7
View File
@@ -6,6 +6,7 @@ export function normalizeSection(value) {
.toLowerCase();
if (
section === "invite" ||
section === "install" ||
section === "devices" ||
section === "support" ||
section === "settings" ||
@@ -28,6 +29,12 @@ export function sectionFromPath(pathname) {
return normalizeSection(section);
}
export function publicInstallShortUuidFromPath(pathname) {
const normalized = String(pathname || "").trim().replace(/\/+$/, "");
const match = normalized.match(/^\/install\/share\/([A-Za-z0-9_-]{8,128})$/);
return match ? match[1] : "";
}
export function adminSectionFromPath(pathname) {
const normalized = String(pathname || "")
.toLowerCase()
@@ -0,0 +1,91 @@
import { writable } from "svelte/store";
export function createInstallGuidesStore({ api, t, showToast }) {
let inFlight = null;
const state = writable({
enabled: false,
config: null,
source: null,
subscription: null,
error: "",
loading: false,
loaded: false,
});
async function fetchGuides(path, force = false) {
if (inFlight?.path === path) return inFlight.promise;
let snapshot;
state.update((s) => {
snapshot = s;
return s;
});
if (!force && snapshot?.loaded) return snapshot;
const promise = (async () => {
state.update((s) => ({ ...s, loading: true, error: "" }));
try {
const response = await api(path);
const next = {
enabled: Boolean(response?.enabled),
config: response?.config || null,
source: response?.source || null,
subscription: response?.subscription || null,
error: response?.error || "",
loading: false,
loaded: true,
};
state.set(next);
return next;
} catch (error) {
const message =
error?.message || t("wa_install_unavailable", {}, "Instructions unavailable");
if (typeof showToast === "function") showToast(message);
const next = {
enabled: false,
config: null,
source: null,
subscription: null,
error: message,
loading: false,
loaded: true,
};
state.set(next);
return next;
} finally {
inFlight = null;
}
})();
inFlight = { path, promise };
return promise;
}
async function load(force = false) {
return fetchGuides("/subscription-guides", force);
}
async function loadPublic(shortUuid, force = false) {
const encoded = encodeURIComponent(String(shortUuid || ""));
return fetchGuides(`/subscription-guides/public/${encoded}`, force);
}
function reset() {
inFlight = null;
state.set({
enabled: false,
config: null,
source: null,
subscription: null,
error: "",
loading: false,
loaded: false,
});
}
return {
subscribe: state.subscribe,
set: state.set,
update: state.update,
load,
loadPublic,
reset,
};
}