UX Sprint: consent before register + welcome screen
- showWelcome() first screen for non-Telegram users - 'Open in Telegram' CTA + 'or use email' button - emailRegister shows consent overlay BEFORE API call - After consent accept, registers and continues with onboarding - Bumped consent UX: 4 checkboxes + legal links - escapeHtml: quotes added to avoid edge XSS
This commit is contained in:
parent
7fa2dc83e4
commit
4cc8858a25
264
public/js/app.js
264
public/js/app.js
@ -2,9 +2,9 @@
|
||||
const App = {
|
||||
consentGiven: false,
|
||||
isMobileMode: false,
|
||||
_preReg: null, // callback после согласия
|
||||
|
||||
async init() {
|
||||
// Определяем: десктоп — стартуем в mobile-mode для удобства разработки
|
||||
const isTouch = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
|
||||
const smallScreen = window.innerWidth <= 768;
|
||||
const savedMode = localStorage.getItem('buhapp.mode');
|
||||
@ -13,7 +13,6 @@ const App = {
|
||||
} else if (savedMode === 'mobile') {
|
||||
this.isMobileMode = true;
|
||||
} else {
|
||||
// авто: если экран узкий — нативный мобильный, иначе мобильный-эмулятор
|
||||
this.isMobileMode = !smallScreen;
|
||||
}
|
||||
this.applyMode();
|
||||
@ -22,24 +21,19 @@ const App = {
|
||||
this.isMobileMode = !this.isMobileMode;
|
||||
localStorage.setItem('buhapp.mode', this.isMobileMode ? 'mobile' : 'desktop');
|
||||
this.applyMode();
|
||||
// оповестим карту чтобы она пересчитала размер
|
||||
if (window.MapView?.map) {
|
||||
setTimeout(() => window.MapView.map.invalidateSize(), 100);
|
||||
}
|
||||
};
|
||||
|
||||
// bind tabbar
|
||||
document.querySelectorAll('.tab').forEach(btn => {
|
||||
btn.onclick = () => this.showView(btn.dataset.view);
|
||||
});
|
||||
|
||||
// restore tokens from localStorage
|
||||
API.Store.load();
|
||||
|
||||
if (TG_APP.isAvailable && TG_APP.initData) {
|
||||
// авторизация через Telegram
|
||||
if (TG_APP.isTelegram && TG_APP.user) {
|
||||
try {
|
||||
await API.Auth.loginWithTelegram(TG_APP.initData);
|
||||
await API.Auth.me();
|
||||
await this.checkConsent();
|
||||
await this.maybeOnboard();
|
||||
this.showView('map');
|
||||
@ -47,105 +41,108 @@ const App = {
|
||||
this.showError('Не удалось войти через Telegram: ' + e.message);
|
||||
}
|
||||
} else if (API.Store.access) {
|
||||
// уже залогинен
|
||||
try {
|
||||
await API.Auth.me();
|
||||
await this.checkConsent();
|
||||
await this.maybeOnboard();
|
||||
this.showView('map');
|
||||
} catch (e) {
|
||||
API.Store.clear();
|
||||
this.showError('Сессия истекла: ' + e.message);
|
||||
}
|
||||
} else {
|
||||
// WebApp открыт вне Telegram — предложим email login
|
||||
this.showEmailLogin();
|
||||
// Вне Telegram — показать email login (но сначала согласия)
|
||||
this.showWelcome();
|
||||
}
|
||||
|
||||
// bind input events for chat
|
||||
document.body.addEventListener('click', (e) => {
|
||||
const sendBtn = e.target.closest('#msg-send');
|
||||
if (sendBtn) { window.ChatView.send(); return; }
|
||||
const cancel = e.target.closest('#edit-cancel');
|
||||
if (cancel) { document.getElementById('msg-input').value = ''; document.getElementById('edit-bar').style.display='none'; return; }
|
||||
});
|
||||
document.body.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && e.target.id === 'msg-input') {
|
||||
if (!e.shiftKey) { e.preventDefault(); window.ChatView.send(); }
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && document.activeElement?.id === 'msg-input') return;
|
||||
if (e.key === 'Escape') {
|
||||
if (window.ChatView?.cleanup) window.ChatView.cleanup();
|
||||
if (API.Store.access) this.showView('map');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
applyMode() {
|
||||
if (this.isMobileMode) {
|
||||
document.body.classList.add('mobile-mode');
|
||||
document.getElementById('mode-toggle').textContent = '🖥️ На весь экран';
|
||||
} else {
|
||||
document.body.classList.remove('mobile-mode');
|
||||
document.getElementById('mode-toggle').textContent = '📱 Мобильный';
|
||||
}
|
||||
},
|
||||
|
||||
async checkConsent() {
|
||||
// проверяем на бэкенде — если все 4 уже приняты, не показываем
|
||||
try {
|
||||
const s = await API.ConsentApi.status();
|
||||
if (s.all_accepted) {
|
||||
return true;
|
||||
}
|
||||
if (s.all_accepted) return true;
|
||||
} catch (e) {
|
||||
console.warn('consent status check failed', e);
|
||||
}
|
||||
// не показываем если нет авторизации — это для авторизованных
|
||||
if (!API.Store.user) return true;
|
||||
this.showConsent();
|
||||
return false;
|
||||
},
|
||||
|
||||
// Показываем welcome-flow новому пользователю ОДИН раз.
|
||||
// Возвращает Promise, который резолвится когда пользователь прошёл/пропустил онбординг.
|
||||
async maybeOnboard() {
|
||||
if (!window.OnboardingView) {
|
||||
console.warn('[onboarding] OnboardingView not loaded');
|
||||
return;
|
||||
}
|
||||
if (OnboardingView.isDone()) return;
|
||||
const root = document.getElementById('app');
|
||||
await OnboardingView.render(root);
|
||||
},
|
||||
|
||||
showConsent() {
|
||||
const ov = document.createElement('div');
|
||||
ov.id = 'consent-overlay';
|
||||
ov.innerHTML = `
|
||||
<h1>🍻 Добро пожаловать в BuhApp</h1>
|
||||
<p class="sub">Приложение для поиска компании. Только для лиц 18+.</p>
|
||||
<div class="card" style="background:var(--bg-elev);">
|
||||
<p style="margin-bottom:12px;font-size:13px;">
|
||||
⚠️ <strong>Внимание</strong>: BuhApp — это платформа для знакомств и встреч с другими пользователями.
|
||||
Включает в себя риски, связанные с употреблением алкоголя и личными встречами.
|
||||
</p>
|
||||
<p style="font-size:13px;">
|
||||
Пожалуйста, ознакомьтесь и примите следующие документы:
|
||||
</p>
|
||||
<div class="consent-line"><input type="checkbox" id="c-adult"><label for="c-adult">Мне исполнилось 18 лет (обязательно)</label></div>
|
||||
<div class="consent-line"><input type="checkbox" id="c-terms"><label for="c-terms">Пользовательское соглашение (v1.0)</label></div>
|
||||
<div class="consent-line"><input type="checkbox" id="c-privacy"><label for="c-privacy">Политика конфиденциальности (152-ФЗ, v1.0)</label></div>
|
||||
<div class="consent-line"><input type="checkbox" id="c-disclaimer"><label for="c-disclaimer">Отказ от ответственности (v1.0)</label></div>
|
||||
<button class="btn" id="c-accept" style="margin-top:16px;">Принять и продолжить</button>
|
||||
|
||||
<div class="card">
|
||||
<strong>Перед тем как продолжить, прочти и прими:</strong>
|
||||
<div class="consent-line">
|
||||
<input type="checkbox" id="c-adult">
|
||||
<label for="c-adult">Мне уже исполнилось 18 лет</label>
|
||||
</div>
|
||||
<div class="consent-line">
|
||||
<input type="checkbox" id="c-terms">
|
||||
<label for="c-terms">Согласен с <a href="/legal/TERMS.html" target="_blank">Пользовательским соглашением</a></label>
|
||||
</div>
|
||||
<div class="consent-line">
|
||||
<input type="checkbox" id="c-privacy">
|
||||
<label for="c-privacy">Согласен с <a href="/legal/PRIVACY.html" target="_blank">Политикой конфиденциальности</a></label>
|
||||
</div>
|
||||
<div class="consent-line">
|
||||
<input type="checkbox" id="c-disclaimer">
|
||||
<label for="c-disclaimer">Принимаю <a href="/legal/DISCLAIMER.html" target="_blank">Отказ от ответственности</a></label>
|
||||
</div>
|
||||
<button class="btn" id="c-submit" disabled>Принять и продолжить</button>
|
||||
<div class="privacy-note" style="margin-top:12px;">Без принятия согласий пользование приложением невозможно.</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(ov);
|
||||
// цепляемся к #mobile-frame если он есть
|
||||
const target = document.querySelector('body.mobile-mode #mobile-frame') || document.body;
|
||||
target.appendChild(ov);
|
||||
|
||||
document.getElementById('c-accept').onclick = async () => {
|
||||
const a = document.getElementById('c-adult').checked;
|
||||
const t = document.getElementById('c-terms').checked;
|
||||
const p = document.getElementById('c-privacy').checked;
|
||||
const d = document.getElementById('c-disclaimer').checked;
|
||||
if (!a || !t || !p || !d) {
|
||||
TG_APP.showAlert('Нужно принять все 4 пункта');
|
||||
return;
|
||||
}
|
||||
const check = () => {
|
||||
const ok = document.getElementById('c-adult').checked
|
||||
&& document.getElementById('c-terms').checked
|
||||
&& document.getElementById('c-privacy').checked
|
||||
&& document.getElementById('c-disclaimer').checked;
|
||||
document.getElementById('c-submit').disabled = !ok;
|
||||
};
|
||||
['c-adult','c-terms','c-privacy','c-disclaimer'].forEach(id =>
|
||||
document.getElementById(id).addEventListener('change', check)
|
||||
);
|
||||
|
||||
document.getElementById('c-submit').onclick = async () => {
|
||||
try {
|
||||
await API.api('/api/v1/auth/consents', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
adult: a, terms: t, privacy: p, disclaimer: d,
|
||||
},
|
||||
});
|
||||
TG_APP.haptic('notification');
|
||||
await API.ConsentApi.accept({ adult: true, terms: true, privacy: true, disclaimer: true });
|
||||
ov.remove();
|
||||
this.consentGiven = true;
|
||||
await this.maybeOnboard();
|
||||
this.showView('map');
|
||||
if (this._preReg) {
|
||||
const fn = this._preReg;
|
||||
this._preReg = null;
|
||||
fn();
|
||||
} else {
|
||||
await this.maybeOnboard();
|
||||
this.showView('map');
|
||||
}
|
||||
} catch (e) {
|
||||
TG_APP.showAlert('Ошибка: ' + e.message);
|
||||
}
|
||||
@ -163,26 +160,33 @@ const App = {
|
||||
`;
|
||||
},
|
||||
|
||||
applyMode() {
|
||||
if (this.isMobileMode) {
|
||||
document.body.classList.add('mobile-mode');
|
||||
document.getElementById('mode-toggle').textContent = '🖥️ На весь экран';
|
||||
} else {
|
||||
document.body.classList.remove('mobile-mode');
|
||||
document.getElementById('mode-toggle').textContent = '📱 Мобильный';
|
||||
}
|
||||
showWelcome() {
|
||||
// Приветственный экран вне Telegram — выбор: email login или открыть в Telegram
|
||||
document.getElementById('app').innerHTML = `
|
||||
<div class="view">
|
||||
<h1>🍻 BuhApp</h1>
|
||||
<div class="sub">Приложение для поиска компании (18+)</div>
|
||||
<div class="card">
|
||||
<p>BuhApp работает внутри Telegram — это безопаснее, проще и быстрее.</p>
|
||||
<a class="btn" href="https://t.me/buhoiapp_bot" target="_blank">🚀 Открыть в Telegram</a>
|
||||
<button class="btn ghost" id="use-email">Или войти по email</button>
|
||||
</div>
|
||||
<div class="card" style="font-size:12px;color:var(--text-dim);">
|
||||
🍻 Только для лиц 18+. Использование = принятие
|
||||
<a href="/legal/TERMS.html" target="_blank">соглашения</a> и
|
||||
<a href="/legal/DISCLAIMER.html" target="_blank">отказа от ответственности</a>.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('use-email').onclick = () => this.showEmailLogin();
|
||||
},
|
||||
|
||||
showEmailLogin() {
|
||||
document.getElementById('app').innerHTML = `
|
||||
<div class="view">
|
||||
<h1>🍻 BuhApp</h1>
|
||||
<div class="sub">Приложение для поиска компании (Telegram Mini App)</div>
|
||||
<h1>Вход</h1>
|
||||
<div class="sub">Войди или зарегистрируйся по email</div>
|
||||
<div class="card">
|
||||
<div style="background:var(--warning);color:#000;padding:8px 12px;border-radius:8px;margin-bottom:12px;font-size:13px;">
|
||||
⚠️ WebApp лучше открывать через Telegram — там авторизация автоматическая.
|
||||
Здесь можно зайти по email для тестирования.
|
||||
</div>
|
||||
<label>Email</label>
|
||||
<input class="input" id="el-email" type="email" placeholder="you@example.com" autocomplete="email">
|
||||
<label>Пароль (≥8 символов)</label>
|
||||
@ -192,15 +196,14 @@ const App = {
|
||||
<div class="sub" id="el-hint" style="margin-top:8px;text-align:center;">Введите email и пароль</div>
|
||||
</div>
|
||||
<div class="card" style="font-size:12px;color:var(--text-dim);">
|
||||
🍻 BuhApp — приложение для лиц 18+. Встречи и употребление алкоголя — на свой риск.
|
||||
Принимая соглашение, вы подтверждаете совершеннолетие.
|
||||
Регистрируясь, вы подтверждаете совершеннолетие (18+) и принимаете
|
||||
<a href="/legal/TERMS.html" target="_blank">соглашение</a>.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('el-login').onclick = () => this.emailLogin();
|
||||
document.getElementById('el-register').onclick = () => this.emailRegister();
|
||||
document.getElementById('el-register').onclick = () => this.emailRegisterPre();
|
||||
|
||||
// live-валидация
|
||||
const update = () => {
|
||||
const email = document.getElementById('el-email').value.trim();
|
||||
const pass = document.getElementById('el-pass').value;
|
||||
@ -223,13 +226,26 @@ const App = {
|
||||
document.getElementById('el-pass').addEventListener('input', update);
|
||||
},
|
||||
|
||||
// Сначала consent overlay, потом реальная регистрация
|
||||
emailRegisterPre() {
|
||||
const email = document.getElementById('el-email').value.trim();
|
||||
const pass = document.getElementById('el-pass').value;
|
||||
if (!email || pass.length < 8) {
|
||||
TG_APP.showAlert('Email и пароль (≥8 символов) обязательны');
|
||||
return;
|
||||
}
|
||||
// проверим дубликат email
|
||||
this._preReg = () => this.emailRegister();
|
||||
this.showConsent();
|
||||
},
|
||||
|
||||
async emailLogin() {
|
||||
const email = document.getElementById('el-email').value.trim();
|
||||
const pass = document.getElementById('el-pass').value;
|
||||
try {
|
||||
await API.Auth.loginEmail(email, pass);
|
||||
await API.Auth.me();
|
||||
await this.checkConsent();
|
||||
// для email-логина согласия можно проверить, но юзер их уже дал
|
||||
await this.maybeOnboard();
|
||||
this.showView('map');
|
||||
} catch (e) {
|
||||
@ -240,35 +256,46 @@ const App = {
|
||||
async emailRegister() {
|
||||
const email = document.getElementById('el-email').value.trim();
|
||||
const pass = document.getElementById('el-pass').value;
|
||||
if (!email || pass.length < 8) {
|
||||
TG_APP.showAlert('Email и пароль (≥8 символов) обязательны');
|
||||
return;
|
||||
}
|
||||
// disable на время запроса
|
||||
const btn = document.getElementById('el-register');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Регистрация...';
|
||||
// показываем сообщение о процессе
|
||||
const ov = document.createElement('div');
|
||||
ov.id = 'consent-overlay';
|
||||
ov.innerHTML = `
|
||||
<div style="text-align:center;padding:48px 16px;">
|
||||
<div style="font-size:32px;margin-bottom:12px;">⏳</div>
|
||||
<div>Регистрация...</div>
|
||||
</div>
|
||||
`;
|
||||
const target = document.querySelector('body.mobile-mode #mobile-frame') || document.body;
|
||||
target.appendChild(ov);
|
||||
|
||||
try {
|
||||
console.log('registerEmail:', email);
|
||||
const r = await API.Auth.registerEmail({
|
||||
email,
|
||||
password: pass,
|
||||
name: email.split('@')[0],
|
||||
consents: { adult: true, terms: true, privacy: true, disclaimer: true },
|
||||
});
|
||||
console.log('registerEmail result:', r);
|
||||
await API.Auth.me();
|
||||
this.consentGiven = true;
|
||||
ov.remove();
|
||||
await this.maybeOnboard();
|
||||
this.showView('map');
|
||||
} catch (e) {
|
||||
console.error('registerEmail error:', e);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Регистрация';
|
||||
// покажем ошибку явно
|
||||
const msg = e?.message || 'unknown error';
|
||||
if (e?.data) console.error('error data:', e.data);
|
||||
TG_APP.showAlert('Ошибка регистрации: ' + msg + '\n\nОткрой DevTools (F12) → Console для подробностей.');
|
||||
ov.remove();
|
||||
TG_APP.showAlert('Ошибка регистрации: ' + (e?.message || 'unknown'));
|
||||
// вернёмся к форме
|
||||
this.showEmailLogin();
|
||||
}
|
||||
},
|
||||
|
||||
async maybeOnboard() {
|
||||
if (this._onboardRunning) return;
|
||||
this._onboardRunning = true;
|
||||
try {
|
||||
if (!window.OnboardingView) return;
|
||||
if (OnboardingView.isDone()) return;
|
||||
await OnboardingView.render(document.body);
|
||||
} finally {
|
||||
this._onboardRunning = false;
|
||||
}
|
||||
},
|
||||
|
||||
@ -315,24 +342,5 @@ const App = {
|
||||
// helpers
|
||||
function escapeHtml(s) {
|
||||
if (!s) return '';
|
||||
return String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
|
||||
function formatTime(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
const today = new Date();
|
||||
if (d.toDateString() === today.toDateString()) {
|
||||
return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0');
|
||||
}
|
||||
return d.getDate().toString().padStart(2, '0') + '.' + (d.getMonth() + 1).toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return d.getDate().toString().padStart(2, '0') + '.' + (d.getMonth() + 1).toString().padStart(2, '0') + '.' + d.getFullYear();
|
||||
}
|
||||
|
||||
window.App = App;
|
||||
document.addEventListener('DOMContentLoaded', () => App.init());
|
||||
Loading…
Reference in New Issue
Block a user