feat(ui): add support and donation modal with CloudTips and crypto addresses

This commit is contained in:
Dushnilin
2026-09-12 22:37:01 +03:00
parent 914d5cdd2d
commit 9673ae101d
7 changed files with 708 additions and 1 deletions
+30
View File
@@ -6649,3 +6649,33 @@ msgstr "Разрешить только: "
msgid "Block: "
msgstr "Блокировать: "
msgid "Support Development"
msgstr "Поддержать разработку"
msgid "If Tachyon powers your daily networking and keeps your connection fast and secure, consider supporting ongoing development! ☕ 🧀 🌭"
msgstr "Если Tachyon помогает вам и делает работу в сети комфортной, вы можете поддержать проект и автора! ☕ 🧀 🌭"
msgid "Credit Cards / SBP / Tinkoff Pay"
msgstr "Карты РФ / СБП / Tinkoff Pay"
msgid "Quick donation via Russian bank cards or SBP"
msgstr "Быстрый перевод картами РФ или через СБП"
msgid "Support on CloudTips"
msgstr "Поддержать проект на CloudTips"
msgid "Cryptocurrency"
msgstr "Криптовалюта"
msgid "Copy Address"
msgstr "Скопировать"
msgid "Copied!"
msgstr "Скопировано!"
msgid "Address copied to clipboard"
msgstr "Адрес скопирован в буфер обмена"
msgid "Failed to copy address"
msgstr "Не удалось скопировать адрес"
+12
View File
@@ -288,3 +288,15 @@ export function renderXIcon24() {
svgEl('path', { d: 'm6 6 12 12' }),
]);
}
export function renderHeartIcon24() {
return createIcon(
'lucide lucide-heart-icon lucide-heart',
[
svgEl('path', {
d: 'M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z',
}),
],
{ width: '24', height: '24' },
);
}
@@ -0,0 +1,256 @@
import { renderButton } from '../../../../partials';
import {
renderCheckIcon24,
renderCopyIcon24,
renderLinkIcon24,
} from '../../../../icons';
import { showToast } from '../../../../helpers/showToast';
interface CryptoItem {
name: string;
badge: string;
address: string;
}
const CRYPTO_LIST: CryptoItem[] = [
{
name: 'Bitcoin',
badge: 'BTC',
address: 'bc1q9ehdv7y9g948jejyflkq7xmau3tytgunxgh35h',
},
{
name: 'Ethereum',
badge: 'ETH / ERC-20',
address: '0x6CB7a4547eD62EF64990D5C6B5D9fdA58EB223E6',
},
{
name: 'TON',
badge: 'TON',
address: 'UQDPhLRjMz5KltDLACAT3YXXHDVEtIDOHky2i33ZIOtsMEoR',
},
{
name: 'Solana',
badge: 'SOL',
address: '3csTGaNeU9KjhCKHEKAU3XLS5UfVjEeBVhXihpsETZwh',
},
];
const CLOUDTIPS_URL = 'https://pay.cloudtips.ru/p/48c57581';
async function copyTextToClipboard(text: string): Promise<boolean> {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
// Fallback to execCommand
}
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
ta.style.top = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
const successful = document.execCommand('copy');
document.body.removeChild(ta);
return successful;
} catch {
return false;
}
}
export function renderSupportModal(): void {
const introBlock = E(
'div',
{
style:
'margin-bottom: 16px; padding: 12px 14px; background: var(--background-color-secondary, rgba(0, 0, 0, 0.05)); border: 1px solid var(--border-color, rgba(128, 128, 128, 0.2)); border-radius: 6px; font-size: 13px; line-height: 1.5; color: var(--text-color-high, inherit);',
},
[
E(
'p',
{ style: 'margin: 0;' },
_(
'If Tachyon powers your daily networking and keeps your connection fast and secure, consider supporting ongoing development! ☕ 🧀 🌭',
),
),
],
);
// CloudTips card
const cloudTipsCard = E(
'div',
{
style:
'margin-bottom: 16px; padding: 14px; background: var(--background-color-secondary, rgba(0, 0, 0, 0.05)); border: 1px solid var(--border-color, rgba(128, 128, 128, 0.2)); border-radius: 6px;',
},
[
E(
'div',
{
style:
'display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;',
},
[
E(
'div',
{ style: 'display: flex; flex-direction: column; gap: 2px;' },
[
E('div', { style: 'font-weight: bold; font-size: 14px;' }, [
'💳 ',
_('Credit Cards / SBP / Tinkoff Pay'),
]),
E(
'div',
{
style:
'font-size: 12px; color: var(--text-color-medium, #888);',
},
_('Quick donation via Russian bank cards or SBP'),
),
],
),
renderButton({
classNames: ['cbi-button-action'],
icon: renderLinkIcon24,
text: _('Support on CloudTips'),
onClick: () => {
window.open(CLOUDTIPS_URL, '_blank', 'noopener,noreferrer');
},
}),
],
),
],
);
// Crypto list section
const cryptoItemsContainer = E('div', {
style: 'display: flex; flex-direction: column; gap: 10px;',
});
CRYPTO_LIST.forEach((item) => {
const addressCode = E(
'code',
{
style:
'font-family: monospace; font-size: 11px; word-break: break-all; flex: 1; padding: 6px 8px; background: var(--background-color-primary, rgba(0, 0, 0, 0.15)); border: 1px solid var(--border-color, rgba(128, 128, 128, 0.2)); border-radius: 4px; user-select: all; line-height: 1.4;',
},
item.address,
);
const copyBtn = renderButton({
classNames: ['cbi-button'],
icon: renderCopyIcon24,
text: _('Copy Address'),
onClick: async () => {
const ok = await copyTextToClipboard(item.address);
if (ok) {
copyBtn.replaceChildren(
renderCheckIcon24(),
document.createTextNode(' ' + _('Copied!')),
);
copyBtn.classList.add('cbi-button-apply');
setTimeout(() => {
copyBtn.replaceChildren(
renderCopyIcon24(),
document.createTextNode(' ' + _('Copy Address')),
);
copyBtn.classList.remove('cbi-button-apply');
}, 2000);
showToast(_('Address copied to clipboard'), 'success');
} else {
showToast(_('Failed to copy address'), 'error');
}
},
});
copyBtn.style.whiteSpace = 'nowrap';
copyBtn.style.flexShrink = '0';
const row = E(
'div',
{
style:
'padding: 10px 12px; background: var(--background-color-secondary, rgba(0, 0, 0, 0.05)); border: 1px solid var(--border-color, rgba(128, 128, 128, 0.2)); border-radius: 6px; display: flex; flex-direction: column; gap: 6px;',
},
[
E(
'div',
{
style: 'display: flex; align-items: center; gap: 8px;',
},
[
E(
'span',
{ style: 'font-weight: bold; font-size: 13px;' },
item.name,
),
E(
'span',
{
class: 'badge cbi-value-title',
style: 'font-size: 11px; padding: 2px 6px; opacity: 0.85;',
},
item.badge,
),
],
),
E(
'div',
{
style:
'display: flex; align-items: center; gap: 8px; flex-wrap: wrap;',
},
[addressCode, copyBtn],
),
],
);
cryptoItemsContainer.appendChild(row);
});
const cryptoSection = E('div', { style: 'margin-bottom: 16px;' }, [
E(
'div',
{
style:
'font-weight: bold; font-size: 14px; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;',
},
['🪙 ', _('Cryptocurrency')],
),
cryptoItemsContainer,
]);
// Modal footer with Close button
const footer = E(
'div',
{
style:
'display: flex; justify-content: flex-end; padding-top: 12px; border-top: 1px solid var(--border-color, rgba(128, 128, 128, 0.2));',
},
[
renderButton({
classNames: ['cbi-button'],
text: _('Close'),
onClick: () => {
if (ui.hideModal) ui.hideModal();
},
}),
],
);
const modalContent = E(
'div',
{
style:
'max-width: 640px; width: 100%; box-sizing: border-box; padding: 4px;',
},
[introBlock, cloudTipsCard, cryptoSection, footer],
);
ui.showModal(`💖 ${_('Support Development')}`, modalContent);
}
@@ -1,6 +1,11 @@
import { renderBookOpenTextIcon24, renderSendIcon24 } from '../../../../icons';
import {
renderBookOpenTextIcon24,
renderHeartIcon24,
renderSendIcon24,
} from '../../../../icons';
import { renderButton } from '../../../../partials';
import { insertIf } from '../../../../helpers';
import { renderSupportModal } from './renderSupportModal';
export function renderWikiDisclaimer(kind: 'default' | 'error' | 'warning') {
const iconWrap = E('span', {
@@ -47,5 +52,11 @@ export function renderWikiDisclaimer(kind: 'default' | 'error' | 'warning') {
'noopener,noreferrer',
),
}),
renderButton({
classNames: ['cbi-button-save'],
icon: renderHeartIcon24,
text: _('Support Development'),
onClick: () => renderSupportModal(),
}),
]);
}
@@ -0,0 +1,135 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => {
class MockMutationObserver {
observe() {}
disconnect() {}
takeRecords() {
return [];
}
}
(globalThis as any).MutationObserver = MockMutationObserver;
function createDummyElement(tag: string) {
return {
tagName: tag.toUpperCase(),
children: [] as any[],
style: {} as Record<string, string>,
textContent: '',
innerHTML: '',
setAttribute() {},
getAttribute() {
return null;
},
addEventListener() {},
querySelectorAll: function () {
return [];
},
querySelector: function () {
return null;
},
replaceChildren: function (...nodes: any[]) {
(this as any).children = nodes;
},
appendChild: function (node: any) {
(this as any).children.push(node);
},
append: function (...nodes: any[]) {
nodes.forEach((node) => (this as any).children.push(node));
},
classList: {
add() {},
remove() {},
},
};
}
(globalThis as any).document = {
body: createDummyElement('body') as any,
createElement: (tag: string) => createDummyElement(tag) as any,
createElementNS: (_ns: string, tag: string) =>
createDummyElement(tag) as any,
createTextNode: (text: string) => ({ textContent: text }),
getElementById: () => null,
querySelector: () => null,
querySelectorAll: () => [],
execCommand: vi.fn().mockReturnValue(true),
};
(globalThis as any).E = (tag: string, attrs?: any, children?: any) => {
const el = (globalThis as any).document.createElement(tag);
if (attrs) {
const { style, ...rest } = attrs;
Object.assign(el, rest);
if (style && typeof style === 'string') {
style.split(';').forEach((rule: string) => {
const [k, v] = rule.split(':').map((s: string) => s.trim());
if (k && v) {
el.style[k] = v;
}
});
} else if (style && typeof style === 'object') {
Object.assign(el.style, style);
}
}
if (children) {
if (Array.isArray(children)) {
children.forEach((c) => {
if (c) el.appendChild(c);
});
} else {
el.appendChild(children);
}
}
return el;
};
(globalThis as any)._ = (str: string) => str;
const showModal = vi.fn();
const hideModal = vi.fn();
(globalThis as any).ui = {
showModal,
hideModal,
};
return {
showModal,
hideModal,
};
});
import { renderSupportModal } from '../partials/renderSupportModal';
import { renderWikiDisclaimer } from '../partials/renderWikiDisclaimer';
describe('renderSupportModal & renderWikiDisclaimer', () => {
beforeEach(() => {
mocks.showModal.mockReset();
mocks.hideModal.mockReset();
});
it('renders Support Development button inside renderWikiDisclaimer', () => {
const disclaimer = renderWikiDisclaimer('default') as any;
expect(disclaimer).toBeDefined();
// disclaimer has content, Open Project Page btn, Telegram Channel btn, Support Development btn
const buttons = disclaimer.children.filter(
(c: any) =>
c.tagName === 'BUTTON' ||
(c.classList && c.classList.toString().includes('btn')),
);
expect(buttons.length).toBe(3);
});
it('opens support modal with title and content when renderSupportModal is called', () => {
renderSupportModal();
expect(mocks.showModal).toHaveBeenCalledTimes(1);
const title = mocks.showModal.mock.calls[0][0];
expect(title).toContain('Support Development');
const modalContent = mocks.showModal.mock.calls[0][1];
expect(modalContent).toBeDefined();
});
});
@@ -1742,6 +1742,17 @@ function renderXIcon24() {
svgEl("path", { d: "m6 6 12 12" })
]);
}
function renderHeartIcon24() {
return createIcon(
"lucide lucide-heart-icon lucide-heart",
[
svgEl("path", {
d: "M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"
})
],
{ width: "24", height: "24" }
);
}
// src/helpers/prettyBytes.ts
function prettyBytes(n) {
@@ -15311,6 +15322,222 @@ function normalizeCompiledVersion(version, commitSha) {
return version;
}
// src/tachyon/tabs/diagnostic/partials/renderSupportModal.ts
var CRYPTO_LIST = [
{
name: "Bitcoin",
badge: "BTC",
address: "bc1q9ehdv7y9g948jejyflkq7xmau3tytgunxgh35h"
},
{
name: "Ethereum",
badge: "ETH / ERC-20",
address: "0x6CB7a4547eD62EF64990D5C6B5D9fdA58EB223E6"
},
{
name: "TON",
badge: "TON",
address: "UQDPhLRjMz5KltDLACAT3YXXHDVEtIDOHky2i33ZIOtsMEoR"
},
{
name: "Solana",
badge: "SOL",
address: "3csTGaNeU9KjhCKHEKAU3XLS5UfVjEeBVhXihpsETZwh"
}
];
var CLOUDTIPS_URL = "https://pay.cloudtips.ru/p/48c57581";
async function copyTextToClipboard(text) {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
}
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
ta.style.top = "0";
document.body.appendChild(ta);
ta.focus();
ta.select();
const successful = document.execCommand("copy");
document.body.removeChild(ta);
return successful;
} catch {
return false;
}
}
function renderSupportModal() {
const introBlock = E(
"div",
{
style: "margin-bottom: 16px; padding: 12px 14px; background: var(--background-color-secondary, rgba(0, 0, 0, 0.05)); border: 1px solid var(--border-color, rgba(128, 128, 128, 0.2)); border-radius: 6px; font-size: 13px; line-height: 1.5; color: var(--text-color-high, inherit);"
},
[
E(
"p",
{ style: "margin: 0;" },
_(
"If Tachyon powers your daily networking and keeps your connection fast and secure, consider supporting ongoing development! ☕ 🧀 🌭"
)
)
]
);
const cloudTipsCard = E(
"div",
{
style: "margin-bottom: 16px; padding: 14px; background: var(--background-color-secondary, rgba(0, 0, 0, 0.05)); border: 1px solid var(--border-color, rgba(128, 128, 128, 0.2)); border-radius: 6px;"
},
[
E(
"div",
{
style: "display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;"
},
[
E(
"div",
{ style: "display: flex; flex-direction: column; gap: 2px;" },
[
E("div", { style: "font-weight: bold; font-size: 14px;" }, [
"💳 ",
_("Credit Cards / SBP / Tinkoff Pay")
]),
E(
"div",
{
style: "font-size: 12px; color: var(--text-color-medium, #888);"
},
_("Quick donation via Russian bank cards or SBP")
)
]
),
renderButton({
classNames: ["cbi-button-action"],
icon: renderLinkIcon24,
text: _("Support on CloudTips"),
onClick: () => {
window.open(CLOUDTIPS_URL, "_blank", "noopener,noreferrer");
}
})
]
)
]
);
const cryptoItemsContainer = E("div", {
style: "display: flex; flex-direction: column; gap: 10px;"
});
CRYPTO_LIST.forEach((item) => {
const addressCode = E(
"code",
{
style: "font-family: monospace; font-size: 11px; word-break: break-all; flex: 1; padding: 6px 8px; background: var(--background-color-primary, rgba(0, 0, 0, 0.15)); border: 1px solid var(--border-color, rgba(128, 128, 128, 0.2)); border-radius: 4px; user-select: all; line-height: 1.4;"
},
item.address
);
const copyBtn = renderButton({
classNames: ["cbi-button"],
icon: renderCopyIcon24,
text: _("Copy Address"),
onClick: async () => {
const ok = await copyTextToClipboard(item.address);
if (ok) {
copyBtn.replaceChildren(
renderCheckIcon24(),
document.createTextNode(" " + _("Copied!"))
);
copyBtn.classList.add("cbi-button-apply");
setTimeout(() => {
copyBtn.replaceChildren(
renderCopyIcon24(),
document.createTextNode(" " + _("Copy Address"))
);
copyBtn.classList.remove("cbi-button-apply");
}, 2e3);
showToast(_("Address copied to clipboard"), "success");
} else {
showToast(_("Failed to copy address"), "error");
}
}
});
copyBtn.style.whiteSpace = "nowrap";
copyBtn.style.flexShrink = "0";
const row = E(
"div",
{
style: "padding: 10px 12px; background: var(--background-color-secondary, rgba(0, 0, 0, 0.05)); border: 1px solid var(--border-color, rgba(128, 128, 128, 0.2)); border-radius: 6px; display: flex; flex-direction: column; gap: 6px;"
},
[
E(
"div",
{
style: "display: flex; align-items: center; gap: 8px;"
},
[
E(
"span",
{ style: "font-weight: bold; font-size: 13px;" },
item.name
),
E(
"span",
{
class: "badge cbi-value-title",
style: "font-size: 11px; padding: 2px 6px; opacity: 0.85;"
},
item.badge
)
]
),
E(
"div",
{
style: "display: flex; align-items: center; gap: 8px; flex-wrap: wrap;"
},
[addressCode, copyBtn]
)
]
);
cryptoItemsContainer.appendChild(row);
});
const cryptoSection = E("div", { style: "margin-bottom: 16px;" }, [
E(
"div",
{
style: "font-weight: bold; font-size: 14px; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;"
},
["🪙 ", _("Cryptocurrency")]
),
cryptoItemsContainer
]);
const footer = E(
"div",
{
style: "display: flex; justify-content: flex-end; padding-top: 12px; border-top: 1px solid var(--border-color, rgba(128, 128, 128, 0.2));"
},
[
renderButton({
classNames: ["cbi-button"],
text: _("Close"),
onClick: () => {
if (ui.hideModal) ui.hideModal();
}
})
]
);
const modalContent = E(
"div",
{
style: "max-width: 640px; width: 100%; box-sizing: border-box; padding: 4px;"
},
[introBlock, cloudTipsCard, cryptoSection, footer]
);
ui.showModal(`💖 ${_("Support Development")}`, modalContent);
}
// src/tachyon/tabs/diagnostic/partials/renderWikiDisclaimer.ts
function renderWikiDisclaimer(kind) {
const iconWrap = E("span", {
@@ -15352,6 +15579,12 @@ function renderWikiDisclaimer(kind) {
"_blank",
"noopener,noreferrer"
)
}),
renderButton({
classNames: ["cbi-button-save"],
icon: renderHeartIcon24,
text: _("Support Development"),
onClick: () => renderSupportModal()
})
]);
}
+30
View File
@@ -6649,3 +6649,33 @@ msgstr "Разрешить только: "
msgid "Block: "
msgstr "Блокировать: "
msgid "Support Development"
msgstr "Поддержать разработку"
msgid "If Tachyon powers your daily networking and keeps your connection fast and secure, consider supporting ongoing development! ☕ 🧀 🌭"
msgstr "Если Tachyon помогает вам и делает работу в сети комфортной, вы можете поддержать проект и автора! ☕ 🧀 🌭"
msgid "Credit Cards / SBP / Tinkoff Pay"
msgstr "Карты РФ / СБП / Tinkoff Pay"
msgid "Quick donation via Russian bank cards or SBP"
msgstr "Быстрый перевод картами РФ или через СБП"
msgid "Support on CloudTips"
msgstr "Поддержать проект на CloudTips"
msgid "Cryptocurrency"
msgstr "Криптовалюта"
msgid "Copy Address"
msgstr "Скопировать"
msgid "Copied!"
msgstr "Скопировано!"
msgid "Address copied to clipboard"
msgstr "Адрес скопирован в буфер обмена"
msgid "Failed to copy address"
msgstr "Не удалось скопировать адрес"