Воронка пейвола, «Ожидают оплаты», публичная оферта, внешние алерты
- 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:
219
app/main.py
219
app/main.py
@@ -76,6 +76,9 @@ SUB_LINK_SECRET = os.getenv("SUB_LINK_SECRET", "") # общий секрет
|
||||
BOT_TOKEN = os.getenv("TOKEN", "")
|
||||
PAYMENT_NOTIFY_CHAT_ID = int(os.getenv("PAYMENT_NOTIFY_CHAT_ID", "156921693"))
|
||||
|
||||
# ── Внешние алерты (vds178: рестарт туннеля и т.п.) ───────────────────────────
|
||||
ALERT_TOKEN = os.getenv("ALERT_TOKEN", "") # низкопривилегированный токен только для /api/alert
|
||||
|
||||
# ── Вход по почте (magic-code через SMTP) ─────────────────────────────────────
|
||||
SMTP_HOST = os.getenv("SMTP_HOST", "")
|
||||
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
||||
@@ -330,6 +333,16 @@ def _db_ensure_auth_table():
|
||||
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.
|
||||
con.execute("""
|
||||
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")
|
||||
if job_id and _db_get_url_res_f(job_id) == "awaiting_payment":
|
||||
_db_set_url_res_f(job_id, "-")
|
||||
# Воронка: успешная оплата пейвольных покупок (файл/подписка)
|
||||
if purpose in ("file", "sub"):
|
||||
_bump_funnel("pay_success")
|
||||
# Уведомление владельцу в Telegram (в отдельном потоке — не блокируем event loop)
|
||||
threading.Thread(target=_notify_owner_payment, args=(payment_id,), daemon=True).start()
|
||||
except Exception as e:
|
||||
@@ -2699,6 +2715,111 @@ _API_DOCS_HTML = """<!DOCTYPE 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="/">← 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>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
@@ -2795,6 +2916,18 @@ _ADMIN_HTML = """<!doctype html>
|
||||
<tbody id="t-sources"></tbody></table></div>
|
||||
</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>
|
||||
<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>
|
||||
@@ -2884,6 +3017,29 @@ _ADMIN_HTML = """<!doctype html>
|
||||
'</td><td class="num">'+Math.round((x.c||0)/stot*100)+'%</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();
|
||||
document.getElementById('t-daily').innerHTML = daily.length ? daily.map(function(x){
|
||||
@@ -3031,6 +3187,53 @@ async def api_hit(request: Request):
|
||||
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:
|
||||
if not ADMIN_TOKEN:
|
||||
return False
|
||||
@@ -3094,6 +3297,22 @@ def _admin_collect_stats() -> dict:
|
||||
# Источники трафика
|
||||
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")]
|
||||
# Воронка пейвола: показы → клики по оплате → успешные оплаты (всего и за 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)")}
|
||||
pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)"
|
||||
|
||||
@@ -1113,6 +1113,7 @@
|
||||
<input type="checkbox" id="paywall-autorenew" checked style="width:16px;height:16px;cursor:pointer">
|
||||
<span data-i18n="paywall_autorenew">Продлевать автоматически каждый месяц</span>
|
||||
</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>
|
||||
<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>
|
||||
@@ -1223,7 +1224,7 @@
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
pdf2scan — <span data-i18n="footer_text">превращаем PDF в скан</span> | <a href="https://t.me/pdf2scan_bot" style="color:var(--blue); text-decoration:none;" data-i18n="footer_tg">Telegram-бот</a> | <a href="/developers" style="color:var(--blue); text-decoration:none;" data-i18n="footer_api">API для бизнеса</a> | <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 — <span data-i18n="footer_text">превращаем PDF в скан</span> | <a href="https://t.me/pdf2scan_bot" style="color:var(--blue); text-decoration:none;" data-i18n="footer_tg">Telegram-бот</a> | <a href="/developers" style="color:var(--blue); text-decoration:none;" data-i18n="footer_api">API для бизнеса</a> | <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> | <a href="/offer" style="color:var(--blue); text-decoration:none;" data-i18n="footer_offer">Оферта</a>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@@ -1323,6 +1324,9 @@
|
||||
footer_tg: 'Telegram-бот',
|
||||
footer_api: 'API для бизнеса',
|
||||
footer_donate: '🍩 Поддержать',
|
||||
footer_offer: 'Оферта',
|
||||
paywall_offer: 'Оплачивая, вы принимаете условия',
|
||||
paywall_offer_link: 'публичной оферты',
|
||||
err_no_file: 'Файл не выбран',
|
||||
err_task_not_found:'Задача не найдена. Попробуйте отправить файл снова.',
|
||||
err_network: 'Ошибка сети при загрузке файла',
|
||||
@@ -1467,6 +1471,9 @@
|
||||
footer_tg: 'Telegram bot',
|
||||
footer_api: 'API for business',
|
||||
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_task_not_found:'Task not found. Please try submitting the file again.',
|
||||
err_network: 'Network error while uploading',
|
||||
@@ -1612,6 +1619,9 @@
|
||||
footer_tg: 'Telegram-Bot',
|
||||
footer_api: 'API für Unternehmen',
|
||||
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_task_not_found:'Aufgabe nicht gefunden. Bitte versuchen Sie, die Datei erneut zu senden.',
|
||||
err_network: 'Netzwerkfehler beim Hochladen',
|
||||
@@ -1757,6 +1767,9 @@
|
||||
footer_tg: 'Bot Telegram',
|
||||
footer_api: 'API pour entreprises',
|
||||
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_task_not_found:'Tâche introuvable. Veuillez soumettre le fichier à nouveau.',
|
||||
err_network: 'Erreur réseau lors du téléchargement',
|
||||
@@ -1902,6 +1915,9 @@
|
||||
footer_tg: 'Bot Telegram',
|
||||
footer_api: 'API per aziende',
|
||||
footer_donate: '🍩 Sostieni',
|
||||
footer_offer: 'Termini',
|
||||
paywall_offer: 'Pagando accetti i',
|
||||
paywall_offer_link: 'termini di offerta',
|
||||
err_no_file: 'Nessun file selezionato',
|
||||
err_task_not_found:'Attività non trovata. Prova a inviare nuovamente il file.',
|
||||
err_network: 'Errore di rete durante il caricamento',
|
||||
@@ -2047,6 +2063,9 @@
|
||||
footer_tg: 'Bot de Telegram',
|
||||
footer_api: 'API para empresas',
|
||||
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_task_not_found:'Tarea no encontrada. Intenta enviar el archivo de nuevo.',
|
||||
err_network: 'Error de red al subir',
|
||||
@@ -2192,6 +2211,9 @@
|
||||
footer_tg: 'Bot do Telegram',
|
||||
footer_api: 'API para empresas',
|
||||
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_task_not_found:'Tarefa não encontrada. Tente enviar o arquivo novamente.',
|
||||
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 }; }
|
||||
}
|
||||
|
||||
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) {
|
||||
track('paywall_shown');
|
||||
paywallMode = mode || 'file';
|
||||
paywallJob = jobId;
|
||||
paywallFilePrice = filePrice || 99;
|
||||
@@ -2420,6 +2447,7 @@
|
||||
}
|
||||
|
||||
async function _payViaKassa(purpose) {
|
||||
track('pay_click');
|
||||
const email = (document.getElementById('paywall-email').value || '').trim();
|
||||
const msg = document.getElementById('paywall-msg');
|
||||
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { msg.textContent = t('pay_need_email'); return; }
|
||||
|
||||
Reference in New Issue
Block a user