qr only
This commit is contained in:
@@ -187,7 +187,7 @@
|
||||
id="note"
|
||||
class="note-input"
|
||||
placeholder="Причина платежа..."
|
||||
rows="3"
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
142
public/Fastcheck_API (1).txt
Normal file
142
public/Fastcheck_API (1).txt
Normal file
@@ -0,0 +1,142 @@
|
||||
eFastcheck.store
|
||||
General Information
|
||||
Information exchange with the Fastcheck server is realized via RESTful API. All requests to the server must be executed via HTTPS using GET||POST||PUT||DELETE requests to the given ROOT address. Body of requests must be in JSON format. All not public requests must be signed by the client and the public key must be sent to the server for client identification and sign checking.
|
||||
|
||||
|
||||
|
||||
|
||||
Check if server is available
|
||||
Client needs to periodically check if the server is available by sending “ping” to the client. On error corresponding message must be shown.
|
||||
Protocol: https
|
||||
Root Path: api.Fastcheck.store
|
||||
Type GET
|
||||
Path /ping
|
||||
Request Parameters:
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
Response (OK):
|
||||
{
|
||||
"message": "pong",
|
||||
}
|
||||
________________
|
||||
|
||||
|
||||
Create new websession
|
||||
Creates a new websession for qr generation. By timeout a new websession must be requested, after the user shows some activity (click on qr).
|
||||
Protocol: https
|
||||
Root Path: api.Fastcheck.store
|
||||
Type GET
|
||||
Path /websession
|
||||
Request Parameters:
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
Response (OK):
|
||||
{
|
||||
"sessionId": “1AF3781BF6B94604B771AEA1D44FA63A”
|
||||
"userId" : "",
|
||||
"expires" : "sessionId",
|
||||
"userSessionId": "",
|
||||
"Status": false
|
||||
}
|
||||
________________
|
||||
|
||||
|
||||
Check websession status
|
||||
Check if the user is already logged in. a new websession for qr generation. By timeout a new websession must be requested, after the user shows some activity (click on qr).
|
||||
Protocol: https
|
||||
Root Path: api.Fastcheck.store
|
||||
Type GET
|
||||
Path /websession/:webSessionID
|
||||
Request Parameters:
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
Response (OK):
|
||||
{
|
||||
"sessionId": “1AF3781BF6B94604B771AEA1D44FA63A”,
|
||||
"userId" : "kHaAe9roaC2uq63AKGE/8+Ti/t/iFro68QhEZ1dRGLo",
|
||||
"expires" : "sessionId",
|
||||
"userSessionId": "8A94EFEFD003426A9B456C48CAC99BE6",
|
||||
"Status": true
|
||||
}
|
||||
________________
|
||||
Delete websession status
|
||||
Delete the session to log out from the system.
|
||||
Protocol: https
|
||||
Root Path: api.Fastcheck.store
|
||||
Type DELETE
|
||||
Path /websession/:webSessionID
|
||||
Request Parameters:
|
||||
{
|
||||
"sessionId": “1AF3781BF6B94604B771AEA1D44FA63A”
|
||||
}
|
||||
Response (OK):
|
||||
{
|
||||
}
|
||||
________________
|
||||
|
||||
|
||||
Check Fastcheck status
|
||||
Check if fastcheck exists and get the amount assigned to check.
|
||||
Protocol: https
|
||||
Root Path: api.Fastcheck.store
|
||||
Type GET
|
||||
Path /fastcheck
|
||||
|
||||
|
||||
Request Parameters:
|
||||
{
|
||||
"fastcheck": “1234-5678-0001”,
|
||||
}
|
||||
Response (OK):
|
||||
{
|
||||
"fastcheck": "1234-5678-0001",
|
||||
"expiration": 2021-07-07T09:08:18Z ,
|
||||
"Status": true
|
||||
}
|
||||
________________
|
||||
New Fastcheck
|
||||
Create a fastcheck for a given amount. The Users must have a sufficient amount on the balance.
|
||||
Protocol: https
|
||||
Root Path: api.Fastcheck.store
|
||||
Type POST
|
||||
Path /fastcheck
|
||||
HEADER: Authorization - {"sessionID": "1AF3781BF6B94604B771AEA1D44FA63A"}
|
||||
Request Parameters:
|
||||
{
|
||||
"amount": 158000,
|
||||
"currency": "RUB"
|
||||
}
|
||||
Response (OK):
|
||||
{
|
||||
"fastcheck": "1234-5678-0001",
|
||||
"expiration": 2021-07-07T09:08:18Z ,
|
||||
"code": "5864",
|
||||
"Status": true
|
||||
}
|
||||
________________
|
||||
Accept Fastcheck
|
||||
Accept fastcheck to the user balance.
|
||||
Protocol: https
|
||||
Root Path: api.Fastcheck.store
|
||||
Type POST
|
||||
Path /fastcheck
|
||||
HEADER: Authorization - {"sessionID": "1AF3781BF6B94604B771AEA1D44FA63A"}
|
||||
Request Parameters:
|
||||
{
|
||||
"fastcheck": "1234-5678-0001",
|
||||
"code": "5864"
|
||||
}
|
||||
Response (404-ERROR):
|
||||
{
|
||||
"message": "not authorized"
|
||||
}
|
||||
Response (200-OK):
|
||||
{
|
||||
"message": "ok"
|
||||
}
|
||||
262
public/SBP QR API.txt
Normal file
262
public/SBP QR API.txt
Normal file
@@ -0,0 +1,262 @@
|
||||
General Information
|
||||
Information exchange with the SBP server is realized via RESTful API. All requests to the server must be executed via HTTPS using GET||POST||PUT||DELETE requests to the given ROOT address. Body of requests must be in JSON format. All not public requests must be signed by the client and the public key must be sent to the server for client identification and sign checking.
|
||||
|
||||
|
||||
Header:
|
||||
“Authorization”: {JSON WITH KEY AND PARTNERID}
|
||||
|
||||
|
||||
Check if server is available
|
||||
Client needs to periodically check if the server is available by sending “ping” to the client. On error corresponding message must be shown.
|
||||
Protocol: https
|
||||
Root Path: QR.VITANOVA.NETWORK
|
||||
Type GET
|
||||
Path /ping
|
||||
Request Parameters:
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
Response (Error):
|
||||
{
|
||||
"message": "pong",
|
||||
"status": "Wrong Header"
|
||||
}
|
||||
Response (OK):
|
||||
{
|
||||
"message": "pong",
|
||||
"status": "Correct Header"
|
||||
}
|
||||
________________
|
||||
|
||||
|
||||
Create New QR code
|
||||
Create New QR for payment via SBP
|
||||
Protocol: https
|
||||
Root Path: QR.VITANOVA.NETWORK
|
||||
Type POST
|
||||
Path /qr
|
||||
Request Parameters:
|
||||
{
|
||||
"amount": 10.00, //amount from 10Rub to 499.000 Rub
|
||||
"qrDescription": "Item description",
|
||||
"order": "540", //orderid at partner’s platform
|
||||
"partnerID": 102 //same as in header
|
||||
"Phonemask": 79xxxx66265 //User phone number mask, needed only for crypto based operations. Payment will be accepted only from phone numbers corresponding to the mask
|
||||
"Namelastname": Hakxx Sargxxxx /Mask for User name, lastname in cyrilic, needed only for crypto based operations. Payment will be accepted only from the user corresponding to that mask.
|
||||
}
|
||||
|
||||
|
||||
Response !=200(Error):
|
||||
{
|
||||
"error": "wrong key"
|
||||
}
|
||||
Response =200(OK):
|
||||
{
|
||||
"qrId": "BD10002CI1V3JP1T8QR8TIQ8K35RBVQB",
|
||||
"qrStatus": "NEW",
|
||||
"qrExpirationDate": "2025-11-20T10:10:44Z",
|
||||
"Payload": "https://qr.nspk.ru/BD10002CI1V3JP1T8QR8TIQ8K35RBVQB?type=02&bank=100000000007&sum=1000&cur=RUB&crc=8ACC",
|
||||
"qrUrl": "https://e-commerce.raiffeisen.ru/api/sbp/v1/qr/BD10002CI1V3JP1T8QR8TIQ8K35RBVQB/image"
|
||||
}
|
||||
________________
|
||||
|
||||
|
||||
Check Dynamic QR code
|
||||
Check QR status
|
||||
Protocol: https
|
||||
Root Path: QR.VITANOVA.NETWORK
|
||||
Type GET
|
||||
Path /qr/dynamic/{qrId}
|
||||
|
||||
|
||||
Request Parameters:
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Response !=200(Error):
|
||||
{
|
||||
"error": "Error from the bank "
|
||||
}
|
||||
Response =200(OK):
|
||||
{
|
||||
`json:"nspkID"` //": "AD100060JFQF8FSB9Q28FFL88IH6SST0" `json:"amount"` // "1235"
|
||||
`json:"currency" // "RUB"
|
||||
`json:"order"` // "126" partner order id PaymentDetails
|
||||
`json:"paymentDetails"` // "Назначение платежа 2",
|
||||
`json:"qrType"` //"QRDynamic",
|
||||
`json:"qrExpirationDate"` //: "2025-11-22T09:14:38+03:00" `json:"sbpBank"` // "raiffeisen"
|
||||
`json:"sbpMerchant"` //"Dexar"
|
||||
`json:"sbpMerchantId"` //"", uint64
|
||||
`json:"sbpOperationId"` //0 Status
|
||||
`json:"status"` //": "NEW", "APPROVED", "REJECTED", "COMPLETED"
|
||||
`json:"nspkurl"` //"https://qr.nspk.ru/AD100060JFQF8FSB9Q28FFL88IH6SST0
|
||||
`json:"statusurl"` // "https://partner.com/1234321/status" url for checking QR `json:"redirectUrl"` //"https://fastcheck.store/"
|
||||
`json:"qrDescription"` //"QR для оплаты заказа"
|
||||
`json:"additionalInfo"` // TTL
|
||||
`json:"TTL"` //10 timeout in minutes
|
||||
`json:"callbackUrl"` // https://partner.com/1234321 callback after QR get paid
|
||||
`json:"retry"` //0 retry count for calling partner
|
||||
`json:"partnerID"` //103 Partner created QR PartnerqrID `json:"partnerqrID"` //QR ID in partner system RequestIP
|
||||
`json:"requestIP"` //IP address of client requested QR
|
||||
}
|
||||
________________
|
||||
|
||||
|
||||
Check Static QR code
|
||||
Get all qr-s paid by static QR for today, skipping already read qr codes
|
||||
Protocol: https
|
||||
Root Path: QR.VITANOVA.NETWORK
|
||||
Type GET
|
||||
Path /qr/static/{qrId}?skip=25
|
||||
Request Parameters:
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Response =200(OK):
|
||||
[{
|
||||
`json:"nspkID"` //": "AD100060JFQF8FSB9Q28FFL88IH6SST0" `json:"amount"` // "1235"
|
||||
`json:"currency" // "RUB"
|
||||
`json:"order"` // "126" partner order id PaymentDetails
|
||||
`json:"paymentDetails"` // "Назначение платежа 2",
|
||||
`json:"qrType"` //"QRDynamic",
|
||||
`json:"qrExpirationDate"` //: "2025-11-22T09:14:38+03:00" `json:"sbpBank"` // "raiffeisen"
|
||||
`json:"sbpMerchant"` //"Dexar"
|
||||
`json:"sbpMerchantId"` //"", uint64
|
||||
`json:"sbpOperationId"` //0 Status
|
||||
`json:"status"` //": "NEW", "APPROVED", "REJECTED", "COMPLETED"
|
||||
`json:"nspkurl"` //"https://qr.nspk.ru/AD100060JFQF8FSB9Q28FFL88IH6SST0
|
||||
`json:"statusurl"` // "https://partner.com/1234321/status" url for checking QR `json:"redirectUrl"` //"https://fastcheck.store/"
|
||||
`json:"qrDescription"` //"QR для оплаты заказа"
|
||||
`json:"additionalInfo"` // TTL
|
||||
`json:"TTL"` //10 timeout in minutes
|
||||
`json:"callbackUrl"` // https://partner.com/1234321 callback after QR get paid
|
||||
`json:"retry"` //0 retry count for calling partner
|
||||
`json:"partnerID"` //103 Partner created QR PartnerqrID `json:"partnerqrID"` //QR ID in partner system RequestIP
|
||||
`json:"requestIP"` //IP address of client requested QR
|
||||
}]
|
||||
|
||||
|
||||
________________
|
||||
|
||||
|
||||
Delete QR
|
||||
Delete unused QR. If QR is not paid until expiration time, it will be automatically deleted.
|
||||
Protocol: https
|
||||
Root Path: QR.VITANOVA.NETWORK
|
||||
Type DELETE
|
||||
Path /qr/{qrId}
|
||||
Request Parameters:
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Response !=200(Error):
|
||||
{
|
||||
"error": "Error from the bank "
|
||||
|
||||
|
||||
}
|
||||
Response =200(OK):
|
||||
{
|
||||
}
|
||||
________________
|
||||
Check Partner
|
||||
Returns partner status, with balance and transactions. Each transaction id is QR code, which can be checked additionally.
|
||||
Root Path: API.VITANOVA.NETWORK
|
||||
Type Get
|
||||
Path /partners/{partnerID}
|
||||
Request Parameters:
|
||||
{
|
||||
}
|
||||
Response !=200(Error):
|
||||
{
|
||||
"error": "Not authorized "
|
||||
}
|
||||
Response =200(OK):
|
||||
{
|
||||
"telegram_id": 8285633,
|
||||
"username": "ZZZ",
|
||||
"first_name": "АMAN",
|
||||
"last_name": "",
|
||||
"balance": 22,
|
||||
"transaction": [
|
||||
{
|
||||
"additionalInfo": "Ручка",
|
||||
"paymentPurpose": "Ручка",
|
||||
"amount": 22,
|
||||
"code": "SUCCESS",
|
||||
"createDate": "2025-11-22T15:57:40.925104+03:00",
|
||||
"currency": "RUB",
|
||||
"order": "8285633735_301",
|
||||
"paymentStatus": "SUCCESS",
|
||||
"qrId": "AD10004C1K9N71MN907RD56UOA0BHIBR",
|
||||
"transactionDate": "2025-11-22T15:58:14.814187+03:00",
|
||||
"transactionId": 771515533,
|
||||
"qrExpirationDate": "2025-11-22T16:12:40+03:00"
|
||||
}
|
||||
],
|
||||
"inn": 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
________________
|
||||
|
||||
|
||||
Withdraw
|
||||
Get amount from balance and Creates fastcheck, which then can be for buying usdt, transferring to bank account and to bank card. Fastcheck can be checked on site or via API only by Id, but can be used only with code.
|
||||
Root Path: QR.VITANOVA.NETWORK
|
||||
Type POST
|
||||
Path/partners/withdraw/{partnerID}
|
||||
Request Parameters:
|
||||
{
|
||||
“amount”: 10600.00
|
||||
“currency”: “RUB”
|
||||
“partnerId: “1023454”
|
||||
“wallet”: “TBia4uHnb3oSSZm5isP284cA7Np1v15Vhi”
|
||||
“”
|
||||
“rate”:79.50
|
||||
}
|
||||
|
||||
|
||||
Response !=200(Error):
|
||||
{
|
||||
"error": "Not enough amount on balance "
|
||||
}
|
||||
Response !=200(Error):
|
||||
{
|
||||
"error": "Rate is not correct "
|
||||
}
|
||||
Response =200(OK):
|
||||
{
|
||||
“trxID”:”T5Mv2v8n9L7jY4k1pW3QhUoZfE9R1X3s7rY6tB0pA2C4D6E8F5H”
|
||||
}
|
||||
________________
|
||||
RATE
|
||||
Get currency exchange rate.
|
||||
Root Path: QR.VITANOVA.NETWORK
|
||||
Type GET
|
||||
Path/partners/rate
|
||||
Request Parameters:
|
||||
|
||||
|
||||
|
||||
|
||||
Response !=200(Error):
|
||||
{
|
||||
"error": "Not Authorized "
|
||||
}
|
||||
|
||||
|
||||
Response =200(OK):
|
||||
|
||||
|
||||
{
|
||||
"rate": 78.5
|
||||
}
|
||||
26
public/example.json
Normal file
26
public/example.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"nspkID": "AD10001UFDAT5FDD9LVPEJVDHN75UHHK",
|
||||
"amount": 100,
|
||||
"currency": "RUB",
|
||||
"order": "",
|
||||
"paymentDetails": "",
|
||||
"qrType": "QRDynamic",
|
||||
"qrExpirationDate": "",
|
||||
"qrExecutionDate": "",
|
||||
"sbpBank": "",
|
||||
"sbpMerchant": "",
|
||||
"sbpMerchantId": "",
|
||||
"sbpOperationId": 633730903,
|
||||
"status": "",
|
||||
"nspkurl": "https://qr.nspk.ru/AD10001UFDAT5FDD9LVPEJVDHN75UHHK",
|
||||
"statusurl": "https://pay.payworld.online/webapi/SbpPaymentStatus?id=3426680585\u0026sbpOperationId=633730903\u0026sector=24819\u0026signature=NjZkZjMyZDBjZTI1Zjc1NWUyZjYxNDRkM2ZjN2ViZWQ2YzcwYTc0NWM1ZWRlMGE1YzQyZWNlOTVjNWY1ODUxYg%3D%3D",
|
||||
"redirectUrl": "",
|
||||
"qrDescription": "Payment for shop 00897f30-414f-4b89-b1af-25596b15411c",
|
||||
"additionalInfo": "",
|
||||
"TTL": 0,
|
||||
"callbackUrl": "",
|
||||
"retry": 0,
|
||||
"partnerID": "",
|
||||
"partnerqrID": "00897f30-414f-4b89-b1af-25596b15411c",
|
||||
"requestIP": ""
|
||||
}
|
||||
@@ -62,7 +62,8 @@
|
||||
"create_btn": "Create",
|
||||
"amount_hint": "Allowed amount:",
|
||||
"qr_label": "Scan QR to pay",
|
||||
"qr_waiting": "Waiting for payment confirmation…"
|
||||
"qr_waiting": "Waiting for payment confirmation…",
|
||||
"payment_done": "Payment completed successfully."
|
||||
},
|
||||
"sbp": {
|
||||
"title": "Pay via SBP",
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
"create_btn": "Ստեղծել",
|
||||
"amount_hint": "Թույլատրելի գումար՝",
|
||||
"qr_label": "Սկանավորեք QR-կոդը վճարելու համար",
|
||||
"qr_waiting": "Սպասում ենք վճարման հաստատման…"
|
||||
"qr_waiting": "Սպասում ենք վճարման հաստատման…",
|
||||
"payment_done": "Վճարումը հաջողությամբ ավարտվեց."
|
||||
},
|
||||
"sbp": {
|
||||
"title": "Վճարել SBP-ով",
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
"create_btn": "Создать",
|
||||
"amount_hint": "Допустимая сумма:",
|
||||
"qr_label": "Отсканируйте QR для оплаты",
|
||||
"qr_waiting": "Ожидаем подтверждения оплаты…"
|
||||
"qr_waiting": "Ожидаем подтверждения оплаты…",
|
||||
"payment_done": "Оплата успешно завершена."
|
||||
},
|
||||
"sbp": {
|
||||
"title": "Оплата через СБП",
|
||||
|
||||
302
public/index (4).html
Normal file
302
public/index (4).html
Normal file
@@ -0,0 +1,302 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Оплата через СБП</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #1e40af;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #1e40af 0%, #2563eb 40%, #0ea5e9 100%);
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.page { align-items: flex-end; padding: 0; }
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.card { border-radius: 24px 24px 0 0; max-width: 100%; box-shadow: 0 -8px 40px rgba(0,0,0,.15); }
|
||||
}
|
||||
|
||||
.card__header {
|
||||
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
|
||||
padding: 32px 28px 28px;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 480px) { .card__header { padding: 28px 24px 24px; } }
|
||||
|
||||
.card__title { color: #fff; font-size: 22px; font-weight: 700; margin: 14px 0 4px; letter-spacing: -.3px; }
|
||||
.card__subtitle { color: rgba(255,255,255,.7); font-size: 13px; }
|
||||
|
||||
.card__body { padding: 28px 28px 20px; }
|
||||
@media (max-width: 480px) { .card__body { padding: 24px 20px 16px; } }
|
||||
|
||||
.card__footer { padding: 0 28px 24px; display: flex; justify-content: center; }
|
||||
@media (max-width: 480px) { .card__footer { padding: 0 20px 32px; } }
|
||||
|
||||
.sbp-logo {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
background: rgba(255,255,255,.15); backdrop-filter: blur(8px);
|
||||
border-radius: 16px; padding: 12px 20px;
|
||||
border: 1px solid rgba(255,255,255,.25);
|
||||
}
|
||||
.sbp-logo img { height: 40px; display: block; }
|
||||
@media (max-width: 480px) { .sbp-logo img { height: 34px; } }
|
||||
|
||||
.field { margin-bottom: 16px; }
|
||||
.field__label {
|
||||
display: block; font-size: 13px; font-weight: 600; color: #64748b;
|
||||
margin-bottom: 8px; text-transform: uppercase; letter-spacing: .6px;
|
||||
}
|
||||
.field__error { display: block; margin-top: 6px; font-size: 13px; color: #ef4444; font-weight: 500; }
|
||||
.field__error:empty { display: none; }
|
||||
|
||||
.input-wrap {
|
||||
display: flex; align-items: center;
|
||||
border: 2px solid #e2e8f0; border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
transition: border-color .2s, box-shadow .2s, background .2s;
|
||||
}
|
||||
.input-wrap:focus-within {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
|
||||
background: #fff;
|
||||
}
|
||||
.input-wrap--error { border-color: #ef4444; box-shadow: 0 0 0 4px rgba(239,68,68,.1); }
|
||||
.input-wrap__prefix { padding: 0 4px 0 18px; font-size: 26px; font-weight: 700; color: #2563eb; user-select: none; line-height: 1; }
|
||||
.input-wrap__input {
|
||||
flex: 1; border: none; background: transparent;
|
||||
padding: 16px 16px 16px 8px; font-size: 32px; font-weight: 700;
|
||||
color: #0f172a; outline: none; min-width: 0; font-family: inherit;
|
||||
appearance: textfield; -moz-appearance: textfield;
|
||||
}
|
||||
.input-wrap__input::placeholder { color: #cbd5e1; }
|
||||
.input-wrap__input::-webkit-outer-spin-button,
|
||||
.input-wrap__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||||
@media (max-width: 480px) { .input-wrap__input { font-size: 28px; padding: 14px 14px 14px 6px; } }
|
||||
|
||||
.currency-badge {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
background: #f1f5f9; border-radius: 12px; padding: 12px 16px; margin-bottom: 20px;
|
||||
}
|
||||
.currency-badge__flag { font-size: 22px; line-height: 1; }
|
||||
.currency-badge__code { font-size: 15px; font-weight: 700; color: #0f172a; }
|
||||
.currency-badge__name { font-size: 13px; color: #64748b; margin-left: auto; }
|
||||
|
||||
.pay-btn {
|
||||
width: 100%; display: flex; align-items: center; justify-content: center;
|
||||
gap: 10px; padding: 17px 24px;
|
||||
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
|
||||
color: #fff; border: none; border-radius: 14px;
|
||||
font-size: 17px; font-weight: 700; letter-spacing: .2px;
|
||||
cursor: pointer; font-family: inherit;
|
||||
transition: opacity .15s, transform .1s, box-shadow .15s;
|
||||
box-shadow: 0 6px 20px rgba(37,99,235,.38);
|
||||
}
|
||||
.pay-btn:hover { opacity: .92; box-shadow: 0 8px 28px rgba(37,99,235,.45); }
|
||||
.pay-btn:active { transform: scale(.98); opacity: .88; }
|
||||
.pay-btn:disabled { opacity: .6; cursor: not-allowed; transform: none; }
|
||||
.pay-btn__icon { display: flex; align-items: center; }
|
||||
@media (max-width: 480px) { .pay-btn { padding: 16px 24px; font-size: 16px; } }
|
||||
|
||||
.secure-badge {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 12px; color: #94a3b8; font-weight: 500;
|
||||
}
|
||||
|
||||
.note-input {
|
||||
width: 100%; border: 2px solid #e2e8f0; border-radius: 14px;
|
||||
background: #f8fafc; padding: 14px 16px;
|
||||
font-size: 15px; font-weight: 500; color: #0f172a;
|
||||
font-family: inherit; resize: vertical; outline: none;
|
||||
transition: border-color .2s, box-shadow .2s, background .2s;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.note-input::placeholder { color: #cbd5e1; font-weight: 400; }
|
||||
.note-input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page">
|
||||
<div class="card">
|
||||
|
||||
<div class="card__header">
|
||||
<div class="sbp-logo">
|
||||
<img src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg" alt="СБП" />
|
||||
</div>
|
||||
<h1 class="card__title">Оплата через СБП</h1>
|
||||
<p class="card__subtitle">Система быстрых платежей</p>
|
||||
</div>
|
||||
|
||||
<div class="card__body">
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="amount">Сумма платежа</label>
|
||||
<div class="input-wrap" id="inputWrap">
|
||||
<span class="input-wrap__prefix">₽</span>
|
||||
<input
|
||||
id="amount"
|
||||
type="number"
|
||||
class="input-wrap__input"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
placeholder="0"
|
||||
autofocus
|
||||
value="50"
|
||||
/>
|
||||
</div>
|
||||
<span class="field__error" id="error"></span>
|
||||
</div>
|
||||
|
||||
<div class="currency-badge">
|
||||
<span class="currency-badge__flag">🇷🇺</span>
|
||||
<span class="currency-badge__code">RUB</span>
|
||||
<span class="currency-badge__name">Российский рубль</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="note">Примечание</label>
|
||||
<textarea
|
||||
id="note"
|
||||
class="note-input"
|
||||
placeholder="Причина платежа..."
|
||||
rows="3"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button class="pay-btn" id="payBtn" onclick="goToPayment()">
|
||||
<span class="pay-btn__icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/>
|
||||
<line x1="1" y1="10" x2="23" y2="10"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span id="btnText">Перейти к оплате</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card__footer">
|
||||
<span class="secure-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
Защищённое соединение
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = 'https://qr.vitanova.network:567/qr';
|
||||
|
||||
const amountInput = document.getElementById('amount');
|
||||
const noteInput = document.getElementById('note');
|
||||
const errorEl = document.getElementById('error');
|
||||
const payBtn = document.getElementById('payBtn');
|
||||
const btnText = document.getElementById('btnText');
|
||||
const inputWrap = document.getElementById('inputWrap');
|
||||
|
||||
amountInput.addEventListener('input', function () {
|
||||
if (Number(this.value) > 0) {
|
||||
errorEl.textContent = '';
|
||||
inputWrap.classList.remove('input-wrap--error');
|
||||
}
|
||||
});
|
||||
|
||||
function getPaymentId() {
|
||||
return new URLSearchParams(window.location.search).get('id');
|
||||
}
|
||||
|
||||
function setLoading(loading) {
|
||||
payBtn.disabled = loading;
|
||||
btnText.textContent = loading
|
||||
? 'Подождите...'
|
||||
: 'Перейти к оплате';
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorEl.textContent = msg;
|
||||
inputWrap.classList.add('input-wrap--error');
|
||||
}
|
||||
|
||||
async function goToPayment() {
|
||||
const amount = Number(amountInput.value);
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
showError('Введите корректную сумму');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = getPaymentId();
|
||||
if (!id) {
|
||||
showError('Не указан идентификатор платежа (параметр id)');
|
||||
return;
|
||||
}
|
||||
|
||||
errorEl.textContent = '';
|
||||
inputWrap.classList.remove('input-wrap--error');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
qrtype: 'QRDynamic',
|
||||
amount: amount,
|
||||
currency: 'RUB',
|
||||
partnerqrID: id,
|
||||
qrDescription: noteInput.value.trim()
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.nspkurl) {
|
||||
window.location.href = data.nspkurl;
|
||||
} else {
|
||||
showError("nspkurl не найден в ответе сервера");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showError("Ошибка запроса");
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,21 +1,6 @@
|
||||
import { isDevMode } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Endpoint constants for the Fastcheck backend (see public/api.txt).
|
||||
* Centralised so they can be swapped in one place.
|
||||
* In dev mode (ng serve) requests go through the Angular proxy (proxy.conf.json)
|
||||
* to avoid CORS issues. In production the real URLs are used.
|
||||
*/
|
||||
export const FASTCHECK_API = isDevMode()
|
||||
? '/proxy/fastcheck'
|
||||
: 'https://api.fastcheck.store';
|
||||
|
||||
// Legacy QR endpoint kept for the SBP amount → payload redirect flow.
|
||||
export const QR_API = isDevMode()
|
||||
? '/proxy/legacy-qr/qr'
|
||||
: 'https://qr.vitanova.network:567/qr';
|
||||
|
||||
// New QR Vitanova API (dynamic QR, settings, polling).
|
||||
// QR Vitanova API (dynamic QR, settings, polling).
|
||||
export const QR_VITANOVA_API = isDevMode()
|
||||
? '/proxy/qr-vitanova/api'
|
||||
: 'https://qr.vitanova.network/api';
|
||||
|
||||
@@ -3,17 +3,6 @@
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () => {
|
||||
// Branch: ?id=<orderId> means legacy SBP merchant flow.
|
||||
const hasLegacyId = typeof window !== 'undefined'
|
||||
&& new URLSearchParams(window.location.search).has('id');
|
||||
return hasLegacyId
|
||||
? import('./pages/legacy-pay-page/legacy-pay-page').then((m) => m.LegacyPayPage)
|
||||
: import('./pages/fastcheck-page/fastcheck-page').then((m) => m.FastcheckPage);
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
loadComponent: () =>
|
||||
import('./pages/create-page/create-page').then((m) => m.CreatePage)
|
||||
},
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
export interface FastcheckData {
|
||||
fastcheck: string;
|
||||
amount: number | null;
|
||||
code: string;
|
||||
expiration?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared state between the home (Fastcheck) page and the create-new page.
|
||||
* When a new fastcheck is created via POST /fastcheck, the create page stores
|
||||
* the returned data here and the home page reads it to autofill its fields.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FastcheckService {
|
||||
readonly created = signal<FastcheckData | null>(null);
|
||||
|
||||
setCreated(data: FastcheckData): void {
|
||||
this.created.set(data);
|
||||
}
|
||||
|
||||
consume(): FastcheckData | null {
|
||||
const value = this.created();
|
||||
this.created.set(null);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,6 @@
|
||||
<div class="card">
|
||||
|
||||
<div class="card__header">
|
||||
<a class="back" routerLink="/" [attr.aria-label]="'create.back_label' | translate">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="card__title">
|
||||
{{ 'create.title' | translate }}
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
@@ -102,7 +96,7 @@
|
||||
[ngModel]="note()"
|
||||
(ngModelChange)="onNoteChange($event)"
|
||||
[placeholder]="'create.note_placeholder' | translate"
|
||||
rows="3"
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
</div>
|
||||
@@ -140,6 +134,9 @@
|
||||
@if (qrPolling()) {
|
||||
<p class="qr-modal__hint">{{ 'create.qr_waiting' | translate }}</p>
|
||||
}
|
||||
@if (paymentDone()) {
|
||||
<p class="qr-modal__hint qr-modal__hint--done">{{ 'create.payment_done' | translate }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -45,6 +45,16 @@
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
gap: 6px;
|
||||
}
|
||||
@@ -64,6 +74,11 @@
|
||||
-webkit-appearance: none;
|
||||
font-family: inherit;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
flex: 0 0 72px;
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
height: 52px;
|
||||
padding: 6px;
|
||||
@@ -106,6 +121,15 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.chip {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FastcheckService } from '../../fastcheck.service';
|
||||
import { FASTCHECK_API, QR_VITANOVA_API } from '../../api';
|
||||
import { QR_VITANOVA_API } from '../../api';
|
||||
import { TranslatePipe } from '../../translate/translate.pipe';
|
||||
import { TranslationService } from '../../translate/translation.service';
|
||||
|
||||
@@ -33,15 +31,6 @@ interface QrStatusResponse {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface CreateFastcheckResponse {
|
||||
id?: string; // real field name from server
|
||||
fastcheck?: string; // per API doc fallback
|
||||
expiration?: string;
|
||||
code?: string;
|
||||
amount?: number;
|
||||
Status?: boolean;
|
||||
}
|
||||
|
||||
/** Generate a v4-like UUID without crypto dependency. */
|
||||
function generateUUID(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
@@ -52,14 +41,12 @@ function generateUUID(): string {
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-page',
|
||||
imports: [FormsModule, RouterLink, TranslatePipe],
|
||||
imports: [FormsModule, TranslatePipe],
|
||||
templateUrl: './create-page.html',
|
||||
styleUrl: './create-page.scss'
|
||||
})
|
||||
export class CreatePage {
|
||||
private http = inject(HttpClient);
|
||||
private store = inject(FastcheckService);
|
||||
private router = inject(Router);
|
||||
private i18n = inject(TranslationService);
|
||||
|
||||
private t(key: string): string { return this.i18n.translate(key); }
|
||||
@@ -91,6 +78,7 @@ export class CreatePage {
|
||||
qrImageUrl = signal<string | null>(null);
|
||||
qrPolling = signal<boolean>(false);
|
||||
qrStatus = signal<string>('');
|
||||
paymentDone = signal<boolean>(false);
|
||||
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
||||
private activeQrId = '';
|
||||
|
||||
@@ -101,9 +89,6 @@ export class CreatePage {
|
||||
private get userId(): string {
|
||||
return new URLSearchParams(window.location.search).get('userid-value') ?? '';
|
||||
}
|
||||
private get sessionId(): string {
|
||||
return new URLSearchParams(window.location.search).get('session') ?? '';
|
||||
}
|
||||
private get reference(): string {
|
||||
return new URLSearchParams(window.location.search).get('ref') ?? window.location.hostname;
|
||||
}
|
||||
@@ -204,7 +189,7 @@ export class CreatePage {
|
||||
this.qrStatus.set(st);
|
||||
if (st === 'COMPLETED' || st === 'APPROVED') {
|
||||
this.stopPolling();
|
||||
this.createFastcheck();
|
||||
this.paymentDone.set(true);
|
||||
} else if (st === 'REJECTED') {
|
||||
this.stopPolling();
|
||||
this.error.set(this.t('errors.payment_failed'));
|
||||
@@ -225,34 +210,6 @@ export class CreatePage {
|
||||
this.qrPolling.set(false);
|
||||
}
|
||||
|
||||
private createFastcheck(): void {
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.sessionId) headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
|
||||
|
||||
this.http
|
||||
.post<CreateFastcheckResponse>(
|
||||
`${FASTCHECK_API}/fastcheck`,
|
||||
{ amount: this.amount(), currency: this.currency() },
|
||||
{ headers }
|
||||
)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
const fcNumber = res?.id ?? res?.fastcheck ?? '';
|
||||
const payload = {
|
||||
fastcheck: fcNumber,
|
||||
code: res?.code ?? '',
|
||||
amount: res?.amount ?? this.amount() ?? null,
|
||||
expiration: res?.expiration
|
||||
};
|
||||
if (fcNumber) {
|
||||
this.store.setCreated(payload);
|
||||
}
|
||||
this.router.navigate(['/'], { state: fcNumber ? payload : {} });
|
||||
},
|
||||
error: () => this.router.navigate(['/'])
|
||||
});
|
||||
}
|
||||
|
||||
onAmountChange(value: number | null): void {
|
||||
this.amount.set(value || null);
|
||||
if (value && value > 0) this.error.set('');
|
||||
@@ -266,6 +223,7 @@ export class CreatePage {
|
||||
this.qrImageUrl.set(null);
|
||||
this.qrPolling.set(false);
|
||||
this.qrStatus.set('');
|
||||
this.paymentDone.set(false);
|
||||
if (this.pollHandle !== null) {
|
||||
clearInterval(this.pollHandle);
|
||||
this.pollHandle = null;
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
<div class="page">
|
||||
<div class="card">
|
||||
|
||||
<div class="card__header">
|
||||
<img class="card__brand" src="/logo_big.png"
|
||||
alt="fastCHECK" width="220" height="60" />
|
||||
<p class="card__subtitle">
|
||||
{{ 'fastcheck.subtitle' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card__body">
|
||||
|
||||
<!-- Fastcheck number + new -->
|
||||
<div class="field">
|
||||
<label class="field__label" for="fcNumber">
|
||||
{{ 'fastcheck.number_label' | translate }}
|
||||
</label>
|
||||
<div class="row">
|
||||
<input
|
||||
id="fcNumber"
|
||||
type="text"
|
||||
class="input"
|
||||
[ngModel]="fastcheckNumber()"
|
||||
(ngModelChange)="onNumberChange($event)"
|
||||
[placeholder]="'fastcheck.number_placeholder' | translate"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
maxlength="20"
|
||||
/>
|
||||
<a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый fastCHECK">{{ 'fastcheck.number_new' | translate }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount -->
|
||||
<div class="field">
|
||||
<label class="field__label" for="fcAmount">{{ 'fastcheck.amount_label' | translate }}</label>
|
||||
<div class="input-wrap">
|
||||
<span class="input-wrap__prefix">₽</span>
|
||||
<input
|
||||
id="fcAmount"
|
||||
type="number"
|
||||
class="input-wrap__input"
|
||||
[ngModel]="fastcheckAmount()"
|
||||
(ngModelChange)="onAmountChange($event)"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
placeholder="0"
|
||||
[disabled]="true"
|
||||
/>
|
||||
</div>
|
||||
@if (amountLoading()) {
|
||||
<span class="field__hint">{{ 'fastcheck.amount_checking' | translate }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Share row — always visible, enabled once amount is known -->
|
||||
<div class="share-row">
|
||||
<!-- <button type="button" class="share-btn share-btn--email" (click)="shareByEmail()"
|
||||
[disabled]="fastcheckAmount() === null || amountLoading()"
|
||||
[title]="'fastcheck.share_email' | translate">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
||||
<path d="M2 7l10 7 10-7"/>
|
||||
</svg>
|
||||
{{ 'fastcheck.share_email' | translate }}
|
||||
</button> -->
|
||||
<button type="button" class="share-btn share-btn--tg" (click)="shareByTelegram()"
|
||||
[disabled]="fastcheckAmount() === null || amountLoading()"
|
||||
[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 -->
|
||||
<div class="field">
|
||||
<label class="field__label" for="fcCode">{{ 'fastcheck.code_label' | translate }}</label>
|
||||
<input
|
||||
id="fcCode"
|
||||
type="text"
|
||||
class="input"
|
||||
[ngModel]="fastcheckCode()"
|
||||
(ngModelChange)="onCodeChange($event)"
|
||||
[placeholder]="'fastcheck.code_placeholder' | translate"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
[disabled]="!codeEnabled()"
|
||||
/>
|
||||
@if (error()) {
|
||||
<span class="field__error">{{ error() }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button class="pay-btn" type="button" (click)="pay()" [disabled]="!canPay()">
|
||||
<span class="pay-btn__icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ 'fastcheck.pay_btn' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card__footer">
|
||||
<span class="secure-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
{{ 'common.secure' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
@use './../../../shared' as *;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
|
||||
.input { flex: 1; min-width: 0; }
|
||||
}
|
||||
|
||||
.share-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background .15s, border-color .15s;
|
||||
|
||||
&--email {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
&:hover { background: #e2e8f0; border-color: #cbd5e1; }
|
||||
}
|
||||
|
||||
&--tg {
|
||||
background: #e7f3fe;
|
||||
color: #0088cc;
|
||||
border-color: #bfdbfe;
|
||||
&:hover { background: #dbeafe; border-color: #93c5fd; }
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: .4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
height: 48px;
|
||||
min-width: 64px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
transition: opacity .15s, transform .1s, background .15s;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&--ghost {
|
||||
background: #f1f5f9;
|
||||
color: #2563eb;
|
||||
border-color: #e2e8f0;
|
||||
|
||||
&:hover { background: #e2e8f0; }
|
||||
&:active { transform: scale(.97); }
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
padding: 0 14px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color .2s, box-shadow .2s, background .2s;
|
||||
|
||||
&::placeholder { color: #cbd5e1; font-weight: 500; }
|
||||
|
||||
&:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
|
||||
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;
|
||||
-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; }
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FastcheckService } from '../../fastcheck.service';
|
||||
import { FASTCHECK_API } from '../../api';
|
||||
import { TranslatePipe } from '../../translate/translate.pipe';
|
||||
import { TranslationService } from '../../translate/translation.service';
|
||||
|
||||
interface WebSessionResponse {
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
expires: string;
|
||||
userSessionId: string;
|
||||
Status: boolean;
|
||||
}
|
||||
|
||||
interface CheckFastcheckResponse {
|
||||
id: string;
|
||||
code: string;
|
||||
owneID: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
createdAt: string;
|
||||
creattransactionID: string;
|
||||
firedAT: string;
|
||||
firetransactionID: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-fastcheck-page',
|
||||
imports: [FormsModule, RouterLink, TranslatePipe],
|
||||
templateUrl: './fastcheck-page.html',
|
||||
styleUrl: './fastcheck-page.scss'
|
||||
})
|
||||
export class FastcheckPage {
|
||||
private http = inject(HttpClient);
|
||||
private store = inject(FastcheckService);
|
||||
private router = inject(Router);
|
||||
private i18n = inject(TranslationService);
|
||||
|
||||
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>('');
|
||||
codeEnabled = signal<boolean>(false);
|
||||
error = signal<string>('');
|
||||
amountLoading = signal<boolean>(false);
|
||||
|
||||
popupOpen = signal<boolean>(false);
|
||||
popupLoading = signal<boolean>(false);
|
||||
popupError = signal<string>('');
|
||||
webSessionId = signal<string>('');
|
||||
paid = signal<boolean>(false);
|
||||
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
||||
private lastLookedUpNumber = '';
|
||||
|
||||
canPay = computed(() => {
|
||||
const digits = this.fastcheckNumber().replace(/\D/g, '');
|
||||
const codeDigits = this.fastcheckCode().replace(/\D/g, '');
|
||||
return digits.length === 18 && codeDigits.length === 6
|
||||
&& this.codeEnabled() && !this.amountLoading();
|
||||
});
|
||||
|
||||
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 ?? {}) : {};
|
||||
const created = (navState?.fastcheck)
|
||||
? { fastcheck: navState.fastcheck, code: navState.code ?? '', amount: navState.amount ?? null, expiration: navState.expiration }
|
||||
: this.store.consume();
|
||||
|
||||
if (created) {
|
||||
this.fastcheckNumber.set(created.fastcheck);
|
||||
this.fastcheckAmount.set(created.amount);
|
||||
this.fastcheckCode.set(created.code);
|
||||
this.codeEnabled.set(true);
|
||||
}
|
||||
|
||||
// ?iid=xxxxxx-xxxxxx-xxxxxx — auto-fill and trigger lookup
|
||||
const iidParam = new URLSearchParams(window.location.search).get('iid') ?? '';
|
||||
if (iidParam && !created) {
|
||||
const digits = iidParam.replace(/\D/g, '').slice(0, 18);
|
||||
const groups: string[] = [];
|
||||
for (let i = 0; i < digits.length; i += 6) groups.push(digits.slice(i, i + 6));
|
||||
const masked = groups.join('-');
|
||||
this.fastcheckNumber.set(masked);
|
||||
if (digits.length === 18) this.lookupFastcheck(masked);
|
||||
}
|
||||
}
|
||||
|
||||
pay(): void {
|
||||
if (!this.canPay()) {
|
||||
return;
|
||||
}
|
||||
this.error.set('');
|
||||
this.openPopup();
|
||||
}
|
||||
|
||||
private openPopup(): void {
|
||||
this.popupOpen.set(true);
|
||||
this.popupError.set('');
|
||||
this.paid.set(false);
|
||||
this.popupLoading.set(true);
|
||||
|
||||
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.webSessionId()) {
|
||||
// Best-effort logout; ignore errors.
|
||||
this.http
|
||||
.request('DELETE', `${FASTCHECK_API}/websession/${this.webSessionId()}`, {
|
||||
body: { sessionId: this.webSessionId() }
|
||||
})
|
||||
.subscribe({ error: () => undefined });
|
||||
}
|
||||
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();
|
||||
this.acceptFastcheck(sessionId);
|
||||
}
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
private stopPolling(): void {
|
||||
if (this.pollHandle !== null) {
|
||||
clearInterval(this.pollHandle);
|
||||
this.pollHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
private acceptFastcheck(sessionId: string): void {
|
||||
this.popupLoading.set(true);
|
||||
this.http
|
||||
.post(
|
||||
`${FASTCHECK_API}/fastcheck`,
|
||||
{ fastcheck: this.fastcheckNumber().trim(), code: this.fastcheckCode().trim() },
|
||||
{ headers: { Authorization: JSON.stringify({ sessionID: sessionId }) } }
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.popupLoading.set(false);
|
||||
this.paid.set(true);
|
||||
// Fire DELETE to mark fastcheck as consumed on the merchant side.
|
||||
this.http
|
||||
.delete(`${FASTCHECK_API}/fastcheck/${encodeURIComponent(this.fastcheckNumber())}`)
|
||||
.subscribe({ error: () => undefined });
|
||||
this.fireMerchantCallback();
|
||||
},
|
||||
error: () => {
|
||||
this.popupLoading.set(false);
|
||||
this.popupError.set(this.t('errors.payment_failed'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fireMerchantCallback(): void {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const returnUrl = params.get('return_url');
|
||||
if (returnUrl) {
|
||||
setTimeout(() => {
|
||||
window.location.href = `${returnUrl}${returnUrl.includes('?') ? '&' : '?'}fastcheck=${encodeURIComponent(
|
||||
this.fastcheckNumber()
|
||||
)}&status=ok`;
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
onAmountChange(value: number | null): void {
|
||||
this.fastcheckAmount.set(value);
|
||||
}
|
||||
|
||||
/** Mask fastcheck number as XXXXXX-XXXXXX-XXXXXX, allow only digits. */
|
||||
onNumberChange(raw: string): void {
|
||||
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 18);
|
||||
const groups: string[] = [];
|
||||
for (let i = 0; i < digits.length; i += 6) {
|
||||
groups.push(digits.slice(i, i + 6));
|
||||
}
|
||||
const masked = groups.join('-');
|
||||
this.fastcheckNumber.set(masked);
|
||||
this.error.set('');
|
||||
|
||||
if (digits.length < 18 && this.lastLookedUpNumber) {
|
||||
this.fastcheckAmount.set(null);
|
||||
this.codeEnabled.set(false);
|
||||
this.lastLookedUpNumber = '';
|
||||
}
|
||||
|
||||
if (digits.length === 18 && masked !== this.lastLookedUpNumber) {
|
||||
this.lookupFastcheck(masked);
|
||||
}
|
||||
}
|
||||
|
||||
/** Allow only digits, max 6, in the code field. */
|
||||
onCodeChange(raw: string): void {
|
||||
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 6);
|
||||
this.fastcheckCode.set(digits);
|
||||
this.error.set('');
|
||||
}
|
||||
|
||||
private lookupFastcheck(number: string): void {
|
||||
this.lastLookedUpNumber = number;
|
||||
this.amountLoading.set(true);
|
||||
this.fastcheckAmount.set(null);
|
||||
this.codeEnabled.set(false);
|
||||
|
||||
// API doc: GET /fastcheck/<id>
|
||||
this.http
|
||||
.get<CheckFastcheckResponse>(`${FASTCHECK_API}/fastcheck/${number}`)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
this.amountLoading.set(false);
|
||||
if (res?.id) {
|
||||
this.fastcheckAmount.set(typeof res.amount === 'number' ? res.amount : null);
|
||||
this.codeEnabled.set(true);
|
||||
} else {
|
||||
this.error.set(this.t('errors.not_found'));
|
||||
this.lastLookedUpNumber = '';
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
this.amountLoading.set(false);
|
||||
const serverMsg: string | undefined = err?.error?.message;
|
||||
this.error.set(serverMsg ?? this.t('errors.lookup_failed'));
|
||||
this.lastLookedUpNumber = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shareByEmail(): void {
|
||||
const num = this.fastcheckNumber();
|
||||
const amount = this.fastcheckAmount();
|
||||
const subject = encodeURIComponent('fastCHECK');
|
||||
const body = encodeURIComponent(`Номер: ${num}\nСумма: ${amount} ₽\nhttps://qr.vitanova.network/`);
|
||||
window.open(`mailto:?subject=${subject}&body=${body}`, '_blank');
|
||||
}
|
||||
|
||||
shareByTelegram(): void {
|
||||
const num = this.fastcheckNumber();
|
||||
const amount = this.fastcheckAmount();
|
||||
const text = encodeURIComponent(`fastCHECK: ${num} — ${amount} ₽`);
|
||||
window.open(`https://t.me/share/url?url=https%3A%2F%2Fqr.vitanova.network%2F&text=${text}`, '_blank');
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<div class="page">
|
||||
<div class="card">
|
||||
|
||||
<div class="card__header">
|
||||
<div class="sbp-logo">
|
||||
<img src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg"
|
||||
alt="СБП" />
|
||||
</div>
|
||||
<h1 class="card__title">{{ 'sbp.title' | translate }}</h1>
|
||||
<p class="card__subtitle">{{ 'sbp.subtitle' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card__body">
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="amount">{{ 'sbp.amount_label' | translate }}</label>
|
||||
<div class="input-wrap" [class.input-wrap--error]="error()">
|
||||
<span class="input-wrap__prefix">₽</span>
|
||||
<input
|
||||
id="amount"
|
||||
type="number"
|
||||
class="input-wrap__input"
|
||||
[ngModel]="amount()"
|
||||
(ngModelChange)="onAmountChange($event)"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
placeholder="0"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
@if (error()) {
|
||||
<span class="field__error">{{ error() }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="currency-badge">
|
||||
<span class="currency-badge__flag">🇷🇺</span>
|
||||
<span class="currency-badge__code">RUB</span>
|
||||
<span class="currency-badge__name">{{ 'sbp.currency_name' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="note">{{ 'sbp.note_label' | translate }}</label>
|
||||
<textarea
|
||||
id="note"
|
||||
class="note-input"
|
||||
[ngModel]="note()"
|
||||
(ngModelChange)="onNoteChange($event)"
|
||||
[placeholder]="'sbp.note_placeholder' | translate"
|
||||
rows="3"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@if (nspkUrl()) {
|
||||
<div class="qr-pay">
|
||||
<img
|
||||
[src]="'https://api.qrserver.com/v1/create-qr-code/?size=240x240&margin=8&data=' + nspkUrl()"
|
||||
width="240" height="240"
|
||||
alt="SBP QR"
|
||||
/>
|
||||
<p class="qr-pay__hint">Отсканируйте QR-код в приложении вашего банка</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="pay-btn" type="button" (click)="pay()" [disabled]="loading() || !!nspkUrl()">
|
||||
<span class="pay-btn__icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
</span>
|
||||
@if (loading()) {
|
||||
{{ 'sbp.pay_loading' | translate }}
|
||||
} @else {
|
||||
{{ 'sbp.pay_btn' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card__footer">
|
||||
<span class="secure-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
{{ 'common.secure' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,81 +0,0 @@
|
||||
@use './../../../shared' as *;
|
||||
|
||||
.sbp-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 16px;
|
||||
padding: 12px 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
margin-bottom: 14px;
|
||||
|
||||
img {
|
||||
height: 40px;
|
||||
display: block;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.currency-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&__flag { font-size: 22px; line-height: 1; }
|
||||
&__code { font-size: 15px; font-weight: 700; color: #0f172a; }
|
||||
&__name { font-size: 13px; color: #64748b; margin-left: auto; }
|
||||
}
|
||||
|
||||
.qr-pay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.note-input {
|
||||
width: 100%;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
padding: 14px 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #0f172a;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
|
||||
line-height: 1.5;
|
||||
|
||||
&::placeholder { color: #cbd5e1; font-weight: 400; }
|
||||
|
||||
&:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { Component, computed, inject, isDevMode, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { TranslatePipe } from '../../translate/translate.pipe';
|
||||
import { TranslationService } from '../../translate/translation.service';
|
||||
|
||||
interface LegacyPayResponse {
|
||||
nspkurl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy SBP merchant payment flow.
|
||||
* Activated when the root URL has `?id=<orderId>`.
|
||||
* Mirrors public/payment.html behaviour:
|
||||
* POST https://qr.vitanova.network:567/qr
|
||||
* { payment, amount, currency, id, note } -> { payload: '<sbp-deep-link>' }
|
||||
* then window.location.href = payload.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-legacy-pay-page',
|
||||
imports: [FormsModule, TranslatePipe],
|
||||
templateUrl: './legacy-pay-page.html',
|
||||
styleUrl: './legacy-pay-page.scss'
|
||||
})
|
||||
export class LegacyPayPage {
|
||||
private http = inject(HttpClient);
|
||||
private route = inject(ActivatedRoute);
|
||||
private i18n = inject(TranslationService);
|
||||
|
||||
private t(key: string): string { return this.i18n.translate(key); }
|
||||
|
||||
private readonly LEGACY_API = isDevMode()
|
||||
? '/proxy/legacy-qr/qr'
|
||||
: 'https://qr.vitanova.network:567/qr';
|
||||
|
||||
amount = signal<number | null>(null);
|
||||
note = signal<string>('');
|
||||
error = signal<string>('');
|
||||
loading = signal<boolean>(false);
|
||||
nspkUrl = signal<string>('');
|
||||
|
||||
get isMobile(): boolean {
|
||||
return window.innerWidth < 768;
|
||||
}
|
||||
|
||||
paymentId = signal<string>('');
|
||||
|
||||
canPay = computed(() => {
|
||||
const a = this.amount();
|
||||
return !!this.paymentId() && a !== null && a > 0 && !this.loading();
|
||||
});
|
||||
|
||||
constructor() {
|
||||
const id = this.route.snapshot.queryParamMap.get('id') ?? '';
|
||||
this.paymentId.set(id);
|
||||
}
|
||||
|
||||
onAmountChange(value: number | null): void {
|
||||
this.amount.set(value);
|
||||
if (this.error()) this.error.set('');
|
||||
}
|
||||
|
||||
onNoteChange(value: string): void {
|
||||
this.note.set(value);
|
||||
}
|
||||
|
||||
pay(): void {
|
||||
if (!this.canPay()) {
|
||||
if (!this.paymentId()) {
|
||||
this.error.set(this.t('errors.not_found'));
|
||||
} else {
|
||||
this.error.set(this.t('errors.invalid_amount'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.error.set('');
|
||||
this.loading.set(true);
|
||||
|
||||
const body = {
|
||||
qrtype: 'QRDynamic',
|
||||
amount: this.amount(),
|
||||
currency: 'RUB',
|
||||
partnerqrID: this.paymentId(),
|
||||
qrDescription: this.note().trim()
|
||||
};
|
||||
|
||||
this.http.post<LegacyPayResponse>(this.LEGACY_API, body).subscribe({
|
||||
next: (res) => {
|
||||
this.loading.set(false);
|
||||
if (res?.nspkurl) {
|
||||
if (this.isMobile) {
|
||||
window.location.href = res.nspkurl;
|
||||
} else {
|
||||
this.nspkUrl.set(res.nspkurl);
|
||||
}
|
||||
} else {
|
||||
this.error.set(this.t('errors.payment_failed'));
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.loading.set(false);
|
||||
this.error.set(this.t('errors.lookup_failed'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user