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.desc' | translate }}
+
+
+
+
{{ 'auth.checking' | translate }}
+
+
+
+
+
+
+
{{ 'auth.qr_hint' | translate }}
+
+
+
+
+
![]()
+
+
+
+
+
{{ (messageKey() || 'auth.expired') | translate }}
+
+
+
+
{{ 'auth.redirect_note' | 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()) {
-
-
-
-
- @if (paid()) {
-
-
- @if (loginOnly()) {
-
{{ 'fastcheck.modal_loggedin_title' | translate }}
-
{{ 'fastcheck.modal_loggedin_sub' | translate }}
- } @else {
-
{{ 'fastcheck.modal_paid_title' | translate }}
-
- fastCHECK
- {{ 'fastcheck.modal_paid_sub' | translate }}
-
- }
-
- } @else {
-

-
{{ 'fastcheck.modal_title' | translate }}
-
{{ 'fastcheck.modal_sub' | translate }}
-
- @if (popupLoading() && !webSessionId()) {
-
{{ 'fastcheck.modal_loading' | translate }}
- }
-
- @if (webSessionId() && !isMobile) {
-
![QR Telegram]()
- }
-
- @if (webSessionId()) {
-
-
- {{ 'fastcheck.modal_open_tg' | translate }}
-
- }
-
- @if (popupLoading() && webSessionId()) {
-
{{ 'fastcheck.modal_confirming' | translate }}
- } @else if (webSessionId()) {
-
{{ 'fastcheck.modal_waiting' | translate }}
- }
-
- @if (popupError()) {
-
{{ popupError() }}
- }
- }
-
-
-}
+
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' })