| @@ -293,8 +293,21 @@ | ||
| 293 | 293 | 'message' => __('New customer registration is disabled.', 'yatra'), |
| 294 | 294 | ], 403); |
| 295 | 295 | } |
| 296 | 296 | |
| 297 | + // reCAPTCHA v3 (no-op unless the registration form is protected in settings). | |
| 298 | + $recaptcha = \Yatra\Services\RecaptchaService::verifyForm( | |
| 299 | + 'registration', | |
| 300 | + (string) ($request->get_param('recaptcha_token') ?? ''), | |
| 301 | + $_SERVER['REMOTE_ADDR'] ?? null | |
| 302 | + ); | |
| 303 | + if (empty($recaptcha['success'])) { | |
| 304 | + return new \WP_REST_Response([ | |
| 305 | + 'success' => false, | |
| 306 | + 'message' => $recaptcha['message'] ?? __('reCAPTCHA verification failed.', 'yatra'), | |
| 307 | + ], 400); | |
| 308 | + } | |
| 309 | + | |
| 297 | 310 | $first_name = sanitize_text_field($request->get_param('first_name') ?? ''); |
| 298 | 311 | $last_name = sanitize_text_field($request->get_param('last_name') ?? ''); |
| 299 | 312 | $email = sanitize_email($request->get_param('email') ?? ''); |
| 300 | 313 | $phone = sanitize_text_field($request->get_param('phone') ?? ''); |
| @@ -529,27 +542,38 @@ | ||
| 529 | 542 | } |
| 530 | 543 | |
| 531 | 544 | $stored_token = get_user_meta($user_id, 'yatra_verification_token', true); |
| 532 | 545 | $token_expiry = get_user_meta($user_id, 'yatra_verification_token_expiry', true); |
| 533 | - | |
| 546 | + | |
| 547 | + // Idempotent path: the token+expiry are deleted as soon as a successful | |
| 548 | + // verification completes (see below), so a second click on the same | |
| 549 | + // link previously fell into the "Invalid or expired" branch and made | |
| 550 | + // already-verified customers believe their account was broken. Detect | |
| 551 | + // the prior-success state explicitly and reuse the success page so the | |
| 552 | + // outcome is clear regardless of how many times the link is clicked. | |
| 553 | + $already_verified = get_user_meta($user_id, 'yatra_email_verified', true) === '1'; | |
| 554 | + if ($already_verified) { | |
| 555 | + self::showVerificationSuccess(true); | |
| 556 | + return; | |
| 557 | + } | |
| 558 | + | |
| 534 | 559 | if (empty($stored_token) || $stored_token !== $token) { |
| 535 | 560 | self::showVerificationError(__('Invalid or expired verification link.', 'yatra')); |
| 536 | 561 | return; |
| 537 | 562 | } |
| 538 | - | |
| 563 | + | |
| 539 | 564 | if ($token_expiry && time() > (int) $token_expiry) { |
| 540 | 565 | self::showVerificationError(__('This verification link has expired. Please register again.', 'yatra')); |
| 541 | 566 | return; |
| 542 | 567 | } |
| 543 | - | |
| 568 | + | |
| 544 | 569 | // Mark as verified |
| 545 | 570 | update_user_meta($user_id, 'yatra_email_verified', '1'); |
| 546 | 571 | delete_user_meta($user_id, 'yatra_verification_token'); |
| 547 | 572 | delete_user_meta($user_id, 'yatra_verification_token_expiry'); |
| 548 | - | |
| 549 | - $redirect_url = add_query_arg(['email_verified' => '1'], yatra_get_checkout_url()); | |
| 550 | - wp_safe_redirect($redirect_url); | |
| 551 | - exit; | |
| 573 | + | |
| 574 | + self::showVerificationSuccess(false); | |
| 575 | + return; | |
| 552 | 576 | } |
| 553 | 577 | |
| 554 | 578 | /** |
| 555 | 579 | * Send verification email |
| @@ -578,9 +602,9 @@ | ||
| 578 | 602 | : __('If you did not create this account, you can ignore this email.', 'yatra'); |
| 579 | 603 | |
| 580 | 604 | $expiryNoticeHtml = esc_html( |
| 581 | 605 | sprintf( |
| 582 | - /* translators: %d: hours until expiry */ | |
| 606 | + /* translators: %d: hours until link expiry */ | |
| 583 | 607 | __('This verification link expires in %d hours for your security.', 'yatra'), |
| 584 | 608 | 24 |
| 585 | 609 | ) |
| 586 | 610 | ); |
| @@ -590,8 +614,9 @@ | ||
| 590 | 614 | $email, |
| 591 | 615 | [ |
| 592 | 616 | 'customer_first_name' => $first_name, |
| 593 | 617 | 'customer_name' => $first_name, |
| 618 | + 'customer_email' => $email, | |
| 594 | 619 | 'verification_link' => $verificationUrl, |
| 595 | 620 | 'intro_paragraph' => $introParagraph, |
| 596 | 621 | 'footer_note' => $footerNote, |
| 597 | 622 | 'expiry_notice_html' => $expiryNoticeHtml, |
| @@ -599,21 +624,81 @@ | ||
| 599 | 624 | ); |
| 600 | 625 | } |
| 601 | 626 | |
| 602 | 627 | /** |
| 603 | - * Show verification error page | |
| 628 | + * Show verification error page (hard-fail dead-end with a route back home). | |
| 629 | + * Every string is translatable — operators run Yatra in many locales and | |
| 630 | + * the pre-3.0.5 hardcoded "Verification Failed" heading was untranslatable. | |
| 604 | 631 | */ |
| 605 | 632 | private static function showVerificationError(string $message): void |
| 606 | 633 | { |
| 634 | + $heading = esc_html__('Verification Failed', 'yatra'); | |
| 635 | + $cta = esc_html__('Go to Homepage', 'yatra'); | |
| 636 | + | |
| 607 | 637 | wp_die( |
| 608 | 638 | '<div style="text-align: center; padding: 50px; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;"> |
| 609 | - <h1 style="color: #dc2626; margin-bottom: 20px;">Verification Failed</h1> | |
| 639 | + <h1 style="color: #dc2626; margin-bottom: 20px;">' . $heading . '</h1> | |
| 610 | 640 | <p style="color: #4b5563; font-size: 16px; margin-bottom: 30px;">' . esc_html($message) . '</p> |
| 611 | - <a href="' . esc_url(home_url()) . '" style="display: inline-block; background: #3b82f6; color: #fff; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Go to Homepage</a> | |
| 641 | + <a href="' . esc_url(home_url()) . '" style="display: inline-block; background: #3b82f6; color: #fff; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">' . $cta . '</a> | |
| 612 | 642 | </div>', |
| 613 | 643 | __('Verification Failed', 'yatra'), |
| 614 | 644 | ['response' => 400] |
| 615 | 645 | ); |
| 646 | + } | |
| 647 | + | |
| 648 | + /** | |
| 649 | + * Render an unambiguous "email verified" confirmation page. | |
| 650 | + * | |
| 651 | + * The pre-3.0.5 flow silently 302-redirected to the checkout URL with a | |
| 652 | + * `?email_verified=1` flag, relying on `booking-auth.php` to surface a | |
| 653 | + * one-liner notice. That notice only renders when the auth form itself | |
| 654 | + * renders (no booking session / logged-in user → notice never shown), so | |
| 655 | + * customers regularly saw "nothing happened" after clicking the link. | |
| 656 | + * | |
| 657 | + * Now we render a dedicated success page with explicit confirmation, the | |
| 658 | + * verified email-state, and a primary CTA back into the booking flow. | |
| 659 | + * Idempotent: a second click on the same link reaches `$already=true` | |
| 660 | + * and shows "Your email is already verified" instead of the misleading | |
| 661 | + * "Invalid or expired link" error. | |
| 662 | + * | |
| 663 | + * @param bool $already True when the user has already been verified by an | |
| 664 | + * earlier click on the same link (idempotent path). | |
| 665 | + */ | |
| 666 | + private static function showVerificationSuccess(bool $already): void | |
| 667 | + { | |
| 668 | + $checkoutUrl = function_exists('yatra_get_checkout_url') | |
| 669 | + ? yatra_get_checkout_url() | |
| 670 | + : home_url('/'); | |
| 671 | + $continueUrl = add_query_arg(['email_verified' => '1'], $checkoutUrl); | |
| 672 | + | |
| 673 | + $heading = $already | |
| 674 | + ? esc_html__('Email Already Verified', 'yatra') | |
| 675 | + : esc_html__('Email Verified', 'yatra'); | |
| 676 | + $message = $already | |
| 677 | + ? esc_html__('Your email address is already verified — no further action is needed. You can continue with your booking.', 'yatra') | |
| 678 | + : esc_html__('Your email address has been verified successfully. You can now log in and continue with your booking.', 'yatra'); | |
| 679 | + $cta = esc_html__('Continue to Checkout', 'yatra'); | |
| 680 | + $homeCta = esc_html__('Go to Homepage', 'yatra'); | |
| 681 | + $title = $already | |
| 682 | + ? __('Email Already Verified', 'yatra') | |
| 683 | + : __('Email Verified', 'yatra'); | |
| 684 | + | |
| 685 | + // Inline-only styling so the page renders correctly regardless of | |
| 686 | + // theme stylesheet load order (wp_die() can fire before themes | |
| 687 | + // enqueue their styles). | |
| 688 | + $body = '<div style="text-align: center; padding: 50px 20px; max-width: 520px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;">' | |
| 689 | + . '<div style="display: inline-flex; align-items: center; justify-content: center; width: 72px; height: 72px; border-radius: 50%; background: #d1fae5; margin: 0 auto 24px;">' | |
| 690 | + . '<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' | |
| 691 | + . '<polyline points="20 6 9 17 4 12"></polyline>' | |
| 692 | + . '</svg>' | |
| 693 | + . '</div>' | |
| 694 | + . '<h1 style="color: #065f46; margin: 0 0 12px; font-size: 26px;">' . $heading . '</h1>' | |
| 695 | + . '<p style="color: #4b5563; font-size: 16px; line-height: 1.6; margin: 0 0 28px;">' . $message . '</p>' | |
| 696 | + . '<a href="' . esc_url($continueUrl) . '" style="display: inline-block; background: #059669; color: #fff; padding: 12px 28px; border-radius: 8px; text-decoration: none; font-weight: 600; margin-right: 8px;">' . $cta . '</a>' | |
| 697 | + . '<a href="' . esc_url(home_url()) . '" style="display: inline-block; color: #4b5563; padding: 12px 16px; text-decoration: none; font-weight: 500;">' . $homeCta . '</a>' | |
| 698 | + . '</div>'; | |
| 699 | + | |
| 700 | + wp_die($body, $title, ['response' => 200]); | |
| 616 | 701 | } |
| 617 | 702 | |
| 618 | 703 | /** |
| 619 | 704 | * Inform users that the link is only for email template previews. |