diff --git a/public/flags/cn.svg b/public/flags/cn.svg new file mode 100644 index 0000000..cdef061 --- /dev/null +++ b/public/flags/cn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/i18n/cn.json b/public/i18n/cn.json new file mode 100644 index 0000000..ea708c5 --- /dev/null +++ b/public/i18n/cn.json @@ -0,0 +1,147 @@ +{ + "header": { + "nav_about": "关于服务", + "nav_contacts": "联系方式", + "nav_partners": "合作伙伴", + "nav_support": "支持", + "aria_nav": "导航", + "aria_menu": "移动菜单", + "aria_burger": "菜单", + "aria_close": "关闭菜单" + }, + "footer": { + "desc": "面向个人的创新型虚拟支票服务。在线创建数字支票,并可通过合作银行的 ATM 24/7 提现。", + "contacts_heading": "联系方式", + "russia": "俄罗斯", + "armenia": "亚美尼亚", + "support_label": "技术支持", + "support_hours": "24/7", + "questions_label": "咨询", + "questions_hours": "10:00–19:00 MSK", + "legal_heading": "法律信息", + "legal_company": "LLC «VIAEXPORT»", + "legal_inn_ru": "税号(RU):9909675800", + "legal_inn_am": "税号(AM):01051049", + "legal_kpp": "KPP:770287001", + "legal_ogrn": "OGRN:282.110.1296681", + "legal_address": "亚美尼亚,0201,埃里温,明斯卡亚街 21-23 号,44 室", + "rights": "LLC «VIAEXPORT». 保留所有权利。", + "director": "董事:Amirkhanyan Sargis Artashesovich" + }, + "fastcheck": { + "subtitle": "输入 fastCHECK 信息或创建新的", + "number_label": "fastCHECK 编号", + "number_placeholder": "123456-123456-123456", + "number_new": "新建", + "amount_label": "金额", + "amount_checking": "正在检查…", + "code_label": "验证码", + "code_placeholder": "000000", + "pay_btn": "支付", + "modal_title": "通过 Telegram 登录", + "modal_sub": "扫描二维码或打开链接", + "modal_loading": "加载中…", + "modal_open_tg": "在 Telegram 中打开", + "modal_confirming": "正在确认支付…", + "modal_waiting": "等待登录…", + "modal_paid_title": "已支付", + "modal_paid_sub": "fastCHECK 已成功接收。", + "share_email": "通过电子邮件发送", + "share_tg": "发送到 Telegram", + "modal_loggedin_title": "已登录", + "modal_loggedin_sub": "您已登录 fastCHECK。" + }, + "auth": { + "close_aria": "关闭对话框", + "title": "需要登录", + "desc": "请通过 Telegram 登录以继续您的订单。", + "checking": "检查中...", + "telegram_btn": "使用 Telegram 登录", + "qr_hint": "或扫描二维码", + "qr_alt": "二维码", + "refresh_aria": "刷新二维码", + "expired": "二维码已过期。点击刷新", + "redirect_note": "登录后您将返回此页面。", + "session_failed": "无法启动 Telegram 登录。点击重试。" + }, + "create": { + "title": "新建", + "subtitle": "输入充值金额", + "back_label": "返回", + "payment_label": "支付方式", + "currency_label": "货币", + "amount_label": "支付金额", + "note_label": "备注", + "note_placeholder": "付款原因...", + "creating": "创建中…", + "create_btn": "创建", + "amount_hint": "允许金额:", + "qr_label": "扫描二维码支付", + "qr_waiting": "等待支付确认…" + }, + "sbp": { + "title": "通过 SBP 支付", + "subtitle": "快速支付系统", + "amount_label": "支付金额", + "currency_name": "俄罗斯卢布", + "note_label": "备注", + "note_placeholder": "付款原因...", + "pay_loading": "请稍候...", + "pay_btn": "前往支付" + }, + "about": { + "title": "关于服务", + "lead": "fastCHECK 是一项面向个人、全年无休的创新型虚拟支票服务。", + "what_title": "什么是 fastCHECK?", + "what_text": "fastCHECK 是一种数字支票,您可以在线创建,并可在合作银行的 ATM 上随时提现。无需排队,无需前往办公室,只需您的手机和最近的 ATM。", + "how_title": "如何运作?", + "step1": "登录并创建一张所需金额的 fastCHECK。", + "step2": "保存支票编号和 5 位代码。", + "step3": "在网站上输入信息并通过 Telegram 确认。", + "step4": "以您方便的方式收款。", + "why_title": "为什么选择 fastCHECK?", + "why1": "24/7 可用,包括周末和节假日。", + "why2": "通过 Telegram 进行安全授权。", + "why3": "支持 SBP 和其他常用支付方式。", + "why4": "处理速度快,从几秒到几分钟。", + "company_title": "关于公司", + "company_text": "该服务由 LLC «VIAEXPORT»(税号 9909675800)开发。公司注册于俄罗斯和亚美尼亚。法律地址:亚美尼亚,0201,埃里温,明斯卡亚街 21-23 号,44 室。" + }, + "contacts": { + "title": "联系方式", + "lead": "我们 24/7 在线。请选择您偏好的联系方式。", + "ru_label": "电话 — 俄罗斯", + "am_label": "电话 — 亚美尼亚", + "email_label": "电子邮箱", + "tg_label": "Telegram 机器人", + "hours_title": "工作时间" + }, + "errors": { + "not_found": "未找到支付或已过期。", + "lookup_failed": "无法验证编号。请重试。", + "session_failed": "无法创建会话。请重试。", + "payment_failed": "无法处理付款。请检查验证码后重试。", + "invalid_code": "验证码无效。请检查后重试。", + "invalid_amount": "请输入有效金额。", + "settings_failed": "无法加载设置。您可以手动继续。", + "settings_missing_id": "缺少合作伙伴 ID。您可以手动继续。" + }, + "common": { + "secure": "安全连接" + }, + "partners": { + "title": "合作伙伴", + "lead": "接受 fastCHECK 作为支付方式的商店、服务和公司。", + "cat_finance": "金融", + "cat_retail": "零售", + "cat_hotels": "酒店", + "cat_services": "服务", + "p1_desc": "覆盖整个亚美尼亚的货币兑换与转账服务。", + "p2_desc": "支持使用 fastCHECK 为账户充值的外汇经纪商。", + "p3_desc": "覆盖俄罗斯和独联体地区配送的在线零售商。", + "p4_desc": "通过 fastCHECK 进行酒店预订和支付。", + "cta_title": "想成为合作伙伴吗?", + "cta_text": "将 fastCHECK 接入您的业务,快速且手续最少。", + "cta_btn": "联系我们" + } +} \ No newline at end of file diff --git a/public/i18n/en.json b/public/i18n/en.json index 701139d..bba829d 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -51,6 +51,19 @@ "modal_loggedin_title": "Signed in", "modal_loggedin_sub": "You are now signed in to fastCHECK." }, + "auth": { + "close_aria": "Close dialog", + "title": "Login required", + "desc": "Please log in via Telegram to proceed with your order.", + "checking": "Checking...", + "telegram_btn": "Log in with Telegram", + "qr_hint": "Or scan the QR code", + "qr_alt": "QR code", + "refresh_aria": "Refresh QR code", + "expired": "QR code expired. Click to refresh", + "redirect_note": "You will be redirected back after login.", + "session_failed": "Unable to start Telegram login. Click to retry." + }, "create": { "title": "New", "subtitle": "Enter the amount to top up", diff --git a/public/i18n/hy.json b/public/i18n/hy.json index ef8a880..d5aad80 100644 --- a/public/i18n/hy.json +++ b/public/i18n/hy.json @@ -51,6 +51,19 @@ "modal_loggedin_title": "Մուտք գործել", "modal_loggedin_sub": "Դուք մուտք գործել եք fastCHECK:" }, + "auth": { + "close_aria": "Փակել պատուհանը", + "title": "Պահանջվում է մուտք", + "desc": "Խնդրում ենք մուտք գործել Telegram-ի միջոցով՝ պատվերը շարունակելու համար:", + "checking": "Ստուգվում է…", + "telegram_btn": "Մուտք գործել Telegram-ով", + "qr_hint": "Կամ սկանավորեք QR կոդը", + "qr_alt": "QR կոդ", + "refresh_aria": "Թարմացնել QR կոդը", + "expired": "QR կոդը ժամկետանց է։ Սեղմեք թարմացնելու համար", + "redirect_note": "Մուտքից հետո դուք կվերադառնաք այս էջ։", + "session_failed": "Չհաջողվեց սկսել Telegram մուտքը։ Սեղմեք՝ կրկին փորձելու համար։" + }, "create": { "title": "Նոր", "subtitle": "Նշեք համալրման գումարը", diff --git a/public/i18n/ru.json b/public/i18n/ru.json index ce46cd1..20ab7e8 100644 --- a/public/i18n/ru.json +++ b/public/i18n/ru.json @@ -51,6 +51,19 @@ "modal_loggedin_title": "Вы вошли", "modal_loggedin_sub": "Вы авторизованы в fastCHECK." }, + "auth": { + "close_aria": "Закрыть диалог", + "title": "Требуется вход", + "desc": "Пожалуйста, войдите через Telegram, чтобы продолжить оформление заказа.", + "checking": "Проверяем...", + "telegram_btn": "Войти через Telegram", + "qr_hint": "Или отсканируйте QR-код", + "qr_alt": "QR-код", + "refresh_aria": "Обновить QR-код", + "expired": "Срок действия QR-кода истёк. Нажмите, чтобы обновить", + "redirect_note": "После входа вы вернётесь обратно.", + "session_failed": "Не удалось запустить вход через Telegram. Нажмите, чтобы повторить." + }, "create": { "title": "Новый", "subtitle": "Укажите сумму для пополнения", diff --git a/src/app/auth-dialog/auth-dialog.html b/src/app/auth-dialog/auth-dialog.html new file mode 100644 index 0000000..56ed28c --- /dev/null +++ b/src/app/auth-dialog/auth-dialog.html @@ -0,0 +1,66 @@ +@if (open()) { +
+
+ + +
+ + +

{{ 'auth.title' | translate }}

+ + + + +
+ + +
+

{{ 'auth.qr_hint' | translate }}

+ +
+
+
+ +
+ +
+ +
+ + + + + {{ (messageKey() || 'auth.expired') | translate }} +
+
+ + +
+
+
+
+} \ No newline at end of file diff --git a/src/app/auth-dialog/auth-dialog.scss b/src/app/auth-dialog/auth-dialog.scss new file mode 100644 index 0000000..578eeb8 --- /dev/null +++ b/src/app/auth-dialog/auth-dialog.scss @@ -0,0 +1,280 @@ +:host { + --bg-card: #ffffff; + --bg-hover: #f0f0f0; + --text-primary: #1a1a1a; + --text-secondary: #666666; + --accent-color: #497671; + --accent-light: rgba(73, 118, 113, 0.1); + --telegram: #2aabee; + --telegram-hover: #229ed9; + --border: #e8e8e8; + --shadow: 0 20px 60px rgba(0, 0, 0, 0.15); +} + +.login-overlay { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.2s ease; + padding: 16px; + overflow-y: auto; +} + +.login-dialog { + position: relative; + background: var(--bg-card); + border-radius: 20px; + padding: 32px 28px; + max-width: 400px; + width: 100%; + max-height: calc(100dvh - 32px); + overflow-y: auto; + text-align: center; + box-shadow: var(--shadow); + animation: scaleIn 0.25s ease; +} + +.close-btn { + position: absolute; + top: 12px; + right: 12px; + width: 32px; + height: 32px; + border: none; + border-radius: 50%; + background: var(--bg-hover); + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.close-btn:hover { + background: #e0e0e0; + color: #333; +} + +.login-icon { + margin: 0 auto 16px; + width: 72px; + height: 72px; + border-radius: 50%; + background: var(--accent-light); + color: var(--accent-color); + display: flex; + align-items: center; + justify-content: center; +} + +h2 { + margin: 0 0 8px; + font-size: 20px; + font-weight: 700; + color: var(--text-primary); +} + +.login-desc { + margin: 0 0 24px; + font-size: 14px; + color: var(--text-secondary); + line-height: 1.5; +} + +.telegram-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + padding: 14px 24px; + border: none; + border-radius: 12px; + background: var(--telegram); + color: #fff; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.telegram-btn:hover { + background: var(--telegram-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3); +} + +.telegram-btn:active { + transform: translateY(0); +} + +.tg-icon { + flex-shrink: 0; +} + +.qr-section { + margin-top: 20px; +} + +.qr-hint { + margin: 0 0 12px; + font-size: 13px; + color: #999; +} + +.qr-container { + display: inline-flex; + padding: 12px; + background: #fff; + border-radius: 12px; + border: 1px solid var(--border); +} + +.qr-container img { + display: block; + border-radius: 4px; +} + +.qr-loading { + align-items: center; + justify-content: center; + width: 204px; + height: 204px; +} + +.qr-loading .spinner, +.login-status .spinner { + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.qr-loading .spinner { + width: 32px; + height: 32px; + border: 3px solid #e0e0e0; + border-top-color: var(--accent-color); +} + +.qr-expired { + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + width: 204px; + height: 204px; + cursor: pointer; + color: #999; + transition: color 0.2s ease; +} + +.qr-expired:hover { + color: var(--accent-color); +} + +.qr-expired span { + font-size: 13px; +} + +.login-note { + margin: 16px 0 0; + font-size: 12px; + color: #999; + line-height: 1.4; +} + +.login-status { + display: none; + align-items: center; + justify-content: center; + gap: 10px; + padding: 16px; + color: var(--text-secondary); + font-size: 14px; +} + +.login-status .spinner { + width: 20px; + height: 20px; + border: 2px solid #e0e0e0; + border-top-color: var(--accent-color); +} + +.dialog-content[data-state='checking'] .login-status { + display: flex; +} + +.dialog-content[data-state='checking'] .action-block, +.dialog-content[data-state='loading'] .qr-ready, +.dialog-content[data-state='loading'] .qr-expired, +.dialog-content[data-state='expired'] .qr-ready, +.dialog-content[data-state='expired'] .qr-loading, +.dialog-content[data-state='error'] .qr-loading, +.dialog-content[data-state='checking'] .qr-section, +.dialog-content[data-state='checking'] .login-note { + display: none; +} + +.dialog-content[data-state='ready'] .qr-loading, +.dialog-content[data-state='ready'] .qr-expired, +.dialog-content[data-state='expired'] .qr-loading, +.dialog-content[data-state='error'] .qr-loading { + display: none; +} + +.dialog-content[data-state='error'] .qr-ready, +.dialog-content[data-state='error'] .qr-expired { + display: inline-flex; +} + +.metadata { + margin-top: 22px; + padding-top: 18px; + border-top: 1px solid #e9edf2; + font-size: 13px; + color: var(--text-secondary); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@media (max-width: 480px) { + .login-dialog { + padding: 24px 20px; + border-radius: 16px; + max-height: calc(100dvh - 24px); + } + + .qr-container img { + width: 140px; + height: 140px; + } + + .qr-loading, + .qr-expired { + width: 164px; + height: 164px; + } +} \ No newline at end of file diff --git a/src/app/auth-dialog/auth-dialog.ts b/src/app/auth-dialog/auth-dialog.ts new file mode 100644 index 0000000..68c2ce5 --- /dev/null +++ b/src/app/auth-dialog/auth-dialog.ts @@ -0,0 +1,248 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, computed, effect, inject, input, output, signal } from '@angular/core'; +import { FASTCHECK_API } from '../api'; +import { TranslatePipe } from '../translate/translate.pipe'; + +interface WebSessionResponse { + sessionId: string; + userId: string; + expires: string; + userSessionId: string; + Status: boolean; +} + +export type AuthDialogMode = 'payment' | 'login' | 'new'; + +export interface AuthDialogAuthorizedEvent { + sessionId: string; + userId: string; + userSessionId: string; +} + +type AuthDialogState = 'loading' | 'ready' | 'checking' | 'expired' | 'error'; + +@Component({ + selector: 'app-auth-dialog', + imports: [TranslatePipe], + templateUrl: './auth-dialog.html', + styleUrl: './auth-dialog.scss' +}) +export class AuthDialogComponent { + private readonly http = inject(HttpClient); + private readonly telegramBot = 'DexarSupport_bot'; + private readonly sessionStorageKey = 'fc_session'; + private readonly maxPollAttempts = 100; + + open = input(false); + mode = input('login'); + processing = input(false); + + authorized = output(); + closed = output(); + + state = signal('loading'); + webSessionId = signal(''); + messageKey = signal(''); + + telegramLink = computed(() => { + const sessionId = this.webSessionId(); + return sessionId + ? `https://t.me/${this.telegramBot}?start=${encodeURIComponent(sessionId)}` + : `https://t.me/${this.telegramBot}`; + }); + + qrUrl = computed(() => { + const link = this.telegramLink(); + return `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(link)}`; + }); + + get isMobile(): boolean { + return typeof window !== 'undefined' && window.innerWidth < 768; + } + + private pollHandle: ReturnType | null = null; + private pollAttempts = 0; + private wasOpen = false; + private authenticated = false; + + constructor() { + effect(() => { + const isOpen = this.open(); + + if (isOpen && !this.wasOpen) { + this.wasOpen = true; + this.startAuthFlow(); + return; + } + + if (!isOpen && this.wasOpen) { + this.wasOpen = false; + this.finishFlow(); + } + }); + + effect(() => { + if (!this.open()) return; + if (!this.processing()) return; + this.state.set('checking'); + }); + } + + requestClose(): void { + this.closed.emit(); + } + + openTelegram(): void { + const link = this.telegramLink(); + if (!link) return; + window.open(link, '_blank', 'noopener'); + } + + refreshQr(): void { + this.cleanupSession(false); + this.startAuthFlow(); + } + + private startAuthFlow(): void { + this.stopPolling(); + this.authenticated = false; + this.pollAttempts = 0; + this.messageKey.set(''); + this.webSessionId.set(''); + this.state.set('checking'); + + const existingSession = localStorage.getItem(this.sessionStorageKey) ?? ''; + if (existingSession) { + this.checkExistingSession(existingSession); + return; + } + + this.createSession(); + } + + private checkExistingSession(sessionId: string): void { + this.http.get(`${FASTCHECK_API}/websession/${sessionId}`).subscribe({ + next: (response) => { + if (response?.Status) { + this.webSessionId.set(sessionId); + this.handleAuthorized(response, sessionId); + return; + } + + localStorage.removeItem(this.sessionStorageKey); + this.createSession(); + }, + error: () => { + localStorage.removeItem(this.sessionStorageKey); + this.createSession(); + } + }); + } + + private createSession(): void { + this.state.set('loading'); + this.http.get(`${FASTCHECK_API}/websession`).subscribe({ + next: (response) => { + const sessionId = response?.sessionId ?? ''; + if (!sessionId) { + this.messageKey.set('auth.session_failed'); + this.state.set('error'); + return; + } + + this.webSessionId.set(sessionId); + + if (this.isMobile) { + this.state.set('checking'); + window.location.href = this.telegramLink(); + this.startPolling(sessionId); + return; + } + + this.state.set('ready'); + this.startPolling(sessionId); + }, + error: () => { + this.messageKey.set('auth.session_failed'); + this.state.set('error'); + } + }); + } + + private startPolling(sessionId: string): void { + this.stopPolling(); + this.pollAttempts = 0; + this.pollHandle = setInterval(() => { + this.pollAttempts += 1; + if (this.pollAttempts >= this.maxPollAttempts) { + this.stopPolling(); + this.messageKey.set('auth.expired'); + this.state.set('expired'); + return; + } + + this.http.get(`${FASTCHECK_API}/websession/${sessionId}`).subscribe({ + next: (response) => { + if (!response?.Status) return; + this.handleAuthorized(response, sessionId); + }, + error: () => undefined + }); + }, 5000); + } + + private handleAuthorized(response: WebSessionResponse, sessionId: string): void { + if (this.authenticated) return; + + this.authenticated = true; + this.stopPolling(); + this.webSessionId.set(sessionId); + this.state.set('checking'); + this.authorized.emit({ + sessionId, + userId: response.userId ?? '', + userSessionId: response.userSessionId ?? '' + }); + } + + private finishFlow(): void { + const shouldPersistSession = this.authenticated && (this.mode() === 'login' || this.mode() === 'new'); + this.cleanupSession(shouldPersistSession); + this.messageKey.set(''); + this.webSessionId.set(''); + this.state.set('loading'); + this.authenticated = false; + this.pollAttempts = 0; + } + + private cleanupSession(persistSession: boolean): void { + this.stopPolling(); + + const sessionId = this.webSessionId(); + if (!sessionId) { + if (!persistSession) { + localStorage.removeItem(this.sessionStorageKey); + } + return; + } + + if (persistSession) { + localStorage.setItem(this.sessionStorageKey, sessionId); + return; + } + + this.http + .request('DELETE', `${FASTCHECK_API}/websession/${sessionId}`, { + body: { sessionId } + }) + .subscribe({ error: () => undefined }); + + localStorage.removeItem(this.sessionStorageKey); + } + + private stopPolling(): void { + if (this.pollHandle === null) return; + clearInterval(this.pollHandle); + this.pollHandle = null; + } +} \ No newline at end of file diff --git a/src/app/pages/fastcheck-page/fastcheck-page.html b/src/app/pages/fastcheck-page/fastcheck-page.html index 2beb3da..f5f301f 100644 --- a/src/app/pages/fastcheck-page/fastcheck-page.html +++ b/src/app/pages/fastcheck-page/fastcheck-page.html @@ -118,62 +118,10 @@ - -@if (popupOpen()) { - -} + diff --git a/src/app/pages/fastcheck-page/fastcheck-page.scss b/src/app/pages/fastcheck-page/fastcheck-page.scss index 425193b..b4bce0a 100644 --- a/src/app/pages/fastcheck-page/fastcheck-page.scss +++ b/src/app/pages/fastcheck-page/fastcheck-page.scss @@ -93,164 +93,3 @@ background: #fff; } } - -// ─── Modal (Telegram QR popup) ────────────────────────────────────────────── -.modal { - position: fixed; - inset: 0; - z-index: 1000; - background: rgba(15, 23, 42, .55); - backdrop-filter: blur(6px); - display: flex; - align-items: center; - justify-content: center; - padding: 16px; - animation: fade-in .15s ease-out; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - - @media (max-width: 480px) { - align-items: stretch; - padding: 0; - } - - &__card { - position: relative; - background: #fff; - border-radius: 24px; - width: 100%; - max-width: 360px; - padding: 28px 24px 24px; - text-align: center; - box-shadow: 0 24px 60px rgba(0,0,0,.25); - animation: pop-in .2s ease-out; - margin: auto; - - @media (max-width: 480px) { - max-width: 100%; - border-radius: 0; - box-shadow: none; - padding: calc(28px + env(safe-area-inset-top)) 20px calc(28px + env(safe-area-inset-bottom)); - margin: 0; - min-height: 100dvh; - display: flex; - flex-direction: column; - justify-content: center; - } - } - - &__close { - position: absolute; - top: 8px; - right: 8px; - width: 44px; - height: 44px; - border-radius: 50%; - border: none; - background: #f1f5f9; - color: #475569; - font-size: 24px; - line-height: 1; - cursor: pointer; - font-family: inherit; - transition: background .15s; - appearance: none; - -webkit-appearance: none; - - &:hover { background: #e2e8f0; } - } - - &__title { - font-size: 20px; - font-weight: 700; - color: #0f172a; - margin: 4px 0 6px; - } - - &__sub { - font-size: 14px; - color: #64748b; - margin: 0 0 18px; - } - - &__hint { - font-size: 13px; - color: #94a3b8; - margin: 14px 0 0; - } - - &__error { - font-size: 13px; - color: #ef4444; - font-weight: 500; - margin: 12px 0 0; - } - - &__success { - padding: 12px 0 4px; - - svg { display: block; margin: 0 auto 10px; } - } -} - -.qr { - display: flex; - align-items: center; - justify-content: center; - background: #f8fafc; - border: 2px solid #e2e8f0; - border-radius: 16px; - padding: 12px; - width: 264px; - height: 264px; - max-width: 100%; - margin: 0 auto; - - @media (max-width: 380px) { - width: min(264px, 70vw); - height: auto; - aspect-ratio: 1; - } - - &__placeholder { - color: #94a3b8; - font-size: 14px; - } - - img { - width: 100%; - height: auto; - max-width: 240px; - display: block; - } -} - -.tg-link { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - margin-top: 16px; - padding: 14px 22px; - min-height: 48px; - border-radius: 12px; - background: #229ED9; - color: #fff; - font-size: 15px; - font-weight: 700; - text-decoration: none; - transition: opacity .15s; - - &:hover { opacity: .9; } - &:active { transform: scale(.97); } -} - -@keyframes fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes pop-in { - from { transform: translateY(12px) scale(.98); opacity: 0; } - to { transform: translateY(0) scale(1); opacity: 1; } -} diff --git a/src/app/pages/fastcheck-page/fastcheck-page.ts b/src/app/pages/fastcheck-page/fastcheck-page.ts index 4e0c283..5f8a684 100644 --- a/src/app/pages/fastcheck-page/fastcheck-page.ts +++ b/src/app/pages/fastcheck-page/fastcheck-page.ts @@ -3,6 +3,7 @@ import { FormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { FastcheckService } from '../../fastcheck.service'; import { API_VITANOVA_NETWORK, FASTCHECK_API, FASTCHECK_STORE_API, QR_VITANOVA_API } from '../../api'; +import { AuthDialogAuthorizedEvent, AuthDialogComponent, AuthDialogMode } from '../../auth-dialog/auth-dialog'; import { TranslatePipe } from '../../translate/translate.pipe'; import { TranslationService } from '../../translate/translation.service'; @@ -46,7 +47,7 @@ interface SettingsResponse { @Component({ selector: 'app-fastcheck-page', - imports: [FormsModule, TranslatePipe], + imports: [FormsModule, TranslatePipe, AuthDialogComponent], templateUrl: './fastcheck-page.html', styleUrl: './fastcheck-page.scss' }) @@ -59,9 +60,6 @@ export class FastcheckPage { private t(key: string): string { return this.i18n.translate(key); } - // Telegram bot used for the sign-in deep link. - private readonly telegramBot = 'DexarSupport_bot'; - fastcheckNumber = signal(''); fastcheckAmount = signal(null); fastcheckCode = signal(''); @@ -91,15 +89,9 @@ export class FastcheckPage { telegramId = signal(''); fastcheckCurrency = signal('RUB'); - popupOpen = signal(false); - popupLoading = signal(false); - popupError = signal(''); - webSessionId = signal(''); - paid = signal(false); - loginOnly = signal(false); - isNewFlow = signal(false); - sessionToken = signal(localStorage.getItem('fc_session') ?? ''); - private pollHandle: ReturnType | null = null; + authOpen = signal(false); + authMode = signal('payment'); + authProcessing = signal(false); private lastLookedUpNumber = ''; canPay = computed(() => { @@ -114,22 +106,6 @@ export class FastcheckPage { */ canShare = computed(() => this.canPay()); - telegramLink = computed(() => { - const sid = this.webSessionId(); - return sid - ? `https://t.me/${this.telegramBot}?start=${encodeURIComponent(sid)}` - : `https://t.me/${this.telegramBot}`; - }); - - qrUrl = computed(() => { - const link = this.telegramLink(); - return `https://api.qrserver.com/v1/create-qr-code/?size=240x240&margin=8&data=${encodeURIComponent(link)}`; - }); - - get isMobile(): boolean { - return typeof window !== 'undefined' && window.innerWidth < 768; - } - constructor() { // Pull autofill data: prefer router navigation state, fall back to service. const navState = typeof window !== 'undefined' ? (window.history?.state ?? {}) : {}; @@ -263,7 +239,7 @@ export class FastcheckPage { const status = (res.status ?? '').toUpperCase(); if (status === 'COMPLETED' || status === 'APPROVED') { - this.paid.set(true); + this.error.set(''); } } @@ -276,9 +252,7 @@ export class FastcheckPage { pay(): void { if (!this.canPay()) return; this.error.set(''); - this.loginOnly.set(false); - this.isNewFlow.set(false); - this.openPopup(); + this.openAuth('payment'); } /** @@ -289,7 +263,6 @@ export class FastcheckPage { shareByTelegram(): void { if (!this.canShare()) return; this.error.set(''); - this.isNewFlow.set(false); const tg = this.telegramId(); if (tg) { @@ -297,16 +270,14 @@ export class FastcheckPage { return; } - // No telegramID yet — trigger identification via Telegram-bot. - this.loginOnly.set(true); - this.openPopup(); + this.openAuth('login'); } createNewFastcheck(event: Event): void { event.preventDefault(); const id = this.partnerId() || this.defaultPartnerId; - const sessionId = this.sessionToken(); + const sessionId = localStorage.getItem('fc_session') ?? ''; const headers: Record = { Authorization: JSON.stringify({ sessionID: sessionId, partnerID: id }) }; @@ -316,26 +287,20 @@ export class FastcheckPage { .subscribe({ next: () => { // Authorized partner: skip Telegram auth popup and go directly. - this.isNewFlow.set(true); - this.loginOnly.set(false); - this.doRedirectToNew(); + this.doRedirectToNew(sessionId); }, error: () => { // Not authorized: force fresh Telegram auth QR popup. - this.sessionToken.set(''); localStorage.removeItem('fc_session'); - this.isNewFlow.set(true); - this.loginOnly.set(false); - this.openPopup(); + this.openAuth('new'); } }); } - private doRedirectToNew(): void { - const tok = this.webSessionId(); + private doRedirectToNew(sessionId?: string): void { + const tok = sessionId || localStorage.getItem('fc_session') || ''; if (tok) { localStorage.setItem('fc_session', tok); - this.sessionToken.set(tok); } window.location.href = this.newQrUrl(); } @@ -352,126 +317,56 @@ export class FastcheckPage { }; this.http.post(url, body).subscribe({ next: () => { - this.popupOpen.set(true); - this.paid.set(true); - this.loginOnly.set(true); + this.authProcessing.set(false); + this.authOpen.set(false); }, error: () => { + this.authProcessing.set(false); + this.authOpen.set(false); this.error.set(this.t('errors.payment_failed')); } }); } - private openPopup(): void { - this.popupOpen.set(true); - this.popupError.set(''); - this.paid.set(false); - this.popupLoading.set(true); + private openAuth(mode: AuthDialogMode): void { + this.authMode.set(mode); + this.authProcessing.set(false); + this.authOpen.set(true); + } - const existing = this.sessionToken(); - if (existing) { - this.http.get(`${FASTCHECK_API}/websession/${existing}`).subscribe({ - next: (res) => { - if (res?.Status) { - this.popupLoading.set(false); - this.webSessionId.set(existing); - if (this.isNewFlow()) { - this.doRedirectToNew(); - } else if (this.loginOnly()) { - this.paid.set(true); - } else { - this.acceptFastcheck(existing); - } - } else { - this.sessionToken.set(''); - this.createNewSession(); - } - }, - error: () => { this.sessionToken.set(''); this.createNewSession(); } - }); + onAuthClosed(): void { + this.authProcessing.set(false); + this.authOpen.set(false); + } + + onAuthAuthorized(event: AuthDialogAuthorizedEvent): void { + this.authProcessing.set(true); + + if (this.authMode() === 'new') { + this.authProcessing.set(false); + this.authOpen.set(false); + this.doRedirectToNew(event.sessionId); return; } - this.createNewSession(); - } - - private createNewSession(): void { - this.http.get(`${FASTCHECK_API}/websession`).subscribe({ - next: (res) => { - this.popupLoading.set(false); - this.webSessionId.set(res.sessionId); - if (this.isMobile) { - window.location.href = `https://t.me/${this.telegramBot}?start=${encodeURIComponent(res.sessionId)}`; - } else { - this.startPolling(res.sessionId); - } - }, - error: () => { - this.popupLoading.set(false); - this.popupError.set(this.t('errors.session_failed')); + if (this.authMode() === 'login') { + const tg = event.userId || event.userSessionId || ''; + if (!tg) { + this.authProcessing.set(false); + this.authOpen.set(false); + this.error.set(this.t('errors.payment_failed')); + return; } - }); - } - closePopup(): void { - this.popupOpen.set(false); - this.stopPolling(); - if ((this.loginOnly() || this.isNewFlow()) && this.paid()) { - // Keep session alive — user is logged in, preserve token for next action. - const tok = this.webSessionId(); - localStorage.setItem('fc_session', tok); - this.sessionToken.set(tok); - } else if (this.webSessionId()) { - // Best-effort logout; ignore errors. - this.http - .request('DELETE', `${FASTCHECK_API}/websession/${this.webSessionId()}`, { - body: { sessionId: this.webSessionId() } - }) - .subscribe({ error: () => undefined }); - localStorage.removeItem('fc_session'); - this.sessionToken.set(''); + this.telegramId.set(tg); + this.sendFastcheckToTelegram(tg); + return; } - this.webSessionId.set(''); - } - private startPolling(sessionId: string): void { - this.stopPolling(); - this.pollHandle = setInterval(() => { - this.http - .get(`${FASTCHECK_API}/websession/${sessionId}`) - .subscribe({ - next: (res) => { - if (res?.Status) { - this.stopPolling(); - if (this.isNewFlow()) { - this.doRedirectToNew(); - } else if (this.loginOnly()) { - // Identified — use userId as telegramID and send the fastcheck. - const tg = res.userId || res.userSessionId || ''; - if (tg) { - this.telegramId.set(tg); - this.sendFastcheckToTelegram(tg); - } - this.paid.set(true); - } else { - this.acceptFastcheck(sessionId); - } - } - }, - error: () => undefined - }); - }, 3000); - } - - private stopPolling(): void { - if (this.pollHandle !== null) { - clearInterval(this.pollHandle); - this.pollHandle = null; - } + this.acceptFastcheck(event.sessionId); } private acceptFastcheck(sessionId: string): void { - this.popupLoading.set(true); this.http .post( `${FASTCHECK_API}/fastcheck`, @@ -480,8 +375,8 @@ export class FastcheckPage { ) .subscribe({ next: () => { - this.popupLoading.set(false); - this.paid.set(true); + this.authProcessing.set(false); + this.authOpen.set(false); // Fire DELETE to mark fastcheck as consumed on the merchant side. this.http .delete(`${FASTCHECK_API}/fastcheck/${encodeURIComponent(this.fastcheckNumber())}`) @@ -489,8 +384,9 @@ export class FastcheckPage { this.fireMerchantCallback(); }, error: () => { - this.popupLoading.set(false); - this.popupError.set(this.t('errors.payment_failed')); + this.authProcessing.set(false); + this.authOpen.set(false); + this.error.set(this.t('errors.payment_failed')); } }); } diff --git a/src/app/site-header/site-header.ts b/src/app/site-header/site-header.ts index 7283803..b61926e 100644 --- a/src/app/site-header/site-header.ts +++ b/src/app/site-header/site-header.ts @@ -22,6 +22,7 @@ export class SiteHeader { langs: LangOption[] = [ { code: 'ru', label: 'Русский', flag: '/flags/ru.svg' }, { code: 'en', label: 'English', flag: '/flags/en.svg' }, + { code: 'cn', label: '中文', flag: '/flags/cn.svg' }, { code: 'hy', label: 'Հայերեն', flag: '/flags/arm.svg' }, ]; diff --git a/src/app/translate/translation.service.ts b/src/app/translate/translation.service.ts index c2d6478..82e7b7a 100644 --- a/src/app/translate/translation.service.ts +++ b/src/app/translate/translation.service.ts @@ -1,7 +1,7 @@ import { Injectable, inject, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -export type Lang = 'ru' | 'en' | 'hy'; +export type Lang = 'ru' | 'en' | 'hy' | 'cn'; type Translations = Record>; @Injectable({ providedIn: 'root' })