changes
This commit is contained in:
@@ -12,5 +12,12 @@
|
|||||||
"changeOrigin": true,
|
"changeOrigin": true,
|
||||||
"pathRewrite": { "^/proxy/qr-vitanova": "" },
|
"pathRewrite": { "^/proxy/qr-vitanova": "" },
|
||||||
"logLevel": "debug"
|
"logLevel": "debug"
|
||||||
|
},
|
||||||
|
"/proxy/fastcheck-store": {
|
||||||
|
"target": "https://fastcheck.store",
|
||||||
|
"secure": true,
|
||||||
|
"changeOrigin": true,
|
||||||
|
"pathRewrite": { "^/proxy/fastcheck-store": "" },
|
||||||
|
"logLevel": "debug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,3 +17,12 @@ export const FASTCHECK_API = isDevMode()
|
|||||||
export const QR_VITANOVA_API = isDevMode()
|
export const QR_VITANOVA_API = isDevMode()
|
||||||
? '/proxy/qr-vitanova/api'
|
? '/proxy/qr-vitanova/api'
|
||||||
: 'https://qr.vitanova.network/api';
|
: 'https://qr.vitanova.network/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base URL for the public fastcheck.store API (note: different host than
|
||||||
|
* FASTCHECK_API which points at api.fastcheck.store).
|
||||||
|
* Used for /api/fastcheck/settings/{id} and /api/fastcheck/message/{tgId}.
|
||||||
|
*/
|
||||||
|
export const FASTCHECK_STORE_API = isDevMode()
|
||||||
|
? '/proxy/fastcheck-store/api'
|
||||||
|
: 'https://fastcheck.store/api';
|
||||||
|
|||||||
@@ -59,18 +59,6 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Share row — enabled once a valid fastcheck number is entered, even if there is nothing to pay -->
|
|
||||||
<div class="share-row">
|
|
||||||
<button type="button" class="share-btn share-btn--tg" (click)="shareByTelegram()"
|
|
||||||
[disabled]="!canShare()"
|
|
||||||
[title]="'fastcheck.share_tg' | translate">
|
|
||||||
<svg width="16" height="16" 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.share_tg' | translate }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Code -->
|
<!-- Code -->
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field__label" for="fcCode">{{ 'fastcheck.code_label' | translate }}</label>
|
<label class="field__label" for="fcCode">{{ 'fastcheck.code_label' | translate }}</label>
|
||||||
@@ -91,7 +79,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (hasPartnerId()) {
|
@if (validId()) {
|
||||||
<button class="pay-btn" type="button" (click)="pay()" [disabled]="!canPay()">
|
<button class="pay-btn" type="button" (click)="pay()" [disabled]="!canPay()">
|
||||||
<span class="pay-btn__icon">
|
<span class="pay-btn__icon">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
@@ -102,6 +90,15 @@
|
|||||||
</span>
|
</span>
|
||||||
{{ 'fastcheck.pay_btn' | translate }}
|
{{ 'fastcheck.pay_btn' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button class="pay-btn" type="button" (click)="shareByTelegram()" [disabled]="!canShare()">
|
||||||
|
<span class="pay-btn__icon">
|
||||||
|
<svg width="20" height="20" 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>
|
||||||
|
</span>
|
||||||
|
{{ 'fastcheck.share_tg' | translate }}
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { FastcheckService } from '../../fastcheck.service';
|
import { FastcheckService } from '../../fastcheck.service';
|
||||||
import { FASTCHECK_API, QR_VITANOVA_API } from '../../api';
|
import { FASTCHECK_API, FASTCHECK_STORE_API, QR_VITANOVA_API } from '../../api';
|
||||||
import { TranslatePipe } from '../../translate/translate.pipe';
|
import { TranslatePipe } from '../../translate/translate.pipe';
|
||||||
import { TranslationService } from '../../translate/translation.service';
|
import { TranslationService } from '../../translate/translation.service';
|
||||||
|
|
||||||
@@ -27,24 +27,21 @@ interface CheckFastcheckResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response of GET /api/settings?id=<partnerId>.
|
* Response of POST /api/fastcheck/settings/{id}.
|
||||||
* Shape is defensive — backend may include min/max limits and/or an
|
* Validates the partner id and returns active fastcheck data + callbackurl.
|
||||||
* already-active fastcheck (or QR) for this partner so the page can autofill.
|
|
||||||
*/
|
*/
|
||||||
interface SettingsResponse {
|
interface SettingsResponse {
|
||||||
minAmount?: number;
|
|
||||||
maxAmount?: number;
|
|
||||||
// Possible active fastcheck data — backend may use different casing.
|
|
||||||
fastcheck?: string;
|
fastcheck?: string;
|
||||||
fastcheckNumber?: string;
|
fastcheckNumber?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
|
Code?: string;
|
||||||
amount?: number;
|
amount?: number;
|
||||||
|
currency?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
// QR-side info (not rendered on this page yet, but accepted for future use).
|
telegramID?: string;
|
||||||
qrId?: string;
|
callbackurl?: string;
|
||||||
qrUrl?: string;
|
callbackUrl?: string;
|
||||||
payload?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -84,8 +81,15 @@ export class FastcheckPage {
|
|||||||
// Non-blocking settings hint shown above the form when /settings fails.
|
// Non-blocking settings hint shown above the form when /settings fails.
|
||||||
settingsError = signal<string>('');
|
settingsError = signal<string>('');
|
||||||
settingsLoaded = signal<boolean>(false);
|
settingsLoaded = signal<boolean>(false);
|
||||||
// True only after /settings returned 200 — used to disable Pay button otherwise.
|
// True only after POST /fastcheck/settings/{id} returned 200.
|
||||||
settingsOk = signal<boolean>(false);
|
// Drives button visibility: validId → Pay; !validId → Send-to-Telegram.
|
||||||
|
validId = signal<boolean>(false);
|
||||||
|
// Ready callback URL from settings response — we just redirect here on Pay.
|
||||||
|
callbackUrl = signal<string>('');
|
||||||
|
// telegramID returned by settings (or from URL); empty means user has no
|
||||||
|
// linked telegram yet and we must run the login flow before sending.
|
||||||
|
telegramId = signal<string>('');
|
||||||
|
fastcheckCurrency = signal<string>('RUB');
|
||||||
|
|
||||||
popupOpen = signal<boolean>(false);
|
popupOpen = signal<boolean>(false);
|
||||||
popupLoading = signal<boolean>(false);
|
popupLoading = signal<boolean>(false);
|
||||||
@@ -100,19 +104,14 @@ export class FastcheckPage {
|
|||||||
canPay = computed(() => {
|
canPay = computed(() => {
|
||||||
const digits = this.fastcheckNumber().replace(/\D/g, '');
|
const digits = this.fastcheckNumber().replace(/\D/g, '');
|
||||||
const codeDigits = this.fastcheckCode().replace(/\D/g, '');
|
const codeDigits = this.fastcheckCode().replace(/\D/g, '');
|
||||||
return digits.length === 18 && codeDigits.length === 6
|
return digits.length === 18 && codeDigits.length === 6 && !this.amountLoading();
|
||||||
&& this.codeEnabled() && !this.amountLoading();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share via Telegram is allowed as soon as a valid 18-digit fastcheck is
|
* Send via Telegram is enabled with the same rules as Pay (18-digit number
|
||||||
* entered and the lookup is finished — even if there is nothing to pay
|
* + 6-digit code). Button is only shown when validId is false.
|
||||||
* (amount is null/zero). Pay button stays governed by canPay().
|
|
||||||
*/
|
*/
|
||||||
canShare = computed(() => {
|
canShare = computed(() => this.canPay());
|
||||||
const digits = this.fastcheckNumber().replace(/\D/g, '');
|
|
||||||
return digits.length === 18 && !this.amountLoading();
|
|
||||||
});
|
|
||||||
|
|
||||||
telegramLink = computed(() => {
|
telegramLink = computed(() => {
|
||||||
const sid = this.webSessionId();
|
const sid = this.webSessionId();
|
||||||
@@ -185,41 +184,55 @@ export class FastcheckPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/settings?id=<partnerId>. Idempotent — applies response to the
|
* POST /api/fastcheck/settings/{id} on each load.
|
||||||
* current UI without creating any new check. Errors are non-blocking.
|
* Validates the partner id and may return active fastcheck data + callbackurl.
|
||||||
*
|
* If response is non-200 → validId stays false and we fall back to the
|
||||||
* @param alreadyAutofilled - true when constructor already populated the form
|
* default partner id; UI shows the Telegram-send button instead of Pay.
|
||||||
* from nav state / iid; in that case settings must not overwrite it.
|
|
||||||
*/
|
*/
|
||||||
private loadSettings(alreadyAutofilled: boolean): void {
|
private loadSettings(alreadyAutofilled: boolean): void {
|
||||||
const id = this.partnerId();
|
const id = this.partnerId();
|
||||||
if (!id) {
|
if (!id) {
|
||||||
this.settingsError.set(this.t('errors.settings_missing_id'));
|
|
||||||
this.settingsLoaded.set(true);
|
this.settingsLoaded.set(true);
|
||||||
this.settingsOk.set(false);
|
this.validId.set(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${QR_VITANOVA_API}/settings?id=${encodeURIComponent(id)}`;
|
// Pre-fill body from URL query params — partner usually forwards them.
|
||||||
this.http.get<SettingsResponse>(url).subscribe({
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const body = {
|
||||||
|
fastcheck: params.get('fastcheck') ?? this.fastcheckNumber() ?? '',
|
||||||
|
Code: params.get('code') ?? this.fastcheckCode() ?? '',
|
||||||
|
telegramID: params.get('telegramID') ?? params.get('tg') ?? '',
|
||||||
|
callbackurl: params.get('callbackurl') ?? params.get('callback') ?? ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${FASTCHECK_STORE_API}/fastcheck/settings/${encodeURIComponent(id)}`;
|
||||||
|
this.http.post<SettingsResponse>(url, body).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.settingsLoaded.set(true);
|
this.settingsLoaded.set(true);
|
||||||
this.settingsError.set('');
|
this.settingsError.set('');
|
||||||
this.settingsOk.set(true);
|
this.validId.set(true);
|
||||||
this.applySettings(res ?? {}, alreadyAutofilled);
|
this.applySettings(res ?? {}, alreadyAutofilled);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
// Non-blocking: keep manual mode available, but Pay stays disabled.
|
// Not a valid id — fall back to default and hide Pay button.
|
||||||
this.settingsLoaded.set(true);
|
this.settingsLoaded.set(true);
|
||||||
this.settingsOk.set(false);
|
this.validId.set(false);
|
||||||
|
this.partnerId.set(this.defaultPartnerId);
|
||||||
|
this.hasPartnerId.set(false);
|
||||||
this.settingsError.set(this.t('errors.settings_failed'));
|
this.settingsError.set(this.t('errors.settings_failed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply settings response to UI state without creating any new check. */
|
/** Apply settings response — autofill, callback url, telegram id. */
|
||||||
private applySettings(res: SettingsResponse, alreadyAutofilled: boolean): void {
|
private applySettings(res: SettingsResponse, alreadyAutofilled: boolean): void {
|
||||||
// Active check autofill — only if user hasn't already got data on screen.
|
const cb = res.callbackurl ?? res.callbackUrl ?? '';
|
||||||
|
if (cb) this.callbackUrl.set(cb);
|
||||||
|
|
||||||
|
if (res.telegramID) this.telegramId.set(res.telegramID);
|
||||||
|
if (res.currency) this.fastcheckCurrency.set(res.currency);
|
||||||
|
|
||||||
if (alreadyAutofilled) return;
|
if (alreadyAutofilled) return;
|
||||||
|
|
||||||
const rawNumber = res.fastcheck ?? res.fastcheckNumber ?? '';
|
const rawNumber = res.fastcheck ?? res.fastcheckNumber ?? '';
|
||||||
@@ -230,38 +243,85 @@ export class FastcheckPage {
|
|||||||
for (let i = 0; i < digits.length; i += 6) groups.push(digits.slice(i, i + 6));
|
for (let i = 0; i < digits.length; i += 6) groups.push(digits.slice(i, i + 6));
|
||||||
this.fastcheckNumber.set(groups.join('-'));
|
this.fastcheckNumber.set(groups.join('-'));
|
||||||
if (digits.length === 18) {
|
if (digits.length === 18) {
|
||||||
// Trigger the regular lookup so amount/code-enabled stay consistent.
|
|
||||||
this.lookupFastcheck(groups.join('-'));
|
this.lookupFastcheck(groups.join('-'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof res.amount === 'number') {
|
if (typeof res.amount === 'number') this.fastcheckAmount.set(res.amount);
|
||||||
this.fastcheckAmount.set(res.amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof res.code === 'string' && res.code) {
|
const rawCode = res.code ?? res.Code ?? '';
|
||||||
const codeDigits = res.code.replace(/\D/g, '').slice(0, 6);
|
if (rawCode) {
|
||||||
|
const codeDigits = String(rawCode).replace(/\D/g, '').slice(0, 6);
|
||||||
this.fastcheckCode.set(codeDigits);
|
this.fastcheckCode.set(codeDigits);
|
||||||
this.codeEnabled.set(true);
|
this.codeEnabled.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final statuses end the flow — no polling, no new requests.
|
|
||||||
const status = (res.status ?? '').toUpperCase();
|
const status = (res.status ?? '').toUpperCase();
|
||||||
if (status === 'COMPLETED' || status === 'APPROVED') {
|
if (status === 'COMPLETED' || status === 'APPROVED') {
|
||||||
this.paid.set(true);
|
this.paid.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pay button — redirect to the ready callback URL returned by /settings.
|
||||||
|
* Falls back to opening the legacy Telegram-accept popup if no URL is set.
|
||||||
|
*/
|
||||||
pay(): void {
|
pay(): void {
|
||||||
if (!this.canPay()) {
|
if (!this.canPay()) return;
|
||||||
|
const url = this.callbackUrl();
|
||||||
|
if (url) {
|
||||||
|
window.location.href = url;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// No callback was provided — keep the legacy Telegram-accept flow.
|
||||||
this.error.set('');
|
this.error.set('');
|
||||||
this.loginOnly.set(false);
|
this.loginOnly.set(false);
|
||||||
this.openPopup();
|
this.openPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send fastcheck to Telegram. If we already know the telegramID, fire
|
||||||
|
* POST /api/fastcheck/message/{telegramID} directly. Otherwise run the
|
||||||
|
* Telegram-login popup first; after login we'll retry the send.
|
||||||
|
*/
|
||||||
|
shareByTelegram(): void {
|
||||||
|
if (!this.canShare()) return;
|
||||||
|
this.error.set('');
|
||||||
|
|
||||||
|
const tg = this.telegramId();
|
||||||
|
if (tg) {
|
||||||
|
this.sendFastcheckToTelegram(tg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No telegramID yet — trigger identification via Telegram-bot.
|
||||||
|
this.loginOnly.set(true);
|
||||||
|
this.openPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/fastcheck/message/{telegramID} with all fastcheck fields. */
|
||||||
|
private sendFastcheckToTelegram(telegramId: string): void {
|
||||||
|
const url = `${FASTCHECK_STORE_API}/fastcheck/message/${encodeURIComponent(telegramId)}`;
|
||||||
|
const body = {
|
||||||
|
id: this.partnerId(),
|
||||||
|
fastcheck: this.fastcheckNumber(),
|
||||||
|
code: this.fastcheckCode(),
|
||||||
|
amount: this.fastcheckAmount(),
|
||||||
|
currency: this.fastcheckCurrency()
|
||||||
|
};
|
||||||
|
this.http.post(url, body).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.popupOpen.set(true);
|
||||||
|
this.paid.set(true);
|
||||||
|
this.loginOnly.set(true);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.error.set(this.t('errors.payment_failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private openPopup(): void {
|
private openPopup(): void {
|
||||||
this.popupOpen.set(true);
|
this.popupOpen.set(true);
|
||||||
this.popupError.set('');
|
this.popupError.set('');
|
||||||
@@ -342,6 +402,12 @@ export class FastcheckPage {
|
|||||||
if (res?.Status) {
|
if (res?.Status) {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
if (this.loginOnly()) {
|
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);
|
this.paid.set(true);
|
||||||
} else {
|
} else {
|
||||||
this.acceptFastcheck(sessionId);
|
this.acceptFastcheck(sessionId);
|
||||||
@@ -458,9 +524,4 @@ export class FastcheckPage {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
shareByTelegram(): void {
|
|
||||||
this.loginOnly.set(true);
|
|
||||||
this.openPopup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user