Plugin Directory

Changeset 3780739


Ignore:
Timestamp:
11/02/2026 09:09:50 PM (22 hours ago)
Author:
naimurrahmannahid
Message:

Release 2.1.2: optional checkout email OTP, reorganized Naimur OTP admin, editable email templates

  • Optional checkout email verification (off by default) for the Checkout block and classic checkout, enforced on the server before the order is created
  • Naimur OTP admin: Overview with status and setup checklist; one Verification page with Registration, Password Reset and Checkout tabs; Email Templates; Security; Activity & Diagnostics; General Settings; Help & Documentation
  • Plain-language settings, unsaved-changes warning, inline validation, settings finder and contextual help
  • Email HTML, security limits and general settings require a site administrator
  • Responsive email templates with a sanitized HTML editor and sandboxed live preview
  • Accessible code popups for registration, password reset and checkout
  • Server-side resend cooldown; CAPTCHA display removed (it was never verified on the server)
  • Declared WooCommerce HPOS and Cart/Checkout blocks compatibility
  • All 2.1.1 security protections retained
  • assets: screenshots 1-12
Location:
naimur-email-otp-for-woocommerce
Files:
42 added
8 deleted
38 edited
1 copied

Legend:

Unmodified
Added
Removed
  • naimur-email-otp-for-woocommerce/tags/2.1.2/assets/css/override-myaccount.css

    r3587442 r3780739  
    8888#ceovw-corporate-app .ceovw-footer-nav { text-align: center; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid #e5e7eb; font-size: 0.875rem; color: #6b7280; }
    8989
    90 /* OTP Modal */
    91 #ceovw-corporate-app .ceovw-modal-overlay {
    92     position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    93     background: rgba(17, 24, 39, 0.7); backdrop-filter: blur(4px);
    94     display: flex; justify-content: center; align-items: center; z-index: 99999;
    95 }
    96 #ceovw-corporate-app .ceovw-modal-content {
    97     background: #fff; padding: 2.5rem; border-radius: 1rem; width: 100%; max-width: 24rem;
    98     position: relative; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); text-align: center;
    99     animation: ceovw-slide-up 0.3s ease-out;
    100 }
    101 @keyframes ceovw-slide-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
    102 #ceovw-corporate-app .ceovw-modal-close {
    103     position: absolute; top: 1rem; right: 1rem; background: none; border: none; font-size: 1.5rem;
    104     cursor: pointer; color: #9ca3af; transition: color 0.2s;
    105 }
    106 #ceovw-corporate-app .ceovw-modal-close:hover { color: #111827; }
    107 #ceovw-corporate-app .ceovw-otp-inputs { display: flex; justify-content: center; gap: 0.5rem; margin: 2rem 0; }
    108 #ceovw-corporate-app .ceovw-otp-box {
    109     width: 3rem !important; height: 3.5rem !important; text-align: center; font-size: 1.5rem !important;
    110     font-weight: 700; border: 2px solid #e5e7eb !important; border-radius: 0.5rem !important; padding: 0 !important;
    111 }
    112 #ceovw-corporate-app .ceovw-otp-box:focus { border-color: var(--ceovw-brand) !important; }
    113 #ceovw-corporate-app .ceovw-error-msg { color: #dc2626; font-size: 0.875rem; margin-bottom: 1rem; height: 1.25rem; }
    114 #ceovw-corporate-app .ceovw-resend-wrapper { margin-top: 1.5rem; font-size: 0.875rem; color: #6b7280; }
    115 #ceovw-corporate-app #ceovw-btn-resend:disabled { color: #9ca3af; cursor: not-allowed; text-decoration: none; }
     90/* Inline form messages (replace browser alerts) */
     91#ceovw-corporate-app .ceovw-form-msg { margin: 0 0 1rem; padding: 0.7rem 0.9rem; border-radius: 0.6rem; font-size: 0.875rem; line-height: 1.45; text-align: left; }
     92#ceovw-corporate-app .ceovw-form-msg[hidden] { display: none; }
     93#ceovw-corporate-app .ceovw-form-msg.is-error { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
     94#ceovw-corporate-app .ceovw-form-msg.is-success { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; }
     95#ceovw-corporate-app input:focus-visible, #ceovw-corporate-app button:focus-visible, #ceovw-corporate-app a:focus-visible { outline: 2px solid var(--ceovw-brand, #2563eb); outline-offset: 2px; }
     96#ceovw-corporate-app .ceovw-btn:disabled { cursor: default; opacity: .85; }
     97
     98/* OTP Modal: same visual language as the checkout popup */
     99#ceovw-corporate-app .ceovw-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; padding: 16px; background: rgba(15, 23, 42, 0.55); display: flex; justify-content: center; align-items: center; z-index: 99999; box-sizing: border-box; }
     100#ceovw-corporate-app .ceovw-modal-content { background: #fff; padding: 2rem 1.75rem 1.25rem; border-radius: 18px; width: 100%; max-width: 420px; max-height: calc(100vh - 32px); overflow-y: auto; position: relative; box-shadow: 0 24px 60px rgba(15, 23, 42, 0.28); text-align: center; animation: ceovw-slide-up 0.2s ease-out; }
     101@keyframes ceovw-slide-up { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }
     102#ceovw-corporate-app .ceovw-modal-close { position: absolute; top: 10px; right: 12px; width: 36px; height: 36px; padding: 0; border: none; border-radius: 10px; background: transparent; font-size: 26px; line-height: 1; cursor: pointer; color: #6b7280; }
     103#ceovw-corporate-app .ceovw-modal-close:hover { background: #f3f4f6; color: #111827; }
     104#ceovw-corporate-app .ceovw-modal-icon { display: flex; align-items: center; justify-content: center; width: 64px; height: 64px; margin: 0 auto 14px; border-radius: 50%; color: var(--ceovw-brand, #2563eb); background: #eef2ff; background: color-mix(in srgb, var(--ceovw-brand, #2563eb) 12%, #fff); }
     105#ceovw-corporate-app .ceovw-modal-header h3 { margin: 0 0 8px; font-size: 21px; font-weight: 700; line-height: 1.3; color: #111827; }
     106#ceovw-corporate-app .ceovw-modal-header p { margin: 0; font-size: 14.5px; line-height: 1.55; color: #4b5563; overflow-wrap: anywhere; }
     107#ceovw-corporate-app .ceovw-otp-inputs { display: flex; justify-content: center; gap: 8px; margin: 1.25rem 0 0.25rem; }
     108#ceovw-corporate-app .ceovw-otp-box { width: 46px !important; height: 56px !important; margin: 0 !important; padding: 0 !important; text-align: center; font-size: 24px !important; font-weight: 700; color: #111827; background: #f9fafb !important; border: 1.5px solid #d1d5db !important; border-radius: 12px !important; box-shadow: none; }
     109#ceovw-corporate-app .ceovw-otp-box:focus { border-color: var(--ceovw-brand, #2563eb) !important; background: #fff !important; box-shadow: 0 0 0 3px rgba(37, 99, 235, .15); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ceovw-brand, #2563eb) 15%, transparent); outline: none; }
     110#ceovw-corporate-app .ceovw-error-msg { min-height: 1.4rem; margin: 10px 0; color: #b91c1c; font-size: 13.5px; line-height: 1.4; }
     111#ceovw-corporate-app .ceovw-error-msg.is-success { color: #047857; font-weight: 600; }
     112#ceovw-corporate-app #ceovw-btn-verify { height: 48px; border-radius: 12px; }
     113#ceovw-corporate-app .ceovw-resend-wrapper { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 6px; margin-top: 1rem; font-size: 0.875rem; color: #6b7280; }
     114#ceovw-corporate-app #ceovw-btn-resend { padding: 4px 2px; border: 0; background: none; font: inherit; font-weight: 600; color: var(--ceovw-brand, #2563eb); cursor: pointer; }
     115#ceovw-corporate-app #ceovw-btn-resend:disabled { color: #9ca3af; cursor: not-allowed; text-decoration: none; font-weight: 500; }
     116#ceovw-corporate-app .ceovw-modal-tip { margin: 12px 0 0; padding-top: 12px; border-top: 1px solid #f1f5f9; font-size: 12.5px; line-height: 1.5; color: #6b7280; }
    116117
    117118/* Responsive */
    … …  
    119120    #ceovw-corporate-app .ceovw-app-card { padding: 2rem 1.5rem; }
    120121    #ceovw-corporate-app .ceovw-field-row { flex-direction: column; gap: 0; }
    121     #ceovw-corporate-app .ceovw-otp-box { width: 2.5rem !important; height: 3rem !important; font-size: 1.25rem !important; }
     122    #ceovw-corporate-app .ceovw-modal-overlay { align-items: flex-end; padding: 0; }
     123    #ceovw-corporate-app .ceovw-modal-content { max-width: none; border-radius: 18px 18px 0 0; padding: 1.75rem 1.1rem 1.1rem; }
     124    #ceovw-corporate-app .ceovw-otp-inputs { gap: 6px; }
     125    #ceovw-corporate-app .ceovw-otp-box { width: calc((100% - 30px) / 6) !important; max-width: 48px; height: 52px !important; font-size: 22px !important; }
    122126}
     127@media (prefers-reduced-motion: reduce) {
     128    #ceovw-corporate-app .ceovw-modal-content, #ceovw-corporate-app .btn-loader { animation: none; }
     129}
  • naimur-email-otp-for-woocommerce/tags/2.1.2/assets/js/main.js

    r3780003 r3780739  
    22    "use strict";
    33
    4     let currentFlow = '';
     4    const T = (window.CEOVW_APP && CEOVW_APP.i18n) || {};
     5    let currentFlow = '';
    56    let targetEmail = '';
    67    let resendTimer = null;
    78    let resetToken = ''; // Password reset authorization: kept in memory only, never persisted or logged
     9    let lastFocus = null;
     10    let verifying = false;
     11
     12    // Inline form messages (replace browser alerts; announced by screen readers)
     13    const formMsg = ($form, text, kind) => {
     14        const $m = $form.find('.ceovw-form-msg');
     15        if (!text) { $m.prop('hidden', true).text('').removeClass('is-error is-success'); return; }
     16        $m.text(text).prop('hidden', false).toggleClass('is-success', kind === 'success').toggleClass('is-error', kind !== 'success');
     17    };
     18    const showView = (target, focus) => {
     19        $('.ceovw-view-pane').hide();
     20        const $v = $(`#ceovw-view-${target}`).fadeIn(300);
     21        $v.find('.ceovw-form-msg').prop('hidden', true).text('');
     22        if (focus !== false) { setTimeout(() => $v.find('input:not([type=hidden]):visible').first().trigger('focus'), 320); }
     23        return $v;
     24    };
     25    const missing = ($form) => $form.find('input[required]').filter(function(){ return !$.trim(this.value); }).first();
     26    const validEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test($.trim(v));
    827
    928    // View Switcher (Smooth Fade)
    … …  
    1231        const target = $(this).data('target');
    1332        if (target !== 'newpass') resetToken = '';
    14         $('.ceovw-view-pane').hide();
    15         $(`#ceovw-view-${target}`).fadeIn(300);
     33        showView(target);
    1634    });
    1735
    1836    const setBtnLoading = ($btn, isLoading) => {
    1937        if(isLoading) {
    20             $btn.prop('disabled', true).addClass('loading');
     38            $btn.prop('disabled', true).addClass('loading').attr('aria-busy', 'true');
    2139        } else {
    22             $btn.prop('disabled', false).removeClass('loading');
     40            $btn.prop('disabled', false).removeClass('loading').attr('aria-busy', 'false');
    2341        }
    2442    };
    2543
    26     const getCaptchaToken = ($form) => {
    27         if (CEOVW_APP.settings.captcha === 'turnstile') {
    28             return $form.find('[name="cf-turnstile-response"]').val() || '';
    29         } else if (CEOVW_APP.settings.captcha === 'recaptcha') {
    30             return $form.find('[name="g-recaptcha-response"]').val() || '';
    31         }
    32         return '';
     44    // Resend delay: server value when provided, otherwise the per-flow setting
     45    const resendDelay = (res) => {
     46        const s = res && res.data ? parseInt(res.data.resend_in, 10) : 0;
     47        const cfg = (CEOVW_APP.settings && CEOVW_APP.settings.resend) || {};
     48        return s > 0 ? s : (parseInt(cfg[currentFlow], 10) || 60);
    3349    };
    3450
    … …  
    3955        let pass = $form.find('input[name="password"]').val();
    4056        let confirm = $form.find('input[name="password_confirm"]').val();
    41        
    42         if (pass !== confirm) {
    43             alert("Passwords do not match!");
    44             return;
    45         }
    46 
    4757        let email = $form.find('input[name="email"]').val();
    4858        let name = $form.find('input[name="ceovw_full_name"]').val();
     59        const $empty = missing($form);
     60
     61        if ($empty.length) { formMsg($form, T.required); $empty.trigger('focus'); return; }
     62        if (!validEmail(email)) { formMsg($form, T.badEmail); $form.find('input[name="email"]').trigger('focus'); return; }
     63        if (pass !== confirm) { formMsg($form, T.mismatch); $form.find('input[name="password_confirm"]').trigger('focus'); return; }
     64        formMsg($form, '');
     65
    4966        let $btn = $('#ceovw-btn-reg');
    50 
    5167        setBtnLoading($btn, true);
    5268
    … …  
    5470            action: 'ceovw_process_registration_req',
    5571            nonce: CEOVW_APP.nonce,
    56             email: email, name: name, password: pass,
    57             captcha: getCaptchaToken($form)
     72            email: email, name: name, password: pass
    5873        }, function(res){
    5974            setBtnLoading($btn, false);
    … …  
    6277                targetEmail = email;
    6378                resetToken = '';
    64                 openOTPModal(email); // JSON Success হলে এই পপআপটি ওপেন হবে
    65             } else {
    66                 alert(res.data.message || CEOVW_APP.i18n.error);
    67             }
    68         }).fail(function(xhr, status, error) {
    69             setBtnLoading($btn, false);
    70             console.error("AJAX Error:", error);
    71             alert(CEOVW_APP.i18n.error);
     79                openOTPModal(email, resendDelay(res));
     80            } else {
     81                formMsg($form, (res.data && res.data.message) || T.error);
     82            }
     83        }).fail(function() {
     84            setBtnLoading($btn, false);
     85            formMsg($form, T.error);
    7286        });
    7387    });
    … …  
    7892        let $form = $(this);
    7993        let email = $form.find('input[name="reset_email"]').val();
     94        if (!validEmail(email)) { formMsg($form, T.badEmail); $form.find('input[name="reset_email"]').trigger('focus'); return; }
     95        formMsg($form, '');
    8096        let $btn = $('#ceovw-btn-forgot');
    8197        resetToken = '';
    … …  
    86102            action: 'ceovw_process_forgot_req',
    87103            nonce: CEOVW_APP.nonce,
    88             email: email,
    89             captcha: getCaptchaToken($form)
     104            email: email
    90105        }, function(res){
    91106            setBtnLoading($btn, false);
    … …  
    93108                currentFlow = 'forgot';
    94109                targetEmail = email;
    95                 openOTPModal(email);
    96             } else {
    97                 alert(res.data.message || CEOVW_APP.i18n.error);
    98             }
    99         }).fail(function() { 
    100             setBtnLoading($btn, false); 
    101             alert(CEOVW_APP.i18n.error);
     110                openOTPModal(email, resendDelay(res));
     111            } else {
     112                formMsg($form, (res.data && res.data.message) || T.error);
     113            }
     114        }).fail(function() {
     115            setBtnLoading($btn, false);
     116            formMsg($form, T.error);
    102117        });
    103118    });
    … …  
    106121    const $modal = $('#ceovw-otp-modal');
    107122    const $otpInputs = $('.ceovw-otp-box');
    108    
    109     function openOTPModal(email) {
     123    const $otpMsg = $('#ceovw-otp-error');
     124    const otpMessage = (text, kind) => { $otpMsg.text(text || '').toggleClass('is-success', kind === 'success'); };
     125    const code = () => $otpInputs.map(function(){ return $(this).val(); }).get().join('');
     126
     127    function openOTPModal(email, wait) {
     128        lastFocus = document.activeElement;
    110129        $('#ceovw-target-email').text(email);
    111130        $otpInputs.val('');
    112         $('#ceovw-otp-error').text('');
     131        otpMessage('');
     132        verifying = false;
    113133        $modal.fadeIn(200);
    114         $otpInputs.first().focus();
    115         startResendTimer(60);
     134        $otpInputs.first().trigger('focus');
     135        startResendTimer(wait || resendDelay());
    116136    }
    117137
    118     $modal.find('.ceovw-modal-close').on('click', () => $modal.fadeOut(200));
     138    function closeOTPModal() {
     139        $modal.fadeOut(200);
     140        if (lastFocus && document.body.contains(lastFocus)) { lastFocus.focus(); }
     141    }
     142
     143    $modal.find('.ceovw-modal-close').on('click', closeOTPModal);
     144    $modal.on('click', function(e){ if (e.target === this) { closeOTPModal(); } });
     145
     146    // Keyboard: Escape closes, Tab stays inside the dialog
     147    $modal.on('keydown', function(e){
     148        if (e.key === 'Escape') { e.preventDefault(); closeOTPModal(); return; }
     149        if (e.key !== 'Tab') return;
     150        const $f = $modal.find('button:not(:disabled), input').filter(':visible');
     151        if (!$f.length) return;
     152        const first = $f[0], last = $f[$f.length - 1];
     153        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
     154        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
     155    });
     156
     157    // Spread several digits (paste or one-time-code autofill) across the boxes starting at index `from`
     158    const fill = (digits, from) => {
     159        digits.split('').slice(0, 6 - from).forEach((d, i) => { $otpInputs.eq(from + i).val(d); });
     160        const next = Math.min(from + digits.length, 5);
     161        $otpInputs.eq(next).trigger('focus');
     162    };
     163    const maybeSubmit = () => { if (code().length === 6) $('#ceovw-btn-verify').trigger('click'); };
    119164
    120165    $otpInputs.on('input', function() {
    121         this.value = this.value.replace(/\D/g, '');
    122         if(this.value && $(this).next('.ceovw-otp-box').length) {
    123             $(this).next('.ceovw-otp-box').focus();
     166        const digits = this.value.replace(/\D/g, '');
     167        const i = $otpInputs.index(this);
     168        if (digits.length > 1) { this.value = ''; fill(digits, digits.length >= 6 ? 0 : i); }
     169        else {
     170            this.value = digits;
     171            if (digits && i < 5) { $otpInputs.eq(i + 1).trigger('focus'); }
    124172        }
    125         let fullCode = $otpInputs.map(function(){ return $(this).val(); }).get().join('');
    126         if(fullCode.length === 6) $('#ceovw-btn-verify').click();
     173        if (digits) otpMessage('');
     174        maybeSubmit();
    127175    });
    128176
    129177    $otpInputs.on('keydown', function(e) {
    130         if(e.key === "Backspace" && !this.value && $(this).prev('.ceovw-otp-box').length) {
    131             $(this).prev('.ceovw-otp-box').focus().val('');
    132         }
    133     });
     178        const i = $otpInputs.index(this);
     179        if (e.key === "Backspace" && !this.value && i > 0) { e.preventDefault(); $otpInputs.eq(i - 1).val('').trigger('focus'); }
     180        else if (e.key === 'ArrowLeft' && i > 0) { e.preventDefault(); $otpInputs.eq(i - 1).trigger('focus'); }
     181        else if (e.key === 'ArrowRight' && i < 5) { e.preventDefault(); $otpInputs.eq(i + 1).trigger('focus'); }
     182        else if (e.key === 'Enter') { e.preventDefault(); $('#ceovw-btn-verify').trigger('click'); }
     183    });
     184
     185    $otpInputs.on('focus', function(){ this.select(); });
    134186
    135187    $otpInputs.on('paste', function(e) {
    136188        e.preventDefault();
    137         let pasteData = (e.originalEvent || e).clipboardData.getData('text').replace(/\D/g, "").slice(0, 6);
    138         $otpInputs.each(function(i) {
    139             if(pasteData[i]) $(this).val(pasteData[i]);
    140         });
    141         if(pasteData.length === 6) $('#ceovw-btn-verify').click();
     189        const pasteData = ((e.originalEvent || e).clipboardData.getData('text') || '').replace(/\D/g, "").slice(0, 6);
     190        if (!pasteData) return;
     191        fill(pasteData, pasteData.length >= 6 ? 0 : $otpInputs.index(this));
     192        maybeSubmit();
    142193    });
    143194
    144195    // Verify OTP
    145196    $('#ceovw-btn-verify').on('click', function(){
    146         let code = $otpInputs.map(function(){ return $(this).val(); }).get().join('');
    147         if(code.length !== 6) return;
     197        const value = code();
     198        if (verifying) return;
     199        if (value.length !== 6) { otpMessage(T.enterCode); $otpInputs.filter(function(){ return !this.value; }).first().trigger('focus'); return; }
    148200
    149201        let $btn = $(this);
     202        verifying = true;
    150203        setBtnLoading($btn, true);
    151         $('#ceovw-otp-error').text('');
     204        otpMessage('');
    152205
    153206        $.post(CEOVW_APP.ajax_url, {
    154207            action: 'ceovw_verify_otp',
    155208            nonce: CEOVW_APP.nonce,
    156             email: targetEmail, code: code, flow: currentFlow
    157         }, function(res){
    158             if(res.success) {
    159                 $btn.find('.btn-text').text(CEOVW_APP.i18n.verified);
     209            email: targetEmail, code: value, flow: currentFlow
     210        }, function(res){
     211            if(res.success) {
     212                $btn.find('.btn-text').text(T.verified);
     213                otpMessage(T.verified, 'success');
    160214                setTimeout(() => {
    161215                    $modal.fadeOut(200);
    … …  
    164218                    } else if (currentFlow === 'forgot') {
    165219                        resetToken = (res.data && res.data.reset_token) || '';
    166                         $('.ceovw-view-pane').hide();
    167                         $('#ceovw-view-newpass').fadeIn(300);
     220                        showView('newpass');
    168221                        $('#ceovw-verified-email').val(targetEmail);
    169222                        setBtnLoading($btn, false);
    170                         $btn.find('.btn-text').text('Verify Code');
     223                        $btn.find('.btn-text').text(T.verifyBtn);
     224                        verifying = false;
    171225                    }
    172226                }, 1000);
    173227            } else {
     228                verifying = false;
    174229                setBtnLoading($btn, false);
    175                 $('#ceovw-otp-error').text(res.data.message || CEOVW_APP.i18n.error);
     230                otpMessage((res.data && res.data.message) || T.error);
    176231                $otpInputs.val('');
    177                 $otpInputs.first().focus();
    178             }
    179         }).fail(function() {
    180             setBtnLoading($btn, false);
    181             $('#ceovw-otp-error').text(CEOVW_APP.i18n.error);
     232                $otpInputs.first().trigger('focus');
     233            }
     234        }).fail(function() {
     235            verifying = false;
     236            setBtnLoading($btn, false);
     237            otpMessage(T.error);
    182238        });
    183239    });
    … …  
    189245        let $txt = $('#ceovw-timer-text');
    190246        $btn.prop('disabled', true);
    191        
     247        $txt.text(`(${seconds}s)`);
     248
    192249        resendTimer = setInterval(() => {
    193250            seconds--;
    … …  
    205262        let $btn = $(this);
    206263        $btn.prop('disabled', true);
    207         $('#ceovw-timer-text').text('...');
    208        
     264        $('#ceovw-timer-text').text('…');
     265
    209266        $.post(CEOVW_APP.ajax_url, {
    210267            action: currentFlow === 'register' ? 'ceovw_process_registration_req' : 'ceovw_process_forgot_req',
    … …  
    214271        }, function(res){
    215272            if(res.success) {
    216                 startResendTimer(60);
    217                 $('#ceovw-otp-error').text('New code sent!').css('color', 'green');
    218                 setTimeout(()=> $('#ceovw-otp-error').text('').css('color', '#dc2626'), 3000);
    219             } else {
    220                 alert(res.data.message);
    221                 $btn.prop('disabled', false);
    222             }
    223         });
     273                startResendTimer(resendDelay(res));
     274                otpMessage(T.codeSent, 'success');
     275                $otpInputs.val(''); $otpInputs.first().trigger('focus');
     276            } else {
     277                otpMessage((res.data && res.data.message) || T.error);
     278                if (res.data && parseInt(res.data.resend_in, 10) > 0) { startResendTimer(parseInt(res.data.resend_in, 10)); }
     279                else { $btn.prop('disabled', false); $('#ceovw-timer-text').text(''); }
     280            }
     281        }).fail(() => { otpMessage(T.error); $btn.prop('disabled', false); $('#ceovw-timer-text').text(''); });
    224282    });
    225283
    … …  
    230288        let pass = $form.find('input[name="new_password"]').val();
    231289        let confirm = $form.find('input[name="new_password_confirm"]').val();
    232        
    233         if(pass !== confirm) { alert("Passwords do not match."); return; }
    234        
     290        const $empty = missing($form);
     291
     292        if ($empty.length) { formMsg($form, T.required); $empty.trigger('focus'); return; }
     293        if(pass !== confirm) { formMsg($form, T.mismatch); $form.find('input[name="new_password_confirm"]').trigger('focus'); return; }
     294        formMsg($form, '');
     295
    235296        let $btn = $('#ceovw-btn-setpass');
    236297        setBtnLoading($btn, true);
    … …  
    246307                setBtnLoading($btn, false);
    247308                $form[0].reset();
    248                 alert((res.data && res.data.message) || CEOVW_APP.i18n.error);
    249                 $('.ceovw-view-pane').hide();
    250                 $('#ceovw-form-login input[name="username"]').val(targetEmail);
    251                 $('#ceovw-view-login').fadeIn(300);
     309                const $login = showView('login', false);
     310                $login.find('input[name="username"]').val(targetEmail);
     311                formMsg($login.find('form'), (res.data && res.data.message) || T.updated, 'success');
     312                setTimeout(() => $login.find('input[name="password"]').trigger('focus'), 320);
    252313            } else {
    253314                setBtnLoading($btn, false);
    254                 alert((res.data && res.data.message) || CEOVW_APP.i18n.error);
    255315                if (res.data && res.data.code === 'reset_expired') {
    256316                    // Authorization expired or already used: restart the forgot password flow
    257317                    resetToken = '';
    258318                    $form[0].reset();
    259                     $('.ceovw-view-pane').hide();
    260                     $('#ceovw-view-forgot').fadeIn(300);
     319                    const $forgot = showView('forgot');
     320                    formMsg($forgot.find('form'), (res.data && res.data.message) || T.error);
     321                } else {
     322                    formMsg($form, (res.data && res.data.message) || T.error);
    261323                }
    262324            }
    263         }).fail(() => { setBtnLoading($btn, false); alert(CEOVW_APP.i18n.error); });
    264     });
    265 
     325        }).fail(() => { setBtnLoading($btn, false); formMsg($form, T.error); });
     326    });
    266327})(jQuery);
  • naimur-email-otp-for-woocommerce/tags/2.1.2/assets/src/js/main.js

    r3780003 r3780739  
    22    "use strict";
    33
    4     let currentFlow = '';
     4    const T = (window.CEOVW_APP && CEOVW_APP.i18n) || {};
     5    let currentFlow = '';
    56    let targetEmail = '';
    67    let resendTimer = null;
    78    let resetToken = ''; // Password reset authorization: kept in memory only, never persisted or logged
     9    let lastFocus = null;
     10    let verifying = false;
     11
     12    // Inline form messages (replace browser alerts; announced by screen readers)
     13    const formMsg = ($form, text, kind) => {
     14        const $m = $form.find('.ceovw-form-msg');
     15        if (!text) { $m.prop('hidden', true).text('').removeClass('is-error is-success'); return; }
     16        $m.text(text).prop('hidden', false).toggleClass('is-success', kind === 'success').toggleClass('is-error', kind !== 'success');
     17    };
     18    const showView = (target, focus) => {
     19        $('.ceovw-view-pane').hide();
     20        const $v = $(`#ceovw-view-${target}`).fadeIn(300);
     21        $v.find('.ceovw-form-msg').prop('hidden', true).text('');
     22        if (focus !== false) { setTimeout(() => $v.find('input:not([type=hidden]):visible').first().trigger('focus'), 320); }
     23        return $v;
     24    };
     25    const missing = ($form) => $form.find('input[required]').filter(function(){ return !$.trim(this.value); }).first();
     26    const validEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test($.trim(v));
    827
    928    // View Switcher (Smooth Fade)
    … …  
    1231        const target = $(this).data('target');
    1332        if (target !== 'newpass') resetToken = '';
    14         $('.ceovw-view-pane').hide();
    15         $(`#ceovw-view-${target}`).fadeIn(300);
    16     });
    17 
    18     // Helper: Button Loading State
     33        showView(target);
     34    });
     35
    1936    const setBtnLoading = ($btn, isLoading) => {
    2037        if(isLoading) {
    21             $btn.prop('disabled', true).addClass('loading');
     38            $btn.prop('disabled', true).addClass('loading').attr('aria-busy', 'true');
    2239        } else {
    23             $btn.prop('disabled', false).removeClass('loading');
     40            $btn.prop('disabled', false).removeClass('loading').attr('aria-busy', 'false');
    2441        }
    2542    };
    2643
    27     // Helper: Captcha Token
    28     const getCaptchaToken = ($form) => {
    29         if (CEOVW_APP.settings.captcha === 'turnstile') {
    30             return $form.find('[name="cf-turnstile-response"]').val() || '';
    31         } else if (CEOVW_APP.settings.captcha === 'recaptcha') {
    32             return $form.find('[name="g-recaptcha-response"]').val() || '';
    33         }
    34         return '';
    35     };
    36 
    37     // Registration Form Submit
     44    // Resend delay: server value when provided, otherwise the per-flow setting
     45    const resendDelay = (res) => {
     46        const s = res && res.data ? parseInt(res.data.resend_in, 10) : 0;
     47        const cfg = (CEOVW_APP.settings && CEOVW_APP.settings.resend) || {};
     48        return s > 0 ? s : (parseInt(cfg[currentFlow], 10) || 60);
     49    };
     50
     51    // Registration Flow
    3852    $('#ceovw-form-register').on('submit', function(e){
    3953        e.preventDefault();
    … …  
    4155        let pass = $form.find('input[name="password"]').val();
    4256        let confirm = $form.find('input[name="password_confirm"]').val();
    43        
    44         if (pass !== confirm) {
    45             alert("Passwords do not match!");
    46             return;
    47         }
    48 
    4957        let email = $form.find('input[name="email"]').val();
    5058        let name = $form.find('input[name="ceovw_full_name"]').val();
     59        const $empty = missing($form);
     60
     61        if ($empty.length) { formMsg($form, T.required); $empty.trigger('focus'); return; }
     62        if (!validEmail(email)) { formMsg($form, T.badEmail); $form.find('input[name="email"]').trigger('focus'); return; }
     63        if (pass !== confirm) { formMsg($form, T.mismatch); $form.find('input[name="password_confirm"]').trigger('focus'); return; }
     64        formMsg($form, '');
     65
    5166        let $btn = $('#ceovw-btn-reg');
    52 
    5367        setBtnLoading($btn, true);
    5468
    … …  
    5670            action: 'ceovw_process_registration_req',
    5771            nonce: CEOVW_APP.nonce,
    58             email: email, name: name, password: pass,
    59             captcha: getCaptchaToken($form)
     72            email: email, name: name, password: pass
    6073        }, function(res){
    6174            setBtnLoading($btn, false);
    … …  
    6477                targetEmail = email;
    6578                resetToken = '';
    66                 openOTPModal(email);
    67             } else {
    68                 alert(res.data.message || CEOVW_APP.i18n.error);
    69             }
    70         }).fail(() => { setBtnLoading($btn, false); alert(CEOVW_APP.i18n.error); });
    71     });
    72 
    73     // Forgot Password Form Submit
     79                openOTPModal(email, resendDelay(res));
     80            } else {
     81                formMsg($form, (res.data && res.data.message) || T.error);
     82            }
     83        }).fail(function() {
     84            setBtnLoading($btn, false);
     85            formMsg($form, T.error);
     86        });
     87    });
     88
     89    // Forgot Password Flow
    7490    $('#ceovw-form-forgot').on('submit', function(e){
    7591        e.preventDefault();
    7692        let $form = $(this);
    7793        let email = $form.find('input[name="reset_email"]').val();
     94        if (!validEmail(email)) { formMsg($form, T.badEmail); $form.find('input[name="reset_email"]').trigger('focus'); return; }
     95        formMsg($form, '');
    7896        let $btn = $('#ceovw-btn-forgot');
    7997        resetToken = '';
    … …  
    84102            action: 'ceovw_process_forgot_req',
    85103            nonce: CEOVW_APP.nonce,
    86             email: email,
    87             captcha: getCaptchaToken($form)
     104            email: email
    88105        }, function(res){
    89106            setBtnLoading($btn, false);
    … …  
    91108                currentFlow = 'forgot';
    92109                targetEmail = email;
    93                 openOTPModal(email);
    94             } else {
    95                 alert(res.data.message || CEOVW_APP.i18n.error);
    96             }
    97         }).fail(() => { setBtnLoading($btn, false); alert(CEOVW_APP.i18n.error); });
    98     });
    99 
    100     // OTP Modal Logic & Auto-tab
     110                openOTPModal(email, resendDelay(res));
     111            } else {
     112                formMsg($form, (res.data && res.data.message) || T.error);
     113            }
     114        }).fail(function() {
     115            setBtnLoading($btn, false);
     116            formMsg($form, T.error);
     117        });
     118    });
     119
     120    // OTP Modal Logic
    101121    const $modal = $('#ceovw-otp-modal');
    102122    const $otpInputs = $('.ceovw-otp-box');
    103    
    104     function openOTPModal(email) {
     123    const $otpMsg = $('#ceovw-otp-error');
     124    const otpMessage = (text, kind) => { $otpMsg.text(text || '').toggleClass('is-success', kind === 'success'); };
     125    const code = () => $otpInputs.map(function(){ return $(this).val(); }).get().join('');
     126
     127    function openOTPModal(email, wait) {
     128        lastFocus = document.activeElement;
    105129        $('#ceovw-target-email').text(email);
    106130        $otpInputs.val('');
    107         $('#ceovw-otp-error').text('');
     131        otpMessage('');
     132        verifying = false;
    108133        $modal.fadeIn(200);
    109         $otpInputs.first().focus();
    110         startResendTimer(60); // 60s cooldown start
     134        $otpInputs.first().trigger('focus');
     135        startResendTimer(wait || resendDelay());
    111136    }
    112137
    113     $modal.find('.ceovw-modal-close').on('click', () => $modal.fadeOut(200));
     138    function closeOTPModal() {
     139        $modal.fadeOut(200);
     140        if (lastFocus && document.body.contains(lastFocus)) { lastFocus.focus(); }
     141    }
     142
     143    $modal.find('.ceovw-modal-close').on('click', closeOTPModal);
     144    $modal.on('click', function(e){ if (e.target === this) { closeOTPModal(); } });
     145
     146    // Keyboard: Escape closes, Tab stays inside the dialog
     147    $modal.on('keydown', function(e){
     148        if (e.key === 'Escape') { e.preventDefault(); closeOTPModal(); return; }
     149        if (e.key !== 'Tab') return;
     150        const $f = $modal.find('button:not(:disabled), input').filter(':visible');
     151        if (!$f.length) return;
     152        const first = $f[0], last = $f[$f.length - 1];
     153        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
     154        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
     155    });
     156
     157    // Spread several digits (paste or one-time-code autofill) across the boxes starting at index `from`
     158    const fill = (digits, from) => {
     159        digits.split('').slice(0, 6 - from).forEach((d, i) => { $otpInputs.eq(from + i).val(d); });
     160        const next = Math.min(from + digits.length, 5);
     161        $otpInputs.eq(next).trigger('focus');
     162    };
     163    const maybeSubmit = () => { if (code().length === 6) $('#ceovw-btn-verify').trigger('click'); };
    114164
    115165    $otpInputs.on('input', function() {
    116         this.value = this.value.replace(/\D/g, '');
    117         if(this.value && $(this).next('.ceovw-otp-box').length) {
    118             $(this).next('.ceovw-otp-box').focus();
     166        const digits = this.value.replace(/\D/g, '');
     167        const i = $otpInputs.index(this);
     168        if (digits.length > 1) { this.value = ''; fill(digits, digits.length >= 6 ? 0 : i); }
     169        else {
     170            this.value = digits;
     171            if (digits && i < 5) { $otpInputs.eq(i + 1).trigger('focus'); }
    119172        }
    120         let fullCode = $otpInputs.map(function(){ return $(this).val(); }).get().join('');
    121         if(fullCode.length === 6) $('#ceovw-btn-verify').click();
     173        if (digits) otpMessage('');
     174        maybeSubmit();
    122175    });
    123176
    124177    $otpInputs.on('keydown', function(e) {
    125         if(e.key === "Backspace" && !this.value && $(this).prev('.ceovw-otp-box').length) {
    126             $(this).prev('.ceovw-otp-box').focus().val('');
    127         }
    128     });
     178        const i = $otpInputs.index(this);
     179        if (e.key === "Backspace" && !this.value && i > 0) { e.preventDefault(); $otpInputs.eq(i - 1).val('').trigger('focus'); }
     180        else if (e.key === 'ArrowLeft' && i > 0) { e.preventDefault(); $otpInputs.eq(i - 1).trigger('focus'); }
     181        else if (e.key === 'ArrowRight' && i < 5) { e.preventDefault(); $otpInputs.eq(i + 1).trigger('focus'); }
     182        else if (e.key === 'Enter') { e.preventDefault(); $('#ceovw-btn-verify').trigger('click'); }
     183    });
     184
     185    $otpInputs.on('focus', function(){ this.select(); });
    129186
    130187    $otpInputs.on('paste', function(e) {
    131188        e.preventDefault();
    132         let pasteData = (e.originalEvent || e).clipboardData.getData('text').replace(/\D/g, "").slice(0, 6);
    133         $otpInputs.each(function(i) {
    134             if(pasteData[i]) $(this).val(pasteData[i]);
    135         });
    136         if(pasteData.length === 6) $('#ceovw-btn-verify').click();
    137     });
    138 
    139     // Verify OTP Action
     189        const pasteData = ((e.originalEvent || e).clipboardData.getData('text') || '').replace(/\D/g, "").slice(0, 6);
     190        if (!pasteData) return;
     191        fill(pasteData, pasteData.length >= 6 ? 0 : $otpInputs.index(this));
     192        maybeSubmit();
     193    });
     194
     195    // Verify OTP
    140196    $('#ceovw-btn-verify').on('click', function(){
    141         let code = $otpInputs.map(function(){ return $(this).val(); }).get().join('');
    142         if(code.length !== 6) return;
     197        const value = code();
     198        if (verifying) return;
     199        if (value.length !== 6) { otpMessage(T.enterCode); $otpInputs.filter(function(){ return !this.value; }).first().trigger('focus'); return; }
    143200
    144201        let $btn = $(this);
     202        verifying = true;
    145203        setBtnLoading($btn, true);
    146         $('#ceovw-otp-error').text('');
     204        otpMessage('');
    147205
    148206        $.post(CEOVW_APP.ajax_url, {
    149207            action: 'ceovw_verify_otp',
    150208            nonce: CEOVW_APP.nonce,
    151             email: targetEmail, code: code, flow: currentFlow
    152         }, function(res){
    153             if(res.success) {
    154                 $btn.find('.btn-text').text(CEOVW_APP.i18n.verified);
     209            email: targetEmail, code: value, flow: currentFlow
     210        }, function(res){
     211            if(res.success) {
     212                $btn.find('.btn-text').text(T.verified);
     213                otpMessage(T.verified, 'success');
    155214                setTimeout(() => {
    156215                    $modal.fadeOut(200);
    … …  
    159218                    } else if (currentFlow === 'forgot') {
    160219                        resetToken = (res.data && res.data.reset_token) || '';
    161                         $('.ceovw-view-pane').hide();
    162                         $('#ceovw-view-newpass').fadeIn(300);
     220                        showView('newpass');
    163221                        $('#ceovw-verified-email').val(targetEmail);
    164222                        setBtnLoading($btn, false);
    165                         $btn.find('.btn-text').text('Verify Code');
     223                        $btn.find('.btn-text').text(T.verifyBtn);
     224                        verifying = false;
    166225                    }
    167226                }, 1000);
    168227            } else {
     228                verifying = false;
    169229                setBtnLoading($btn, false);
    170                 $('#ceovw-otp-error').text(res.data.message || CEOVW_APP.i18n.error);
     230                otpMessage((res.data && res.data.message) || T.error);
    171231                $otpInputs.val('');
    172                 $otpInputs.first().focus();
    173             }
    174         }).fail(() => { setBtnLoading($btn, false); $('#ceovw-otp-error').text(CEOVW_APP.i18n.error); });
    175     });
    176 
    177     // Timer Logic for Resend
     232                $otpInputs.first().trigger('focus');
     233            }
     234        }).fail(function() {
     235            verifying = false;
     236            setBtnLoading($btn, false);
     237            otpMessage(T.error);
     238        });
     239    });
     240
     241    // Resend Timer
    178242    function startResendTimer(seconds) {
    179243        clearInterval(resendTimer);
    … …  
    181245        let $txt = $('#ceovw-timer-text');
    182246        $btn.prop('disabled', true);
    183        
     247        $txt.text(`(${seconds}s)`);
     248
    184249        resendTimer = setInterval(() => {
    185250            seconds--;
    … …  
    197262        let $btn = $(this);
    198263        $btn.prop('disabled', true);
    199         $('#ceovw-timer-text').text('...');
    200        
     264        $('#ceovw-timer-text').text('…');
     265
    201266        $.post(CEOVW_APP.ajax_url, {
    202267            action: currentFlow === 'register' ? 'ceovw_process_registration_req' : 'ceovw_process_forgot_req',
    … …  
    206271        }, function(res){
    207272            if(res.success) {
    208                 startResendTimer(60);
    209                 $('#ceovw-otp-error').text('New code sent!').css('color', 'green');
    210                 setTimeout(()=> $('#ceovw-otp-error').text('').css('color', '#dc2626'), 3000);
    211             } else {
    212                 alert(res.data.message);
    213                 $btn.prop('disabled', false);
    214             }
    215         });
    216     });
    217 
    218     // Set New Password Action
     273                startResendTimer(resendDelay(res));
     274                otpMessage(T.codeSent, 'success');
     275                $otpInputs.val(''); $otpInputs.first().trigger('focus');
     276            } else {
     277                otpMessage((res.data && res.data.message) || T.error);
     278                if (res.data && parseInt(res.data.resend_in, 10) > 0) { startResendTimer(parseInt(res.data.resend_in, 10)); }
     279                else { $btn.prop('disabled', false); $('#ceovw-timer-text').text(''); }
     280            }
     281        }).fail(() => { otpMessage(T.error); $btn.prop('disabled', false); $('#ceovw-timer-text').text(''); });
     282    });
     283
     284    // Set New Password
    219285    $('#ceovw-form-newpass').on('submit', function(e){
    220286        e.preventDefault();
    … …  
    222288        let pass = $form.find('input[name="new_password"]').val();
    223289        let confirm = $form.find('input[name="new_password_confirm"]').val();
    224        
    225         if(pass !== confirm) { alert("Passwords do not match."); return; }
    226        
     290        const $empty = missing($form);
     291
     292        if ($empty.length) { formMsg($form, T.required); $empty.trigger('focus'); return; }
     293        if(pass !== confirm) { formMsg($form, T.mismatch); $form.find('input[name="new_password_confirm"]').trigger('focus'); return; }
     294        formMsg($form, '');
     295
    227296        let $btn = $('#ceovw-btn-setpass');
    228297        setBtnLoading($btn, true);
    … …  
    238307                setBtnLoading($btn, false);
    239308                $form[0].reset();
    240                 alert((res.data && res.data.message) || CEOVW_APP.i18n.error);
    241                 $('.ceovw-view-pane').hide();
    242                 $('#ceovw-form-login input[name="username"]').val(targetEmail);
    243                 $('#ceovw-view-login').fadeIn(300);
     309                const $login = showView('login', false);
     310                $login.find('input[name="username"]').val(targetEmail);
     311                formMsg($login.find('form'), (res.data && res.data.message) || T.updated, 'success');
     312                setTimeout(() => $login.find('input[name="password"]').trigger('focus'), 320);
    244313            } else {
    245314                setBtnLoading($btn, false);
    246                 alert((res.data && res.data.message) || CEOVW_APP.i18n.error);
    247315                if (res.data && res.data.code === 'reset_expired') {
    248316                    // Authorization expired or already used: restart the forgot password flow
    249317                    resetToken = '';
    250318                    $form[0].reset();
    251                     $('.ceovw-view-pane').hide();
    252                     $('#ceovw-view-forgot').fadeIn(300);
     319                    const $forgot = showView('forgot');
     320                    formMsg($forgot.find('form'), (res.data && res.data.message) || T.error);
     321                } else {
     322                    formMsg($form, (res.data && res.data.message) || T.error);
    253323                }
    254324            }
    255         }).fail(() => { setBtnLoading($btn, false); alert(CEOVW_APP.i18n.error); });
    256     });
    257 
     325        }).fail(() => { setBtnLoading($btn, false); formMsg($form, T.error); });
     326    });
    258327})(jQuery);
  • naimur-email-otp-for-woocommerce/tags/2.1.2/includes/class-ajax.php

    r3780003 r3780739  
    2222        return ($ip === '::1' || $ip === '127.0.0.1') ? '127.0.0.1 (Localhost)' : $ip;
    2323    }
     24
     25    /** Coarse device label for activity logs (no full user agent is stored). */
     26    public static function device_label() { return self::get_user_device(); }
    2427
    2528    private static function get_user_device() {
    … …  
    7881        $is_resend = ! empty( $_POST['is_resend'] );
    7982
     83        if ( ! CEOVW_Options::get( 'registration_enabled' ) ) { wp_send_json_error(['message' => __( 'Registration is not available.', 'naimur-email-otp-for-woocommerce' )]); }
    8084        if ( ! is_email( $email ) ) { wp_send_json_error(['message' => 'Invalid email address.']); }
    8185        if ( email_exists( $email ) ) { wp_send_json_error(['message' => 'An account is already registered with your email address.']); }
    8286        if ( CEOVW_Security::is_rate_limited( 'send' ) ) { wp_send_json_error(['message' => 'Too many requests. Please try again later.']); }
    8387        if ( ! CEOVW_Security::can_request_otp( $email ) ) { wp_send_json_error(['message' => 'Too many requests. Please try again later.']); }
     88        $wait = CEOVW_Security::resend_wait( $email, 'register' );
     89        /* translators: %d: seconds */
     90        if ( $wait > 0 ) { wp_send_json_error(['message' => sprintf( __( 'Please wait %d seconds before requesting a new code.', 'naimur-email-otp-for-woocommerce' ), $wait ), 'resend_in' => $wait]); }
    8491
    8592        if ( $is_resend && $password === '' ) {
    … …  
    101108            CEOVW_Logs::push_email_log(['time' => time(), 'email' => $email, 'status' => 'Success', 'message' => 'Registration OTP sent via email.']);
    102109            CEOVW_Security::record_otp_request($email);
    103             wp_send_json_success(['message' => 'OTP sent successfully.']);
     110            wp_send_json_success(['message' => 'OTP sent successfully.', 'resend_in' => CEOVW_Options::resend_seconds( 'register' )]);
    104111        } else {
    105             CEOVW_Security::clear_otp( $email );
     112            CEOVW_Security::clear_flow( $email, 'register' );
    106113            CEOVW_Logs::push_email_log(['time' => time(), 'email' => $email, 'status' => 'Failed', 'message' => 'Server failed to send email.']);
    107114            wp_send_json_error(['message' => 'Failed to send email. Check SMTP settings.']);
    … …  
    117124        $generic = 'If an account exists for this email, a verification code has been sent.';
    118125
     126        if ( ! CEOVW_Options::get( 'forgot_enabled' ) ) { wp_send_json_error(['message' => __( 'Password reset by code is not available. Please use the standard lost password link.', 'naimur-email-otp-for-woocommerce' )]); }
    119127        if ( ! is_email( $email ) ) { wp_send_json_error(['message' => 'Please enter a valid email address.']); }
    120128        if ( CEOVW_Security::is_rate_limited( 'send' ) ) { wp_send_json_error(['message' => 'Too many requests. Please try again later.']); }
    … …  
    131139        }
    132140
     141        // Server-side resend cooldown. The response stays identical, so the cooldown cannot reveal whether an account exists.
     142        if ( CEOVW_Security::resend_wait( $email, 'forgot' ) > 0 ) {
     143            self::clean_buffer();
     144            wp_send_json_success(['message' => $generic]);
     145        }
     146
    133147        $otp = CEOVW_Security::generate_and_save_otp( $email, 'forgot' );
    134         $sent = $otp !== false && CEOVW_Emails::send_otp_email( $email, $otp, 'forgot' );
     148        $sent = $otp !== false && CEOVW_Emails::send_otp_email( $email, $otp, 'forgot', $user->display_name );
    135149
    136150        self::clean_buffer();
    … …  
    139153            CEOVW_Logs::push_email_log(['time' => time(), 'email' => $email, 'status' => 'Success', 'message' => 'Password reset OTP sent.']);
    140154        } else {
    141             CEOVW_Security::clear_otp( $email );
     155            CEOVW_Security::clear_flow( $email, 'forgot' );
    142156            CEOVW_Logs::push_email_log(['time' => time(), 'email' => $email, 'status' => 'Failed', 'message' => 'Server failed to send email.']);
    143157        }
    … …  
    154168        $code  = sanitize_text_field( $_POST['code'] ?? '' );
    155169        $flow  = sanitize_text_field( $_POST['flow'] ?? '' );
     170
     171        // This endpoint serves the My Account flows only; checkout has its own session-bound endpoint.
     172        if ( ! in_array( $flow, ['register', 'forgot'], true ) ) { wp_send_json_error(['message' => 'Invalid code. Please try again.']); }
     173        if ( ( 'register' === $flow && ! CEOVW_Options::get( 'registration_enabled' ) ) || ( 'forgot' === $flow && ! CEOVW_Options::get( 'forgot_enabled' ) ) ) {
     174            wp_send_json_error(['message' => __( 'This verification is not available.', 'naimur-email-otp-for-woocommerce' )]);
     175        }
    156176
    157177        $row = CEOVW_Security::verify_otp_code( $email, $code, $flow );
    … …  
    175195                    CEOVW_Users::auto_login( $user_id );
    176196                    CEOVW_Security::clear_otp( $email );
    177                     CEOVW_Logs::push_activity_log(['time' => time(), 'user_id' => $user_id, 'email' => $email, 'ip' => $real_ip, 'device' => $device]);
     197                    CEOVW_Logs::push_activity_log(['time' => time(), 'user_id' => $user_id, 'email' => $email, 'ip' => $real_ip, 'device' => $device, 'event' => 'registration_verified']);
    178198                    wp_send_json_success(['message' => 'Account created and verified.']);
    179199                } else {
    … …  
    188208
    189209            $user = get_user_by( 'email', $email );
    190             CEOVW_Logs::push_activity_log(['time' => time(), 'user_id' => $user ? $user->ID : 0, 'email' => $email, 'ip' => $real_ip, 'device' => $device]);
     210            CEOVW_Logs::push_activity_log(['time' => time(), 'user_id' => $user ? $user->ID : 0, 'email' => $email, 'ip' => $real_ip, 'device' => $device, 'event' => 'reset_verified']);
    191211            // The raw token is only returned to this browser flow; the database stores its SHA-256 hash.
    192212            wp_send_json_success(['message' => 'Code verified.', 'reset_token' => $token]);
    … …  
    199219
    200220        $expired = ['message' => 'Your password reset session has expired. Please request a new code.', 'code' => 'reset_expired'];
     221        if ( ! CEOVW_Options::get( 'forgot_enabled' ) ) { wp_send_json_error( $expired ); }
    201222
    202223        if ( CEOVW_Security::is_rate_limited( 'reset' ) ) { wp_send_json_error(['message' => 'Too many requests. Please try again later.']); }
  • naimur-email-otp-for-woocommerce/tags/2.1.2/includes/class-emails.php

    r3587442 r3780739  
    44class CEOVW_Emails {
    55
     6    /** Internal flow name -> template type. */
     7    public static function type_for_flow( $flow ) {
     8        $map = array( 'register' => 'registration', 'forgot' => 'forgot', 'checkout' => 'checkout' );
     9        return isset( $map[ $flow ] ) ? $map[ $flow ] : 'registration';
     10    }
     11
     12    /**
     13     * Sends an OTP email using the stored (sanitized) template for the flow.
     14     * Templates are stored as HTML in the database; no template file is ever built from a setting value.
     15     * Uses wp_mail(), so SMTP plugins handle delivery.
     16     */
    617    public static function send_otp_email( $email, $otp, $flow, $name = '' ) {
    7         $subject = get_option('naimur-email-otp-for-woocommerce_email_subject', 'Your Verification Code');
    8         $template_id = get_option('naimur-email-otp-for-woocommerce_email_template', '1');
    9        
    10         $message = '';
    11         if ( $template_id === 'custom' ) {
    12             $message = str_replace( '[naimur_email_otp_code]', $otp, get_option('naimur-email-otp-for-woocommerce_custom_email_html', 'Your code is [naimur_email_otp_code]') );
    13         } else {
    14             // Include your default template files here
    15             ob_start();
    16             include NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH . "templates/email-template-$template_id.php";
    17             $message = ob_get_clean();
    18             $message = str_replace( '[naimur_email_otp_code]', $otp, $message );
     18        $type = self::type_for_flow( $flow );
     19        $out  = CEOVW_Templates::render( $type, array(
     20            'customer_name'  => $name,
     21            'customer_email' => $email,
     22            'otp_code'       => $otp,
     23            'expiry_minutes' => (int) ceil( CEOVW_Options::ttl( $flow ) / 60 ),
     24        ) );
     25        return wp_mail( $email, $out['subject'], $out['html'], self::headers() );
     26    }
     27
     28    /** Test email from the admin (sample code, never a real OTP). */
     29    public static function send_test( $type, $to, $override = null ) {
     30        $out = CEOVW_Templates::render( $type, CEOVW_Templates::sample_vars( $type ), $override );
     31        $ok  = wp_mail( $to, '[' . __( 'Test', 'naimur-email-otp-for-woocommerce' ) . '] ' . $out['subject'], $out['html'], self::headers() );
     32        if ( $ok ) { update_option( 'ceovw_test_email_sent_at', time(), false ); }
     33        return $ok;
     34    }
     35
     36    /** Headers for this plugin's emails only (no global From filter, so other plugins' emails are unaffected). */
     37    public static function headers() {
     38        $h    = array( 'Content-Type: text/html; charset=UTF-8' );
     39        $from = CEOVW_Options::get( 'sender_email' );
     40        if ( $from ) {
     41            $name = str_replace( array( "\r", "\n", '"', '<', '>' ), '', (string) CEOVW_Options::get( 'sender_name' ) );
     42            $h[]  = 'From: ' . ( '' !== $name ? '"' . $name . '" ' : '' ) . '<' . $from . '>';
    1943        }
    20 
    21         return wp_mail( $email, $subject, $message, ['Content-Type: text/html; charset=UTF-8'] );
     44        return $h;
    2245    }
    2346}
  • naimur-email-otp-for-woocommerce/tags/2.1.2/includes/class-logs.php

    r3780003 r3780739  
    77    const EMAIL_T_TTL = 7; // days
    88
    9     public static function init(){ add_action('admin_menu', [__CLASS__, 'menu']); }
    10    
    11     public static function menu(){
    12         add_submenu_page('woocommerce', esc_html__('Email/Activity Logs', 'naimur-email-otp-for-woocommerce'), esc_html__('Email Verification Logs', 'naimur-email-otp-for-woocommerce'), 'manage_woocommerce', 'naimur-email-otp-for-woocommerce-logs', [__CLASS__, 'render']);
    13     }
    14    
     9    // Logs are shown on the "Activity & Diagnostics" admin page (includes/admin/views/diagnostics.php).
     10
    1511    public static function get_email_logs(){ $logs = get_transient(self::EMAIL_T_KEY); return is_array($logs)?$logs:[]; }
    1612   
    … …  
    4137        update_option(self::ACTIVITY_KEY, $logs, false);
    4238    }
    43    
    44     public static function render(){
    45         if (isset($_POST['ceovw_clear_logs']) && check_admin_referer('ceovw_logs')){
    46             if ($_POST['ceovw_clear_logs'] === 'email') { delete_transient(self::EMAIL_T_KEY); }
    47             if ($_POST['ceovw_clear_logs'] === 'activity') { update_option(self::ACTIVITY_KEY, [] ); }
    48             echo '<div class="updated"><p>'.esc_html__('Logs cleared.', 'naimur-email-otp-for-woocommerce').'</p></div>';
    49         }
    50        
    51         $email_logs = self::get_email_logs();
    52         $activity = self::get_activity_logs();
    53         ?>
    54        
    55         <div class="wrap">
    56             <h1><?php esc_html_e('Email & Activity Logs', 'naimur-email-otp-for-woocommerce'); ?></h1>
    57            
    58             <h2><?php esc_html_e('Last Sent OTP Emails', 'naimur-email-otp-for-woocommerce'); ?></h2>
    59             <form method="post">
    60                 <?php wp_nonce_field('ceovw_logs'); ?>
    61                 <input type="hidden" name="ceovw_clear_logs" value="email" />
    62                 <button class="button"><?php esc_html_e('Clear Email Logs','naimur-email-otp-for-woocommerce'); ?></button>
    63             </form>
    64             <table class="widefat striped">
    65                 <thead>
    66                     <tr>
    67                         <th><?php esc_html_e('Time','naimur-email-otp-for-woocommerce'); ?></th>
    68                         <th><?php esc_html_e('Email','naimur-email-otp-for-woocommerce'); ?></th>
    69                         <th><?php esc_html_e('Code','naimur-email-otp-for-woocommerce'); ?></th>
    70                         <th><?php esc_html_e('Status','naimur-email-otp-for-woocommerce'); ?></th>
    71                         <th><?php esc_html_e('Message','naimur-email-otp-for-woocommerce'); ?></th>
    72                     </tr>
    73                 </thead>
    74                 <tbody>
    75                 <?php if (empty($email_logs)): ?>
    76                     <tr><td colspan="5"><?php esc_html_e('No logs yet.','naimur-email-otp-for-woocommerce'); ?></td></tr>
    77                 <?php else: foreach (array_reverse($email_logs) as $row): ?>
    78                     <tr>
    79                         <td><?php echo esc_html( date_i18n( get_option('date_format').' '.get_option('time_format'), $row['time'] ?? time() ) ); ?></td>
    80                         <td><?php echo esc_html(ceovw_mask_email($row['email'] ?? '')); ?></td>
    81                         <td><code>••••••</code></td>
    82                         <td><?php echo esc_html($row['status'] ?? ''); ?></td>
    83                         <td><?php echo esc_html($row['message'] ?? ''); ?></td>
    84                     </tr>
    85                 <?php endforeach; endif; ?>
    86                 </tbody>
    87             </table>
    88 
    89             <h2 style="margin-top:28px;"><?php esc_html_e('Activity (Who verified when)','naimur-email-otp-for-woocommerce'); ?></h2>
    90             <form method="post">
    91                 <?php wp_nonce_field('ceovw_logs'); ?>
    92                 <input type="hidden" name="ceovw_clear_logs" value="activity" />
    93                 <button class="button"><?php esc_html_e('Clear Activity Logs','naimur-email-otp-for-woocommerce'); ?></button>
    94             </form>
    95             <table class="widefat striped">
    96                 <thead>
    97                     <tr>
    98                         <th><?php esc_html_e('Time','naimur-email-otp-for-woocommerce'); ?></th>
    99                         <th><?php esc_html_e('User ID','naimur-email-otp-for-woocommerce'); ?></th>
    100                         <th><?php esc_html_e('Email','naimur-email-otp-for-woocommerce'); ?></th>
    101                         <th><?php esc_html_e('Device','naimur-email-otp-for-woocommerce'); ?></th>
    102                         <th><?php esc_html_e('IP','naimur-email-otp-for-woocommerce'); ?></th>
    103                     </tr>
    104                 </thead>
    105                 <tbody>
    106                 <?php if (empty($activity)): ?>
    107                     <tr><td colspan="5"><?php esc_html_e('No activity yet.','naimur-email-otp-for-woocommerce'); ?></td></tr>
    108                 <?php else: foreach (array_reverse($activity) as $row): ?>
    109                     <tr>
    110                         <td><?php echo esc_html( date_i18n( get_option('date_format').' '.get_option('time_format'), $row['time'] ?? time() ) ); ?></td>
    111                         <td><?php echo esc_html($row['user_id'] ?? ''); ?></td>
    112                         <td><?php echo esc_html(ceovw_mask_email($row['email'] ?? '')); ?></td>
    113                         <td><strong><?php echo esc_html($row['device'] ?? '-'); ?></strong></td>
    114                         <td><?php echo esc_html($row['ip'] ?? ''); ?></td>
    115                     </tr>
    116                 <?php endforeach; endif; ?>
    117                 </tbody>
    118             </table>
    119         </div>
    120         <?php
    121     }
    12239}
    123 CEOVW_Logs::init();
  • naimur-email-otp-for-woocommerce/tags/2.1.2/includes/class-security.php

    r3780003 r3780739  
    44class CEOVW_Security {
    55
    6     const MAX_ATTEMPTS = 5;
     6    const MAX_ATTEMPTS = 5;   // default; per-flow value comes from CEOVW_Options (range 3-10)
    77
    88    private static function table() {
    … …  
    1818     * @return string|false The plain OTP (for the email only), or false on failure.
    1919     */
    20     public static function generate_and_save_otp( $email, $flow, $password = '', $name = '', $temp_data = null ) {
     20    public static function generate_and_save_otp( $email, $flow, $password = '', $name = '', $temp_data = null, $context_hash = null ) {
    2121        global $wpdb;
    2222        $table = self::table();
    … …  
    3232        }
    3333
    34         $wpdb->delete( $table, ['email' => $email] );
     34        // Replace only this email's pending row for the same flow (and, for checkout, the same session),
     35        // so a new code for one flow can never cancel another flow's authorization.
     36        if ( null !== $context_hash ) {
     37            $wpdb->delete( $table, ['email' => $email, 'flow' => $flow, 'context_hash' => $context_hash] );
     38        } else {
     39            $wpdb->delete( $table, ['email' => $email, 'flow' => $flow] );
     40        }
    3541
    3642        $inserted = $wpdb->insert( $table, [
    … …  
    4046            'payload'    => $payload,
    4147            'attempts'   => 0,
    42             'token_hash' => null,
    43             'expires_at' => time() + ceovw_otp_ttl(),
    44             'created_at' => current_time( 'mysql' )
     48            'token_hash'   => null,
     49            'expires_at'   => time() + ceovw_otp_ttl( $flow ),
     50            'sent_at'      => time(),
     51            'context_hash' => $context_hash,
     52            'created_at'   => current_time( 'mysql' )
    4553        ]);
    4654
    … …  
    5664     * @return object|WP_Error The consumed row (with payload) on success.
    5765     */
    58     public static function verify_otp_code( $email, $code, $flow ) {
     66    public static function verify_otp_code( $email, $code, $flow, $context_hash = null ) {
    5967        global $wpdb;
    6068        $table = self::table();
    6169        $now   = time();
    62 
    63         if ( ! in_array( $flow, ['register', 'forgot'], true ) || ! preg_match( '/^\d{6}$/', $code ) ) {
     70        $max   = class_exists( 'CEOVW_Options' ) ? CEOVW_Options::max_attempts( $flow ) : self::MAX_ATTEMPTS;
     71
     72        if ( ! in_array( $flow, ['register', 'forgot', 'checkout'], true ) || ! is_string( $code ) || ! preg_match( '/^\d{6}$/', $code ) ) {
    6473            return new WP_Error( 'invalid', 'Invalid code. Please try again.' );
    6574        }
    66 
    67         $row = $wpdb->get_row( $wpdb->prepare(
    68             "SELECT * FROM $table WHERE email = %s AND flow = %s AND token_hash IS NULL AND otp <> '' ORDER BY id DESC LIMIT 1",
    69             $email, $flow
    70         ) );
     75        // Checkout codes are bound to the WooCommerce session that requested them.
     76        if ( 'checkout' === $flow && ( ! is_string( $context_hash ) || '' === $context_hash ) ) {
     77            return new WP_Error( 'invalid', 'Invalid code. Please try again.' );
     78        }
     79
     80        $row = 'checkout' === $flow
     81            ? $wpdb->get_row( $wpdb->prepare(
     82                "SELECT * FROM $table WHERE email = %s AND flow = %s AND context_hash = %s AND token_hash IS NULL AND otp <> '' ORDER BY id DESC LIMIT 1",
     83                $email, $flow, $context_hash ) )
     84            : $wpdb->get_row( $wpdb->prepare(
     85                "SELECT * FROM $table WHERE email = %s AND flow = %s AND token_hash IS NULL AND otp <> '' ORDER BY id DESC LIMIT 1",
     86                $email, $flow ) );
    7187
    7288        if ( ! $row ) {
    … …  
    8298        $reserved = $wpdb->query( $wpdb->prepare(
    8399            "UPDATE $table SET attempts = attempts + 1 WHERE id = %d AND token_hash IS NULL AND attempts < %d AND expires_at > %d",
    84             $row->id, self::MAX_ATTEMPTS, $now
     100            $row->id, $max, $now
    85101        ) );
    86102
    … …  
    88104        // so a parallel request that already issued a reset authorization is never undone.
    89105        if ( ! $reserved ) {
    90             $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE id = %d AND token_hash IS NULL AND attempts >= %d", $row->id, self::MAX_ATTEMPTS ) );
     106            $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE id = %d AND token_hash IS NULL AND attempts >= %d", $row->id, $max ) );
    91107            return new WP_Error( 'locked', 'Too many incorrect attempts. Please request a new code.' );
    92108        }
    … …  
    96112        if ( ! hash_equals( (string) $row->otp, $expected ) ) {
    97113            // Invalidate the OTP once the attempt limit is reached.
    98             $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE id = %d AND token_hash IS NULL AND attempts >= %d", $row->id, self::MAX_ATTEMPTS ) );
     114            $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE id = %d AND token_hash IS NULL AND attempts >= %d", $row->id, $max ) );
    99115            return new WP_Error( 'invalid', 'Invalid code. Please try again.' );
    100116        }
    … …  
    162178    }
    163179
     180    /**
     181     * Seconds until another code may be emailed for this email/flow (server-side resend cooldown). 0 = allowed now.
     182     * Only pending codes count; an issued authorization (token_hash set) does not block a new request.
     183     */
     184    public static function resend_wait( $email, $flow, $context_hash = null ) {
     185        global $wpdb;
     186        $table = self::table();
     187        $sent = null !== $context_hash
     188            ? (int) $wpdb->get_var( $wpdb->prepare( "SELECT MAX(sent_at) FROM $table WHERE email = %s AND flow = %s AND context_hash = %s AND token_hash IS NULL", $email, $flow, $context_hash ) )
     189            : (int) $wpdb->get_var( $wpdb->prepare( "SELECT MAX(sent_at) FROM $table WHERE email = %s AND flow = %s AND token_hash IS NULL", $email, $flow ) );
     190        $cool = class_exists( 'CEOVW_Options' ) ? CEOVW_Options::resend_seconds( $flow ) : 60;
     191        return $sent ? max( 0, $sent + $cool - time() ) : 0;
     192    }
     193
     194    /**
     195     * Checkout: converts a verified checkout OTP into a single-use, short-lived authorization bound to the
     196     * same email and WooCommerce session. Separate from password reset (flow = 'checkout').
     197     * Returns the raw token (kept only in the server-side WooCommerce session, never sent to the browser).
     198     */
     199    public static function issue_checkout_auth( $row, $context_hash ) {
     200        global $wpdb;
     201        $table = self::table();
     202        $token = bin2hex( random_bytes( 32 ) );
     203        $ttl   = max( 60, (int) apply_filters( 'ceovw_checkout_auth_ttl', 30 * MINUTE_IN_SECONDS ) );
     204        $updated = $wpdb->query( $wpdb->prepare(
     205            "UPDATE $table SET otp = '', attempts = 0, token_hash = %s, expires_at = %d
     206             WHERE id = %d AND flow = 'checkout' AND context_hash = %s AND otp = %s AND token_hash IS NULL AND expires_at > %d",
     207            hash( 'sha256', $token ), time() + $ttl, $row->id, $context_hash, $row->otp, time()
     208        ) );
     209        return $updated ? $token : false;
     210    }
     211
     212    /** Non-consuming check: valid checkout authorization for this exact email + session. */
     213    public static function find_checkout_auth( $email, $context_hash, $token ) {
     214        global $wpdb;
     215        if ( ! is_string( $token ) || ! preg_match( '/^[a-f0-9]{64}$/', $token ) || ! $context_hash ) { return null; }
     216        $table = self::table();
     217        return $wpdb->get_row( $wpdb->prepare(
     218            "SELECT id, email, token_hash FROM $table WHERE token_hash = %s AND flow = 'checkout' AND email = %s AND context_hash = %s AND expires_at > %d LIMIT 1",
     219            hash( 'sha256', $token ), $email, $context_hash, time()
     220        ) );
     221    }
     222
     223    /** Consumes a checkout authorization exactly once (atomic DELETE). */
     224    public static function consume_checkout_auth( $email, $context_hash, $token ) {
     225        global $wpdb;
     226        if ( ! is_string( $token ) || ! preg_match( '/^[a-f0-9]{64}$/', $token ) || ! $context_hash ) { return false; }
     227        $table = self::table();
     228        return (bool) $wpdb->query( $wpdb->prepare(
     229            "DELETE FROM $table WHERE token_hash = %s AND flow = 'checkout' AND email = %s AND context_hash = %s AND expires_at > %d",
     230            hash( 'sha256', $token ), $email, $context_hash, time()
     231        ) );
     232    }
     233
    164234    public static function can_request_otp( $email ) {
    165235        global $wpdb;
    … …  
    168238
    169239        $count = $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*) FROM $table WHERE email = %s AND action_type = 'otp_request' AND created_at > %s", $email, $twenty_four_hours_ago) );
    170         return $count < 5;
     240        $limit = class_exists( 'CEOVW_Options' ) ? (int) CEOVW_Options::get( 'rl_email_per_day' ) : 5;
     241        return $count < $limit;
    171242    }
    172243
    … …  
    193264        $table = $wpdb->prefix . 'ceovw_logs';
    194265
     266        $o = class_exists( 'CEOVW_Options' );
    195267        $defaults = [
    196             'send'   => ['limit' => 10, 'window' => HOUR_IN_SECONDS],
    197             'verify' => ['limit' => 30, 'window' => 15 * MINUTE_IN_SECONDS],
    198             'reset'  => ['limit' => 10, 'window' => 15 * MINUTE_IN_SECONDS],
     268            'send'   => ['limit' => $o ? (int) CEOVW_Options::get( 'rl_send_per_hour' ) : 10, 'window' => HOUR_IN_SECONDS],
     269            'verify' => ['limit' => $o ? (int) CEOVW_Options::get( 'rl_verify_per_15min' ) : 30, 'window' => 15 * MINUTE_IN_SECONDS],
     270            'reset'  => ['limit' => $o ? (int) CEOVW_Options::get( 'rl_reset_per_15min' ) : 10, 'window' => 15 * MINUTE_IN_SECONDS],
    199271        ];
    200272        if ( ! isset( $defaults[ $action ] ) ) { return false; }
    … …  
    242314    }
    243315
     316    /** Removes this email's rows for one flow only. */
     317    public static function clear_flow( $email, $flow ) {
     318        global $wpdb;
     319        $wpdb->delete( self::table(), ['email' => $email, 'flow' => $flow] );
     320    }
     321
    244322    public static function clear_otp( $email ) {
    245323        global $wpdb;
  • naimur-email-otp-for-woocommerce/tags/2.1.2/includes/class-settings.php

    r3780003 r3780739  
    22if (!defined('ABSPATH')) { exit; }
    33
     4/**
     5 * Settings now live in CEOVW_Options and the "Naimur OTP" admin pages (includes/admin).
     6 * This class only keeps the legacy [naimur_email_otp_code] shortcode registered so content
     7 * written for versions before 2.1.2 does not show a raw shortcode.
     8 */
    49class CEOVW_Settings {
    510    public static function init(){
    6         add_action('admin_menu', [__CLASS__, 'menu']);
    7         add_action('admin_init', [__CLASS__, 'register']);
    811        add_shortcode('naimur_email_otp_code', [__CLASS__, 'shortcode_code']);
    9         add_action('wp_ajax_ceovw_preview_template', [__CLASS__, 'preview_template']);
    10     }
    11 
    12     public static function menu(){
    13         // Main Settings Page
    14         add_submenu_page('woocommerce', esc_html__('Email Verification', 'naimur-email-otp-for-woocommerce'), esc_html__('Email Verification', 'naimur-email-otp-for-woocommerce'), 'manage_woocommerce', 'naimur-email-otp-for-woocommerce-settings', [__CLASS__, 'render']);
    15        
    16         // Help, Docs & Support Page
    17         add_submenu_page('woocommerce', esc_html__('OTP Help & Docs', 'naimur-email-otp-for-woocommerce'), esc_html__('OTP Help & Docs', 'naimur-email-otp-for-woocommerce'), 'manage_woocommerce', 'naimur-email-otp-for-woocommerce-help', [__CLASS__, 'render_help_page']);
    18     }
    19 
    20     public static function register(){
    21         $sanitize_text = 'sanitize_text_field';
    22        
    23         // Email Template Settings
    24         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_email_subject', ['sanitize_callback' => $sanitize_text]);
    25         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_email_template', ['sanitize_callback' => $sanitize_text]);
    26        
    27         // Custom Email HTML with capability check (allows <style>, <html> tags for admins)
    28         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_custom_email_html', [
    29             'sanitize_callback' => function($html) {
    30                 return current_user_can('unfiltered_html') ? $html : wp_kses_post($html);
    31             }
    32         ]);
    33        
    34         // Design Settings
    35         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_brand_color', ['sanitize_callback' => 'sanitize_hex_color']);
    36        
    37         // OTP Logic Settings
    38         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_code_length', ['sanitize_callback' => 'absint']);
    39         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_resend_cooldown', ['sanitize_callback' => 'absint']);
    40        
    41         // Override Settings
    42         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_override_enabled', ['sanitize_callback' => 'rest_sanitize_boolean']);
    43         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_myaccount_page_id', ['sanitize_callback' => 'absint']);
    44        
    45         // Captcha Settings
    46         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_captcha_type', ['sanitize_callback' => $sanitize_text]);
    47         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_recaptcha_site_key', ['sanitize_callback' => $sanitize_text]);
    48         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_recaptcha_secret_key', ['sanitize_callback' => $sanitize_text]);
    49         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_turnstile_site_key', ['sanitize_callback' => $sanitize_text]);
    50         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_turnstile_secret_key', ['sanitize_callback' => $sanitize_text]);
    51        
    52         // Form Header Settings
    53         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_login_header_html', ['sanitize_callback' => 'wp_kses_post']);
    54         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_register_header_html', ['sanitize_callback' => 'wp_kses_post']);
    55     }
    56 
    57     public static function render(){
    58         if ( ! current_user_can( 'manage_woocommerce' ) ) { return; }
    59         ?>
    60         <div class="wrap">
    61             <h1><?php /* translators: %s: plugin version */ echo esc_html( sprintf( __('Customer Email OTP Verification for WooCommerce (v%s)', 'naimur-email-otp-for-woocommerce'), NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION ) ); ?></h1>
    62             <form method="post" action="options.php">
    63                 <?php settings_fields('naimur-email-otp-for-woocommerce'); ?>
    64                 <table class="form-table" role="presentation">
    65                     <!-- Email Template Section -->
    66                     <tr>
    67                         <th><label for="naimur-email-otp-for-woocommerce_email_subject"><?php esc_html_e('Email Subject', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    68                         <td>
    69                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_email_subject" id="naimur-email-otp-for-woocommerce_email_subject" value="<?php echo esc_attr(get_option('naimur-email-otp-for-woocommerce_email_subject', 'Your Verification Code')); ?>" />
    70                         </td>
    71                     </tr>
    72                     <tr>
    73                         <th><label for="naimur-email-otp-for-woocommerce_email_template"><?php esc_html_e('Select Email Template', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    74                         <td>
    75                             <select name="naimur-email-otp-for-woocommerce_email_template" id="naimur-email-otp-for-woocommerce_email_template">
    76                                 <option value="1" <?php selected(get_option('naimur-email-otp-for-woocommerce_email_template', '1'), '1'); ?>><?php esc_html_e('Template 1 (Simple)', 'naimur-email-otp-for-woocommerce'); ?></option>
    77                                 <option value="2" <?php selected(get_option('naimur-email-otp-for-woocommerce_email_template'), '2'); ?>><?php esc_html_e('Template 2 (Modern)', 'naimur-email-otp-for-woocommerce'); ?></option>
    78                                 <option value="3" <?php selected(get_option('naimur-email-otp-for-woocommerce_email_template'), '3'); ?>><?php esc_html_e('Template 3 (Classic)', 'naimur-email-otp-for-woocommerce'); ?></option>
    79                                 <option value="custom" <?php selected(get_option('naimur-email-otp-for-woocommerce_email_template'), 'custom'); ?>><?php esc_html_e('Custom HTML', 'naimur-email-otp-for-woocommerce'); ?></option>
    80                             </select>
    81                         </td>
    82                     </tr>
    83                    
    84                     <!-- Live Gutenberg-like HTML Editor -->
    85                     <tr id="ceovw_custom_html_row" style="display: none;">
    86                         <th><label for="naimur-email-otp-for-woocommerce_custom_email_html"><?php esc_html_e('Write Custom HTML/CSS', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    87                         <td>
    88                             <textarea name="naimur-email-otp-for-woocommerce_custom_email_html" id="naimur-email-otp-for-woocommerce_custom_email_html" rows="12" class="large-text code" placeholder="<html><body><h1>Your Code: [naimur_email_otp_code]</h1></body></html>"><?php echo esc_textarea(get_option('naimur-email-otp-for-woocommerce_custom_email_html', '')); ?></textarea>
    89                             <p class="description"><?php esc_html_e('Use the shortcode [naimur_email_otp_code] where you want the 6-digit OTP to appear. Changes will preview live below.', 'naimur-email-otp-for-woocommerce'); ?></p>
    90                         </td>
    91                     </tr>
    92 
    93                     <tr>
    94                         <th><?php esc_html_e('Live Visual Preview', 'naimur-email-otp-for-woocommerce'); ?></th>
    95                         <td>
    96                             <div id="ceovw_template_preview_container" style="border: 1px solid #dcdcdc; border-radius: 8px; padding: 15px; background: #f9f9f9; max-width: 600px; min-height: 200px; box-shadow: inset 0 0 10px rgba(0,0,0,0.02);">
    97                                 <!-- Template Preview will be loaded here dynamically -->
    98                             </div>
    99                         </td>
    100                     </tr>
    101 
    102                     <!-- Design Section -->
    103                     <tr>
    104                         <th><label for="naimur-email-otp-for-woocommerce_brand_color"><?php esc_html_e('Brand color (HEX)', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    105                         <td><input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_brand_color" id="naimur-email-otp-for-woocommerce_brand_color" value="<?php echo esc_attr(get_option('naimur-email-otp-for-woocommerce_brand_color', '#3b82f6')); ?>" placeholder="#3b82f6" /></td>
    106                     </tr>
    107                    
    108                     <!-- Override UI Section -->
    109                     <tr>
    110                         <th><?php esc_html_e('Modern Login/Register UI', 'naimur-email-otp-for-woocommerce'); ?></th>
    111                         <td>
    112                             <label><input type="checkbox" name="naimur-email-otp-for-woocommerce_override_enabled" value="1" <?php checked(get_option('naimur-email-otp-for-woocommerce_override_enabled', 0), 1); ?> /> <?php esc_html_e('Enable custom modern design', 'naimur-email-otp-for-woocommerce'); ?></label><br/>
    113                             <div style="margin-top: 10px;">
    114                                 <label for="naimur-email-otp-for-woocommerce_myaccount_page_id" style="display:block; margin-bottom:5px; font-weight:500;"><?php esc_html_e('Select My Account page', 'naimur-email-otp-for-woocommerce'); ?></label>
    115                                 <?php wp_dropdown_pages(['name'=>'naimur-email-otp-for-woocommerce_myaccount_page_id', 'show_option_none'=>'— Select —', 'option_none_value'=>'0', 'selected'=>(int)get_option('naimur-email-otp-for-woocommerce_myaccount_page_id', 0)]); ?>
    116                             </div>
    117                         </td>
    118                     </tr>
    119                    
    120                     <!-- Captcha Section -->
    121                     <tr>
    122                         <th><label for="naimur-email-otp-for-woocommerce_captcha_type"><?php esc_html_e('Captcha Provider', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    123                         <td>
    124                             <select name="naimur-email-otp-for-woocommerce_captcha_type" id="naimur-email-otp-for-woocommerce_captcha_type">
    125                                 <option value="none" <?php selected(get_option('naimur-email-otp-for-woocommerce_captcha_type'), 'none'); ?>><?php esc_html_e('None', 'naimur-email-otp-for-woocommerce'); ?></option>
    126                                 <option value="recaptcha" <?php selected(get_option('naimur-email-otp-for-woocommerce_captcha_type'), 'recaptcha'); ?>><?php esc_html_e('Google reCAPTCHA v2', 'naimur-email-otp-for-woocommerce'); ?></option>
    127                                 <option value="turnstile" <?php selected(get_option('naimur-email-otp-for-woocommerce_captcha_type'), 'turnstile'); ?>><?php esc_html_e('Cloudflare Turnstile', 'naimur-email-otp-for-woocommerce'); ?></option>
    128                             </select>
    129                         </td>
    130                     </tr>
    131                     <tr>
    132                         <th><?php esc_html_e('Google reCAPTCHA Keys', 'naimur-email-otp-for-woocommerce'); ?></th>
    133                         <td>
    134                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_recaptcha_site_key" placeholder="Site Key" value="<?php echo esc_attr( get_option('naimur-email-otp-for-woocommerce_recaptcha_site_key','') ); ?>" style="margin-bottom: 5px;" /><br/>
    135                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_recaptcha_secret_key" placeholder="Secret Key" value="<?php echo esc_attr( get_option('naimur-email-otp-for-woocommerce_recaptcha_secret_key','') ); ?>" />
    136                         </td>
    137                     </tr>
    138                     <tr>
    139                         <th><?php esc_html_e('Cloudflare Turnstile Keys', 'naimur-email-otp-for-woocommerce'); ?></th>
    140                         <td>
    141                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_turnstile_site_key" placeholder="Site Key" value="<?php echo esc_attr( get_option('naimur-email-otp-for-woocommerce_turnstile_site_key','') ); ?>" style="margin-bottom: 5px;" /><br/>
    142                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_turnstile_secret_key" placeholder="Secret Key" value="<?php echo esc_attr( get_option('naimur-email-otp-for-woocommerce_turnstile_secret_key','') ); ?>" />
    143                         </td>
    144                     </tr>
    145 
    146                     <!-- Form Texts -->
    147                     <tr>
    148                         <th><label for="naimur-email-otp-for-woocommerce_login_header_html"><?php esc_html_e( 'Login header HTML', 'naimur-email-otp-for-woocommerce' ); ?></label></th>
    149                         <td><?php wp_editor( get_option( 'naimur-email-otp-for-woocommerce_login_header_html', '<p class="ceovw-muted">Please enter your details</p><h1>Welcome back</h1>' ), 'naimur-email-otp-for-woocommerce_login_header_html', [ 'textarea_rows' => 5 ] ); ?></td>
    150                     </tr>
    151                     <tr>
    152                         <th><label for="naimur-email-otp-for-woocommerce_register_header_html"><?php esc_html_e( 'Register header HTML', 'naimur-email-otp-for-woocommerce' ); ?></label></th>
    153                         <td><?php wp_editor( get_option( 'naimur-email-otp-for-woocommerce_register_header_html', '<h1>Create your account</h1>' ), 'naimur-email-otp-for-woocommerce_register_header_html', [ 'textarea_rows' => 5 ] ); ?></td>
    154                     </tr>
    155                 </table>
    156                 <?php submit_button(); ?>
    157             </form>
    158         </div>
    159 
    160         <script>
    161         jQuery(document).ready(function($) {
    162             var templateSelect = $('#naimur-email-otp-for-woocommerce_email_template');
    163             var customRow = $('#ceovw_custom_html_row');
    164             var customTextarea = $('#naimur-email-otp-for-woocommerce_custom_email_html');
    165             var previewContainer = $('#ceovw_template_preview_container');
    166 
    167             function renderIframe(htmlContent) {
    168                 var encodedStr = htmlContent.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    169                 previewContainer.html('<iframe srcdoc="' + encodedStr + '" style="width:100%; height:450px; border:1px solid #eaeaea; background:#ffffff; border-radius: 4px;"></iframe>');
    170             }
    171 
    172             function updatePreview() {
    173                 var selected_template = templateSelect.val();
    174                
    175                 if (selected_template === 'custom') {
    176                     // Show Custom HTML Textarea
    177                     customRow.show();
    178                     // Live Gutenberg-style rendering
    179                     var rawHtml = customTextarea.val();
    180                     var liveHtml = rawHtml.replace(/\[naimur_email_otp_code\]/g, '<strong style="color:var(--ceovw-brand, #3b82f6); font-size:24px;">123456</strong>');
    181                     renderIframe(liveHtml);
    182                 } else {
    183                     // Hide Custom HTML Textarea & Load Pre-defined Template via AJAX
    184                     customRow.hide();
    185                     previewContainer.html('<p style="color:#666; font-style:italic;">Loading template preview...</p>');
    186                    
    187                     $.post(ajaxurl, {
    188                         action: 'ceovw_preview_template',
    189                         template_id: selected_template,
    190                         _ajax_nonce: '<?php echo wp_create_nonce('ceovw_preview_nonce'); ?>'
    191                     }, function(response) {
    192                         renderIframe(response);
    193                     });
    194                 }
    195             }
    196            
    197             // Initialize
    198             updatePreview();
    199            
    200             // Trigger on Dropdown Change
    201             templateSelect.on('change', updatePreview);
    202            
    203             // Trigger Live Preview on Typing (The Gutenberg effect!)
    204             customTextarea.on('input', updatePreview);
    205         });
    206         </script>
    207     <?php }
    208 
    209     public static function render_help_page(){
    210         if ( ! current_user_can( 'manage_woocommerce' ) ) { return; }
    211         ?>
    212         <div class="wrap">
    213             <h1><?php esc_html_e('Help, Documentation & Support', 'naimur-email-otp-for-woocommerce'); ?></h1>
    214             <p><?php esc_html_e('Thank you for using Naimur Email OTP Verification for WooCommerce. Below you will find useful information to manage and troubleshoot the plugin.', 'naimur-email-otp-for-woocommerce'); ?></p>
    215            
    216             <div class="card" style="max-width: 800px; margin-top: 20px; padding: 20px;">
    217                 <h2 style="margin-top: 0;"><?php esc_html_e('How to Enable the Modern Login/Register UI?', 'naimur-email-otp-for-woocommerce'); ?></h2>
    218                 <p><?php esc_html_e('You do not need to use any shortcodes manually. The plugin comes with an auto-execution system.', 'naimur-email-otp-for-woocommerce'); ?></p>
    219                 <ol style="font-size: 14px; line-height: 1.6;">
    220                     <li><?php esc_html_e('Go to the plugin Settings tab.', 'naimur-email-otp-for-woocommerce'); ?></li>
    221                     <li><?php esc_html_e('Check the box "Enable custom modern design".', 'naimur-email-otp-for-woocommerce'); ?></li>
    222                     <li><?php esc_html_e('From the dropdown, select your WooCommerce "My Account" page.', 'naimur-email-otp-for-woocommerce'); ?></li>
    223                     <li><?php esc_html_e('Save changes.', 'naimur-email-otp-for-woocommerce'); ?></li>
    224                 </ol>
    225                 <p style="color: #646970;"><em><?php esc_html_e('That is it! The plugin will automatically override the default boring WooCommerce form with our modern, responsive, and secure UI design.', 'naimur-email-otp-for-woocommerce'); ?></em></p>
    226             </div>
    227 
    228             <div class="card" style="max-width: 800px; margin-top: 20px; padding: 20px;">
    229                 <h2 style="margin-top: 0;"><?php esc_html_e('Need Support or Found a Bug?', 'naimur-email-otp-for-woocommerce'); ?></h2>
    230                 <p><?php esc_html_e('If you are facing any issues, conflicts, or have questions about how to set up the plugin, please visit our official support forum on WordPress.org. Explain your issue in detail, and we will try to resolve it as soon as possible.', 'naimur-email-otp-for-woocommerce'); ?></p>
    231                 <a href="https://wordpress.org/support/plugin/naimur-email-otp-for-woocommerce/" target="_blank" class="button button-primary"><?php esc_html_e('Get Support on WordPress.org', 'naimur-email-otp-for-woocommerce'); ?></a>
    232             </div>
    233 
    234             <div class="card" style="max-width: 800px; margin-top: 20px; padding: 20px;">
    235                 <h2 style="margin-top: 0;"><?php esc_html_e('Author Personal Consultation', 'naimur-email-otp-for-woocommerce'); ?></h2>
    236                 <p><?php esc_html_e('Need a custom feature, specialized WordPress & WooCommerce development, or one-on-one consultation? You can reach out to the author (Naimur Rahman Nahid) directly through the personal contact form.', 'naimur-email-otp-for-woocommerce'); ?></p>
    237                 <a href="https://naimurrahmannahid.com/contact/" target="_blank" class="button"><?php esc_html_e('Contact for Consultation', 'naimur-email-otp-for-woocommerce'); ?></a>
    238             </div>
    239         </div>
    240         <?php
    24112    }
    24213
    24314    public static function shortcode_code($atts, $content = ''){ return $content; }
    244    
    245     // AJAX Handler for Pre-defined Template Preview
    246     public static function preview_template(){
    247         check_ajax_referer('ceovw_preview_nonce');
    248         if (!current_user_can('manage_woocommerce')) wp_die('-');
    249 
    250         // Only the bundled templates can be previewed. The ID must exactly match the allowlist
    251         // before it is used in a file path; anything else (missing, arrays, path characters) is rejected.
    252         $allowed_templates = ['1', '2', '3'];
    253         $template = ( isset($_POST['template_id']) && is_string($_POST['template_id']) ) ? wp_unslash($_POST['template_id']) : '';
    254         if ( ! in_array( $template, $allowed_templates, true ) ) {
    255             wp_die( esc_html__( 'Invalid template.', 'naimur-email-otp-for-woocommerce' ), '', array( 'response' => 400 ) );
    256         }
    257 
    258         $code = '<strong style="color:#3b82f6; font-size:24px;">123456</strong>';
    259         $file = NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH . 'templates/email-template-' . $template . '.php';
    260         if (file_exists($file)) {
    261             // Read file and replace shortcode for preview
    262             $content = file_get_contents($file);
    263             echo str_replace('[naimur_email_otp_code]', $code, $content);
    264         } else {
    265             echo '<p>Template file not found.</p>';
    266         }
    267         wp_die();
    268     }
    26915}
    27016CEOVW_Settings::init();
  • naimur-email-otp-for-woocommerce/tags/2.1.2/includes/helpers.php

    r3780003 r3780739  
    2323    // token_hash: SHA-256 of the single-use password reset token
    2424    // expires_at: unix timestamp (UTC) after which the row is invalid
     25    // sent_at:    unix timestamp of the last email for this row (server-side resend cooldown, 2.1.2)
     26    // context_hash: HMAC of the WooCommerce session a checkout verification is bound to (2.1.2)
    2527    $sql2 = "CREATE TABLE {$wpdb->prefix}ceovw_otps (
    2628        id mediumint(9) NOT NULL AUTO_INCREMENT,
    … …  
    3234        token_hash char(64) DEFAULT NULL,
    3335        expires_at bigint(20) unsigned NOT NULL DEFAULT 0,
     36        sent_at bigint(20) unsigned NOT NULL DEFAULT 0,
     37        context_hash char(64) DEFAULT NULL,
    3438        created_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
    3539        PRIMARY KEY  (id),
    3640        KEY email (email),
    3741        KEY token_hash (token_hash),
    38         KEY expires_at (expires_at)
     42        KEY expires_at (expires_at),
     43        KEY context_hash (context_hash)
    3944    ) $charset_collate;";
    4045
    … …  
    4550
    4651/**
    47  * Confirms the OTP table has the columns required by the 2.1.1 authorization logic.
     52 * Confirms the OTP table has the columns required by the current authorization logic.
    4853 */
    4954function ceovw_schema_is_valid() {
    … …  
    5459    if ( empty( $columns ) ) { return false; }
    5560
    56     foreach ( ['attempts', 'token_hash', 'expires_at', 'otp'] as $column ) {
     61    foreach ( ['attempts', 'token_hash', 'expires_at', 'otp', 'sent_at', 'context_hash'] as $column ) {
    5762        if ( ! isset( $columns[ $column ] ) ) { return false; }
    5863    }
    … …  
    8489
    8590/**
    86  * 2.1.0 -> 2.1.1 migration. Saves the new DB version only when every required step succeeds.
     91 * Database/settings migration (2.1.0 / 2.1.1 -> current). Saves the new DB version only when every required step succeeds.
    8792 */
    8893function ceovw_run_upgrade() {
    … …  
    122127    // 7. Ensure the cleanup event exists.
    123128    ceovw_schedule_cleanup();
     129
     130    // 7b. 2.1.2: carry the pre-2.1.2 email template settings (custom HTML / subject) into the new template store.
     131    // Deferred to init when needed: the default templates are translated, and translations must not load earlier.
     132    if ( class_exists( 'CEOVW_Templates' ) ) {
     133        if ( did_action( 'init' ) ) { CEOVW_Templates::migrate_legacy(); }
     134        else { add_action( 'init', array( 'CEOVW_Templates', 'migrate_legacy' ), 1 ); }
     135    }
    124136
    125137    // 8. Mark the migration as complete.
    … …  
    230242    $cutoff = wp_date( 'Y-m-d H:i:s', time() - 2 * DAY_IN_SECONDS );
    231243    $wpdb->query( $wpdb->prepare( "DELETE FROM $logs WHERE created_at < %s", $cutoff ) );
     244
     245    // Activity log retention (General settings).
     246    if ( class_exists( 'CEOVW_Options' ) ) {
     247        $keep_after = time() - DAY_IN_SECONDS * (int) CEOVW_Options::get( 'log_retention_days' );
     248        $activity   = get_option( 'ceovw_activity_logs', array() );
     249        if ( is_array( $activity ) && $activity ) {
     250            $kept = array_values( array_filter( $activity, function ( $e ) use ( $keep_after ) { return is_array( $e ) && (int) ( $e['time'] ?? 0 ) >= $keep_after; } ) );
     251            if ( count( $kept ) !== count( $activity ) ) { update_option( 'ceovw_activity_logs', $kept, false ); }
     252        }
     253    }
     254    update_option( 'ceovw_last_cleanup', time(), false );
    232255}
    233256
    … …  
    235258 * Seconds an emailed OTP (and its temporary registration data) stays valid.
    236259 */
    237 function ceovw_otp_ttl() {
    238     return max( 60, (int) apply_filters( 'ceovw_otp_expiry', 300 ) );
     260function ceovw_otp_ttl( $flow = 'register' ) {
     261    return class_exists( 'CEOVW_Options' ) ? CEOVW_Options::ttl( $flow ) : max( 60, (int) apply_filters( 'ceovw_otp_expiry', 300, $flow ) );
    239262}
    240263
  • naimur-email-otp-for-woocommerce/tags/2.1.2/languages/naimur-email-otp-for-woocommerce.pot

    r3587442 r3780739  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Naimur Email OTP Verification for WooCommerce 1.2.0\n"
     5"Project-Id-Version: Naimur Email OTP Verification for WooCommerce 2.1.2\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/naimur-email-otp-for-woocommerce\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-06-22T09:54:30+06:00\n"
     12"POT-Creation-Date: 2026-11-03T00:02:16+06:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    14 "X-Generator: WP-CLI 2.10.0\n"
     14"X-Generator: GNU xgettext\n"
    1515"X-Domain: naimur-email-otp-for-woocommerce\n"
    1616
    … …  
    2020
    2121#. Description of the plugin
    22 msgid "Verify WooCommerce customer emails using a 6-digit OTP before account creation. Modern UI with Google reCAPTCHA & Cloudflare Turnstile protection."
     22msgid "Verify WooCommerce customer emails with a 6-digit OTP for registration, password reset and (optionally) checkout, with editable email templates and a modern admin."
    2323msgstr ""
    2424
    … …  
    2727msgstr ""
    2828
    29 #: naimur-email-otp-for-woocommerce.php:68
    30 #: includes/class-settings.php:13
    31 #: includes/class-settings.php:13
    32 msgid "Email Verification"
    33 msgstr ""
    34 
    35 #: naimur-email-otp-for-woocommerce.php:64
     29#: includes/admin/class-admin.php:27
     30msgid "Overview"
     31msgstr ""
     32
     33#: includes/admin/class-admin.php:27
     34msgid "See what is protected, what still needs setting up, and what happened recently."
     35msgstr ""
     36
     37#: includes/admin/class-admin.php:28
     38msgid "Choose where customers must confirm their email address with a 6-digit code."
     39msgstr ""
     40
     41#: includes/admin/class-admin.php:28 includes/admin/class-admin.php:168
     42#: includes/admin/class-admin.php:169 includes/admin/class-admin.php:170
     43#: includes/admin/class-admin.php:171 includes/admin/class-admin.php:172
     44#: includes/admin/class-admin.php:173 includes/admin/class-admin.php:174
     45#: includes/admin/class-admin.php:175 includes/admin/views/general.php:10
     46#: includes/admin/views/help.php:10 includes/admin/views/security.php:21
     47msgid "Verification"
     48msgstr ""
     49
     50#: includes/admin/class-admin.php:29
     51msgid "Change the wording and design of the emails that carry the verification codes."
     52msgstr ""
     53
     54#: includes/admin/class-admin.php:29 includes/admin/class-admin.php:176
     55#: includes/admin/views/help.php:65
     56msgid "Email Templates"
     57msgstr ""
     58
     59#: includes/admin/class-admin.php:30
     60msgid "Limits that stop abuse, such as too many codes or too many guesses."
     61msgstr ""
     62
     63#: includes/admin/class-admin.php:30 includes/admin/class-admin.php:177
     64#: includes/admin/class-admin.php:178 includes/admin/views/help.php:75
     65msgid "Security"
     66msgstr ""
     67
     68#: includes/admin/class-admin.php:31 includes/admin/class-admin.php:179
     69#: includes/admin/class-admin.php:180 includes/admin/class-admin.php:181
     70#: includes/admin/views/help.php:21
     71msgid "Activity & Diagnostics"
     72msgstr ""
     73
     74#: includes/admin/class-admin.php:31
     75msgid "Send a test email, review recent emails and check that everything is working."
     76msgstr ""
     77
     78#: includes/admin/class-admin.php:32 includes/admin/class-admin.php:182
     79#: includes/admin/class-admin.php:183 includes/admin/class-admin.php:184
     80#: includes/admin/class-admin.php:185 includes/admin/views/help.php:19
     81msgid "General Settings"
     82msgstr ""
     83
     84#: includes/admin/class-admin.php:32
     85msgid "Sender details, branding and what happens to your data when the plugin is deleted."
     86msgstr ""
     87
     88#: includes/admin/class-admin.php:33
     89msgid "Help & Documentation"
     90msgstr ""
     91
     92#: includes/admin/class-admin.php:33
     93msgid "Step-by-step guides and answers to common questions."
     94msgstr ""
     95
     96#: includes/admin/class-admin.php:66 includes/admin/class-admin.php:215
     97msgid "Naimur OTP"
     98msgstr ""
     99
     100#: includes/admin/class-admin.php:123
     101msgid "Settings saved."
     102msgstr ""
     103
     104#: includes/admin/class-admin.php:124
     105msgid "All changes saved"
     106msgstr ""
     107
     108#: includes/admin/class-admin.php:125
     109msgid "Saving…"
     110msgstr ""
     111
     112#: includes/admin/class-admin.php:126
     113msgid "Something went wrong. Please try again."
     114msgstr ""
     115
     116#: includes/admin/class-admin.php:127
     117msgid "Saved. Some values were adjusted to the allowed range."
     118msgstr ""
     119
     120#: includes/admin/class-admin.php:128
     121msgid "Please correct the highlighted fields."
     122msgstr ""
     123
     124#. translators: 1: minimum, 2: maximum
     125#: includes/admin/class-admin.php:130
     126#, php-format
     127msgid "Enter a whole number from %1$d to %2$d."
     128msgstr ""
     129
     130#: includes/admin/class-admin.php:131
     131msgid "Enter a valid email address, for example name@yourstore.com."
     132msgstr ""
     133
     134#: includes/admin/class-admin.php:132
     135msgid "Enter a full web address starting with https://"
     136msgstr ""
     137
     138#: includes/admin/class-admin.php:133
     139msgid "Confirm"
     140msgstr ""
     141
     142#: includes/admin/class-admin.php:134 includes/class-checkout.php:256
     143msgid "Cancel"
     144msgstr ""
     145
     146#: includes/admin/class-admin.php:135
     147msgid "Placeholder inserted."
     148msgstr ""
     149
     150#: includes/admin/class-admin.php:136
     151msgid "Test email sent."
     152msgstr ""
     153
     154#: includes/admin/class-admin.php:137
     155msgid "Choose logo"
     156msgstr ""
     157
     158#: includes/admin/class-admin.php:138
     159msgid "Use this image"
     160msgstr ""
     161
     162#: includes/admin/class-admin.php:139 includes/admin/views/general.php:30
     163msgid "No logo"
     164msgstr ""
     165
     166#: includes/admin/class-admin.php:140 includes/admin/views/help.php:156
     167msgid "No help topics match your search."
     168msgstr ""
     169
     170#: includes/admin/class-admin.php:141
     171msgid "No matching settings."
     172msgstr ""
     173
     174#: includes/admin/class-admin.php:142
     175msgid "Unsaved changes"
     176msgstr ""
     177
     178#: includes/admin/class-admin.php:143
     179msgid "You have unsaved changes. Leave this page?"
     180msgstr ""
     181
     182#: includes/admin/class-admin.php:144
     183msgid "Changes discarded."
     184msgstr ""
     185
     186#: includes/admin/class-admin.php:145
     187msgid "Customized template"
     188msgstr ""
     189
     190#: includes/admin/class-admin.php:146
     191msgid "Built-in default template"
     192msgstr ""
     193
     194#: includes/admin/class-admin.php:147 includes/admin/views/templates.php:19
     195msgid "Customized"
     196msgstr ""
     197
     198#: includes/admin/class-admin.php:148 includes/admin/views/templates.php:19
     199msgid "Default"
     200msgstr ""
     201
     202#: includes/admin/class-admin.php:149
     203msgid "You have unsaved changes. Discard them and switch template?"
     204msgstr ""
     205
     206#: includes/admin/class-admin.php:150
     207msgid "Template saved."
     208msgstr ""
     209
     210#: includes/admin/class-admin.php:151
     211msgid "Template restored to the default."
     212msgstr ""
     213
     214#: includes/admin/class-admin.php:152
     215msgid "Just now"
     216msgstr ""
     217
     218#: includes/admin/class-admin.php:153
     219msgid "Log cleared."
     220msgstr ""
     221
     222#: includes/admin/class-admin.php:154 includes/admin/views/dashboard.php:10
     223#: includes/admin/views/templates.php:13
     224#: includes/admin/views/verification.php:10
     225#: includes/admin/views/verification.php:64
     226msgid "On"
     227msgstr ""
     228
     229#: includes/admin/class-admin.php:155 includes/admin/views/dashboard.php:11
     230#: includes/admin/views/templates.php:13
     231#: includes/admin/views/verification.php:11
     232#: includes/admin/views/verification.php:64
     233msgid "Off"
     234msgstr ""
     235
     236#: includes/admin/class-admin.php:156 includes/admin/views/verification.php:129
     237msgid "Guests and logged-in customers"
     238msgstr ""
     239
     240#: includes/admin/class-admin.php:157 includes/admin/views/verification.php:129
     241msgid "Guest customers only"
     242msgstr ""
     243
     244#. translators: %d: number of matching help topics
     245#: includes/admin/class-admin.php:159
     246#, php-format
     247msgid "%d matching topics"
     248msgstr ""
     249
     250#: includes/admin/class-admin.php:168
     251msgid "Customer login and registration forms"
     252msgstr ""
     253
     254#: includes/admin/class-admin.php:169 includes/admin/class-admin.php:231
     255#: includes/class-templates.php:14
     256msgid "Registration OTP"
     257msgstr ""
     258
     259#: includes/admin/class-admin.php:170 includes/admin/class-admin.php:232
     260#: includes/class-templates.php:15
     261msgid "Password Reset OTP"
     262msgstr ""
     263
     264#: includes/admin/class-admin.php:171 includes/admin/class-admin.php:233
     265#: includes/class-templates.php:16
     266msgid "Checkout OTP"
     267msgstr ""
     268
     269#: includes/admin/class-admin.php:172 includes/admin/views/verification.php:33
     270msgid "Code Expiration Time"
     271msgstr ""
     272
     273#: includes/admin/class-admin.php:173 includes/admin/views/verification.php:34
     274msgid "Wait Before Resending"
     275msgstr ""
     276
     277#: includes/admin/class-admin.php:174 includes/admin/views/verification.php:35
     278msgid "Wrong Code Attempts Allowed"
     279msgstr ""
     280
     281#: includes/admin/class-admin.php:175 includes/admin/views/verification.php:112
     282msgid "Also verify logged-in customers"
     283msgstr ""
     284
     285#: includes/admin/class-admin.php:176
     286msgid "Email templates"
     287msgstr ""
     288
     289#: includes/admin/class-admin.php:177 includes/admin/views/security.php:7
     290msgid "Rate limits"
     291msgstr ""
     292
     293#: includes/admin/class-admin.php:178
     294msgid "Visitor IP address and proxies"
     295msgstr ""
     296
     297#: includes/admin/class-admin.php:179 includes/admin/views/dashboard.php:51
     298#: includes/admin/views/diagnostics.php:31 includes/admin/views/help.php:86
     299msgid "Send a test email"
     300msgstr ""
     301
     302#: includes/admin/class-admin.php:180 includes/admin/views/diagnostics.php:48
     303msgid "Email log"
     304msgstr ""
     305
     306#: includes/admin/class-admin.php:181 includes/admin/views/diagnostics.php:99
     307msgid "Keep activity history for"
     308msgstr ""
     309
     310#: includes/admin/class-admin.php:182
     311msgid "Sender name and email"
     312msgstr ""
     313
     314#: includes/admin/class-admin.php:183 includes/admin/views/general.php:28
     315msgid "Store logo for emails"
     316msgstr ""
     317
     318#: includes/admin/class-admin.php:184 includes/admin/views/general.php:42
     319msgid "Brand colour"
     320msgstr ""
     321
     322#: includes/admin/class-admin.php:185
     323msgid "Delete data when the plugin is deleted"
     324msgstr ""
     325
     326#: includes/admin/class-admin.php:198
     327msgid "You do not have permission to access this page."
     328msgstr ""
     329
     330#: includes/admin/class-admin.php:203
     331msgid "Naimur Email OTP"
     332msgstr ""
     333
     334#. translators: %s: plugin version
     335#: includes/admin/class-admin.php:203
     336#, php-format
     337msgid "Version %s"
     338msgstr ""
     339
     340#: includes/admin/class-admin.php:204
     341msgid "Find a setting"
     342msgstr ""
     343
     344#: includes/admin/class-admin.php:205
     345msgid "Find a setting…"
     346msgstr ""
     347
     348#: includes/admin/class-admin.php:205
     349msgid "Type to search, then use the arrow keys to choose a result."
     350msgstr ""
     351
     352#: includes/admin/class-admin.php:207
     353msgid "Naimur OTP sections"
     354msgstr ""
     355
     356#: includes/admin/class-admin.php:214
     357msgid "Breadcrumb"
     358msgstr ""
     359
     360#. translators: 1: minimum, 2: maximum
     361#: includes/admin/class-admin.php:276
     362#, php-format
     363msgid "Allowed range: %1$d to %2$d."
     364msgstr ""
     365
     366#: includes/admin/class-admin.php:300
     367msgid "Learn more"
     368msgstr ""
     369
     370#: includes/admin/class-admin.php:305 includes/admin/views/templates.php:67
     371msgid "Discard changes"
     372msgstr ""
     373
     374#: includes/admin/class-admin.php:305
     375msgid "Save changes"
     376msgstr ""
     377
     378#: includes/admin/class-admin.php:384
     379msgid "Never"
     380msgstr ""
     381
     382#: includes/admin/class-admin.php:386
     383#, php-format
     384msgid "%s ago"
     385msgstr ""
     386
     387#. translators: %s: human time difference
     388#: includes/admin/class-admin.php:386
     389#, php-format
     390msgid "in %s"
     391msgstr ""
     392
     393#: includes/admin/class-admin.php:392
     394msgid "Your session expired. Please reload the page."
     395msgstr ""
     396
     397#: includes/admin/class-admin.php:393 includes/admin/class-admin.php:408
     398msgid "You do not have permission to do this."
     399msgstr ""
     400
     401#: includes/admin/class-admin.php:407
     402msgid "Unknown settings section."
     403msgstr ""
     404
     405#: includes/admin/class-admin.php:415
     406msgid "Please enter a valid sender email address."
     407msgstr ""
     408
     409#: includes/admin/class-admin.php:450 naimur-email-otp-for-woocommerce.php:116
     410msgid "Please enter a valid email address."
     411msgstr ""
     412
     413#: includes/admin/class-admin.php:458
     414msgid "WordPress could not send the email. Check your mail or SMTP configuration."
     415msgstr ""
     416
     417#. translators: %s: recipient email address
     418#: includes/admin/class-admin.php:459
     419#, php-format
     420msgid "Test email sent to %s. Check the inbox (and the spam folder)."
     421msgstr ""
     422
     423#: includes/admin/class-admin.php:467
     424msgid "Unknown log."
     425msgstr ""
     426
     427#: includes/admin/class-admin.php:468
     428msgid "Logs cleared."
     429msgstr ""
     430
     431#: includes/admin/class-admin.php:473
     432msgid "The database upgrade has not completed yet."
     433msgstr ""
     434
     435#: includes/admin/class-admin.php:476
     436msgid "Cleanup finished."
     437msgstr ""
     438
     439#: includes/admin/views/dashboard.php:12 includes/admin/views/templates.php:13
     440#: includes/admin/views/verification.php:12
     441msgid "Needs setup"
     442msgstr ""
     443
     444#: includes/admin/views/dashboard.php:17
     445msgid "Registration Verification"
     446msgstr ""
     447
     448#: includes/admin/views/dashboard.php:19
     449msgid "New accounts are created without an email code."
     450msgstr ""
     451
     452#: includes/admin/views/dashboard.php:19
     453msgid "New customers confirm their email before their account is created."
     454msgstr ""
     455
     456#: includes/admin/views/dashboard.php:19
     457msgid "Turned on, but the customer login and registration forms are off, so no code is asked for yet."
     458msgstr ""
     459
     460#: includes/admin/views/dashboard.php:21
     461msgid "Registration settings"
     462msgstr ""
     463
     464#: includes/admin/views/dashboard.php:25
     465msgid "Password Reset Verification"
     466msgstr ""
     467
     468#: includes/admin/views/dashboard.php:27
     469msgid "Customers reset their password with an emailed code."
     470msgstr ""
     471
     472#: includes/admin/views/dashboard.php:27
     473msgid "The standard WooCommerce reset link is used."
     474msgstr ""
     475
     476#: includes/admin/views/dashboard.php:27
     477msgid "Turned on, but the customer login and registration forms are off, so the standard WooCommerce reset link is used."
     478msgstr ""
     479
     480#: includes/admin/views/dashboard.php:29
     481msgid "Password reset settings"
     482msgstr ""
     483
     484#: includes/admin/views/dashboard.php:33
     485msgid "Checkout Verification"
     486msgstr ""
     487
     488#: includes/admin/views/dashboard.php:35
     489msgid "Optional. Orders are placed without an email code."
     490msgstr ""
     491
     492#: includes/admin/views/dashboard.php:35
     493msgid "Orders are created only after the billing email is confirmed."
     494msgstr ""
     495
     496#: includes/admin/views/dashboard.php:37
     497msgid "Checkout settings"
     498msgstr ""
     499
     500#: includes/admin/views/dashboard.php:41
     501msgid "Email Delivery"
     502msgstr ""
     503
     504#: includes/admin/views/dashboard.php:43
     505msgid "Working"
     506msgstr ""
     507
     508#: includes/admin/views/dashboard.php:44
     509msgid "Not tested yet"
     510msgstr ""
     511
     512#: includes/admin/views/dashboard.php:45
     513msgid "Problem found"
     514msgstr ""
     515
     516#: includes/admin/views/dashboard.php:47
     517msgid "The most recent email could not be sent. Check your mail or SMTP setup."
     518msgstr ""
     519
     520#. translators: %s: relative time, e.g. "5 mins ago"
     521#: includes/admin/views/dashboard.php:48
     522#, php-format
     523msgid "Last test email sent %s."
     524msgstr ""
     525
     526#: includes/admin/views/dashboard.php:49
     527msgid "Emails are being handed to your mail system."
     528msgstr ""
     529
     530#: includes/admin/views/dashboard.php:49
     531msgid "Send a test email to make sure codes reach your customers."
     532msgstr ""
     533
     534#: includes/admin/views/dashboard.php:55
     535msgid "Security Health"
     536msgstr ""
     537
     538#: includes/admin/views/dashboard.php:56
     539#: includes/admin/views/diagnostics.php:82
     540msgid "Healthy"
     541msgstr ""
     542
     543#: includes/admin/views/dashboard.php:56
     544msgid "Needs attention"
     545msgstr ""
     546
     547#: includes/admin/views/dashboard.php:57
     548msgid "A database update is still running. Verification is paused until it finishes."
     549msgstr ""
     550
     551#: includes/admin/views/dashboard.php:58
     552msgid "Rate limits are active and expired codes are removed automatically."
     553msgstr ""
     554
     555#: includes/admin/views/dashboard.php:58
     556msgid "The automatic cleanup of expired codes is not running on time."
     557msgstr ""
     558
     559#: includes/admin/views/dashboard.php:60 includes/admin/views/help.php:69
     560msgid "Security settings"
     561msgstr ""
     562
     563#: includes/admin/views/dashboard.php:60
     564msgid "View diagnostics"
     565msgstr ""
     566
     567#: includes/admin/views/dashboard.php:65
     568msgid "Registration and password reset codes appear inside these forms on your My Account page."
     569msgstr ""
     570
     571#: includes/admin/views/dashboard.php:65
     572msgid "Turn on the customer login and registration forms"
     573msgstr ""
     574
     575#: includes/admin/views/dashboard.php:66
     576msgid "Emails from your own domain are less likely to land in spam."
     577msgstr ""
     578
     579#: includes/admin/views/dashboard.php:66
     580msgid "Set the sender name and email address"
     581msgstr ""
     582
     583#: includes/admin/views/dashboard.php:67
     584msgid "Add your store logo to the emails"
     585msgstr ""
     586
     587#: includes/admin/views/dashboard.php:67
     588msgid "Customers trust emails that look like your store."
     589msgstr ""
     590
     591#: includes/admin/views/dashboard.php:68
     592msgid "Confirms that your site can deliver the codes."
     593msgstr ""
     594
     595#: includes/admin/views/dashboard.php:68
     596msgid "Send a test email and check that it arrives"
     597msgstr ""
     598
     599#: includes/admin/views/dashboard.php:69
     600msgid "Place a test order yourself first."
     601msgstr ""
     602
     603#: includes/admin/views/dashboard.php:69
     604msgid "Turn on checkout verification"
     605msgstr ""
     606
     607#: includes/admin/views/dashboard.php:79
     608msgid "Welcome to Naimur Email OTP"
     609msgstr ""
     610
     611#: includes/admin/views/dashboard.php:80
     612msgid "Stop fake accounts and protect your customers by asking for a one-time code sent by email when they register, reset a password or check out."
     613msgstr ""
     614
     615#: includes/admin/views/dashboard.php:85
     616msgid "Everything looks good"
     617msgstr ""
     618
     619#. translators: %d: number of items that need attention
     620#: includes/admin/views/dashboard.php:88
     621#, php-format
     622msgid "%d item needs attention"
     623msgid_plural "%d items need attention"
     624msgstr[0] ""
     625msgstr[1] ""
     626
     627#. translators: %s: plugin version
     628#: includes/admin/views/dashboard.php:91
     629#, php-format
     630msgid "Plugin version %s"
     631msgstr ""
     632
     633#: includes/admin/views/dashboard.php:95 includes/admin/views/dashboard.php:147
     634#: includes/admin/views/dashboard.php:149
     635#: includes/admin/views/diagnostics.php:53
     636#: includes/admin/views/diagnostics.php:55
     637msgid "Status"
     638msgstr ""
     639
     640#. translators: 1: completed steps, 2: total steps
     641#: includes/admin/views/dashboard.php:109
     642#, php-format
     643msgid "%1$d of %2$d done"
     644msgstr ""
     645
     646#: includes/admin/views/dashboard.php:109
     647msgid "Setup checklist"
     648msgstr ""
     649
     650#: includes/admin/views/dashboard.php:110
     651msgid "Setup progress"
     652msgstr ""
     653
     654#: includes/admin/views/dashboard.php:117
     655msgid "Optional"
     656msgstr ""
     657
     658#: includes/admin/views/dashboard.php:120
     659msgid "Done"
     660msgstr ""
     661
     662#: includes/admin/views/dashboard.php:120
     663msgid "Not done yet"
     664msgstr ""
     665
     666#: includes/admin/views/dashboard.php:124
     667msgid "Sender and logo settings can be changed by a site administrator."
     668msgstr ""
     669
     670#: includes/admin/views/dashboard.php:127
     671msgid "Quick actions"
     672msgstr ""
     673
     674#: includes/admin/views/dashboard.php:129
     675msgid "Configure Checkout OTP"
     676msgstr ""
     677
     678#: includes/admin/views/dashboard.php:129
     679msgid "Turn it on or off and choose who is affected."
     680msgstr ""
     681
     682#: includes/admin/views/dashboard.php:131
     683msgid "Change the subject, text and design."
     684msgstr ""
     685
     686#: includes/admin/views/dashboard.php:131
     687msgid "Edit Email Template"
     688msgstr ""
     689
     690#: includes/admin/views/dashboard.php:133
     691msgid "Check that codes reach the inbox."
     692msgstr ""
     693
     694#: includes/admin/views/dashboard.php:133
     695msgid "Send a Test Email"
     696msgstr ""
     697
     698#: includes/admin/views/dashboard.php:135
     699msgid "Limits for codes and wrong guesses."
     700msgstr ""
     701
     702#: includes/admin/views/dashboard.php:135
     703msgid "Open Security Settings"
     704msgstr ""
     705
     706#: includes/admin/views/dashboard.php:137
     707msgid "Step-by-step guides and answers."
     708msgstr ""
     709
     710#: includes/admin/views/dashboard.php:137
     711msgid "View Documentation"
     712msgstr ""
     713
     714#: includes/admin/views/dashboard.php:143
     715msgid "Recent emails"
     716msgstr ""
     717
     718#: includes/admin/views/dashboard.php:143
     719msgid "View all"
     720msgstr ""
     721
     722#: includes/admin/views/dashboard.php:145
     723msgid "No verification emails yet. They appear here once customers start receiving codes."
     724msgstr ""
     725
     726#: includes/admin/views/dashboard.php:147
     727#: includes/admin/views/dashboard.php:149
     728#: includes/admin/views/diagnostics.php:35
     729#: includes/admin/views/diagnostics.php:53
     730#: includes/admin/views/diagnostics.php:55
     731#: includes/admin/views/diagnostics.php:66
     732#: includes/admin/views/diagnostics.php:68
     733msgid "Email"
     734msgstr ""
     735
     736#: includes/admin/views/dashboard.php:147
     737#: includes/admin/views/dashboard.php:149
     738#: includes/admin/views/diagnostics.php:66
     739#: includes/admin/views/diagnostics.php:68
     740msgid "Event"
     741msgstr ""
     742
     743#: includes/admin/views/dashboard.php:147
     744#: includes/admin/views/dashboard.php:149
     745#: includes/admin/views/diagnostics.php:53
     746#: includes/admin/views/diagnostics.php:55
     747#: includes/admin/views/diagnostics.php:66
     748#: includes/admin/views/diagnostics.php:68
     749msgid "Time"
     750msgstr ""
     751
     752#: includes/admin/views/diagnostics.php:13
     753msgid "WooCommerce is not active. This plugin needs WooCommerce."
     754msgstr ""
     755
     756#: includes/admin/views/diagnostics.php:14
     757msgid "The database update has not finished. Verification is paused until it does; reload this page to try again."
     758msgstr ""
     759
     760#: includes/admin/views/diagnostics.php:15
     761msgid "No SMTP plugin found. Emails are sent with the basic server mail function, which is often marked as spam. An SMTP plugin is recommended."
     762msgstr ""
     763
     764#: includes/admin/views/diagnostics.php:16
     765msgid "WP-Cron is turned off (DISABLE_WP_CRON). Make sure a server cron job runs wp-cron.php, or expired codes are only removed when you click \"Run cleanup now\"."
     766msgstr ""
     767
     768#: includes/admin/views/diagnostics.php:17
     769msgid "Checkout verification is on, but the checkout page type was not recognised."
     770msgstr ""
     771
     772#: includes/admin/views/diagnostics.php:20
     773msgid "Checkout email verified"
     774msgstr ""
     775
     776#: includes/admin/views/diagnostics.php:21
     777msgid "Order placed with verified email"
     778msgstr ""
     779
     780#: includes/admin/views/diagnostics.php:22
     781msgid "Registered with verified email"
     782msgstr ""
     783
     784#: includes/admin/views/diagnostics.php:23
     785msgid "Password reset code verified"
     786msgstr ""
     787
     788#: includes/admin/views/diagnostics.php:27
     789msgid "Things to check"
     790msgstr ""
     791
     792#: includes/admin/views/diagnostics.php:31
     793msgid "Emails not arriving?"
     794msgstr ""
     795
     796#: includes/admin/views/diagnostics.php:32
     797msgid "Sends one of your saved emails with sample data (code 123456) so you can check that it arrives and looks right."
     798msgstr ""
     799
     800#: includes/admin/views/diagnostics.php:39
     801msgid "Send to"
     802msgstr ""
     803
     804#: includes/admin/views/diagnostics.php:42
     805msgid "Send test email"
     806msgstr ""
     807
     808#: includes/admin/views/diagnostics.php:44
     809msgid "If it does not arrive within a few minutes, check the spam folder and your SMTP plugin's log."
     810msgstr ""
     811
     812#: includes/admin/views/diagnostics.php:48
     813#: includes/admin/views/diagnostics.php:62
     814msgid "Clear log"
     815msgstr ""
     816
     817#: includes/admin/views/diagnostics.php:48
     818msgid "Clear the email log? This cannot be undone."
     819msgstr ""
     820
     821#: includes/admin/views/diagnostics.php:49
     822msgid "\"Success\" means WordPress handed the email to your mail system. It does not guarantee it reached the inbox. The log keeps the last 100 emails for 7 days."
     823msgstr ""
     824
     825#: includes/admin/views/diagnostics.php:51
     826msgid "No emails logged yet."
     827msgstr ""
     828
     829#: includes/admin/views/diagnostics.php:53
     830#: includes/admin/views/diagnostics.php:55
     831msgid "Message"
     832msgstr ""
     833
     834#: includes/admin/views/diagnostics.php:62
     835msgid "Clear the activity log? This cannot be undone."
     836msgstr ""
     837
     838#: includes/admin/views/diagnostics.php:62
     839msgid "Verification activity"
     840msgstr ""
     841
     842#: includes/admin/views/diagnostics.php:64
     843msgid "No verification activity yet."
     844msgstr ""
     845
     846#: includes/admin/views/diagnostics.php:66
     847#: includes/admin/views/diagnostics.php:68
     848msgid "Device"
     849msgstr ""
     850
     851#: includes/admin/views/diagnostics.php:66
     852#: includes/admin/views/diagnostics.php:68
     853msgid "User"
     854msgstr ""
     855
     856#: includes/admin/views/diagnostics.php:68
     857msgid "Email verified"
     858msgstr ""
     859
     860#: includes/admin/views/diagnostics.php:68
     861msgid "Guest"
     862msgstr ""
     863
     864#: includes/admin/views/diagnostics.php:76
     865msgid "System information"
     866msgstr ""
     867
     868#: includes/admin/views/diagnostics.php:78
     869msgid "Plugin"
     870msgstr ""
     871
     872#: includes/admin/views/diagnostics.php:80
     873msgid "Not active"
     874msgstr ""
     875
     876#: includes/admin/views/diagnostics.php:82
     877#: includes/admin/views/verification.php:162
     878msgid "Database"
     879msgstr ""
     880
     881#: includes/admin/views/diagnostics.php:82
     882msgid "Needs update"
     883msgstr ""
     884
     885#: includes/admin/views/diagnostics.php:83
     886msgid "Codes waiting to be used"
     887msgstr ""
     888
     889#: includes/admin/views/diagnostics.php:84
     890#: includes/admin/views/verification.php:47
     891#: includes/admin/views/verification.php:152
     892msgid "Checkout block"
     893msgstr ""
     894
     895#: includes/admin/views/diagnostics.php:84
     896msgid "Checkout page"
     897msgstr ""
     898
     899#: includes/admin/views/diagnostics.php:84
     900#: includes/admin/views/verification.php:154
     901msgid "Classic checkout"
     902msgstr ""
     903
     904#: includes/admin/views/diagnostics.php:84
     905msgid "Not recognised"
     906msgstr ""
     907
     908#: includes/admin/views/diagnostics.php:84
     909msgid "Not set"
     910msgstr ""
     911
     912#: includes/admin/views/diagnostics.php:85
     913msgid "High-performance order storage"
     914msgstr ""
     915
     916#: includes/admin/views/diagnostics.php:85
     917#: includes/admin/views/verification.php:160
     918msgid "Order storage"
     919msgstr ""
     920
     921#: includes/admin/views/diagnostics.php:85
     922msgid "WordPress posts"
     923msgstr ""
     924
     925#: includes/admin/views/diagnostics.php:86
     926msgid "Mail"
     927msgstr ""
     928
     929#: includes/admin/views/diagnostics.php:86
     930msgid "PHP mail (no SMTP plugin found)"
     931msgstr ""
     932
     933#: includes/admin/views/diagnostics.php:88
     934msgid "Include these versions when you ask for support. Never share codes, passwords or customer data."
     935msgstr ""
     936
     937#: includes/admin/views/diagnostics.php:91
     938msgid "Maintenance"
     939msgstr ""
     940
     941#: includes/admin/views/diagnostics.php:93
     942msgid "Next cleanup"
     943msgstr ""
     944
     945#: includes/admin/views/diagnostics.php:93
     946msgid "Not scheduled"
     947msgstr ""
     948
     949#: includes/admin/views/diagnostics.php:94
     950msgid "Last cleanup"
     951msgstr ""
     952
     953#: includes/admin/views/diagnostics.php:96
     954msgid "Expired codes and old history are removed automatically every hour. You can also run it now."
     955msgstr ""
     956
     957#: includes/admin/views/diagnostics.php:97
     958msgid "Run cleanup now"
     959msgstr ""
     960
     961#: includes/admin/views/diagnostics.php:99
     962msgid "Older verification activity is removed by the cleanup."
     963msgstr ""
     964
     965#: includes/admin/views/diagnostics.php:99
     966msgid "days"
     967msgstr ""
     968
     969#. translators: %s: link to the Verification page
     970#: includes/admin/views/general.php:9
     971#, php-format
     972msgid "Looking for the customer login and registration form settings? They are now on the %s page, next to the codes they control."
     973msgstr ""
     974
     975#: includes/admin/views/general.php:14
     976msgid "Sender"
     977msgstr ""
     978
     979#: includes/admin/views/general.php:15
     980msgid "Who the verification emails come from. Only emails sent by this plugin are changed. Leave empty to use your WordPress defaults."
     981msgstr ""
     982
     983#: includes/admin/views/general.php:18
     984msgid "Sender name"
     985msgstr ""
     986
     987#: includes/admin/views/general.php:18
     988msgid "Usually your store name."
     989msgstr ""
     990
     991#: includes/admin/views/general.php:19
     992msgid "Sender email"
     993msgstr ""
     994
     995#: includes/admin/views/general.php:19
     996msgid "Use an address on your own domain so emails are not marked as spam. Also shown to customers as the support address."
     997msgstr ""
     998
     999#: includes/admin/views/general.php:25
     1000msgid "Branding"
     1001msgstr ""
     1002
     1003#: includes/admin/views/general.php:34
     1004msgid "Choose from Media Library"
     1005msgstr ""
     1006
     1007#: includes/admin/views/general.php:35
     1008msgid "Remove"
     1009msgstr ""
     1010
     1011#: includes/admin/views/general.php:39
     1012msgid "When empty, the WooCommerce email header image is used."
     1013msgstr ""
     1014
     1015#: includes/admin/views/general.php:39
     1016msgid "When empty, your store name is shown as text."
     1017msgstr ""
     1018
     1019#: includes/admin/views/general.php:44
     1020msgid "Used for buttons and highlights in the emails, the checkout popup and the customer forms."
     1021msgstr ""
     1022
     1023#: includes/admin/views/general.php:50
     1024msgid "When the plugin is deleted"
     1025msgstr ""
     1026
     1027#: includes/admin/views/general.php:51
     1028msgid "Delete all plugin data when the plugin is deleted"
     1029msgstr ""
     1030
     1031#: includes/admin/views/general.php:51
     1032msgid "When off, your settings and email templates are kept so you can reinstall later. Pending codes, reset authorizations and logs are always removed."
     1033msgstr ""
     1034
     1035#: includes/admin/views/help.php:13
     1036msgid "Getting started"
     1037msgstr ""
     1038
     1039#: includes/admin/views/help.php:15
     1040msgid "Follow these steps once. The Overview page shows a checklist that ticks itself off as you go."
     1041msgstr ""
     1042
     1043#. translators: %s: link to the Verification page
     1044#: includes/admin/views/help.php:17
     1045#, php-format
     1046msgid "1. Open %s and turn on the customer login and registration forms (Step 1). Choose your My Account page."
     1047msgstr ""
     1048
     1049#. translators: %s: link to General Settings
     1050#: includes/admin/views/help.php:19
     1051#, php-format
     1052msgid "2. In %s, set your sender name, sender email and store logo so emails look like they come from your store."
     1053msgstr ""
     1054
     1055#. translators: %s: link to Activity & Diagnostics
     1056#: includes/admin/views/help.php:21
     1057#, php-format
     1058msgid "3. Send a test email from %s and make sure it arrives."
     1059msgstr ""
     1060
     1061#: includes/admin/views/help.php:22
     1062msgid "4. Optional: turn on Checkout OTP after you have placed a test order yourself."
     1063msgstr ""
     1064
     1065#: includes/admin/views/help.php:26
     1066msgid "Registration OTP setup"
     1067msgstr ""
     1068
     1069#: includes/admin/views/help.php:28
     1070msgid "When a customer signs up on your My Account page, they receive a 6-digit code by email and must enter it before the account is created. Email addresses that are not confirmed never become accounts."
     1071msgstr ""
     1072
     1073#: includes/admin/views/help.php:29
     1074msgid "It needs the customer login and registration forms (Step 1 on the Verification page). If those forms are off, the Registration tab shows \"Needs setup\"."
     1075msgstr ""
     1076
     1077#: includes/admin/views/help.php:30
     1078msgid "Code Expiration Time, Wait Before Resending and Wrong Code Attempts Allowed can be set separately for registration."
     1079msgstr ""
     1080
     1081#. translators: %s: link to the Registration tab
     1082#. translators: %s: link to the Password Reset tab
     1083#. translators: %s: link to the Checkout tab
     1084#. translators: %s: link to the test email section
     1085#: includes/admin/views/help.php:32 includes/admin/views/help.php:43
     1086#: includes/admin/views/help.php:54 includes/admin/views/help.php:86
     1087#, php-format
     1088msgid "Go to %s."
     1089msgstr ""
     1090
     1091#: includes/admin/views/help.php:32
     1092msgid "Registration OTP settings"
     1093msgstr ""
     1094
     1095#: includes/admin/views/help.php:36
     1096msgid "Password reset setup"
     1097msgstr ""
     1098
     1099#: includes/admin/views/help.php:38
     1100msgid "The customer clicks \"Forgot password?\", enters their email, receives a code and, after entering it, chooses a new password with the \"Update Password\" button."
     1101msgstr ""
     1102
     1103#: includes/admin/views/help.php:39
     1104msgid "The reset permission works once and expires after 5 minutes. The customer then signs in with the new password; they are never signed in automatically."
     1105msgstr ""
     1106
     1107#: includes/admin/views/help.php:40
     1108msgid "The form shows the same message whether or not an account exists, so it cannot be used to find out who is registered."
     1109msgstr ""
     1110
     1111#: includes/admin/views/help.php:41
     1112msgid "If you turn this off, the standard WooCommerce \"Lost your password?\" link is used instead."
     1113msgstr ""
     1114
     1115#: includes/admin/views/help.php:43
     1116msgid "Password Reset OTP settings"
     1117msgstr ""
     1118
     1119#: includes/admin/views/help.php:47
     1120msgid "Checkout OTP setup"
     1121msgstr ""
     1122
     1123#: includes/admin/views/help.php:49
     1124msgid "Off by default. When on, clicking Place order opens a popup asking for the code sent to the billing email. The order is created only after the code is verified, so no payment is taken before that."
     1125msgstr ""
     1126
     1127#: includes/admin/views/help.php:50
     1128msgid "Guests are always asked. Logged-in customers are asked too unless you turn off \"Also verify logged-in customers\"."
     1129msgstr ""
     1130
     1131#: includes/admin/views/help.php:51
     1132msgid "A verification is valid once, for the same browser session and the exact same email. Changing the email needs a new code. If a payment fails, the customer can retry payment for the same order without a new code."
     1133msgstr ""
     1134
     1135#: includes/admin/views/help.php:52
     1136msgid "Before turning it on: place a test order with each payment method you use and check that the popup appears and the order completes."
     1137msgstr ""
     1138
     1139#: includes/admin/views/help.php:54
     1140msgid "Checkout OTP settings"
     1141msgstr ""
     1142
     1143#: includes/admin/views/help.php:58
     1144msgid "Email template customization"
     1145msgstr ""
     1146
     1147#: includes/admin/views/help.php:60
     1148msgid "Each email (Registration, Password Reset, Checkout) has its own subject, preheader and design. Choose one with the tabs at the top of the Email Templates page. The preview on the right updates as you type; use Desktop and Mobile to check both sizes."
     1149msgstr ""
     1150
     1151#: includes/admin/views/help.php:61
     1152msgid "Click a placeholder to insert it where your cursor is. {{otp_code}} must appear in every email. Others: {{site_name}}, {{site_url}}, {{customer_name}}, {{customer_email}}, {{expiry_minutes}}, {{support_email}}, {{site_logo}}, {{brand_color}} and {{preheader}}."
     1153msgstr ""
     1154
     1155#: includes/admin/views/help.php:62
     1156msgid "For safety, scripts, forms, iframes, event handlers and javascript: links are removed when you save, and PHP is never run. Placeholder values are escaped."
     1157msgstr ""
     1158
     1159#: includes/admin/views/help.php:63
     1160msgid "\"Send test\" emails what is in the editor now. \"Discard changes\" returns to the last saved version, and \"Reset to default\" restores the built-in design."
     1161msgstr ""
     1162
     1163#. translators: %s: link to Email Templates
     1164#: includes/admin/views/help.php:65
     1165#, php-format
     1166msgid "Go to %s. Only site administrators can edit email HTML."
     1167msgstr ""
     1168
     1169#: includes/admin/views/help.php:71
     1170msgid "Rate limits count codes sent per visitor and per email address, and code checks per visitor. They reset automatically after their time window, so a blocked visitor can try again later."
     1171msgstr ""
     1172
     1173#: includes/admin/views/help.php:72
     1174msgid "The visitor address comes from REMOTE_ADDR. Forwarded headers are ignored because they can be forged. Behind Cloudflare or another proxy, a developer can supply the real address with the ceovw_client_ip filter after checking the request came from the proxy."
     1175msgstr ""
     1176
     1177#: includes/admin/views/help.php:73
     1178msgid "CAPTCHA is not available in this version. Earlier versions showed CAPTCHA widgets without checking them on the server, so they were removed rather than give a false sense of protection."
     1179msgstr ""
     1180
     1181#. translators: %s: link to Security
     1182#: includes/admin/views/help.php:75
     1183#, php-format
     1184msgid "Go to %s. Only site administrators can change these limits."
     1185msgstr ""
     1186
     1187#: includes/admin/views/help.php:79
     1188msgid "SMTP troubleshooting: emails are not arriving"
     1189msgstr ""
     1190
     1191#: includes/admin/views/help.php:81
     1192msgid "1. Install and set up an SMTP plugin, for example WP Mail SMTP or FluentSMTP. Many hosts block or spam-filter the basic PHP mail function."
     1193msgstr ""
     1194
     1195#: includes/admin/views/help.php:82
     1196msgid "2. Use a sender email address on your own domain (General Settings)."
     1197msgstr ""
     1198
     1199#: includes/admin/views/help.php:83
     1200msgid "3. Send a test email and check the inbox and the spam folder."
     1201msgstr ""
     1202
     1203#: includes/admin/views/help.php:84
     1204msgid "4. Look at the email log. \"Failed\" means WordPress could not hand the email over; check your SMTP plugin's settings and log. \"Success\" means it was handed over, not that it reached the inbox."
     1205msgstr ""
     1206
     1207#: includes/admin/views/help.php:90
     1208msgid "WooCommerce compatibility"
     1209msgstr ""
     1210
     1211#: includes/admin/views/help.php:92
     1212msgid "Checkout block and classic checkout ([woocommerce_checkout] shortcode): supported."
     1213msgstr ""
     1214
     1215#: includes/admin/views/help.php:93
     1216msgid "High-performance order storage (HPOS) and WordPress posts storage: supported."
     1217msgstr ""
     1218
     1219#: includes/admin/views/help.php:94
     1220msgid "Payment methods: tested with Cash on delivery and Direct bank transfer. Other gateways have not been tested, so place a test order with yours before turning Checkout OTP on."
     1221msgstr ""
     1222
     1223#: includes/admin/views/help.php:95
     1224msgid "Express payment buttons outside the checkout form (for example Apple Pay or Google Pay on the cart page) cannot show the popup. While Checkout OTP is on, those orders are refused and the customer completes the purchase through the checkout form."
     1225msgstr ""
     1226
     1227#: includes/admin/views/help.php:96
     1228msgid "Custom checkout pages that use neither the block nor the shortcode are still protected on the server, but the popup may not appear. Activity & Diagnostics shows which checkout type your store uses."
     1229msgstr ""
     1230
     1231#: includes/admin/views/help.php:100
     1232msgid "Frequently asked questions"
     1233msgstr ""
     1234
     1235#: includes/admin/views/help.php:102
     1236msgid "Does Checkout OTP turn on by itself after updating?"
     1237msgstr ""
     1238
     1239#: includes/admin/views/help.php:102
     1240msgid "No. It stays off until you turn it on."
     1241msgstr ""
     1242
     1243#: includes/admin/views/help.php:103
     1244msgid "Can a customer reuse a code?"
     1245msgstr ""
     1246
     1247#: includes/admin/views/help.php:103
     1248msgid "No. Every code works once, only for the purpose it was sent for, and only until it expires."
     1249msgstr ""
     1250
     1251#: includes/admin/views/help.php:104
     1252msgid "Shop managers can turn verification on or off and send test emails. Email HTML, security limits and general settings need a site administrator."
     1253msgstr ""
     1254
     1255#: includes/admin/views/help.php:104
     1256msgid "Who can change these settings?"
     1257msgstr ""
     1258
     1259#: includes/admin/views/help.php:105
     1260msgid "Pending codes, reset permissions and logs are always removed. Settings and templates are kept unless \"Delete all plugin data\" is on in General Settings."
     1261msgstr ""
     1262
     1263#: includes/admin/views/help.php:105
     1264msgid "What happens when I delete the plugin?"
     1265msgstr ""
     1266
     1267#: includes/admin/views/help.php:106
     1268msgid "Are codes or passwords stored in the logs?"
     1269msgstr ""
     1270
     1271#: includes/admin/views/help.php:106
     1272msgid "No. Codes are stored only as hashes, logs never contain codes, passwords or tokens, and email addresses are masked."
     1273msgstr ""
     1274
     1275#: includes/admin/views/help.php:110
     1276msgid "Common errors and solutions"
     1277msgstr ""
     1278
     1279#: includes/admin/views/help.php:112
     1280msgid "\"Too many requests. Please try again later.\" or \"Please wait … seconds before requesting a new code.\""
     1281msgstr ""
     1282
     1283#: includes/admin/views/help.php:112
     1284msgid "A rate limit or the resend wait was reached. The customer can try again after the time shown. If many real customers see this, raise the limits on the Security page."
     1285msgstr ""
     1286
     1287#: includes/admin/views/help.php:113
     1288msgid "\"That code is not correct.\" or \"This code has expired.\""
     1289msgstr ""
     1290
     1291#: includes/admin/views/help.php:113
     1292msgid "The code was mistyped, already used, replaced by a newer one, or older than the Code Expiration Time. Ask the customer to request a new code and use the newest email."
     1293msgstr ""
     1294
     1295#: includes/admin/views/help.php:114
     1296msgid "\"Please verify your email address to place your order.\""
     1297msgstr ""
     1298
     1299#: includes/admin/views/help.php:114
     1300msgid "The order came from a place that cannot show the popup, such as an express payment button. The customer should use the checkout form."
     1301msgstr ""
     1302
     1303#: includes/admin/views/help.php:115
     1304msgid "Registration shows no code step"
     1305msgstr ""
     1306
     1307#: includes/admin/views/help.php:115
     1308msgid "The customer login and registration forms are off, or the wrong My Account page is selected. Check Step 1 on the Verification page."
     1309msgstr ""
     1310
     1311#: includes/admin/views/help.php:116
     1312msgid "\"This service is temporarily unavailable.\" or \"Email verification is temporarily unavailable…\""
     1313msgstr ""
     1314
     1315#: includes/admin/views/help.php:116
     1316msgid "Usually the plugin's database update has not finished, so verification is paused for safety. Open any admin page to let it finish; if the message continues, check Activity & Diagnostics and ask for support."
     1317msgstr ""
     1318
     1319#: includes/admin/views/help.php:120
     1320msgid "For developers"
     1321msgstr ""
     1322
     1323#: includes/admin/views/help.php:122
     1324msgid "ceovw_client_ip (filter): return the visitor IP address used for rate limits."
     1325msgstr ""
     1326
     1327#: includes/admin/views/help.php:123
     1328msgid "ceovw_otp_expiry (filter): code lifetime in seconds, receives the flow name. Minimum 60."
     1329msgstr ""
     1330
     1331#: includes/admin/views/help.php:124
     1332msgid "ceovw_checkout_auth_ttl (filter): how long a verified checkout email stays valid before the order is placed, in seconds. Default 1800."
     1333msgstr ""
     1334
     1335#: includes/admin/views/help.php:125
     1336msgid "ceovw_admin_capability (filter): capability needed for these pages. Default manage_woocommerce."
     1337msgstr ""
     1338
     1339#: includes/admin/views/help.php:126
     1340msgid "ceovw_settings_capability (filter): capability needed for email HTML, security limits and general settings. Default manage_options."
     1341msgstr ""
     1342
     1343#: includes/admin/views/help.php:127
     1344msgid "Codes and reset permissions are stored as hashes in the wp_ceovw_otps table; rate-limit history is in wp_ceovw_logs. A scheduled event removes expired records every hour."
     1345msgstr ""
     1346
     1347#: includes/admin/views/help.php:134
     1348msgid "Topics"
     1349msgstr ""
     1350
     1351#: includes/admin/views/help.php:143
     1352msgid "Search the documentation"
     1353msgstr ""
     1354
     1355#: includes/admin/views/help.php:144
     1356msgid "For example: checkout, SMTP, placeholders"
     1357msgstr ""
     1358
     1359#: includes/admin/views/help.php:159
     1360msgid "Still need help?"
     1361msgstr ""
     1362
     1363#. translators: %s: link to the WordPress.org support forum
     1364#: includes/admin/views/help.php:163
     1365#, php-format
     1366msgid "Ask in the %s. Please include the versions shown under System information in Activity & Diagnostics, but never post codes, passwords or personal data."
     1367msgstr ""
     1368
     1369#: includes/admin/views/help.php:164
     1370msgid "(opens in a new tab)"
     1371msgstr ""
     1372
     1373#: includes/admin/views/help.php:164
     1374msgid "WordPress.org support forum"
     1375msgstr ""
     1376
     1377#: includes/admin/views/security.php:8
     1378msgid "These limits apply to registration, password reset and checkout codes. They stop one visitor from flooding inboxes or guessing codes. The defaults suit most stores; values outside the safe range are adjusted automatically."
     1379msgstr ""
     1380
     1381#: includes/admin/views/security.php:11
     1382msgid "Codes per visitor, per hour"
     1383msgstr ""
     1384
     1385#: includes/admin/views/security.php:11
     1386msgid "How many codes one visitor (IP address) can request in an hour."
     1387msgstr ""
     1388
     1389#: includes/admin/views/security.php:11
     1390msgid "per hour"
     1391msgstr ""
     1392
     1393#: includes/admin/views/security.php:12
     1394msgid "Codes per email address, per day"
     1395msgstr ""
     1396
     1397#: includes/admin/views/security.php:12
     1398msgid "Protects one email address from receiving too many codes."
     1399msgstr ""
     1400
     1401#: includes/admin/views/security.php:12
     1402msgid "per day"
     1403msgstr ""
     1404
     1405#: includes/admin/views/security.php:13
     1406msgid "Code checks per visitor, per 15 minutes"
     1407msgstr ""
     1408
     1409#: includes/admin/views/security.php:13
     1410msgid "How many times one visitor can try codes. Slows down guessing."
     1411msgstr ""
     1412
     1413#: includes/admin/views/security.php:13 includes/admin/views/security.php:14
     1414msgid "per 15 min"
     1415msgstr ""
     1416
     1417#: includes/admin/views/security.php:14
     1418msgid "Applies to the last step of a password reset."
     1419msgstr ""
     1420
     1421#: includes/admin/views/security.php:14
     1422msgid "Password changes per visitor, per 15 minutes"
     1423msgstr ""
     1424
     1425#. translators: %s: link to the Verification page
     1426#: includes/admin/views/security.php:20
     1427#, php-format
     1428msgid "Code expiration time, resend wait and wrong attempts are set for each verification type on the %s page."
     1429msgstr ""
     1430
     1431#: includes/admin/views/security.php:28
     1432msgid "Visitor IP address"
     1433msgstr ""
     1434
     1435#: includes/admin/views/security.php:29
     1436msgid "Rate limits use the address your web server reports for each connection (REMOTE_ADDR). Headers such as X-Forwarded-For are ignored because visitors can forge them."
     1437msgstr ""
     1438
     1439#: includes/admin/views/security.php:31
     1440msgid "Your address right now"
     1441msgstr ""
     1442
     1443#: includes/admin/views/security.php:33
     1444msgid "Custom address resolver"
     1445msgstr ""
     1446
     1447#: includes/admin/views/security.php:34
     1448msgid "Active (ceovw_client_ip filter)"
     1449msgstr ""
     1450
     1451#: includes/admin/views/security.php:34
     1452msgid "None"
     1453msgstr ""
     1454
     1455#: includes/admin/views/security.php:36
     1456msgid "Using Cloudflare or another proxy? Every visitor may then share the proxy's address. A developer can pass the real address with the ceovw_client_ip filter after checking the request really came from the proxy."
     1457msgstr ""
     1458
     1459#: includes/admin/views/security.php:39
     1460msgid "CAPTCHA"
     1461msgstr ""
     1462
     1463#: includes/admin/views/security.php:39
     1464msgid "Unavailable"
     1465msgstr ""
     1466
     1467#: includes/admin/views/security.php:40
     1468msgid "CAPTCHA is not part of this version. Earlier versions showed reCAPTCHA or Turnstile widgets without verifying them on the server, so they gave no real protection and have been removed. Rate limits, per-email limits and attempt limits protect the forms instead."
     1469msgstr ""
     1470
     1471#: includes/admin/views/security.php:42
     1472msgid "A CAPTCHA provider was selected in an earlier version. Its saved keys are kept but not used."
     1473msgstr ""
     1474
     1475#: includes/admin/views/security.php:48
     1476msgid "Always-on protections"
     1477msgstr ""
     1478
     1479#: includes/admin/views/security.php:49
     1480msgid "These cannot be turned off."
     1481msgstr ""
     1482
     1483#: includes/admin/views/security.php:51
     1484msgid "Codes are random 6-digit numbers from a cryptographically secure generator"
     1485msgstr ""
     1486
     1487#: includes/admin/views/security.php:52
     1488msgid "Codes are stored only as a keyed hash, never in plain text"
     1489msgstr ""
     1490
     1491#: includes/admin/views/security.php:53
     1492msgid "Each code works once, and wrong attempts are counted safely"
     1493msgstr ""
     1494
     1495#: includes/admin/views/security.php:54
     1496msgid "A registration code cannot reset a password or verify a checkout"
     1497msgstr ""
     1498
     1499#: includes/admin/views/security.php:55
     1500msgid "A verified password reset is valid once, for 5 minutes, and never signs the customer in"
     1501msgstr ""
     1502
     1503#: includes/admin/views/security.php:56
     1504msgid "Password reset shows the same message whether or not the account exists"
     1505msgstr ""
     1506
     1507#: includes/admin/views/security.php:57
     1508msgid "Checkout verification is tied to the browser session and the exact email"
     1509msgstr ""
     1510
     1511#: includes/admin/views/security.php:58
     1512msgid "Logs never contain codes, passwords or tokens, and emails are masked"
     1513msgstr ""
     1514
     1515#: includes/admin/views/security.php:59
     1516msgid "Expired records are removed by a scheduled cleanup"
     1517msgstr ""
     1518
     1519#: includes/admin/views/security.php:60
     1520msgid "Verification is blocked while a database update is pending"
     1521msgstr ""
     1522
     1523#: includes/admin/views/templates.php:16
     1524msgid "Email template"
     1525msgstr ""
     1526
     1527#. translators: 1: verification name, 2: On/Off, 3: link to the settings
     1528#: includes/admin/views/templates.php:34
     1529#, php-format
     1530msgid "Sent for %1$s (currently %2$s). %3$s"
     1531msgstr ""
     1532
     1533#: includes/admin/views/templates.php:36
     1534msgid "Change settings"
     1535msgstr ""
     1536
     1537#: includes/admin/views/templates.php:42
     1538msgid "Email subject"
     1539msgstr ""
     1540
     1541#: includes/admin/views/templates.php:46
     1542msgid "Preheader"
     1543msgstr ""
     1544
     1545#: includes/admin/views/templates.php:48
     1546msgid "The short preview line many inboxes show after the subject."
     1547msgstr ""
     1548
     1549#: includes/admin/views/templates.php:51
     1550msgid "Email design (HTML)"
     1551msgstr ""
     1552
     1553#: includes/admin/views/templates.php:53
     1554msgid "For safety, scripts, forms, iframes, event handlers and unsafe links are removed when you save. PHP is never run."
     1555msgstr ""
     1556
     1557#: includes/admin/views/templates.php:56
     1558msgid "(click to insert at the cursor)"
     1559msgstr ""
     1560
     1561#: includes/admin/views/templates.php:56
     1562msgid "Available placeholders"
     1563msgstr ""
     1564
     1565#. translators: 1: placeholder, 2: description
     1566#: includes/admin/views/templates.php:59
     1567#, php-format
     1568msgid "Insert %1$s: %2$s"
     1569msgstr ""
     1570
     1571#: includes/admin/views/templates.php:65
     1572msgid "Replace this template with the built-in default? Your changes will be lost."
     1573msgstr ""
     1574
     1575#: includes/admin/views/templates.php:65
     1576msgid "Reset to default"
     1577msgstr ""
     1578
     1579#: includes/admin/views/templates.php:68
     1580msgid "Save template"
     1581msgstr ""
     1582
     1583#: includes/admin/views/templates.php:74
     1584msgid "Live preview"
     1585msgstr ""
     1586
     1587#: includes/admin/views/templates.php:75
     1588msgid "Preview width"
     1589msgstr ""
     1590
     1591#: includes/admin/views/templates.php:76
     1592msgid "Desktop"
     1593msgstr ""
     1594
     1595#: includes/admin/views/templates.php:77
     1596msgid "Mobile"
     1597msgstr ""
     1598
     1599#: includes/admin/views/templates.php:80
     1600msgid "Subject:"
     1601msgstr ""
     1602
     1603#: includes/admin/views/templates.php:81
     1604msgid "Email preview"
     1605msgstr ""
     1606
     1607#: includes/admin/views/templates.php:82
     1608msgid "The preview uses sample data (Jane Doe, code 123456) and runs with scripts disabled."
     1609msgstr ""
     1610
     1611#: includes/admin/views/templates.php:84
     1612msgid "Send a test of this email"
     1613msgstr ""
     1614
     1615#: includes/admin/views/templates.php:87
     1616msgid "Send test"
     1617msgstr ""
     1618
     1619#: includes/admin/views/templates.php:89
     1620msgid "Sends what is in the editor now, with sample data. It does not create a real code."
     1621msgstr ""
     1622
     1623#: includes/admin/views/templates.php:91
     1624msgid "Template guide"
     1625msgstr ""
     1626
     1627#: includes/admin/views/verification.php:18
     1628msgid "Ask new customers for a code when they create an account"
     1629msgstr ""
     1630
     1631#: includes/admin/views/verification.php:19
     1632msgid "The account is created only after the customer enters the 6-digit code sent to their email."
     1633msgstr ""
     1634
     1635#: includes/admin/views/verification.php:20
     1636msgid "Stops fake sign-ups and typos in email addresses. Customers fill in the registration form, receive a code by email and enter it to finish creating their account."
     1637msgstr ""
     1638
     1639#: includes/admin/views/verification.php:25
     1640msgid "Let customers reset their password with an emailed code"
     1641msgstr ""
     1642
     1643#: includes/admin/views/verification.php:26
     1644msgid "When off, the standard WooCommerce \"Lost your password?\" link is used instead."
     1645msgstr ""
     1646
     1647#: includes/admin/views/verification.php:27
     1648msgid "Customers enter their email, receive a code and then choose a new password. They are not signed in automatically afterwards, and the form never reveals whether an email is registered."
     1649msgstr ""
     1650
     1651#: includes/admin/views/verification.php:33
     1652msgid "How long the verification code remains valid. 5 minutes is recommended."
     1653msgstr ""
     1654
     1655#: includes/admin/views/verification.php:33
     1656msgid "minutes"
     1657msgstr ""
     1658
     1659#: includes/admin/views/verification.php:34
     1660msgid "How long customers wait before they can ask for a new code."
     1661msgstr ""
     1662
     1663#: includes/admin/views/verification.php:34
     1664msgid "seconds"
     1665msgstr ""
     1666
     1667#: includes/admin/views/verification.php:35
     1668msgid "After this many wrong entries the code stops working and a new one is needed."
     1669msgstr ""
     1670
     1671#: includes/admin/views/verification.php:35
     1672msgid "tries"
     1673msgstr ""
     1674
     1675#: includes/admin/views/verification.php:39
     1676msgid "Related:"
     1677msgstr ""
     1678
     1679#: includes/admin/views/verification.php:41
     1680msgid "Edit this email"
     1681msgstr ""
     1682
     1683#: includes/admin/views/verification.php:42
     1684msgid "Security limits"
     1685msgstr ""
     1686
     1687#: includes/admin/views/verification.php:44
     1688msgid "How it works"
     1689msgstr ""
     1690
     1691#: includes/admin/views/verification.php:48
     1692msgid "Classic checkout (shortcode)"
     1693msgstr ""
     1694
     1695#: includes/admin/views/verification.php:49
     1696msgid "Custom checkout page (not recognised)"
     1697msgstr ""
     1698
     1699#: includes/admin/views/verification.php:50
     1700msgid "No checkout page set"
     1701msgstr ""
     1702
     1703#: includes/admin/views/verification.php:53
     1704msgid "Verification type"
     1705msgstr ""
     1706
     1707#: includes/admin/views/verification.php:63
     1708msgid "Step 1: Customer login and registration forms"
     1709msgstr ""
     1710
     1711#: includes/admin/views/verification.php:66
     1712msgid "Registration and password reset codes are asked for inside the plugin's own sign-in, sign-up and password reset forms. They replace the standard forms on your My Account page for visitors who are not signed in. This setting is shared by the Registration and Password Reset tabs."
     1713msgstr ""
     1714
     1715#: includes/admin/views/verification.php:67
     1716msgid "Required for registration and password reset codes. Checkout verification works without it."
     1717msgstr ""
     1718
     1719#: includes/admin/views/verification.php:67
     1720msgid "Use the Naimur OTP login, registration and password reset forms"
     1721msgstr ""
     1722
     1723#: includes/admin/views/verification.php:69
     1724msgid "My Account page"
     1725msgstr ""
     1726
     1727#: includes/admin/views/verification.php:74
     1728msgid "— Select a page —"
     1729msgstr ""
     1730
     1731#: includes/admin/views/verification.php:79
     1732msgid "Usually the same page as WooCommerce → Settings → Advanced → My account page."
     1733msgstr ""
     1734
     1735#: includes/admin/views/verification.php:93
     1736msgid "Go to Step 1"
     1737msgstr ""
     1738
     1739#: includes/admin/views/verification.php:93
     1740msgid "This needs the customer login and registration forms. Turn them on in Step 1 above."
     1741msgstr ""
     1742
     1743#: includes/admin/views/verification.php:109
     1744msgid "Customers confirm their billing email with a 6-digit code before the order is created. The check runs on your server, so no order is created and no payment is taken until the code is verified."
     1745msgstr ""
     1746
     1747#: includes/admin/views/verification.php:111
     1748msgid "Off by default. Place a test order with your own payment method before turning this on for customers."
     1749msgstr ""
     1750
     1751#: includes/admin/views/verification.php:111
     1752msgid "Require email verification at checkout"
     1753msgstr ""
     1754
     1755#: includes/admin/views/verification.php:112
     1756msgid "When off, only guest checkouts are asked for a code."
     1757msgstr ""
     1758
     1759#: includes/admin/views/verification.php:120
     1760msgid "How checkout verification works"
     1761msgstr ""
     1762
     1763#: includes/admin/views/verification.php:122
     1764msgid "Customer places order"
     1765msgstr ""
     1766
     1767#: includes/admin/views/verification.php:122
     1768msgid "They fill in checkout and click Place order."
     1769msgstr ""
     1770
     1771#: includes/admin/views/verification.php:123
     1772msgid "A popup opens and a code goes to the billing email."
     1773msgstr ""
     1774
     1775#: includes/admin/views/verification.php:123
     1776msgid "Email code sent"
     1777msgstr ""
     1778
     1779#: includes/admin/views/verification.php:124
     1780msgid "Customer verifies"
     1781msgstr ""
     1782
     1783#: includes/admin/views/verification.php:124
     1784msgid "They type or paste the 6-digit code."
     1785msgstr ""
     1786
     1787#: includes/admin/views/verification.php:125
     1788msgid "Checkout continues"
     1789msgstr ""
     1790
     1791#: includes/admin/views/verification.php:125
     1792msgid "The order is placed automatically."
     1793msgstr ""
     1794
     1795#: includes/admin/views/verification.php:128
     1796msgid "Which customers are affected?"
     1797msgstr ""
     1798
     1799#: includes/admin/views/verification.php:129
     1800msgid "A verified email stays verified for that browser session, so customers are not asked twice."
     1801msgstr ""
     1802
     1803#: includes/admin/views/verification.php:130
     1804msgid "When does the code popup appear?"
     1805msgstr ""
     1806
     1807#: includes/admin/views/verification.php:131
     1808msgid "Right after the customer clicks Place order, before the order is created. If they change the billing email, a new code is needed."
     1809msgstr ""
     1810
     1811#: includes/admin/views/verification.php:132
     1812msgid "How long does the code last?"
     1813msgstr ""
     1814
     1815#. translators: %s: number of minutes
     1816#: includes/admin/views/verification.php:133
     1817#, php-format
     1818msgid "%s minutes, set by Code Expiration Time above."
     1819msgstr ""
     1820
     1821#: includes/admin/views/verification.php:134
     1822msgid "What if the customer cannot verify?"
     1823msgstr ""
     1824
     1825#. translators: %s: number of seconds
     1826#: includes/admin/views/verification.php:135
     1827#, php-format
     1828msgid "The order is not created and no payment is taken. They can ask for a new code after %s seconds, correct their email address, or close the popup and keep their cart."
     1829msgstr ""
     1830
     1831#: includes/admin/views/verification.php:136
     1832msgid "Which email is sent?"
     1833msgstr ""
     1834
     1835#: includes/admin/views/verification.php:139
     1836msgid "Checkout OTP email template"
     1837msgstr ""
     1838
     1839#. translators: %s: link to the email template
     1840#: includes/admin/views/verification.php:139
     1841#, php-format
     1842msgid "The %s."
     1843msgstr ""
     1844
     1845#: includes/admin/views/verification.php:141
     1846msgid "The Checkout OTP email template."
     1847msgstr ""
     1848
     1849#: includes/admin/views/verification.php:148
     1850msgid "Compatibility"
     1851msgstr ""
     1852
     1853#: includes/admin/views/verification.php:150
     1854msgid "Your checkout page"
     1855msgstr ""
     1856
     1857#: includes/admin/views/verification.php:153
     1858#: includes/admin/views/verification.php:155
     1859msgid "Supported"
     1860msgstr ""
     1861
     1862#: includes/admin/views/verification.php:153
     1863msgid "The code popup opens when Place order is clicked."
     1864msgstr ""
     1865
     1866#: includes/admin/views/verification.php:155
     1867msgid "Pages using the [woocommerce_checkout] shortcode."
     1868msgstr ""
     1869
     1870#: includes/admin/views/verification.php:156
     1871msgid "Payment methods"
     1872msgstr ""
     1873
     1874#: includes/admin/views/verification.php:157
     1875msgid "Test yours first"
     1876msgstr ""
     1877
     1878#: includes/admin/views/verification.php:157
     1879msgid "Tested with Cash on delivery and Direct bank transfer. Other payment gateways have not been tested; place a test order with yours before turning this on."
     1880msgstr ""
     1881
     1882#: includes/admin/views/verification.php:158
     1883msgid "Express payment buttons"
     1884msgstr ""
     1885
     1886#: includes/admin/views/verification.php:159
     1887msgid "Buttons such as Apple Pay or Google Pay outside the checkout form cannot show the popup. While verification is on, those orders are refused with \"Please verify your email address to place your order.\" and the customer uses the checkout form instead."
     1888msgstr ""
     1889
     1890#: includes/admin/views/verification.php:159
     1891msgid "Not supported"
     1892msgstr ""
     1893
     1894#: includes/admin/views/verification.php:161
     1895msgid "High-performance order storage (supported)"
     1896msgstr ""
     1897
     1898#: includes/admin/views/verification.php:161
     1899msgid "WordPress posts storage (supported)"
     1900msgstr ""
     1901
     1902#: includes/admin/views/verification.php:163
     1903msgid "Ready"
     1904msgstr ""
     1905
     1906#: includes/admin/views/verification.php:163
     1907msgid "Update pending: checkout is blocked until it completes"
     1908msgstr ""
     1909
     1910#: includes/admin/views/verification.php:166
     1911msgid "Your checkout page uses neither the Checkout block nor the [woocommerce_checkout] shortcode. Verification is still enforced on the server, but the code popup may not appear. Test before enabling."
     1912msgstr ""
     1913
     1914#: includes/class-ajax.php:83
     1915msgid "Registration is not available."
     1916msgstr ""
     1917
     1918#. translators: %d: seconds
     1919#: includes/class-ajax.php:90
     1920#, php-format
     1921msgid "Please wait %d seconds before requesting a new code."
     1922msgstr ""
     1923
     1924#: includes/class-ajax.php:126
     1925msgid "Password reset by code is not available. Please use the standard lost password link."
     1926msgstr ""
     1927
     1928#: includes/class-ajax.php:174
     1929msgid "This verification is not available."
     1930msgstr ""
     1931
     1932#: includes/class-checkout.php:77
     1933msgid "Please verify your email address to place your order."
     1934msgstr ""
     1935
     1936#: includes/class-checkout.php:86
     1937msgid "Email verification is temporarily unavailable, so orders cannot be placed right now. Please try again shortly."
     1938msgstr ""
     1939
     1940#: includes/class-checkout.php:151 includes/class-checkout.php:165
     1941msgid "Your session has expired. Please refresh the page and try again."
     1942msgstr ""
     1943
     1944#: includes/class-checkout.php:153
     1945msgid "Email verification is not required."
     1946msgstr ""
     1947
     1948#: includes/class-checkout.php:157
     1949msgid "Please enter a valid billing email address first."
     1950msgstr ""
     1951
     1952#: includes/class-checkout.php:163
     1953msgid "Too many requests. Please try again later."
     1954msgstr ""
     1955
     1956#: includes/class-checkout.php:169
     1957msgid "A code was already sent. Please check your inbox."
     1958msgstr ""
     1959
     1960#: includes/class-checkout.php:171
     1961msgid "Too many codes were requested for this email. Please try again later."
     1962msgstr ""
     1963
     1964#: includes/class-checkout.php:179
     1965msgid "We could not send the verification email. Please check the address and try again."
     1966msgstr ""
     1967
     1968#: includes/class-checkout.php:192
     1969msgid "Too many attempts. Please try again later."
     1970msgstr ""
     1971
     1972#: includes/class-checkout.php:199 includes/class-checkout.php:254
     1973msgid "This code has expired. Please request a new one."
     1974msgstr ""
     1975
     1976#: includes/class-checkout.php:200
     1977msgid "Too many incorrect attempts. Please request a new code."
     1978msgstr ""
     1979
     1980#: includes/class-checkout.php:201 includes/class-checkout.php:207
     1981msgid "That code is not correct. Please try again."
     1982msgstr ""
     1983
     1984#: includes/class-checkout.php:239
     1985msgid "Verify your email"
     1986msgstr ""
     1987
     1988#. translators: %s: masked email address
     1989#: includes/class-checkout.php:241
     1990#, php-format
     1991msgid "We sent a 6-digit code to %s. Enter it below to place your order."
     1992msgstr ""
     1993
     1994#: includes/class-checkout.php:242
     1995msgid "Sending your code…"
     1996msgstr ""
     1997
     1998#. translators: %d: position of the digit (1-6)
     1999#. translators: %d: digit position
     2000#: includes/class-checkout.php:244 templates/modal.php:14
     2001#, php-format
     2002msgid "Digit %d of 6"
     2003msgstr ""
     2004
     2005#: includes/class-checkout.php:245
     2006msgid "Verify and place order"
     2007msgstr ""
     2008
     2009#: includes/class-checkout.php:246
     2010msgid "Verifying…"
     2011msgstr ""
     2012
     2013#: includes/class-checkout.php:247
     2014msgid "Email verified. Placing your order…"
     2015msgstr ""
     2016
     2017#: includes/class-checkout.php:248
     2018msgid "Resend code"
     2019msgstr ""
     2020
     2021#. translators: %d: seconds
     2022#: includes/class-checkout.php:250
     2023#, php-format
     2024msgid "Resend code in %ds"
     2025msgstr ""
     2026
     2027#: includes/class-checkout.php:251 naimur-email-otp-for-woocommerce.php:119
     2028msgid "A new code has been sent."
     2029msgstr ""
     2030
     2031#. translators: %s: time like 4:59
     2032#: includes/class-checkout.php:253
     2033#, php-format
     2034msgid "Code expires in %s"
     2035msgstr ""
     2036
     2037#: includes/class-checkout.php:255
     2038msgid "Change email"
     2039msgstr ""
     2040
     2041#: includes/class-checkout.php:257 templates/modal.php:6
     2042msgid "Close"
     2043msgstr ""
     2044
     2045#: includes/class-checkout.php:258 naimur-email-otp-for-woocommerce.php:118
     2046msgid "Please enter all 6 digits."
     2047msgstr ""
     2048
     2049#: includes/class-checkout.php:259
     2050msgid "Something went wrong. Please check your connection and try again."
     2051msgstr ""
     2052
     2053#: includes/class-checkout.php:260
     2054msgid "Try again"
     2055msgstr ""
     2056
     2057#: includes/class-emails.php:31
     2058msgid "Test"
     2059msgstr ""
     2060
     2061#: includes/class-templates.php:23
     2062msgid "Your store name"
     2063msgstr ""
     2064
     2065#: includes/class-templates.php:24
     2066msgid "Your store address (URL)"
     2067msgstr ""
     2068
     2069#: includes/class-templates.php:25
     2070msgid "Customer name (or \"there\" when unknown)"
     2071msgstr ""
     2072
     2073#: includes/class-templates.php:26
     2074msgid "Customer email address"
     2075msgstr ""
     2076
     2077#: includes/class-templates.php:27
     2078msgid "The 6-digit code (required)"
     2079msgstr ""
     2080
     2081#: includes/class-templates.php:28
     2082msgid "Minutes until the code expires"
     2083msgstr ""
     2084
     2085#: includes/class-templates.php:29
     2086msgid "Support email address"
     2087msgstr ""
     2088
     2089#: includes/class-templates.php:30
     2090msgid "Store logo image (or the store name when no logo is set)"
     2091msgstr ""
     2092
     2093#: includes/class-templates.php:31
     2094msgid "Brand accent colour (hex)"
     2095msgstr ""
     2096
     2097#: includes/class-templates.php:32
     2098msgid "Preheader text (inbox preview line)"
     2099msgstr ""
     2100
     2101#: includes/class-templates.php:39
     2102msgid "Your {{site_name}} verification code"
     2103msgstr ""
     2104
     2105#: includes/class-templates.php:40
     2106msgid "Use this code to confirm your email address."
     2107msgstr ""
     2108
     2109#: includes/class-templates.php:41
     2110msgid "Confirm your email address"
     2111msgstr ""
     2112
     2113#: includes/class-templates.php:42
     2114msgid "Hi {{customer_name}}, use the code below to finish creating your {{site_name}} account."
     2115msgstr ""
     2116
     2117#: includes/class-templates.php:45
     2118msgid "Your {{site_name}} password reset code"
     2119msgstr ""
     2120
     2121#: includes/class-templates.php:46
     2122msgid "Use this code to reset your password."
     2123msgstr ""
     2124
     2125#: includes/class-templates.php:47
     2126msgid "Reset your password"
     2127msgstr ""
     2128
     2129#: includes/class-templates.php:48
     2130msgid "We received a request to reset the password for {{customer_email}}. Enter the code below to continue."
     2131msgstr ""
     2132
     2133#: includes/class-templates.php:51
     2134msgid "Your {{site_name}} checkout verification code"
     2135msgstr ""
     2136
     2137#: includes/class-templates.php:52
     2138msgid "Use this code to confirm your email and place your order."
     2139msgstr ""
     2140
     2141#: includes/class-templates.php:53
     2142msgid "Verify your email to place your order"
     2143msgstr ""
     2144
     2145#: includes/class-templates.php:54
     2146msgid "Hi {{customer_name}}, enter the code below on the checkout page to confirm {{customer_email}} and complete your order."
     2147msgstr ""
     2148
     2149#: includes/class-templates.php:63
     2150msgid "This code expires in {{expiry_minutes}} minutes and can be used once."
     2151msgstr ""
     2152
     2153#: includes/class-templates.php:64
     2154msgid "If you did not request this code, you can ignore this email. Never share this code with anyone. We will never ask you for it."
     2155msgstr ""
     2156
     2157#: includes/class-templates.php:65
     2158msgid "Need help? Contact us at {{support_email}}."
     2159msgstr ""
     2160
     2161#: includes/class-templates.php:159
     2162msgid "The email subject is required."
     2163msgstr ""
     2164
     2165#: includes/class-templates.php:160
     2166msgid "The email subject must be 200 characters or fewer."
     2167msgstr ""
     2168
     2169#: includes/class-templates.php:161
     2170msgid "The preheader must be 255 characters or fewer."
     2171msgstr ""
     2172
     2173#: includes/class-templates.php:162
     2174msgid "The HTML is too large (maximum 100 KB)."
     2175msgstr ""
     2176
     2177#: includes/class-templates.php:165
     2178msgid "The HTML must contain the {{otp_code}} placeholder, otherwise customers will not receive their code."
     2179msgstr ""
     2180
     2181#: includes/class-templates.php:168
     2182msgid "Some HTML was removed for security: scripts, forms, event handlers, unsafe links or unsupported tags are not allowed in emails."
     2183msgstr ""
     2184
     2185#: includes/class-templates.php:228
     2186msgid "there"
     2187msgstr ""
     2188
     2189#: includes/class-users.php:12
     2190msgid "An account is already registered with your email address."
     2191msgstr ""
     2192
     2193#: naimur-email-otp-for-woocommerce.php:110
     2194msgid "Processing..."
     2195msgstr ""
     2196
     2197#: naimur-email-otp-for-woocommerce.php:111
     2198msgid "Verified! Redirecting..."
     2199msgstr ""
     2200
     2201#: naimur-email-otp-for-woocommerce.php:112
     2202msgid "An error occurred. Please try again."
     2203msgstr ""
     2204
     2205#: naimur-email-otp-for-woocommerce.php:113
     2206msgid "Resend in"
     2207msgstr ""
     2208
     2209#: naimur-email-otp-for-woocommerce.php:115
     2210msgid "Please fill in all fields."
     2211msgstr ""
     2212
     2213#: naimur-email-otp-for-woocommerce.php:117
     2214msgid "The passwords do not match."
     2215msgstr ""
     2216
     2217#: naimur-email-otp-for-woocommerce.php:120 templates/modal.php:19
     2218msgid "Verify Code"
     2219msgstr ""
     2220
     2221#: naimur-email-otp-for-woocommerce.php:121
     2222msgid "Your password has been updated. Please sign in."
     2223msgstr ""
     2224
     2225#: naimur-email-otp-for-woocommerce.php:134
     2226msgid "Settings"
     2227msgstr ""
     2228
     2229#: templates/modal.php:9
     2230msgid "Check your email"
     2231msgstr ""
     2232
     2233#: templates/modal.php:10
    362234msgid "Enter the 6-digit code sent to"
    372235msgstr ""
    382236
    39 #: naimur-email-otp-for-woocommerce.php:65
    40 #: templates/modal.php:16
    41 msgid "Resend code"
    42 msgstr ""
    43 
    44 #: naimur-email-otp-for-woocommerce.php:66
    45 #: templates/modal.php:15
    46 msgid "Verify & Create Account"
    47 msgstr ""
    48 
    49 #: naimur-email-otp-for-woocommerce.php:67
    50 msgid "Invalid or expired code. Try again."
    51 msgstr ""
    52 
    53 #: naimur-email-otp-for-woocommerce.php:78
    54 msgid "Settings"
    55 msgstr ""
    56 
    57 #: naimur-email-otp-for-woocommerce.php:79
    58 msgid "Logs"
    59 msgstr ""
    60 
    61 #: includes/class-ajax.php:19
    62 msgid "Invalid form data."
    63 msgstr ""
    64 
    65 #: includes/class-ajax.php:22
    66 msgid "Email already registered. Please log in."
    67 msgstr ""
    68 
    69 #: includes/class-ajax.php:28
    70 msgid "Security verification failed. Please try again."
    71 msgstr ""
    72 
    73 #: includes/class-ajax.php:41
    74 msgid "Failed to send email. Contact support."
    75 msgstr ""
    76 
    77 #: includes/class-ajax.php:44
    78 msgid "Code sent. Check your inbox."
    79 msgstr ""
    80 
    81 #: includes/class-ajax.php:55
    82 msgid "No pending verification. Please resend code."
    83 msgstr ""
    84 
    85 #: includes/class-ajax.php:63
    86 msgid "Too many attempts. Start over."
    87 msgstr ""
    88 
    89 #: includes/class-ajax.php:67
    90 msgid "Invalid or expired code."
    91 msgstr ""
    92 
    93 #: includes/class-security.php:35
    94 msgid "Please complete the security check."
    95 msgstr ""
    96 
    97 #: includes/class-security.php:43
    98 msgid "Security verification failed."
    99 msgstr ""
    100 
    101 #: includes/class-settings.php:44
    102 msgid "Customer Email OTP Verification for WooCommerce (v1.2.0)"
    103 msgstr ""
    104 
    105 #: includes/class-settings.php:49
    106 msgid "Brand color (HEX)"
    107 msgstr ""
    108 
    109 #: includes/class-settings.php:53
    110 msgid "Modern Login/Register UI"
    111 msgstr ""
    112 
    113 #: includes/class-settings.php:55
    114 msgid "Enable custom modern design"
    115 msgstr ""
    116 
    117 #: includes/class-settings.php:56
    118 msgid "Select My Account page"
    119 msgstr ""
    120 
    121 #: includes/class-settings.php:61
    122 msgid "Captcha Provider"
    123 msgstr ""
    124 
    125 #: includes/class-settings.php:64
    126 msgid "None"
    127 msgstr ""
    128 
    129 #: includes/class-settings.php:65
    130 msgid "Google reCAPTCHA v2"
    131 msgstr ""
    132 
    133 #: includes/class-settings.php:66
    134 msgid "Cloudflare Turnstile"
    135 msgstr ""
    136 
    137 #: includes/class-settings.php:71
    138 msgid "Google reCAPTCHA Keys"
    139 msgstr ""
    140 
    141 #: includes/class-settings.php:78
    142 msgid "Cloudflare Turnstile Keys"
    143 msgstr ""
    144 
    145 #: includes/class-settings.php:85
    146 msgid "Login header HTML"
    147 msgstr ""
    148 
    149 #: includes/class-settings.php:89
    150 msgid "Register header HTML"
    151 msgstr ""
    152 
    153 #: includes/class-settings.php:95
    154 msgid "View Logs"
    155 msgstr ""
    156 
    157 #: includes/class-users.php:10
    158 msgid "Authenticated"
    159 msgstr ""
    160 
    161 #: templates/modal.php:4
    162 msgid "Close"
    163 msgstr ""
    164 
    165 #: templates/modal.php:6
    166 msgid "Verify your email"
    167 msgstr ""
    168 
    169 #: templates/woocommerce/myaccount/form-login.php:13
    170 #: templates/woocommerce/myaccount/form-login.php:55
    171 msgid "Email address"
    172 msgstr ""
    173 
    174 #: templates/woocommerce/myaccount/form-login.php:17
    175 #: templates/woocommerce/myaccount/form-login.php:59
     2237#: templates/modal.php:12
     2238msgid "Verification code"
     2239msgstr ""
     2240
     2241#: templates/modal.php:23
     2242msgid "Didn't get it?"
     2243msgstr ""
     2244
     2245#: templates/modal.php:24
     2246msgid "Resend Code"
     2247msgstr ""
     2248
     2249#: templates/modal.php:27
     2250msgid "Tip: you can paste the whole code into the first box. Check your spam folder if it has not arrived."
     2251msgstr ""
     2252
     2253#: templates/woocommerce/myaccount/form-login.php:14
     2254msgid "Welcome Back"
     2255msgstr ""
     2256
     2257#: templates/woocommerce/myaccount/form-login.php:15
     2258msgid "Sign in to your account"
     2259msgstr ""
     2260
     2261#: templates/woocommerce/myaccount/form-login.php:20
     2262#: templates/woocommerce/myaccount/form-login.php:58
     2263msgid "Email Address"
     2264msgstr ""
     2265
     2266#: templates/woocommerce/myaccount/form-login.php:24
     2267#: templates/woocommerce/myaccount/form-login.php:63
    1762268msgid "Password"
    1772269msgstr ""
    1782270
     2271#: templates/woocommerce/myaccount/form-login.php:29
     2272msgid "Remember me"
     2273msgstr ""
     2274
    1792275#: templates/woocommerce/myaccount/form-login.php:30
    180 msgid "Remember me"
    181 msgstr ""
    182 
    183 #: templates/woocommerce/myaccount/form-login.php:31
    1842276msgid "Forgot password?"
    1852277msgstr ""
    1862278
    187 #: templates/woocommerce/myaccount/form-login.php:36
     2279#: templates/woocommerce/myaccount/form-login.php:34
     2280msgid "Sign In"
     2281msgstr ""
     2282
     2283#: templates/woocommerce/myaccount/form-login.php:40
     2284msgid "Don't have an account?"
     2285msgstr ""
     2286
     2287#: templates/woocommerce/myaccount/form-login.php:40
     2288msgid "Sign up"
     2289msgstr ""
     2290
     2291#: templates/woocommerce/myaccount/form-login.php:49
     2292msgid "Create Account"
     2293msgstr ""
     2294
     2295#: templates/woocommerce/myaccount/form-login.php:50
     2296msgid "We'll email you a 6-digit code to confirm your address."
     2297msgstr ""
     2298
     2299#: templates/woocommerce/myaccount/form-login.php:54
     2300msgid "Full Name"
     2301msgstr ""
     2302
     2303#: templates/woocommerce/myaccount/form-login.php:67
     2304msgid "Confirm Password"
     2305msgstr ""
     2306
     2307#: templates/woocommerce/myaccount/form-login.php:73
     2308msgid "Sign Up & Verify"
     2309msgstr ""
     2310
     2311#: templates/woocommerce/myaccount/form-login.php:78
     2312msgid "Already have an account?"
     2313msgstr ""
     2314
     2315#: templates/woocommerce/myaccount/form-login.php:78
    1882316msgid "Sign in"
    1892317msgstr ""
    1902318
    191 #: templates/woocommerce/myaccount/form-login.php:39
    192 msgid "Don't have an account?"
    193 msgstr ""
    194 
    195 #: templates/woocommerce/myaccount/form-login.php:39
    196 msgid "Sign up"
    197 msgstr ""
    198 
    199 #: templates/woocommerce/myaccount/form-login.php:51
    200 msgid "Full Name"
    201 msgstr ""
    202 
    203 #: templates/woocommerce/myaccount/form-login.php:63
    204 msgid "Confirm Password"
    205 msgstr ""
    206 
    207 #: templates/woocommerce/myaccount/form-login.php:77
    208 msgid "Create Account"
    209 msgstr ""
    210 
    211 #: templates/woocommerce/myaccount/form-login.php:81
    212 msgid "Back to sign in"
    213 msgstr ""
     2319#: templates/woocommerce/myaccount/form-login.php:87
     2320msgid "Reset Password"
     2321msgstr ""
     2322
     2323#: templates/woocommerce/myaccount/form-login.php:88
     2324msgid "Enter your email and we'll send you a 6-digit code."
     2325msgstr ""
     2326
     2327#: templates/woocommerce/myaccount/form-login.php:92
     2328msgid "Registered Email"
     2329msgstr ""
     2330
     2331#: templates/woocommerce/myaccount/form-login.php:97
     2332msgid "Send Verification Code"
     2333msgstr ""
     2334
     2335#: templates/woocommerce/myaccount/form-login.php:102
     2336msgid "Back to Sign in"
     2337msgstr ""
     2338
     2339#: templates/woocommerce/myaccount/form-login.php:109
     2340msgid "Set New Password"
     2341msgstr ""
     2342
     2343#: templates/woocommerce/myaccount/form-login.php:110
     2344msgid "Choose a new password for your account."
     2345msgstr ""
     2346
     2347#: templates/woocommerce/myaccount/form-login.php:115
     2348msgid "New Password"
     2349msgstr ""
     2350
     2351#: templates/woocommerce/myaccount/form-login.php:119
     2352msgid "Confirm New Password"
     2353msgstr ""
     2354
     2355#: templates/woocommerce/myaccount/form-login.php:124
     2356msgid "Update Password"
     2357msgstr ""
  • naimur-email-otp-for-woocommerce/tags/2.1.2/naimur-email-otp-for-woocommerce.php

    r3780003 r3780739  
    33 * Plugin Name: Naimur Email OTP Verification for WooCommerce
    44 * Plugin URI: https://naimurrahmannahid.com/naimur-otp-plugin/
    5  * Description: Verify WooCommerce customer emails with a 6-digit OTP for registration and password reset, with a modern, speed-optimized My Account UI.
    6  * Version: 2.1.1
     5 * Description: Verify WooCommerce customer emails with a 6-digit OTP for registration, password reset and (optionally) checkout, with editable email templates and a modern admin.
     6 * Version: 2.1.2
    77 * Requires at least: 6.0
    88 * Tested up to: 7.1
    99 * Requires PHP: 7.4
    1010 * Requires Plugins: woocommerce
    11  * WC tested up to: 10.8
     11 * WC tested up to: 11.2
    1212 * Author: Naimur Rahman Nahid
    1313 * Author URI: https://naimurrahmannahid.com/
    … …  
    1717if ( ! defined( 'ABSPATH' ) ) { exit; }
    1818
    19 define('NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION', '2.1.1');
    20 define('CEOVW_DB_VERSION', '2.1.1');
     19define('NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION', '2.1.2');
     20define('CEOVW_DB_VERSION', '2.1.2');
    2121define('NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH', plugin_dir_path(__FILE__));
    2222define('NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_URL', plugin_dir_url(__FILE__));
    2323
    2424// Load all files exactly matching your folder structure
     25require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-options.php';
     26require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-templates.php';
    2527require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/helpers.php';
    2628require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-ajax.php';
    … …  
    3133require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-settings.php';
    3234require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-users.php';
     35require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-checkout.php';
     36
     37CEOVW_Checkout::init();
     38if ( is_admin() ) {
     39    require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/admin/class-admin.php';
     40    CEOVW_Admin::init();
     41}
     42
     43// Declare compatibility with WooCommerce features this plugin was tested with
     44add_action( 'before_woocommerce_init', function() {
     45    if ( class_exists( '\\Automattic\\WooCommerce\\Utilities\\FeaturesUtil' ) ) {
     46        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
     47        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true );
     48    }
     49});
    3350
    3451// Activation Hooks for DB & Cron
    … …  
    6986// Enqueue Highly Optimized Assets
    7087add_action('wp_enqueue_scripts', function() {
    71     $override_enabled = (bool) get_option('naimur-email-otp-for-woocommerce_override_enabled', false);
    72     $my_page_id       = (int) get_option('naimur-email-otp-for-woocommerce_myaccount_page_id', 0);
     88    $override_enabled = (bool) CEOVW_Options::get('modern_ui');
     89    $my_page_id       = (int) CEOVW_Options::get('myaccount_page_id');
    7390
    7491    $should_enqueue = false;
    … …  
    8097    wp_enqueue_style('ceovw-modal-css', NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_URL.'assets/css/modal.css', [], NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION);
    8198    wp_enqueue_style('ceovw-corporate-ui', NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_URL.'assets/css/override-myaccount.css', [], NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION);
    82    
    83     // Captcha Integration
    84     $captcha_type = get_option('naimur-email-otp-for-woocommerce_captcha_type', 'none');
    85     if ( $captcha_type === 'recaptcha' && get_option('naimur-email-otp-for-woocommerce_recaptcha_site_key') ) {
    86         wp_enqueue_script('google-recaptcha','https://www.google.com/recaptcha/api.js',[],NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION,true);
    87         wp_enqueue_script('ceovw-boot-app', NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_URL.'assets/js/boot.js', ['jquery'], NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION, true);
    88     } elseif ( $captcha_type === 'turnstile' && get_option('naimur-email-otp-for-woocommerce_turnstile_site_key') ) {
    89         wp_enqueue_script('cloudflare-turnstile','https://challenges.cloudflare.com/turnstile/v0/api.js',[],NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION,true);
    90     }
     99
     100    // CAPTCHA is not loaded: earlier versions displayed it without server-side verification (see Security & Rate Limits)
    91101
    92102    // Main App Logic
    … …  
    102112            'error'      => esc_html__('An error occurred. Please try again.', 'naimur-email-otp-for-woocommerce'),
    103113            'resend_in'  => esc_html__('Resend in', 'naimur-email-otp-for-woocommerce'),
     114            // Shown with .text() in JavaScript, so plain (unescaped) strings are safe here.
     115            'required'   => __('Please fill in all fields.', 'naimur-email-otp-for-woocommerce'),
     116            'badEmail'   => __('Please enter a valid email address.', 'naimur-email-otp-for-woocommerce'),
     117            'mismatch'   => __('The passwords do not match.', 'naimur-email-otp-for-woocommerce'),
     118            'enterCode'  => __('Please enter all 6 digits.', 'naimur-email-otp-for-woocommerce'),
     119            'codeSent'   => __('A new code has been sent.', 'naimur-email-otp-for-woocommerce'),
     120            'verifyBtn'  => __('Verify Code', 'naimur-email-otp-for-woocommerce'),
     121            'updated'    => __('Your password has been updated. Please sign in.', 'naimur-email-otp-for-woocommerce'),
    104122        ],
    105123        'settings'     => [
    106             'captcha'  => sanitize_text_field($captcha_type),
    107             'site_key' => $captcha_type === 'recaptcha' ? get_option('naimur-email-otp-for-woocommerce_recaptcha_site_key') : get_option('naimur-email-otp-for-woocommerce_turnstile_site_key'),
     124            'resend' => [
     125                'register' => CEOVW_Options::resend_seconds('registration'),
     126                'forgot'   => CEOVW_Options::resend_seconds('forgot'),
     127            ],
    108128        ]
    109129    ]);
    … …  
    112132// Settings Links
    113133add_filter('plugin_action_links_'.plugin_basename(__FILE__), function($links){
    114     $links[] = '<a href="'.esc_url(admin_url('admin.php?page=naimur-email-otp-for-woocommerce-settings')).'">'.esc_html__('Settings', 'naimur-email-otp-for-woocommerce').'</a>';
     134    $links[] = '<a href="'.esc_url(admin_url('admin.php?page=ceovw-dashboard')).'">'.esc_html__('Settings', 'naimur-email-otp-for-woocommerce').'</a>';
    115135    return $links;
    116136});
  • naimur-email-otp-for-woocommerce/tags/2.1.2/readme.txt

    r3780003 r3780739  
    22Contributors: naimurrahmannahid
    33Donate link: https://buymeacoffee.com/naimurrahmannahid
    4 Tags: woocommerce, otp, email verification, customer
     4Tags: woocommerce, otp, email verification, checkout, customer
    55Requires at least: 6.0
    66Tested up to: 7.1
    77Requires PHP: 7.4
    8 Stable tag: 2.1.1
     8Stable tag: 2.1.2
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1111
    12 Verify WooCommerce customer emails with a 6-digit OTP during registration and secure password recovery.
     12Verify WooCommerce customer emails with a 6-digit code at registration, password reset and (optionally) checkout.
    1313
    1414== Description ==
    1515
    16 **Naimur Email OTP Verification for WooCommerce** confirms that a customer really owns their email address before an account is created, and uses the same one-time code (OTP) to protect password recovery.
     16**Naimur Email OTP Verification for WooCommerce** confirms that a customer really owns their email address. A one-time code (OTP) is emailed and must be entered before the action completes.
    1717
    1818The OTP is used for:
    … …  
    2020* New customer registration
    2121* Password reset (Forgot Password)
    22 
    23 The plugin does not add OTP to login or checkout.
     22* Checkout (optional, off by default): the order is placed only after the billing email is verified
     23
     24The plugin does not add OTP to login.
    2425
    2526= Key features =
    2627
    2728* 6-digit email OTP for WooCommerce customer registration
    28 * OTP-based Forgot Password flow with a single-use, short-lived reset authorization
    29 * Modern, responsive My Account sign-in, registration and password recovery forms
    30 * OTP entry with auto-advance, paste support and a resend button with countdown
    31 * Three bundled email templates plus your own custom HTML template, with a live preview in the admin
    32 * Email and activity logs in the admin (OTP codes are never shown; email addresses are masked)
    33 * Server-side protection: OTP attempt limits, code expiry and rate limits for sending, verification and password reset
    34 * Translation-ready, with developer filters such as `ceovw_otp_expiry`, `ceovw_rate_limit` and `ceovw_client_ip`
     29* OTP-based Forgot Password flow with a single-use, short-lived reset authorization (no automatic login after a reset)
     30* Optional checkout email verification for the Checkout block and the classic checkout, for guests and (optionally) logged-in customers. Verification is enforced on the server before the order is created, so no payment is taken from an unverified checkout
     31* Accessible verification popup at checkout: 6-digit input with paste support, countdown, resend with cooldown, change email, mobile bottom-sheet layout
     32* Three responsive email templates (registration, password reset, checkout) that use your store logo, name and brand colour
     33* HTML template editor with syntax highlighting, placeholders, a sandboxed live preview (desktop and mobile) and test emails
     34* "Naimur OTP" admin area: overview with setup checklist, one Verification page with Registration, Password Reset and Checkout tabs, email templates, security limits, branding, activity and diagnostics, searchable help and a settings finder
     35* Server-side protection: hashed codes, attempt limits, expiry, single use, per-IP and per-email rate limits, purpose and session binding
     36* Compatible with WooCommerce High-Performance Order Storage (HPOS) and the Cart and Checkout blocks
     37* Translation-ready, with developer filters such as `ceovw_otp_expiry`, `ceovw_checkout_auth_ttl`, `ceovw_rate_limit` and `ceovw_client_ip`
    3538
    3639= How it works =
    3740
    38 1. The customer fills in the registration form and receives a 6-digit code by email.
    39 2. The account is created only after the correct code is entered.
    40 3. For a forgotten password, the customer requests a code, verifies it, and then sets a new password. After the reset they sign in with the new password; they are not logged in automatically.
     411. Registration: the customer fills in the registration form and receives a 6-digit code by email. The account is created only after the correct code is entered.
     422. Password reset: the customer requests a code, verifies it and sets a new password, then signs in with it.
     433. Checkout (when enabled): after clicking Place order, a popup asks for the code sent to the billing email. Once verified, the order is placed automatically. Changing the email requires a new code.
    4144
    4245= Documentation =
    4346
    44 Full documentation for setup, usage and customization: https://naimurrahmannahid.com/naimur-otp-plugin/
     47Built-in documentation is available under **Naimur OTP → Help & Documentation**. More guides: https://naimurrahmannahid.com/naimur-otp-plugin/
    4548
    4649Video tutorial:
    … …  
    4851[youtube https://youtu.be/AehMWNLxkVk]
    4952
    50 == External services ==
    51 
    52 This plugin can optionally display a Google reCAPTCHA or Cloudflare Turnstile widget on the forms of its My Account UI. When enabled, the provider's script is loaded in the visitor's browser and the visitor's browser communicates directly with the provider. In version 2.1.1 the plugin's server does not send the CAPTCHA response to the provider for verification.
    53 
    54 **Google reCAPTCHA:**
    55 When reCAPTCHA is enabled in the plugin settings and a site key is configured, the visitor's browser loads the reCAPTCHA script from Google, and Google may receive:
    56 * The IP address of the user
    57 * Browser and usage information collected by the reCAPTCHA widget
    58 
    59 Service Provider:
    60 * Google reCAPTCHA – Provided by Google LLC
    61 * Terms of Service: https://www.google.com/recaptcha/terms/
    62 * Privacy Policy: https://policies.google.com/privacy/
    63 
    64 **Cloudflare Turnstile:**
    65 When Turnstile is enabled and a site key is configured, the visitor's browser loads the Turnstile script from Cloudflare. Similar to reCAPTCHA, IP and browser telemetry may be processed by Cloudflare.
    66 
    67 Service provider:
    68 * Cloudflare Turnstile – provided by Cloudflare, Inc.
    69 * Terms of Service: https://www.cloudflare.com/website-terms/
    70 * Privacy Policy: https://www.cloudflare.com/privacypolicy/
    71 
    7253== Source code ==
    7354
    … …  
    7859* `assets/src/js/main.js`
    7960
    80 This file contains the original source code that can be viewed, studied and modified by developers.
     61All other JavaScript and CSS files (`assets/js/checkout-otp.js`, `assets/admin/admin.js` and the stylesheets) are shipped unminified.
    8162
    8263== Installation ==
    8364
    84651. In your WordPress admin, go to **Plugins → Add New**, search for "Naimur Email OTP Verification for WooCommerce", then install and activate it. WooCommerce must be installed and active.
    85 2. Go to **WooCommerce → Email Verification**.
    86 3. Set the email subject and choose an email template (or write your own custom HTML using the `[naimur_email_otp_code]` placeholder). The live preview shows the result.
    87 4. To use the modern forms, tick **Enable custom modern design** and select your WooCommerce My Account page, then save.
     662. Open **Naimur OTP → Verification**. In Step 1, turn on the customer login and registration forms and select your My Account page.
     673. Open **Naimur OTP → General Settings** and set your sender name, sender email and logo.
     684. Review the emails under **Naimur OTP → Email Templates** and send yourself a test email.
     695. Optional: turn on **Naimur OTP → Verification → Checkout OTP** after placing a test order yourself.
    88705. Make sure your site can send email. Using an SMTP plugin is recommended for reliable delivery.
    8971
    … …  
    9173
    9274= Where is the OTP used? =
    93 For new customer registration and for password reset (Forgot Password). The plugin does not add OTP to login or checkout.
    94 
    95 = Can I customize the verification email? =
    96 Yes. You can set the email subject, choose one of three bundled templates, or write your own HTML template. Use the `[naimur_email_otp_code]` placeholder where the code should appear. The settings page shows a live preview.
     75For new customer registration, for password reset and, if you enable it, at checkout. The plugin does not add OTP to login.
     76
     77= Does checkout verification work with the Checkout block? =
     78Yes. It supports the Checkout block (Store API) and the classic `[woocommerce_checkout]` shortcode. The check runs on the server when the order is placed, so it cannot be skipped by disabling JavaScript or calling the API directly.
     79
     80= Can a customer be charged before verifying? =
     81No. The order is not created until the billing email is verified, so payment is never requested for an unverified checkout. If a payment fails, the customer can retry payment for the same order and the same email without a new code.
     82
     83= What about express payment buttons (Apple Pay, Google Pay)? =
     84Express buttons outside the checkout form skip the verification popup. While checkout verification is required, such orders are refused with the message "Please verify your email address to place your order." and the customer can complete the purchase through the checkout form. Turn checkout verification off if you rely on express-only payments.
     85
     86= Can I customize the verification emails? =
     87Yes. Each email has its own subject, preheader and HTML. Use placeholders such as `{{otp_code}}`, `{{customer_name}}`, `{{site_name}}`, `{{expiry_minutes}}` and `{{support_email}}`. Scripts, forms, iframes and unsafe links are removed when you save, and PHP is never executed.
    9788
    9889= How long is a code valid? =
    99 Codes expire after 5 minutes and allow up to 5 verification attempts. Developers can change the expiry with the `ceovw_otp_expiry` filter.
     905 minutes by default, with 5 attempts. You can set expiry (1–30 minutes), resend delay and attempts separately for registration, password reset and checkout.
    10091
    10192= Is the customer logged in after resetting their password? =
    102 No. Since version 2.1.1 the customer is returned to the sign-in form and signs in with the new password.
     93No. The customer returns to the sign-in form and signs in with the new password.
    10394
    10495= Does this plugin work with SMTP plugins? =
    105 Yes. Emails are sent with WordPress's `wp_mail()`, so SMTP plugins that hook into `wp_mail()` (for example WP Mail SMTP, FluentSMTP or Post SMTP) handle delivery.
    106 
    107 = Do reCAPTCHA and Cloudflare Turnstile protect the forms? =
    108 Not in version 2.1.1. The widgets can be displayed on the plugin's forms, but the CAPTCHA response is not verified on the server, so they do not currently block automated requests. OTP requests, OTP verification and password resets are protected by server-side rate limits instead. Server-side CAPTCHA verification is planned for a future release.
     96Yes. Emails are sent with WordPress's `wp_mail()`, so SMTP plugins that hook into `wp_mail()` (for example WP Mail SMTP, FluentSMTP or Post SMTP) handle delivery. The sender name and address you set apply only to this plugin's emails.
     97
     98= Is there CAPTCHA? =
     99No. Earlier versions could display reCAPTCHA or Turnstile without verifying it on the server, which gave no real protection, so it was removed in 2.1.2. The forms are protected by server-side rate limits and attempt limits.
    109100
    110101= Does it slow down the site? =
    111 The plugin's styles and scripts load only on the WooCommerce My Account page for visitors who are not logged in.
     102No noticeable impact. The My Account scripts load only on the My Account page for visitors who are not logged in, the checkout script loads only on the checkout page when checkout verification is enabled, and admin assets load only on the plugin's own admin pages.
    112103
    113104= My site is behind a proxy or CDN. Which IP address is used? =
    114105For rate limiting and activity logs the plugin uses `REMOTE_ADDR` and does not trust forwarded headers. If your server sits behind a trusted proxy, return the real client IP through the `ceovw_client_ip` filter.
    115106
     107= What happens to my data if I delete the plugin? =
     108Pending codes, reset authorizations and logs are always removed. Settings and email templates are kept unless you turn on "Delete all plugin data" under General Settings.
     109
    116110== Screenshots ==
    117111
    118 1. Customer registration form in the modern My Account design.
    119 2. Email OTP verification during customer registration.
    120 3. Forgot Password: request a verification code by email.
    121 4. Set a new password after the code is verified.
    122 5. After a password reset the customer returns to the sign-in form (no automatic login).
    123 6. Email subject and template selection with live preview.
    124 7. Custom HTML email template with live preview.
    125 8. Email and activity logs with masked email addresses and hidden OTP codes.
     1121. Overview with the status of each verification, email delivery and security health, a setup checklist and quick actions.
     1132. Checkout verification popup on the Checkout block.
     1143. Checkout verification popup on a phone.
     1154. Email template editor with syntax highlighting and live preview.
     1165. Checkout OTP tab: settings, how checkout verification works and compatibility.
     1176. Verification page with Registration, Password Reset and Checkout tabs.
     1187. Security settings: rate limits and always-on protections.
     1198. Checkout verification email.
     1209. Customer registration form on the My Account page.
     12110. Email OTP verification during customer registration.
     12211. Set a new password after the code is verified.
     12312. Activity & Diagnostics: test email, email log with masked addresses and system information.
    126124
    127125== Changelog ==
     126
     127= 2.1.2 =
     128Added:
     129* Optional checkout email verification (off by default) for the Checkout block and classic checkout, for guests and logged-in customers. Enforced on the server before the order is created; verification is single-use and bound to the browser session, the exact email and the checkout purpose.
     130* Accessible checkout verification popup with paste support, countdown, resend cooldown, change email and a mobile layout.
     131* The registration and password reset code popup now matches the checkout popup: dialog semantics, Escape to close, focus kept inside, paste and one-time-code autofill, arrow and Backspace keys, and inline messages instead of browser alerts.
     132* New "Naimur OTP" admin menu with Overview, Verification (Registration, Password Reset and Checkout tabs), Email Templates, Security, Activity & Diagnostics, General Settings and Help & Documentation.
     133* Overview page with real status for each verification, email delivery and security health, a setup checklist and quick actions.
     134* Plain-language settings with help text, unsaved-changes warning, Discard changes, inline validation, a settings finder and contextual links to the documentation.
     135* Three new responsive email templates (registration, password reset, checkout) using your store logo and brand colour.
     136* HTML email template editor with syntax highlighting, placeholders, sandboxed live preview (desktop and mobile), reset and test email.
     137* Per-flow settings for code expiry, resend delay and attempts; adjustable rate limits within safe ranges.
     138* Sender name and email for this plugin's emails only, store logo, uninstall data retention option and activity log retention.
     139* Server-side resend cooldown for all flows.
     140* Declared compatibility with WooCommerce HPOS and the Cart and Checkout blocks.
     141Changed:
     142* Settings moved from WooCommerce → Email Verification to the new Naimur OTP menu. Existing settings are kept; old admin links redirect.
     143* Email HTML, security limits and general settings now require a site administrator (manage_options). Shop managers can still turn verification on or off and send test emails.
     144* A custom HTML template and custom subject from earlier versions are migrated to the registration and password reset templates. The bundled templates 1–3 are replaced by the new design.
     145* Password reset button now reads "Update Password" (the customer is not logged in automatically).
     146* When password reset or registration OTP is turned off, the standard WooCommerce links are shown instead.
     147Removed:
     148* reCAPTCHA and Cloudflare Turnstile widgets. They were never verified on the server. Saved keys are kept but unused, and no third-party scripts are loaded.
     149Security:
     150* All 2.1.1 protections retained: hashed codes, single-use reset authorization, atomic attempt counting, expiry, rate limits, cron cleanup, no automatic login after reset.
     151* Email HTML is filtered with a strict allowlist; placeholder values are escaped.
     152* Verification fails closed while a database upgrade is pending.
    128153
    129154= 2.1.1 =
    … …  
    170195== Upgrade Notice ==
    171196
     197= 2.1.2 =
     198Adds optional checkout email verification (off by default), a new Naimur OTP admin menu and editable responsive email templates. Settings are migrated automatically. reCAPTCHA/Turnstile display was removed. Requires WooCommerce.
     199
    172200= 2.1.1 =
    173201Security release. Hardens password reset and OTP verification. Recommended for all users. After a password reset, customers now sign in with their new password. Pending password reset sessions started before the update must be restarted.
  • naimur-email-otp-for-woocommerce/tags/2.1.2/templates/modal.php

    r3587442 r3780739  
    11<?php defined('ABSPATH') || exit; ?>
    22
    3 <!-- OTP MODAL OVERLAY -->
     3<!-- OTP MODAL OVERLAY (same visual language as the checkout popup) -->
    44<div id="ceovw-otp-modal" class="ceovw-modal-overlay" style="display:none;">
    5     <div class="ceovw-modal-content">
    6         <button class="ceovw-modal-close" aria-label="Close">×</button>
     5    <div class="ceovw-modal-content" role="dialog" aria-modal="true" aria-labelledby="ceovw-otp-title" aria-describedby="ceovw-otp-desc">
     6        <button type="button" class="ceovw-modal-close" aria-label="<?php esc_attr_e('Close', 'naimur-email-otp-for-woocommerce'); ?>"><span aria-hidden="true">&times;</span></button>
     7        <div class="ceovw-modal-icon" aria-hidden="true"><svg viewBox="0 0 48 48" width="34" height="34" fill="none"><rect x="6" y="11" width="36" height="26" rx="5" stroke="currentColor" stroke-width="2.5"/><path d="M8 15l16 11 16-11" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
    78        <div class="ceovw-modal-header">
    8             <h3><?php esc_html_e('Verification Required', 'naimur-email-otp-for-woocommerce'); ?></h3>
    9             <p><?php esc_html_e('Enter the 6-digit code sent to', 'naimur-email-otp-for-woocommerce'); ?> <strong id="ceovw-target-email"></strong></p>
     9            <h3 id="ceovw-otp-title"><?php esc_html_e('Check your email', 'naimur-email-otp-for-woocommerce'); ?></h3>
     10            <p id="ceovw-otp-desc"><?php esc_html_e('Enter the 6-digit code sent to', 'naimur-email-otp-for-woocommerce'); ?> <strong id="ceovw-target-email"></strong></p>
    1011        </div>
    11         <div class="ceovw-otp-inputs">
    12             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required autofocus>
    13             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
    14             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
    15             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
    16             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
    17             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
     12        <div class="ceovw-otp-inputs" role="group" aria-label="<?php esc_attr_e('Verification code', 'naimur-email-otp-for-woocommerce'); ?>">
     13            <?php for ( $ceovw_i = 1; $ceovw_i <= 6; $ceovw_i++ ) : ?>
     14                <input type="text" inputmode="numeric" pattern="[0-9]*" class="ceovw-otp-box" autocomplete="<?php echo 1 === $ceovw_i ? 'one-time-code' : 'off'; ?>" aria-label="<?php echo esc_attr( sprintf( /* translators: %d: digit position */ __( 'Digit %d of 6', 'naimur-email-otp-for-woocommerce' ), $ceovw_i ) ); ?>">
     15            <?php endfor; ?>
    1816        </div>
    19         <div id="ceovw-otp-error" class="ceovw-error-msg"></div>
    20         <button class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-verify">
     17        <div id="ceovw-otp-error" class="ceovw-error-msg" role="alert" aria-live="assertive"></div>
     18        <button type="button" class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-verify">
    2119            <span class="btn-text"><?php esc_html_e('Verify Code', 'naimur-email-otp-for-woocommerce'); ?></span>
    22             <span class="btn-loader"></span>
     20            <span class="btn-loader" aria-hidden="true"></span>
    2321        </button>
    2422        <div class="ceovw-resend-wrapper">
    25             <button id="ceovw-btn-resend" class="ceovw-link" disabled><?php esc_html_e('Resend Code', 'naimur-email-otp-for-woocommerce'); ?></button>
    26             <span id="ceovw-timer-text">(60s)</span>
     23            <span class="ceovw-resend-q"><?php esc_html_e("Didn't get it?", 'naimur-email-otp-for-woocommerce'); ?></span>
     24            <button type="button" id="ceovw-btn-resend" class="ceovw-link" disabled><?php esc_html_e('Resend Code', 'naimur-email-otp-for-woocommerce'); ?></button>
     25            <span id="ceovw-timer-text"></span>
    2726        </div>
     27        <p class="ceovw-modal-tip"><?php esc_html_e('Tip: you can paste the whole code into the first box. Check your spam folder if it has not arrived.', 'naimur-email-otp-for-woocommerce'); ?></p>
    2828    </div>
    2929</div>
  • naimur-email-otp-for-woocommerce/tags/2.1.2/templates/woocommerce/myaccount/form-login.php

    r3587442 r3780739  
    1 <?php 
    2 defined('ABSPATH') || exit; 
     1<?php
     2defined('ABSPATH') || exit;
    33
    4 // ক্যাপচা টাইপ এবং সাইট কি (Site Key) ফেচ করা
    5 $captcha_type = get_option('naimur-email-otp-for-woocommerce_captcha_type', 'none');
    6 $captcha_html = '';
    7 
    8 if ($captcha_type === 'recaptcha') {
    9     $site_key = get_option('naimur-email-otp-for-woocommerce_recaptcha_site_key');
    10     if (!empty($site_key)) {
    11         $captcha_html = '<div class="g-recaptcha" data-sitekey="' . esc_attr($site_key) . '" style="margin-bottom: 15px; display: flex; justify-content: center;"></div>';
    12     }
    13 } elseif ($captcha_type === 'turnstile') {
    14     $site_key = get_option('naimur-email-otp-for-woocommerce_turnstile_site_key');
    15     if (!empty($site_key)) {
    16         $captcha_html = '<div class="cf-turnstile" data-sitekey="' . esc_attr($site_key) . '" style="margin-bottom: 15px; display: flex; justify-content: center;"></div>';
    17     }
    18 }
     4$ceovw_reg_on    = (bool) CEOVW_Options::get('registration_enabled');
     5$ceovw_forgot_on = (bool) CEOVW_Options::get('forgot_enabled');
    196?>
    207
    21 <div id="ceovw-corporate-app" style="--ceovw-brand: <?php echo esc_attr(get_option('naimur-email-otp-for-woocommerce_brand_color', '#2563eb')); ?>;">
     8<div id="ceovw-corporate-app" style="--ceovw-brand: <?php echo esc_attr(CEOVW_Options::get('brand_color')); ?>;">
    229  <div class="ceovw-app-card">
    23      
     10
    2411      <!-- 1. LOGIN VIEW -->
    2512      <div id="ceovw-view-login" class="ceovw-view-pane active">
    2613        <div class="ceovw-header">
    2714            <h2><?php esc_html_e('Welcome Back', 'naimur-email-otp-for-woocommerce'); ?></h2>
    28             <p><?php esc_html_e('Access your dashboard', 'naimur-email-otp-for-woocommerce'); ?></p>
     15            <p><?php esc_html_e('Sign in to your account', 'naimur-email-otp-for-woocommerce'); ?></p>
    2916        </div>
    3017        <form id="ceovw-form-login" method="post">
    3118          <?php wp_nonce_field( 'woocommerce-login', 'woocommerce-login-nonce' ); ?>
    3219          <div class="ceovw-field-group">
    33             <label><?php esc_html_e('Email Address', 'naimur-email-otp-for-woocommerce'); ?></label>
    34             <input type="email" name="username" required autocomplete="username" />
     20            <label for="ceovw-login-email"><?php esc_html_e('Email Address', 'naimur-email-otp-for-woocommerce'); ?></label>
     21            <input type="email" id="ceovw-login-email" name="username" required autocomplete="username" />
    3522          </div>
    3623          <div class="ceovw-field-group">
    37             <label><?php esc_html_e('Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    38             <input type="password" name="password" required autocomplete="current-password" />
     24            <label for="ceovw-login-pass"><?php esc_html_e('Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     25            <input type="password" id="ceovw-login-pass" name="password" required autocomplete="current-password" />
    3926          </div>
    40          
    41           <!-- ডাইনামিক ক্যাপচা রেন্ডার -->
    42           <?php echo $captcha_html; ?>
    4327
    4428          <div class="ceovw-actions-row">
    4529              <label class="ceovw-checkbox"><input type="checkbox" name="rememberme" value="forever" /> <?php esc_html_e('Remember me', 'naimur-email-otp-for-woocommerce'); ?></label>
    46               <a href="#" data-target="forgot" class="ceovw-link ceovw-switch-view"><?php esc_html_e('Forgot password?', 'naimur-email-otp-for-woocommerce'); ?></a>
     30              <?php if ($ceovw_forgot_on) : ?><a href="#" data-target="forgot" class="ceovw-link ceovw-switch-view"><?php esc_html_e('Forgot password?', 'naimur-email-otp-for-woocommerce'); ?></a><?php else : ?><a href="<?php echo esc_url(wc_lostpassword_url()); ?>" class="ceovw-link"><?php esc_html_e('Forgot password?', 'naimur-email-otp-for-woocommerce'); ?></a><?php endif; ?>
    4731          </div>
     32          <div class="ceovw-form-msg" role="status" aria-live="polite" hidden></div>
    4833          <button type="submit" name="login" class="ceovw-btn ceovw-btn-primary">
    4934              <span class="btn-text"><?php esc_html_e('Sign In', 'naimur-email-otp-for-woocommerce'); ?></span>
    50               <span class="btn-loader"></span>
     35              <span class="btn-loader" aria-hidden="true"></span>
    5136          </button>
    5237        </form>
     38        <?php if ($ceovw_reg_on) : ?>
    5339        <div class="ceovw-footer-nav">
    5440            <p><?php esc_html_e("Don't have an account?", 'naimur-email-otp-for-woocommerce'); ?> <a href="#" data-target="register" class="ceovw-link ceovw-switch-view"><?php esc_html_e('Sign up', 'naimur-email-otp-for-woocommerce'); ?></a></p>
    5541        </div>
     42        <?php endif; ?>
    5643      </div>
    5744
     45      <?php if ($ceovw_reg_on) : ?>
    5846      <!-- 2. REGISTER VIEW -->
    5947      <div id="ceovw-view-register" class="ceovw-view-pane" style="display:none;">
    6048        <div class="ceovw-header">
    6149            <h2><?php esc_html_e('Create Account', 'naimur-email-otp-for-woocommerce'); ?></h2>
    62             <p><?php esc_html_e('Join our platform today', 'naimur-email-otp-for-woocommerce'); ?></p>
     50            <p><?php esc_html_e("We'll email you a 6-digit code to confirm your address.", 'naimur-email-otp-for-woocommerce'); ?></p>
    6351        </div>
    64         <form id="ceovw-form-register" method="post">
     52        <form id="ceovw-form-register" method="post" novalidate>
    6553          <div class="ceovw-field-group">
    66             <label><?php esc_html_e('Full Name', 'naimur-email-otp-for-woocommerce'); ?></label>
    67             <input type="text" name="ceovw_full_name" required />
     54            <label for="ceovw-reg-name"><?php esc_html_e('Full Name', 'naimur-email-otp-for-woocommerce'); ?></label>
     55            <input type="text" id="ceovw-reg-name" name="ceovw_full_name" required autocomplete="name" />
    6856          </div>
    6957          <div class="ceovw-field-group">
    70             <label><?php esc_html_e('Email Address', 'naimur-email-otp-for-woocommerce'); ?></label>
    71             <input type="email" name="email" required />
     58            <label for="ceovw-reg-email"><?php esc_html_e('Email Address', 'naimur-email-otp-for-woocommerce'); ?></label>
     59            <input type="email" id="ceovw-reg-email" name="email" required autocomplete="email" />
    7260          </div>
    7361          <div class="ceovw-field-row">
    7462              <div class="ceovw-field-group">
    75                 <label><?php esc_html_e('Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    76                 <input type="password" name="password" required />
     63                <label for="ceovw-reg-pass"><?php esc_html_e('Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     64                <input type="password" id="ceovw-reg-pass" name="password" required autocomplete="new-password" />
    7765              </div>
    7866              <div class="ceovw-field-group">
    79                 <label><?php esc_html_e('Confirm Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    80                 <input type="password" name="password_confirm" required />
     67                <label for="ceovw-reg-pass2"><?php esc_html_e('Confirm Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     68                <input type="password" id="ceovw-reg-pass2" name="password_confirm" required autocomplete="new-password" />
    8169              </div>
    8270          </div>
    83 
    84           <!-- ডাইনামিক ক্যাপচা রেন্ডার -->
    85           <?php echo $captcha_html; ?>
    86 
     71          <div class="ceovw-form-msg" role="alert" aria-live="assertive" hidden></div>
    8772          <button type="submit" class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-reg">
    8873              <span class="btn-text"><?php esc_html_e('Sign Up & Verify', 'naimur-email-otp-for-woocommerce'); ?></span>
    89               <span class="btn-loader"></span>
     74              <span class="btn-loader" aria-hidden="true"></span>
    9075          </button>
    9176        </form>
    … …  
    9479        </div>
    9580      </div>
     81      <?php endif; ?>
    9682
     83      <?php if ($ceovw_forgot_on) : ?>
    9784      <!-- 3. FORGOT PASSWORD VIEW -->
    9885      <div id="ceovw-view-forgot" class="ceovw-view-pane" style="display:none;">
    9986        <div class="ceovw-header">
    10087            <h2><?php esc_html_e('Reset Password', 'naimur-email-otp-for-woocommerce'); ?></h2>
    101             <p><?php esc_html_e('Enter your email to receive a secure OTP', 'naimur-email-otp-for-woocommerce'); ?></p>
     88            <p><?php esc_html_e("Enter your email and we'll send you a 6-digit code.", 'naimur-email-otp-for-woocommerce'); ?></p>
    10289        </div>
    103         <form id="ceovw-form-forgot" method="post">
     90        <form id="ceovw-form-forgot" method="post" novalidate>
    10491          <div class="ceovw-field-group">
    105             <label><?php esc_html_e('Registered Email', 'naimur-email-otp-for-woocommerce'); ?></label>
    106             <input type="email" name="reset_email" required />
     92            <label for="ceovw-forgot-email"><?php esc_html_e('Registered Email', 'naimur-email-otp-for-woocommerce'); ?></label>
     93            <input type="email" id="ceovw-forgot-email" name="reset_email" required autocomplete="email" />
    10794          </div>
    108 
    109           <!-- ডাইনামিক ক্যাপচা রেন্ডার -->
    110           <?php echo $captcha_html; ?>
    111 
     95          <div class="ceovw-form-msg" role="alert" aria-live="assertive" hidden></div>
    11296          <button type="submit" class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-forgot">
    11397              <span class="btn-text"><?php esc_html_e('Send Verification Code', 'naimur-email-otp-for-woocommerce'); ?></span>
    114               <span class="btn-loader"></span>
     98              <span class="btn-loader" aria-hidden="true"></span>
    11599          </button>
    116100        </form>
    … …  
    124108        <div class="ceovw-header">
    125109            <h2><?php esc_html_e('Set New Password', 'naimur-email-otp-for-woocommerce'); ?></h2>
    126             <p><?php esc_html_e('Secure your account with a new password', 'naimur-email-otp-for-woocommerce'); ?></p>
     110            <p><?php esc_html_e('Choose a new password for your account.', 'naimur-email-otp-for-woocommerce'); ?></p>
    127111        </div>
    128         <form id="ceovw-form-newpass" method="post">
     112        <form id="ceovw-form-newpass" method="post" novalidate>
    129113          <input type="hidden" name="verified_email" id="ceovw-verified-email" value="">
    130114          <div class="ceovw-field-group">
    131             <label><?php esc_html_e('New Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    132             <input type="password" name="new_password" required />
     115            <label for="ceovw-new-pass"><?php esc_html_e('New Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     116            <input type="password" id="ceovw-new-pass" name="new_password" required autocomplete="new-password" />
    133117          </div>
    134118          <div class="ceovw-field-group">
    135             <label><?php esc_html_e('Confirm New Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    136             <input type="password" name="new_password_confirm" required />
     119            <label for="ceovw-new-pass2"><?php esc_html_e('Confirm New Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     120            <input type="password" id="ceovw-new-pass2" name="new_password_confirm" required autocomplete="new-password" />
    137121          </div>
     122          <div class="ceovw-form-msg" role="alert" aria-live="assertive" hidden></div>
    138123          <button type="submit" class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-setpass">
    139               <span class="btn-text"><?php esc_html_e('Update & Login', 'naimur-email-otp-for-woocommerce'); ?></span>
    140               <span class="btn-loader"></span>
     124              <span class="btn-text"><?php esc_html_e('Update Password', 'naimur-email-otp-for-woocommerce'); ?></span>
     125              <span class="btn-loader" aria-hidden="true"></span>
    141126          </button>
    142127        </form>
    143128      </div>
     129      <?php endif; ?>
    144130
    145131  </div>
  • naimur-email-otp-for-woocommerce/tags/2.1.2/uninstall.php

    r3780003 r3780739  
    1616delete_option('ceovw_db_version');
    1717delete_option('ceovw_upgrade_lock');
     18delete_option('ceovw_last_cleanup');
     19
     20// Settings and email templates are kept unless the store owner chose to delete them (General Settings).
     21$ceovw_settings = get_option('ceovw_settings', array());
     22if (is_array($ceovw_settings) && !empty($ceovw_settings['delete_data_on_uninstall'])) {
     23    foreach (array('ceovw_settings', 'ceovw_email_templates', 'ceovw_test_email_sent_at') as $ceovw_option) {
     24        delete_option($ceovw_option);
     25    }
     26    $ceovw_legacy = $wpdb->get_col($wpdb->prepare("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like('naimur-email-otp-for-woocommerce_') . '%'));
     27    foreach ($ceovw_legacy as $ceovw_option) {
     28        delete_option($ceovw_option);
     29    }
     30}
  • naimur-email-otp-for-woocommerce/trunk/assets/css/override-myaccount.css

    r3587442 r3780739  
    8888#ceovw-corporate-app .ceovw-footer-nav { text-align: center; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid #e5e7eb; font-size: 0.875rem; color: #6b7280; }
    8989
    90 /* OTP Modal */
    91 #ceovw-corporate-app .ceovw-modal-overlay {
    92     position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    93     background: rgba(17, 24, 39, 0.7); backdrop-filter: blur(4px);
    94     display: flex; justify-content: center; align-items: center; z-index: 99999;
    95 }
    96 #ceovw-corporate-app .ceovw-modal-content {
    97     background: #fff; padding: 2.5rem; border-radius: 1rem; width: 100%; max-width: 24rem;
    98     position: relative; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); text-align: center;
    99     animation: ceovw-slide-up 0.3s ease-out;
    100 }
    101 @keyframes ceovw-slide-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
    102 #ceovw-corporate-app .ceovw-modal-close {
    103     position: absolute; top: 1rem; right: 1rem; background: none; border: none; font-size: 1.5rem;
    104     cursor: pointer; color: #9ca3af; transition: color 0.2s;
    105 }
    106 #ceovw-corporate-app .ceovw-modal-close:hover { color: #111827; }
    107 #ceovw-corporate-app .ceovw-otp-inputs { display: flex; justify-content: center; gap: 0.5rem; margin: 2rem 0; }
    108 #ceovw-corporate-app .ceovw-otp-box {
    109     width: 3rem !important; height: 3.5rem !important; text-align: center; font-size: 1.5rem !important;
    110     font-weight: 700; border: 2px solid #e5e7eb !important; border-radius: 0.5rem !important; padding: 0 !important;
    111 }
    112 #ceovw-corporate-app .ceovw-otp-box:focus { border-color: var(--ceovw-brand) !important; }
    113 #ceovw-corporate-app .ceovw-error-msg { color: #dc2626; font-size: 0.875rem; margin-bottom: 1rem; height: 1.25rem; }
    114 #ceovw-corporate-app .ceovw-resend-wrapper { margin-top: 1.5rem; font-size: 0.875rem; color: #6b7280; }
    115 #ceovw-corporate-app #ceovw-btn-resend:disabled { color: #9ca3af; cursor: not-allowed; text-decoration: none; }
     90/* Inline form messages (replace browser alerts) */
     91#ceovw-corporate-app .ceovw-form-msg { margin: 0 0 1rem; padding: 0.7rem 0.9rem; border-radius: 0.6rem; font-size: 0.875rem; line-height: 1.45; text-align: left; }
     92#ceovw-corporate-app .ceovw-form-msg[hidden] { display: none; }
     93#ceovw-corporate-app .ceovw-form-msg.is-error { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
     94#ceovw-corporate-app .ceovw-form-msg.is-success { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; }
     95#ceovw-corporate-app input:focus-visible, #ceovw-corporate-app button:focus-visible, #ceovw-corporate-app a:focus-visible { outline: 2px solid var(--ceovw-brand, #2563eb); outline-offset: 2px; }
     96#ceovw-corporate-app .ceovw-btn:disabled { cursor: default; opacity: .85; }
     97
     98/* OTP Modal: same visual language as the checkout popup */
     99#ceovw-corporate-app .ceovw-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; padding: 16px; background: rgba(15, 23, 42, 0.55); display: flex; justify-content: center; align-items: center; z-index: 99999; box-sizing: border-box; }
     100#ceovw-corporate-app .ceovw-modal-content { background: #fff; padding: 2rem 1.75rem 1.25rem; border-radius: 18px; width: 100%; max-width: 420px; max-height: calc(100vh - 32px); overflow-y: auto; position: relative; box-shadow: 0 24px 60px rgba(15, 23, 42, 0.28); text-align: center; animation: ceovw-slide-up 0.2s ease-out; }
     101@keyframes ceovw-slide-up { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }
     102#ceovw-corporate-app .ceovw-modal-close { position: absolute; top: 10px; right: 12px; width: 36px; height: 36px; padding: 0; border: none; border-radius: 10px; background: transparent; font-size: 26px; line-height: 1; cursor: pointer; color: #6b7280; }
     103#ceovw-corporate-app .ceovw-modal-close:hover { background: #f3f4f6; color: #111827; }
     104#ceovw-corporate-app .ceovw-modal-icon { display: flex; align-items: center; justify-content: center; width: 64px; height: 64px; margin: 0 auto 14px; border-radius: 50%; color: var(--ceovw-brand, #2563eb); background: #eef2ff; background: color-mix(in srgb, var(--ceovw-brand, #2563eb) 12%, #fff); }
     105#ceovw-corporate-app .ceovw-modal-header h3 { margin: 0 0 8px; font-size: 21px; font-weight: 700; line-height: 1.3; color: #111827; }
     106#ceovw-corporate-app .ceovw-modal-header p { margin: 0; font-size: 14.5px; line-height: 1.55; color: #4b5563; overflow-wrap: anywhere; }
     107#ceovw-corporate-app .ceovw-otp-inputs { display: flex; justify-content: center; gap: 8px; margin: 1.25rem 0 0.25rem; }
     108#ceovw-corporate-app .ceovw-otp-box { width: 46px !important; height: 56px !important; margin: 0 !important; padding: 0 !important; text-align: center; font-size: 24px !important; font-weight: 700; color: #111827; background: #f9fafb !important; border: 1.5px solid #d1d5db !important; border-radius: 12px !important; box-shadow: none; }
     109#ceovw-corporate-app .ceovw-otp-box:focus { border-color: var(--ceovw-brand, #2563eb) !important; background: #fff !important; box-shadow: 0 0 0 3px rgba(37, 99, 235, .15); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ceovw-brand, #2563eb) 15%, transparent); outline: none; }
     110#ceovw-corporate-app .ceovw-error-msg { min-height: 1.4rem; margin: 10px 0; color: #b91c1c; font-size: 13.5px; line-height: 1.4; }
     111#ceovw-corporate-app .ceovw-error-msg.is-success { color: #047857; font-weight: 600; }
     112#ceovw-corporate-app #ceovw-btn-verify { height: 48px; border-radius: 12px; }
     113#ceovw-corporate-app .ceovw-resend-wrapper { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 6px; margin-top: 1rem; font-size: 0.875rem; color: #6b7280; }
     114#ceovw-corporate-app #ceovw-btn-resend { padding: 4px 2px; border: 0; background: none; font: inherit; font-weight: 600; color: var(--ceovw-brand, #2563eb); cursor: pointer; }
     115#ceovw-corporate-app #ceovw-btn-resend:disabled { color: #9ca3af; cursor: not-allowed; text-decoration: none; font-weight: 500; }
     116#ceovw-corporate-app .ceovw-modal-tip { margin: 12px 0 0; padding-top: 12px; border-top: 1px solid #f1f5f9; font-size: 12.5px; line-height: 1.5; color: #6b7280; }
    116117
    117118/* Responsive */
    … …  
    119120    #ceovw-corporate-app .ceovw-app-card { padding: 2rem 1.5rem; }
    120121    #ceovw-corporate-app .ceovw-field-row { flex-direction: column; gap: 0; }
    121     #ceovw-corporate-app .ceovw-otp-box { width: 2.5rem !important; height: 3rem !important; font-size: 1.25rem !important; }
     122    #ceovw-corporate-app .ceovw-modal-overlay { align-items: flex-end; padding: 0; }
     123    #ceovw-corporate-app .ceovw-modal-content { max-width: none; border-radius: 18px 18px 0 0; padding: 1.75rem 1.1rem 1.1rem; }
     124    #ceovw-corporate-app .ceovw-otp-inputs { gap: 6px; }
     125    #ceovw-corporate-app .ceovw-otp-box { width: calc((100% - 30px) / 6) !important; max-width: 48px; height: 52px !important; font-size: 22px !important; }
    122126}
     127@media (prefers-reduced-motion: reduce) {
     128    #ceovw-corporate-app .ceovw-modal-content, #ceovw-corporate-app .btn-loader { animation: none; }
     129}
  • naimur-email-otp-for-woocommerce/trunk/assets/js/main.js

    r3780003 r3780739  
    22    "use strict";
    33
    4     let currentFlow = '';
     4    const T = (window.CEOVW_APP && CEOVW_APP.i18n) || {};
     5    let currentFlow = '';
    56    let targetEmail = '';
    67    let resendTimer = null;
    78    let resetToken = ''; // Password reset authorization: kept in memory only, never persisted or logged
     9    let lastFocus = null;
     10    let verifying = false;
     11
     12    // Inline form messages (replace browser alerts; announced by screen readers)
     13    const formMsg = ($form, text, kind) => {
     14        const $m = $form.find('.ceovw-form-msg');
     15        if (!text) { $m.prop('hidden', true).text('').removeClass('is-error is-success'); return; }
     16        $m.text(text).prop('hidden', false).toggleClass('is-success', kind === 'success').toggleClass('is-error', kind !== 'success');
     17    };
     18    const showView = (target, focus) => {
     19        $('.ceovw-view-pane').hide();
     20        const $v = $(`#ceovw-view-${target}`).fadeIn(300);
     21        $v.find('.ceovw-form-msg').prop('hidden', true).text('');
     22        if (focus !== false) { setTimeout(() => $v.find('input:not([type=hidden]):visible').first().trigger('focus'), 320); }
     23        return $v;
     24    };
     25    const missing = ($form) => $form.find('input[required]').filter(function(){ return !$.trim(this.value); }).first();
     26    const validEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test($.trim(v));
    827
    928    // View Switcher (Smooth Fade)
    … …  
    1231        const target = $(this).data('target');
    1332        if (target !== 'newpass') resetToken = '';
    14         $('.ceovw-view-pane').hide();
    15         $(`#ceovw-view-${target}`).fadeIn(300);
     33        showView(target);
    1634    });
    1735
    1836    const setBtnLoading = ($btn, isLoading) => {
    1937        if(isLoading) {
    20             $btn.prop('disabled', true).addClass('loading');
     38            $btn.prop('disabled', true).addClass('loading').attr('aria-busy', 'true');
    2139        } else {
    22             $btn.prop('disabled', false).removeClass('loading');
     40            $btn.prop('disabled', false).removeClass('loading').attr('aria-busy', 'false');
    2341        }
    2442    };
    2543
    26     const getCaptchaToken = ($form) => {
    27         if (CEOVW_APP.settings.captcha === 'turnstile') {
    28             return $form.find('[name="cf-turnstile-response"]').val() || '';
    29         } else if (CEOVW_APP.settings.captcha === 'recaptcha') {
    30             return $form.find('[name="g-recaptcha-response"]').val() || '';
    31         }
    32         return '';
     44    // Resend delay: server value when provided, otherwise the per-flow setting
     45    const resendDelay = (res) => {
     46        const s = res && res.data ? parseInt(res.data.resend_in, 10) : 0;
     47        const cfg = (CEOVW_APP.settings && CEOVW_APP.settings.resend) || {};
     48        return s > 0 ? s : (parseInt(cfg[currentFlow], 10) || 60);
    3349    };
    3450
    … …  
    3955        let pass = $form.find('input[name="password"]').val();
    4056        let confirm = $form.find('input[name="password_confirm"]').val();
    41        
    42         if (pass !== confirm) {
    43             alert("Passwords do not match!");
    44             return;
    45         }
    46 
    4757        let email = $form.find('input[name="email"]').val();
    4858        let name = $form.find('input[name="ceovw_full_name"]').val();
     59        const $empty = missing($form);
     60
     61        if ($empty.length) { formMsg($form, T.required); $empty.trigger('focus'); return; }
     62        if (!validEmail(email)) { formMsg($form, T.badEmail); $form.find('input[name="email"]').trigger('focus'); return; }
     63        if (pass !== confirm) { formMsg($form, T.mismatch); $form.find('input[name="password_confirm"]').trigger('focus'); return; }
     64        formMsg($form, '');
     65
    4966        let $btn = $('#ceovw-btn-reg');
    50 
    5167        setBtnLoading($btn, true);
    5268
    … …  
    5470            action: 'ceovw_process_registration_req',
    5571            nonce: CEOVW_APP.nonce,
    56             email: email, name: name, password: pass,
    57             captcha: getCaptchaToken($form)
     72            email: email, name: name, password: pass
    5873        }, function(res){
    5974            setBtnLoading($btn, false);
    … …  
    6277                targetEmail = email;
    6378                resetToken = '';
    64                 openOTPModal(email); // JSON Success হলে এই পপআপটি ওপেন হবে
    65             } else {
    66                 alert(res.data.message || CEOVW_APP.i18n.error);
    67             }
    68         }).fail(function(xhr, status, error) {
    69             setBtnLoading($btn, false);
    70             console.error("AJAX Error:", error);
    71             alert(CEOVW_APP.i18n.error);
     79                openOTPModal(email, resendDelay(res));
     80            } else {
     81                formMsg($form, (res.data && res.data.message) || T.error);
     82            }
     83        }).fail(function() {
     84            setBtnLoading($btn, false);
     85            formMsg($form, T.error);
    7286        });
    7387    });
    … …  
    7892        let $form = $(this);
    7993        let email = $form.find('input[name="reset_email"]').val();
     94        if (!validEmail(email)) { formMsg($form, T.badEmail); $form.find('input[name="reset_email"]').trigger('focus'); return; }
     95        formMsg($form, '');
    8096        let $btn = $('#ceovw-btn-forgot');
    8197        resetToken = '';
    … …  
    86102            action: 'ceovw_process_forgot_req',
    87103            nonce: CEOVW_APP.nonce,
    88             email: email,
    89             captcha: getCaptchaToken($form)
     104            email: email
    90105        }, function(res){
    91106            setBtnLoading($btn, false);
    … …  
    93108                currentFlow = 'forgot';
    94109                targetEmail = email;
    95                 openOTPModal(email);
    96             } else {
    97                 alert(res.data.message || CEOVW_APP.i18n.error);
    98             }
    99         }).fail(function() { 
    100             setBtnLoading($btn, false); 
    101             alert(CEOVW_APP.i18n.error);
     110                openOTPModal(email, resendDelay(res));
     111            } else {
     112                formMsg($form, (res.data && res.data.message) || T.error);
     113            }
     114        }).fail(function() {
     115            setBtnLoading($btn, false);
     116            formMsg($form, T.error);
    102117        });
    103118    });
    … …  
    106121    const $modal = $('#ceovw-otp-modal');
    107122    const $otpInputs = $('.ceovw-otp-box');
    108    
    109     function openOTPModal(email) {
     123    const $otpMsg = $('#ceovw-otp-error');
     124    const otpMessage = (text, kind) => { $otpMsg.text(text || '').toggleClass('is-success', kind === 'success'); };
     125    const code = () => $otpInputs.map(function(){ return $(this).val(); }).get().join('');
     126
     127    function openOTPModal(email, wait) {
     128        lastFocus = document.activeElement;
    110129        $('#ceovw-target-email').text(email);
    111130        $otpInputs.val('');
    112         $('#ceovw-otp-error').text('');
     131        otpMessage('');
     132        verifying = false;
    113133        $modal.fadeIn(200);
    114         $otpInputs.first().focus();
    115         startResendTimer(60);
     134        $otpInputs.first().trigger('focus');
     135        startResendTimer(wait || resendDelay());
    116136    }
    117137
    118     $modal.find('.ceovw-modal-close').on('click', () => $modal.fadeOut(200));
     138    function closeOTPModal() {
     139        $modal.fadeOut(200);
     140        if (lastFocus && document.body.contains(lastFocus)) { lastFocus.focus(); }
     141    }
     142
     143    $modal.find('.ceovw-modal-close').on('click', closeOTPModal);
     144    $modal.on('click', function(e){ if (e.target === this) { closeOTPModal(); } });
     145
     146    // Keyboard: Escape closes, Tab stays inside the dialog
     147    $modal.on('keydown', function(e){
     148        if (e.key === 'Escape') { e.preventDefault(); closeOTPModal(); return; }
     149        if (e.key !== 'Tab') return;
     150        const $f = $modal.find('button:not(:disabled), input').filter(':visible');
     151        if (!$f.length) return;
     152        const first = $f[0], last = $f[$f.length - 1];
     153        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
     154        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
     155    });
     156
     157    // Spread several digits (paste or one-time-code autofill) across the boxes starting at index `from`
     158    const fill = (digits, from) => {
     159        digits.split('').slice(0, 6 - from).forEach((d, i) => { $otpInputs.eq(from + i).val(d); });
     160        const next = Math.min(from + digits.length, 5);
     161        $otpInputs.eq(next).trigger('focus');
     162    };
     163    const maybeSubmit = () => { if (code().length === 6) $('#ceovw-btn-verify').trigger('click'); };
    119164
    120165    $otpInputs.on('input', function() {
    121         this.value = this.value.replace(/\D/g, '');
    122         if(this.value && $(this).next('.ceovw-otp-box').length) {
    123             $(this).next('.ceovw-otp-box').focus();
     166        const digits = this.value.replace(/\D/g, '');
     167        const i = $otpInputs.index(this);
     168        if (digits.length > 1) { this.value = ''; fill(digits, digits.length >= 6 ? 0 : i); }
     169        else {
     170            this.value = digits;
     171            if (digits && i < 5) { $otpInputs.eq(i + 1).trigger('focus'); }
    124172        }
    125         let fullCode = $otpInputs.map(function(){ return $(this).val(); }).get().join('');
    126         if(fullCode.length === 6) $('#ceovw-btn-verify').click();
     173        if (digits) otpMessage('');
     174        maybeSubmit();
    127175    });
    128176
    129177    $otpInputs.on('keydown', function(e) {
    130         if(e.key === "Backspace" && !this.value && $(this).prev('.ceovw-otp-box').length) {
    131             $(this).prev('.ceovw-otp-box').focus().val('');
    132         }
    133     });
     178        const i = $otpInputs.index(this);
     179        if (e.key === "Backspace" && !this.value && i > 0) { e.preventDefault(); $otpInputs.eq(i - 1).val('').trigger('focus'); }
     180        else if (e.key === 'ArrowLeft' && i > 0) { e.preventDefault(); $otpInputs.eq(i - 1).trigger('focus'); }
     181        else if (e.key === 'ArrowRight' && i < 5) { e.preventDefault(); $otpInputs.eq(i + 1).trigger('focus'); }
     182        else if (e.key === 'Enter') { e.preventDefault(); $('#ceovw-btn-verify').trigger('click'); }
     183    });
     184
     185    $otpInputs.on('focus', function(){ this.select(); });
    134186
    135187    $otpInputs.on('paste', function(e) {
    136188        e.preventDefault();
    137         let pasteData = (e.originalEvent || e).clipboardData.getData('text').replace(/\D/g, "").slice(0, 6);
    138         $otpInputs.each(function(i) {
    139             if(pasteData[i]) $(this).val(pasteData[i]);
    140         });
    141         if(pasteData.length === 6) $('#ceovw-btn-verify').click();
     189        const pasteData = ((e.originalEvent || e).clipboardData.getData('text') || '').replace(/\D/g, "").slice(0, 6);
     190        if (!pasteData) return;
     191        fill(pasteData, pasteData.length >= 6 ? 0 : $otpInputs.index(this));
     192        maybeSubmit();
    142193    });
    143194
    144195    // Verify OTP
    145196    $('#ceovw-btn-verify').on('click', function(){
    146         let code = $otpInputs.map(function(){ return $(this).val(); }).get().join('');
    147         if(code.length !== 6) return;
     197        const value = code();
     198        if (verifying) return;
     199        if (value.length !== 6) { otpMessage(T.enterCode); $otpInputs.filter(function(){ return !this.value; }).first().trigger('focus'); return; }
    148200
    149201        let $btn = $(this);
     202        verifying = true;
    150203        setBtnLoading($btn, true);
    151         $('#ceovw-otp-error').text('');
     204        otpMessage('');
    152205
    153206        $.post(CEOVW_APP.ajax_url, {
    154207            action: 'ceovw_verify_otp',
    155208            nonce: CEOVW_APP.nonce,
    156             email: targetEmail, code: code, flow: currentFlow
    157         }, function(res){
    158             if(res.success) {
    159                 $btn.find('.btn-text').text(CEOVW_APP.i18n.verified);
     209            email: targetEmail, code: value, flow: currentFlow
     210        }, function(res){
     211            if(res.success) {
     212                $btn.find('.btn-text').text(T.verified);
     213                otpMessage(T.verified, 'success');
    160214                setTimeout(() => {
    161215                    $modal.fadeOut(200);
    … …  
    164218                    } else if (currentFlow === 'forgot') {
    165219                        resetToken = (res.data && res.data.reset_token) || '';
    166                         $('.ceovw-view-pane').hide();
    167                         $('#ceovw-view-newpass').fadeIn(300);
     220                        showView('newpass');
    168221                        $('#ceovw-verified-email').val(targetEmail);
    169222                        setBtnLoading($btn, false);
    170                         $btn.find('.btn-text').text('Verify Code');
     223                        $btn.find('.btn-text').text(T.verifyBtn);
     224                        verifying = false;
    171225                    }
    172226                }, 1000);
    173227            } else {
     228                verifying = false;
    174229                setBtnLoading($btn, false);
    175                 $('#ceovw-otp-error').text(res.data.message || CEOVW_APP.i18n.error);
     230                otpMessage((res.data && res.data.message) || T.error);
    176231                $otpInputs.val('');
    177                 $otpInputs.first().focus();
    178             }
    179         }).fail(function() {
    180             setBtnLoading($btn, false);
    181             $('#ceovw-otp-error').text(CEOVW_APP.i18n.error);
     232                $otpInputs.first().trigger('focus');
     233            }
     234        }).fail(function() {
     235            verifying = false;
     236            setBtnLoading($btn, false);
     237            otpMessage(T.error);
    182238        });
    183239    });
    … …  
    189245        let $txt = $('#ceovw-timer-text');
    190246        $btn.prop('disabled', true);
    191        
     247        $txt.text(`(${seconds}s)`);
     248
    192249        resendTimer = setInterval(() => {
    193250            seconds--;
    … …  
    205262        let $btn = $(this);
    206263        $btn.prop('disabled', true);
    207         $('#ceovw-timer-text').text('...');
    208        
     264        $('#ceovw-timer-text').text('…');
     265
    209266        $.post(CEOVW_APP.ajax_url, {
    210267            action: currentFlow === 'register' ? 'ceovw_process_registration_req' : 'ceovw_process_forgot_req',
    … …  
    214271        }, function(res){
    215272            if(res.success) {
    216                 startResendTimer(60);
    217                 $('#ceovw-otp-error').text('New code sent!').css('color', 'green');
    218                 setTimeout(()=> $('#ceovw-otp-error').text('').css('color', '#dc2626'), 3000);
    219             } else {
    220                 alert(res.data.message);
    221                 $btn.prop('disabled', false);
    222             }
    223         });
     273                startResendTimer(resendDelay(res));
     274                otpMessage(T.codeSent, 'success');
     275                $otpInputs.val(''); $otpInputs.first().trigger('focus');
     276            } else {
     277                otpMessage((res.data && res.data.message) || T.error);
     278                if (res.data && parseInt(res.data.resend_in, 10) > 0) { startResendTimer(parseInt(res.data.resend_in, 10)); }
     279                else { $btn.prop('disabled', false); $('#ceovw-timer-text').text(''); }
     280            }
     281        }).fail(() => { otpMessage(T.error); $btn.prop('disabled', false); $('#ceovw-timer-text').text(''); });
    224282    });
    225283
    … …  
    230288        let pass = $form.find('input[name="new_password"]').val();
    231289        let confirm = $form.find('input[name="new_password_confirm"]').val();
    232        
    233         if(pass !== confirm) { alert("Passwords do not match."); return; }
    234        
     290        const $empty = missing($form);
     291
     292        if ($empty.length) { formMsg($form, T.required); $empty.trigger('focus'); return; }
     293        if(pass !== confirm) { formMsg($form, T.mismatch); $form.find('input[name="new_password_confirm"]').trigger('focus'); return; }
     294        formMsg($form, '');
     295
    235296        let $btn = $('#ceovw-btn-setpass');
    236297        setBtnLoading($btn, true);
    … …  
    246307                setBtnLoading($btn, false);
    247308                $form[0].reset();
    248                 alert((res.data && res.data.message) || CEOVW_APP.i18n.error);
    249                 $('.ceovw-view-pane').hide();
    250                 $('#ceovw-form-login input[name="username"]').val(targetEmail);
    251                 $('#ceovw-view-login').fadeIn(300);
     309                const $login = showView('login', false);
     310                $login.find('input[name="username"]').val(targetEmail);
     311                formMsg($login.find('form'), (res.data && res.data.message) || T.updated, 'success');
     312                setTimeout(() => $login.find('input[name="password"]').trigger('focus'), 320);
    252313            } else {
    253314                setBtnLoading($btn, false);
    254                 alert((res.data && res.data.message) || CEOVW_APP.i18n.error);
    255315                if (res.data && res.data.code === 'reset_expired') {
    256316                    // Authorization expired or already used: restart the forgot password flow
    257317                    resetToken = '';
    258318                    $form[0].reset();
    259                     $('.ceovw-view-pane').hide();
    260                     $('#ceovw-view-forgot').fadeIn(300);
     319                    const $forgot = showView('forgot');
     320                    formMsg($forgot.find('form'), (res.data && res.data.message) || T.error);
     321                } else {
     322                    formMsg($form, (res.data && res.data.message) || T.error);
    261323                }
    262324            }
    263         }).fail(() => { setBtnLoading($btn, false); alert(CEOVW_APP.i18n.error); });
    264     });
    265 
     325        }).fail(() => { setBtnLoading($btn, false); formMsg($form, T.error); });
     326    });
    266327})(jQuery);
  • naimur-email-otp-for-woocommerce/trunk/assets/src/js/main.js

    r3780003 r3780739  
    22    "use strict";
    33
    4     let currentFlow = '';
     4    const T = (window.CEOVW_APP && CEOVW_APP.i18n) || {};
     5    let currentFlow = '';
    56    let targetEmail = '';
    67    let resendTimer = null;
    78    let resetToken = ''; // Password reset authorization: kept in memory only, never persisted or logged
     9    let lastFocus = null;
     10    let verifying = false;
     11
     12    // Inline form messages (replace browser alerts; announced by screen readers)
     13    const formMsg = ($form, text, kind) => {
     14        const $m = $form.find('.ceovw-form-msg');
     15        if (!text) { $m.prop('hidden', true).text('').removeClass('is-error is-success'); return; }
     16        $m.text(text).prop('hidden', false).toggleClass('is-success', kind === 'success').toggleClass('is-error', kind !== 'success');
     17    };
     18    const showView = (target, focus) => {
     19        $('.ceovw-view-pane').hide();
     20        const $v = $(`#ceovw-view-${target}`).fadeIn(300);
     21        $v.find('.ceovw-form-msg').prop('hidden', true).text('');
     22        if (focus !== false) { setTimeout(() => $v.find('input:not([type=hidden]):visible').first().trigger('focus'), 320); }
     23        return $v;
     24    };
     25    const missing = ($form) => $form.find('input[required]').filter(function(){ return !$.trim(this.value); }).first();
     26    const validEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test($.trim(v));
    827
    928    // View Switcher (Smooth Fade)
    … …  
    1231        const target = $(this).data('target');
    1332        if (target !== 'newpass') resetToken = '';
    14         $('.ceovw-view-pane').hide();
    15         $(`#ceovw-view-${target}`).fadeIn(300);
    16     });
    17 
    18     // Helper: Button Loading State
     33        showView(target);
     34    });
     35
    1936    const setBtnLoading = ($btn, isLoading) => {
    2037        if(isLoading) {
    21             $btn.prop('disabled', true).addClass('loading');
     38            $btn.prop('disabled', true).addClass('loading').attr('aria-busy', 'true');
    2239        } else {
    23             $btn.prop('disabled', false).removeClass('loading');
     40            $btn.prop('disabled', false).removeClass('loading').attr('aria-busy', 'false');
    2441        }
    2542    };
    2643
    27     // Helper: Captcha Token
    28     const getCaptchaToken = ($form) => {
    29         if (CEOVW_APP.settings.captcha === 'turnstile') {
    30             return $form.find('[name="cf-turnstile-response"]').val() || '';
    31         } else if (CEOVW_APP.settings.captcha === 'recaptcha') {
    32             return $form.find('[name="g-recaptcha-response"]').val() || '';
    33         }
    34         return '';
    35     };
    36 
    37     // Registration Form Submit
     44    // Resend delay: server value when provided, otherwise the per-flow setting
     45    const resendDelay = (res) => {
     46        const s = res && res.data ? parseInt(res.data.resend_in, 10) : 0;
     47        const cfg = (CEOVW_APP.settings && CEOVW_APP.settings.resend) || {};
     48        return s > 0 ? s : (parseInt(cfg[currentFlow], 10) || 60);
     49    };
     50
     51    // Registration Flow
    3852    $('#ceovw-form-register').on('submit', function(e){
    3953        e.preventDefault();
    … …  
    4155        let pass = $form.find('input[name="password"]').val();
    4256        let confirm = $form.find('input[name="password_confirm"]').val();
    43        
    44         if (pass !== confirm) {
    45             alert("Passwords do not match!");
    46             return;
    47         }
    48 
    4957        let email = $form.find('input[name="email"]').val();
    5058        let name = $form.find('input[name="ceovw_full_name"]').val();
     59        const $empty = missing($form);
     60
     61        if ($empty.length) { formMsg($form, T.required); $empty.trigger('focus'); return; }
     62        if (!validEmail(email)) { formMsg($form, T.badEmail); $form.find('input[name="email"]').trigger('focus'); return; }
     63        if (pass !== confirm) { formMsg($form, T.mismatch); $form.find('input[name="password_confirm"]').trigger('focus'); return; }
     64        formMsg($form, '');
     65
    5166        let $btn = $('#ceovw-btn-reg');
    52 
    5367        setBtnLoading($btn, true);
    5468
    … …  
    5670            action: 'ceovw_process_registration_req',
    5771            nonce: CEOVW_APP.nonce,
    58             email: email, name: name, password: pass,
    59             captcha: getCaptchaToken($form)
     72            email: email, name: name, password: pass
    6073        }, function(res){
    6174            setBtnLoading($btn, false);
    … …  
    6477                targetEmail = email;
    6578                resetToken = '';
    66                 openOTPModal(email);
    67             } else {
    68                 alert(res.data.message || CEOVW_APP.i18n.error);
    69             }
    70         }).fail(() => { setBtnLoading($btn, false); alert(CEOVW_APP.i18n.error); });
    71     });
    72 
    73     // Forgot Password Form Submit
     79                openOTPModal(email, resendDelay(res));
     80            } else {
     81                formMsg($form, (res.data && res.data.message) || T.error);
     82            }
     83        }).fail(function() {
     84            setBtnLoading($btn, false);
     85            formMsg($form, T.error);
     86        });
     87    });
     88
     89    // Forgot Password Flow
    7490    $('#ceovw-form-forgot').on('submit', function(e){
    7591        e.preventDefault();
    7692        let $form = $(this);
    7793        let email = $form.find('input[name="reset_email"]').val();
     94        if (!validEmail(email)) { formMsg($form, T.badEmail); $form.find('input[name="reset_email"]').trigger('focus'); return; }
     95        formMsg($form, '');
    7896        let $btn = $('#ceovw-btn-forgot');
    7997        resetToken = '';
    … …  
    84102            action: 'ceovw_process_forgot_req',
    85103            nonce: CEOVW_APP.nonce,
    86             email: email,
    87             captcha: getCaptchaToken($form)
     104            email: email
    88105        }, function(res){
    89106            setBtnLoading($btn, false);
    … …  
    91108                currentFlow = 'forgot';
    92109                targetEmail = email;
    93                 openOTPModal(email);
    94             } else {
    95                 alert(res.data.message || CEOVW_APP.i18n.error);
    96             }
    97         }).fail(() => { setBtnLoading($btn, false); alert(CEOVW_APP.i18n.error); });
    98     });
    99 
    100     // OTP Modal Logic & Auto-tab
     110                openOTPModal(email, resendDelay(res));
     111            } else {
     112                formMsg($form, (res.data && res.data.message) || T.error);
     113            }
     114        }).fail(function() {
     115            setBtnLoading($btn, false);
     116            formMsg($form, T.error);
     117        });
     118    });
     119
     120    // OTP Modal Logic
    101121    const $modal = $('#ceovw-otp-modal');
    102122    const $otpInputs = $('.ceovw-otp-box');
    103    
    104     function openOTPModal(email) {
     123    const $otpMsg = $('#ceovw-otp-error');
     124    const otpMessage = (text, kind) => { $otpMsg.text(text || '').toggleClass('is-success', kind === 'success'); };
     125    const code = () => $otpInputs.map(function(){ return $(this).val(); }).get().join('');
     126
     127    function openOTPModal(email, wait) {
     128        lastFocus = document.activeElement;
    105129        $('#ceovw-target-email').text(email);
    106130        $otpInputs.val('');
    107         $('#ceovw-otp-error').text('');
     131        otpMessage('');
     132        verifying = false;
    108133        $modal.fadeIn(200);
    109         $otpInputs.first().focus();
    110         startResendTimer(60); // 60s cooldown start
     134        $otpInputs.first().trigger('focus');
     135        startResendTimer(wait || resendDelay());
    111136    }
    112137
    113     $modal.find('.ceovw-modal-close').on('click', () => $modal.fadeOut(200));
     138    function closeOTPModal() {
     139        $modal.fadeOut(200);
     140        if (lastFocus && document.body.contains(lastFocus)) { lastFocus.focus(); }
     141    }
     142
     143    $modal.find('.ceovw-modal-close').on('click', closeOTPModal);
     144    $modal.on('click', function(e){ if (e.target === this) { closeOTPModal(); } });
     145
     146    // Keyboard: Escape closes, Tab stays inside the dialog
     147    $modal.on('keydown', function(e){
     148        if (e.key === 'Escape') { e.preventDefault(); closeOTPModal(); return; }
     149        if (e.key !== 'Tab') return;
     150        const $f = $modal.find('button:not(:disabled), input').filter(':visible');
     151        if (!$f.length) return;
     152        const first = $f[0], last = $f[$f.length - 1];
     153        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
     154        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
     155    });
     156
     157    // Spread several digits (paste or one-time-code autofill) across the boxes starting at index `from`
     158    const fill = (digits, from) => {
     159        digits.split('').slice(0, 6 - from).forEach((d, i) => { $otpInputs.eq(from + i).val(d); });
     160        const next = Math.min(from + digits.length, 5);
     161        $otpInputs.eq(next).trigger('focus');
     162    };
     163    const maybeSubmit = () => { if (code().length === 6) $('#ceovw-btn-verify').trigger('click'); };
    114164
    115165    $otpInputs.on('input', function() {
    116         this.value = this.value.replace(/\D/g, '');
    117         if(this.value && $(this).next('.ceovw-otp-box').length) {
    118             $(this).next('.ceovw-otp-box').focus();
     166        const digits = this.value.replace(/\D/g, '');
     167        const i = $otpInputs.index(this);
     168        if (digits.length > 1) { this.value = ''; fill(digits, digits.length >= 6 ? 0 : i); }
     169        else {
     170            this.value = digits;
     171            if (digits && i < 5) { $otpInputs.eq(i + 1).trigger('focus'); }
    119172        }
    120         let fullCode = $otpInputs.map(function(){ return $(this).val(); }).get().join('');
    121         if(fullCode.length === 6) $('#ceovw-btn-verify').click();
     173        if (digits) otpMessage('');
     174        maybeSubmit();
    122175    });
    123176
    124177    $otpInputs.on('keydown', function(e) {
    125         if(e.key === "Backspace" && !this.value && $(this).prev('.ceovw-otp-box').length) {
    126             $(this).prev('.ceovw-otp-box').focus().val('');
    127         }
    128     });
     178        const i = $otpInputs.index(this);
     179        if (e.key === "Backspace" && !this.value && i > 0) { e.preventDefault(); $otpInputs.eq(i - 1).val('').trigger('focus'); }
     180        else if (e.key === 'ArrowLeft' && i > 0) { e.preventDefault(); $otpInputs.eq(i - 1).trigger('focus'); }
     181        else if (e.key === 'ArrowRight' && i < 5) { e.preventDefault(); $otpInputs.eq(i + 1).trigger('focus'); }
     182        else if (e.key === 'Enter') { e.preventDefault(); $('#ceovw-btn-verify').trigger('click'); }
     183    });
     184
     185    $otpInputs.on('focus', function(){ this.select(); });
    129186
    130187    $otpInputs.on('paste', function(e) {
    131188        e.preventDefault();
    132         let pasteData = (e.originalEvent || e).clipboardData.getData('text').replace(/\D/g, "").slice(0, 6);
    133         $otpInputs.each(function(i) {
    134             if(pasteData[i]) $(this).val(pasteData[i]);
    135         });
    136         if(pasteData.length === 6) $('#ceovw-btn-verify').click();
    137     });
    138 
    139     // Verify OTP Action
     189        const pasteData = ((e.originalEvent || e).clipboardData.getData('text') || '').replace(/\D/g, "").slice(0, 6);
     190        if (!pasteData) return;
     191        fill(pasteData, pasteData.length >= 6 ? 0 : $otpInputs.index(this));
     192        maybeSubmit();
     193    });
     194
     195    // Verify OTP
    140196    $('#ceovw-btn-verify').on('click', function(){
    141         let code = $otpInputs.map(function(){ return $(this).val(); }).get().join('');
    142         if(code.length !== 6) return;
     197        const value = code();
     198        if (verifying) return;
     199        if (value.length !== 6) { otpMessage(T.enterCode); $otpInputs.filter(function(){ return !this.value; }).first().trigger('focus'); return; }
    143200
    144201        let $btn = $(this);
     202        verifying = true;
    145203        setBtnLoading($btn, true);
    146         $('#ceovw-otp-error').text('');
     204        otpMessage('');
    147205
    148206        $.post(CEOVW_APP.ajax_url, {
    149207            action: 'ceovw_verify_otp',
    150208            nonce: CEOVW_APP.nonce,
    151             email: targetEmail, code: code, flow: currentFlow
    152         }, function(res){
    153             if(res.success) {
    154                 $btn.find('.btn-text').text(CEOVW_APP.i18n.verified);
     209            email: targetEmail, code: value, flow: currentFlow
     210        }, function(res){
     211            if(res.success) {
     212                $btn.find('.btn-text').text(T.verified);
     213                otpMessage(T.verified, 'success');
    155214                setTimeout(() => {
    156215                    $modal.fadeOut(200);
    … …  
    159218                    } else if (currentFlow === 'forgot') {
    160219                        resetToken = (res.data && res.data.reset_token) || '';
    161                         $('.ceovw-view-pane').hide();
    162                         $('#ceovw-view-newpass').fadeIn(300);
     220                        showView('newpass');
    163221                        $('#ceovw-verified-email').val(targetEmail);
    164222                        setBtnLoading($btn, false);
    165                         $btn.find('.btn-text').text('Verify Code');
     223                        $btn.find('.btn-text').text(T.verifyBtn);
     224                        verifying = false;
    166225                    }
    167226                }, 1000);
    168227            } else {
     228                verifying = false;
    169229                setBtnLoading($btn, false);
    170                 $('#ceovw-otp-error').text(res.data.message || CEOVW_APP.i18n.error);
     230                otpMessage((res.data && res.data.message) || T.error);
    171231                $otpInputs.val('');
    172                 $otpInputs.first().focus();
    173             }
    174         }).fail(() => { setBtnLoading($btn, false); $('#ceovw-otp-error').text(CEOVW_APP.i18n.error); });
    175     });
    176 
    177     // Timer Logic for Resend
     232                $otpInputs.first().trigger('focus');
     233            }
     234        }).fail(function() {
     235            verifying = false;
     236            setBtnLoading($btn, false);
     237            otpMessage(T.error);
     238        });
     239    });
     240
     241    // Resend Timer
    178242    function startResendTimer(seconds) {
    179243        clearInterval(resendTimer);
    … …  
    181245        let $txt = $('#ceovw-timer-text');
    182246        $btn.prop('disabled', true);
    183        
     247        $txt.text(`(${seconds}s)`);
     248
    184249        resendTimer = setInterval(() => {
    185250            seconds--;
    … …  
    197262        let $btn = $(this);
    198263        $btn.prop('disabled', true);
    199         $('#ceovw-timer-text').text('...');
    200        
     264        $('#ceovw-timer-text').text('…');
     265
    201266        $.post(CEOVW_APP.ajax_url, {
    202267            action: currentFlow === 'register' ? 'ceovw_process_registration_req' : 'ceovw_process_forgot_req',
    … …  
    206271        }, function(res){
    207272            if(res.success) {
    208                 startResendTimer(60);
    209                 $('#ceovw-otp-error').text('New code sent!').css('color', 'green');
    210                 setTimeout(()=> $('#ceovw-otp-error').text('').css('color', '#dc2626'), 3000);
    211             } else {
    212                 alert(res.data.message);
    213                 $btn.prop('disabled', false);
    214             }
    215         });
    216     });
    217 
    218     // Set New Password Action
     273                startResendTimer(resendDelay(res));
     274                otpMessage(T.codeSent, 'success');
     275                $otpInputs.val(''); $otpInputs.first().trigger('focus');
     276            } else {
     277                otpMessage((res.data && res.data.message) || T.error);
     278                if (res.data && parseInt(res.data.resend_in, 10) > 0) { startResendTimer(parseInt(res.data.resend_in, 10)); }
     279                else { $btn.prop('disabled', false); $('#ceovw-timer-text').text(''); }
     280            }
     281        }).fail(() => { otpMessage(T.error); $btn.prop('disabled', false); $('#ceovw-timer-text').text(''); });
     282    });
     283
     284    // Set New Password
    219285    $('#ceovw-form-newpass').on('submit', function(e){
    220286        e.preventDefault();
    … …  
    222288        let pass = $form.find('input[name="new_password"]').val();
    223289        let confirm = $form.find('input[name="new_password_confirm"]').val();
    224        
    225         if(pass !== confirm) { alert("Passwords do not match."); return; }
    226        
     290        const $empty = missing($form);
     291
     292        if ($empty.length) { formMsg($form, T.required); $empty.trigger('focus'); return; }
     293        if(pass !== confirm) { formMsg($form, T.mismatch); $form.find('input[name="new_password_confirm"]').trigger('focus'); return; }
     294        formMsg($form, '');
     295
    227296        let $btn = $('#ceovw-btn-setpass');
    228297        setBtnLoading($btn, true);
    … …  
    238307                setBtnLoading($btn, false);
    239308                $form[0].reset();
    240                 alert((res.data && res.data.message) || CEOVW_APP.i18n.error);
    241                 $('.ceovw-view-pane').hide();
    242                 $('#ceovw-form-login input[name="username"]').val(targetEmail);
    243                 $('#ceovw-view-login').fadeIn(300);
     309                const $login = showView('login', false);
     310                $login.find('input[name="username"]').val(targetEmail);
     311                formMsg($login.find('form'), (res.data && res.data.message) || T.updated, 'success');
     312                setTimeout(() => $login.find('input[name="password"]').trigger('focus'), 320);
    244313            } else {
    245314                setBtnLoading($btn, false);
    246                 alert((res.data && res.data.message) || CEOVW_APP.i18n.error);
    247315                if (res.data && res.data.code === 'reset_expired') {
    248316                    // Authorization expired or already used: restart the forgot password flow
    249317                    resetToken = '';
    250318                    $form[0].reset();
    251                     $('.ceovw-view-pane').hide();
    252                     $('#ceovw-view-forgot').fadeIn(300);
     319                    const $forgot = showView('forgot');
     320                    formMsg($forgot.find('form'), (res.data && res.data.message) || T.error);
     321                } else {
     322                    formMsg($form, (res.data && res.data.message) || T.error);
    253323                }
    254324            }
    255         }).fail(() => { setBtnLoading($btn, false); alert(CEOVW_APP.i18n.error); });
    256     });
    257 
     325        }).fail(() => { setBtnLoading($btn, false); formMsg($form, T.error); });
     326    });
    258327})(jQuery);
  • naimur-email-otp-for-woocommerce/trunk/includes/class-ajax.php

    r3780003 r3780739  
    2222        return ($ip === '::1' || $ip === '127.0.0.1') ? '127.0.0.1 (Localhost)' : $ip;
    2323    }
     24
     25    /** Coarse device label for activity logs (no full user agent is stored). */
     26    public static function device_label() { return self::get_user_device(); }
    2427
    2528    private static function get_user_device() {
    … …  
    7881        $is_resend = ! empty( $_POST['is_resend'] );
    7982
     83        if ( ! CEOVW_Options::get( 'registration_enabled' ) ) { wp_send_json_error(['message' => __( 'Registration is not available.', 'naimur-email-otp-for-woocommerce' )]); }
    8084        if ( ! is_email( $email ) ) { wp_send_json_error(['message' => 'Invalid email address.']); }
    8185        if ( email_exists( $email ) ) { wp_send_json_error(['message' => 'An account is already registered with your email address.']); }
    8286        if ( CEOVW_Security::is_rate_limited( 'send' ) ) { wp_send_json_error(['message' => 'Too many requests. Please try again later.']); }
    8387        if ( ! CEOVW_Security::can_request_otp( $email ) ) { wp_send_json_error(['message' => 'Too many requests. Please try again later.']); }
     88        $wait = CEOVW_Security::resend_wait( $email, 'register' );
     89        /* translators: %d: seconds */
     90        if ( $wait > 0 ) { wp_send_json_error(['message' => sprintf( __( 'Please wait %d seconds before requesting a new code.', 'naimur-email-otp-for-woocommerce' ), $wait ), 'resend_in' => $wait]); }
    8491
    8592        if ( $is_resend && $password === '' ) {
    … …  
    101108            CEOVW_Logs::push_email_log(['time' => time(), 'email' => $email, 'status' => 'Success', 'message' => 'Registration OTP sent via email.']);
    102109            CEOVW_Security::record_otp_request($email);
    103             wp_send_json_success(['message' => 'OTP sent successfully.']);
     110            wp_send_json_success(['message' => 'OTP sent successfully.', 'resend_in' => CEOVW_Options::resend_seconds( 'register' )]);
    104111        } else {
    105             CEOVW_Security::clear_otp( $email );
     112            CEOVW_Security::clear_flow( $email, 'register' );
    106113            CEOVW_Logs::push_email_log(['time' => time(), 'email' => $email, 'status' => 'Failed', 'message' => 'Server failed to send email.']);
    107114            wp_send_json_error(['message' => 'Failed to send email. Check SMTP settings.']);
    … …  
    117124        $generic = 'If an account exists for this email, a verification code has been sent.';
    118125
     126        if ( ! CEOVW_Options::get( 'forgot_enabled' ) ) { wp_send_json_error(['message' => __( 'Password reset by code is not available. Please use the standard lost password link.', 'naimur-email-otp-for-woocommerce' )]); }
    119127        if ( ! is_email( $email ) ) { wp_send_json_error(['message' => 'Please enter a valid email address.']); }
    120128        if ( CEOVW_Security::is_rate_limited( 'send' ) ) { wp_send_json_error(['message' => 'Too many requests. Please try again later.']); }
    … …  
    131139        }
    132140
     141        // Server-side resend cooldown. The response stays identical, so the cooldown cannot reveal whether an account exists.
     142        if ( CEOVW_Security::resend_wait( $email, 'forgot' ) > 0 ) {
     143            self::clean_buffer();
     144            wp_send_json_success(['message' => $generic]);
     145        }
     146
    133147        $otp = CEOVW_Security::generate_and_save_otp( $email, 'forgot' );
    134         $sent = $otp !== false && CEOVW_Emails::send_otp_email( $email, $otp, 'forgot' );
     148        $sent = $otp !== false && CEOVW_Emails::send_otp_email( $email, $otp, 'forgot', $user->display_name );
    135149
    136150        self::clean_buffer();
    … …  
    139153            CEOVW_Logs::push_email_log(['time' => time(), 'email' => $email, 'status' => 'Success', 'message' => 'Password reset OTP sent.']);
    140154        } else {
    141             CEOVW_Security::clear_otp( $email );
     155            CEOVW_Security::clear_flow( $email, 'forgot' );
    142156            CEOVW_Logs::push_email_log(['time' => time(), 'email' => $email, 'status' => 'Failed', 'message' => 'Server failed to send email.']);
    143157        }
    … …  
    154168        $code  = sanitize_text_field( $_POST['code'] ?? '' );
    155169        $flow  = sanitize_text_field( $_POST['flow'] ?? '' );
     170
     171        // This endpoint serves the My Account flows only; checkout has its own session-bound endpoint.
     172        if ( ! in_array( $flow, ['register', 'forgot'], true ) ) { wp_send_json_error(['message' => 'Invalid code. Please try again.']); }
     173        if ( ( 'register' === $flow && ! CEOVW_Options::get( 'registration_enabled' ) ) || ( 'forgot' === $flow && ! CEOVW_Options::get( 'forgot_enabled' ) ) ) {
     174            wp_send_json_error(['message' => __( 'This verification is not available.', 'naimur-email-otp-for-woocommerce' )]);
     175        }
    156176
    157177        $row = CEOVW_Security::verify_otp_code( $email, $code, $flow );
    … …  
    175195                    CEOVW_Users::auto_login( $user_id );
    176196                    CEOVW_Security::clear_otp( $email );
    177                     CEOVW_Logs::push_activity_log(['time' => time(), 'user_id' => $user_id, 'email' => $email, 'ip' => $real_ip, 'device' => $device]);
     197                    CEOVW_Logs::push_activity_log(['time' => time(), 'user_id' => $user_id, 'email' => $email, 'ip' => $real_ip, 'device' => $device, 'event' => 'registration_verified']);
    178198                    wp_send_json_success(['message' => 'Account created and verified.']);
    179199                } else {
    … …  
    188208
    189209            $user = get_user_by( 'email', $email );
    190             CEOVW_Logs::push_activity_log(['time' => time(), 'user_id' => $user ? $user->ID : 0, 'email' => $email, 'ip' => $real_ip, 'device' => $device]);
     210            CEOVW_Logs::push_activity_log(['time' => time(), 'user_id' => $user ? $user->ID : 0, 'email' => $email, 'ip' => $real_ip, 'device' => $device, 'event' => 'reset_verified']);
    191211            // The raw token is only returned to this browser flow; the database stores its SHA-256 hash.
    192212            wp_send_json_success(['message' => 'Code verified.', 'reset_token' => $token]);
    … …  
    199219
    200220        $expired = ['message' => 'Your password reset session has expired. Please request a new code.', 'code' => 'reset_expired'];
     221        if ( ! CEOVW_Options::get( 'forgot_enabled' ) ) { wp_send_json_error( $expired ); }
    201222
    202223        if ( CEOVW_Security::is_rate_limited( 'reset' ) ) { wp_send_json_error(['message' => 'Too many requests. Please try again later.']); }
  • naimur-email-otp-for-woocommerce/trunk/includes/class-emails.php

    r3587442 r3780739  
    44class CEOVW_Emails {
    55
     6    /** Internal flow name -> template type. */
     7    public static function type_for_flow( $flow ) {
     8        $map = array( 'register' => 'registration', 'forgot' => 'forgot', 'checkout' => 'checkout' );
     9        return isset( $map[ $flow ] ) ? $map[ $flow ] : 'registration';
     10    }
     11
     12    /**
     13     * Sends an OTP email using the stored (sanitized) template for the flow.
     14     * Templates are stored as HTML in the database; no template file is ever built from a setting value.
     15     * Uses wp_mail(), so SMTP plugins handle delivery.
     16     */
    617    public static function send_otp_email( $email, $otp, $flow, $name = '' ) {
    7         $subject = get_option('naimur-email-otp-for-woocommerce_email_subject', 'Your Verification Code');
    8         $template_id = get_option('naimur-email-otp-for-woocommerce_email_template', '1');
    9        
    10         $message = '';
    11         if ( $template_id === 'custom' ) {
    12             $message = str_replace( '[naimur_email_otp_code]', $otp, get_option('naimur-email-otp-for-woocommerce_custom_email_html', 'Your code is [naimur_email_otp_code]') );
    13         } else {
    14             // Include your default template files here
    15             ob_start();
    16             include NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH . "templates/email-template-$template_id.php";
    17             $message = ob_get_clean();
    18             $message = str_replace( '[naimur_email_otp_code]', $otp, $message );
     18        $type = self::type_for_flow( $flow );
     19        $out  = CEOVW_Templates::render( $type, array(
     20            'customer_name'  => $name,
     21            'customer_email' => $email,
     22            'otp_code'       => $otp,
     23            'expiry_minutes' => (int) ceil( CEOVW_Options::ttl( $flow ) / 60 ),
     24        ) );
     25        return wp_mail( $email, $out['subject'], $out['html'], self::headers() );
     26    }
     27
     28    /** Test email from the admin (sample code, never a real OTP). */
     29    public static function send_test( $type, $to, $override = null ) {
     30        $out = CEOVW_Templates::render( $type, CEOVW_Templates::sample_vars( $type ), $override );
     31        $ok  = wp_mail( $to, '[' . __( 'Test', 'naimur-email-otp-for-woocommerce' ) . '] ' . $out['subject'], $out['html'], self::headers() );
     32        if ( $ok ) { update_option( 'ceovw_test_email_sent_at', time(), false ); }
     33        return $ok;
     34    }
     35
     36    /** Headers for this plugin's emails only (no global From filter, so other plugins' emails are unaffected). */
     37    public static function headers() {
     38        $h    = array( 'Content-Type: text/html; charset=UTF-8' );
     39        $from = CEOVW_Options::get( 'sender_email' );
     40        if ( $from ) {
     41            $name = str_replace( array( "\r", "\n", '"', '<', '>' ), '', (string) CEOVW_Options::get( 'sender_name' ) );
     42            $h[]  = 'From: ' . ( '' !== $name ? '"' . $name . '" ' : '' ) . '<' . $from . '>';
    1943        }
    20 
    21         return wp_mail( $email, $subject, $message, ['Content-Type: text/html; charset=UTF-8'] );
     44        return $h;
    2245    }
    2346}
  • naimur-email-otp-for-woocommerce/trunk/includes/class-logs.php

    r3780003 r3780739  
    77    const EMAIL_T_TTL = 7; // days
    88
    9     public static function init(){ add_action('admin_menu', [__CLASS__, 'menu']); }
    10    
    11     public static function menu(){
    12         add_submenu_page('woocommerce', esc_html__('Email/Activity Logs', 'naimur-email-otp-for-woocommerce'), esc_html__('Email Verification Logs', 'naimur-email-otp-for-woocommerce'), 'manage_woocommerce', 'naimur-email-otp-for-woocommerce-logs', [__CLASS__, 'render']);
    13     }
    14    
     9    // Logs are shown on the "Activity & Diagnostics" admin page (includes/admin/views/diagnostics.php).
     10
    1511    public static function get_email_logs(){ $logs = get_transient(self::EMAIL_T_KEY); return is_array($logs)?$logs:[]; }
    1612   
    … …  
    4137        update_option(self::ACTIVITY_KEY, $logs, false);
    4238    }
    43    
    44     public static function render(){
    45         if (isset($_POST['ceovw_clear_logs']) && check_admin_referer('ceovw_logs')){
    46             if ($_POST['ceovw_clear_logs'] === 'email') { delete_transient(self::EMAIL_T_KEY); }
    47             if ($_POST['ceovw_clear_logs'] === 'activity') { update_option(self::ACTIVITY_KEY, [] ); }
    48             echo '<div class="updated"><p>'.esc_html__('Logs cleared.', 'naimur-email-otp-for-woocommerce').'</p></div>';
    49         }
    50        
    51         $email_logs = self::get_email_logs();
    52         $activity = self::get_activity_logs();
    53         ?>
    54        
    55         <div class="wrap">
    56             <h1><?php esc_html_e('Email & Activity Logs', 'naimur-email-otp-for-woocommerce'); ?></h1>
    57            
    58             <h2><?php esc_html_e('Last Sent OTP Emails', 'naimur-email-otp-for-woocommerce'); ?></h2>
    59             <form method="post">
    60                 <?php wp_nonce_field('ceovw_logs'); ?>
    61                 <input type="hidden" name="ceovw_clear_logs" value="email" />
    62                 <button class="button"><?php esc_html_e('Clear Email Logs','naimur-email-otp-for-woocommerce'); ?></button>
    63             </form>
    64             <table class="widefat striped">
    65                 <thead>
    66                     <tr>
    67                         <th><?php esc_html_e('Time','naimur-email-otp-for-woocommerce'); ?></th>
    68                         <th><?php esc_html_e('Email','naimur-email-otp-for-woocommerce'); ?></th>
    69                         <th><?php esc_html_e('Code','naimur-email-otp-for-woocommerce'); ?></th>
    70                         <th><?php esc_html_e('Status','naimur-email-otp-for-woocommerce'); ?></th>
    71                         <th><?php esc_html_e('Message','naimur-email-otp-for-woocommerce'); ?></th>
    72                     </tr>
    73                 </thead>
    74                 <tbody>
    75                 <?php if (empty($email_logs)): ?>
    76                     <tr><td colspan="5"><?php esc_html_e('No logs yet.','naimur-email-otp-for-woocommerce'); ?></td></tr>
    77                 <?php else: foreach (array_reverse($email_logs) as $row): ?>
    78                     <tr>
    79                         <td><?php echo esc_html( date_i18n( get_option('date_format').' '.get_option('time_format'), $row['time'] ?? time() ) ); ?></td>
    80                         <td><?php echo esc_html(ceovw_mask_email($row['email'] ?? '')); ?></td>
    81                         <td><code>••••••</code></td>
    82                         <td><?php echo esc_html($row['status'] ?? ''); ?></td>
    83                         <td><?php echo esc_html($row['message'] ?? ''); ?></td>
    84                     </tr>
    85                 <?php endforeach; endif; ?>
    86                 </tbody>
    87             </table>
    88 
    89             <h2 style="margin-top:28px;"><?php esc_html_e('Activity (Who verified when)','naimur-email-otp-for-woocommerce'); ?></h2>
    90             <form method="post">
    91                 <?php wp_nonce_field('ceovw_logs'); ?>
    92                 <input type="hidden" name="ceovw_clear_logs" value="activity" />
    93                 <button class="button"><?php esc_html_e('Clear Activity Logs','naimur-email-otp-for-woocommerce'); ?></button>
    94             </form>
    95             <table class="widefat striped">
    96                 <thead>
    97                     <tr>
    98                         <th><?php esc_html_e('Time','naimur-email-otp-for-woocommerce'); ?></th>
    99                         <th><?php esc_html_e('User ID','naimur-email-otp-for-woocommerce'); ?></th>
    100                         <th><?php esc_html_e('Email','naimur-email-otp-for-woocommerce'); ?></th>
    101                         <th><?php esc_html_e('Device','naimur-email-otp-for-woocommerce'); ?></th>
    102                         <th><?php esc_html_e('IP','naimur-email-otp-for-woocommerce'); ?></th>
    103                     </tr>
    104                 </thead>
    105                 <tbody>
    106                 <?php if (empty($activity)): ?>
    107                     <tr><td colspan="5"><?php esc_html_e('No activity yet.','naimur-email-otp-for-woocommerce'); ?></td></tr>
    108                 <?php else: foreach (array_reverse($activity) as $row): ?>
    109                     <tr>
    110                         <td><?php echo esc_html( date_i18n( get_option('date_format').' '.get_option('time_format'), $row['time'] ?? time() ) ); ?></td>
    111                         <td><?php echo esc_html($row['user_id'] ?? ''); ?></td>
    112                         <td><?php echo esc_html(ceovw_mask_email($row['email'] ?? '')); ?></td>
    113                         <td><strong><?php echo esc_html($row['device'] ?? '-'); ?></strong></td>
    114                         <td><?php echo esc_html($row['ip'] ?? ''); ?></td>
    115                     </tr>
    116                 <?php endforeach; endif; ?>
    117                 </tbody>
    118             </table>
    119         </div>
    120         <?php
    121     }
    12239}
    123 CEOVW_Logs::init();
  • naimur-email-otp-for-woocommerce/trunk/includes/class-security.php

    r3780003 r3780739  
    44class CEOVW_Security {
    55
    6     const MAX_ATTEMPTS = 5;
     6    const MAX_ATTEMPTS = 5;   // default; per-flow value comes from CEOVW_Options (range 3-10)
    77
    88    private static function table() {
    … …  
    1818     * @return string|false The plain OTP (for the email only), or false on failure.
    1919     */
    20     public static function generate_and_save_otp( $email, $flow, $password = '', $name = '', $temp_data = null ) {
     20    public static function generate_and_save_otp( $email, $flow, $password = '', $name = '', $temp_data = null, $context_hash = null ) {
    2121        global $wpdb;
    2222        $table = self::table();
    … …  
    3232        }
    3333
    34         $wpdb->delete( $table, ['email' => $email] );
     34        // Replace only this email's pending row for the same flow (and, for checkout, the same session),
     35        // so a new code for one flow can never cancel another flow's authorization.
     36        if ( null !== $context_hash ) {
     37            $wpdb->delete( $table, ['email' => $email, 'flow' => $flow, 'context_hash' => $context_hash] );
     38        } else {
     39            $wpdb->delete( $table, ['email' => $email, 'flow' => $flow] );
     40        }
    3541
    3642        $inserted = $wpdb->insert( $table, [
    … …  
    4046            'payload'    => $payload,
    4147            'attempts'   => 0,
    42             'token_hash' => null,
    43             'expires_at' => time() + ceovw_otp_ttl(),
    44             'created_at' => current_time( 'mysql' )
     48            'token_hash'   => null,
     49            'expires_at'   => time() + ceovw_otp_ttl( $flow ),
     50            'sent_at'      => time(),
     51            'context_hash' => $context_hash,
     52            'created_at'   => current_time( 'mysql' )
    4553        ]);
    4654
    … …  
    5664     * @return object|WP_Error The consumed row (with payload) on success.
    5765     */
    58     public static function verify_otp_code( $email, $code, $flow ) {
     66    public static function verify_otp_code( $email, $code, $flow, $context_hash = null ) {
    5967        global $wpdb;
    6068        $table = self::table();
    6169        $now   = time();
    62 
    63         if ( ! in_array( $flow, ['register', 'forgot'], true ) || ! preg_match( '/^\d{6}$/', $code ) ) {
     70        $max   = class_exists( 'CEOVW_Options' ) ? CEOVW_Options::max_attempts( $flow ) : self::MAX_ATTEMPTS;
     71
     72        if ( ! in_array( $flow, ['register', 'forgot', 'checkout'], true ) || ! is_string( $code ) || ! preg_match( '/^\d{6}$/', $code ) ) {
    6473            return new WP_Error( 'invalid', 'Invalid code. Please try again.' );
    6574        }
    66 
    67         $row = $wpdb->get_row( $wpdb->prepare(
    68             "SELECT * FROM $table WHERE email = %s AND flow = %s AND token_hash IS NULL AND otp <> '' ORDER BY id DESC LIMIT 1",
    69             $email, $flow
    70         ) );
     75        // Checkout codes are bound to the WooCommerce session that requested them.
     76        if ( 'checkout' === $flow && ( ! is_string( $context_hash ) || '' === $context_hash ) ) {
     77            return new WP_Error( 'invalid', 'Invalid code. Please try again.' );
     78        }
     79
     80        $row = 'checkout' === $flow
     81            ? $wpdb->get_row( $wpdb->prepare(
     82                "SELECT * FROM $table WHERE email = %s AND flow = %s AND context_hash = %s AND token_hash IS NULL AND otp <> '' ORDER BY id DESC LIMIT 1",
     83                $email, $flow, $context_hash ) )
     84            : $wpdb->get_row( $wpdb->prepare(
     85                "SELECT * FROM $table WHERE email = %s AND flow = %s AND token_hash IS NULL AND otp <> '' ORDER BY id DESC LIMIT 1",
     86                $email, $flow ) );
    7187
    7288        if ( ! $row ) {
    … …  
    8298        $reserved = $wpdb->query( $wpdb->prepare(
    8399            "UPDATE $table SET attempts = attempts + 1 WHERE id = %d AND token_hash IS NULL AND attempts < %d AND expires_at > %d",
    84             $row->id, self::MAX_ATTEMPTS, $now
     100            $row->id, $max, $now
    85101        ) );
    86102
    … …  
    88104        // so a parallel request that already issued a reset authorization is never undone.
    89105        if ( ! $reserved ) {
    90             $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE id = %d AND token_hash IS NULL AND attempts >= %d", $row->id, self::MAX_ATTEMPTS ) );
     106            $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE id = %d AND token_hash IS NULL AND attempts >= %d", $row->id, $max ) );
    91107            return new WP_Error( 'locked', 'Too many incorrect attempts. Please request a new code.' );
    92108        }
    … …  
    96112        if ( ! hash_equals( (string) $row->otp, $expected ) ) {
    97113            // Invalidate the OTP once the attempt limit is reached.
    98             $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE id = %d AND token_hash IS NULL AND attempts >= %d", $row->id, self::MAX_ATTEMPTS ) );
     114            $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE id = %d AND token_hash IS NULL AND attempts >= %d", $row->id, $max ) );
    99115            return new WP_Error( 'invalid', 'Invalid code. Please try again.' );
    100116        }
    … …  
    162178    }
    163179
     180    /**
     181     * Seconds until another code may be emailed for this email/flow (server-side resend cooldown). 0 = allowed now.
     182     * Only pending codes count; an issued authorization (token_hash set) does not block a new request.
     183     */
     184    public static function resend_wait( $email, $flow, $context_hash = null ) {
     185        global $wpdb;
     186        $table = self::table();
     187        $sent = null !== $context_hash
     188            ? (int) $wpdb->get_var( $wpdb->prepare( "SELECT MAX(sent_at) FROM $table WHERE email = %s AND flow = %s AND context_hash = %s AND token_hash IS NULL", $email, $flow, $context_hash ) )
     189            : (int) $wpdb->get_var( $wpdb->prepare( "SELECT MAX(sent_at) FROM $table WHERE email = %s AND flow = %s AND token_hash IS NULL", $email, $flow ) );
     190        $cool = class_exists( 'CEOVW_Options' ) ? CEOVW_Options::resend_seconds( $flow ) : 60;
     191        return $sent ? max( 0, $sent + $cool - time() ) : 0;
     192    }
     193
     194    /**
     195     * Checkout: converts a verified checkout OTP into a single-use, short-lived authorization bound to the
     196     * same email and WooCommerce session. Separate from password reset (flow = 'checkout').
     197     * Returns the raw token (kept only in the server-side WooCommerce session, never sent to the browser).
     198     */
     199    public static function issue_checkout_auth( $row, $context_hash ) {
     200        global $wpdb;
     201        $table = self::table();
     202        $token = bin2hex( random_bytes( 32 ) );
     203        $ttl   = max( 60, (int) apply_filters( 'ceovw_checkout_auth_ttl', 30 * MINUTE_IN_SECONDS ) );
     204        $updated = $wpdb->query( $wpdb->prepare(
     205            "UPDATE $table SET otp = '', attempts = 0, token_hash = %s, expires_at = %d
     206             WHERE id = %d AND flow = 'checkout' AND context_hash = %s AND otp = %s AND token_hash IS NULL AND expires_at > %d",
     207            hash( 'sha256', $token ), time() + $ttl, $row->id, $context_hash, $row->otp, time()
     208        ) );
     209        return $updated ? $token : false;
     210    }
     211
     212    /** Non-consuming check: valid checkout authorization for this exact email + session. */
     213    public static function find_checkout_auth( $email, $context_hash, $token ) {
     214        global $wpdb;
     215        if ( ! is_string( $token ) || ! preg_match( '/^[a-f0-9]{64}$/', $token ) || ! $context_hash ) { return null; }
     216        $table = self::table();
     217        return $wpdb->get_row( $wpdb->prepare(
     218            "SELECT id, email, token_hash FROM $table WHERE token_hash = %s AND flow = 'checkout' AND email = %s AND context_hash = %s AND expires_at > %d LIMIT 1",
     219            hash( 'sha256', $token ), $email, $context_hash, time()
     220        ) );
     221    }
     222
     223    /** Consumes a checkout authorization exactly once (atomic DELETE). */
     224    public static function consume_checkout_auth( $email, $context_hash, $token ) {
     225        global $wpdb;
     226        if ( ! is_string( $token ) || ! preg_match( '/^[a-f0-9]{64}$/', $token ) || ! $context_hash ) { return false; }
     227        $table = self::table();
     228        return (bool) $wpdb->query( $wpdb->prepare(
     229            "DELETE FROM $table WHERE token_hash = %s AND flow = 'checkout' AND email = %s AND context_hash = %s AND expires_at > %d",
     230            hash( 'sha256', $token ), $email, $context_hash, time()
     231        ) );
     232    }
     233
    164234    public static function can_request_otp( $email ) {
    165235        global $wpdb;
    … …  
    168238
    169239        $count = $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*) FROM $table WHERE email = %s AND action_type = 'otp_request' AND created_at > %s", $email, $twenty_four_hours_ago) );
    170         return $count < 5;
     240        $limit = class_exists( 'CEOVW_Options' ) ? (int) CEOVW_Options::get( 'rl_email_per_day' ) : 5;
     241        return $count < $limit;
    171242    }
    172243
    … …  
    193264        $table = $wpdb->prefix . 'ceovw_logs';
    194265
     266        $o = class_exists( 'CEOVW_Options' );
    195267        $defaults = [
    196             'send'   => ['limit' => 10, 'window' => HOUR_IN_SECONDS],
    197             'verify' => ['limit' => 30, 'window' => 15 * MINUTE_IN_SECONDS],
    198             'reset'  => ['limit' => 10, 'window' => 15 * MINUTE_IN_SECONDS],
     268            'send'   => ['limit' => $o ? (int) CEOVW_Options::get( 'rl_send_per_hour' ) : 10, 'window' => HOUR_IN_SECONDS],
     269            'verify' => ['limit' => $o ? (int) CEOVW_Options::get( 'rl_verify_per_15min' ) : 30, 'window' => 15 * MINUTE_IN_SECONDS],
     270            'reset'  => ['limit' => $o ? (int) CEOVW_Options::get( 'rl_reset_per_15min' ) : 10, 'window' => 15 * MINUTE_IN_SECONDS],
    199271        ];
    200272        if ( ! isset( $defaults[ $action ] ) ) { return false; }
    … …  
    242314    }
    243315
     316    /** Removes this email's rows for one flow only. */
     317    public static function clear_flow( $email, $flow ) {
     318        global $wpdb;
     319        $wpdb->delete( self::table(), ['email' => $email, 'flow' => $flow] );
     320    }
     321
    244322    public static function clear_otp( $email ) {
    245323        global $wpdb;
  • naimur-email-otp-for-woocommerce/trunk/includes/class-settings.php

    r3780003 r3780739  
    22if (!defined('ABSPATH')) { exit; }
    33
     4/**
     5 * Settings now live in CEOVW_Options and the "Naimur OTP" admin pages (includes/admin).
     6 * This class only keeps the legacy [naimur_email_otp_code] shortcode registered so content
     7 * written for versions before 2.1.2 does not show a raw shortcode.
     8 */
    49class CEOVW_Settings {
    510    public static function init(){
    6         add_action('admin_menu', [__CLASS__, 'menu']);
    7         add_action('admin_init', [__CLASS__, 'register']);
    811        add_shortcode('naimur_email_otp_code', [__CLASS__, 'shortcode_code']);
    9         add_action('wp_ajax_ceovw_preview_template', [__CLASS__, 'preview_template']);
    10     }
    11 
    12     public static function menu(){
    13         // Main Settings Page
    14         add_submenu_page('woocommerce', esc_html__('Email Verification', 'naimur-email-otp-for-woocommerce'), esc_html__('Email Verification', 'naimur-email-otp-for-woocommerce'), 'manage_woocommerce', 'naimur-email-otp-for-woocommerce-settings', [__CLASS__, 'render']);
    15        
    16         // Help, Docs & Support Page
    17         add_submenu_page('woocommerce', esc_html__('OTP Help & Docs', 'naimur-email-otp-for-woocommerce'), esc_html__('OTP Help & Docs', 'naimur-email-otp-for-woocommerce'), 'manage_woocommerce', 'naimur-email-otp-for-woocommerce-help', [__CLASS__, 'render_help_page']);
    18     }
    19 
    20     public static function register(){
    21         $sanitize_text = 'sanitize_text_field';
    22        
    23         // Email Template Settings
    24         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_email_subject', ['sanitize_callback' => $sanitize_text]);
    25         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_email_template', ['sanitize_callback' => $sanitize_text]);
    26        
    27         // Custom Email HTML with capability check (allows <style>, <html> tags for admins)
    28         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_custom_email_html', [
    29             'sanitize_callback' => function($html) {
    30                 return current_user_can('unfiltered_html') ? $html : wp_kses_post($html);
    31             }
    32         ]);
    33        
    34         // Design Settings
    35         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_brand_color', ['sanitize_callback' => 'sanitize_hex_color']);
    36        
    37         // OTP Logic Settings
    38         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_code_length', ['sanitize_callback' => 'absint']);
    39         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_resend_cooldown', ['sanitize_callback' => 'absint']);
    40        
    41         // Override Settings
    42         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_override_enabled', ['sanitize_callback' => 'rest_sanitize_boolean']);
    43         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_myaccount_page_id', ['sanitize_callback' => 'absint']);
    44        
    45         // Captcha Settings
    46         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_captcha_type', ['sanitize_callback' => $sanitize_text]);
    47         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_recaptcha_site_key', ['sanitize_callback' => $sanitize_text]);
    48         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_recaptcha_secret_key', ['sanitize_callback' => $sanitize_text]);
    49         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_turnstile_site_key', ['sanitize_callback' => $sanitize_text]);
    50         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_turnstile_secret_key', ['sanitize_callback' => $sanitize_text]);
    51        
    52         // Form Header Settings
    53         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_login_header_html', ['sanitize_callback' => 'wp_kses_post']);
    54         register_setting('naimur-email-otp-for-woocommerce', 'naimur-email-otp-for-woocommerce_register_header_html', ['sanitize_callback' => 'wp_kses_post']);
    55     }
    56 
    57     public static function render(){
    58         if ( ! current_user_can( 'manage_woocommerce' ) ) { return; }
    59         ?>
    60         <div class="wrap">
    61             <h1><?php /* translators: %s: plugin version */ echo esc_html( sprintf( __('Customer Email OTP Verification for WooCommerce (v%s)', 'naimur-email-otp-for-woocommerce'), NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION ) ); ?></h1>
    62             <form method="post" action="options.php">
    63                 <?php settings_fields('naimur-email-otp-for-woocommerce'); ?>
    64                 <table class="form-table" role="presentation">
    65                     <!-- Email Template Section -->
    66                     <tr>
    67                         <th><label for="naimur-email-otp-for-woocommerce_email_subject"><?php esc_html_e('Email Subject', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    68                         <td>
    69                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_email_subject" id="naimur-email-otp-for-woocommerce_email_subject" value="<?php echo esc_attr(get_option('naimur-email-otp-for-woocommerce_email_subject', 'Your Verification Code')); ?>" />
    70                         </td>
    71                     </tr>
    72                     <tr>
    73                         <th><label for="naimur-email-otp-for-woocommerce_email_template"><?php esc_html_e('Select Email Template', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    74                         <td>
    75                             <select name="naimur-email-otp-for-woocommerce_email_template" id="naimur-email-otp-for-woocommerce_email_template">
    76                                 <option value="1" <?php selected(get_option('naimur-email-otp-for-woocommerce_email_template', '1'), '1'); ?>><?php esc_html_e('Template 1 (Simple)', 'naimur-email-otp-for-woocommerce'); ?></option>
    77                                 <option value="2" <?php selected(get_option('naimur-email-otp-for-woocommerce_email_template'), '2'); ?>><?php esc_html_e('Template 2 (Modern)', 'naimur-email-otp-for-woocommerce'); ?></option>
    78                                 <option value="3" <?php selected(get_option('naimur-email-otp-for-woocommerce_email_template'), '3'); ?>><?php esc_html_e('Template 3 (Classic)', 'naimur-email-otp-for-woocommerce'); ?></option>
    79                                 <option value="custom" <?php selected(get_option('naimur-email-otp-for-woocommerce_email_template'), 'custom'); ?>><?php esc_html_e('Custom HTML', 'naimur-email-otp-for-woocommerce'); ?></option>
    80                             </select>
    81                         </td>
    82                     </tr>
    83                    
    84                     <!-- Live Gutenberg-like HTML Editor -->
    85                     <tr id="ceovw_custom_html_row" style="display: none;">
    86                         <th><label for="naimur-email-otp-for-woocommerce_custom_email_html"><?php esc_html_e('Write Custom HTML/CSS', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    87                         <td>
    88                             <textarea name="naimur-email-otp-for-woocommerce_custom_email_html" id="naimur-email-otp-for-woocommerce_custom_email_html" rows="12" class="large-text code" placeholder="<html><body><h1>Your Code: [naimur_email_otp_code]</h1></body></html>"><?php echo esc_textarea(get_option('naimur-email-otp-for-woocommerce_custom_email_html', '')); ?></textarea>
    89                             <p class="description"><?php esc_html_e('Use the shortcode [naimur_email_otp_code] where you want the 6-digit OTP to appear. Changes will preview live below.', 'naimur-email-otp-for-woocommerce'); ?></p>
    90                         </td>
    91                     </tr>
    92 
    93                     <tr>
    94                         <th><?php esc_html_e('Live Visual Preview', 'naimur-email-otp-for-woocommerce'); ?></th>
    95                         <td>
    96                             <div id="ceovw_template_preview_container" style="border: 1px solid #dcdcdc; border-radius: 8px; padding: 15px; background: #f9f9f9; max-width: 600px; min-height: 200px; box-shadow: inset 0 0 10px rgba(0,0,0,0.02);">
    97                                 <!-- Template Preview will be loaded here dynamically -->
    98                             </div>
    99                         </td>
    100                     </tr>
    101 
    102                     <!-- Design Section -->
    103                     <tr>
    104                         <th><label for="naimur-email-otp-for-woocommerce_brand_color"><?php esc_html_e('Brand color (HEX)', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    105                         <td><input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_brand_color" id="naimur-email-otp-for-woocommerce_brand_color" value="<?php echo esc_attr(get_option('naimur-email-otp-for-woocommerce_brand_color', '#3b82f6')); ?>" placeholder="#3b82f6" /></td>
    106                     </tr>
    107                    
    108                     <!-- Override UI Section -->
    109                     <tr>
    110                         <th><?php esc_html_e('Modern Login/Register UI', 'naimur-email-otp-for-woocommerce'); ?></th>
    111                         <td>
    112                             <label><input type="checkbox" name="naimur-email-otp-for-woocommerce_override_enabled" value="1" <?php checked(get_option('naimur-email-otp-for-woocommerce_override_enabled', 0), 1); ?> /> <?php esc_html_e('Enable custom modern design', 'naimur-email-otp-for-woocommerce'); ?></label><br/>
    113                             <div style="margin-top: 10px;">
    114                                 <label for="naimur-email-otp-for-woocommerce_myaccount_page_id" style="display:block; margin-bottom:5px; font-weight:500;"><?php esc_html_e('Select My Account page', 'naimur-email-otp-for-woocommerce'); ?></label>
    115                                 <?php wp_dropdown_pages(['name'=>'naimur-email-otp-for-woocommerce_myaccount_page_id', 'show_option_none'=>'— Select —', 'option_none_value'=>'0', 'selected'=>(int)get_option('naimur-email-otp-for-woocommerce_myaccount_page_id', 0)]); ?>
    116                             </div>
    117                         </td>
    118                     </tr>
    119                    
    120                     <!-- Captcha Section -->
    121                     <tr>
    122                         <th><label for="naimur-email-otp-for-woocommerce_captcha_type"><?php esc_html_e('Captcha Provider', 'naimur-email-otp-for-woocommerce'); ?></label></th>
    123                         <td>
    124                             <select name="naimur-email-otp-for-woocommerce_captcha_type" id="naimur-email-otp-for-woocommerce_captcha_type">
    125                                 <option value="none" <?php selected(get_option('naimur-email-otp-for-woocommerce_captcha_type'), 'none'); ?>><?php esc_html_e('None', 'naimur-email-otp-for-woocommerce'); ?></option>
    126                                 <option value="recaptcha" <?php selected(get_option('naimur-email-otp-for-woocommerce_captcha_type'), 'recaptcha'); ?>><?php esc_html_e('Google reCAPTCHA v2', 'naimur-email-otp-for-woocommerce'); ?></option>
    127                                 <option value="turnstile" <?php selected(get_option('naimur-email-otp-for-woocommerce_captcha_type'), 'turnstile'); ?>><?php esc_html_e('Cloudflare Turnstile', 'naimur-email-otp-for-woocommerce'); ?></option>
    128                             </select>
    129                         </td>
    130                     </tr>
    131                     <tr>
    132                         <th><?php esc_html_e('Google reCAPTCHA Keys', 'naimur-email-otp-for-woocommerce'); ?></th>
    133                         <td>
    134                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_recaptcha_site_key" placeholder="Site Key" value="<?php echo esc_attr( get_option('naimur-email-otp-for-woocommerce_recaptcha_site_key','') ); ?>" style="margin-bottom: 5px;" /><br/>
    135                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_recaptcha_secret_key" placeholder="Secret Key" value="<?php echo esc_attr( get_option('naimur-email-otp-for-woocommerce_recaptcha_secret_key','') ); ?>" />
    136                         </td>
    137                     </tr>
    138                     <tr>
    139                         <th><?php esc_html_e('Cloudflare Turnstile Keys', 'naimur-email-otp-for-woocommerce'); ?></th>
    140                         <td>
    141                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_turnstile_site_key" placeholder="Site Key" value="<?php echo esc_attr( get_option('naimur-email-otp-for-woocommerce_turnstile_site_key','') ); ?>" style="margin-bottom: 5px;" /><br/>
    142                             <input type="text" class="regular-text" name="naimur-email-otp-for-woocommerce_turnstile_secret_key" placeholder="Secret Key" value="<?php echo esc_attr( get_option('naimur-email-otp-for-woocommerce_turnstile_secret_key','') ); ?>" />
    143                         </td>
    144                     </tr>
    145 
    146                     <!-- Form Texts -->
    147                     <tr>
    148                         <th><label for="naimur-email-otp-for-woocommerce_login_header_html"><?php esc_html_e( 'Login header HTML', 'naimur-email-otp-for-woocommerce' ); ?></label></th>
    149                         <td><?php wp_editor( get_option( 'naimur-email-otp-for-woocommerce_login_header_html', '<p class="ceovw-muted">Please enter your details</p><h1>Welcome back</h1>' ), 'naimur-email-otp-for-woocommerce_login_header_html', [ 'textarea_rows' => 5 ] ); ?></td>
    150                     </tr>
    151                     <tr>
    152                         <th><label for="naimur-email-otp-for-woocommerce_register_header_html"><?php esc_html_e( 'Register header HTML', 'naimur-email-otp-for-woocommerce' ); ?></label></th>
    153                         <td><?php wp_editor( get_option( 'naimur-email-otp-for-woocommerce_register_header_html', '<h1>Create your account</h1>' ), 'naimur-email-otp-for-woocommerce_register_header_html', [ 'textarea_rows' => 5 ] ); ?></td>
    154                     </tr>
    155                 </table>
    156                 <?php submit_button(); ?>
    157             </form>
    158         </div>
    159 
    160         <script>
    161         jQuery(document).ready(function($) {
    162             var templateSelect = $('#naimur-email-otp-for-woocommerce_email_template');
    163             var customRow = $('#ceovw_custom_html_row');
    164             var customTextarea = $('#naimur-email-otp-for-woocommerce_custom_email_html');
    165             var previewContainer = $('#ceovw_template_preview_container');
    166 
    167             function renderIframe(htmlContent) {
    168                 var encodedStr = htmlContent.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    169                 previewContainer.html('<iframe srcdoc="' + encodedStr + '" style="width:100%; height:450px; border:1px solid #eaeaea; background:#ffffff; border-radius: 4px;"></iframe>');
    170             }
    171 
    172             function updatePreview() {
    173                 var selected_template = templateSelect.val();
    174                
    175                 if (selected_template === 'custom') {
    176                     // Show Custom HTML Textarea
    177                     customRow.show();
    178                     // Live Gutenberg-style rendering
    179                     var rawHtml = customTextarea.val();
    180                     var liveHtml = rawHtml.replace(/\[naimur_email_otp_code\]/g, '<strong style="color:var(--ceovw-brand, #3b82f6); font-size:24px;">123456</strong>');
    181                     renderIframe(liveHtml);
    182                 } else {
    183                     // Hide Custom HTML Textarea & Load Pre-defined Template via AJAX
    184                     customRow.hide();
    185                     previewContainer.html('<p style="color:#666; font-style:italic;">Loading template preview...</p>');
    186                    
    187                     $.post(ajaxurl, {
    188                         action: 'ceovw_preview_template',
    189                         template_id: selected_template,
    190                         _ajax_nonce: '<?php echo wp_create_nonce('ceovw_preview_nonce'); ?>'
    191                     }, function(response) {
    192                         renderIframe(response);
    193                     });
    194                 }
    195             }
    196            
    197             // Initialize
    198             updatePreview();
    199            
    200             // Trigger on Dropdown Change
    201             templateSelect.on('change', updatePreview);
    202            
    203             // Trigger Live Preview on Typing (The Gutenberg effect!)
    204             customTextarea.on('input', updatePreview);
    205         });
    206         </script>
    207     <?php }
    208 
    209     public static function render_help_page(){
    210         if ( ! current_user_can( 'manage_woocommerce' ) ) { return; }
    211         ?>
    212         <div class="wrap">
    213             <h1><?php esc_html_e('Help, Documentation & Support', 'naimur-email-otp-for-woocommerce'); ?></h1>
    214             <p><?php esc_html_e('Thank you for using Naimur Email OTP Verification for WooCommerce. Below you will find useful information to manage and troubleshoot the plugin.', 'naimur-email-otp-for-woocommerce'); ?></p>
    215            
    216             <div class="card" style="max-width: 800px; margin-top: 20px; padding: 20px;">
    217                 <h2 style="margin-top: 0;"><?php esc_html_e('How to Enable the Modern Login/Register UI?', 'naimur-email-otp-for-woocommerce'); ?></h2>
    218                 <p><?php esc_html_e('You do not need to use any shortcodes manually. The plugin comes with an auto-execution system.', 'naimur-email-otp-for-woocommerce'); ?></p>
    219                 <ol style="font-size: 14px; line-height: 1.6;">
    220                     <li><?php esc_html_e('Go to the plugin Settings tab.', 'naimur-email-otp-for-woocommerce'); ?></li>
    221                     <li><?php esc_html_e('Check the box "Enable custom modern design".', 'naimur-email-otp-for-woocommerce'); ?></li>
    222                     <li><?php esc_html_e('From the dropdown, select your WooCommerce "My Account" page.', 'naimur-email-otp-for-woocommerce'); ?></li>
    223                     <li><?php esc_html_e('Save changes.', 'naimur-email-otp-for-woocommerce'); ?></li>
    224                 </ol>
    225                 <p style="color: #646970;"><em><?php esc_html_e('That is it! The plugin will automatically override the default boring WooCommerce form with our modern, responsive, and secure UI design.', 'naimur-email-otp-for-woocommerce'); ?></em></p>
    226             </div>
    227 
    228             <div class="card" style="max-width: 800px; margin-top: 20px; padding: 20px;">
    229                 <h2 style="margin-top: 0;"><?php esc_html_e('Need Support or Found a Bug?', 'naimur-email-otp-for-woocommerce'); ?></h2>
    230                 <p><?php esc_html_e('If you are facing any issues, conflicts, or have questions about how to set up the plugin, please visit our official support forum on WordPress.org. Explain your issue in detail, and we will try to resolve it as soon as possible.', 'naimur-email-otp-for-woocommerce'); ?></p>
    231                 <a href="https://wordpress.org/support/plugin/naimur-email-otp-for-woocommerce/" target="_blank" class="button button-primary"><?php esc_html_e('Get Support on WordPress.org', 'naimur-email-otp-for-woocommerce'); ?></a>
    232             </div>
    233 
    234             <div class="card" style="max-width: 800px; margin-top: 20px; padding: 20px;">
    235                 <h2 style="margin-top: 0;"><?php esc_html_e('Author Personal Consultation', 'naimur-email-otp-for-woocommerce'); ?></h2>
    236                 <p><?php esc_html_e('Need a custom feature, specialized WordPress & WooCommerce development, or one-on-one consultation? You can reach out to the author (Naimur Rahman Nahid) directly through the personal contact form.', 'naimur-email-otp-for-woocommerce'); ?></p>
    237                 <a href="https://naimurrahmannahid.com/contact/" target="_blank" class="button"><?php esc_html_e('Contact for Consultation', 'naimur-email-otp-for-woocommerce'); ?></a>
    238             </div>
    239         </div>
    240         <?php
    24112    }
    24213
    24314    public static function shortcode_code($atts, $content = ''){ return $content; }
    244    
    245     // AJAX Handler for Pre-defined Template Preview
    246     public static function preview_template(){
    247         check_ajax_referer('ceovw_preview_nonce');
    248         if (!current_user_can('manage_woocommerce')) wp_die('-');
    249 
    250         // Only the bundled templates can be previewed. The ID must exactly match the allowlist
    251         // before it is used in a file path; anything else (missing, arrays, path characters) is rejected.
    252         $allowed_templates = ['1', '2', '3'];
    253         $template = ( isset($_POST['template_id']) && is_string($_POST['template_id']) ) ? wp_unslash($_POST['template_id']) : '';
    254         if ( ! in_array( $template, $allowed_templates, true ) ) {
    255             wp_die( esc_html__( 'Invalid template.', 'naimur-email-otp-for-woocommerce' ), '', array( 'response' => 400 ) );
    256         }
    257 
    258         $code = '<strong style="color:#3b82f6; font-size:24px;">123456</strong>';
    259         $file = NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH . 'templates/email-template-' . $template . '.php';
    260         if (file_exists($file)) {
    261             // Read file and replace shortcode for preview
    262             $content = file_get_contents($file);
    263             echo str_replace('[naimur_email_otp_code]', $code, $content);
    264         } else {
    265             echo '<p>Template file not found.</p>';
    266         }
    267         wp_die();
    268     }
    26915}
    27016CEOVW_Settings::init();
  • naimur-email-otp-for-woocommerce/trunk/includes/helpers.php

    r3780003 r3780739  
    2323    // token_hash: SHA-256 of the single-use password reset token
    2424    // expires_at: unix timestamp (UTC) after which the row is invalid
     25    // sent_at:    unix timestamp of the last email for this row (server-side resend cooldown, 2.1.2)
     26    // context_hash: HMAC of the WooCommerce session a checkout verification is bound to (2.1.2)
    2527    $sql2 = "CREATE TABLE {$wpdb->prefix}ceovw_otps (
    2628        id mediumint(9) NOT NULL AUTO_INCREMENT,
    … …  
    3234        token_hash char(64) DEFAULT NULL,
    3335        expires_at bigint(20) unsigned NOT NULL DEFAULT 0,
     36        sent_at bigint(20) unsigned NOT NULL DEFAULT 0,
     37        context_hash char(64) DEFAULT NULL,
    3438        created_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
    3539        PRIMARY KEY  (id),
    3640        KEY email (email),
    3741        KEY token_hash (token_hash),
    38         KEY expires_at (expires_at)
     42        KEY expires_at (expires_at),
     43        KEY context_hash (context_hash)
    3944    ) $charset_collate;";
    4045
    … …  
    4550
    4651/**
    47  * Confirms the OTP table has the columns required by the 2.1.1 authorization logic.
     52 * Confirms the OTP table has the columns required by the current authorization logic.
    4853 */
    4954function ceovw_schema_is_valid() {
    … …  
    5459    if ( empty( $columns ) ) { return false; }
    5560
    56     foreach ( ['attempts', 'token_hash', 'expires_at', 'otp'] as $column ) {
     61    foreach ( ['attempts', 'token_hash', 'expires_at', 'otp', 'sent_at', 'context_hash'] as $column ) {
    5762        if ( ! isset( $columns[ $column ] ) ) { return false; }
    5863    }
    … …  
    8489
    8590/**
    86  * 2.1.0 -> 2.1.1 migration. Saves the new DB version only when every required step succeeds.
     91 * Database/settings migration (2.1.0 / 2.1.1 -> current). Saves the new DB version only when every required step succeeds.
    8792 */
    8893function ceovw_run_upgrade() {
    … …  
    122127    // 7. Ensure the cleanup event exists.
    123128    ceovw_schedule_cleanup();
     129
     130    // 7b. 2.1.2: carry the pre-2.1.2 email template settings (custom HTML / subject) into the new template store.
     131    // Deferred to init when needed: the default templates are translated, and translations must not load earlier.
     132    if ( class_exists( 'CEOVW_Templates' ) ) {
     133        if ( did_action( 'init' ) ) { CEOVW_Templates::migrate_legacy(); }
     134        else { add_action( 'init', array( 'CEOVW_Templates', 'migrate_legacy' ), 1 ); }
     135    }
    124136
    125137    // 8. Mark the migration as complete.
    … …  
    230242    $cutoff = wp_date( 'Y-m-d H:i:s', time() - 2 * DAY_IN_SECONDS );
    231243    $wpdb->query( $wpdb->prepare( "DELETE FROM $logs WHERE created_at < %s", $cutoff ) );
     244
     245    // Activity log retention (General settings).
     246    if ( class_exists( 'CEOVW_Options' ) ) {
     247        $keep_after = time() - DAY_IN_SECONDS * (int) CEOVW_Options::get( 'log_retention_days' );
     248        $activity   = get_option( 'ceovw_activity_logs', array() );
     249        if ( is_array( $activity ) && $activity ) {
     250            $kept = array_values( array_filter( $activity, function ( $e ) use ( $keep_after ) { return is_array( $e ) && (int) ( $e['time'] ?? 0 ) >= $keep_after; } ) );
     251            if ( count( $kept ) !== count( $activity ) ) { update_option( 'ceovw_activity_logs', $kept, false ); }
     252        }
     253    }
     254    update_option( 'ceovw_last_cleanup', time(), false );
    232255}
    233256
    … …  
    235258 * Seconds an emailed OTP (and its temporary registration data) stays valid.
    236259 */
    237 function ceovw_otp_ttl() {
    238     return max( 60, (int) apply_filters( 'ceovw_otp_expiry', 300 ) );
     260function ceovw_otp_ttl( $flow = 'register' ) {
     261    return class_exists( 'CEOVW_Options' ) ? CEOVW_Options::ttl( $flow ) : max( 60, (int) apply_filters( 'ceovw_otp_expiry', 300, $flow ) );
    239262}
    240263
  • naimur-email-otp-for-woocommerce/trunk/languages/naimur-email-otp-for-woocommerce.pot

    r3587442 r3780739  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Naimur Email OTP Verification for WooCommerce 1.2.0\n"
     5"Project-Id-Version: Naimur Email OTP Verification for WooCommerce 2.1.2\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/naimur-email-otp-for-woocommerce\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-06-22T09:54:30+06:00\n"
     12"POT-Creation-Date: 2026-11-03T00:02:16+06:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    14 "X-Generator: WP-CLI 2.10.0\n"
     14"X-Generator: GNU xgettext\n"
    1515"X-Domain: naimur-email-otp-for-woocommerce\n"
    1616
    … …  
    2020
    2121#. Description of the plugin
    22 msgid "Verify WooCommerce customer emails using a 6-digit OTP before account creation. Modern UI with Google reCAPTCHA & Cloudflare Turnstile protection."
     22msgid "Verify WooCommerce customer emails with a 6-digit OTP for registration, password reset and (optionally) checkout, with editable email templates and a modern admin."
    2323msgstr ""
    2424
    … …  
    2727msgstr ""
    2828
    29 #: naimur-email-otp-for-woocommerce.php:68
    30 #: includes/class-settings.php:13
    31 #: includes/class-settings.php:13
    32 msgid "Email Verification"
    33 msgstr ""
    34 
    35 #: naimur-email-otp-for-woocommerce.php:64
     29#: includes/admin/class-admin.php:27
     30msgid "Overview"
     31msgstr ""
     32
     33#: includes/admin/class-admin.php:27
     34msgid "See what is protected, what still needs setting up, and what happened recently."
     35msgstr ""
     36
     37#: includes/admin/class-admin.php:28
     38msgid "Choose where customers must confirm their email address with a 6-digit code."
     39msgstr ""
     40
     41#: includes/admin/class-admin.php:28 includes/admin/class-admin.php:168
     42#: includes/admin/class-admin.php:169 includes/admin/class-admin.php:170
     43#: includes/admin/class-admin.php:171 includes/admin/class-admin.php:172
     44#: includes/admin/class-admin.php:173 includes/admin/class-admin.php:174
     45#: includes/admin/class-admin.php:175 includes/admin/views/general.php:10
     46#: includes/admin/views/help.php:10 includes/admin/views/security.php:21
     47msgid "Verification"
     48msgstr ""
     49
     50#: includes/admin/class-admin.php:29
     51msgid "Change the wording and design of the emails that carry the verification codes."
     52msgstr ""
     53
     54#: includes/admin/class-admin.php:29 includes/admin/class-admin.php:176
     55#: includes/admin/views/help.php:65
     56msgid "Email Templates"
     57msgstr ""
     58
     59#: includes/admin/class-admin.php:30
     60msgid "Limits that stop abuse, such as too many codes or too many guesses."
     61msgstr ""
     62
     63#: includes/admin/class-admin.php:30 includes/admin/class-admin.php:177
     64#: includes/admin/class-admin.php:178 includes/admin/views/help.php:75
     65msgid "Security"
     66msgstr ""
     67
     68#: includes/admin/class-admin.php:31 includes/admin/class-admin.php:179
     69#: includes/admin/class-admin.php:180 includes/admin/class-admin.php:181
     70#: includes/admin/views/help.php:21
     71msgid "Activity & Diagnostics"
     72msgstr ""
     73
     74#: includes/admin/class-admin.php:31
     75msgid "Send a test email, review recent emails and check that everything is working."
     76msgstr ""
     77
     78#: includes/admin/class-admin.php:32 includes/admin/class-admin.php:182
     79#: includes/admin/class-admin.php:183 includes/admin/class-admin.php:184
     80#: includes/admin/class-admin.php:185 includes/admin/views/help.php:19
     81msgid "General Settings"
     82msgstr ""
     83
     84#: includes/admin/class-admin.php:32
     85msgid "Sender details, branding and what happens to your data when the plugin is deleted."
     86msgstr ""
     87
     88#: includes/admin/class-admin.php:33
     89msgid "Help & Documentation"
     90msgstr ""
     91
     92#: includes/admin/class-admin.php:33
     93msgid "Step-by-step guides and answers to common questions."
     94msgstr ""
     95
     96#: includes/admin/class-admin.php:66 includes/admin/class-admin.php:215
     97msgid "Naimur OTP"
     98msgstr ""
     99
     100#: includes/admin/class-admin.php:123
     101msgid "Settings saved."
     102msgstr ""
     103
     104#: includes/admin/class-admin.php:124
     105msgid "All changes saved"
     106msgstr ""
     107
     108#: includes/admin/class-admin.php:125
     109msgid "Saving…"
     110msgstr ""
     111
     112#: includes/admin/class-admin.php:126
     113msgid "Something went wrong. Please try again."
     114msgstr ""
     115
     116#: includes/admin/class-admin.php:127
     117msgid "Saved. Some values were adjusted to the allowed range."
     118msgstr ""
     119
     120#: includes/admin/class-admin.php:128
     121msgid "Please correct the highlighted fields."
     122msgstr ""
     123
     124#. translators: 1: minimum, 2: maximum
     125#: includes/admin/class-admin.php:130
     126#, php-format
     127msgid "Enter a whole number from %1$d to %2$d."
     128msgstr ""
     129
     130#: includes/admin/class-admin.php:131
     131msgid "Enter a valid email address, for example name@yourstore.com."
     132msgstr ""
     133
     134#: includes/admin/class-admin.php:132
     135msgid "Enter a full web address starting with https://"
     136msgstr ""
     137
     138#: includes/admin/class-admin.php:133
     139msgid "Confirm"
     140msgstr ""
     141
     142#: includes/admin/class-admin.php:134 includes/class-checkout.php:256
     143msgid "Cancel"
     144msgstr ""
     145
     146#: includes/admin/class-admin.php:135
     147msgid "Placeholder inserted."
     148msgstr ""
     149
     150#: includes/admin/class-admin.php:136
     151msgid "Test email sent."
     152msgstr ""
     153
     154#: includes/admin/class-admin.php:137
     155msgid "Choose logo"
     156msgstr ""
     157
     158#: includes/admin/class-admin.php:138
     159msgid "Use this image"
     160msgstr ""
     161
     162#: includes/admin/class-admin.php:139 includes/admin/views/general.php:30
     163msgid "No logo"
     164msgstr ""
     165
     166#: includes/admin/class-admin.php:140 includes/admin/views/help.php:156
     167msgid "No help topics match your search."
     168msgstr ""
     169
     170#: includes/admin/class-admin.php:141
     171msgid "No matching settings."
     172msgstr ""
     173
     174#: includes/admin/class-admin.php:142
     175msgid "Unsaved changes"
     176msgstr ""
     177
     178#: includes/admin/class-admin.php:143
     179msgid "You have unsaved changes. Leave this page?"
     180msgstr ""
     181
     182#: includes/admin/class-admin.php:144
     183msgid "Changes discarded."
     184msgstr ""
     185
     186#: includes/admin/class-admin.php:145
     187msgid "Customized template"
     188msgstr ""
     189
     190#: includes/admin/class-admin.php:146
     191msgid "Built-in default template"
     192msgstr ""
     193
     194#: includes/admin/class-admin.php:147 includes/admin/views/templates.php:19
     195msgid "Customized"
     196msgstr ""
     197
     198#: includes/admin/class-admin.php:148 includes/admin/views/templates.php:19
     199msgid "Default"
     200msgstr ""
     201
     202#: includes/admin/class-admin.php:149
     203msgid "You have unsaved changes. Discard them and switch template?"
     204msgstr ""
     205
     206#: includes/admin/class-admin.php:150
     207msgid "Template saved."
     208msgstr ""
     209
     210#: includes/admin/class-admin.php:151
     211msgid "Template restored to the default."
     212msgstr ""
     213
     214#: includes/admin/class-admin.php:152
     215msgid "Just now"
     216msgstr ""
     217
     218#: includes/admin/class-admin.php:153
     219msgid "Log cleared."
     220msgstr ""
     221
     222#: includes/admin/class-admin.php:154 includes/admin/views/dashboard.php:10
     223#: includes/admin/views/templates.php:13
     224#: includes/admin/views/verification.php:10
     225#: includes/admin/views/verification.php:64
     226msgid "On"
     227msgstr ""
     228
     229#: includes/admin/class-admin.php:155 includes/admin/views/dashboard.php:11
     230#: includes/admin/views/templates.php:13
     231#: includes/admin/views/verification.php:11
     232#: includes/admin/views/verification.php:64
     233msgid "Off"
     234msgstr ""
     235
     236#: includes/admin/class-admin.php:156 includes/admin/views/verification.php:129
     237msgid "Guests and logged-in customers"
     238msgstr ""
     239
     240#: includes/admin/class-admin.php:157 includes/admin/views/verification.php:129
     241msgid "Guest customers only"
     242msgstr ""
     243
     244#. translators: %d: number of matching help topics
     245#: includes/admin/class-admin.php:159
     246#, php-format
     247msgid "%d matching topics"
     248msgstr ""
     249
     250#: includes/admin/class-admin.php:168
     251msgid "Customer login and registration forms"
     252msgstr ""
     253
     254#: includes/admin/class-admin.php:169 includes/admin/class-admin.php:231
     255#: includes/class-templates.php:14
     256msgid "Registration OTP"
     257msgstr ""
     258
     259#: includes/admin/class-admin.php:170 includes/admin/class-admin.php:232
     260#: includes/class-templates.php:15
     261msgid "Password Reset OTP"
     262msgstr ""
     263
     264#: includes/admin/class-admin.php:171 includes/admin/class-admin.php:233
     265#: includes/class-templates.php:16
     266msgid "Checkout OTP"
     267msgstr ""
     268
     269#: includes/admin/class-admin.php:172 includes/admin/views/verification.php:33
     270msgid "Code Expiration Time"
     271msgstr ""
     272
     273#: includes/admin/class-admin.php:173 includes/admin/views/verification.php:34
     274msgid "Wait Before Resending"
     275msgstr ""
     276
     277#: includes/admin/class-admin.php:174 includes/admin/views/verification.php:35
     278msgid "Wrong Code Attempts Allowed"
     279msgstr ""
     280
     281#: includes/admin/class-admin.php:175 includes/admin/views/verification.php:112
     282msgid "Also verify logged-in customers"
     283msgstr ""
     284
     285#: includes/admin/class-admin.php:176
     286msgid "Email templates"
     287msgstr ""
     288
     289#: includes/admin/class-admin.php:177 includes/admin/views/security.php:7
     290msgid "Rate limits"
     291msgstr ""
     292
     293#: includes/admin/class-admin.php:178
     294msgid "Visitor IP address and proxies"
     295msgstr ""
     296
     297#: includes/admin/class-admin.php:179 includes/admin/views/dashboard.php:51
     298#: includes/admin/views/diagnostics.php:31 includes/admin/views/help.php:86
     299msgid "Send a test email"
     300msgstr ""
     301
     302#: includes/admin/class-admin.php:180 includes/admin/views/diagnostics.php:48
     303msgid "Email log"
     304msgstr ""
     305
     306#: includes/admin/class-admin.php:181 includes/admin/views/diagnostics.php:99
     307msgid "Keep activity history for"
     308msgstr ""
     309
     310#: includes/admin/class-admin.php:182
     311msgid "Sender name and email"
     312msgstr ""
     313
     314#: includes/admin/class-admin.php:183 includes/admin/views/general.php:28
     315msgid "Store logo for emails"
     316msgstr ""
     317
     318#: includes/admin/class-admin.php:184 includes/admin/views/general.php:42
     319msgid "Brand colour"
     320msgstr ""
     321
     322#: includes/admin/class-admin.php:185
     323msgid "Delete data when the plugin is deleted"
     324msgstr ""
     325
     326#: includes/admin/class-admin.php:198
     327msgid "You do not have permission to access this page."
     328msgstr ""
     329
     330#: includes/admin/class-admin.php:203
     331msgid "Naimur Email OTP"
     332msgstr ""
     333
     334#. translators: %s: plugin version
     335#: includes/admin/class-admin.php:203
     336#, php-format
     337msgid "Version %s"
     338msgstr ""
     339
     340#: includes/admin/class-admin.php:204
     341msgid "Find a setting"
     342msgstr ""
     343
     344#: includes/admin/class-admin.php:205
     345msgid "Find a setting…"
     346msgstr ""
     347
     348#: includes/admin/class-admin.php:205
     349msgid "Type to search, then use the arrow keys to choose a result."
     350msgstr ""
     351
     352#: includes/admin/class-admin.php:207
     353msgid "Naimur OTP sections"
     354msgstr ""
     355
     356#: includes/admin/class-admin.php:214
     357msgid "Breadcrumb"
     358msgstr ""
     359
     360#. translators: 1: minimum, 2: maximum
     361#: includes/admin/class-admin.php:276
     362#, php-format
     363msgid "Allowed range: %1$d to %2$d."
     364msgstr ""
     365
     366#: includes/admin/class-admin.php:300
     367msgid "Learn more"
     368msgstr ""
     369
     370#: includes/admin/class-admin.php:305 includes/admin/views/templates.php:67
     371msgid "Discard changes"
     372msgstr ""
     373
     374#: includes/admin/class-admin.php:305
     375msgid "Save changes"
     376msgstr ""
     377
     378#: includes/admin/class-admin.php:384
     379msgid "Never"
     380msgstr ""
     381
     382#: includes/admin/class-admin.php:386
     383#, php-format
     384msgid "%s ago"
     385msgstr ""
     386
     387#. translators: %s: human time difference
     388#: includes/admin/class-admin.php:386
     389#, php-format
     390msgid "in %s"
     391msgstr ""
     392
     393#: includes/admin/class-admin.php:392
     394msgid "Your session expired. Please reload the page."
     395msgstr ""
     396
     397#: includes/admin/class-admin.php:393 includes/admin/class-admin.php:408
     398msgid "You do not have permission to do this."
     399msgstr ""
     400
     401#: includes/admin/class-admin.php:407
     402msgid "Unknown settings section."
     403msgstr ""
     404
     405#: includes/admin/class-admin.php:415
     406msgid "Please enter a valid sender email address."
     407msgstr ""
     408
     409#: includes/admin/class-admin.php:450 naimur-email-otp-for-woocommerce.php:116
     410msgid "Please enter a valid email address."
     411msgstr ""
     412
     413#: includes/admin/class-admin.php:458
     414msgid "WordPress could not send the email. Check your mail or SMTP configuration."
     415msgstr ""
     416
     417#. translators: %s: recipient email address
     418#: includes/admin/class-admin.php:459
     419#, php-format
     420msgid "Test email sent to %s. Check the inbox (and the spam folder)."
     421msgstr ""
     422
     423#: includes/admin/class-admin.php:467
     424msgid "Unknown log."
     425msgstr ""
     426
     427#: includes/admin/class-admin.php:468
     428msgid "Logs cleared."
     429msgstr ""
     430
     431#: includes/admin/class-admin.php:473
     432msgid "The database upgrade has not completed yet."
     433msgstr ""
     434
     435#: includes/admin/class-admin.php:476
     436msgid "Cleanup finished."
     437msgstr ""
     438
     439#: includes/admin/views/dashboard.php:12 includes/admin/views/templates.php:13
     440#: includes/admin/views/verification.php:12
     441msgid "Needs setup"
     442msgstr ""
     443
     444#: includes/admin/views/dashboard.php:17
     445msgid "Registration Verification"
     446msgstr ""
     447
     448#: includes/admin/views/dashboard.php:19
     449msgid "New accounts are created without an email code."
     450msgstr ""
     451
     452#: includes/admin/views/dashboard.php:19
     453msgid "New customers confirm their email before their account is created."
     454msgstr ""
     455
     456#: includes/admin/views/dashboard.php:19
     457msgid "Turned on, but the customer login and registration forms are off, so no code is asked for yet."
     458msgstr ""
     459
     460#: includes/admin/views/dashboard.php:21
     461msgid "Registration settings"
     462msgstr ""
     463
     464#: includes/admin/views/dashboard.php:25
     465msgid "Password Reset Verification"
     466msgstr ""
     467
     468#: includes/admin/views/dashboard.php:27
     469msgid "Customers reset their password with an emailed code."
     470msgstr ""
     471
     472#: includes/admin/views/dashboard.php:27
     473msgid "The standard WooCommerce reset link is used."
     474msgstr ""
     475
     476#: includes/admin/views/dashboard.php:27
     477msgid "Turned on, but the customer login and registration forms are off, so the standard WooCommerce reset link is used."
     478msgstr ""
     479
     480#: includes/admin/views/dashboard.php:29
     481msgid "Password reset settings"
     482msgstr ""
     483
     484#: includes/admin/views/dashboard.php:33
     485msgid "Checkout Verification"
     486msgstr ""
     487
     488#: includes/admin/views/dashboard.php:35
     489msgid "Optional. Orders are placed without an email code."
     490msgstr ""
     491
     492#: includes/admin/views/dashboard.php:35
     493msgid "Orders are created only after the billing email is confirmed."
     494msgstr ""
     495
     496#: includes/admin/views/dashboard.php:37
     497msgid "Checkout settings"
     498msgstr ""
     499
     500#: includes/admin/views/dashboard.php:41
     501msgid "Email Delivery"
     502msgstr ""
     503
     504#: includes/admin/views/dashboard.php:43
     505msgid "Working"
     506msgstr ""
     507
     508#: includes/admin/views/dashboard.php:44
     509msgid "Not tested yet"
     510msgstr ""
     511
     512#: includes/admin/views/dashboard.php:45
     513msgid "Problem found"
     514msgstr ""
     515
     516#: includes/admin/views/dashboard.php:47
     517msgid "The most recent email could not be sent. Check your mail or SMTP setup."
     518msgstr ""
     519
     520#. translators: %s: relative time, e.g. "5 mins ago"
     521#: includes/admin/views/dashboard.php:48
     522#, php-format
     523msgid "Last test email sent %s."
     524msgstr ""
     525
     526#: includes/admin/views/dashboard.php:49
     527msgid "Emails are being handed to your mail system."
     528msgstr ""
     529
     530#: includes/admin/views/dashboard.php:49
     531msgid "Send a test email to make sure codes reach your customers."
     532msgstr ""
     533
     534#: includes/admin/views/dashboard.php:55
     535msgid "Security Health"
     536msgstr ""
     537
     538#: includes/admin/views/dashboard.php:56
     539#: includes/admin/views/diagnostics.php:82
     540msgid "Healthy"
     541msgstr ""
     542
     543#: includes/admin/views/dashboard.php:56
     544msgid "Needs attention"
     545msgstr ""
     546
     547#: includes/admin/views/dashboard.php:57
     548msgid "A database update is still running. Verification is paused until it finishes."
     549msgstr ""
     550
     551#: includes/admin/views/dashboard.php:58
     552msgid "Rate limits are active and expired codes are removed automatically."
     553msgstr ""
     554
     555#: includes/admin/views/dashboard.php:58
     556msgid "The automatic cleanup of expired codes is not running on time."
     557msgstr ""
     558
     559#: includes/admin/views/dashboard.php:60 includes/admin/views/help.php:69
     560msgid "Security settings"
     561msgstr ""
     562
     563#: includes/admin/views/dashboard.php:60
     564msgid "View diagnostics"
     565msgstr ""
     566
     567#: includes/admin/views/dashboard.php:65
     568msgid "Registration and password reset codes appear inside these forms on your My Account page."
     569msgstr ""
     570
     571#: includes/admin/views/dashboard.php:65
     572msgid "Turn on the customer login and registration forms"
     573msgstr ""
     574
     575#: includes/admin/views/dashboard.php:66
     576msgid "Emails from your own domain are less likely to land in spam."
     577msgstr ""
     578
     579#: includes/admin/views/dashboard.php:66
     580msgid "Set the sender name and email address"
     581msgstr ""
     582
     583#: includes/admin/views/dashboard.php:67
     584msgid "Add your store logo to the emails"
     585msgstr ""
     586
     587#: includes/admin/views/dashboard.php:67
     588msgid "Customers trust emails that look like your store."
     589msgstr ""
     590
     591#: includes/admin/views/dashboard.php:68
     592msgid "Confirms that your site can deliver the codes."
     593msgstr ""
     594
     595#: includes/admin/views/dashboard.php:68
     596msgid "Send a test email and check that it arrives"
     597msgstr ""
     598
     599#: includes/admin/views/dashboard.php:69
     600msgid "Place a test order yourself first."
     601msgstr ""
     602
     603#: includes/admin/views/dashboard.php:69
     604msgid "Turn on checkout verification"
     605msgstr ""
     606
     607#: includes/admin/views/dashboard.php:79
     608msgid "Welcome to Naimur Email OTP"
     609msgstr ""
     610
     611#: includes/admin/views/dashboard.php:80
     612msgid "Stop fake accounts and protect your customers by asking for a one-time code sent by email when they register, reset a password or check out."
     613msgstr ""
     614
     615#: includes/admin/views/dashboard.php:85
     616msgid "Everything looks good"
     617msgstr ""
     618
     619#. translators: %d: number of items that need attention
     620#: includes/admin/views/dashboard.php:88
     621#, php-format
     622msgid "%d item needs attention"
     623msgid_plural "%d items need attention"
     624msgstr[0] ""
     625msgstr[1] ""
     626
     627#. translators: %s: plugin version
     628#: includes/admin/views/dashboard.php:91
     629#, php-format
     630msgid "Plugin version %s"
     631msgstr ""
     632
     633#: includes/admin/views/dashboard.php:95 includes/admin/views/dashboard.php:147
     634#: includes/admin/views/dashboard.php:149
     635#: includes/admin/views/diagnostics.php:53
     636#: includes/admin/views/diagnostics.php:55
     637msgid "Status"
     638msgstr ""
     639
     640#. translators: 1: completed steps, 2: total steps
     641#: includes/admin/views/dashboard.php:109
     642#, php-format
     643msgid "%1$d of %2$d done"
     644msgstr ""
     645
     646#: includes/admin/views/dashboard.php:109
     647msgid "Setup checklist"
     648msgstr ""
     649
     650#: includes/admin/views/dashboard.php:110
     651msgid "Setup progress"
     652msgstr ""
     653
     654#: includes/admin/views/dashboard.php:117
     655msgid "Optional"
     656msgstr ""
     657
     658#: includes/admin/views/dashboard.php:120
     659msgid "Done"
     660msgstr ""
     661
     662#: includes/admin/views/dashboard.php:120
     663msgid "Not done yet"
     664msgstr ""
     665
     666#: includes/admin/views/dashboard.php:124
     667msgid "Sender and logo settings can be changed by a site administrator."
     668msgstr ""
     669
     670#: includes/admin/views/dashboard.php:127
     671msgid "Quick actions"
     672msgstr ""
     673
     674#: includes/admin/views/dashboard.php:129
     675msgid "Configure Checkout OTP"
     676msgstr ""
     677
     678#: includes/admin/views/dashboard.php:129
     679msgid "Turn it on or off and choose who is affected."
     680msgstr ""
     681
     682#: includes/admin/views/dashboard.php:131
     683msgid "Change the subject, text and design."
     684msgstr ""
     685
     686#: includes/admin/views/dashboard.php:131
     687msgid "Edit Email Template"
     688msgstr ""
     689
     690#: includes/admin/views/dashboard.php:133
     691msgid "Check that codes reach the inbox."
     692msgstr ""
     693
     694#: includes/admin/views/dashboard.php:133
     695msgid "Send a Test Email"
     696msgstr ""
     697
     698#: includes/admin/views/dashboard.php:135
     699msgid "Limits for codes and wrong guesses."
     700msgstr ""
     701
     702#: includes/admin/views/dashboard.php:135
     703msgid "Open Security Settings"
     704msgstr ""
     705
     706#: includes/admin/views/dashboard.php:137
     707msgid "Step-by-step guides and answers."
     708msgstr ""
     709
     710#: includes/admin/views/dashboard.php:137
     711msgid "View Documentation"
     712msgstr ""
     713
     714#: includes/admin/views/dashboard.php:143
     715msgid "Recent emails"
     716msgstr ""
     717
     718#: includes/admin/views/dashboard.php:143
     719msgid "View all"
     720msgstr ""
     721
     722#: includes/admin/views/dashboard.php:145
     723msgid "No verification emails yet. They appear here once customers start receiving codes."
     724msgstr ""
     725
     726#: includes/admin/views/dashboard.php:147
     727#: includes/admin/views/dashboard.php:149
     728#: includes/admin/views/diagnostics.php:35
     729#: includes/admin/views/diagnostics.php:53
     730#: includes/admin/views/diagnostics.php:55
     731#: includes/admin/views/diagnostics.php:66
     732#: includes/admin/views/diagnostics.php:68
     733msgid "Email"
     734msgstr ""
     735
     736#: includes/admin/views/dashboard.php:147
     737#: includes/admin/views/dashboard.php:149
     738#: includes/admin/views/diagnostics.php:66
     739#: includes/admin/views/diagnostics.php:68
     740msgid "Event"
     741msgstr ""
     742
     743#: includes/admin/views/dashboard.php:147
     744#: includes/admin/views/dashboard.php:149
     745#: includes/admin/views/diagnostics.php:53
     746#: includes/admin/views/diagnostics.php:55
     747#: includes/admin/views/diagnostics.php:66
     748#: includes/admin/views/diagnostics.php:68
     749msgid "Time"
     750msgstr ""
     751
     752#: includes/admin/views/diagnostics.php:13
     753msgid "WooCommerce is not active. This plugin needs WooCommerce."
     754msgstr ""
     755
     756#: includes/admin/views/diagnostics.php:14
     757msgid "The database update has not finished. Verification is paused until it does; reload this page to try again."
     758msgstr ""
     759
     760#: includes/admin/views/diagnostics.php:15
     761msgid "No SMTP plugin found. Emails are sent with the basic server mail function, which is often marked as spam. An SMTP plugin is recommended."
     762msgstr ""
     763
     764#: includes/admin/views/diagnostics.php:16
     765msgid "WP-Cron is turned off (DISABLE_WP_CRON). Make sure a server cron job runs wp-cron.php, or expired codes are only removed when you click \"Run cleanup now\"."
     766msgstr ""
     767
     768#: includes/admin/views/diagnostics.php:17
     769msgid "Checkout verification is on, but the checkout page type was not recognised."
     770msgstr ""
     771
     772#: includes/admin/views/diagnostics.php:20
     773msgid "Checkout email verified"
     774msgstr ""
     775
     776#: includes/admin/views/diagnostics.php:21
     777msgid "Order placed with verified email"
     778msgstr ""
     779
     780#: includes/admin/views/diagnostics.php:22
     781msgid "Registered with verified email"
     782msgstr ""
     783
     784#: includes/admin/views/diagnostics.php:23
     785msgid "Password reset code verified"
     786msgstr ""
     787
     788#: includes/admin/views/diagnostics.php:27
     789msgid "Things to check"
     790msgstr ""
     791
     792#: includes/admin/views/diagnostics.php:31
     793msgid "Emails not arriving?"
     794msgstr ""
     795
     796#: includes/admin/views/diagnostics.php:32
     797msgid "Sends one of your saved emails with sample data (code 123456) so you can check that it arrives and looks right."
     798msgstr ""
     799
     800#: includes/admin/views/diagnostics.php:39
     801msgid "Send to"
     802msgstr ""
     803
     804#: includes/admin/views/diagnostics.php:42
     805msgid "Send test email"
     806msgstr ""
     807
     808#: includes/admin/views/diagnostics.php:44
     809msgid "If it does not arrive within a few minutes, check the spam folder and your SMTP plugin's log."
     810msgstr ""
     811
     812#: includes/admin/views/diagnostics.php:48
     813#: includes/admin/views/diagnostics.php:62
     814msgid "Clear log"
     815msgstr ""
     816
     817#: includes/admin/views/diagnostics.php:48
     818msgid "Clear the email log? This cannot be undone."
     819msgstr ""
     820
     821#: includes/admin/views/diagnostics.php:49
     822msgid "\"Success\" means WordPress handed the email to your mail system. It does not guarantee it reached the inbox. The log keeps the last 100 emails for 7 days."
     823msgstr ""
     824
     825#: includes/admin/views/diagnostics.php:51
     826msgid "No emails logged yet."
     827msgstr ""
     828
     829#: includes/admin/views/diagnostics.php:53
     830#: includes/admin/views/diagnostics.php:55
     831msgid "Message"
     832msgstr ""
     833
     834#: includes/admin/views/diagnostics.php:62
     835msgid "Clear the activity log? This cannot be undone."
     836msgstr ""
     837
     838#: includes/admin/views/diagnostics.php:62
     839msgid "Verification activity"
     840msgstr ""
     841
     842#: includes/admin/views/diagnostics.php:64
     843msgid "No verification activity yet."
     844msgstr ""
     845
     846#: includes/admin/views/diagnostics.php:66
     847#: includes/admin/views/diagnostics.php:68
     848msgid "Device"
     849msgstr ""
     850
     851#: includes/admin/views/diagnostics.php:66
     852#: includes/admin/views/diagnostics.php:68
     853msgid "User"
     854msgstr ""
     855
     856#: includes/admin/views/diagnostics.php:68
     857msgid "Email verified"
     858msgstr ""
     859
     860#: includes/admin/views/diagnostics.php:68
     861msgid "Guest"
     862msgstr ""
     863
     864#: includes/admin/views/diagnostics.php:76
     865msgid "System information"
     866msgstr ""
     867
     868#: includes/admin/views/diagnostics.php:78
     869msgid "Plugin"
     870msgstr ""
     871
     872#: includes/admin/views/diagnostics.php:80
     873msgid "Not active"
     874msgstr ""
     875
     876#: includes/admin/views/diagnostics.php:82
     877#: includes/admin/views/verification.php:162
     878msgid "Database"
     879msgstr ""
     880
     881#: includes/admin/views/diagnostics.php:82
     882msgid "Needs update"
     883msgstr ""
     884
     885#: includes/admin/views/diagnostics.php:83
     886msgid "Codes waiting to be used"
     887msgstr ""
     888
     889#: includes/admin/views/diagnostics.php:84
     890#: includes/admin/views/verification.php:47
     891#: includes/admin/views/verification.php:152
     892msgid "Checkout block"
     893msgstr ""
     894
     895#: includes/admin/views/diagnostics.php:84
     896msgid "Checkout page"
     897msgstr ""
     898
     899#: includes/admin/views/diagnostics.php:84
     900#: includes/admin/views/verification.php:154
     901msgid "Classic checkout"
     902msgstr ""
     903
     904#: includes/admin/views/diagnostics.php:84
     905msgid "Not recognised"
     906msgstr ""
     907
     908#: includes/admin/views/diagnostics.php:84
     909msgid "Not set"
     910msgstr ""
     911
     912#: includes/admin/views/diagnostics.php:85
     913msgid "High-performance order storage"
     914msgstr ""
     915
     916#: includes/admin/views/diagnostics.php:85
     917#: includes/admin/views/verification.php:160
     918msgid "Order storage"
     919msgstr ""
     920
     921#: includes/admin/views/diagnostics.php:85
     922msgid "WordPress posts"
     923msgstr ""
     924
     925#: includes/admin/views/diagnostics.php:86
     926msgid "Mail"
     927msgstr ""
     928
     929#: includes/admin/views/diagnostics.php:86
     930msgid "PHP mail (no SMTP plugin found)"
     931msgstr ""
     932
     933#: includes/admin/views/diagnostics.php:88
     934msgid "Include these versions when you ask for support. Never share codes, passwords or customer data."
     935msgstr ""
     936
     937#: includes/admin/views/diagnostics.php:91
     938msgid "Maintenance"
     939msgstr ""
     940
     941#: includes/admin/views/diagnostics.php:93
     942msgid "Next cleanup"
     943msgstr ""
     944
     945#: includes/admin/views/diagnostics.php:93
     946msgid "Not scheduled"
     947msgstr ""
     948
     949#: includes/admin/views/diagnostics.php:94
     950msgid "Last cleanup"
     951msgstr ""
     952
     953#: includes/admin/views/diagnostics.php:96
     954msgid "Expired codes and old history are removed automatically every hour. You can also run it now."
     955msgstr ""
     956
     957#: includes/admin/views/diagnostics.php:97
     958msgid "Run cleanup now"
     959msgstr ""
     960
     961#: includes/admin/views/diagnostics.php:99
     962msgid "Older verification activity is removed by the cleanup."
     963msgstr ""
     964
     965#: includes/admin/views/diagnostics.php:99
     966msgid "days"
     967msgstr ""
     968
     969#. translators: %s: link to the Verification page
     970#: includes/admin/views/general.php:9
     971#, php-format
     972msgid "Looking for the customer login and registration form settings? They are now on the %s page, next to the codes they control."
     973msgstr ""
     974
     975#: includes/admin/views/general.php:14
     976msgid "Sender"
     977msgstr ""
     978
     979#: includes/admin/views/general.php:15
     980msgid "Who the verification emails come from. Only emails sent by this plugin are changed. Leave empty to use your WordPress defaults."
     981msgstr ""
     982
     983#: includes/admin/views/general.php:18
     984msgid "Sender name"
     985msgstr ""
     986
     987#: includes/admin/views/general.php:18
     988msgid "Usually your store name."
     989msgstr ""
     990
     991#: includes/admin/views/general.php:19
     992msgid "Sender email"
     993msgstr ""
     994
     995#: includes/admin/views/general.php:19
     996msgid "Use an address on your own domain so emails are not marked as spam. Also shown to customers as the support address."
     997msgstr ""
     998
     999#: includes/admin/views/general.php:25
     1000msgid "Branding"
     1001msgstr ""
     1002
     1003#: includes/admin/views/general.php:34
     1004msgid "Choose from Media Library"
     1005msgstr ""
     1006
     1007#: includes/admin/views/general.php:35
     1008msgid "Remove"
     1009msgstr ""
     1010
     1011#: includes/admin/views/general.php:39
     1012msgid "When empty, the WooCommerce email header image is used."
     1013msgstr ""
     1014
     1015#: includes/admin/views/general.php:39
     1016msgid "When empty, your store name is shown as text."
     1017msgstr ""
     1018
     1019#: includes/admin/views/general.php:44
     1020msgid "Used for buttons and highlights in the emails, the checkout popup and the customer forms."
     1021msgstr ""
     1022
     1023#: includes/admin/views/general.php:50
     1024msgid "When the plugin is deleted"
     1025msgstr ""
     1026
     1027#: includes/admin/views/general.php:51
     1028msgid "Delete all plugin data when the plugin is deleted"
     1029msgstr ""
     1030
     1031#: includes/admin/views/general.php:51
     1032msgid "When off, your settings and email templates are kept so you can reinstall later. Pending codes, reset authorizations and logs are always removed."
     1033msgstr ""
     1034
     1035#: includes/admin/views/help.php:13
     1036msgid "Getting started"
     1037msgstr ""
     1038
     1039#: includes/admin/views/help.php:15
     1040msgid "Follow these steps once. The Overview page shows a checklist that ticks itself off as you go."
     1041msgstr ""
     1042
     1043#. translators: %s: link to the Verification page
     1044#: includes/admin/views/help.php:17
     1045#, php-format
     1046msgid "1. Open %s and turn on the customer login and registration forms (Step 1). Choose your My Account page."
     1047msgstr ""
     1048
     1049#. translators: %s: link to General Settings
     1050#: includes/admin/views/help.php:19
     1051#, php-format
     1052msgid "2. In %s, set your sender name, sender email and store logo so emails look like they come from your store."
     1053msgstr ""
     1054
     1055#. translators: %s: link to Activity & Diagnostics
     1056#: includes/admin/views/help.php:21
     1057#, php-format
     1058msgid "3. Send a test email from %s and make sure it arrives."
     1059msgstr ""
     1060
     1061#: includes/admin/views/help.php:22
     1062msgid "4. Optional: turn on Checkout OTP after you have placed a test order yourself."
     1063msgstr ""
     1064
     1065#: includes/admin/views/help.php:26
     1066msgid "Registration OTP setup"
     1067msgstr ""
     1068
     1069#: includes/admin/views/help.php:28
     1070msgid "When a customer signs up on your My Account page, they receive a 6-digit code by email and must enter it before the account is created. Email addresses that are not confirmed never become accounts."
     1071msgstr ""
     1072
     1073#: includes/admin/views/help.php:29
     1074msgid "It needs the customer login and registration forms (Step 1 on the Verification page). If those forms are off, the Registration tab shows \"Needs setup\"."
     1075msgstr ""
     1076
     1077#: includes/admin/views/help.php:30
     1078msgid "Code Expiration Time, Wait Before Resending and Wrong Code Attempts Allowed can be set separately for registration."
     1079msgstr ""
     1080
     1081#. translators: %s: link to the Registration tab
     1082#. translators: %s: link to the Password Reset tab
     1083#. translators: %s: link to the Checkout tab
     1084#. translators: %s: link to the test email section
     1085#: includes/admin/views/help.php:32 includes/admin/views/help.php:43
     1086#: includes/admin/views/help.php:54 includes/admin/views/help.php:86
     1087#, php-format
     1088msgid "Go to %s."
     1089msgstr ""
     1090
     1091#: includes/admin/views/help.php:32
     1092msgid "Registration OTP settings"
     1093msgstr ""
     1094
     1095#: includes/admin/views/help.php:36
     1096msgid "Password reset setup"
     1097msgstr ""
     1098
     1099#: includes/admin/views/help.php:38
     1100msgid "The customer clicks \"Forgot password?\", enters their email, receives a code and, after entering it, chooses a new password with the \"Update Password\" button."
     1101msgstr ""
     1102
     1103#: includes/admin/views/help.php:39
     1104msgid "The reset permission works once and expires after 5 minutes. The customer then signs in with the new password; they are never signed in automatically."
     1105msgstr ""
     1106
     1107#: includes/admin/views/help.php:40
     1108msgid "The form shows the same message whether or not an account exists, so it cannot be used to find out who is registered."
     1109msgstr ""
     1110
     1111#: includes/admin/views/help.php:41
     1112msgid "If you turn this off, the standard WooCommerce \"Lost your password?\" link is used instead."
     1113msgstr ""
     1114
     1115#: includes/admin/views/help.php:43
     1116msgid "Password Reset OTP settings"
     1117msgstr ""
     1118
     1119#: includes/admin/views/help.php:47
     1120msgid "Checkout OTP setup"
     1121msgstr ""
     1122
     1123#: includes/admin/views/help.php:49
     1124msgid "Off by default. When on, clicking Place order opens a popup asking for the code sent to the billing email. The order is created only after the code is verified, so no payment is taken before that."
     1125msgstr ""
     1126
     1127#: includes/admin/views/help.php:50
     1128msgid "Guests are always asked. Logged-in customers are asked too unless you turn off \"Also verify logged-in customers\"."
     1129msgstr ""
     1130
     1131#: includes/admin/views/help.php:51
     1132msgid "A verification is valid once, for the same browser session and the exact same email. Changing the email needs a new code. If a payment fails, the customer can retry payment for the same order without a new code."
     1133msgstr ""
     1134
     1135#: includes/admin/views/help.php:52
     1136msgid "Before turning it on: place a test order with each payment method you use and check that the popup appears and the order completes."
     1137msgstr ""
     1138
     1139#: includes/admin/views/help.php:54
     1140msgid "Checkout OTP settings"
     1141msgstr ""
     1142
     1143#: includes/admin/views/help.php:58
     1144msgid "Email template customization"
     1145msgstr ""
     1146
     1147#: includes/admin/views/help.php:60
     1148msgid "Each email (Registration, Password Reset, Checkout) has its own subject, preheader and design. Choose one with the tabs at the top of the Email Templates page. The preview on the right updates as you type; use Desktop and Mobile to check both sizes."
     1149msgstr ""
     1150
     1151#: includes/admin/views/help.php:61
     1152msgid "Click a placeholder to insert it where your cursor is. {{otp_code}} must appear in every email. Others: {{site_name}}, {{site_url}}, {{customer_name}}, {{customer_email}}, {{expiry_minutes}}, {{support_email}}, {{site_logo}}, {{brand_color}} and {{preheader}}."
     1153msgstr ""
     1154
     1155#: includes/admin/views/help.php:62
     1156msgid "For safety, scripts, forms, iframes, event handlers and javascript: links are removed when you save, and PHP is never run. Placeholder values are escaped."
     1157msgstr ""
     1158
     1159#: includes/admin/views/help.php:63
     1160msgid "\"Send test\" emails what is in the editor now. \"Discard changes\" returns to the last saved version, and \"Reset to default\" restores the built-in design."
     1161msgstr ""
     1162
     1163#. translators: %s: link to Email Templates
     1164#: includes/admin/views/help.php:65
     1165#, php-format
     1166msgid "Go to %s. Only site administrators can edit email HTML."
     1167msgstr ""
     1168
     1169#: includes/admin/views/help.php:71
     1170msgid "Rate limits count codes sent per visitor and per email address, and code checks per visitor. They reset automatically after their time window, so a blocked visitor can try again later."
     1171msgstr ""
     1172
     1173#: includes/admin/views/help.php:72
     1174msgid "The visitor address comes from REMOTE_ADDR. Forwarded headers are ignored because they can be forged. Behind Cloudflare or another proxy, a developer can supply the real address with the ceovw_client_ip filter after checking the request came from the proxy."
     1175msgstr ""
     1176
     1177#: includes/admin/views/help.php:73
     1178msgid "CAPTCHA is not available in this version. Earlier versions showed CAPTCHA widgets without checking them on the server, so they were removed rather than give a false sense of protection."
     1179msgstr ""
     1180
     1181#. translators: %s: link to Security
     1182#: includes/admin/views/help.php:75
     1183#, php-format
     1184msgid "Go to %s. Only site administrators can change these limits."
     1185msgstr ""
     1186
     1187#: includes/admin/views/help.php:79
     1188msgid "SMTP troubleshooting: emails are not arriving"
     1189msgstr ""
     1190
     1191#: includes/admin/views/help.php:81
     1192msgid "1. Install and set up an SMTP plugin, for example WP Mail SMTP or FluentSMTP. Many hosts block or spam-filter the basic PHP mail function."
     1193msgstr ""
     1194
     1195#: includes/admin/views/help.php:82
     1196msgid "2. Use a sender email address on your own domain (General Settings)."
     1197msgstr ""
     1198
     1199#: includes/admin/views/help.php:83
     1200msgid "3. Send a test email and check the inbox and the spam folder."
     1201msgstr ""
     1202
     1203#: includes/admin/views/help.php:84
     1204msgid "4. Look at the email log. \"Failed\" means WordPress could not hand the email over; check your SMTP plugin's settings and log. \"Success\" means it was handed over, not that it reached the inbox."
     1205msgstr ""
     1206
     1207#: includes/admin/views/help.php:90
     1208msgid "WooCommerce compatibility"
     1209msgstr ""
     1210
     1211#: includes/admin/views/help.php:92
     1212msgid "Checkout block and classic checkout ([woocommerce_checkout] shortcode): supported."
     1213msgstr ""
     1214
     1215#: includes/admin/views/help.php:93
     1216msgid "High-performance order storage (HPOS) and WordPress posts storage: supported."
     1217msgstr ""
     1218
     1219#: includes/admin/views/help.php:94
     1220msgid "Payment methods: tested with Cash on delivery and Direct bank transfer. Other gateways have not been tested, so place a test order with yours before turning Checkout OTP on."
     1221msgstr ""
     1222
     1223#: includes/admin/views/help.php:95
     1224msgid "Express payment buttons outside the checkout form (for example Apple Pay or Google Pay on the cart page) cannot show the popup. While Checkout OTP is on, those orders are refused and the customer completes the purchase through the checkout form."
     1225msgstr ""
     1226
     1227#: includes/admin/views/help.php:96
     1228msgid "Custom checkout pages that use neither the block nor the shortcode are still protected on the server, but the popup may not appear. Activity & Diagnostics shows which checkout type your store uses."
     1229msgstr ""
     1230
     1231#: includes/admin/views/help.php:100
     1232msgid "Frequently asked questions"
     1233msgstr ""
     1234
     1235#: includes/admin/views/help.php:102
     1236msgid "Does Checkout OTP turn on by itself after updating?"
     1237msgstr ""
     1238
     1239#: includes/admin/views/help.php:102
     1240msgid "No. It stays off until you turn it on."
     1241msgstr ""
     1242
     1243#: includes/admin/views/help.php:103
     1244msgid "Can a customer reuse a code?"
     1245msgstr ""
     1246
     1247#: includes/admin/views/help.php:103
     1248msgid "No. Every code works once, only for the purpose it was sent for, and only until it expires."
     1249msgstr ""
     1250
     1251#: includes/admin/views/help.php:104
     1252msgid "Shop managers can turn verification on or off and send test emails. Email HTML, security limits and general settings need a site administrator."
     1253msgstr ""
     1254
     1255#: includes/admin/views/help.php:104
     1256msgid "Who can change these settings?"
     1257msgstr ""
     1258
     1259#: includes/admin/views/help.php:105
     1260msgid "Pending codes, reset permissions and logs are always removed. Settings and templates are kept unless \"Delete all plugin data\" is on in General Settings."
     1261msgstr ""
     1262
     1263#: includes/admin/views/help.php:105
     1264msgid "What happens when I delete the plugin?"
     1265msgstr ""
     1266
     1267#: includes/admin/views/help.php:106
     1268msgid "Are codes or passwords stored in the logs?"
     1269msgstr ""
     1270
     1271#: includes/admin/views/help.php:106
     1272msgid "No. Codes are stored only as hashes, logs never contain codes, passwords or tokens, and email addresses are masked."
     1273msgstr ""
     1274
     1275#: includes/admin/views/help.php:110
     1276msgid "Common errors and solutions"
     1277msgstr ""
     1278
     1279#: includes/admin/views/help.php:112
     1280msgid "\"Too many requests. Please try again later.\" or \"Please wait … seconds before requesting a new code.\""
     1281msgstr ""
     1282
     1283#: includes/admin/views/help.php:112
     1284msgid "A rate limit or the resend wait was reached. The customer can try again after the time shown. If many real customers see this, raise the limits on the Security page."
     1285msgstr ""
     1286
     1287#: includes/admin/views/help.php:113
     1288msgid "\"That code is not correct.\" or \"This code has expired.\""
     1289msgstr ""
     1290
     1291#: includes/admin/views/help.php:113
     1292msgid "The code was mistyped, already used, replaced by a newer one, or older than the Code Expiration Time. Ask the customer to request a new code and use the newest email."
     1293msgstr ""
     1294
     1295#: includes/admin/views/help.php:114
     1296msgid "\"Please verify your email address to place your order.\""
     1297msgstr ""
     1298
     1299#: includes/admin/views/help.php:114
     1300msgid "The order came from a place that cannot show the popup, such as an express payment button. The customer should use the checkout form."
     1301msgstr ""
     1302
     1303#: includes/admin/views/help.php:115
     1304msgid "Registration shows no code step"
     1305msgstr ""
     1306
     1307#: includes/admin/views/help.php:115
     1308msgid "The customer login and registration forms are off, or the wrong My Account page is selected. Check Step 1 on the Verification page."
     1309msgstr ""
     1310
     1311#: includes/admin/views/help.php:116
     1312msgid "\"This service is temporarily unavailable.\" or \"Email verification is temporarily unavailable…\""
     1313msgstr ""
     1314
     1315#: includes/admin/views/help.php:116
     1316msgid "Usually the plugin's database update has not finished, so verification is paused for safety. Open any admin page to let it finish; if the message continues, check Activity & Diagnostics and ask for support."
     1317msgstr ""
     1318
     1319#: includes/admin/views/help.php:120
     1320msgid "For developers"
     1321msgstr ""
     1322
     1323#: includes/admin/views/help.php:122
     1324msgid "ceovw_client_ip (filter): return the visitor IP address used for rate limits."
     1325msgstr ""
     1326
     1327#: includes/admin/views/help.php:123
     1328msgid "ceovw_otp_expiry (filter): code lifetime in seconds, receives the flow name. Minimum 60."
     1329msgstr ""
     1330
     1331#: includes/admin/views/help.php:124
     1332msgid "ceovw_checkout_auth_ttl (filter): how long a verified checkout email stays valid before the order is placed, in seconds. Default 1800."
     1333msgstr ""
     1334
     1335#: includes/admin/views/help.php:125
     1336msgid "ceovw_admin_capability (filter): capability needed for these pages. Default manage_woocommerce."
     1337msgstr ""
     1338
     1339#: includes/admin/views/help.php:126
     1340msgid "ceovw_settings_capability (filter): capability needed for email HTML, security limits and general settings. Default manage_options."
     1341msgstr ""
     1342
     1343#: includes/admin/views/help.php:127
     1344msgid "Codes and reset permissions are stored as hashes in the wp_ceovw_otps table; rate-limit history is in wp_ceovw_logs. A scheduled event removes expired records every hour."
     1345msgstr ""
     1346
     1347#: includes/admin/views/help.php:134
     1348msgid "Topics"
     1349msgstr ""
     1350
     1351#: includes/admin/views/help.php:143
     1352msgid "Search the documentation"
     1353msgstr ""
     1354
     1355#: includes/admin/views/help.php:144
     1356msgid "For example: checkout, SMTP, placeholders"
     1357msgstr ""
     1358
     1359#: includes/admin/views/help.php:159
     1360msgid "Still need help?"
     1361msgstr ""
     1362
     1363#. translators: %s: link to the WordPress.org support forum
     1364#: includes/admin/views/help.php:163
     1365#, php-format
     1366msgid "Ask in the %s. Please include the versions shown under System information in Activity & Diagnostics, but never post codes, passwords or personal data."
     1367msgstr ""
     1368
     1369#: includes/admin/views/help.php:164
     1370msgid "(opens in a new tab)"
     1371msgstr ""
     1372
     1373#: includes/admin/views/help.php:164
     1374msgid "WordPress.org support forum"
     1375msgstr ""
     1376
     1377#: includes/admin/views/security.php:8
     1378msgid "These limits apply to registration, password reset and checkout codes. They stop one visitor from flooding inboxes or guessing codes. The defaults suit most stores; values outside the safe range are adjusted automatically."
     1379msgstr ""
     1380
     1381#: includes/admin/views/security.php:11
     1382msgid "Codes per visitor, per hour"
     1383msgstr ""
     1384
     1385#: includes/admin/views/security.php:11
     1386msgid "How many codes one visitor (IP address) can request in an hour."
     1387msgstr ""
     1388
     1389#: includes/admin/views/security.php:11
     1390msgid "per hour"
     1391msgstr ""
     1392
     1393#: includes/admin/views/security.php:12
     1394msgid "Codes per email address, per day"
     1395msgstr ""
     1396
     1397#: includes/admin/views/security.php:12
     1398msgid "Protects one email address from receiving too many codes."
     1399msgstr ""
     1400
     1401#: includes/admin/views/security.php:12
     1402msgid "per day"
     1403msgstr ""
     1404
     1405#: includes/admin/views/security.php:13
     1406msgid "Code checks per visitor, per 15 minutes"
     1407msgstr ""
     1408
     1409#: includes/admin/views/security.php:13
     1410msgid "How many times one visitor can try codes. Slows down guessing."
     1411msgstr ""
     1412
     1413#: includes/admin/views/security.php:13 includes/admin/views/security.php:14
     1414msgid "per 15 min"
     1415msgstr ""
     1416
     1417#: includes/admin/views/security.php:14
     1418msgid "Applies to the last step of a password reset."
     1419msgstr ""
     1420
     1421#: includes/admin/views/security.php:14
     1422msgid "Password changes per visitor, per 15 minutes"
     1423msgstr ""
     1424
     1425#. translators: %s: link to the Verification page
     1426#: includes/admin/views/security.php:20
     1427#, php-format
     1428msgid "Code expiration time, resend wait and wrong attempts are set for each verification type on the %s page."
     1429msgstr ""
     1430
     1431#: includes/admin/views/security.php:28
     1432msgid "Visitor IP address"
     1433msgstr ""
     1434
     1435#: includes/admin/views/security.php:29
     1436msgid "Rate limits use the address your web server reports for each connection (REMOTE_ADDR). Headers such as X-Forwarded-For are ignored because visitors can forge them."
     1437msgstr ""
     1438
     1439#: includes/admin/views/security.php:31
     1440msgid "Your address right now"
     1441msgstr ""
     1442
     1443#: includes/admin/views/security.php:33
     1444msgid "Custom address resolver"
     1445msgstr ""
     1446
     1447#: includes/admin/views/security.php:34
     1448msgid "Active (ceovw_client_ip filter)"
     1449msgstr ""
     1450
     1451#: includes/admin/views/security.php:34
     1452msgid "None"
     1453msgstr ""
     1454
     1455#: includes/admin/views/security.php:36
     1456msgid "Using Cloudflare or another proxy? Every visitor may then share the proxy's address. A developer can pass the real address with the ceovw_client_ip filter after checking the request really came from the proxy."
     1457msgstr ""
     1458
     1459#: includes/admin/views/security.php:39
     1460msgid "CAPTCHA"
     1461msgstr ""
     1462
     1463#: includes/admin/views/security.php:39
     1464msgid "Unavailable"
     1465msgstr ""
     1466
     1467#: includes/admin/views/security.php:40
     1468msgid "CAPTCHA is not part of this version. Earlier versions showed reCAPTCHA or Turnstile widgets without verifying them on the server, so they gave no real protection and have been removed. Rate limits, per-email limits and attempt limits protect the forms instead."
     1469msgstr ""
     1470
     1471#: includes/admin/views/security.php:42
     1472msgid "A CAPTCHA provider was selected in an earlier version. Its saved keys are kept but not used."
     1473msgstr ""
     1474
     1475#: includes/admin/views/security.php:48
     1476msgid "Always-on protections"
     1477msgstr ""
     1478
     1479#: includes/admin/views/security.php:49
     1480msgid "These cannot be turned off."
     1481msgstr ""
     1482
     1483#: includes/admin/views/security.php:51
     1484msgid "Codes are random 6-digit numbers from a cryptographically secure generator"
     1485msgstr ""
     1486
     1487#: includes/admin/views/security.php:52
     1488msgid "Codes are stored only as a keyed hash, never in plain text"
     1489msgstr ""
     1490
     1491#: includes/admin/views/security.php:53
     1492msgid "Each code works once, and wrong attempts are counted safely"
     1493msgstr ""
     1494
     1495#: includes/admin/views/security.php:54
     1496msgid "A registration code cannot reset a password or verify a checkout"
     1497msgstr ""
     1498
     1499#: includes/admin/views/security.php:55
     1500msgid "A verified password reset is valid once, for 5 minutes, and never signs the customer in"
     1501msgstr ""
     1502
     1503#: includes/admin/views/security.php:56
     1504msgid "Password reset shows the same message whether or not the account exists"
     1505msgstr ""
     1506
     1507#: includes/admin/views/security.php:57
     1508msgid "Checkout verification is tied to the browser session and the exact email"
     1509msgstr ""
     1510
     1511#: includes/admin/views/security.php:58
     1512msgid "Logs never contain codes, passwords or tokens, and emails are masked"
     1513msgstr ""
     1514
     1515#: includes/admin/views/security.php:59
     1516msgid "Expired records are removed by a scheduled cleanup"
     1517msgstr ""
     1518
     1519#: includes/admin/views/security.php:60
     1520msgid "Verification is blocked while a database update is pending"
     1521msgstr ""
     1522
     1523#: includes/admin/views/templates.php:16
     1524msgid "Email template"
     1525msgstr ""
     1526
     1527#. translators: 1: verification name, 2: On/Off, 3: link to the settings
     1528#: includes/admin/views/templates.php:34
     1529#, php-format
     1530msgid "Sent for %1$s (currently %2$s). %3$s"
     1531msgstr ""
     1532
     1533#: includes/admin/views/templates.php:36
     1534msgid "Change settings"
     1535msgstr ""
     1536
     1537#: includes/admin/views/templates.php:42
     1538msgid "Email subject"
     1539msgstr ""
     1540
     1541#: includes/admin/views/templates.php:46
     1542msgid "Preheader"
     1543msgstr ""
     1544
     1545#: includes/admin/views/templates.php:48
     1546msgid "The short preview line many inboxes show after the subject."
     1547msgstr ""
     1548
     1549#: includes/admin/views/templates.php:51
     1550msgid "Email design (HTML)"
     1551msgstr ""
     1552
     1553#: includes/admin/views/templates.php:53
     1554msgid "For safety, scripts, forms, iframes, event handlers and unsafe links are removed when you save. PHP is never run."
     1555msgstr ""
     1556
     1557#: includes/admin/views/templates.php:56
     1558msgid "(click to insert at the cursor)"
     1559msgstr ""
     1560
     1561#: includes/admin/views/templates.php:56
     1562msgid "Available placeholders"
     1563msgstr ""
     1564
     1565#. translators: 1: placeholder, 2: description
     1566#: includes/admin/views/templates.php:59
     1567#, php-format
     1568msgid "Insert %1$s: %2$s"
     1569msgstr ""
     1570
     1571#: includes/admin/views/templates.php:65
     1572msgid "Replace this template with the built-in default? Your changes will be lost."
     1573msgstr ""
     1574
     1575#: includes/admin/views/templates.php:65
     1576msgid "Reset to default"
     1577msgstr ""
     1578
     1579#: includes/admin/views/templates.php:68
     1580msgid "Save template"
     1581msgstr ""
     1582
     1583#: includes/admin/views/templates.php:74
     1584msgid "Live preview"
     1585msgstr ""
     1586
     1587#: includes/admin/views/templates.php:75
     1588msgid "Preview width"
     1589msgstr ""
     1590
     1591#: includes/admin/views/templates.php:76
     1592msgid "Desktop"
     1593msgstr ""
     1594
     1595#: includes/admin/views/templates.php:77
     1596msgid "Mobile"
     1597msgstr ""
     1598
     1599#: includes/admin/views/templates.php:80
     1600msgid "Subject:"
     1601msgstr ""
     1602
     1603#: includes/admin/views/templates.php:81
     1604msgid "Email preview"
     1605msgstr ""
     1606
     1607#: includes/admin/views/templates.php:82
     1608msgid "The preview uses sample data (Jane Doe, code 123456) and runs with scripts disabled."
     1609msgstr ""
     1610
     1611#: includes/admin/views/templates.php:84
     1612msgid "Send a test of this email"
     1613msgstr ""
     1614
     1615#: includes/admin/views/templates.php:87
     1616msgid "Send test"
     1617msgstr ""
     1618
     1619#: includes/admin/views/templates.php:89
     1620msgid "Sends what is in the editor now, with sample data. It does not create a real code."
     1621msgstr ""
     1622
     1623#: includes/admin/views/templates.php:91
     1624msgid "Template guide"
     1625msgstr ""
     1626
     1627#: includes/admin/views/verification.php:18
     1628msgid "Ask new customers for a code when they create an account"
     1629msgstr ""
     1630
     1631#: includes/admin/views/verification.php:19
     1632msgid "The account is created only after the customer enters the 6-digit code sent to their email."
     1633msgstr ""
     1634
     1635#: includes/admin/views/verification.php:20
     1636msgid "Stops fake sign-ups and typos in email addresses. Customers fill in the registration form, receive a code by email and enter it to finish creating their account."
     1637msgstr ""
     1638
     1639#: includes/admin/views/verification.php:25
     1640msgid "Let customers reset their password with an emailed code"
     1641msgstr ""
     1642
     1643#: includes/admin/views/verification.php:26
     1644msgid "When off, the standard WooCommerce \"Lost your password?\" link is used instead."
     1645msgstr ""
     1646
     1647#: includes/admin/views/verification.php:27
     1648msgid "Customers enter their email, receive a code and then choose a new password. They are not signed in automatically afterwards, and the form never reveals whether an email is registered."
     1649msgstr ""
     1650
     1651#: includes/admin/views/verification.php:33
     1652msgid "How long the verification code remains valid. 5 minutes is recommended."
     1653msgstr ""
     1654
     1655#: includes/admin/views/verification.php:33
     1656msgid "minutes"
     1657msgstr ""
     1658
     1659#: includes/admin/views/verification.php:34
     1660msgid "How long customers wait before they can ask for a new code."
     1661msgstr ""
     1662
     1663#: includes/admin/views/verification.php:34
     1664msgid "seconds"
     1665msgstr ""
     1666
     1667#: includes/admin/views/verification.php:35
     1668msgid "After this many wrong entries the code stops working and a new one is needed."
     1669msgstr ""
     1670
     1671#: includes/admin/views/verification.php:35
     1672msgid "tries"
     1673msgstr ""
     1674
     1675#: includes/admin/views/verification.php:39
     1676msgid "Related:"
     1677msgstr ""
     1678
     1679#: includes/admin/views/verification.php:41
     1680msgid "Edit this email"
     1681msgstr ""
     1682
     1683#: includes/admin/views/verification.php:42
     1684msgid "Security limits"
     1685msgstr ""
     1686
     1687#: includes/admin/views/verification.php:44
     1688msgid "How it works"
     1689msgstr ""
     1690
     1691#: includes/admin/views/verification.php:48
     1692msgid "Classic checkout (shortcode)"
     1693msgstr ""
     1694
     1695#: includes/admin/views/verification.php:49
     1696msgid "Custom checkout page (not recognised)"
     1697msgstr ""
     1698
     1699#: includes/admin/views/verification.php:50
     1700msgid "No checkout page set"
     1701msgstr ""
     1702
     1703#: includes/admin/views/verification.php:53
     1704msgid "Verification type"
     1705msgstr ""
     1706
     1707#: includes/admin/views/verification.php:63
     1708msgid "Step 1: Customer login and registration forms"
     1709msgstr ""
     1710
     1711#: includes/admin/views/verification.php:66
     1712msgid "Registration and password reset codes are asked for inside the plugin's own sign-in, sign-up and password reset forms. They replace the standard forms on your My Account page for visitors who are not signed in. This setting is shared by the Registration and Password Reset tabs."
     1713msgstr ""
     1714
     1715#: includes/admin/views/verification.php:67
     1716msgid "Required for registration and password reset codes. Checkout verification works without it."
     1717msgstr ""
     1718
     1719#: includes/admin/views/verification.php:67
     1720msgid "Use the Naimur OTP login, registration and password reset forms"
     1721msgstr ""
     1722
     1723#: includes/admin/views/verification.php:69
     1724msgid "My Account page"
     1725msgstr ""
     1726
     1727#: includes/admin/views/verification.php:74
     1728msgid "— Select a page —"
     1729msgstr ""
     1730
     1731#: includes/admin/views/verification.php:79
     1732msgid "Usually the same page as WooCommerce → Settings → Advanced → My account page."
     1733msgstr ""
     1734
     1735#: includes/admin/views/verification.php:93
     1736msgid "Go to Step 1"
     1737msgstr ""
     1738
     1739#: includes/admin/views/verification.php:93
     1740msgid "This needs the customer login and registration forms. Turn them on in Step 1 above."
     1741msgstr ""
     1742
     1743#: includes/admin/views/verification.php:109
     1744msgid "Customers confirm their billing email with a 6-digit code before the order is created. The check runs on your server, so no order is created and no payment is taken until the code is verified."
     1745msgstr ""
     1746
     1747#: includes/admin/views/verification.php:111
     1748msgid "Off by default. Place a test order with your own payment method before turning this on for customers."
     1749msgstr ""
     1750
     1751#: includes/admin/views/verification.php:111
     1752msgid "Require email verification at checkout"
     1753msgstr ""
     1754
     1755#: includes/admin/views/verification.php:112
     1756msgid "When off, only guest checkouts are asked for a code."
     1757msgstr ""
     1758
     1759#: includes/admin/views/verification.php:120
     1760msgid "How checkout verification works"
     1761msgstr ""
     1762
     1763#: includes/admin/views/verification.php:122
     1764msgid "Customer places order"
     1765msgstr ""
     1766
     1767#: includes/admin/views/verification.php:122
     1768msgid "They fill in checkout and click Place order."
     1769msgstr ""
     1770
     1771#: includes/admin/views/verification.php:123
     1772msgid "A popup opens and a code goes to the billing email."
     1773msgstr ""
     1774
     1775#: includes/admin/views/verification.php:123
     1776msgid "Email code sent"
     1777msgstr ""
     1778
     1779#: includes/admin/views/verification.php:124
     1780msgid "Customer verifies"
     1781msgstr ""
     1782
     1783#: includes/admin/views/verification.php:124
     1784msgid "They type or paste the 6-digit code."
     1785msgstr ""
     1786
     1787#: includes/admin/views/verification.php:125
     1788msgid "Checkout continues"
     1789msgstr ""
     1790
     1791#: includes/admin/views/verification.php:125
     1792msgid "The order is placed automatically."
     1793msgstr ""
     1794
     1795#: includes/admin/views/verification.php:128
     1796msgid "Which customers are affected?"
     1797msgstr ""
     1798
     1799#: includes/admin/views/verification.php:129
     1800msgid "A verified email stays verified for that browser session, so customers are not asked twice."
     1801msgstr ""
     1802
     1803#: includes/admin/views/verification.php:130
     1804msgid "When does the code popup appear?"
     1805msgstr ""
     1806
     1807#: includes/admin/views/verification.php:131
     1808msgid "Right after the customer clicks Place order, before the order is created. If they change the billing email, a new code is needed."
     1809msgstr ""
     1810
     1811#: includes/admin/views/verification.php:132
     1812msgid "How long does the code last?"
     1813msgstr ""
     1814
     1815#. translators: %s: number of minutes
     1816#: includes/admin/views/verification.php:133
     1817#, php-format
     1818msgid "%s minutes, set by Code Expiration Time above."
     1819msgstr ""
     1820
     1821#: includes/admin/views/verification.php:134
     1822msgid "What if the customer cannot verify?"
     1823msgstr ""
     1824
     1825#. translators: %s: number of seconds
     1826#: includes/admin/views/verification.php:135
     1827#, php-format
     1828msgid "The order is not created and no payment is taken. They can ask for a new code after %s seconds, correct their email address, or close the popup and keep their cart."
     1829msgstr ""
     1830
     1831#: includes/admin/views/verification.php:136
     1832msgid "Which email is sent?"
     1833msgstr ""
     1834
     1835#: includes/admin/views/verification.php:139
     1836msgid "Checkout OTP email template"
     1837msgstr ""
     1838
     1839#. translators: %s: link to the email template
     1840#: includes/admin/views/verification.php:139
     1841#, php-format
     1842msgid "The %s."
     1843msgstr ""
     1844
     1845#: includes/admin/views/verification.php:141
     1846msgid "The Checkout OTP email template."
     1847msgstr ""
     1848
     1849#: includes/admin/views/verification.php:148
     1850msgid "Compatibility"
     1851msgstr ""
     1852
     1853#: includes/admin/views/verification.php:150
     1854msgid "Your checkout page"
     1855msgstr ""
     1856
     1857#: includes/admin/views/verification.php:153
     1858#: includes/admin/views/verification.php:155
     1859msgid "Supported"
     1860msgstr ""
     1861
     1862#: includes/admin/views/verification.php:153
     1863msgid "The code popup opens when Place order is clicked."
     1864msgstr ""
     1865
     1866#: includes/admin/views/verification.php:155
     1867msgid "Pages using the [woocommerce_checkout] shortcode."
     1868msgstr ""
     1869
     1870#: includes/admin/views/verification.php:156
     1871msgid "Payment methods"
     1872msgstr ""
     1873
     1874#: includes/admin/views/verification.php:157
     1875msgid "Test yours first"
     1876msgstr ""
     1877
     1878#: includes/admin/views/verification.php:157
     1879msgid "Tested with Cash on delivery and Direct bank transfer. Other payment gateways have not been tested; place a test order with yours before turning this on."
     1880msgstr ""
     1881
     1882#: includes/admin/views/verification.php:158
     1883msgid "Express payment buttons"
     1884msgstr ""
     1885
     1886#: includes/admin/views/verification.php:159
     1887msgid "Buttons such as Apple Pay or Google Pay outside the checkout form cannot show the popup. While verification is on, those orders are refused with \"Please verify your email address to place your order.\" and the customer uses the checkout form instead."
     1888msgstr ""
     1889
     1890#: includes/admin/views/verification.php:159
     1891msgid "Not supported"
     1892msgstr ""
     1893
     1894#: includes/admin/views/verification.php:161
     1895msgid "High-performance order storage (supported)"
     1896msgstr ""
     1897
     1898#: includes/admin/views/verification.php:161
     1899msgid "WordPress posts storage (supported)"
     1900msgstr ""
     1901
     1902#: includes/admin/views/verification.php:163
     1903msgid "Ready"
     1904msgstr ""
     1905
     1906#: includes/admin/views/verification.php:163
     1907msgid "Update pending: checkout is blocked until it completes"
     1908msgstr ""
     1909
     1910#: includes/admin/views/verification.php:166
     1911msgid "Your checkout page uses neither the Checkout block nor the [woocommerce_checkout] shortcode. Verification is still enforced on the server, but the code popup may not appear. Test before enabling."
     1912msgstr ""
     1913
     1914#: includes/class-ajax.php:83
     1915msgid "Registration is not available."
     1916msgstr ""
     1917
     1918#. translators: %d: seconds
     1919#: includes/class-ajax.php:90
     1920#, php-format
     1921msgid "Please wait %d seconds before requesting a new code."
     1922msgstr ""
     1923
     1924#: includes/class-ajax.php:126
     1925msgid "Password reset by code is not available. Please use the standard lost password link."
     1926msgstr ""
     1927
     1928#: includes/class-ajax.php:174
     1929msgid "This verification is not available."
     1930msgstr ""
     1931
     1932#: includes/class-checkout.php:77
     1933msgid "Please verify your email address to place your order."
     1934msgstr ""
     1935
     1936#: includes/class-checkout.php:86
     1937msgid "Email verification is temporarily unavailable, so orders cannot be placed right now. Please try again shortly."
     1938msgstr ""
     1939
     1940#: includes/class-checkout.php:151 includes/class-checkout.php:165
     1941msgid "Your session has expired. Please refresh the page and try again."
     1942msgstr ""
     1943
     1944#: includes/class-checkout.php:153
     1945msgid "Email verification is not required."
     1946msgstr ""
     1947
     1948#: includes/class-checkout.php:157
     1949msgid "Please enter a valid billing email address first."
     1950msgstr ""
     1951
     1952#: includes/class-checkout.php:163
     1953msgid "Too many requests. Please try again later."
     1954msgstr ""
     1955
     1956#: includes/class-checkout.php:169
     1957msgid "A code was already sent. Please check your inbox."
     1958msgstr ""
     1959
     1960#: includes/class-checkout.php:171
     1961msgid "Too many codes were requested for this email. Please try again later."
     1962msgstr ""
     1963
     1964#: includes/class-checkout.php:179
     1965msgid "We could not send the verification email. Please check the address and try again."
     1966msgstr ""
     1967
     1968#: includes/class-checkout.php:192
     1969msgid "Too many attempts. Please try again later."
     1970msgstr ""
     1971
     1972#: includes/class-checkout.php:199 includes/class-checkout.php:254
     1973msgid "This code has expired. Please request a new one."
     1974msgstr ""
     1975
     1976#: includes/class-checkout.php:200
     1977msgid "Too many incorrect attempts. Please request a new code."
     1978msgstr ""
     1979
     1980#: includes/class-checkout.php:201 includes/class-checkout.php:207
     1981msgid "That code is not correct. Please try again."
     1982msgstr ""
     1983
     1984#: includes/class-checkout.php:239
     1985msgid "Verify your email"
     1986msgstr ""
     1987
     1988#. translators: %s: masked email address
     1989#: includes/class-checkout.php:241
     1990#, php-format
     1991msgid "We sent a 6-digit code to %s. Enter it below to place your order."
     1992msgstr ""
     1993
     1994#: includes/class-checkout.php:242
     1995msgid "Sending your code…"
     1996msgstr ""
     1997
     1998#. translators: %d: position of the digit (1-6)
     1999#. translators: %d: digit position
     2000#: includes/class-checkout.php:244 templates/modal.php:14
     2001#, php-format
     2002msgid "Digit %d of 6"
     2003msgstr ""
     2004
     2005#: includes/class-checkout.php:245
     2006msgid "Verify and place order"
     2007msgstr ""
     2008
     2009#: includes/class-checkout.php:246
     2010msgid "Verifying…"
     2011msgstr ""
     2012
     2013#: includes/class-checkout.php:247
     2014msgid "Email verified. Placing your order…"
     2015msgstr ""
     2016
     2017#: includes/class-checkout.php:248
     2018msgid "Resend code"
     2019msgstr ""
     2020
     2021#. translators: %d: seconds
     2022#: includes/class-checkout.php:250
     2023#, php-format
     2024msgid "Resend code in %ds"
     2025msgstr ""
     2026
     2027#: includes/class-checkout.php:251 naimur-email-otp-for-woocommerce.php:119
     2028msgid "A new code has been sent."
     2029msgstr ""
     2030
     2031#. translators: %s: time like 4:59
     2032#: includes/class-checkout.php:253
     2033#, php-format
     2034msgid "Code expires in %s"
     2035msgstr ""
     2036
     2037#: includes/class-checkout.php:255
     2038msgid "Change email"
     2039msgstr ""
     2040
     2041#: includes/class-checkout.php:257 templates/modal.php:6
     2042msgid "Close"
     2043msgstr ""
     2044
     2045#: includes/class-checkout.php:258 naimur-email-otp-for-woocommerce.php:118
     2046msgid "Please enter all 6 digits."
     2047msgstr ""
     2048
     2049#: includes/class-checkout.php:259
     2050msgid "Something went wrong. Please check your connection and try again."
     2051msgstr ""
     2052
     2053#: includes/class-checkout.php:260
     2054msgid "Try again"
     2055msgstr ""
     2056
     2057#: includes/class-emails.php:31
     2058msgid "Test"
     2059msgstr ""
     2060
     2061#: includes/class-templates.php:23
     2062msgid "Your store name"
     2063msgstr ""
     2064
     2065#: includes/class-templates.php:24
     2066msgid "Your store address (URL)"
     2067msgstr ""
     2068
     2069#: includes/class-templates.php:25
     2070msgid "Customer name (or \"there\" when unknown)"
     2071msgstr ""
     2072
     2073#: includes/class-templates.php:26
     2074msgid "Customer email address"
     2075msgstr ""
     2076
     2077#: includes/class-templates.php:27
     2078msgid "The 6-digit code (required)"
     2079msgstr ""
     2080
     2081#: includes/class-templates.php:28
     2082msgid "Minutes until the code expires"
     2083msgstr ""
     2084
     2085#: includes/class-templates.php:29
     2086msgid "Support email address"
     2087msgstr ""
     2088
     2089#: includes/class-templates.php:30
     2090msgid "Store logo image (or the store name when no logo is set)"
     2091msgstr ""
     2092
     2093#: includes/class-templates.php:31
     2094msgid "Brand accent colour (hex)"
     2095msgstr ""
     2096
     2097#: includes/class-templates.php:32
     2098msgid "Preheader text (inbox preview line)"
     2099msgstr ""
     2100
     2101#: includes/class-templates.php:39
     2102msgid "Your {{site_name}} verification code"
     2103msgstr ""
     2104
     2105#: includes/class-templates.php:40
     2106msgid "Use this code to confirm your email address."
     2107msgstr ""
     2108
     2109#: includes/class-templates.php:41
     2110msgid "Confirm your email address"
     2111msgstr ""
     2112
     2113#: includes/class-templates.php:42
     2114msgid "Hi {{customer_name}}, use the code below to finish creating your {{site_name}} account."
     2115msgstr ""
     2116
     2117#: includes/class-templates.php:45
     2118msgid "Your {{site_name}} password reset code"
     2119msgstr ""
     2120
     2121#: includes/class-templates.php:46
     2122msgid "Use this code to reset your password."
     2123msgstr ""
     2124
     2125#: includes/class-templates.php:47
     2126msgid "Reset your password"
     2127msgstr ""
     2128
     2129#: includes/class-templates.php:48
     2130msgid "We received a request to reset the password for {{customer_email}}. Enter the code below to continue."
     2131msgstr ""
     2132
     2133#: includes/class-templates.php:51
     2134msgid "Your {{site_name}} checkout verification code"
     2135msgstr ""
     2136
     2137#: includes/class-templates.php:52
     2138msgid "Use this code to confirm your email and place your order."
     2139msgstr ""
     2140
     2141#: includes/class-templates.php:53
     2142msgid "Verify your email to place your order"
     2143msgstr ""
     2144
     2145#: includes/class-templates.php:54
     2146msgid "Hi {{customer_name}}, enter the code below on the checkout page to confirm {{customer_email}} and complete your order."
     2147msgstr ""
     2148
     2149#: includes/class-templates.php:63
     2150msgid "This code expires in {{expiry_minutes}} minutes and can be used once."
     2151msgstr ""
     2152
     2153#: includes/class-templates.php:64
     2154msgid "If you did not request this code, you can ignore this email. Never share this code with anyone. We will never ask you for it."
     2155msgstr ""
     2156
     2157#: includes/class-templates.php:65
     2158msgid "Need help? Contact us at {{support_email}}."
     2159msgstr ""
     2160
     2161#: includes/class-templates.php:159
     2162msgid "The email subject is required."
     2163msgstr ""
     2164
     2165#: includes/class-templates.php:160
     2166msgid "The email subject must be 200 characters or fewer."
     2167msgstr ""
     2168
     2169#: includes/class-templates.php:161
     2170msgid "The preheader must be 255 characters or fewer."
     2171msgstr ""
     2172
     2173#: includes/class-templates.php:162
     2174msgid "The HTML is too large (maximum 100 KB)."
     2175msgstr ""
     2176
     2177#: includes/class-templates.php:165
     2178msgid "The HTML must contain the {{otp_code}} placeholder, otherwise customers will not receive their code."
     2179msgstr ""
     2180
     2181#: includes/class-templates.php:168
     2182msgid "Some HTML was removed for security: scripts, forms, event handlers, unsafe links or unsupported tags are not allowed in emails."
     2183msgstr ""
     2184
     2185#: includes/class-templates.php:228
     2186msgid "there"
     2187msgstr ""
     2188
     2189#: includes/class-users.php:12
     2190msgid "An account is already registered with your email address."
     2191msgstr ""
     2192
     2193#: naimur-email-otp-for-woocommerce.php:110
     2194msgid "Processing..."
     2195msgstr ""
     2196
     2197#: naimur-email-otp-for-woocommerce.php:111
     2198msgid "Verified! Redirecting..."
     2199msgstr ""
     2200
     2201#: naimur-email-otp-for-woocommerce.php:112
     2202msgid "An error occurred. Please try again."
     2203msgstr ""
     2204
     2205#: naimur-email-otp-for-woocommerce.php:113
     2206msgid "Resend in"
     2207msgstr ""
     2208
     2209#: naimur-email-otp-for-woocommerce.php:115
     2210msgid "Please fill in all fields."
     2211msgstr ""
     2212
     2213#: naimur-email-otp-for-woocommerce.php:117
     2214msgid "The passwords do not match."
     2215msgstr ""
     2216
     2217#: naimur-email-otp-for-woocommerce.php:120 templates/modal.php:19
     2218msgid "Verify Code"
     2219msgstr ""
     2220
     2221#: naimur-email-otp-for-woocommerce.php:121
     2222msgid "Your password has been updated. Please sign in."
     2223msgstr ""
     2224
     2225#: naimur-email-otp-for-woocommerce.php:134
     2226msgid "Settings"
     2227msgstr ""
     2228
     2229#: templates/modal.php:9
     2230msgid "Check your email"
     2231msgstr ""
     2232
     2233#: templates/modal.php:10
    362234msgid "Enter the 6-digit code sent to"
    372235msgstr ""
    382236
    39 #: naimur-email-otp-for-woocommerce.php:65
    40 #: templates/modal.php:16
    41 msgid "Resend code"
    42 msgstr ""
    43 
    44 #: naimur-email-otp-for-woocommerce.php:66
    45 #: templates/modal.php:15
    46 msgid "Verify & Create Account"
    47 msgstr ""
    48 
    49 #: naimur-email-otp-for-woocommerce.php:67
    50 msgid "Invalid or expired code. Try again."
    51 msgstr ""
    52 
    53 #: naimur-email-otp-for-woocommerce.php:78
    54 msgid "Settings"
    55 msgstr ""
    56 
    57 #: naimur-email-otp-for-woocommerce.php:79
    58 msgid "Logs"
    59 msgstr ""
    60 
    61 #: includes/class-ajax.php:19
    62 msgid "Invalid form data."
    63 msgstr ""
    64 
    65 #: includes/class-ajax.php:22
    66 msgid "Email already registered. Please log in."
    67 msgstr ""
    68 
    69 #: includes/class-ajax.php:28
    70 msgid "Security verification failed. Please try again."
    71 msgstr ""
    72 
    73 #: includes/class-ajax.php:41
    74 msgid "Failed to send email. Contact support."
    75 msgstr ""
    76 
    77 #: includes/class-ajax.php:44
    78 msgid "Code sent. Check your inbox."
    79 msgstr ""
    80 
    81 #: includes/class-ajax.php:55
    82 msgid "No pending verification. Please resend code."
    83 msgstr ""
    84 
    85 #: includes/class-ajax.php:63
    86 msgid "Too many attempts. Start over."
    87 msgstr ""
    88 
    89 #: includes/class-ajax.php:67
    90 msgid "Invalid or expired code."
    91 msgstr ""
    92 
    93 #: includes/class-security.php:35
    94 msgid "Please complete the security check."
    95 msgstr ""
    96 
    97 #: includes/class-security.php:43
    98 msgid "Security verification failed."
    99 msgstr ""
    100 
    101 #: includes/class-settings.php:44
    102 msgid "Customer Email OTP Verification for WooCommerce (v1.2.0)"
    103 msgstr ""
    104 
    105 #: includes/class-settings.php:49
    106 msgid "Brand color (HEX)"
    107 msgstr ""
    108 
    109 #: includes/class-settings.php:53
    110 msgid "Modern Login/Register UI"
    111 msgstr ""
    112 
    113 #: includes/class-settings.php:55
    114 msgid "Enable custom modern design"
    115 msgstr ""
    116 
    117 #: includes/class-settings.php:56
    118 msgid "Select My Account page"
    119 msgstr ""
    120 
    121 #: includes/class-settings.php:61
    122 msgid "Captcha Provider"
    123 msgstr ""
    124 
    125 #: includes/class-settings.php:64
    126 msgid "None"
    127 msgstr ""
    128 
    129 #: includes/class-settings.php:65
    130 msgid "Google reCAPTCHA v2"
    131 msgstr ""
    132 
    133 #: includes/class-settings.php:66
    134 msgid "Cloudflare Turnstile"
    135 msgstr ""
    136 
    137 #: includes/class-settings.php:71
    138 msgid "Google reCAPTCHA Keys"
    139 msgstr ""
    140 
    141 #: includes/class-settings.php:78
    142 msgid "Cloudflare Turnstile Keys"
    143 msgstr ""
    144 
    145 #: includes/class-settings.php:85
    146 msgid "Login header HTML"
    147 msgstr ""
    148 
    149 #: includes/class-settings.php:89
    150 msgid "Register header HTML"
    151 msgstr ""
    152 
    153 #: includes/class-settings.php:95
    154 msgid "View Logs"
    155 msgstr ""
    156 
    157 #: includes/class-users.php:10
    158 msgid "Authenticated"
    159 msgstr ""
    160 
    161 #: templates/modal.php:4
    162 msgid "Close"
    163 msgstr ""
    164 
    165 #: templates/modal.php:6
    166 msgid "Verify your email"
    167 msgstr ""
    168 
    169 #: templates/woocommerce/myaccount/form-login.php:13
    170 #: templates/woocommerce/myaccount/form-login.php:55
    171 msgid "Email address"
    172 msgstr ""
    173 
    174 #: templates/woocommerce/myaccount/form-login.php:17
    175 #: templates/woocommerce/myaccount/form-login.php:59
     2237#: templates/modal.php:12
     2238msgid "Verification code"
     2239msgstr ""
     2240
     2241#: templates/modal.php:23
     2242msgid "Didn't get it?"
     2243msgstr ""
     2244
     2245#: templates/modal.php:24
     2246msgid "Resend Code"
     2247msgstr ""
     2248
     2249#: templates/modal.php:27
     2250msgid "Tip: you can paste the whole code into the first box. Check your spam folder if it has not arrived."
     2251msgstr ""
     2252
     2253#: templates/woocommerce/myaccount/form-login.php:14
     2254msgid "Welcome Back"
     2255msgstr ""
     2256
     2257#: templates/woocommerce/myaccount/form-login.php:15
     2258msgid "Sign in to your account"
     2259msgstr ""
     2260
     2261#: templates/woocommerce/myaccount/form-login.php:20
     2262#: templates/woocommerce/myaccount/form-login.php:58
     2263msgid "Email Address"
     2264msgstr ""
     2265
     2266#: templates/woocommerce/myaccount/form-login.php:24
     2267#: templates/woocommerce/myaccount/form-login.php:63
    1762268msgid "Password"
    1772269msgstr ""
    1782270
     2271#: templates/woocommerce/myaccount/form-login.php:29
     2272msgid "Remember me"
     2273msgstr ""
     2274
    1792275#: templates/woocommerce/myaccount/form-login.php:30
    180 msgid "Remember me"
    181 msgstr ""
    182 
    183 #: templates/woocommerce/myaccount/form-login.php:31
    1842276msgid "Forgot password?"
    1852277msgstr ""
    1862278
    187 #: templates/woocommerce/myaccount/form-login.php:36
     2279#: templates/woocommerce/myaccount/form-login.php:34
     2280msgid "Sign In"
     2281msgstr ""
     2282
     2283#: templates/woocommerce/myaccount/form-login.php:40
     2284msgid "Don't have an account?"
     2285msgstr ""
     2286
     2287#: templates/woocommerce/myaccount/form-login.php:40
     2288msgid "Sign up"
     2289msgstr ""
     2290
     2291#: templates/woocommerce/myaccount/form-login.php:49
     2292msgid "Create Account"
     2293msgstr ""
     2294
     2295#: templates/woocommerce/myaccount/form-login.php:50
     2296msgid "We'll email you a 6-digit code to confirm your address."
     2297msgstr ""
     2298
     2299#: templates/woocommerce/myaccount/form-login.php:54
     2300msgid "Full Name"
     2301msgstr ""
     2302
     2303#: templates/woocommerce/myaccount/form-login.php:67
     2304msgid "Confirm Password"
     2305msgstr ""
     2306
     2307#: templates/woocommerce/myaccount/form-login.php:73
     2308msgid "Sign Up & Verify"
     2309msgstr ""
     2310
     2311#: templates/woocommerce/myaccount/form-login.php:78
     2312msgid "Already have an account?"
     2313msgstr ""
     2314
     2315#: templates/woocommerce/myaccount/form-login.php:78
    1882316msgid "Sign in"
    1892317msgstr ""
    1902318
    191 #: templates/woocommerce/myaccount/form-login.php:39
    192 msgid "Don't have an account?"
    193 msgstr ""
    194 
    195 #: templates/woocommerce/myaccount/form-login.php:39
    196 msgid "Sign up"
    197 msgstr ""
    198 
    199 #: templates/woocommerce/myaccount/form-login.php:51
    200 msgid "Full Name"
    201 msgstr ""
    202 
    203 #: templates/woocommerce/myaccount/form-login.php:63
    204 msgid "Confirm Password"
    205 msgstr ""
    206 
    207 #: templates/woocommerce/myaccount/form-login.php:77
    208 msgid "Create Account"
    209 msgstr ""
    210 
    211 #: templates/woocommerce/myaccount/form-login.php:81
    212 msgid "Back to sign in"
    213 msgstr ""
     2319#: templates/woocommerce/myaccount/form-login.php:87
     2320msgid "Reset Password"
     2321msgstr ""
     2322
     2323#: templates/woocommerce/myaccount/form-login.php:88
     2324msgid "Enter your email and we'll send you a 6-digit code."
     2325msgstr ""
     2326
     2327#: templates/woocommerce/myaccount/form-login.php:92
     2328msgid "Registered Email"
     2329msgstr ""
     2330
     2331#: templates/woocommerce/myaccount/form-login.php:97
     2332msgid "Send Verification Code"
     2333msgstr ""
     2334
     2335#: templates/woocommerce/myaccount/form-login.php:102
     2336msgid "Back to Sign in"
     2337msgstr ""
     2338
     2339#: templates/woocommerce/myaccount/form-login.php:109
     2340msgid "Set New Password"
     2341msgstr ""
     2342
     2343#: templates/woocommerce/myaccount/form-login.php:110
     2344msgid "Choose a new password for your account."
     2345msgstr ""
     2346
     2347#: templates/woocommerce/myaccount/form-login.php:115
     2348msgid "New Password"
     2349msgstr ""
     2350
     2351#: templates/woocommerce/myaccount/form-login.php:119
     2352msgid "Confirm New Password"
     2353msgstr ""
     2354
     2355#: templates/woocommerce/myaccount/form-login.php:124
     2356msgid "Update Password"
     2357msgstr ""
  • naimur-email-otp-for-woocommerce/trunk/naimur-email-otp-for-woocommerce.php

    r3780003 r3780739  
    33 * Plugin Name: Naimur Email OTP Verification for WooCommerce
    44 * Plugin URI: https://naimurrahmannahid.com/naimur-otp-plugin/
    5  * Description: Verify WooCommerce customer emails with a 6-digit OTP for registration and password reset, with a modern, speed-optimized My Account UI.
    6  * Version: 2.1.1
     5 * Description: Verify WooCommerce customer emails with a 6-digit OTP for registration, password reset and (optionally) checkout, with editable email templates and a modern admin.
     6 * Version: 2.1.2
    77 * Requires at least: 6.0
    88 * Tested up to: 7.1
    99 * Requires PHP: 7.4
    1010 * Requires Plugins: woocommerce
    11  * WC tested up to: 10.8
     11 * WC tested up to: 11.2
    1212 * Author: Naimur Rahman Nahid
    1313 * Author URI: https://naimurrahmannahid.com/
    … …  
    1717if ( ! defined( 'ABSPATH' ) ) { exit; }
    1818
    19 define('NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION', '2.1.1');
    20 define('CEOVW_DB_VERSION', '2.1.1');
     19define('NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION', '2.1.2');
     20define('CEOVW_DB_VERSION', '2.1.2');
    2121define('NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH', plugin_dir_path(__FILE__));
    2222define('NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_URL', plugin_dir_url(__FILE__));
    2323
    2424// Load all files exactly matching your folder structure
     25require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-options.php';
     26require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-templates.php';
    2527require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/helpers.php';
    2628require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-ajax.php';
    … …  
    3133require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-settings.php';
    3234require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-users.php';
     35require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/class-checkout.php';
     36
     37CEOVW_Checkout::init();
     38if ( is_admin() ) {
     39    require_once NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_PATH.'includes/admin/class-admin.php';
     40    CEOVW_Admin::init();
     41}
     42
     43// Declare compatibility with WooCommerce features this plugin was tested with
     44add_action( 'before_woocommerce_init', function() {
     45    if ( class_exists( '\\Automattic\\WooCommerce\\Utilities\\FeaturesUtil' ) ) {
     46        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
     47        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true );
     48    }
     49});
    3350
    3451// Activation Hooks for DB & Cron
    … …  
    6986// Enqueue Highly Optimized Assets
    7087add_action('wp_enqueue_scripts', function() {
    71     $override_enabled = (bool) get_option('naimur-email-otp-for-woocommerce_override_enabled', false);
    72     $my_page_id       = (int) get_option('naimur-email-otp-for-woocommerce_myaccount_page_id', 0);
     88    $override_enabled = (bool) CEOVW_Options::get('modern_ui');
     89    $my_page_id       = (int) CEOVW_Options::get('myaccount_page_id');
    7390
    7491    $should_enqueue = false;
    … …  
    8097    wp_enqueue_style('ceovw-modal-css', NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_URL.'assets/css/modal.css', [], NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION);
    8198    wp_enqueue_style('ceovw-corporate-ui', NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_URL.'assets/css/override-myaccount.css', [], NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION);
    82    
    83     // Captcha Integration
    84     $captcha_type = get_option('naimur-email-otp-for-woocommerce_captcha_type', 'none');
    85     if ( $captcha_type === 'recaptcha' && get_option('naimur-email-otp-for-woocommerce_recaptcha_site_key') ) {
    86         wp_enqueue_script('google-recaptcha','https://www.google.com/recaptcha/api.js',[],NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION,true);
    87         wp_enqueue_script('ceovw-boot-app', NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_URL.'assets/js/boot.js', ['jquery'], NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION, true);
    88     } elseif ( $captcha_type === 'turnstile' && get_option('naimur-email-otp-for-woocommerce_turnstile_site_key') ) {
    89         wp_enqueue_script('cloudflare-turnstile','https://challenges.cloudflare.com/turnstile/v0/api.js',[],NAIMUR_EMAIL_OTP_FOR_WOOCOMMERCE_VERSION,true);
    90     }
     99
     100    // CAPTCHA is not loaded: earlier versions displayed it without server-side verification (see Security & Rate Limits)
    91101
    92102    // Main App Logic
    … …  
    102112            'error'      => esc_html__('An error occurred. Please try again.', 'naimur-email-otp-for-woocommerce'),
    103113            'resend_in'  => esc_html__('Resend in', 'naimur-email-otp-for-woocommerce'),
     114            // Shown with .text() in JavaScript, so plain (unescaped) strings are safe here.
     115            'required'   => __('Please fill in all fields.', 'naimur-email-otp-for-woocommerce'),
     116            'badEmail'   => __('Please enter a valid email address.', 'naimur-email-otp-for-woocommerce'),
     117            'mismatch'   => __('The passwords do not match.', 'naimur-email-otp-for-woocommerce'),
     118            'enterCode'  => __('Please enter all 6 digits.', 'naimur-email-otp-for-woocommerce'),
     119            'codeSent'   => __('A new code has been sent.', 'naimur-email-otp-for-woocommerce'),
     120            'verifyBtn'  => __('Verify Code', 'naimur-email-otp-for-woocommerce'),
     121            'updated'    => __('Your password has been updated. Please sign in.', 'naimur-email-otp-for-woocommerce'),
    104122        ],
    105123        'settings'     => [
    106             'captcha'  => sanitize_text_field($captcha_type),
    107             'site_key' => $captcha_type === 'recaptcha' ? get_option('naimur-email-otp-for-woocommerce_recaptcha_site_key') : get_option('naimur-email-otp-for-woocommerce_turnstile_site_key'),
     124            'resend' => [
     125                'register' => CEOVW_Options::resend_seconds('registration'),
     126                'forgot'   => CEOVW_Options::resend_seconds('forgot'),
     127            ],
    108128        ]
    109129    ]);
    … …  
    112132// Settings Links
    113133add_filter('plugin_action_links_'.plugin_basename(__FILE__), function($links){
    114     $links[] = '<a href="'.esc_url(admin_url('admin.php?page=naimur-email-otp-for-woocommerce-settings')).'">'.esc_html__('Settings', 'naimur-email-otp-for-woocommerce').'</a>';
     134    $links[] = '<a href="'.esc_url(admin_url('admin.php?page=ceovw-dashboard')).'">'.esc_html__('Settings', 'naimur-email-otp-for-woocommerce').'</a>';
    115135    return $links;
    116136});
  • naimur-email-otp-for-woocommerce/trunk/readme.txt

    r3780003 r3780739  
    22Contributors: naimurrahmannahid
    33Donate link: https://buymeacoffee.com/naimurrahmannahid
    4 Tags: woocommerce, otp, email verification, customer
     4Tags: woocommerce, otp, email verification, checkout, customer
    55Requires at least: 6.0
    66Tested up to: 7.1
    77Requires PHP: 7.4
    8 Stable tag: 2.1.1
     8Stable tag: 2.1.2
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1111
    12 Verify WooCommerce customer emails with a 6-digit OTP during registration and secure password recovery.
     12Verify WooCommerce customer emails with a 6-digit code at registration, password reset and (optionally) checkout.
    1313
    1414== Description ==
    1515
    16 **Naimur Email OTP Verification for WooCommerce** confirms that a customer really owns their email address before an account is created, and uses the same one-time code (OTP) to protect password recovery.
     16**Naimur Email OTP Verification for WooCommerce** confirms that a customer really owns their email address. A one-time code (OTP) is emailed and must be entered before the action completes.
    1717
    1818The OTP is used for:
    … …  
    2020* New customer registration
    2121* Password reset (Forgot Password)
    22 
    23 The plugin does not add OTP to login or checkout.
     22* Checkout (optional, off by default): the order is placed only after the billing email is verified
     23
     24The plugin does not add OTP to login.
    2425
    2526= Key features =
    2627
    2728* 6-digit email OTP for WooCommerce customer registration
    28 * OTP-based Forgot Password flow with a single-use, short-lived reset authorization
    29 * Modern, responsive My Account sign-in, registration and password recovery forms
    30 * OTP entry with auto-advance, paste support and a resend button with countdown
    31 * Three bundled email templates plus your own custom HTML template, with a live preview in the admin
    32 * Email and activity logs in the admin (OTP codes are never shown; email addresses are masked)
    33 * Server-side protection: OTP attempt limits, code expiry and rate limits for sending, verification and password reset
    34 * Translation-ready, with developer filters such as `ceovw_otp_expiry`, `ceovw_rate_limit` and `ceovw_client_ip`
     29* OTP-based Forgot Password flow with a single-use, short-lived reset authorization (no automatic login after a reset)
     30* Optional checkout email verification for the Checkout block and the classic checkout, for guests and (optionally) logged-in customers. Verification is enforced on the server before the order is created, so no payment is taken from an unverified checkout
     31* Accessible verification popup at checkout: 6-digit input with paste support, countdown, resend with cooldown, change email, mobile bottom-sheet layout
     32* Three responsive email templates (registration, password reset, checkout) that use your store logo, name and brand colour
     33* HTML template editor with syntax highlighting, placeholders, a sandboxed live preview (desktop and mobile) and test emails
     34* "Naimur OTP" admin area: overview with setup checklist, one Verification page with Registration, Password Reset and Checkout tabs, email templates, security limits, branding, activity and diagnostics, searchable help and a settings finder
     35* Server-side protection: hashed codes, attempt limits, expiry, single use, per-IP and per-email rate limits, purpose and session binding
     36* Compatible with WooCommerce High-Performance Order Storage (HPOS) and the Cart and Checkout blocks
     37* Translation-ready, with developer filters such as `ceovw_otp_expiry`, `ceovw_checkout_auth_ttl`, `ceovw_rate_limit` and `ceovw_client_ip`
    3538
    3639= How it works =
    3740
    38 1. The customer fills in the registration form and receives a 6-digit code by email.
    39 2. The account is created only after the correct code is entered.
    40 3. For a forgotten password, the customer requests a code, verifies it, and then sets a new password. After the reset they sign in with the new password; they are not logged in automatically.
     411. Registration: the customer fills in the registration form and receives a 6-digit code by email. The account is created only after the correct code is entered.
     422. Password reset: the customer requests a code, verifies it and sets a new password, then signs in with it.
     433. Checkout (when enabled): after clicking Place order, a popup asks for the code sent to the billing email. Once verified, the order is placed automatically. Changing the email requires a new code.
    4144
    4245= Documentation =
    4346
    44 Full documentation for setup, usage and customization: https://naimurrahmannahid.com/naimur-otp-plugin/
     47Built-in documentation is available under **Naimur OTP → Help & Documentation**. More guides: https://naimurrahmannahid.com/naimur-otp-plugin/
    4548
    4649Video tutorial:
    … …  
    4851[youtube https://youtu.be/AehMWNLxkVk]
    4952
    50 == External services ==
    51 
    52 This plugin can optionally display a Google reCAPTCHA or Cloudflare Turnstile widget on the forms of its My Account UI. When enabled, the provider's script is loaded in the visitor's browser and the visitor's browser communicates directly with the provider. In version 2.1.1 the plugin's server does not send the CAPTCHA response to the provider for verification.
    53 
    54 **Google reCAPTCHA:**
    55 When reCAPTCHA is enabled in the plugin settings and a site key is configured, the visitor's browser loads the reCAPTCHA script from Google, and Google may receive:
    56 * The IP address of the user
    57 * Browser and usage information collected by the reCAPTCHA widget
    58 
    59 Service Provider:
    60 * Google reCAPTCHA – Provided by Google LLC
    61 * Terms of Service: https://www.google.com/recaptcha/terms/
    62 * Privacy Policy: https://policies.google.com/privacy/
    63 
    64 **Cloudflare Turnstile:**
    65 When Turnstile is enabled and a site key is configured, the visitor's browser loads the Turnstile script from Cloudflare. Similar to reCAPTCHA, IP and browser telemetry may be processed by Cloudflare.
    66 
    67 Service provider:
    68 * Cloudflare Turnstile – provided by Cloudflare, Inc.
    69 * Terms of Service: https://www.cloudflare.com/website-terms/
    70 * Privacy Policy: https://www.cloudflare.com/privacypolicy/
    71 
    7253== Source code ==
    7354
    … …  
    7859* `assets/src/js/main.js`
    7960
    80 This file contains the original source code that can be viewed, studied and modified by developers.
     61All other JavaScript and CSS files (`assets/js/checkout-otp.js`, `assets/admin/admin.js` and the stylesheets) are shipped unminified.
    8162
    8263== Installation ==
    8364
    84651. In your WordPress admin, go to **Plugins → Add New**, search for "Naimur Email OTP Verification for WooCommerce", then install and activate it. WooCommerce must be installed and active.
    85 2. Go to **WooCommerce → Email Verification**.
    86 3. Set the email subject and choose an email template (or write your own custom HTML using the `[naimur_email_otp_code]` placeholder). The live preview shows the result.
    87 4. To use the modern forms, tick **Enable custom modern design** and select your WooCommerce My Account page, then save.
     662. Open **Naimur OTP → Verification**. In Step 1, turn on the customer login and registration forms and select your My Account page.
     673. Open **Naimur OTP → General Settings** and set your sender name, sender email and logo.
     684. Review the emails under **Naimur OTP → Email Templates** and send yourself a test email.
     695. Optional: turn on **Naimur OTP → Verification → Checkout OTP** after placing a test order yourself.
    88705. Make sure your site can send email. Using an SMTP plugin is recommended for reliable delivery.
    8971
    … …  
    9173
    9274= Where is the OTP used? =
    93 For new customer registration and for password reset (Forgot Password). The plugin does not add OTP to login or checkout.
    94 
    95 = Can I customize the verification email? =
    96 Yes. You can set the email subject, choose one of three bundled templates, or write your own HTML template. Use the `[naimur_email_otp_code]` placeholder where the code should appear. The settings page shows a live preview.
     75For new customer registration, for password reset and, if you enable it, at checkout. The plugin does not add OTP to login.
     76
     77= Does checkout verification work with the Checkout block? =
     78Yes. It supports the Checkout block (Store API) and the classic `[woocommerce_checkout]` shortcode. The check runs on the server when the order is placed, so it cannot be skipped by disabling JavaScript or calling the API directly.
     79
     80= Can a customer be charged before verifying? =
     81No. The order is not created until the billing email is verified, so payment is never requested for an unverified checkout. If a payment fails, the customer can retry payment for the same order and the same email without a new code.
     82
     83= What about express payment buttons (Apple Pay, Google Pay)? =
     84Express buttons outside the checkout form skip the verification popup. While checkout verification is required, such orders are refused with the message "Please verify your email address to place your order." and the customer can complete the purchase through the checkout form. Turn checkout verification off if you rely on express-only payments.
     85
     86= Can I customize the verification emails? =
     87Yes. Each email has its own subject, preheader and HTML. Use placeholders such as `{{otp_code}}`, `{{customer_name}}`, `{{site_name}}`, `{{expiry_minutes}}` and `{{support_email}}`. Scripts, forms, iframes and unsafe links are removed when you save, and PHP is never executed.
    9788
    9889= How long is a code valid? =
    99 Codes expire after 5 minutes and allow up to 5 verification attempts. Developers can change the expiry with the `ceovw_otp_expiry` filter.
     905 minutes by default, with 5 attempts. You can set expiry (1–30 minutes), resend delay and attempts separately for registration, password reset and checkout.
    10091
    10192= Is the customer logged in after resetting their password? =
    102 No. Since version 2.1.1 the customer is returned to the sign-in form and signs in with the new password.
     93No. The customer returns to the sign-in form and signs in with the new password.
    10394
    10495= Does this plugin work with SMTP plugins? =
    105 Yes. Emails are sent with WordPress's `wp_mail()`, so SMTP plugins that hook into `wp_mail()` (for example WP Mail SMTP, FluentSMTP or Post SMTP) handle delivery.
    106 
    107 = Do reCAPTCHA and Cloudflare Turnstile protect the forms? =
    108 Not in version 2.1.1. The widgets can be displayed on the plugin's forms, but the CAPTCHA response is not verified on the server, so they do not currently block automated requests. OTP requests, OTP verification and password resets are protected by server-side rate limits instead. Server-side CAPTCHA verification is planned for a future release.
     96Yes. Emails are sent with WordPress's `wp_mail()`, so SMTP plugins that hook into `wp_mail()` (for example WP Mail SMTP, FluentSMTP or Post SMTP) handle delivery. The sender name and address you set apply only to this plugin's emails.
     97
     98= Is there CAPTCHA? =
     99No. Earlier versions could display reCAPTCHA or Turnstile without verifying it on the server, which gave no real protection, so it was removed in 2.1.2. The forms are protected by server-side rate limits and attempt limits.
    109100
    110101= Does it slow down the site? =
    111 The plugin's styles and scripts load only on the WooCommerce My Account page for visitors who are not logged in.
     102No noticeable impact. The My Account scripts load only on the My Account page for visitors who are not logged in, the checkout script loads only on the checkout page when checkout verification is enabled, and admin assets load only on the plugin's own admin pages.
    112103
    113104= My site is behind a proxy or CDN. Which IP address is used? =
    114105For rate limiting and activity logs the plugin uses `REMOTE_ADDR` and does not trust forwarded headers. If your server sits behind a trusted proxy, return the real client IP through the `ceovw_client_ip` filter.
    115106
     107= What happens to my data if I delete the plugin? =
     108Pending codes, reset authorizations and logs are always removed. Settings and email templates are kept unless you turn on "Delete all plugin data" under General Settings.
     109
    116110== Screenshots ==
    117111
    118 1. Customer registration form in the modern My Account design.
    119 2. Email OTP verification during customer registration.
    120 3. Forgot Password: request a verification code by email.
    121 4. Set a new password after the code is verified.
    122 5. After a password reset the customer returns to the sign-in form (no automatic login).
    123 6. Email subject and template selection with live preview.
    124 7. Custom HTML email template with live preview.
    125 8. Email and activity logs with masked email addresses and hidden OTP codes.
     1121. Overview with the status of each verification, email delivery and security health, a setup checklist and quick actions.
     1132. Checkout verification popup on the Checkout block.
     1143. Checkout verification popup on a phone.
     1154. Email template editor with syntax highlighting and live preview.
     1165. Checkout OTP tab: settings, how checkout verification works and compatibility.
     1176. Verification page with Registration, Password Reset and Checkout tabs.
     1187. Security settings: rate limits and always-on protections.
     1198. Checkout verification email.
     1209. Customer registration form on the My Account page.
     12110. Email OTP verification during customer registration.
     12211. Set a new password after the code is verified.
     12312. Activity & Diagnostics: test email, email log with masked addresses and system information.
    126124
    127125== Changelog ==
     126
     127= 2.1.2 =
     128Added:
     129* Optional checkout email verification (off by default) for the Checkout block and classic checkout, for guests and logged-in customers. Enforced on the server before the order is created; verification is single-use and bound to the browser session, the exact email and the checkout purpose.
     130* Accessible checkout verification popup with paste support, countdown, resend cooldown, change email and a mobile layout.
     131* The registration and password reset code popup now matches the checkout popup: dialog semantics, Escape to close, focus kept inside, paste and one-time-code autofill, arrow and Backspace keys, and inline messages instead of browser alerts.
     132* New "Naimur OTP" admin menu with Overview, Verification (Registration, Password Reset and Checkout tabs), Email Templates, Security, Activity & Diagnostics, General Settings and Help & Documentation.
     133* Overview page with real status for each verification, email delivery and security health, a setup checklist and quick actions.
     134* Plain-language settings with help text, unsaved-changes warning, Discard changes, inline validation, a settings finder and contextual links to the documentation.
     135* Three new responsive email templates (registration, password reset, checkout) using your store logo and brand colour.
     136* HTML email template editor with syntax highlighting, placeholders, sandboxed live preview (desktop and mobile), reset and test email.
     137* Per-flow settings for code expiry, resend delay and attempts; adjustable rate limits within safe ranges.
     138* Sender name and email for this plugin's emails only, store logo, uninstall data retention option and activity log retention.
     139* Server-side resend cooldown for all flows.
     140* Declared compatibility with WooCommerce HPOS and the Cart and Checkout blocks.
     141Changed:
     142* Settings moved from WooCommerce → Email Verification to the new Naimur OTP menu. Existing settings are kept; old admin links redirect.
     143* Email HTML, security limits and general settings now require a site administrator (manage_options). Shop managers can still turn verification on or off and send test emails.
     144* A custom HTML template and custom subject from earlier versions are migrated to the registration and password reset templates. The bundled templates 1–3 are replaced by the new design.
     145* Password reset button now reads "Update Password" (the customer is not logged in automatically).
     146* When password reset or registration OTP is turned off, the standard WooCommerce links are shown instead.
     147Removed:
     148* reCAPTCHA and Cloudflare Turnstile widgets. They were never verified on the server. Saved keys are kept but unused, and no third-party scripts are loaded.
     149Security:
     150* All 2.1.1 protections retained: hashed codes, single-use reset authorization, atomic attempt counting, expiry, rate limits, cron cleanup, no automatic login after reset.
     151* Email HTML is filtered with a strict allowlist; placeholder values are escaped.
     152* Verification fails closed while a database upgrade is pending.
    128153
    129154= 2.1.1 =
    … …  
    170195== Upgrade Notice ==
    171196
     197= 2.1.2 =
     198Adds optional checkout email verification (off by default), a new Naimur OTP admin menu and editable responsive email templates. Settings are migrated automatically. reCAPTCHA/Turnstile display was removed. Requires WooCommerce.
     199
    172200= 2.1.1 =
    173201Security release. Hardens password reset and OTP verification. Recommended for all users. After a password reset, customers now sign in with their new password. Pending password reset sessions started before the update must be restarted.
  • naimur-email-otp-for-woocommerce/trunk/templates/modal.php

    r3587442 r3780739  
    11<?php defined('ABSPATH') || exit; ?>
    22
    3 <!-- OTP MODAL OVERLAY -->
     3<!-- OTP MODAL OVERLAY (same visual language as the checkout popup) -->
    44<div id="ceovw-otp-modal" class="ceovw-modal-overlay" style="display:none;">
    5     <div class="ceovw-modal-content">
    6         <button class="ceovw-modal-close" aria-label="Close">×</button>
     5    <div class="ceovw-modal-content" role="dialog" aria-modal="true" aria-labelledby="ceovw-otp-title" aria-describedby="ceovw-otp-desc">
     6        <button type="button" class="ceovw-modal-close" aria-label="<?php esc_attr_e('Close', 'naimur-email-otp-for-woocommerce'); ?>"><span aria-hidden="true">&times;</span></button>
     7        <div class="ceovw-modal-icon" aria-hidden="true"><svg viewBox="0 0 48 48" width="34" height="34" fill="none"><rect x="6" y="11" width="36" height="26" rx="5" stroke="currentColor" stroke-width="2.5"/><path d="M8 15l16 11 16-11" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
    78        <div class="ceovw-modal-header">
    8             <h3><?php esc_html_e('Verification Required', 'naimur-email-otp-for-woocommerce'); ?></h3>
    9             <p><?php esc_html_e('Enter the 6-digit code sent to', 'naimur-email-otp-for-woocommerce'); ?> <strong id="ceovw-target-email"></strong></p>
     9            <h3 id="ceovw-otp-title"><?php esc_html_e('Check your email', 'naimur-email-otp-for-woocommerce'); ?></h3>
     10            <p id="ceovw-otp-desc"><?php esc_html_e('Enter the 6-digit code sent to', 'naimur-email-otp-for-woocommerce'); ?> <strong id="ceovw-target-email"></strong></p>
    1011        </div>
    11         <div class="ceovw-otp-inputs">
    12             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required autofocus>
    13             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
    14             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
    15             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
    16             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
    17             <input type="text" maxlength="1" pattern="[0-9]*" inputmode="numeric" class="ceovw-otp-box" required>
     12        <div class="ceovw-otp-inputs" role="group" aria-label="<?php esc_attr_e('Verification code', 'naimur-email-otp-for-woocommerce'); ?>">
     13            <?php for ( $ceovw_i = 1; $ceovw_i <= 6; $ceovw_i++ ) : ?>
     14                <input type="text" inputmode="numeric" pattern="[0-9]*" class="ceovw-otp-box" autocomplete="<?php echo 1 === $ceovw_i ? 'one-time-code' : 'off'; ?>" aria-label="<?php echo esc_attr( sprintf( /* translators: %d: digit position */ __( 'Digit %d of 6', 'naimur-email-otp-for-woocommerce' ), $ceovw_i ) ); ?>">
     15            <?php endfor; ?>
    1816        </div>
    19         <div id="ceovw-otp-error" class="ceovw-error-msg"></div>
    20         <button class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-verify">
     17        <div id="ceovw-otp-error" class="ceovw-error-msg" role="alert" aria-live="assertive"></div>
     18        <button type="button" class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-verify">
    2119            <span class="btn-text"><?php esc_html_e('Verify Code', 'naimur-email-otp-for-woocommerce'); ?></span>
    22             <span class="btn-loader"></span>
     20            <span class="btn-loader" aria-hidden="true"></span>
    2321        </button>
    2422        <div class="ceovw-resend-wrapper">
    25             <button id="ceovw-btn-resend" class="ceovw-link" disabled><?php esc_html_e('Resend Code', 'naimur-email-otp-for-woocommerce'); ?></button>
    26             <span id="ceovw-timer-text">(60s)</span>
     23            <span class="ceovw-resend-q"><?php esc_html_e("Didn't get it?", 'naimur-email-otp-for-woocommerce'); ?></span>
     24            <button type="button" id="ceovw-btn-resend" class="ceovw-link" disabled><?php esc_html_e('Resend Code', 'naimur-email-otp-for-woocommerce'); ?></button>
     25            <span id="ceovw-timer-text"></span>
    2726        </div>
     27        <p class="ceovw-modal-tip"><?php esc_html_e('Tip: you can paste the whole code into the first box. Check your spam folder if it has not arrived.', 'naimur-email-otp-for-woocommerce'); ?></p>
    2828    </div>
    2929</div>
  • naimur-email-otp-for-woocommerce/trunk/templates/woocommerce/myaccount/form-login.php

    r3587442 r3780739  
    1 <?php 
    2 defined('ABSPATH') || exit; 
     1<?php
     2defined('ABSPATH') || exit;
    33
    4 // ক্যাপচা টাইপ এবং সাইট কি (Site Key) ফেচ করা
    5 $captcha_type = get_option('naimur-email-otp-for-woocommerce_captcha_type', 'none');
    6 $captcha_html = '';
    7 
    8 if ($captcha_type === 'recaptcha') {
    9     $site_key = get_option('naimur-email-otp-for-woocommerce_recaptcha_site_key');
    10     if (!empty($site_key)) {
    11         $captcha_html = '<div class="g-recaptcha" data-sitekey="' . esc_attr($site_key) . '" style="margin-bottom: 15px; display: flex; justify-content: center;"></div>';
    12     }
    13 } elseif ($captcha_type === 'turnstile') {
    14     $site_key = get_option('naimur-email-otp-for-woocommerce_turnstile_site_key');
    15     if (!empty($site_key)) {
    16         $captcha_html = '<div class="cf-turnstile" data-sitekey="' . esc_attr($site_key) . '" style="margin-bottom: 15px; display: flex; justify-content: center;"></div>';
    17     }
    18 }
     4$ceovw_reg_on    = (bool) CEOVW_Options::get('registration_enabled');
     5$ceovw_forgot_on = (bool) CEOVW_Options::get('forgot_enabled');
    196?>
    207
    21 <div id="ceovw-corporate-app" style="--ceovw-brand: <?php echo esc_attr(get_option('naimur-email-otp-for-woocommerce_brand_color', '#2563eb')); ?>;">
     8<div id="ceovw-corporate-app" style="--ceovw-brand: <?php echo esc_attr(CEOVW_Options::get('brand_color')); ?>;">
    229  <div class="ceovw-app-card">
    23      
     10
    2411      <!-- 1. LOGIN VIEW -->
    2512      <div id="ceovw-view-login" class="ceovw-view-pane active">
    2613        <div class="ceovw-header">
    2714            <h2><?php esc_html_e('Welcome Back', 'naimur-email-otp-for-woocommerce'); ?></h2>
    28             <p><?php esc_html_e('Access your dashboard', 'naimur-email-otp-for-woocommerce'); ?></p>
     15            <p><?php esc_html_e('Sign in to your account', 'naimur-email-otp-for-woocommerce'); ?></p>
    2916        </div>
    3017        <form id="ceovw-form-login" method="post">
    3118          <?php wp_nonce_field( 'woocommerce-login', 'woocommerce-login-nonce' ); ?>
    3219          <div class="ceovw-field-group">
    33             <label><?php esc_html_e('Email Address', 'naimur-email-otp-for-woocommerce'); ?></label>
    34             <input type="email" name="username" required autocomplete="username" />
     20            <label for="ceovw-login-email"><?php esc_html_e('Email Address', 'naimur-email-otp-for-woocommerce'); ?></label>
     21            <input type="email" id="ceovw-login-email" name="username" required autocomplete="username" />
    3522          </div>
    3623          <div class="ceovw-field-group">
    37             <label><?php esc_html_e('Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    38             <input type="password" name="password" required autocomplete="current-password" />
     24            <label for="ceovw-login-pass"><?php esc_html_e('Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     25            <input type="password" id="ceovw-login-pass" name="password" required autocomplete="current-password" />
    3926          </div>
    40          
    41           <!-- ডাইনামিক ক্যাপচা রেন্ডার -->
    42           <?php echo $captcha_html; ?>
    4327
    4428          <div class="ceovw-actions-row">
    4529              <label class="ceovw-checkbox"><input type="checkbox" name="rememberme" value="forever" /> <?php esc_html_e('Remember me', 'naimur-email-otp-for-woocommerce'); ?></label>
    46               <a href="#" data-target="forgot" class="ceovw-link ceovw-switch-view"><?php esc_html_e('Forgot password?', 'naimur-email-otp-for-woocommerce'); ?></a>
     30              <?php if ($ceovw_forgot_on) : ?><a href="#" data-target="forgot" class="ceovw-link ceovw-switch-view"><?php esc_html_e('Forgot password?', 'naimur-email-otp-for-woocommerce'); ?></a><?php else : ?><a href="<?php echo esc_url(wc_lostpassword_url()); ?>" class="ceovw-link"><?php esc_html_e('Forgot password?', 'naimur-email-otp-for-woocommerce'); ?></a><?php endif; ?>
    4731          </div>
     32          <div class="ceovw-form-msg" role="status" aria-live="polite" hidden></div>
    4833          <button type="submit" name="login" class="ceovw-btn ceovw-btn-primary">
    4934              <span class="btn-text"><?php esc_html_e('Sign In', 'naimur-email-otp-for-woocommerce'); ?></span>
    50               <span class="btn-loader"></span>
     35              <span class="btn-loader" aria-hidden="true"></span>
    5136          </button>
    5237        </form>
     38        <?php if ($ceovw_reg_on) : ?>
    5339        <div class="ceovw-footer-nav">
    5440            <p><?php esc_html_e("Don't have an account?", 'naimur-email-otp-for-woocommerce'); ?> <a href="#" data-target="register" class="ceovw-link ceovw-switch-view"><?php esc_html_e('Sign up', 'naimur-email-otp-for-woocommerce'); ?></a></p>
    5541        </div>
     42        <?php endif; ?>
    5643      </div>
    5744
     45      <?php if ($ceovw_reg_on) : ?>
    5846      <!-- 2. REGISTER VIEW -->
    5947      <div id="ceovw-view-register" class="ceovw-view-pane" style="display:none;">
    6048        <div class="ceovw-header">
    6149            <h2><?php esc_html_e('Create Account', 'naimur-email-otp-for-woocommerce'); ?></h2>
    62             <p><?php esc_html_e('Join our platform today', 'naimur-email-otp-for-woocommerce'); ?></p>
     50            <p><?php esc_html_e("We'll email you a 6-digit code to confirm your address.", 'naimur-email-otp-for-woocommerce'); ?></p>
    6351        </div>
    64         <form id="ceovw-form-register" method="post">
     52        <form id="ceovw-form-register" method="post" novalidate>
    6553          <div class="ceovw-field-group">
    66             <label><?php esc_html_e('Full Name', 'naimur-email-otp-for-woocommerce'); ?></label>
    67             <input type="text" name="ceovw_full_name" required />
     54            <label for="ceovw-reg-name"><?php esc_html_e('Full Name', 'naimur-email-otp-for-woocommerce'); ?></label>
     55            <input type="text" id="ceovw-reg-name" name="ceovw_full_name" required autocomplete="name" />
    6856          </div>
    6957          <div class="ceovw-field-group">
    70             <label><?php esc_html_e('Email Address', 'naimur-email-otp-for-woocommerce'); ?></label>
    71             <input type="email" name="email" required />
     58            <label for="ceovw-reg-email"><?php esc_html_e('Email Address', 'naimur-email-otp-for-woocommerce'); ?></label>
     59            <input type="email" id="ceovw-reg-email" name="email" required autocomplete="email" />
    7260          </div>
    7361          <div class="ceovw-field-row">
    7462              <div class="ceovw-field-group">
    75                 <label><?php esc_html_e('Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    76                 <input type="password" name="password" required />
     63                <label for="ceovw-reg-pass"><?php esc_html_e('Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     64                <input type="password" id="ceovw-reg-pass" name="password" required autocomplete="new-password" />
    7765              </div>
    7866              <div class="ceovw-field-group">
    79                 <label><?php esc_html_e('Confirm Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    80                 <input type="password" name="password_confirm" required />
     67                <label for="ceovw-reg-pass2"><?php esc_html_e('Confirm Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     68                <input type="password" id="ceovw-reg-pass2" name="password_confirm" required autocomplete="new-password" />
    8169              </div>
    8270          </div>
    83 
    84           <!-- ডাইনামিক ক্যাপচা রেন্ডার -->
    85           <?php echo $captcha_html; ?>
    86 
     71          <div class="ceovw-form-msg" role="alert" aria-live="assertive" hidden></div>
    8772          <button type="submit" class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-reg">
    8873              <span class="btn-text"><?php esc_html_e('Sign Up & Verify', 'naimur-email-otp-for-woocommerce'); ?></span>
    89               <span class="btn-loader"></span>
     74              <span class="btn-loader" aria-hidden="true"></span>
    9075          </button>
    9176        </form>
    … …  
    9479        </div>
    9580      </div>
     81      <?php endif; ?>
    9682
     83      <?php if ($ceovw_forgot_on) : ?>
    9784      <!-- 3. FORGOT PASSWORD VIEW -->
    9885      <div id="ceovw-view-forgot" class="ceovw-view-pane" style="display:none;">
    9986        <div class="ceovw-header">
    10087            <h2><?php esc_html_e('Reset Password', 'naimur-email-otp-for-woocommerce'); ?></h2>
    101             <p><?php esc_html_e('Enter your email to receive a secure OTP', 'naimur-email-otp-for-woocommerce'); ?></p>
     88            <p><?php esc_html_e("Enter your email and we'll send you a 6-digit code.", 'naimur-email-otp-for-woocommerce'); ?></p>
    10289        </div>
    103         <form id="ceovw-form-forgot" method="post">
     90        <form id="ceovw-form-forgot" method="post" novalidate>
    10491          <div class="ceovw-field-group">
    105             <label><?php esc_html_e('Registered Email', 'naimur-email-otp-for-woocommerce'); ?></label>
    106             <input type="email" name="reset_email" required />
     92            <label for="ceovw-forgot-email"><?php esc_html_e('Registered Email', 'naimur-email-otp-for-woocommerce'); ?></label>
     93            <input type="email" id="ceovw-forgot-email" name="reset_email" required autocomplete="email" />
    10794          </div>
    108 
    109           <!-- ডাইনামিক ক্যাপচা রেন্ডার -->
    110           <?php echo $captcha_html; ?>
    111 
     95          <div class="ceovw-form-msg" role="alert" aria-live="assertive" hidden></div>
    11296          <button type="submit" class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-forgot">
    11397              <span class="btn-text"><?php esc_html_e('Send Verification Code', 'naimur-email-otp-for-woocommerce'); ?></span>
    114               <span class="btn-loader"></span>
     98              <span class="btn-loader" aria-hidden="true"></span>
    11599          </button>
    116100        </form>
    … …  
    124108        <div class="ceovw-header">
    125109            <h2><?php esc_html_e('Set New Password', 'naimur-email-otp-for-woocommerce'); ?></h2>
    126             <p><?php esc_html_e('Secure your account with a new password', 'naimur-email-otp-for-woocommerce'); ?></p>
     110            <p><?php esc_html_e('Choose a new password for your account.', 'naimur-email-otp-for-woocommerce'); ?></p>
    127111        </div>
    128         <form id="ceovw-form-newpass" method="post">
     112        <form id="ceovw-form-newpass" method="post" novalidate>
    129113          <input type="hidden" name="verified_email" id="ceovw-verified-email" value="">
    130114          <div class="ceovw-field-group">
    131             <label><?php esc_html_e('New Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    132             <input type="password" name="new_password" required />
     115            <label for="ceovw-new-pass"><?php esc_html_e('New Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     116            <input type="password" id="ceovw-new-pass" name="new_password" required autocomplete="new-password" />
    133117          </div>
    134118          <div class="ceovw-field-group">
    135             <label><?php esc_html_e('Confirm New Password', 'naimur-email-otp-for-woocommerce'); ?></label>
    136             <input type="password" name="new_password_confirm" required />
     119            <label for="ceovw-new-pass2"><?php esc_html_e('Confirm New Password', 'naimur-email-otp-for-woocommerce'); ?></label>
     120            <input type="password" id="ceovw-new-pass2" name="new_password_confirm" required autocomplete="new-password" />
    137121          </div>
     122          <div class="ceovw-form-msg" role="alert" aria-live="assertive" hidden></div>
    138123          <button type="submit" class="ceovw-btn ceovw-btn-primary" id="ceovw-btn-setpass">
    139               <span class="btn-text"><?php esc_html_e('Update & Login', 'naimur-email-otp-for-woocommerce'); ?></span>
    140               <span class="btn-loader"></span>
     124              <span class="btn-text"><?php esc_html_e('Update Password', 'naimur-email-otp-for-woocommerce'); ?></span>
     125              <span class="btn-loader" aria-hidden="true"></span>
    141126          </button>
    142127        </form>
    143128      </div>
     129      <?php endif; ?>
    144130
    145131  </div>
  • naimur-email-otp-for-woocommerce/trunk/uninstall.php

    r3780003 r3780739  
    1616delete_option('ceovw_db_version');
    1717delete_option('ceovw_upgrade_lock');
     18delete_option('ceovw_last_cleanup');
     19
     20// Settings and email templates are kept unless the store owner chose to delete them (General Settings).
     21$ceovw_settings = get_option('ceovw_settings', array());
     22if (is_array($ceovw_settings) && !empty($ceovw_settings['delete_data_on_uninstall'])) {
     23    foreach (array('ceovw_settings', 'ceovw_email_templates', 'ceovw_test_email_sent_at') as $ceovw_option) {
     24        delete_option($ceovw_option);
     25    }
     26    $ceovw_legacy = $wpdb->get_col($wpdb->prepare("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like('naimur-email-otp-for-woocommerce_') . '%'));
     27    foreach ($ceovw_legacy as $ceovw_option) {
     28        delete_option($ceovw_option);
     29    }
     30}
Note: See TracChangeset for help on using the changeset viewer.