6, 'code_ttl' => 180, 'resend_after' => 60, 'max_attempts' => 5, 'channel' => 'sms', 'allow_register' => true, 'redirect_to' => '', 'theme_panel' => true, 'checkout_gate' => false, 'voice_enabled' => false, 'login_mode' => LoginPanel::MODE_PASSWORD_FIRST, 'ip_limit_count' => 10, 'ip_limit_window' => 300, 'identifier' => self::IDENTIFIER_MOBILE, 'ask_name' => true, 'phone_limit_count' => 3, 'phone_limit_window' => 600, 'phone_daily_limit' => 10, 'test_mode' => false, 'test_phones' => '', 'test_ips' => '', 'skin' => SkinRegistry::DEFAULT_SKIN, 'skin_accent' => SkinRegistry::DEFAULT_ACCENT, 'login_page' => 0, 'page_minimal' => true, 'replace_wc_login' => false, 'replace_wp_login' => false, // Monitoring. Kept off the settings screen: the monitoring screen // edits these beside the figures they act on. 'log_days' => 30, 'failsafe_threshold' => 5, 'alert_phones' => '', 'alert_fail_on' => true, 'alert_fail_streak' => 5, 'alert_fail_rate' => 50, 'alert_fail_window' => 30, 'alert_fail_min' => 10, 'alert_failsafe_on' => true, 'alert_volume_on' => false, 'alert_volume_hour' => 100, 'alert_cooldown' => 60, ]; } /** * {@inheritDoc} */ public function settingsSchema(): array { $schema = [ 'identifier' => [ 'type' => 'select', 'label' => __( 'مشتری با چه چیزی وارد شود', 'rahkar-toolkit' ), 'options' => [ self::IDENTIFIER_MOBILE => __( 'فقط شماره موبایل', 'rahkar-toolkit' ), self::IDENTIFIER_MOBILE_EMAIL => __( 'شماره موبایل یا ایمیل (ایمیل با رمز عبور)', 'rahkar-toolkit' ), ], 'help' => __( 'در هر دو حالت ورود و ثبت‌نام یکی است: اگر شماره حساب داشته باشد وارد می‌شود و اگر نداشته باشد حساب ساخته می‌شود. فرم هیچ‌وقت نمی‌گوید «حسابی یافت نشد».', 'rahkar-toolkit' ), 'default' => self::IDENTIFIER_MOBILE, 'section' => 'general', ], 'ask_name' => [ 'type' => 'checkbox', 'label' => __( 'پرسیدن نام از مشتری جدید', 'rahkar-toolkit' ), 'help' => __( 'بعد از کد پرسیده می‌شود، پس مشتری قدیمی اصلاً آن را نمی‌بیند. بدون نام، سفارش‌ها و فاکتورها به‌جای نام، شماره موبایل نشان می‌دهند.', 'rahkar-toolkit' ), 'default' => true, 'section' => 'general', ], 'channel' => [ 'type' => 'select', 'label' => __( 'کانال ارسال کد', 'rahkar-toolkit' ), 'options' => [ 'sms' => __( 'پیامک', 'rahkar-toolkit' ), 'email' => __( 'ایمیل', 'rahkar-toolkit' ), 'both' => __( 'هر دو', 'rahkar-toolkit' ), ], 'default' => 'sms', 'section' => 'general', ], 'code_length' => [ 'type' => 'number', 'label' => __( 'طول کد', 'rahkar-toolkit' ), 'default' => 6, 'min' => 4, 'max' => 8, 'section' => 'general', ], 'code_ttl' => [ 'type' => 'number', 'label' => __( 'اعتبار کد (ثانیه)', 'rahkar-toolkit' ), 'default' => 180, 'min' => 60, 'max' => 900, 'section' => 'general', ], 'resend_after' => [ 'type' => 'number', 'label' => __( 'فاصله تا ارسال مجدد (ثانیه)', 'rahkar-toolkit' ), 'default' => 60, 'min' => 30, 'max' => 600, 'section' => 'general', ], 'allow_register' => [ 'type' => 'checkbox', 'label' => __( 'ثبت‌نام کاربر جدید مجاز باشد', 'rahkar-toolkit' ), 'help' => __( 'در حالت خاموش، فقط شماره‌هایی که از قبل حساب دارند می‌توانند وارد شوند.', 'rahkar-toolkit' ), 'default' => true, 'section' => 'general', ], 'redirect_to' => [ 'type' => 'text', 'label' => __( 'مقصد پس از ورود', 'rahkar-toolkit' ), 'help' => __( 'خالی بگذارید تا به صفحه حساب کاربری ووکامرس برود. نشانی خارج از دامنه پذیرفته نمی‌شود.', 'rahkar-toolkit' ), 'default' => '', 'section' => 'general', ], 'theme_panel' => [ 'type' => 'checkbox', 'label' => __( 'افزودن ورود پیامکی به فرم ورود قالب', 'rahkar-toolkit' ), 'help' => __( 'گزینه ورود پیامکی داخل فرم ورود و ثبت‌نام هر قالبی که فعال باشد قرار می‌گیرد و ظاهرش را از همان قالب می‌گیرد. در وودمارت همه نقاط پوشش داده می‌شود: پاپ‌آپ هدر، صفحه حساب کاربری، ورود در تسویه‌حساب، ویجت المنتور و بلاک گوتنبرگ. تا وقتی کاربر با فرم کار نکند هیچ فایل اضافه‌ای دانلود نمی‌شود.', 'rahkar-toolkit' ), 'default' => true, 'section' => 'theme', ], 'login_mode' => [ 'type' => 'select', 'label' => __( 'روش پیش‌فرض ورود', 'rahkar-toolkit' ), 'options' => [ LoginPanel::MODE_PASSWORD_FIRST => __( 'رمز عبور — با دکمه «ورود با کد پیامکی»', 'rahkar-toolkit' ), LoginPanel::MODE_OTP_FIRST => __( 'کد پیامکی — با امکان بازگشت به رمز عبور', 'rahkar-toolkit' ), LoginPanel::MODE_OTP_ONLY => __( 'فقط کد پیامکی — فرم رمز عبور نمایش داده نمی‌شود', 'rahkar-toolkit' ), ], 'default' => LoginPanel::MODE_PASSWORD_FIRST, 'section' => 'theme', ], 'checkout_gate' => [ 'type' => 'checkbox', 'label' => __( 'تأیید موبایل قبل از تسویه‌حساب', 'rahkar-toolkit' ), 'help' => $this->checkoutGateHelp(), 'default' => false, 'section' => 'theme', ], 'max_attempts' => [ 'type' => 'number', 'label' => __( 'حداکثر تلاش برای هر کد', 'rahkar-toolkit' ), 'default' => 5, 'min' => 3, 'max' => 10, 'section' => 'security', ], 'ip_limit_count' => [ 'type' => 'number', 'label' => __( 'حداکثر درخواست از هر IP', 'rahkar-toolkit' ), 'default' => 10, 'min' => 3, 'max' => 100, 'section' => 'security', ], 'ip_limit_window' => [ 'type' => 'number', 'label' => __( 'بازه محدودیت IP (ثانیه)', 'rahkar-toolkit' ), 'default' => 300, 'min' => 60, 'max' => 3600, 'section' => 'security', ], 'phone_limit_count' => [ 'type' => 'number', 'label' => __( 'حداکثر درخواست کد برای هر شماره', 'rahkar-toolkit' ), 'default' => 3, 'min' => 1, 'max' => 20, 'section' => 'security', ], 'phone_limit_window' => [ 'type' => 'number', 'label' => __( 'بازه محدودیت هر شماره (ثانیه)', 'rahkar-toolkit' ), 'default' => 600, 'min' => 60, 'max' => 86400, 'section' => 'security', ], 'phone_daily_limit' => [ 'type' => 'number', 'label' => __( 'سقف روزانه کد برای هر شماره', 'rahkar-toolkit' ), 'help' => __( 'صفر یعنی بدون سقف. بدون آن، کسی که IP عوض می‌کند می‌تواند صدها پیامک به گوشی یک نفر بفرستد و هزینه‌اش پای شماست.', 'rahkar-toolkit' ), 'default' => 10, 'min' => 0, 'max' => 200, 'section' => 'security', ], 'test_mode' => [ 'type' => 'checkbox', 'label' => __( 'حالت آزمایشی', 'rahkar-toolkit' ), 'help' => __( 'برای شماره‌ها و IPهای زیر، کد به‌جای ارسال روی صفحه نشان داده می‌شود. اگر فهرست‌ها خالی باشند شامل هیچ‌کس نمی‌شود؛ حالت آزمایشیِ همگانی روی فروشگاه زنده یعنی هر کسی می‌تواند به‌جای هر کسی وارد شود.', 'rahkar-toolkit' ), 'default' => false, 'section' => 'test', ], 'test_phones' => [ 'type' => 'textarea', 'label' => __( 'شماره‌های آزمایشی', 'rahkar-toolkit' ), 'help' => __( 'هر شماره در یک خط، یا با ویرگول جدا.', 'rahkar-toolkit' ), 'default' => '', 'rows' => 3, 'monospace' => true, 'section' => 'test', ], 'test_ips' => [ 'type' => 'textarea', 'label' => __( 'IPهای آزمایشی', 'rahkar-toolkit' ), 'default' => '', 'rows' => 3, 'monospace' => true, 'section' => 'test', ], 'login_page' => [ 'type' => 'select', 'label' => __( 'صفحه ورود', 'rahkar-toolkit' ), 'options' => $this->pageChoices(), 'help' => $this->loginPageHelp(), 'default' => 0, 'section' => 'page', ], 'page_minimal' => [ 'type' => 'checkbox', 'label' => __( 'صفحه ورود بدون هدر و فوتر', 'rahkar-toolkit' ), 'help' => __( 'پیشنهادی. صفحه ورود یک کار دارد و منو، جستجو و سبد خرید در هدر فقط جای دیگری برای کلیک به مشتری نیمه‌راه می‌دهند. بدون آن‌ها کاربران بیشتری ورود را تمام می‌کنند و صفحه روی موبایل سریع‌تر باز می‌شود. لوگو و پیوند بازگشت به فروشگاه می‌مانند.', 'rahkar-toolkit' ), 'default' => true, 'section' => 'page', ], 'replace_wc_login' => [ 'type' => 'checkbox', 'label' => __( 'فرستادن مهمان‌ها از «حساب کاربری من» به صفحه ورود', 'rahkar-toolkit' ), 'help' => __( 'مهمانی که به صفحه حساب کاربری ووکامرس می‌رود، به صفحه ورود بالا فرستاده می‌شود و پس از ورود به همان‌جا برمی‌گردد. فرم ورود داخل قالب‌ها سر جایش می‌ماند.', 'rahkar-toolkit' ), 'default' => false, 'section' => 'page', ], 'replace_wp_login' => [ 'type' => 'checkbox', 'label' => __( 'فقط ورود پیامکی — حذف همه فرم‌های ورود با ایمیل و رمز', 'rahkar-toolkit' ), 'help' => sprintf( /* translators: %s: URL that still shows the password form */ __( 'در همه جا فقط فرم ورود پیامکی می‌ماند: صفحه ورود و ثبت‌نام وردپرس (wp-login.php)، صفحه «حساب کاربری من» ووکامرس، پاپ‌آپ ورود قالب و ورود در تسویه‌حساب. فیلدهای ایمیل و رمز حذف می‌شوند و گزینه‌های «روش پیش‌فرض ورود» و «ورود با موبایل یا ایمیل» نادیده گرفته می‌شوند. فراموشی رمز و خروج مثل قبل کار می‌کنند، و اگر ارسال پیامک از کار بیفتد (failsafe) فرم رمز موقتاً برمی‌گردد. برای مدیری که شماره موبایل روی حسابش ندارد، ورود با رمز از این نشانی همچنان ممکن است — آن را جایی نگه دارید: %s', 'rahkar-toolkit' ), add_query_arg( LoginPage::PASSWORD_ARG, '1', wp_login_url() ) ), 'default' => false, 'section' => 'page', ], 'voice_enabled' => [ 'type' => 'checkbox', 'label' => __( 'دریافت کد با تماس تلفنی', 'rahkar-toolkit' ), 'help' => __( 'در مرحله کد، پس از پایان زمان انتظار، مشتری می‌تواند به‌جای پیامک کد را با تماس صوتی بگیرد — برای وقتی که پیامک دیر می‌رسد یا فیلتر می‌شود. هر تماس مثل یک ارسال در محدودیت‌ها حساب می‌شود. فقط وقتی نشان داده می‌شود که دست‌کم یکی از درگاه‌های فعال تماس صوتی را پشتیبانی کند.', 'rahkar-toolkit' ), 'default' => false, 'section' => 'sms', ], ]; return array_merge( $schema, $this->gatewayPatternFields() ); } /** * Is sign-in by code the only way in? Every password form then gives way to * the code form, and email sign-in inside the form is off too. */ public static function codeOnly(): bool { return (bool) Settings::get( self::SLUG . '.replace_wp_login', false ); } /** * What the form accepts: mobile only, or mobile or email. Always mobile * only when sign-in by code is the only way in. */ public static function identifier(): string { if ( self::codeOnly() ) { return self::IDENTIFIER_MOBILE; } return self::IDENTIFIER_MOBILE_EMAIL === Settings::get( self::SLUG . '.identifier', self::IDENTIFIER_MOBILE ) ? self::IDENTIFIER_MOBILE_EMAIL : self::IDENTIFIER_MOBILE; } /** * Published pages, for the login page picker. Settings screen only. * * @return array */ private function pageChoices(): array { $choices = [ 0 => __( '— انتخاب نشده —', 'rahkar-toolkit' ) ]; // Pages that already have a job are left out: see LoginPage::pageId(). $reserved = LoginPage::reservedPageIds(); foreach ( get_pages( [ 'post_status' => 'publish' ] ) ?: [] as $page ) { if ( ! in_array( (int) $page->ID, $reserved, true ) ) { $choices[ (int) $page->ID ] = $page->post_title; } } return $choices; } /** * Help under the login page picker, with a warning when the saved choice is * one of the pages that cannot be it. */ private function loginPageHelp(): string { $help = __( 'برگه‌ای که فرم ورود در آن نشان داده می‌شود. کد کوتاه [rahkar_login] را در متنش بگذارید، یا خالی بگذارید تا فرم خودکار اضافه شود. بهتر است یک برگه تازه با نامی مثل «ورود» بسازید. برگه‌های ووکامرس (حساب کاربری، تسویه‌حساب، سبد خرید، فروشگاه) و صفحه اصلی در فهرست نیستند: مثلاً اگر «حساب کاربری من» صفحه ورود باشد، کاربرِ واردشده بین آن و خودش گیر می‌کند.', 'rahkar-toolkit' ); $saved = (int) Settings::get( self::SLUG . '.login_page', 0 ); if ( $saved > 0 && in_array( $saved, LoginPage::reservedPageIds(), true ) ) { $help = sprintf( /* translators: %s: page title */ __( '⚠ برگه «%s» که قبلاً انتخاب شده از برگه‌های ووکامرس یا صفحه اصلی است و نمی‌تواند صفحه ورود باشد؛ تا وقتی برگه دیگری انتخاب نکنید، صفحه ورود غیرفعال است.', 'rahkar-toolkit' ), get_the_title( $saved ) ) . ' ' . $help; } return $help; } /** * Help under the checkout gate switch, with a warning when the panel the * gate depends on is switched off. * * The gate only puts WooCommerce's login form in front of the checkout; the * code panel inside that form is the theme integration's. With that off, the * gate would show a guest a password-only form on the one page where they are * about to pay — a dead end, since a guest has no password. Said here rather * than enforced silently: the shop owner is one tick away from a working * combination and should be told which tick. */ private function checkoutGateHelp(): string { $help = __( 'کاربر مهمان پیش از دیدن فرم تسویه‌حساب باید شماره‌اش را با کد پیامکی تأیید کند. سبد خرید، کوپن و جلسه دست‌نخورده می‌مانند و پس از تأیید به همان صفحه تسویه‌حساب برمی‌گردد. برای مشتری واردشده هیچ تغییری ندارد.', 'rahkar-toolkit' ); if ( ! Settings::get( self::SLUG . '.theme_panel', true ) && ! Settings::get( self::SLUG . '.replace_wp_login', false ) ) { $help = __( '⚠ تا وقتی گزینه «افزودن ورود پیامکی به فرم ورود قالب» خاموش است این گزینه کار نمی‌کند: مهمانی که رمز عبور ندارد پشت فرم رمز گیر می‌کند.', 'rahkar-toolkit' ) . ' ' . $help; } return $help; } /** * One pattern field and one variable field per SMS gateway. * * Per gateway because a pattern code is registered in one provider's panel * and means nothing to another; with a backup gateway configured, each needs * its own. Built on the settings screen only — listing gateways loads their * classes, which nothing on a storefront request should pay for. * * @return array> */ private function gatewayPatternFields(): array { $fields = []; foreach ( Gateways::available() as $id => $driver ) { $fields[ OtpDispatcher::patternKey( $id ) ] = [ 'type' => 'text', 'label' => sprintf( /* translators: %s: gateway name */ __( 'کد الگوی ورود — %s', 'rahkar-toolkit' ), $driver->name() ), 'help' => __( 'الگوی تأییدشده در پنل همین درگاه. اگر خالی بماند کد به‌صورت متن آزاد ارسال می‌شود که برای شماره‌های عضو لیست سیاه اپراتور تحویل نمی‌شود.', 'rahkar-toolkit' ), 'default' => '', 'section' => 'sms', 'picker' => [ 'gateway' => $id, 'variable' => OtpDispatcher::variableKey( $id ), ], ]; $fields[ OtpDispatcher::variableKey( $id ) ] = [ 'type' => 'text', 'label' => sprintf( /* translators: %s: gateway name */ __( 'نام متغیر کد در الگو — %s', 'rahkar-toolkit' ), $driver->name() ), 'help' => __( 'همان نامی که هنگام ثبت الگو برای کد انتخاب کرده‌اید، معمولاً otp. اگر اشتباه باشد سرویس‌دهنده ارسال را رد می‌کند بدون آنکه بگوید کدام متغیر مشکل دارد.', 'rahkar-toolkit' ), 'default' => 'otp', 'section' => 'sms', ]; } return $fields; } /** * {@inheritDoc} */ public function settingsSections(): array { return [ 'general' => __( 'عمومی', 'rahkar-toolkit' ), 'sms' => __( 'پیامک کد ورود', 'rahkar-toolkit' ), 'theme' => __( 'ادغام با قالب', 'rahkar-toolkit' ), 'security' => __( 'امنیت و محدودیت', 'rahkar-toolkit' ), 'page' => __( 'صفحه ورود', 'rahkar-toolkit' ), 'test' => __( 'حالت آزمایشی', 'rahkar-toolkit' ), ]; } /** * {@inheritDoc} */ /** * The canonical number a user has proved, or '' when they have not. * * Read from the `rahkar_phone` user meta this module writes when an identity * is linked, so it survives the session the code was entered in — a * moderator looking at a submission next week still sees who sent it. * * @param mixed $phone Whatever an earlier filter supplied. * @param mixed $userId User being asked about; 0 means the current one. * * @internal Hooked callback. */ public function verifiedPhone( $phone, $userId = 0 ): string { if ( is_string( $phone ) && '' !== $phone ) { return $phone; } $userId = (int) $userId ?: get_current_user_id(); if ( $userId < 1 ) { return ''; } return (string) get_user_meta( $userId, 'rahkar_phone', true ); } /** * {@inheritDoc} */ public function register(): void { // Queue handlers first, and in every context: a job is drained by // whichever request comes next — a page view, an admin screen, the worker // — and a job whose handler is not hooked in that request is marked done // without having run. add_action( Hooks::OTP_CLEANUP, static fn() => Housekeeping::run() ); add_action( Hooks::OTP_ALERT, static fn( $payload ) => AlertMonitor::handle( is_array( $payload ) ? $payload : [] ) ); if ( Context::is( Context::ANY_BACKGROUND ) ) { return; } /* * Answer the family's "what number did this person prove?" question. * * Other modules — the testimonial endpoint is the first — need to stamp a * submitter's verified number without reaching into this one. The filter * existed and nothing answered it, so the answer came from whatever the * active theme happened to define, which is backwards. This module is the * one that knows, so it is the one that replies. */ add_filter( 'rahkar_wp_verified_phone', [ $this, 'verifiedPhone' ], 10, 2 ); add_shortcode( 'rahkar_login', static fn( $atts ) => Plugin::make( LoginForm::class )->render( $atts ) ); // Both the logged-out and logged-in variants are registered. Without the // second, a stale page in a customer's browser — cached, or simply left // open — posts to an endpoint that does not exist and gets an unhelpful // `0` back instead of a real message. foreach ( [ 'nopriv_', '' ] as $prefix ) { add_action( 'wp_ajax_' . $prefix . 'rahkar_otp_request', static fn() => Plugin::make( AuthController::class )->requestCode() ); add_action( 'wp_ajax_' . $prefix . 'rahkar_otp_verify', static fn() => Plugin::make( AuthController::class )->verifyCode() ); add_action( 'wp_ajax_' . $prefix . 'rahkar_otp_voice', static fn() => Plugin::make( AuthController::class )->requestVoice() ); add_action( 'wp_ajax_' . $prefix . 'rahkar_otp_password', static fn() => Plugin::make( AuthController::class )->passwordLogin() ); } // Signed-in only: the name step comes after the code, on the new session. add_action( 'wp_ajax_rahkar_otp_name', static fn() => Plugin::make( AuthController::class )->saveName() ); // WordPress's privacy tools: the exporter and eraser run over admin-ajax, // so these are registered for AJAX as well. The suggested policy text is // only wanted on the privacy screens, which is checked before the class // is loaded. if ( is_admin() ) { add_filter( 'wp_privacy_personal_data_exporters', static fn( $exporters ) => Privacy::registerExporter( $exporters ) ); add_filter( 'wp_privacy_personal_data_erasers', static fn( $erasers ) => Privacy::registerEraser( $erasers ) ); add_action( 'admin_init', static function (): void { if ( 'options-privacy.php' === ( $GLOBALS['pagenow'] ?? '' ) ) { Privacy::addPolicyContent(); } } ); } // admin-ajax.php counts as the admin too, so it is excluded: the AJAX // handlers above need the rest of this method no more than the admin does, // but a checkout refresh arriving over AJAX needs the phone pre-fill below. if ( is_admin() && ! wp_doing_ajax() ) { add_filter( 'rahkar_wp_diagnostics_tabs', static fn( array $tabs ): array => $tabs + [ Menu::SLUG . '-monitoring' => __( 'امنیت ورود', 'rahkar-toolkit' ) ] ); add_action( 'admin_menu', static function (): void { add_submenu_page( Menu::SLUG, __( 'پوسته‌های ورود', 'rahkar-toolkit' ), __( 'پوسته‌های ورود', 'rahkar-toolkit' ), Menu::CAPABILITY, Menu::SLUG . '-skins', static fn() => Plugin::make( SkinsPage::class )->render() ); // No parent: a tab of the diagnostics screen, not an entry of // its own. See the note on the log screen in Menu::addPages() // for why this is registration rather than removal. add_submenu_page( '', __( 'امنیت ورود', 'rahkar-toolkit' ), __( 'امنیت ورود', 'rahkar-toolkit' ), Menu::CAPABILITY, Menu::SLUG . '-monitoring', static fn() => Plugin::make( MonitoringPage::class )->render() ); }, 16 ); return; } // The login page, the my-account redirect and skin previews all answer on // `template_redirect`. Hooked only when one of them can apply, decided // from the manifest — so a store that uses none of them pays nothing on // its page views, not even a settings read. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Presence check only; the handler requires an admin. $preview = isset( $_GET['rahkar_login_preview'] ); if ( $preview || $this->bootSwitch( 'login_page' ) || $this->bootSwitch( 'replace_wc_login' ) || $this->bootSwitch( 'replace_wp_login' ) ) { add_action( 'template_redirect', static fn() => Plugin::make( LoginPage::class )->route(), 5 ); } if ( $this->bootSwitch( 'login_page' ) ) { add_filter( 'the_posts', static fn( $posts, $query ) => $query instanceof \WP_Query && $query->is_main_query() ? Plugin::make( LoginPage::class )->injectShortcode( $posts ) : $posts, 10, 2 ); } // WordPress's own login screen, only when the store asked for it. if ( $this->bootSwitch( 'replace_wp_login' ) ) { add_action( 'login_init', static fn() => Plugin::make( LoginPage::class )->replaceWpLogin(), 1 ); } // Not gated on any particular theme: `woocommerce_login_form_start` is // WooCommerce's own hook and every theme's login form fires it, so the // panel lands correctly in Woodmart, in Storefront, and in a theme with // no template override at all. // // The callables stay lazy — resolving the panel here would load it on // every request, including ones that never render a login form. Priority // 5 places it above the username and password fields. // "Code only" needs the panel in the theme's forms to take them over, so it // is hooked for that switch as well as for its own. if ( $this->bootSwitch( 'theme_panel' ) || $this->bootSwitch( 'replace_wp_login' ) ) { add_action( 'woocommerce_login_form_start', static fn() => Plugin::make( LoginPanel::class )->renderLoginPanel(), 5 ); add_action( 'woocommerce_register_form_start', static fn() => Plugin::make( LoginPanel::class )->renderRegisterPanel(), 5 ); // The gate rides on the same panel: it only puts WooCommerce's login // form in front of the checkout, and the code panel inside that form is // what makes it passable for a guest with no password. So it is hooked // inside this block rather than beside it — with the panel off there is // nothing to gate with. // // Deferred to `template_redirect` so the conditional tags it depends on // are answerable, and so the gate class is never loaded on a request // that is not the checkout. if ( $this->bootSwitch( 'checkout_gate' ) ) { add_action( 'template_redirect', static function (): void { if ( function_exists( 'is_checkout' ) && is_checkout() && ! is_order_received_page() ) { Plugin::make( CheckoutGate::class )->register(); } } ); } } // A customer who signed in through the header popup should reach checkout // with their number already there. The filter is cheap and the class stays // lazy behind it — written out here rather than through a static helper on // the class, because calling a static method loads the class and the // laziness would buy nothing. add_filter( 'woocommerce_checkout_get_value', static fn( $value, $input ) => Plugin::make( PhonePrefill::class )->fillPhone( $value, (string) $input ), 10, 2 ); } /** * Queue hook that clears out lapsed codes and rate-limit counters. */ public const CLEANUP_HOOK = Hooks::OTP_CLEANUP; }