feat: install instruction inside web app
This commit is contained in:
Generated
+300
-2
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"name": "remnawave-tg-shop",
|
||||
"name": "frontend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"qrcode": "^1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@internationalized/date": "^3.12.1",
|
||||
@@ -1847,6 +1850,30 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
@@ -1915,6 +1942,15 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
@@ -1928,6 +1964,17 @@
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -1938,6 +1985,24 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -1984,6 +2049,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -2028,6 +2102,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
|
||||
@@ -2490,6 +2576,15 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -2560,6 +2655,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@@ -3115,11 +3219,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -3155,6 +3267,15 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
@@ -3339,6 +3460,38 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
|
||||
@@ -3411,6 +3564,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -3444,6 +3603,32 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/style-to-object": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
|
||||
@@ -3748,6 +3933,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -3758,6 +3949,113 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -38,5 +38,8 @@
|
||||
"tailwindcss": "4.3.0",
|
||||
"uplot": "^1.6.32",
|
||||
"vite": "^8.0.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"qrcode": "^1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
+110
-4
@@ -3,6 +3,7 @@
|
||||
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";
|
||||
@@ -15,6 +16,7 @@
|
||||
import TariffDialogs from "./webapp/TariffDialogs.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";
|
||||
@@ -71,6 +73,7 @@
|
||||
adminSectionFromPath,
|
||||
adminUserIdFromPath,
|
||||
normalizeSection,
|
||||
publicInstallShortUuidFromPath,
|
||||
sectionFromPath,
|
||||
supportTicketIdFromPath,
|
||||
syncSectionPath,
|
||||
@@ -99,6 +102,8 @@
|
||||
let activeTab = "home";
|
||||
let screen = "home";
|
||||
let data = isPreviewBoard ? structuredCloneSafe(DEV_MOCK.data) : null;
|
||||
let publicInstallSubscription = null;
|
||||
let publicInstallShortUuid = "";
|
||||
let trialBusy = false;
|
||||
let promoCode = "";
|
||||
let promoBusy = false;
|
||||
@@ -182,6 +187,7 @@
|
||||
});
|
||||
const devicesStore = createDevicesStore({ api, t, showToast });
|
||||
const supportStore = createSupportStore({ api, t, showToast });
|
||||
const installGuidesStore = createInstallGuidesStore({ api, t, showToast });
|
||||
const accountStore = createAccountStore({
|
||||
api,
|
||||
publicApi,
|
||||
@@ -207,6 +213,7 @@
|
||||
setContext("billingStore", billingStore);
|
||||
setContext("devicesStore", devicesStore);
|
||||
setContext("supportStore", supportStore);
|
||||
setContext("installGuidesStore", installGuidesStore);
|
||||
setContext("accountStore", accountStore);
|
||||
|
||||
$: ({
|
||||
@@ -300,6 +307,7 @@
|
||||
: 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 || DEV_MOCK.data.subscription;
|
||||
$: hasActiveTariffSubscription = Boolean(
|
||||
@@ -476,12 +484,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
function canUseInstallGuides(settings = appSettings, sub = subscription) {
|
||||
const enabled =
|
||||
settings === appSettings ? installGuidesEnabled : Boolean(settings?.subscription_guides_enabled);
|
||||
return Boolean(enabled && sub?.active);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (isPreviewBoard) return;
|
||||
const onAnyPointerDown = () => {
|
||||
if (mode === "login") loginEmailTooltipOpen = false;
|
||||
};
|
||||
const onPopState = () => {
|
||||
const publicShortUuid = publicInstallShortUuidFromPath(window.location.pathname);
|
||||
if (publicShortUuid) {
|
||||
void loadPublicInstall(publicShortUuid);
|
||||
return;
|
||||
}
|
||||
if (mode === "publicInstall") {
|
||||
void boot();
|
||||
return;
|
||||
}
|
||||
const section = sectionFromPath(window.location.pathname);
|
||||
if (mode === "login") {
|
||||
setPasswordLoginMode(isPasswordLoginPath(), true);
|
||||
@@ -508,14 +531,17 @@
|
||||
? "home"
|
||||
: section === "support" && !supportEnabled
|
||||
? "home"
|
||||
: section;
|
||||
activeTab = nextSection;
|
||||
: section === "install" && !canUseInstallGuides()
|
||||
? "home"
|
||||
: section;
|
||||
activeTab = nextSection === "install" ? "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);
|
||||
@@ -741,6 +767,11 @@
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
const shareShortUuid = publicInstallShortUuidFromPath(window.location.pathname);
|
||||
if (!MOCK && shareShortUuid) {
|
||||
await loadPublicInstall(shareShortUuid);
|
||||
return;
|
||||
}
|
||||
await runWebappBoot({
|
||||
MOCK,
|
||||
setMode: (next) => {
|
||||
@@ -831,6 +862,12 @@
|
||||
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" ? adminSectionFromPath(window.location.pathname) : null;
|
||||
if (section === "admin" && payload.user?.is_admin) {
|
||||
@@ -846,7 +883,7 @@
|
||||
}
|
||||
const initialSupportTicketId =
|
||||
section === "support" ? supportTicketIdFromPath(window.location.pathname) : null;
|
||||
activeTab = section === "admin" ? "settings" : section;
|
||||
activeTab = section === "admin" ? "settings" : section === "install" ? "home" : section;
|
||||
screen = section;
|
||||
mode = "app";
|
||||
if (payload.settings?.support_tickets_enabled !== false) {
|
||||
@@ -872,6 +909,9 @@
|
||||
if (section === "devices" && payload.settings?.my_devices_enabled) {
|
||||
await devicesStore.loadDevices(true);
|
||||
}
|
||||
if (section === "install") {
|
||||
await installGuidesStore.load(true);
|
||||
}
|
||||
if (section === "support") {
|
||||
if (initialSupportTicketId)
|
||||
await supportStore.openTicket(initialSupportTicketId, { skipPush: true });
|
||||
@@ -912,6 +952,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPublicInstall(shortUuid) {
|
||||
mode = "publicInstall";
|
||||
screen = "install";
|
||||
activeTab = "home";
|
||||
publicInstallShortUuid = shortUuid;
|
||||
publicInstallSubscription = {
|
||||
panel_short_uuid: shortUuid,
|
||||
share_url:
|
||||
typeof window !== "undefined" ? `${window.location.origin}/install/share/${shortUuid}` : "",
|
||||
};
|
||||
const response = await installGuidesStore.loadPublic(shortUuid, true);
|
||||
publicInstallSubscription = response?.subscription || publicInstallSubscription;
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
mode = "login";
|
||||
screen = "login";
|
||||
@@ -976,6 +1030,14 @@
|
||||
openExternalLink(url);
|
||||
}
|
||||
|
||||
function openInstallOrConnect() {
|
||||
if (canUseInstallGuides()) {
|
||||
goInstall();
|
||||
return;
|
||||
}
|
||||
openConnectLink();
|
||||
}
|
||||
|
||||
async function copyText(value, success = t("wa_copied")) {
|
||||
if (!value) {
|
||||
showToast(t("wa_unavailable"));
|
||||
@@ -1057,6 +1119,18 @@
|
||||
syncSectionPath("home");
|
||||
}
|
||||
|
||||
function goInstall() {
|
||||
if (!canUseInstallGuides()) {
|
||||
openConnectLink();
|
||||
return;
|
||||
}
|
||||
billingStore.closePaymentModal();
|
||||
activeTab = "home";
|
||||
screen = "install";
|
||||
syncSectionPath("install");
|
||||
installGuidesStore.load(true);
|
||||
}
|
||||
|
||||
function goInvite() {
|
||||
billingStore.closePaymentModal();
|
||||
activeTab = "invite";
|
||||
@@ -1193,6 +1267,7 @@
|
||||
|
||||
async function handleAdminPersistedSaved(options = {}) {
|
||||
invalidateWebappTariffOptionCaches(billingStore);
|
||||
installGuidesStore.reset();
|
||||
try {
|
||||
await loadData();
|
||||
} catch {
|
||||
@@ -1242,6 +1317,25 @@
|
||||
<BrandMark {brand} size="md" />
|
||||
<div>{t("wa_loading")}</div>
|
||||
</div>
|
||||
{: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 || { panel_short_uuid: publicInstallShortUuid }}
|
||||
{goHome}
|
||||
{openConnectLink}
|
||||
{openExternalLink}
|
||||
{copyText}
|
||||
{t}
|
||||
publicMode
|
||||
/>
|
||||
</div>
|
||||
{:else if mode === "login"}
|
||||
<AuthScreen
|
||||
{screen}
|
||||
@@ -1330,7 +1424,7 @@
|
||||
{trafficMode}
|
||||
{trialBusy}
|
||||
{activateTrial}
|
||||
{openConnectLink}
|
||||
openConnectLink={openInstallOrConnect}
|
||||
{openPaymentModal}
|
||||
{openRegularTopupModal}
|
||||
{openPremiumTopupModal}
|
||||
@@ -1338,6 +1432,18 @@
|
||||
{primaryPayActionLabel}
|
||||
{t}
|
||||
/>
|
||||
{:else if screen === "install"}
|
||||
<InstallGuideScreen
|
||||
{currentLang}
|
||||
telegramPlatform={tg?.platform || ""}
|
||||
{user}
|
||||
{subscription}
|
||||
{goHome}
|
||||
{openConnectLink}
|
||||
{openExternalLink}
|
||||
{copyText}
|
||||
{t}
|
||||
/>
|
||||
{:else if screen === "invite"}
|
||||
<InviteScreen
|
||||
{referral}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { ChevronRight, Eye, EyeOff, Search, X } from "$components/ui/icons.js";
|
||||
import { ChevronRight, Eye, EyeOff, FileText, Search, X } from "$components/ui/icons.js";
|
||||
import * as UiIcons from "$components/ui/icons.js";
|
||||
import { Accordion, Switch } from "$components/ui/primitives.js";
|
||||
import Dialog from "$components/ui/dialog.svelte";
|
||||
@@ -123,6 +123,17 @@
|
||||
closeIconPicker();
|
||||
}
|
||||
|
||||
async function handleJsonFile(field, event) {
|
||||
const file = event?.currentTarget?.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const text = await file.text();
|
||||
settingsStore.markDirty(field.key, text);
|
||||
} finally {
|
||||
event.currentTarget.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function groupSectionFields(section) {
|
||||
const groups = new Map();
|
||||
for (const field of section.fields || []) {
|
||||
@@ -164,6 +175,7 @@
|
||||
notifications: "Уведомления",
|
||||
support: "Поддержка",
|
||||
devices: "Устройства",
|
||||
subscription_guides: "Connection guides",
|
||||
};
|
||||
return adminText(`settings_section_${id}`, {}, map[id] || id);
|
||||
}
|
||||
@@ -315,6 +327,41 @@
|
||||
value={valueFor(field) ?? ""}
|
||||
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
></textarea>
|
||||
{:else if field.type === "json"}
|
||||
<div class="admin-json-toolbar">
|
||||
<input
|
||||
id={"json-file-" + field.key}
|
||||
class="admin-json-file-input"
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
onchange={(event) => handleJsonFile(field, event)}
|
||||
/>
|
||||
<label
|
||||
class="admin-btn admin-btn-sm admin-btn-ghost admin-json-upload"
|
||||
for={"json-file-" + field.key}
|
||||
>
|
||||
<FileText size={13} />
|
||||
{at("settings_json_upload", {}, "Load .json")}
|
||||
</label>
|
||||
{#if valueFor(field)}
|
||||
<AdminButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onclick={() => settingsStore.markDirty(field.key, "")}
|
||||
>
|
||||
<X size={12} />
|
||||
{at("clear", {}, "Clear")}
|
||||
</AdminButton>
|
||||
{/if}
|
||||
</div>
|
||||
<textarea
|
||||
class="admin-setting-textarea admin-setting-json-textarea"
|
||||
rows="10"
|
||||
spellcheck="false"
|
||||
placeholder={fieldPlaceholderText(field)}
|
||||
value={valueFor(field) ?? ""}
|
||||
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
></textarea>
|
||||
{:else if field.secret}
|
||||
<input
|
||||
class="input"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -233,6 +233,31 @@
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
}
|
||||
|
||||
.admin-setting-control .admin-setting-json-textarea {
|
||||
min-height: 240px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
.admin-json-toolbar {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-json-file-input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-json-upload {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-btn.admin-btn-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
@@ -26,6 +26,33 @@ a {
|
||||
padding-bottom: 86px;
|
||||
}
|
||||
|
||||
.public-install-shell {
|
||||
width: min(100%, 440px);
|
||||
min-height: 100dvh;
|
||||
margin: 0 auto;
|
||||
overflow-x: hidden;
|
||||
padding: max(16px, env(safe-area-inset-top)) max(var(--screen-gutter), var(--safe-inline))
|
||||
max(18px, env(safe-area-inset-bottom)) max(var(--screen-gutter), var(--safe-inline));
|
||||
}
|
||||
|
||||
.public-install-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 16px;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.public-install-brand strong {
|
||||
color: var(--accent);
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: grid;
|
||||
min-height: 100dvh;
|
||||
@@ -2214,6 +2241,26 @@ a {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.public-install-shell {
|
||||
width: auto;
|
||||
max-width: none;
|
||||
min-height: 100dvh;
|
||||
margin: 0;
|
||||
overflow-x: visible;
|
||||
padding: max(28px, env(safe-area-inset-top)) var(--desktop-page-gutter) 40px;
|
||||
}
|
||||
|
||||
.public-install-shell > .public-install-brand,
|
||||
.public-install-shell > main {
|
||||
max-width: 1080px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.public-install-shell > .public-install-brand {
|
||||
width: min(100%, 1080px);
|
||||
}
|
||||
|
||||
/* Auth screen: keep it as a focused card in the centre, hide rail. */
|
||||
.phone-screen.auth-screen {
|
||||
width: min(100%, 460px);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
<div class="phone-screen" class:home-screen={screen === "home"}>
|
||||
{#if screen === "invite" || screen === "devices" || screen === "support" || screen === "settings"}
|
||||
{#if screen === "install" || screen === "invite" || screen === "devices" || screen === "support" || screen === "settings"}
|
||||
<header class="app-header accent-title">
|
||||
<div class="brand-row">
|
||||
<BrandMark {brand} />
|
||||
|
||||
@@ -0,0 +1,932 @@
|
||||
<script>
|
||||
import { getContext, onMount } from "svelte";
|
||||
import QRCode from "qrcode";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
Monitor,
|
||||
QrCode,
|
||||
Share2,
|
||||
Smartphone,
|
||||
} from "$components/ui/icons.js";
|
||||
import { AttentionDot } from "$components/ui/index.js";
|
||||
import { Select } from "$components/ui/primitives.js";
|
||||
import Button from "$components/ui/button.svelte";
|
||||
import Card from "$components/ui/card.svelte";
|
||||
|
||||
export let currentLang = "ru";
|
||||
export let telegramPlatform = "";
|
||||
export let user = {};
|
||||
export let subscription = {};
|
||||
export let goHome = () => {};
|
||||
export let openConnectLink = () => {};
|
||||
export let openExternalLink = () => {};
|
||||
export let copyText = async () => {};
|
||||
export let t = (key, _params = {}, fallback = "") => fallback || key;
|
||||
export let publicMode = false;
|
||||
|
||||
const installGuidesStore = getContext("installGuidesStore");
|
||||
const colorTokens = {
|
||||
amber: "#f59e0b",
|
||||
blue: "#3b82f6",
|
||||
cyan: "#06b6d4",
|
||||
emerald: "#10b981",
|
||||
fuchsia: "#d946ef",
|
||||
gray: "#6b7280",
|
||||
green: "#22c55e",
|
||||
indigo: "#6366f1",
|
||||
lime: "#84cc16",
|
||||
neutral: "#737373",
|
||||
orange: "#f97316",
|
||||
pink: "#ec4899",
|
||||
purple: "#a855f7",
|
||||
red: "#ef4444",
|
||||
rose: "#f43f5e",
|
||||
sky: "#0ea5e9",
|
||||
slate: "#64748b",
|
||||
stone: "#78716c",
|
||||
teal: "#14b8a6",
|
||||
violet: "#8b5cf6",
|
||||
yellow: "#eab308",
|
||||
zinc: "#71717a",
|
||||
};
|
||||
let selectedPlatformKey = "";
|
||||
let selectedAppIndex = 0;
|
||||
let qrDataUrl = "";
|
||||
let lastQrValue = "";
|
||||
let qrRequestId = 0;
|
||||
|
||||
onMount(() => {
|
||||
if (!publicMode) installGuidesStore?.load();
|
||||
});
|
||||
|
||||
$: guideState = $installGuidesStore;
|
||||
$: config = guideState?.config || null;
|
||||
$: platforms = Object.entries(config?.platforms || {})
|
||||
.filter(([, platform]) => Array.isArray(platform?.apps) && platform.apps.length)
|
||||
.map(([key, platform]) => ({ key, ...platform }));
|
||||
$: platformOptions = platforms.map((platform) => ({
|
||||
value: platform.key,
|
||||
label: localized(platform.displayName, platform.key),
|
||||
}));
|
||||
$: detectedPlatformKey = detectPlatformKey(platforms.map((platform) => platform.key));
|
||||
$: if (platforms.length && !selectedPlatformKey) {
|
||||
selectedPlatformKey = detectedPlatformKey || platforms[0].key;
|
||||
}
|
||||
$: if (selectedPlatformKey && platforms.length && !platforms.some((p) => p.key === selectedPlatformKey)) {
|
||||
selectedPlatformKey = platforms[0].key;
|
||||
}
|
||||
$: selectedPlatform =
|
||||
platforms.find((platform) => platform.key === selectedPlatformKey) || platforms[0] || null;
|
||||
$: selectedPlatformLabel = selectedPlatform
|
||||
? localized(selectedPlatform.displayName, selectedPlatform.key)
|
||||
: "";
|
||||
$: apps = selectedPlatform?.apps || [];
|
||||
$: if (selectedAppIndex >= apps.length) selectedAppIndex = 0;
|
||||
$: selectedApp = apps[selectedAppIndex] || apps[0] || null;
|
||||
$: guideSubscription = guideState?.subscription || subscription || {};
|
||||
$: finalSubscriptionLink =
|
||||
guideSubscription?.config_link || guideSubscription?.connect_url || subscription?.config_link || "";
|
||||
$: shareUrl = guideSubscription?.share_url || subscription?.install_share_url || "";
|
||||
$: if (finalSubscriptionLink !== lastQrValue) {
|
||||
lastQrValue = finalSubscriptionLink;
|
||||
updateQr(finalSubscriptionLink);
|
||||
}
|
||||
|
||||
function localized(value, fallback = "") {
|
||||
if (typeof value === "string") return value;
|
||||
if (!value || typeof value !== "object") return fallback;
|
||||
const lang = String(currentLang || "ru").split("-")[0].toLowerCase();
|
||||
return (
|
||||
value[lang] ||
|
||||
value.ru ||
|
||||
value.en ||
|
||||
Object.values(value).find((item) => typeof item === "string" && item.trim()) ||
|
||||
fallback
|
||||
);
|
||||
}
|
||||
|
||||
function iconSvg(key) {
|
||||
const iconKey = String(key || "").trim();
|
||||
return iconKey ? config?.svgLibrary?.[iconKey] || "" : "";
|
||||
}
|
||||
|
||||
function iconColorStyle(color) {
|
||||
const raw = String(color || "").trim();
|
||||
const value = colorTokens[raw] || raw;
|
||||
return value ? `--install-icon-color:${value};` : "";
|
||||
}
|
||||
|
||||
function selectPlatform(key) {
|
||||
selectedPlatformKey = key;
|
||||
selectedAppIndex = 0;
|
||||
}
|
||||
|
||||
function platformFallbackIcon(key) {
|
||||
return key === "ios" || key === "android" || key === "androidTV" ? Smartphone : Monitor;
|
||||
}
|
||||
|
||||
function detectPlatformKey(availableKeys) {
|
||||
const available = new Set(availableKeys || []);
|
||||
const tgPlatform = String(telegramPlatform || "").toLowerCase();
|
||||
const nav = typeof navigator === "undefined" ? {} : navigator;
|
||||
const userAgentDataPlatform = String(nav?.userAgentData?.platform || "").toLowerCase();
|
||||
const ua = String(nav?.userAgent || "").toLowerCase();
|
||||
const candidates = [];
|
||||
|
||||
if (tgPlatform.includes("ios")) candidates.push("ios");
|
||||
if (tgPlatform.includes("android")) candidates.push("android");
|
||||
if (tgPlatform.includes("mac")) candidates.push("macos");
|
||||
if (tgPlatform.includes("windows")) candidates.push("windows");
|
||||
if (tgPlatform.includes("linux")) candidates.push("linux");
|
||||
|
||||
if (userAgentDataPlatform.includes("android")) candidates.push("android");
|
||||
if (userAgentDataPlatform.includes("ios")) candidates.push("ios");
|
||||
if (userAgentDataPlatform.includes("mac")) candidates.push("macos");
|
||||
if (userAgentDataPlatform.includes("win")) candidates.push("windows");
|
||||
if (userAgentDataPlatform.includes("linux")) candidates.push("linux");
|
||||
|
||||
if (ua.includes("apple tv")) candidates.push("appleTV");
|
||||
if (ua.includes("android") && /\btv\b|aft|bravia|shield/i.test(ua)) candidates.push("androidTV");
|
||||
if (/iphone|ipad|ipod/.test(ua)) candidates.push("ios");
|
||||
if (ua.includes("android")) candidates.push("android");
|
||||
if (ua.includes("windows")) candidates.push("windows");
|
||||
if (ua.includes("macintosh") || ua.includes("mac os")) candidates.push("macos");
|
||||
if (ua.includes("linux") || ua.includes("x11")) candidates.push("linux");
|
||||
|
||||
return candidates.find((candidate) => available.has(candidate)) || "";
|
||||
}
|
||||
|
||||
function templateValues() {
|
||||
const subscriptionLink = subscription?.config_link || subscription?.connect_url || "";
|
||||
const username = user?.username || user?.first_name || user?.id || "";
|
||||
return {
|
||||
HAPP_CRYPT3_LINK: subscriptionLink,
|
||||
HAPP_CRYPT4_LINK: subscriptionLink,
|
||||
SUBSCRIPTION_LINK: subscriptionLink,
|
||||
USERNAME: username,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTemplate(value) {
|
||||
const replacements = templateValues();
|
||||
return String(value || "").replace(/\{\{\s*([A-Z0-9_]+)\s*\}\}/g, (_match, key) =>
|
||||
Object.prototype.hasOwnProperty.call(replacements, key) ? replacements[key] : ""
|
||||
);
|
||||
}
|
||||
|
||||
function isUnsafeUrl(value) {
|
||||
const url = String(value || "").trim().toLowerCase();
|
||||
return !url || hasControlChars(url) || /^(javascript|data|vbscript):/.test(url);
|
||||
}
|
||||
|
||||
function hasControlChars(value) {
|
||||
return Array.from(String(value || "")).some((char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
return code <= 31 || code === 127;
|
||||
});
|
||||
}
|
||||
|
||||
function openResolvedLink(url) {
|
||||
if (isUnsafeUrl(url)) {
|
||||
openConnectLink();
|
||||
return;
|
||||
}
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
openExternalLink(url);
|
||||
return;
|
||||
}
|
||||
window.location.assign(url);
|
||||
}
|
||||
|
||||
async function handleButton(button) {
|
||||
const value = resolveTemplate(button?.link);
|
||||
if (button?.type === "copyButton") {
|
||||
await copyText(
|
||||
value,
|
||||
localized(config?.baseTranslations?.linkCopiedToClipboard, t("wa_copied", {}, "Copied"))
|
||||
);
|
||||
return;
|
||||
}
|
||||
openResolvedLink(value);
|
||||
}
|
||||
|
||||
async function updateQr(value) {
|
||||
const link = String(value || "").trim();
|
||||
const requestId = ++qrRequestId;
|
||||
if (!link) {
|
||||
qrDataUrl = "";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = await QRCode.toDataURL(link, {
|
||||
errorCorrectionLevel: "M",
|
||||
margin: 1,
|
||||
width: 640,
|
||||
color: {
|
||||
dark: "#000000",
|
||||
light: "#00000000",
|
||||
},
|
||||
});
|
||||
if (requestId === qrRequestId) qrDataUrl = url;
|
||||
} catch (_error) {
|
||||
if (requestId === qrRequestId) qrDataUrl = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function copySubscriptionLink() {
|
||||
await copyText(finalSubscriptionLink, t("wa_install_link_copied", {}, "Link copied"));
|
||||
}
|
||||
|
||||
async function shareInstallGuide() {
|
||||
const url = shareUrl || (typeof window !== "undefined" ? window.location.href : "");
|
||||
if (!url) return;
|
||||
if (typeof navigator !== "undefined" && typeof navigator.share === "function") {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: localized(config?.baseTranslations?.installationGuideHeader, brandTitleFallback()),
|
||||
url,
|
||||
});
|
||||
return;
|
||||
} catch (_error) {
|
||||
void _error;
|
||||
}
|
||||
}
|
||||
await copyText(url, t("wa_install_share_copied", {}, "Share link copied"));
|
||||
}
|
||||
|
||||
function brandTitleFallback() {
|
||||
return t("wa_install_title", {}, "Install");
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="install-layout">
|
||||
<div class="install-topbar" class:public={publicMode}>
|
||||
{#if !publicMode}
|
||||
<Button
|
||||
class="install-back-btn"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onclick={goHome}
|
||||
aria-label={t("wa_back", {}, "Back")}
|
||||
>
|
||||
<ArrowLeft size={21} />
|
||||
</Button>
|
||||
{/if}
|
||||
<div>
|
||||
<h1>{localized(config?.baseTranslations?.installationGuideHeader, t("wa_install_title", {}, "Install"))}</h1>
|
||||
<p>{t("wa_install_subtitle", {}, "Choose your platform and app.")}</p>
|
||||
</div>
|
||||
{#if guideState?.enabled && config && platforms.length}
|
||||
<div class="install-platform-topbar">
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={selectedPlatformKey}
|
||||
items={platformOptions}
|
||||
onValueChange={selectPlatform}
|
||||
>
|
||||
<Select.Trigger
|
||||
class="install-platform-trigger"
|
||||
aria-label={t("wa_install_platform", {}, "Platform")}
|
||||
>
|
||||
<span class="install-platform-trigger-main">
|
||||
{#if selectedPlatform}
|
||||
{@const SelectedFallbackIcon = platformFallbackIcon(selectedPlatform.key)}
|
||||
{#if iconSvg(selectedPlatform.svgIconKey)}
|
||||
<span class="install-svg" aria-hidden="true">{@html iconSvg(selectedPlatform.svgIconKey)}</span>
|
||||
{:else}
|
||||
<svelte:component this={SelectedFallbackIcon} size={19} />
|
||||
{/if}
|
||||
{/if}
|
||||
<span>{selectedPlatformLabel}</span>
|
||||
</span>
|
||||
<ChevronsUpDown size={16} />
|
||||
</Select.Trigger>
|
||||
<Select.Content class="install-platform-content" side="bottom" align="start" sideOffset={6}>
|
||||
<Select.Viewport class="install-platform-viewport">
|
||||
{#each platforms as platform}
|
||||
{@const PlatformFallbackIcon = platformFallbackIcon(platform.key)}
|
||||
<Select.Item
|
||||
value={platform.key}
|
||||
label={localized(platform.displayName, platform.key)}
|
||||
class="install-platform-item"
|
||||
>
|
||||
<span class="install-platform-item-main">
|
||||
{#if iconSvg(platform.svgIconKey)}
|
||||
<span class="install-svg" aria-hidden="true">{@html iconSvg(platform.svgIconKey)}</span>
|
||||
{:else}
|
||||
<svelte:component this={PlatformFallbackIcon} size={18} />
|
||||
{/if}
|
||||
<span>{localized(platform.displayName, platform.key)}</span>
|
||||
</span>
|
||||
<Check size={15} class="install-platform-item-check" />
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if guideState?.loading && !guideState?.loaded}
|
||||
<Card class="install-empty">
|
||||
<p>{t("wa_install_loading", {}, "Loading instructions...")}</p>
|
||||
</Card>
|
||||
{:else if !guideState?.enabled || !config || !platforms.length}
|
||||
<Card class="install-empty">
|
||||
<p>{t("wa_install_unavailable", {}, "Instructions are unavailable.")}</p>
|
||||
<Button class="wide" onclick={openConnectLink}>
|
||||
<ExternalLink size={18} />
|
||||
{t("wa_install_and_configure")}
|
||||
</Button>
|
||||
</Card>
|
||||
{:else}
|
||||
|
||||
{#if apps.length > 1}
|
||||
<section class="install-selector-block" aria-label={t("wa_install_app", {}, "App")}>
|
||||
<div class="install-section-title">
|
||||
<span>{t("wa_install_app", {}, "App")}</span>
|
||||
</div>
|
||||
<div
|
||||
class="install-apps"
|
||||
class:apps-mobile-remainder-one={apps.length % 2 === 1}
|
||||
class:apps-remainder-one={apps.length % 3 === 1}
|
||||
class:apps-remainder-two={apps.length % 3 === 2}
|
||||
>
|
||||
{#each apps as app, index}
|
||||
<button
|
||||
class="install-app-button attention-wrap"
|
||||
class:active={selectedAppIndex === index}
|
||||
class:featured={app.featured}
|
||||
type="button"
|
||||
onclick={() => (selectedAppIndex = index)}
|
||||
>
|
||||
{#if app.featured}
|
||||
<AttentionDot class="install-feature-star" />
|
||||
{/if}
|
||||
{#if iconSvg(app.svgIconKey)}
|
||||
<span class="install-svg" aria-hidden="true">{@html iconSvg(app.svgIconKey)}</span>
|
||||
{/if}
|
||||
<span>{app.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if selectedApp}
|
||||
<section class="install-steps" aria-label={selectedApp.name}>
|
||||
{#each selectedApp.blocks as block}
|
||||
<Card class="install-step">
|
||||
<div class="install-step-icon" style={iconColorStyle(block.svgIconColor)} aria-hidden="true">
|
||||
{#if iconSvg(block.svgIconKey)}
|
||||
{@html iconSvg(block.svgIconKey)}
|
||||
{:else}
|
||||
<Check size={19} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="install-step-body">
|
||||
<h2>{localized(block.title)}</h2>
|
||||
<p>{localized(block.description)}</p>
|
||||
{#if block.buttons?.length}
|
||||
<div class="install-actions">
|
||||
{#each block.buttons as button}
|
||||
<Button
|
||||
variant={button.type === "copyButton" ? "secondary" : "default"}
|
||||
onclick={() => handleButton(button)}
|
||||
>
|
||||
{#if button.type === "copyButton"}
|
||||
<Copy size={16} />
|
||||
{:else}
|
||||
<ExternalLink size={16} />
|
||||
{/if}
|
||||
{localized(button.text)}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
{/each}
|
||||
</section>
|
||||
{#if finalSubscriptionLink && !publicMode}
|
||||
<div class="install-qr-divider" aria-hidden="true">
|
||||
<svg viewBox="0 0 240 18" preserveAspectRatio="none">
|
||||
<path
|
||||
d="M0 9 Q 4 2 8 9 T 16 9 T 24 9 T 32 9 T 40 9 T 48 9 T 56 9 T 64 9 T 72 9 T 80 9 T 88 9 T 96 9 T 104 9 T 112 9 T 120 9 T 128 9 T 136 9 T 144 9 T 152 9 T 160 9 T 168 9 T 176 9 T 184 9 T 192 9 T 200 9 T 208 9 T 216 9 T 224 9 T 232 9 T 240 9"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<Card class="install-subscription-card">
|
||||
<div class="install-subscription-header">
|
||||
<div class="install-subscription-header-icon" aria-hidden="true">
|
||||
<QrCode size={20} />
|
||||
</div>
|
||||
<div class="install-subscription-heading">
|
||||
<h2>{t("wa_install_subscription_link", {}, "Subscription link")}</h2>
|
||||
<p>{t("wa_install_subscription_link_hint", {}, "Scan the QR code or copy the link.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-subscription-body">
|
||||
{#if qrDataUrl}
|
||||
<div class="install-qr-wrap">
|
||||
<img src={qrDataUrl} alt={t("wa_install_qr_alt", {}, "Subscription QR code")} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="install-actions install-subscription-actions">
|
||||
<Button variant="secondary" onclick={copySubscriptionLink}>
|
||||
<Copy size={16} />
|
||||
{t("wa_install_copy_subscription_link", {}, "Copy link")}
|
||||
</Button>
|
||||
<Button onclick={shareInstallGuide}>
|
||||
<Share2 size={16} />
|
||||
{t("wa_install_share", {}, "Share")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.install-layout {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 18px 16px 96px;
|
||||
}
|
||||
|
||||
.install-topbar {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.install-topbar.public {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
:global(.install-back-btn) {
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0;
|
||||
border-color: var(--border-strong);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:global(.install-back-btn svg) {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
stroke-width: 2.6;
|
||||
}
|
||||
|
||||
.install-topbar h1 {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 22px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.install-platform-topbar {
|
||||
grid-column: 1 / -1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.install-topbar p,
|
||||
:global(.install-empty) p,
|
||||
.install-step-body p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
:global(.install-empty) {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.install-selector-block {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.install-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.install-apps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.install-apps.apps-mobile-remainder-one button:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.install-apps button {
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--panel) 88%, transparent);
|
||||
color: var(--text);
|
||||
padding: 10px;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.install-apps button.active {
|
||||
border-color: color-mix(in srgb, var(--accent) 70%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--panel));
|
||||
}
|
||||
|
||||
:global(.install-platform-trigger) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--panel) 88%, transparent);
|
||||
color: var(--text);
|
||||
padding: 0 12px;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
:global(.install-platform-trigger:focus-visible),
|
||||
:global(.install-platform-trigger[data-state="open"]) {
|
||||
outline: 0;
|
||||
border-color: color-mix(in srgb, var(--accent) 68%, var(--border));
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent);
|
||||
}
|
||||
|
||||
:global(.install-platform-trigger > svg) {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.install-platform-trigger-main,
|
||||
.install-platform-item-main {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.install-platform-trigger-main > span:last-child,
|
||||
.install-platform-item-main > span:last-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:global(.install-platform-content) {
|
||||
z-index: 140;
|
||||
width: min(300px, calc(100vw - 32px));
|
||||
min-width: min(300px, calc(100vw - 32px));
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-3);
|
||||
box-shadow: var(--shadow-popover);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
animation: dropdown-enter 0.16s ease-out both;
|
||||
}
|
||||
|
||||
:global(.install-platform-viewport) {
|
||||
max-height: min(290px, 48dvh);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
:global(.install-platform-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
border-radius: 6px;
|
||||
padding: 8px 9px;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.install-platform-item[data-highlighted]) {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
:global(.install-platform-item-check) {
|
||||
flex: 0 0 auto;
|
||||
color: var(--accent);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:global(.install-platform-item[data-selected] .install-platform-item-check) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.install-apps button {
|
||||
position: relative;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.install-apps button > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:global(.install-feature-star.attention-dot) {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 0;
|
||||
background: #facc15;
|
||||
clip-path: polygon(
|
||||
50% 0%,
|
||||
61% 35%,
|
||||
98% 35%,
|
||||
68% 56%,
|
||||
79% 91%,
|
||||
50% 70%,
|
||||
21% 91%,
|
||||
32% 56%,
|
||||
2% 35%,
|
||||
39% 35%
|
||||
);
|
||||
transform: none;
|
||||
animation: install-star-pulse 1.6s ease-out infinite;
|
||||
}
|
||||
|
||||
.install-svg,
|
||||
.install-step-icon {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
color: var(--install-icon-color, var(--accent));
|
||||
}
|
||||
|
||||
.install-svg :global(svg),
|
||||
.install-step-icon :global(svg) {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.install-steps {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:global(.install-step) {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.install-qr-divider {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 24px;
|
||||
color: var(--border-strong);
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.install-qr-divider svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.install-qr-divider path {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 1.2;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
:global(.install-subscription-card) {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.install-subscription-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 12px;
|
||||
padding: 2px 0 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.install-subscription-header-icon {
|
||||
display: inline-flex;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--border));
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
}
|
||||
|
||||
.install-subscription-header-icon :global(svg) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.install-subscription-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.install-subscription-heading h2 {
|
||||
margin: 0 0 4px;
|
||||
color: var(--text);
|
||||
font-size: 16px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.install-subscription-heading p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.install-subscription-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.install-qr-wrap {
|
||||
display: grid;
|
||||
width: 60%;
|
||||
aspect-ratio: 1;
|
||||
place-items: center;
|
||||
justify-self: center;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.install-qr-wrap img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
:global(.theme-dark) .install-qr-wrap img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.install-step-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid color-mix(in srgb, currentColor 38%, var(--border));
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
}
|
||||
|
||||
.install-step-body {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.install-step-body h2 {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 16px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.install-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.install-actions :global(.btn) {
|
||||
flex: 1 1 150px;
|
||||
}
|
||||
|
||||
.install-subscription-actions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-content: start;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.install-subscription-actions :global(.btn) {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 520px) {
|
||||
.install-apps {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.install-apps button,
|
||||
.install-apps.apps-mobile-remainder-one button:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.install-apps.apps-remainder-one button:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.install-apps.apps-remainder-two button:nth-last-child(-n + 2) {
|
||||
grid-column: span 3;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.install-topbar {
|
||||
grid-template-columns: auto minmax(0, 1fr) minmax(260px, 340px);
|
||||
}
|
||||
|
||||
.install-topbar.public {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
|
||||
}
|
||||
|
||||
.install-platform-topbar {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.install-platform-topbar :global(.install-platform-trigger) {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
:global(.install-subscription-card) {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
justify-self: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.install-subscription-header,
|
||||
.install-subscription-body {
|
||||
width: clamp(300px, 28vw, 340px);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.install-qr-wrap {
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes install-star-pulse {
|
||||
0% {
|
||||
filter: drop-shadow(0 0 0 rgba(250, 204, 21, 0.72));
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
65% {
|
||||
filter: drop-shadow(0 0 9px rgba(250, 204, 21, 0));
|
||||
transform: scale(1.18);
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: drop-shadow(0 0 0 rgba(250, 204, 21, 0));
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user