1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Payment & Top Up Balance - GreatFireVPN</title> 7 <meta name="description" content="GreatFireVPN payment page â top up your balance. Pay with Visa/Mastercard, cryptocurrency, or other payment methods. Instant activation."> 8 <link rel="canonical" href="https://greatfirevpn.com/en/payment" /> 9 <link rel="alternate" hreflang="x-default" href="https://greatfirevpn.com/en/payment" /> 10 <link rel="alternate" hreflang="en" href="https://greatfirevpn.com/en/payment" /> 11 <link rel="alternate" hreflang="ru" href="https://greatfirevpn.com/ru/payment" /> 12 <link rel="alternate" hreflang="zh-Hans" href="https://greatfirevpn.com/zh/payment" /> 13 <link rel="alternate" hreflang="fa" href="https://greatfirevpn.com/fa/payment" /> 14 <link rel="alternate" hreflang="vi" href="https://greatfirevpn.com/vi/payment" /> 15 <link rel="alternate" hreflang="my" href="https://greatfirevpn.com/my/payment" /> 16 <link rel="alternate" hreflang="tr" href="https://greatfirevpn.com/tr/payment" /> 17 <link rel="alternate" hreflang="es" href="https://greatfirevpn.com/es/payment" /> 18 <meta property="og:type" content="website"> 19 <meta property="og:title" content="Payment & Top Up Balance - GreatFireVPN"> 20 <meta property="og:description" content="GreatFireVPN payment page â top up your balance. Pay with Visa/Mastercard, cryptocurrency, or other payment methods. Instant activation."> 21 <meta property="og:url" content="https://greatfirevpn.com/en/payment"> 22 <meta property="og:image" content="https://greatfirevpn.com/assets/og-image.png"> 23 <meta name="twitter:card" content="summary_large_image"> 24 <meta name="twitter:title" content="Payment & Top Up Balance - GreatFireVPN"> 25 <meta name="twitter:description" content="GreatFireVPN payment page â top up your balance. Pay with Visa/Mastercard, cryptocurrency, or other payment methods. Instant activation."> 26 <meta name="twitter:image" content="https://greatfirevpn.com/assets/og-image.png"> 27 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 28 <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon.png"> 29 <link rel="apple-touch-icon" href="/images/webclip.png"> 30 31 <!-- Google tag (gtag.js) -->
vendor: 68 bytes, lines 31-32
31 32 <script async src="https://www.googletagmanager.com/gtag/js?id=
32G-0K1MNBSGKK
vendor: 16 bytes, lines 32-33
32"></script> 33
33<script> 34
vendor: 152 bytes, lines 34-38
34window.dataLayer = window.dataLayer || []; 35 function gtag(){dataLayer.push(arguments);} 36 gtag('js', new Date()); 37 38 gtag('config', '
38G-0K1MNBSGKK
vendor: 8 bytes, lines 38-39
38'); 39
39</script>
39 40 41 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> 42 <style> 43 :root { 44 --primary-red: #E70E02; 45 --primary-orange: #FF7F11; 46 --primary-blue: #011140; 47 --accent-blue: #1E90FF; 48 --text-color: #333; 49 --bg-light: #f8f9fa; 50 --bg-dark: #0a0a0a; 51 --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); 52 --card-shadow: 0 4px 20px rgba(0,0,0,0.08); 53 --hover-shadow: 0 15px 35px rgba(0,0,0,0.15); 54 } 55 56 .sr-only { 57 position: absolute; 58 width: 1px; 59 height: 1px; 60 padding: 0; 61 margin: -1px; 62 overflow: hidden; 63 clip: rect(0, 0, 0, 0); 64 white-space: nowrap; 65 border: 0; 66 } 67 68 body { 69 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; 70 margin: 0; 71 padding: 20px; 72 background: var(--bg-light); 73 color: var(--text-color); 74 line-height: 1.6; 75 } 76 77 .container { 78 max-width: 600px; 79 margin: 0 auto; 80 background: white; 81 padding: 20px; 82 border-radius: 20px; 83 box-shadow: var(--card-shadow); 84 } 85 86 .logo { 87 text-align: center; 88 margin-bottom: 20px; 89 } 90 91 .logo img { 92 max-width: 250px; 93 height: auto; 94 transition: var(--transition); 95 } 96 97 .logo img:hover { 98 transform: scale(1.05); 99 filter: brightness(1.1); 100 } 101 102 .user-info { 103 text-align: center; 104 margin-bottom: 20px; 105 padding: 15px; 106 background: var(--bg-light); 107 border-radius: 10px; 108 border: 1px solid rgba(0,0,0,0.05); 109 } 110 111 .logout-link { 112 display: inline-block; 113 margin-left: 10px; 114 font-size: 0.8em; 115 color: #999; 116 text-decoration: none; 117 cursor: pointer; 118 font-weight: normal; 119 } 120 121 .logout-link:hover { 122 color: var(--primary-orange); 123 text-decoration: underline; 124 } 125 126 .price-tag { 127 background: linear-gradient(135deg, rgba(255,127,17,0.1), rgba(231,14,2,0.1)); 128 border-radius: 12px; 129 padding: 15px 20px; 130 margin: 0 auto 30px; 131 max-width: 250px; 132 text-align: center; 133 font-weight: bold; 134 color: var(--primary-blue); 135 font-size: 24px; 136 border: 1px solid rgba(255,127,17,0.2); 137 transition: var(--transition); 138 } 139 140 .package-grid { 141 display: grid; 142 grid-template-columns: repeat(2, 1fr); 143 gap: 10px; 144 margin: 20px 0; 145 } 146 147 .package-card { 148 background: white; 149 border-radius: 15px; 150 padding: 12px; 151 text-align: center; 152 cursor: pointer; 153 transition: var(--transition); 154 border: 2px solid transparent; 155 box-shadow: var(--card-shadow); 156 position: relative; 157 } 158 159 .package-card:hover { 160 transform: translateY(-3px); 161 box-shadow: var(--hover-shadow); 162 } 163 164 .package-card.selected { 165 border-color: var(--primary-orange); 166 background: linear-gradient(135deg, rgba(255,127,17,0.05), rgba(231,14,2,0.05)); 167 } 168 169 .package-duration { 170 font-size: 20px; 171 font-weight: bold; 172 color: var(--primary-blue); 173 margin-bottom: 6px; 174 } 175 176 .package-price { 177 font-size: 24px; 178 font-weight: bold; 179 color: var(--primary-orange); 180 margin-bottom: 4px; 181 } 182 183 .package-price-rub { 184 font-size: 14px; 185 color: #666; 186 margin-bottom: 8px; 187 } 188 189 .package-price-cny { 190 font-size: 14px; 191 color: #666; 192 margin-bottom: 8px; 193 } 194 195 .best-value { 196 position: absolute; 197 top: -8px; 198 right: -8px; 199 background: linear-gradient(135deg, var(--primary-orange), var(--primary-red)); 200 color: white; 201 padding: 3px 10px; 202 border-radius: 15px; 203 font-size: 11px; 204 font-weight: bold; 205 box-shadow: var(--card-shadow); 206 } 207 208 .payment-options { 209 display: grid; 210 gap: 15px; 211 margin-top: 20px; 212 } 213 214 .payment-button { 215 padding: 12px 20px; 216 border: none; 217 border-radius: 12px; 218 font-size: 18px; 219 font-weight: 500; 220 cursor: pointer; 221 transition: var(--transition); 222 background: linear-gradient(45deg, var(--primary-blue), #1a2980); 223 color: white; 224 display: flex; 225 align-items: center; 226 justify-content: center; 227 position: relative; 228 box-shadow: var(--card-shadow); 229 } 230 231 .payment-button:hover { 232 transform: translateY(-2px); 233 box-shadow: var(--hover-shadow); 234 } 235 236 .payment-button:active { 237 transform: translateY(1px); 238 } 239 240 .payment-button i { 241 margin-right: 12px; 242 } 243 244 245 .payment-button.payment-button-unavailable, 246 .payment-button.payment-button-unavailable:hover, 247 .payment-button.payment-button-unavailable:active { 248 background: #cfd4dc; 249 color: #8a8f99;
250 cursor: not-allowed; 251 transform: none; 252 box-shadow: none; 253 opacity: 0.85; 254 } 255 256 .message { 257 padding: 15px; 258 margin: 15px 0; 259 border-radius: 10px; 260 text-align: center; 261 display: none; 262 transition: var(--transition); 263 } 264 265 .success { 266 background: linear-gradient(135deg, rgba(40, 167, 69, 0.1), rgba(40, 167, 69, 0.2)); 267 color: #155724; 268 border: 1px solid rgba(40, 167, 69, 0.3); 269 } 270 271 .error { 272 background: linear-gradient(135deg, rgba(231, 14, 2, 0.1), rgba(231, 14, 2, 0.2)); 273 color: #721c24; 274 border: 1px solid rgba(231, 14, 2, 0.3); 275 } 276 277 .loading .payment-button { 278 opacity: 0.7; 279 cursor: not-allowed; 280 } 281 282 .loading .payment-button::after { 283 content: ''; 284 position: absolute; 285 width: 20px; 286 height: 20px; 287 border: 2px solid rgba(255,255,255,0.3); 288 border-radius: 50%; 289 border-top-color: #ffffff; 290 animation: spin 1s ease-in-out infinite; 291 margin-left: 10px; 292 } 293 294 @keyframes spin { 295 to { transform: rotate(360deg); } 296 } 297 298 /* Account lookup form styles */ 299 .account-lookup { 300 display: none; 301 margin-bottom: 30px; 302 animation: fadeIn 0.5s ease; 303 } 304 305 .account-lookup h2 { 306 color: var(--primary-blue); 307 margin-bottom: 15px; 308 text-align: center; 309 } 310 311 .account-lookup p { 312 color: #666; 313 text-align: center; 314 margin-bottom: 20px; 315 } 316 317 .account-lookup input { 318 width: calc(100% - 30px); 319 padding: 15px; 320 margin: 10px auto; 321 border: 1px solid rgba(0,0,0,0.1); 322 border-radius: 10px; 323 font-size: 16px; 324 transition: var(--transition); 325 background: var(--bg-light); 326 box-sizing: border-box; 327 display: block; 328 } 329 330 .account-lookup input:focus { 331 outline: none; 332 border-color: var(--primary-orange); 333 box-shadow: 0 0 0 3px rgba(255,127,17,0.1); 334 } 335 336 .account-lookup button { 337 width: calc(100% - 30px); 338 padding: 15px; 339 background: linear-gradient(45deg, var(--primary-blue), #1a2980); 340 color: white; 341 border: none; 342 border-radius: 10px; 343 font-size: 16px; 344 font-weight: 500; 345 cursor: pointer; 346 transition: var(--transition); 347 margin: 10px auto; 348 box-sizing: border-box; 349 display: block; 350 } 351 352 #lookup-error-message { 353 width: calc(100% - 30px); 354 margin: 10px auto; 355 box-sizing: border-box; 356 } 357 358 .account-lookup button:hover { 359 transform: translateY(-2px); 360 box-shadow: var(--hover-shadow); 361 } 362 363 .account-lookup button:disabled { 364 background: #ccc; 365 cursor: not-allowed; 366 transform: none; 367 box-shadow: none; 368 } 369 370 @media (min-width: 480px) { 371 .package-grid { 372 gap: 15px; 373 } 374 375 .package-card { 376 padding: 15px; 377 } 378 379 .package-duration { 380 font-size: 22px; 381 margin-bottom: 8px; 382 } 383 384 .package-price { 385 font-size: 26px; 386 margin-bottom: 5px; 387 } 388 389 .package-price-rub { 390 font-size: 16px; 391 margin-bottom: 10px; 392 } 393 394 .package-price-cny { 395 font-size: 16px; 396 margin-bottom: 10px; 397 } 398 399 .best-value { 400 top: -10px; 401 right: -10px; 402 padding: 4px 12px; 403 font-size: 12px; 404 } 405 } 406 407 /* Promo code styles */ 408 .promo-section { 409 margin: 15px 0; 410 padding: 15px; 411 background: var(--bg-light); 412 border-radius: 12px; 413 border: 1px solid rgba(0,0,0,0.05); 414 overflow: hidden; 415 box-sizing: border-box; 416 } 417 418 .promo-input-group { 419 display: flex; 420 gap: 10px; 421 align-items: center; 422 max-width: 100%; 423 } 424 425 .promo-input-group input { 426 flex: 1; 427 min-width: 0; 428 padding: 12px 15px; 429 border: 1px solid rgba(0,0,0,0.1); 430 border-radius: 10px; 431 font-size: 15px; 432 text-transform: uppercase; 433 letter-spacing: 1px; 434 transition: var(--transition); 435 background: white; 436 box-sizing: border-box; 437 } 438 439 .promo-input-group input:focus { 440 outline: none; 441 border-color: var(--primary-orange); 442 box-shadow: 0 0 0 3px rgba(255,127,17,0.1); 443 } 444 445 .promo-input-group button { 446 padding: 12px 20px; 447 background: linear-gradient(45deg, var(--primary-blue), #1a2980); 448 color: white; 449 border: none; 450 border-radius: 10px; 451 font-size: 15px; 452 font-weight: 500; 453 cursor: pointer; 454 transition: var(--transition); 455 white-space: nowrap; 456 flex-shrink: 0; 457 box-sizing: border-box; 458 } 459 460 .promo-input-group button:hover { 461 transform: translateY(-1px); 462 box-shadow: var(--card-shadow); 463 } 464 465 .promo-input-group button:disabled { 466 opacity: 0.6;
467 cursor: not-allowed; 468 transform: none; 469 } 470 471 .promo-status { 472 margin-top: 10px; 473 padding: 10px 15px; 474 border-radius: 8px; 475 font-size: 14px; 476 display: none; 477 } 478 479 .promo-status.promo-success { 480 background: linear-gradient(135deg, rgba(40, 167, 69, 0.1), rgba(40, 167, 69, 0.2)); 481 color: #155724; 482 border: 1px solid rgba(40, 167, 69, 0.3); 483 display: block; 484 } 485 486 .promo-status.promo-error { 487 background: linear-gradient(135deg, rgba(231, 14, 2, 0.1), rgba(231, 14, 2, 0.2)); 488 color: #721c24; 489 border: 1px solid rgba(231, 14, 2, 0.3); 490 display: block; 491 } 492 493 .bonus-badge { 494 display: inline-block; 495 background: linear-gradient(135deg, #28a745, #20c997); 496 color: white; 497 padding: 2px 8px; 498 border-radius: 10px; 499 font-size: 12px; 500 font-weight: bold; 501 margin-top: 4px; 502 animation: fadeIn 0.3s ease; 503 } 504 505 @keyframes fadeIn { 506 from { opacity: 0; transform: translateY(20px); } 507 to { opacity: 1; transform: translateY(0); } 508 } 509 510 .language-selector { 511 position: fixed; 512 top: 20px; 513 right: 20px; 514 z-index: 1000; 515 } 516 517 .language-selector select { 518 padding: 8px 12px; 519 border: 2px solid var(--primary-blue); 520 border-radius: 20px; 521 background: white; 522 color: var(--primary-blue); 523 font-weight: 500; 524 cursor: pointer; 525 transition: var(--transition); 526 } 527 528 .language-selector select:hover { 529 border-color: var(--primary-orange); 530 } 531 532 @media (max-width: 768px) { 533 .language-selector { 534 position: relative; 535 text-align: center; 536 margin: 30px auto 20px; 537 right: auto; 538 top: auto; 539 left: auto; 540 transform: none; 541 background: transparent; 542 padding: 0; 543 box-shadow: none; 544 } 545 } 546 547 /* Freekassa API method picker (test page only) */ 548 .fk-method-panel { 549 display: none; 550 margin-top: -5px; 551 padding: 16px; 552 background: var(--bg-light); 553 border-radius: 12px; 554 border: 1px solid rgba(0,0,0,0.05); 555 animation: fadeIn 0.3s ease; 556 } 557 558 .fk-method-panel.open { display: block; } 559 560 .fk-method-tiles { 561 display: grid; 562 /* 2 columns on small screens (3rd tile wraps); 3 columns from 480px up */ 563 grid-template-columns: repeat(2, 1fr); 564 gap: 10px; 565 margin-bottom: 16px; 566 } 567 568 @media (min-width: 480px) { 569 .fk-method-tiles { 570 grid-template-columns: repeat(3, 1fr); 571 } 572 } 573 574 .fk-method-tile { 575 background: white; 576 border: 2px solid transparent; 577 border-radius: 12px; 578 padding: 14px 8px; 579 text-align: center; 580 cursor: pointer; 581 transition: var(--transition); 582 box-shadow: var(--card-shadow); 583 } 584 585 .fk-method-tile:hover { 586 transform: translateY(-2px); 587 box-shadow: var(--hover-shadow); 588 } 589 590 .fk-method-tile.selected { 591 border-color: var(--primary-orange); 592 background: linear-gradient(135deg, rgba(255,127,17,0.05), rgba(231,14,2,0.05)); 593 } 594 595 .fk-method-tile.disabled { 596 opacity: 0.4; 597 cursor: not-allowed; 598 background: #f3f4f6; 599 box-shadow: none; 600 } 601 602 .fk-method-tile.disabled:hover { 603 transform: none; 604 box-shadow: none; 605 } 606 607 .fk-method-tile.disabled .fk-method-name::after { 608 content: " (unavailable)"; 609 font-size: 11px; 610 font-weight: 400; 611 color: #999; 612 } 613 614 .fk-method-icon { 615 font-size: 26px; 616 margin-bottom: 6px; 617 color: var(--primary-blue); 618 } 619 620 .fk-method-name { 621 font-weight: 600; 622 font-size: 15px; 623 color: var(--primary-blue); 624 margin-bottom: 2px; 625 } 626 627 .fk-method-desc { 628 font-size: 12px; 629 color: #666; 630 } 631 632 .fk-email-group { 633 margin-bottom: 14px; 634 } 635 636 .fk-email-group label { 637 display: block; 638 font-size: 13px; 639 color: #555; 640 margin-bottom: 6px; 641 } 642 643 .fk-email-group input { 644 width: 100%; 645 padding: 12px 14px; 646 border: 1px solid rgba(0,0,0,0.1); 647 border-radius: 10px; 648 font-size: 15px; 649 background: white; 650 box-sizing: border-box; 651 transition: var(--transition); 652 } 653 654 .fk-email-group input:focus { 655 outline: none; 656 border-color: var(--primary-orange); 657 box-shadow: 0 0 0 3px rgba(255,127,17,0.1); 658 } 659 660 .fk-email-group input.invalid { 661 border-color: var(--primary-red); 662 box-shadow: 0 0 0 3px rgba(231,14,2,0.1); 663 } 664 665 .fk-continue-button { 666 width: 100%; 667 background: linear-gradient(135deg, var(--primary-orange), var(--primary-red)); 668 font-weight: 600; 669 font-size: 17px; 670 letter-spacing: 0.3px; 671 box-shadow: 0 6px 18px rgba(231, 14, 2, 0.25); 672 } 673 674 .fk-continue-button:hover:not(:disabled) { 675 background: linear-gradient(135deg, #ff8a2a, #ff2a1c); 676 transform: translateY(-2px); 677 box-shadow: 0 10px 24px rgba(231, 14, 2, 0.35); 678 } 679 680 .fk-continue-button .fk-continue-arrow { 681 margin-left: 10px; 682 transition: var(--transition); 683 } 684 685 .fk-continue-button:hover:not(:disabled) .fk-continue-arrow { 686 transform: translateX(4px); 687 } 688 689 .fk-continue-button:disabled { 690 background: #cfd4dc; 691 color: #8a8f99;
692 cursor: not-allowed; 693 transform: none; 694 box-shadow: none; 695 font-weight: 500; 696 } 697 698 .fk-continue-button:disabled .fk-continue-arrow { 699 opacity: 0.6; 700 } 701 702 .fk-test-badge { 703 display: inline-block; 704 margin-left: 8px; 705 padding: 2px 8px; 706 background: linear-gradient(135deg, var(--primary-orange), var(--primary-red)); 707 color: white; 708 border-radius: 10px; 709 font-size: 10px; 710 font-weight: bold; 711 letter-spacing: 0.5px; 712 vertical-align: middle; 713 } 714 </style> 715</head> 716<body> 717<div class="container"> 718 <h1 class="sr-only">Payment & Top Up Balance - GreatFireVPN</h1> 719 <div class="logo"> 720 <a href="/en/"> 721 <img id="logo-image" src="/assets/logo-blue.png" alt="GreatFire VPN Logo"> 722 </a> 723 </div> 724 725 <!-- Account lookup form (shown when no userId is provided) --> 726 <div id="account-lookup" class="account-lookup" style="display: none;"> 727 <h2>Enter Your Account Name</h2> 728 <p>Please enter your account name to continue with payment.<br>Download the app to register.</p> 729 <input type="text" id="account-name" placeholder="Enter your account name"> 730 <div id="lookup-error-message" class="message error"></div> 731 <button id="lookup-button">Continue</button> 732 </div> 733 734 <!-- Payment section (shown after account lookup or when userId is provided) --> 735 <div id="payment-section" style="display: none;"> 736 <div class="user-info"> 737 <p><span id="topup-text">Top up balance for:</span> <strong id="username">Loading...</strong><a class="logout-link" onclick="logoutAccount()">(switch account)</a></p> 738 </div> 739 740 <div class="package-grid"> 741 <!-- Package grid will be populated by loadPackages function --> 742 </div> 743 744 <div class="promo-section"> 745 <div class="promo-input-group"> 746 <input type="text" id="promo-code-input" placeholder="Enter promo code"> 747 <button id="apply-promo-btn" onclick="applyPromoCode()">Apply</button> 748 </div> 749 <div id="promo-status" class="promo-status"></div> 750 </div> 751 752 <div id="success-message" class="message success"></div> 753 <div id="error-message" class="message error"></div> 754 <div class="payment-options" id="payment-options"> 755 <button class="payment-button payment-button-unavailable" id="stripe-button" type="button" disabled aria-disabled="true" title="Currently unavailable"> 756 <i class="fab fa-stripe"></i> 757 <span id="stripe-text">Pay with Visa/Mastercard (currently unavailable)</span> 758 </button> 759 <button class="payment-button" onclick="addBalance('crypto')"> 760 <i class="fab fa-bitcoin"></i> 761 <span id="crypto-text">Pay with Cryptocurrency</span> 762 </button> 763 <!-- 764 Legacy SCI handler (kept for revert): onclick="addBalance('russian')" 765 Now overridden at runtime by initFkApiFlow() -> toggleFkPanel(). 766 --> 767 <button class="payment-button" id="russian-button" onclick="addBalance('russian')"> 768 <i class="fas fa-credit-card"></i> 769 <span id="russian-card-text">Pay from Russia</span> 770 <!-- TEST badge removed for production; preserved for revert: 771 <span id="fk-test-badge" class="fk-test-badge" style="display:none;">TEST</span> 772 --> 773 </button> 774 <div class="fk-method-panel" id="fk-method-panel"> 775 <div class="fk-method-tiles" id="fk-method-tiles"> 776 <div class="fk-method-tile" data-method-id="44" onclick="onFkTileClick(this)"> 777 <div class="fk-method-icon"><i class="fas fa-bolt"></i></div> 778 <div class="fk-method-name">СÐÐ</div> 779 <div class="fk-method-desc">QR / Faster Payments</div> 780 </div> 781 <div class="fk-method-tile" data-method-id="43" onclick="onFkTileClick(this)"> 782 <div class="fk-method-icon"><i class="fas fa-mobile-screen-button"></i></div> 783 <div class="fk-method-name">SberPay</div> 784 <div class="fk-method-desc">Sberbank app</div> 785 </div> 786 <div class="fk-method-tile" data-method-id="36" onclick="onFkTileClick(this)"> 787 <div class="fk-method-icon"><i class="fas fa-credit-card"></i></div> 788 <div class="fk-method-name">Bank Card</div> 789 <div class="fk-method-desc">Visa / MC / ÐÐÐ </div> 790 </div> 791 </div> 792 <div class="fk-email-group"> 793 <label for="fk-email">Email for receipt</label> 794 <input type="email" id="fk-email" placeholder="[email protected]" autocomplete="email" inputmode="email"> 795 </div> 796 <button class="payment-button fk-continue-button" id="fk-continue" onclick="submitFkPayment()" disabled>
797 <span>Continue to payment</span> 798 <i class="fas fa-arrow-right fk-continue-arrow" aria-hidden="true"></i> 799 </button> 800 <div id="fk-error" class="message error" style="display:none; margin-top: 12px;"></div> 801 </div> 802 <button class="payment-button" onclick="addBalance('chinese1')"> 803 <i class="fas fa-yen-sign"></i> 804 <span id="chinese1-text">Pay from China</span> 805 </button> 806 </div> 807 </div> 808 809 <div class="language-selector"> 810 <select id="language-select"> 811 <option value="en" selected>English</option> 812 <option value="zh">䏿</option> 813 <option value="ru">Ð ÑÑÑкий</option> 814 <option value="fa">ÙØ§Ø±Ø³Û</option> 815 <option value="vi">Tiếng Viá»t</option> 816 <option value="my">áá¼ááºáá¬</option> 817 <option value="tr">Türkçe</option> 818 <option value="es">Español</option> 819 </select> 820 </div> 821 <p style="text-align:center;margin-top:20px;font-size:14px;color:#666">Paying from inside Iran? <a href="/en/pay-from-iran" style="color:var(--primary-orange);font-weight:600;text-decoration:none">Read our payment guide →</a></p> 822</div> 823
824<script> 825 const API_URL = 'https://greatfirevpn.com/gfpayments'; 826 827 // Freekassa API flow. 828 // Previously gated to the test page only (kept commented for fast revert): 829 // const FK_API_ENABLED = window.location.pathname.startsWith('/test-payment'); 830 // Now enabled everywhere â production uses the same worker contract. 831 const FK_API_ENABLED = true; 832 833 const urlParams = new URLSearchParams(window.location.search); 834 let userId = urlParams.get('user_id'); 835 let displayName = urlParams.get('display_name'); 836 837 // Handle MERCHANT_ORDER_ID parameter if present 838 // That's Freekassa-specific for the return URL 839 const merchantOrderId = urlParams.get('MERCHANT_ORDER_ID'); 840 if (merchantOrderId && !userId) { 841 // Extract user_id from MERCHANT_ORDER_ID (format: gfvpn_userid_days_timestamp) 842 const parts = merchantOrderId.split('_'); 843 if (parts.length >= 2) { 844 userId = parts[1]; // The user_id is the second part 845 } 846 } 847 848 // Force locale to match the page language 849 const locale = 'en'; 850 851 // Static translations for this page 852 const lang = { 853 success: 'Successfully updated your balance!', 854 error: 'Error processing payment. Please try again later.', 855 select_package: 'Please select a package first', 856 best_value: 'Best Value', 857 days: { '30': '30 Days', '90': '90 Days', '180': '180 Days', '365': '365 Days' }, 858 lookup_error: 'Account not found. Please check your account name and try again.', 859 lookup_button: 'Continue', 860 promo_empty: 'Please enter a promo code', 861 promo_invalid: 'Promo code not found', 862 promo_expired: 'This promo code has expired', 863 promo_removed: 'Promo code removed', 864 promo_bonus_days: 'bonus days', 865 promo_bonus: 'PROMO', 866 days_unit: 'days', 867 promo_apply: 'Apply', 868 fk_choose_method: 'Please choose a payment method', 869 fk_invalid_email: 'Please enter a valid email', 870 fk_continue: 'Continue to payment', 871 fk_processing: 'Processing...' 872 }; 873 874 let selectedDays = null; 875 let packages = []; 876 let activePromo = null; 877 let selectedFkMethodId = null; 878 879 // GA4 event tracking helper 880 function gfTrack(eventName, params) { 881 if (typeof gtag === 'function') { 882 gtag('event', eventName, params || {}); 883 } 884 } 885 886 // Load packages from JSON file 887 async function loadPackages() { 888 try { 889 const response = await fetch('/packages.json'); 890 const data = await response.json(); 891 packages = data.packages; 892 generatePackageGrid(); 893 } catch (error) { 894 console.error('Failed to load packages:', error); 895 // Fallback to hardcoded packages 896 packages = [ 897 { days: 30, price_usd: 4.99, price_rub: 399, price_cny: 39, featured: false }, 898 { days: 90, price_usd: 13.99, price_rub: 1199, price_cny: 99, featured: false }, 899 { days: 180, price_usd: 24.99, price_rub: 1999, price_cny: 179, featured: false }, 900 { days: 365, price_usd: 39.99, price_rub: 3499, price_cny: 279, featured: true } 901 ]; 902 generatePackageGrid(); 903 } 904 } 905 906 // Generate package grid from loaded data 907 function generatePackageGrid() { 908 const packageGrid = document.querySelector('.package-grid'); 909 if (!packageGrid) return; 910 911 packageGrid.innerHTML = ''; 912 913 packages.forEach(package => { 914 const packageCard = document.createElement('div'); 915 packageCard.className = 'package-card'; 916 packageCard.dataset.days = package.days; 917 packageCard.onclick = () => selectPackage(packageCard); 918 919 if (package.featured) { 920 packageCard.innerHTML = ` 921 <div class="best-value">${lang.best_value}</div> 922 <div class="package-duration">${lang.days[package.days]}</div> 923 <div class="package-price">$${package.price_usd}</div> 924 <div class="package-price-rub" style="display: none;">${package.price_rub} RUB</div> 925 <div class="package-price-cny" style="display: none;">Â¥${package.price_cny}</div> 926 `; 927 } else { 928 packageCard.innerHTML = ` 929 <div class="package-duration">${lang.days[package.days]}</div> 930 <div class="package-price">$${package.price_usd}</div> 931 <div class="package-price-rub" style="display: none;">${package.price_rub} RUB</div> 932 <div class="package-price-cny" style="display: none;">Â¥${package.price_cny}</div> 933 `; 934 } 935 936 packageGrid.appendChild(packageCard); 937 }); 938 } 939 940 function selectPackage(element) { 941 // Remove selected class from all packages 942 document.querySelectorAll('.package-card').forEach(card => { 943 card.classList.remove('selected'); 944 }); 945 946 // Add selected class to clicked package 947 element.classList.add('selected'); 948 949 // Store selected days 950 selectedDays = parseInt(element.dataset.days); 951 952 const pkg = packages.find(p => p.days === selectedDays); 953 gfTrack('package_select', { 954 event_category: 'engagement', 955 days: selectedDays, 956 price_usd: pkg ? pkg.price_usd : null, 957 promo_code: activePromo ? activePromo.code : 'none' 958 }); 959 } 960 961 // Promo code functions 962 async function applyPromoCode() { 963 const input = document.getElementById('promo-code-input'); 964 const status = document.getElementById('promo-status'); 965 const btn = document.getElementById('apply-promo-btn'); 966 const code = input.value.trim().toUpperCase(); 967 968 if (!code) { 969 status.className = 'promo-status promo-error'; 970 status.textContent = lang.promo_empty; 971 gfTrack('promo_code_apply', { event_category: 'engagement', result: 'empty' }); 972 return; 973 } 974 975 // If same promo is already active, remove it 976 if (activePromo && activePromo.code === code) { 977 activePromo = null; 978 status.className = 'promo-status promo-error'; 979 status.textContent = lang.promo_removed; 980 input.value = ''; 981 updatePackageDisplay(); 982 gfTrack('promo_code_apply', { event_category: 'engagement', result: 'removed', promo_code: code }); 983 setTimeout(() => { status.className = 'promo-status'; }, 3000); 984 return; 985 } 986 987 try { 988 btn.disabled = true; 989 btn.textContent = '...'; 990 991 const response = await fetch(`${API_URL}
991/validate-promo`, { 992 method: 'POST', 993 headers: { 'Content-Type': 'application/json' }, 994 body: JSON.stringify({ promo_code: code }) 995 }); 996 997 // Handle 404 or other errors gracefully (endpoint not configured yet) 998 if (!response.ok) { 999 status.className = 'promo-status promo-error'; 1000 status.textContent = lang.promo_invalid; 1001 activePromo = null; 1002 updatePackageDisplay(); 1003 gfTrack('promo_code_apply', { event_category: 'engagement', result: 'invalid', promo_code: code }); 1004 return; 1005 } 1006 1007 const data = await response.json(); 1008 1009 if (!data.valid) { 1010 status.className = 'promo-status promo-error'; 1011 status.textContent = lang.promo_invalid; 1012 activePromo = null; 1013 updatePackageDisplay(); 1014 gfTrack('promo_code_apply', { event_category: 'engagement', result: 'invalid', promo_code: code }); 1015 return; 1016 } 1017 1018 // Valid promo 1019 activePromo = { 1020 code: code, 1021 bonus_days: data.bonus_days, 1022 bonus_percent: data.bonus_percent 1023 }; 1024 1025 let bonusDesc; 1026 if (data.bonus_days) { 1027 bonusDesc = '+' + data.bonus_days + ' ' + lang.promo_bonus_days; 1028 } else if (data.bonus_percent) { 1029 bonusDesc = '+' + data.bonus_percent + '% ' + lang.promo_bonus_days; 1030 } 1031 1032 status.className = 'promo-status promo-success'; 1033 status.textContent = 'â ' + code + ' â ' + bonusDesc; 1034 updatePackageDisplay(); 1035 gfTrack('promo_code_apply', { 1036 event_category: 'engagement', 1037 result: 'success', 1038 promo_code: code, 1039 bonus_days: data.bonus_days || null, 1040 bonus_percent: data.bonus_percent || null 1041 }); 1042 1043 } catch (error) { 1044 // Network errors or endpoint not found - show error 1045 status.className = 'promo-status promo-error'; 1046 status.textContent = lang.promo_invalid; 1047 activePromo = null; 1048 updatePackageDisplay(); 1049 gfTrack('promo_code_apply', { event_category: 'engagement', result: 'error', promo_code: code }); 1050 } finally { 1051 btn.disabled = false; 1052 btn.textContent = lang.promo_apply; 1053 } 1054 } 1055 1056 function getBonusDays(baseDays) { 1057 if (!activePromo) return 0; 1058 if (activePromo.bonus_days) return activePromo.bonus_days; 1059 if (activePromo.bonus_percent) return Math.floor(baseDays * activePromo.bonus_percent / 100); 1060 return 0; 1061 } 1062 1063 function updatePackageDisplay() { 1064 const cards = document.querySelectorAll('.package-card'); 1065 cards.forEach(card => { 1066 const days = parseInt(card.dataset.days); 1067 const durationEl = card.querySelector('.package-duration'); 1068 const existingBadge = card.querySelector('.bonus-badge'); 1069 if (existingBadge) existingBadge.remove(); 1070 1071 // Restore original label 1072 durationEl.textContent = lang.days[days]; 1073 1074 if (activePromo) { 1075 const bonus = getBonusDays(days); 1076 if (bonus > 0) { 1077 durationEl.textContent = days + ' + ' + bonus + ' ' + lang.days_unit; 1078 const badge = document.createElement('div'); 1079 badge.className = 'bonus-badge'; 1080 badge.textContent = 'ð ' + lang.promo_bonus; 1081 durationEl.after(badge); 1082 } 1083 } 1084 }); 1085 } 1086 1087 // Function to fetch account name from user ID 1088 async function fetchAccountName(userId) { 1089 try { 1090 const response = await fetch(`${API_URL}/get-account-name`, { 1091 method: 'POST', 1092 headers: { 1093 'Content-Type': 'application/json', 1094 }, 1095 body: JSON.stringify({ user_id: userId }) 1096 }); 1097 1098 if (!response.ok) { 1099 throw new Error('Failed to fetch account name'); 1100 } 1101 1102 const data = await response.json(); 1103 return data; 1104 } catch (error) { 1105 return null; 1106 } 1107 } 1108 1109 // Function to fetch account ID from account name 1110 // For non-email account names, returns its own UUID 1111 // For email account names, returns the (linked) primary account UUID 1112 async function fetchAccountId(accountName) { 1113 try { 1114 const response = await fetch(`${API_URL}
1114/get-primary-account-uuid`, { 1115 method: 'POST', 1116 headers: { 1117 'Content-Type': 'application/json', 1118 }, 1119 body: JSON.stringify({ 1120 account_name: accountName 1121 }) 1122 }); 1123 1124 const data = await response.json(); 1125 1126 if (!response.ok) { 1127 throw new Error(data.error || 'Account lookup failed'); 1128 } 1129 1130 return data; 1131 } catch (error) { 1132 throw error; 1133 } 1134 } 1135 1136 // Initialize the page 1137 async function initializePage() { 1138 // Use absolute path only on the main domain 1139 if (window.location.hostname === 'greatfirevpn.com') { 1140 document.getElementById('logo-image').src = '/assets/logo-blue.png'; 1141 } 1142 1143 // Load packages first 1144 await loadPackages(); 1145 1146 // English page: Show USD prices (default, already visible) 1147 1148 // Show/hide sections based on whether userId is provided 1149 const accountLookup = document.getElementById('account-lookup'); 1150 const paymentSection = document.getElementById('payment-section'); 1151 1152 if (!userId) { 1153 // Show account lookup, hide payment section 1154 accountLookup.style.display = 'block'; 1155 paymentSection.style.display = 'none'; 1156 1157 // Add event listener for account lookup 1158 document.getElementById('lookup-button').addEventListener('click', lookupAccount); 1159 } else { 1160 // Show payment section and hide account lookup 1161 accountLookup.style.display = 'none'; 1162 paymentSection.style.display = 'block'; 1163 1164 // If display name is not provided, fetch it 1165 if (!displayName) { 1166 const accountName = await fetchAccountName(userId); 1167 displayName = accountName || userId; 1168 } 1169 1170 document.getElementById('username').textContent = displayName; 1171 } 1172 1173 initFkApiFlow(); 1174 } 1175 1176 // Logout / switch account 1177 function logoutAccount() { 1178 userId = null; 1179 displayName = null; 1180 document.getElementById('account-name').value = ''; 1181 document.getElementById('lookup-error-message').style.display = 'none'; 1182 document.getElementById('account-lookup').style.display = 'block'; 1183 document.getElementById('payment-section').style.display = 'none'; 1184 const newUrl = new URL(window.location.href); 1185 newUrl.searchParams.delete('user_id'); 1186 newUrl.searchParams.delete('display_name'); 1187 window.history.pushState({}, '', newUrl); 1188 } 1189 1190 // Account lookup function 1191 async function lookupAccount() { 1192 const accountName = document.getElementById('account-name').value.trim(); 1193 const lookupButton = document.getElementById('lookup-button'); 1194 const errorMsg = document.getElementById('lookup-error-message'); 1195 1196 if (!accountName) { 1197 errorMsg.textContent = lang.lookup_error; 1198 errorMsg.style.display = 'block'; 1199 gfTrack('account_lookup_submit', { event_category: 'engagement', result: 'empty' }); 1200 return; 1201 } 1202 1203 try { 1204 lookupButton.disabled = true; 1205 lookupButton.textContent = 'Looking up...'; 1206 errorMsg.style.display = 'none'; // Clear any previous error 1207 1208 const accountId = await fetchAccountId(accountName); 1209 1210 if (!accountId || typeof accountId !== 'string' || accountId.length !== 36) { 1211 throw new Error('Invalid account ID received'); 1212 } 1213 1214 userId = accountId; 1215 displayName = accountName; 1216 gfTrack('account_lookup_submit', { event_category: 'engagement', result: 'success' }); 1217 1218 // Update UI 1219 document.getElementById('account-lookup').style.display = 'none'; 1220 document.getElementById('payment-section').style.display = 'block'; 1221 document.getElementById('username').textContent = displayName; 1222 1223 // Update URL without reloading 1224 const newUrl = new URL(window.location.href); 1225 newUrl.searchParams.set('user_id', userId); 1226 newUrl.searchParams.set('display_name', displayName); 1227 window.history.pushState({}, '', newUrl); 1228 1229 } catch (error) { 1230 errorMsg.textContent = lang.lookup_error; 1231 errorMsg.style.display = 'block'; 1232 gfTrack('account_lookup_submit', { event_category: 'engagement', result: 'not_found' }); 1233 } finally { 1234 lookupButton.disabled = false;
1235 lookupButton.textContent = lang.lookup_button; 1236 } 1237 } 1238 1239 // ===== Freekassa API method picker (test page only) ===== 1240 1241 // Centralised error reporter: keeps the actual message in the dev tools 1242 // (and GA), returns the generic user-facing string for display. 1243 // Use for any payment-related error that we don't want to leak verbatim. 1244 function reportPaymentError(context, error, extra) { 1245 const detail = (error && (error.message || error.toString())) || 'unknown'; 1246 // eslint-disable-next-line no-console 1247 console.error(`[payment:${context}]`, error, extra || ''); 1248 gfTrack('payment_error', { 1249 event_category: 'error', 1250 context, 1251 error: detail 1252 }); 1253 return lang.error; 1254 } 1255 1256 function isValidEmail(email) { 1257 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); 1258 } 1259 1260 function updateFkContinueButton() { 1261 const btn = document.getElementById('fk-continue'); 1262 if (!btn) return; 1263 const email = (document.getElementById('fk-email').value || '').trim(); 1264 btn.disabled = !(selectedFkMethodId && isValidEmail(email)); 1265 } 1266 1267 function onFkTileClick(tileEl) { 1268 if (tileEl.classList.contains('disabled')) return; 1269 const id = parseInt(tileEl.dataset.methodId, 10); 1270 if (!id) return; 1271 selectFkMethod(id); 1272 } 1273 1274 function selectFkMethod(methodId) { 1275 selectedFkMethodId = methodId; 1276 document.querySelectorAll('.fk-method-tile').forEach(tile => { 1277 tile.classList.toggle('selected', parseInt(tile.dataset.methodId, 10) === methodId); 1278 }); 1279 updateFkContinueButton(); 1280 gfTrack('fk_method_select', { 1281 event_category: 'engagement', 1282 paymentSystemId: methodId, 1283 days: selectedDays 1284 }); 1285 } 1286 1287 async function loadFkPaymentSystems() { 1288 try { 1289 const resp = await fetch(`${API_URL}/freekassa-payment-systems`); 1290 if (!resp.ok) throw new Error('HTTP ' + resp.status); 1291 const data = await resp.json(); 1292 if (!data || !data.success || !Array.isArray(data.currencies)) { 1293 throw new Error('bad response'); 1294 } 1295 const activeIds = new Set( 1296 data.currencies 1297 .filter(c => c && c.is_enabled === true) 1298 .map(c => c.id) 1299 ); 1300 applyFkAvailability(activeIds); 1301 gfTrack('fk_methods_loaded', { 1302 event_category: 'engagement', 1303 count: activeIds.size 1304 }); 1305 } catch (e) { 1306 // Fail open: leave tiles enabled with their hardcoded ids. 1307 // The worker will reject the order if the id is actually inactive. 1308 // eslint-disable-next-line no-console 1309 console.warn('[payment:fk_methods_load]', e); 1310 gfTrack('fk_methods_error', { 1311 event_category: 'engagement', 1312 error: (e && e.message) || 'unknown' 1313 }); 1314 } 1315 } 1316 1317 // Toggle each tile's .disabled class based on the merchant's active id list. 1318 // Tiles keep their fixed data-method-id (44 / 36); we never remap. 1319 function applyFkAvailability(activeIds) { 1320 document.querySelectorAll('.fk-method-tile').forEach(tile => { 1321 const id = parseInt(tile.dataset.methodId, 10); 1322 const available = activeIds.has(id); 1323 tile.classList.toggle('disabled', !available); 1324 1325 // If the user had this tile selected and it just became unavailable, deselect. 1326 if (!available && selectedFkMethodId === id) { 1327 selectedFkMethodId = null; 1328 tile.classList.remove('selected'); 1329 updateFkContinueButton(); 1330 } 1331 }); 1332 } 1333 1334 function toggleFkPanel() { 1335 const panel = document.getElementById('fk-method-panel'); 1336 const errorMsg = document.getElementById('error-message'); 1337 1338 // Require a package first - mirror addBalance() behaviour 1339 if (!selectedDays) { 1340 errorMsg.textContent = lang.select_package; 1341 errorMsg.style.display = 'block'; 1342 gfTrack('payment_method_click', { 1343 event_category: 'conversion', 1344 payment_method: 'russian_api', 1345 result: 'no_package_selected' 1346 }); 1347 return; 1348 } 1349 errorMsg.style.display = 'none'; 1350 1351 const wasOpen = panel.classList.contains('open'); 1352 if (wasOpen) { 1353 panel.classList.remove('open'); 1354 gfTrack('fk_panel_close', { event_category: 'engagement' }); 1355 } else { 1356 panel.classList.add('open'); 1357 // Always reflect the real state on open (disabled until method + valid email) 1358 updateFkContinueButton(); 1359 gfTrack('fk_panel_open', { 1360 event_category: 'engagement', 1361 days: selectedDays, 1362 promo_code: activePromo ? activePromo.code : 'none' 1363 }); 1364 // Smooth-scroll the panel into view on small screens 1365 setTimeout(() => panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 50); 1366 } 1367 } 1368 1369 async function submitFkPayment() { 1370 const continueBtn = document.getElementById('fk-continue'); 1371 const errorEl = document.getElementById('fk-error'); 1372 const emailInput = document.getElementById('fk-email'); 1373 const email = emailInput.value.trim(); 1374 1375 errorEl.style.display = 'none'; 1376 emailInput.classList.remove('invalid'); 1377 1378 if (!selectedDays) { 1379 errorEl.textContent = lang.select_package; 1380 errorEl.style.display = 'block'; 1381 return; 1382 } 1383 if (!selectedFkMethodId) { 1384 errorEl.textContent = lang.fk_choose_method; 1385 errorEl.style.display = 'block'; 1386 return; 1387 } 1388 if (!isValidEmail(email)) { 1389 emailInput.classList.add('invalid'); 1390 errorEl.textContent = lang.fk_invalid_email; 1391 errorEl.style.display = 'block'; 1392 emailInput.focus(); 1393 return; 1394 } 1395 1396 const requestBody = { 1397 userId, 1398 paymentMethod: 'russian_api', 1399 paymentSystemId: selectedFkMethodId, 1400 email, 1401 days: selectedDays 1402 }; 1403 if (activePromo) requestBody.promo_code = activePromo.code; 1404 1405 gfTrack('fk_create_order_attempt', { 1406 event_category: 'conversion', 1407 paymentSystemId: selectedFkMethodId, 1408 days: selectedDays, 1409 promo_code: activePromo ? activePromo.code : 'none' 1410 }); 1411 1412 try { 1413 continueBtn.disabled = true; 1414 const originalLabel = continueBtn.querySelector('span').textContent; 1415 continueBtn.querySelector('span').textContent = lang.fk_processing; 1416 1417 const response = await fetch(`${API_URL}
1417/handle-payment`, { 1418 method: 'POST', 1419 headers: { 'Content-Type': 'application/json' }, 1420 body: JSON.stringify(requestBody) 1421 }); 1422 1423 let data = null; 1424 try { data = await response.json(); } catch (_) { /* non-JSON body */ } 1425 1426 if (!response.ok || !data || !data.url) { 1427 throw new Error((data && data.error) || ('HTTP ' + response.status)); 1428 } 1429 1430 gfTrack('fk_create_order_success', { 1431 event_category: 'conversion', 1432 paymentSystemId: selectedFkMethodId, 1433 days: selectedDays 1434 }); 1435 1436 window.location.href = data.url; 1437 // Keep button disabled while we navigate away 1438 } catch (error) { 1439 errorEl.textContent = reportPaymentError('fk_create_order', error, { 1440 paymentSystemId: selectedFkMethodId, 1441 days: selectedDays 1442 }); 1443 errorEl.style.display = 'block'; 1444 continueBtn.disabled = false; 1445 continueBtn.querySelector('span').textContent = lang.fk_continue; 1446 } 1447 } 1448 1449 function initFkApiFlow() { 1450 if (!FK_API_ENABLED) return; 1451 1452 const russianBtn = document.getElementById('russian-button'); 1453 if (russianBtn) { 1454 // Override the SCI handler with the panel toggle 1455 russianBtn.onclick = toggleFkPanel; 1456 } 1457 1458 // TEST badge removed for production rollout (kept for revert): 1459 // const badge = document.getElementById('fk-test-badge'); 1460 // if (badge) badge.style.display = 'inline-block'; 1461 1462 const emailInput = document.getElementById('fk-email'); 1463 if (emailInput) { 1464 emailInput.addEventListener('input', () => { 1465 emailInput.classList.remove('invalid'); 1466 updateFkContinueButton(); 1467 }); 1468 } 1469 1470 // Discover which methods are actually enabled on the merchant. 1471 // Non-blocking â tiles render with their hardcoded ids first, then get 1472 // greyed out when the response arrives if their id isn't in `is_enabled`. 1473 loadFkPaymentSystems(); 1474 } 1475 1476 async function addBalance(paymentMethod) { 1477 const container = document.getElementById('payment-options'); 1478 const successMsg = document.getElementById('success-message'); 1479 const errorMsg = document.getElementById('error-message'); 1480 const buttons = document.querySelectorAll('.payment-button'); 1481 1482 if (!selectedDays) { 1483 errorMsg.textContent = lang.select_package; 1484 errorMsg.style.display = 'block'; 1485 gfTrack('payment_method_click', { 1486 event_category: 'conversion', 1487 payment_method: paymentMethod, 1488 result: 'no_package_selected' 1489 }); 1490 return; 1491 } 1492 1493 gfTrack('payment_method_click', { 1494 event_category: 'conversion', 1495 payment_method: paymentMethod, 1496 days: selectedDays, 1497 promo_code: activePromo ? activePromo.code : 'none' 1498 }); 1499 1500 try { 1501 container.classList.add('loading'); 1502 buttons.forEach(button => button.disabled = true); 1503 successMsg.style.display = 'none'; 1504 errorMsg.style.display = 'none'; 1505 1506 // Prepare base request body 1507 const requestBody = { 1508 userId, 1509 paymentMethod, 1510 days: selectedDays 1511 }; 1512 1513 // Include promo code if active 1514 if (activePromo) { 1515 requestBody.promo_code = activePromo.code; 1516 } 1517 1518 const response = await fetch(`${API_URL}/handle-payment`, { 1519 method: 'POST', 1520 headers: { 1521 'Content-Type': 'application/json', 1522 }, 1523 body: JSON.stringify(requestBody) 1524 }); 1525 1526 const data = await response.json(); 1527 1528 if (!response.ok) { 1529 throw new Error(data.error || 'Payment processing failed'); 1530 } 1531 1532 // Handle response based on payment method 1533 if (paymentMethod === 'crypto' && data.url) { 1534 // Redirect to the crypto payment page 1535 window.location.href = data.url; 1536 return; 1537 } else if (paymentMethod === 'stripe' && data.checkoutUrl) { 1538 // Just redirect to Stripe's checkout URL as is 1539 window.location.href = data.checkoutUrl; 1540 return; 1541 } else if (paymentMethod === 'russian' && data.url) { 1542 // Redirect to the Russian payment page 1543 window.location.href = data.url; 1544 return; 1545 } else if (paymentMethod === 'chinese1' && data.url) { 1546 // Redirect to the Chinese payment page 1547 window.location.href = data.url; 1548 return; 1549 } 1550 1551 // Show success message for other payment methods which directly increase balance 1552 successMsg.textContent = lang.success; 1553 successMsg.style.display = 'block'; 1554 1555 setTimeout(() => { 1556 window.close(); 1557 }, 3000); 1558 1559 } catch (error) { 1560 errorMsg.textContent = error.message || lang.error; 1561 errorMsg.style.display = 'block'; 1562 } finally { 1563 container.classList.remove('loading'); 1564 buttons.forEach(button => { if (!button.classList.contains('payment-button-unavailable')) button.disabled = false; }); 1565 } 1566 } 1567 1568 // Handle back button/close 1569 window.addEventListener('pageshow', function(event) { 1570 const container = document.getElementById('payment-options'); 1571 const buttons = document.querySelectorAll('.payment-button'); 1572 container.classList.remove('loading'); 1573 buttons.forEach(button => { if (!button.classList.contains('payment-button-unavailable')) button.disabled = false; }); 1574 }); 1575 1576 // Initialize the page when DOM is loaded 1577 document.addEventListener('DOMContentLoaded', initializePage); 1578 1579 // Handle payment success/cancel URLs 1580 const path = window.location.pathname; 1581 const successMsg = document.getElementById('success-message'); 1582 const errorMsg = document.getElementById('error-message'); 1583 1584 if (path.includes('/payment-success')) { 1585 // Show success message 1586 successMsg.textContent = lang.success; 1587 successMsg.style.display = 'block'; 1588 1589 // Optional: Redirect back to app after short delay 1590 setTimeout(() => { 1591 window.close(); 1592 }, 3000); 1593 } else if (path.includes('/payment-cancel')) { 1594 // Show cancel message 1595 errorMsg.textContent = lang.error; 1596 errorMsg.style.display = 'block'; 1597 } 1598</script>
1598 1599
1600<script> 1601 // Language selector functionality 1602 const languageSelect = document.getElementById('language-select'); 1603 languageSelect.addEventListener('change', function(e) { 1604 const newLang = e.target.value; 1605 gfTrack('language_change', { 1606 event_category: 'engagement', 1607 language_from: locale, 1608 language_to: newLang 1609 }); 1610 // Preserve the current URL parameters (userId, etc.) 1611 const urlParams = new URLSearchParams(window.location.search); 1612 const newUrl = '/' + newLang + '/payment' + (urlParams.toString() ? '?' + urlParams.toString() : ''); 1613 window.location.href = newUrl; 1614 }); 1615</script>
1615 1616</body> 1617</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.