changes
This commit is contained in:
1
public/flags/cn.svg
Normal file
1
public/flags/cn.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32"><defs><path id="cn-star" d="m0-30 17.634 54.27-46.166-33.54h57.064l-46.166 33.54Z"/></defs><rect x="1" y="4" width="30" height="24" rx="4" ry="4" fill="#ee1c25"/><g fill="#ffde00"><g transform="translate(8.3 10.8) scale(.11)"><use xlink:href="#cn-star"/></g><g transform="translate(13.8 7.4) rotate(20) scale(.043)"><use xlink:href="#cn-star"/></g><g transform="translate(16.1 10.1) rotate(40) scale(.043)"><use xlink:href="#cn-star"/></g><g transform="translate(15.7 13.5) scale(.043)"><use xlink:href="#cn-star"/></g><g transform="translate(13.2 16) rotate(-20) scale(.043)"><use xlink:href="#cn-star"/></g></g><path d="M27,4H5c-2.209,0-4,1.791-4,4V24c0,2.209,1.791,4,4,4H27c2.209,0,4-1.791,4-4V8c0-2.209-1.791-4-4-4Zm3,20c0,1.654-1.346,3-3,3H5c-1.654,0-3-1.346-3-3V8c0-1.654,1.346-3,3-3H27c1.654,0,3,1.346,3,3V24Z" opacity=".15"/><path d="M27,5H5c-1.657,0-3,1.343-3,3v1c0-1.657,1.343-3,3-3H27c1.657,0,3,1.343,3,3v-1c0-1.657-1.343-3-3-3Z" fill="#fff" opacity=".2"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
147
public/i18n/cn.json
Normal file
147
public/i18n/cn.json
Normal file
@@ -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": "联系我们"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Նշեք համալրման գումարը",
|
||||
|
||||
@@ -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": "Укажите сумму для пополнения",
|
||||
|
||||
66
src/app/auth-dialog/auth-dialog.html
Normal file
66
src/app/auth-dialog/auth-dialog.html
Normal file
@@ -0,0 +1,66 @@
|
||||
@if (open()) {
|
||||
<div class="login-overlay" (click)="requestClose()">
|
||||
<div class="login-dialog" (click)="$event.stopPropagation()">
|
||||
<button class="close-btn" type="button" [attr.aria-label]="'auth.close_aria' | translate" (click)="requestClose()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="dialog-content" [attr.data-state]="state()">
|
||||
<div class="login-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2>{{ 'auth.title' | translate }}</h2>
|
||||
<p class="login-desc">{{ 'auth.desc' | translate }}</p>
|
||||
|
||||
<div class="login-status">
|
||||
<div class="spinner"></div>
|
||||
<span>{{ 'auth.checking' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="action-block">
|
||||
<button class="telegram-btn" type="button" (click)="openTelegram()">
|
||||
<svg class="tg-icon" width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"></path>
|
||||
</svg>
|
||||
{{ 'auth.telegram_btn' | translate }}
|
||||
</button>
|
||||
|
||||
<div class="qr-section">
|
||||
<p class="qr-hint">{{ 'auth.qr_hint' | translate }}</p>
|
||||
|
||||
<div class="qr-container qr-loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
||||
<div class="qr-container qr-ready">
|
||||
<img [src]="qrUrl()" [attr.alt]="'auth.qr_alt' | translate" width="180" height="180" loading="eager" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="qr-container qr-expired"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
[attr.aria-label]="'auth.refresh_aria' | translate"
|
||||
(click)="refreshQr()"
|
||||
(keydown.enter)="refreshQr()"
|
||||
(keydown.space)="refreshQr()"
|
||||
>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"></path>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
|
||||
</svg>
|
||||
<span>{{ (messageKey() || 'auth.expired') | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="login-note">{{ 'auth.redirect_note' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
280
src/app/auth-dialog/auth-dialog.scss
Normal file
280
src/app/auth-dialog/auth-dialog.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
248
src/app/auth-dialog/auth-dialog.ts
Normal file
248
src/app/auth-dialog/auth-dialog.ts
Normal file
@@ -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<AuthDialogMode>('login');
|
||||
processing = input(false);
|
||||
|
||||
authorized = output<AuthDialogAuthorizedEvent>();
|
||||
closed = output<void>();
|
||||
|
||||
state = signal<AuthDialogState>('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<typeof setInterval> | 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<WebSessionResponse>(`${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<WebSessionResponse>(`${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<WebSessionResponse>(`${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;
|
||||
}
|
||||
}
|
||||
@@ -118,62 +118,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telegram sign-in popup -->
|
||||
@if (popupOpen()) {
|
||||
<div class="modal" (click)="closePopup()">
|
||||
<div class="modal__card" (click)="$event.stopPropagation()">
|
||||
<button class="modal__close" type="button" (click)="closePopup()" aria-label="Закрыть">×</button>
|
||||
|
||||
@if (paid()) {
|
||||
<div class="modal__success">
|
||||
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#16a34a"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 6L9 17l-5-5" />
|
||||
</svg>
|
||||
@if (loginOnly()) {
|
||||
<h2 class="modal__title">{{ 'fastcheck.modal_loggedin_title' | translate }}</h2>
|
||||
<p class="modal__sub">{{ 'fastcheck.modal_loggedin_sub' | translate }}</p>
|
||||
} @else {
|
||||
<h2 class="modal__title">{{ 'fastcheck.modal_paid_title' | translate }}</h2>
|
||||
<p class="modal__sub">
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
{{ 'fastcheck.modal_paid_sub' | translate }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<img class="brand-logo brand-logo--small" src="/logo_small.png"
|
||||
alt="fastCHECK" width="32" height="32" />
|
||||
<h2 class="modal__title">{{ 'fastcheck.modal_title' | translate }}</h2>
|
||||
<p class="modal__sub">{{ 'fastcheck.modal_sub' | translate }}</p>
|
||||
|
||||
@if (popupLoading() && !webSessionId()) {
|
||||
<div class="qr__placeholder">{{ 'fastcheck.modal_loading' | translate }}</div>
|
||||
}
|
||||
|
||||
@if (webSessionId() && !isMobile) {
|
||||
<img [src]="qrUrl()" width="240" height="240" alt="QR Telegram" style="border-radius:12px;display:block;margin:0 auto 12px;" />
|
||||
}
|
||||
|
||||
@if (webSessionId()) {
|
||||
<a class="tg-link" [href]="telegramLink()" target="_blank" rel="noopener">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9.04 15.65l-.36 4.06c.51 0 .73-.22.99-.48l2.38-2.27 4.93 3.6c.9.5 1.55.24 1.79-.83l3.24-15.18h.01c.29-1.34-.48-1.86-1.36-1.54L1.13 9.66c-1.32.5-1.3 1.23-.22 1.56l4.92 1.53L17.27 5.6c.54-.34 1.03-.15.62.19" />
|
||||
</svg>
|
||||
{{ 'fastcheck.modal_open_tg' | translate }}
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (popupLoading() && webSessionId()) {
|
||||
<p class="modal__hint">{{ 'fastcheck.modal_confirming' | translate }}</p>
|
||||
} @else if (webSessionId()) {
|
||||
<p class="modal__hint">{{ 'fastcheck.modal_waiting' | translate }}</p>
|
||||
}
|
||||
|
||||
@if (popupError()) {
|
||||
<p class="modal__error">{{ popupError() }}</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<app-auth-dialog
|
||||
[open]="authOpen()"
|
||||
[mode]="authMode()"
|
||||
[processing]="authProcessing()"
|
||||
(authorized)="onAuthAuthorized($event)"
|
||||
(closed)="onAuthClosed()"
|
||||
/>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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<string>('');
|
||||
fastcheckAmount = signal<number | null>(null);
|
||||
fastcheckCode = signal<string>('');
|
||||
@@ -91,15 +89,9 @@ export class FastcheckPage {
|
||||
telegramId = signal<string>('');
|
||||
fastcheckCurrency = signal<string>('RUB');
|
||||
|
||||
popupOpen = signal<boolean>(false);
|
||||
popupLoading = signal<boolean>(false);
|
||||
popupError = signal<string>('');
|
||||
webSessionId = signal<string>('');
|
||||
paid = signal<boolean>(false);
|
||||
loginOnly = signal<boolean>(false);
|
||||
isNewFlow = signal<boolean>(false);
|
||||
sessionToken = signal<string>(localStorage.getItem('fc_session') ?? '');
|
||||
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
||||
authOpen = signal<boolean>(false);
|
||||
authMode = signal<AuthDialogMode>('payment');
|
||||
authProcessing = signal<boolean>(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<string, string> = {
|
||||
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<WebSessionResponse>(`${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);
|
||||
onAuthClosed(): void {
|
||||
this.authProcessing.set(false);
|
||||
this.authOpen.set(false);
|
||||
}
|
||||
} else {
|
||||
this.sessionToken.set('');
|
||||
this.createNewSession();
|
||||
}
|
||||
},
|
||||
error: () => { this.sessionToken.set(''); this.createNewSession(); }
|
||||
});
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
private createNewSession(): void {
|
||||
this.http.get<WebSessionResponse>(`${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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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.webSessionId.set('');
|
||||
}
|
||||
|
||||
private startPolling(sessionId: string): void {
|
||||
this.stopPolling();
|
||||
this.pollHandle = setInterval(() => {
|
||||
this.http
|
||||
.get<WebSessionResponse>(`${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);
|
||||
return;
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
|
||||
|
||||
@@ -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<string, Record<string, string>>;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
||||
Reference in New Issue
Block a user