feat: install instruction inside web app
This commit is contained in:
@@ -38,6 +38,7 @@ export {
|
||||
Menu,
|
||||
MessageSquare,
|
||||
MessageSquarePlus,
|
||||
Monitor,
|
||||
MousePointerClick,
|
||||
Paintbrush,
|
||||
Plus,
|
||||
@@ -50,6 +51,7 @@ export {
|
||||
Send,
|
||||
Server,
|
||||
Settings,
|
||||
Share2,
|
||||
Shield,
|
||||
Sliders,
|
||||
Smartphone,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user