Воронка пейвола, «Ожидают оплаты», публичная оферта, внешние алерты

- WEB_FUNNEL: paywall_shown/pay_click (фронт, /api/track) + pay_success (серверно);
  в админке раздел «Воронка пейвола» с конверсией шагов (30 дн / всего)
- Раздел «Ожидают оплаты» — придержанные файлы (awaiting_payment) как лиды
- /offer|/oferta: публичная оферта (подписка, автопродление, возвраты); ссылки в футере
  (7 языков) и в пейволе («Оплачивая, вы принимаете условия…»)
- POST /api/alert (ALERT_TOKEN, антифлуд 60с) → Telegram владельцу; используется скриптом
  лечения туннеля на vds178

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
berkio_admin_gitea
2026-07-07 14:25:15 +00:00
parent ee5ef3f08b
commit 39b45d3a64
2 changed files with 248 additions and 1 deletions

View File

@@ -76,6 +76,9 @@ SUB_LINK_SECRET = os.getenv("SUB_LINK_SECRET", "") # общий секрет
BOT_TOKEN = os.getenv("TOKEN", "") BOT_TOKEN = os.getenv("TOKEN", "")
PAYMENT_NOTIFY_CHAT_ID = int(os.getenv("PAYMENT_NOTIFY_CHAT_ID", "156921693")) PAYMENT_NOTIFY_CHAT_ID = int(os.getenv("PAYMENT_NOTIFY_CHAT_ID", "156921693"))
# ── Внешние алерты (vds178: рестарт туннеля и т.п.) ───────────────────────────
ALERT_TOKEN = os.getenv("ALERT_TOKEN", "") # низкопривилегированный токен только для /api/alert
# ── Вход по почте (magic-code через SMTP) ───────────────────────────────────── # ── Вход по почте (magic-code через SMTP) ─────────────────────────────────────
SMTP_HOST = os.getenv("SMTP_HOST", "") SMTP_HOST = os.getenv("SMTP_HOST", "")
SMTP_PORT = int(os.getenv("SMTP_PORT", "465")) SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
@@ -330,6 +333,16 @@ def _db_ensure_auth_table():
PRIMARY KEY (day, source) PRIMARY KEY (day, source)
) )
""") """)
# Воронка конверсии: (день, событие) → счётчик.
# События: paywall_shown / pay_click / pay_success.
con.execute("""
CREATE TABLE IF NOT EXISTS WEB_FUNNEL (
day TEXT,
event TEXT,
count INTEGER DEFAULT 0,
PRIMARY KEY (day, event)
)
""")
# Платежи ЮKassa. status: pending/succeeded/canceled. purpose: donate/api_pro/file/sub. # Платежи ЮKassa. status: pending/succeeded/canceled. purpose: donate/api_pro/file/sub.
con.execute(""" con.execute("""
CREATE TABLE IF NOT EXISTS WEB_PAYMENTS ( CREATE TABLE IF NOT EXISTS WEB_PAYMENTS (
@@ -1836,6 +1849,9 @@ def _db_mark_payment_paid(payment_id, purpose, api_key, extra=None, payment_meth
job_id = extra.get("job_id") job_id = extra.get("job_id")
if job_id and _db_get_url_res_f(job_id) == "awaiting_payment": if job_id and _db_get_url_res_f(job_id) == "awaiting_payment":
_db_set_url_res_f(job_id, "-") _db_set_url_res_f(job_id, "-")
# Воронка: успешная оплата пейвольных покупок (файл/подписка)
if purpose in ("file", "sub"):
_bump_funnel("pay_success")
# Уведомление владельцу в Telegram (в отдельном потоке — не блокируем event loop) # Уведомление владельцу в Telegram (в отдельном потоке — не блокируем event loop)
threading.Thread(target=_notify_owner_payment, args=(payment_id,), daemon=True).start() threading.Thread(target=_notify_owner_payment, args=(payment_id,), daemon=True).start()
except Exception as e: except Exception as e:
@@ -2699,6 +2715,111 @@ _API_DOCS_HTML = """<!DOCTYPE html>
</html>""" </html>"""
_OFFER_HTML = """<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Публичная оферта — pdf2scan.online</title>
<meta name="robots" content="noindex, follow">
<style>
body{ margin:0; background:#f2f2f7; color:#1c1c1e; font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif; }
.wrap{ max-width:760px; margin:0 auto; padding:24px 16px 48px; }
.card{ background:#fff; border-radius:16px; padding:28px 26px; }
h1{ font-size:22px; margin:0 0 4px; }
.upd{ color:#8e8e93; font-size:13px; margin-bottom:20px; }
h2{ font-size:16px; margin:22px 0 8px; }
p, li{ font-size:14.5px; color:#3a3a3c; }
ul{ padding-left:20px; }
a{ color:#007AFF; text-decoration:none; }
.back{ display:inline-block; margin-bottom:14px; font-size:14px; }
</style>
</head>
<body>
<div class="wrap">
<a class="back" href="/">&larr; pdf2scan.online</a>
<div class="card">
<h1>Публичная оферта</h1>
<div class="upd">Редакция от 7 июля 2026 г.</div>
<p>Настоящий документ является публичной офертой (ст. 437 ГК РФ) — предложением
ИП Букреева Александра Игоревича (ИНН 575208141840, далее — «Исполнитель»)
заключить договор об оказании услуг сервиса pdf2scan (сайт pdf2scan.online и
Telegram-бот @pdf2scan_bot, далее — «Сервис») на условиях ниже.
Оплата услуг или использование Сервиса означает полное принятие условий (акцепт).</p>
<h2>1. Услуги</h2>
<p>Сервис выполняет автоматизированную обработку электронных документов (PDF, DOC/DOCX,
изображения): создание растрового PDF с эффектом сканирования, с дополнительными
опциями (сшивка, нумерация, титульный лист, PDF/A, распознавание текста).</p>
<h2>2. Бесплатное и платное использование</h2>
<ul>
<li>Бесплатно обрабатываются одиночные файлы размером до 50 МБ и до 100 страниц.</li>
<li><b>Разовая обработка файла сверх лимита</b> — {{FILE_PRICE}} ₽ за файл.</li>
<li><b>Подписка</b> — {{SUB_PRICE}} ₽ за 30 дней: файлы без ограничений лимита и пакетная обработка на сайте.</li>
<li><b>Тариф API «Business»</b> — {{API_PRICE}} ₽ за 30 дней (для интеграций, см. /developers).</li>
</ul>
<p>Цены указаны в рублях РФ, НДС не облагается. Действующие цены отображаются на Сервисе в момент оплаты.</p>
<h2>3. Оплата и чеки</h2>
<p>Оплата проводится через сервис ЮKassa банковской картой или иными доступными способами.
Кассовый чек (54-ФЗ) направляется на электронную почту, указанную при оплате.</p>
<h2>4. Автопродление подписки</h2>
<ul>
<li>Если при оплате подписки включена опция «Продлевать автоматически», способ оплаты
сохраняется, и стоимость подписки списывается автоматически каждые 30 дней
(списание выполняется незадолго до окончания оплаченного периода).</li>
<li>Автопродление можно отключить в любой момент на сайте (карточка подписки после входа
в аккаунт). Отключение действует со следующего периода; уже оплаченный период сохраняется.</li>
<li>При невозможности списания подписка приостанавливается после окончания оплаченного периода.</li>
</ul>
<h2>5. Порядок оказания услуги</h2>
<ul>
<li>Услуга по разовой оплате считается оказанной с момента готовности обработанного файла к скачиванию (на сайте) или его отправки в Telegram.</li>
<li>Результат хранится ограниченное время (до 6 часов на сайте), после чего удаляется.</li>
<li>Загруженные файлы хранятся не более 24 часов и удаляются автоматически; содержимое файлов не передаётся третьим лицам.</li>
</ul>
<h2>6. Возвраты</h2>
<ul>
<li>Если оплаченный файл не был обработан по вине Сервиса — стоимость возвращается по обращению.</li>
<li>От подписки можно отказаться; по обращению возвращается стоимость пропорционально неиспользованному периоду.</li>
<li>Обращения — на контакты из раздела 9; срок рассмотрения — до 10 рабочих дней, возврат — тем же способом, которым была произведена оплата.</li>
</ul>
<h2>7. Ограничения и ответственность</h2>
<ul>
<li>Сервис предоставляется «как есть»; Исполнитель не гарантирует пригодность результата для конкретной цели и корректность обработки повреждённых файлов.</li>
<li>Запрещается загружать материалы, нарушающие законодательство РФ или права третьих лиц.</li>
<li>Совокупная ответственность Исполнителя ограничена суммой соответствующего платежа.</li>
</ul>
<h2>8. Прочие условия</h2>
<p>Исполнитель вправе изменять условия оферты, публикуя новую редакцию на этой странице;
новая редакция действует для оплат, совершённых после публикации. К отношениям сторон
применяется право Российской Федерации.</p>
<h2>9. Исполнитель и контакты</h2>
<p>ИП Букреев Александр Игоревич, ИНН 575208141840<br>
Telegram: <a href="https://t.me/berkio">@berkio</a> · E-mail: <a href="mailto:b3rkut@yandex.ru">b3rkut@yandex.ru</a></p>
</div>
</div>
</body>
</html>"""
@app.get("/offer", response_class=HTMLResponse)
@app.get("/oferta", response_class=HTMLResponse)
async def offer_page():
return (_OFFER_HTML
.replace("{{FILE_PRICE}}", str(int(FILE_PRICE_RUB)))
.replace("{{SUB_PRICE}}", str(int(SUB_PRICE_RUB)))
.replace("{{API_PRICE}}", str(int(API_PRO_PRICE_RUB))))
_ADMIN_HTML = """<!doctype html> _ADMIN_HTML = """<!doctype html>
<html lang="ru"> <html lang="ru">
<head> <head>
@@ -2795,6 +2916,18 @@ _ADMIN_HTML = """<!doctype html>
<tbody id="t-sources"></tbody></table></div> <tbody id="t-sources"></tbody></table></div>
</section> </section>
<section>
<h2>Воронка пейвола <span class="muted" id="funnel-note"></span></h2>
<div class="scroll"><table><thead><tr><th>Шаг</th><th class="num">За 30 дней</th><th class="num">Всего</th><th class="num">Конверсия шага</th></tr></thead>
<tbody id="t-funnel"></tbody></table></div>
</section>
<section>
<h2>Ожидают оплаты <span class="muted" id="held-note"></span></h2>
<div class="scroll"><table><thead><tr><th>Дата</th><th>Файл</th><th class="num">Размер</th><th>Пользователь</th></tr></thead>
<tbody id="t-held"></tbody></table></div>
</section>
<section> <section>
<h2>По дням</h2> <h2>По дням</h2>
<div class="scroll"><table><thead><tr><th>Дата</th><th class="num">Заходы</th><th class="num">Уник.</th><th class="num">Файлов</th><th class="num">Страниц</th><th class="num">Ср/стр</th><th class="num">Исходник</th><th class="num">Результат</th></tr></thead> <div class="scroll"><table><thead><tr><th>Дата</th><th class="num">Заходы</th><th class="num">Уник.</th><th class="num">Файлов</th><th class="num">Страниц</th><th class="num">Ср/стр</th><th class="num">Исходник</th><th class="num">Результат</th></tr></thead>
@@ -2884,6 +3017,29 @@ _ADMIN_HTML = """<!doctype html>
'</td><td class="num">'+Math.round((x.c||0)/stot*100)+'%</td></tr>'; '</td><td class="num">'+Math.round((x.c||0)/stot*100)+'%</td></tr>';
}).join('') : '<tr><td colspan="3" class="muted">Пока нет данных — собирается с этого момента</td></tr>'; }).join('') : '<tr><td colspan="3" class="muted">Пока нет данных — собирается с этого момента</td></tr>';
// Воронка пейвола
var fnT = (d.funnel && d.funnel.total) || {}, fn30 = (d.funnel && d.funnel.last30) || {};
function conv(a,b){ return (b>0) ? Math.round(a/b*100)+'%' : ''; }
var steps = [
['Пейвол показан', fn30.paywall_shown||0, fnT.paywall_shown||0, ''],
['Клик по оплате', fn30.pay_click||0, fnT.pay_click||0, conv(fn30.pay_click||0, fn30.paywall_shown||0)],
['Оплата успешна', fn30.pay_success||0, fnT.pay_success||0, conv(fn30.pay_success||0, fn30.pay_click||0)]
];
document.getElementById('t-funnel').innerHTML = steps.map(function(s){
return '<tr><td>'+s[0]+'</td><td class="num">'+s[1]+'</td><td class="num">'+s[2]+'</td><td class="num">'+s[3]+'</td></tr>';
}).join('');
document.getElementById('funnel-note').textContent = '· конверсия пейвол→оплата за 30 дн: ' +
conv(fn30.pay_success||0, fn30.paywall_shown||0);
// Ожидают оплаты (придержанные файлы)
var held = d.held || [];
document.getElementById('held-note').textContent = '· ' + held.length;
document.getElementById('t-held').innerHTML = held.length ? held.map(function(h){
var who = (h.user_id && h.user_id !== d.web_user_id) ? ('tg:'+h.user_id) : 'сайт';
return '<tr><td>'+esc((h.date||'')+' '+String(h.time||'').slice(0,5))+'</td><td class="fname" title="'+esc(h.name)+'">'+esc(h.name)+
'</td><td class="num">'+size(h.source_size_mb)+'</td><td>'+esc(who)+'</td></tr>';
}).join('') : '<tr><td colspan="4" class="muted">Нет файлов, ожидающих оплаты</td></tr>';
// По дням (свежие сверху) // По дням (свежие сверху)
var daily = (d.daily||[]).slice().reverse(); var daily = (d.daily||[]).slice().reverse();
document.getElementById('t-daily').innerHTML = daily.length ? daily.map(function(x){ document.getElementById('t-daily').innerHTML = daily.length ? daily.map(function(x){
@@ -3031,6 +3187,53 @@ async def api_hit(request: Request):
return resp return resp
def _bump_funnel(event: str):
try:
day = datetime.datetime.now().strftime("%Y-%m-%d")
with sqlite3.connect(DB_PATH, timeout=10) as con:
con.execute(
"INSERT INTO WEB_FUNNEL (day, event, count) VALUES (?, ?, 1) "
"ON CONFLICT(day, event) DO UPDATE SET count = count + 1",
(day, event))
con.commit()
except Exception:
pass
_FUNNEL_EVENTS = {"paywall_shown", "pay_click"}
@app.post("/api/track")
async def api_track(payload: dict):
"""Событие воронки с фронта (paywall_shown/pay_click). pay_success считается серверно."""
ev = str((payload or {}).get("event") or "")[:32]
if ev in _FUNNEL_EVENTS:
await asyncio.to_thread(_bump_funnel, ev)
return {"ok": True}
_last_ext_alert = 0.0
@app.post("/api/alert")
async def api_alert(payload: dict):
"""Внешний алерт (напр. с vds178 при рестарте туннеля) → сообщение владельцу в Telegram.
Авторизация — ALERT_TOKEN (низкопривилегированный, умеет только это)."""
global _last_ext_alert
if not ALERT_TOKEN:
raise HTTPException(status_code=503, detail="alerts not configured")
tok = str((payload or {}).get("token") or "")
if not hmac.compare_digest(tok, ALERT_TOKEN):
raise HTTPException(status_code=401, detail="unauthorized")
now = time.time()
if now - _last_ext_alert < 60: # антифлуд: не чаще раза в минуту
return {"ok": True, "throttled": True}
_last_ext_alert = now
text = "🚨 " + str((payload or {}).get("text") or "alert")[:300]
await asyncio.to_thread(_tg_send, PAYMENT_NOTIFY_CHAT_ID, text)
return {"ok": True}
def _require_admin(request: Request) -> bool: def _require_admin(request: Request) -> bool:
if not ADMIN_TOKEN: if not ADMIN_TOKEN:
return False return False
@@ -3094,6 +3297,22 @@ def _admin_collect_stats() -> dict:
# Источники трафика # Источники трафика
out["sources"] = [dict(r) for r in con.execute( out["sources"] = [dict(r) for r in con.execute(
"SELECT source, SUM(count) c FROM WEB_SOURCES GROUP BY source ORDER BY c DESC LIMIT 30")] "SELECT source, SUM(count) c FROM WEB_SOURCES GROUP BY source ORDER BY c DESC LIMIT 30")]
# Воронка пейвола: показы → клики по оплате → успешные оплаты (всего и за 30 дней)
fun = {"paywall_shown": 0, "pay_click": 0, "pay_success": 0}
fun30 = dict(fun)
cutoff30 = (datetime.date.today() - datetime.timedelta(days=29)).strftime("%Y-%m-%d")
for r in con.execute("SELECT event, SUM(count) c FROM WEB_FUNNEL GROUP BY event"):
if r["event"] in fun:
fun[r["event"]] = r["c"] or 0
for r in con.execute("SELECT event, SUM(count) c FROM WEB_FUNNEL WHERE day >= ? GROUP BY event", (cutoff30,)):
if r["event"] in fun30:
fun30[r["event"]] = r["c"] or 0
out["funnel"] = {"total": fun, "last30": fun30}
# Придержанные файлы (ждут оплаты) — это тёплые лиды
out["held"] = [dict(r) for r in con.execute(
"SELECT date, time, COALESCE(NULLIF(original_name,''), name_or_f) AS name, "
"source_size_mb, user_id FROM FILES WHERE url_res_f='awaiting_payment' "
"ORDER BY rowid DESC LIMIT 50")]
# Документы # Документы
cols = {r[1] for r in con.execute("PRAGMA table_info(FILES)")} cols = {r[1] for r in con.execute("PRAGMA table_info(FILES)")}
pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)" pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)"

View File

@@ -1113,6 +1113,7 @@
<input type="checkbox" id="paywall-autorenew" checked style="width:16px;height:16px;cursor:pointer"> <input type="checkbox" id="paywall-autorenew" checked style="width:16px;height:16px;cursor:pointer">
<span data-i18n="paywall_autorenew">Продлевать автоматически каждый месяц</span> <span data-i18n="paywall_autorenew">Продлевать автоматически каждый месяц</span>
</label> </label>
<div style="font-size:11.5px;color:var(--text-secondary);margin-top:8px"><span data-i18n="paywall_offer">Оплачивая, вы принимаете условия</span> <a href="/offer" target="_blank" style="color:var(--blue)" data-i18n="paywall_offer_link">публичной оферты</a></div>
<div id="paywall-msg" style="font-size:12px;color:var(--text-secondary);margin-top:10px"></div> <div id="paywall-msg" style="font-size:12px;color:var(--text-secondary);margin-top:10px"></div>
<button onclick="resetApp()" style="margin-top:8px;background:none;border:none;color:var(--text-secondary);font-size:13px;cursor:pointer;text-decoration:underline;font-family:inherit" data-i18n="paywall_cancel">Отмена</button> <button onclick="resetApp()" style="margin-top:8px;background:none;border:none;color:var(--text-secondary);font-size:13px;cursor:pointer;text-decoration:underline;font-family:inherit" data-i18n="paywall_cancel">Отмена</button>
</div> </div>
@@ -1223,7 +1224,7 @@
</section> </section>
<footer> <footer>
pdf2scan &mdash; <span data-i18n="footer_text">превращаем PDF в скан</span> &nbsp;|&nbsp; <a href="https://t.me/pdf2scan_bot" style="color:var(--blue); text-decoration:none;" data-i18n="footer_tg">Telegram-бот</a> &nbsp;|&nbsp; <a href="/developers" style="color:var(--blue); text-decoration:none;" data-i18n="footer_api">API для бизнеса</a> &nbsp;|&nbsp; <a href="#" onclick="showDonateCard(false);document.getElementById('donate-card').scrollIntoView({behavior:'smooth'});return false;" style="color:var(--blue); text-decoration:none;" data-i18n="footer_donate">🍩 Поддержать</a> pdf2scan &mdash; <span data-i18n="footer_text">превращаем PDF в скан</span> &nbsp;|&nbsp; <a href="https://t.me/pdf2scan_bot" style="color:var(--blue); text-decoration:none;" data-i18n="footer_tg">Telegram-бот</a> &nbsp;|&nbsp; <a href="/developers" style="color:var(--blue); text-decoration:none;" data-i18n="footer_api">API для бизнеса</a> &nbsp;|&nbsp; <a href="#" onclick="showDonateCard(false);document.getElementById('donate-card').scrollIntoView({behavior:'smooth'});return false;" style="color:var(--blue); text-decoration:none;" data-i18n="footer_donate">🍩 Поддержать</a> &nbsp;|&nbsp; <a href="/offer" style="color:var(--blue); text-decoration:none;" data-i18n="footer_offer">Оферта</a>
</footer> </footer>
<script> <script>
@@ -1323,6 +1324,9 @@
footer_tg: 'Telegram-бот', footer_tg: 'Telegram-бот',
footer_api: 'API для бизнеса', footer_api: 'API для бизнеса',
footer_donate: '🍩 Поддержать', footer_donate: '🍩 Поддержать',
footer_offer: 'Оферта',
paywall_offer: 'Оплачивая, вы принимаете условия',
paywall_offer_link: 'публичной оферты',
err_no_file: 'Файл не выбран', err_no_file: 'Файл не выбран',
err_task_not_found:'Задача не найдена. Попробуйте отправить файл снова.', err_task_not_found:'Задача не найдена. Попробуйте отправить файл снова.',
err_network: 'Ошибка сети при загрузке файла', err_network: 'Ошибка сети при загрузке файла',
@@ -1467,6 +1471,9 @@
footer_tg: 'Telegram bot', footer_tg: 'Telegram bot',
footer_api: 'API for business', footer_api: 'API for business',
footer_donate: '🍩 Support', footer_donate: '🍩 Support',
footer_offer: 'Terms',
paywall_offer: 'By paying you accept the',
paywall_offer_link: 'public offer terms',
err_no_file: 'No file selected', err_no_file: 'No file selected',
err_task_not_found:'Task not found. Please try submitting the file again.', err_task_not_found:'Task not found. Please try submitting the file again.',
err_network: 'Network error while uploading', err_network: 'Network error while uploading',
@@ -1612,6 +1619,9 @@
footer_tg: 'Telegram-Bot', footer_tg: 'Telegram-Bot',
footer_api: 'API für Unternehmen', footer_api: 'API für Unternehmen',
footer_donate: '🍩 Unterstützen', footer_donate: '🍩 Unterstützen',
footer_offer: 'AGB',
paywall_offer: 'Mit der Zahlung akzeptieren Sie die',
paywall_offer_link: 'Angebotsbedingungen',
err_no_file: 'Keine Datei ausgewählt', err_no_file: 'Keine Datei ausgewählt',
err_task_not_found:'Aufgabe nicht gefunden. Bitte versuchen Sie, die Datei erneut zu senden.', err_task_not_found:'Aufgabe nicht gefunden. Bitte versuchen Sie, die Datei erneut zu senden.',
err_network: 'Netzwerkfehler beim Hochladen', err_network: 'Netzwerkfehler beim Hochladen',
@@ -1757,6 +1767,9 @@
footer_tg: 'Bot Telegram', footer_tg: 'Bot Telegram',
footer_api: 'API pour entreprises', footer_api: 'API pour entreprises',
footer_donate: '🍩 Soutenir', footer_donate: '🍩 Soutenir',
footer_offer: 'Conditions',
paywall_offer: 'En payant vous acceptez les',
paywall_offer_link: 'conditions générales',
err_no_file: 'Aucun fichier sélectionné', err_no_file: 'Aucun fichier sélectionné',
err_task_not_found:'Tâche introuvable. Veuillez soumettre le fichier à nouveau.', err_task_not_found:'Tâche introuvable. Veuillez soumettre le fichier à nouveau.',
err_network: 'Erreur réseau lors du téléchargement', err_network: 'Erreur réseau lors du téléchargement',
@@ -1902,6 +1915,9 @@
footer_tg: 'Bot Telegram', footer_tg: 'Bot Telegram',
footer_api: 'API per aziende', footer_api: 'API per aziende',
footer_donate: '🍩 Sostieni', footer_donate: '🍩 Sostieni',
footer_offer: 'Termini',
paywall_offer: 'Pagando accetti i',
paywall_offer_link: 'termini di offerta',
err_no_file: 'Nessun file selezionato', err_no_file: 'Nessun file selezionato',
err_task_not_found:'Attività non trovata. Prova a inviare nuovamente il file.', err_task_not_found:'Attività non trovata. Prova a inviare nuovamente il file.',
err_network: 'Errore di rete durante il caricamento', err_network: 'Errore di rete durante il caricamento',
@@ -2047,6 +2063,9 @@
footer_tg: 'Bot de Telegram', footer_tg: 'Bot de Telegram',
footer_api: 'API para empresas', footer_api: 'API para empresas',
footer_donate: '🍩 Apoyar', footer_donate: '🍩 Apoyar',
footer_offer: 'Términos',
paywall_offer: 'Al pagar aceptas los',
paywall_offer_link: 'términos de la oferta',
err_no_file: 'Ningún archivo seleccionado', err_no_file: 'Ningún archivo seleccionado',
err_task_not_found:'Tarea no encontrada. Intenta enviar el archivo de nuevo.', err_task_not_found:'Tarea no encontrada. Intenta enviar el archivo de nuevo.',
err_network: 'Error de red al subir', err_network: 'Error de red al subir',
@@ -2192,6 +2211,9 @@
footer_tg: 'Bot do Telegram', footer_tg: 'Bot do Telegram',
footer_api: 'API para empresas', footer_api: 'API para empresas',
footer_donate: '🍩 Apoiar', footer_donate: '🍩 Apoiar',
footer_offer: 'Termos',
paywall_offer: 'Ao pagar você aceita os',
paywall_offer_link: 'termos da oferta',
err_no_file: 'Nenhum arquivo selecionado', err_no_file: 'Nenhum arquivo selecionado',
err_task_not_found:'Tarefa não encontrada. Tente enviar o arquivo novamente.', err_task_not_found:'Tarefa não encontrada. Tente enviar o arquivo novamente.',
err_network: 'Erro de rede ao fazer upload', err_network: 'Erro de rede ao fazer upload',
@@ -2391,7 +2413,12 @@
} catch (e) { return { requires_payment: false, sub_active: false, paywall_enabled: false, file_price: 99, sub_price: 299 }; } } catch (e) { return { requires_payment: false, sub_active: false, paywall_enabled: false, file_price: 99, sub_price: 299 }; }
} }
function track(event) {
try { fetch('/api/track', { method: 'POST', keepalive: true, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event: event }) }).catch(function(){}); } catch (_) {}
}
function showPaywall(jobId, filePrice, subPrice, mode) { function showPaywall(jobId, filePrice, subPrice, mode) {
track('paywall_shown');
paywallMode = mode || 'file'; paywallMode = mode || 'file';
paywallJob = jobId; paywallJob = jobId;
paywallFilePrice = filePrice || 99; paywallFilePrice = filePrice || 99;
@@ -2420,6 +2447,7 @@
} }
async function _payViaKassa(purpose) { async function _payViaKassa(purpose) {
track('pay_click');
const email = (document.getElementById('paywall-email').value || '').trim(); const email = (document.getElementById('paywall-email').value || '').trim();
const msg = document.getElementById('paywall-msg'); const msg = document.getElementById('paywall-msg');
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { msg.textContent = t('pay_need_email'); return; } if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { msg.textContent = t('pay_need_email'); return; }