init(); } /** * Boot sequence. */ private function init(): void { // No manual textdomain loading here — this plugin's text domain // (aawahid-simple-deposits-for-woocommerce) matches its WordPress.org // slug, so WordPress automatically loads the correct translation // for every string as needed (has done since WP 4.6). Calling // load_plugin_textdomain() ourselves is redundant and is flagged // by Plugin Check as a discouraged function, so it's intentionally // not present anywhere in this plugin. if ( ! $this->is_woocommerce_active() ) { add_action( 'admin_notices', [ $this, 'woocommerce_missing_notice' ] ); return; } $this->wire_modules(); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ] ); } /** * Enqueue admin-only CSS. Scoped to product edit screens and the * WooCommerce settings screen so it never loads site-wide. */ public function enqueue_admin_assets( string $hook ): void { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; $is_product_screen = $screen && in_array( $screen->id, [ 'product', 'edit-product' ], true ); // Read-only check of which admin screen we're on, purely to decide // whether to enqueue a stylesheet — no data is processed or // persisted, so there's nothing here for a nonce to protect. // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- read-only screen check, no state change; sanitized via sanitize_text_field()+wp_unslash() below regardless. $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- read-only screen check, no state change; sanitized via sanitize_text_field()+wp_unslash() below regardless. $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; $is_wc_settings = ( 'wc-settings' === $page && 'sdfw_deposits' === $tab ); if ( $is_wc_settings ) { wp_enqueue_script( 'sdfw-admin', SDFW_URL . 'assets/js/admin.js', [], SDFW_VERSION, true ); } if ( ! $is_product_screen && ! $is_wc_settings ) { return; } wp_enqueue_style( 'sdfw-admin', SDFW_URL . 'assets/css/admin.css', [], SDFW_VERSION ); } /** * Enqueue frontend assets. * * frontend.css is shared by three independent features — the * single product page deposit badge (SDFW_Product_Badge), the * cart/checkout deposit breakdown rows (SDFW_Cart), and the * checkout payment-mode toggle (SDFW_Gateway) — so it loads on all * three page types. product-badge.js and its localised variation * amounts are only ever relevant on the single product page, so * those stay scoped to is_product() alone. frontend.js (the * payment-mode toggle's checkout-refresh trigger) is scoped to * is_checkout() alone for the same reason. */ public function enqueue_frontend_assets(): void { $is_product = function_exists( 'is_product' ) && is_product(); $is_cart = function_exists( 'is_cart' ) && is_cart(); $is_checkout = function_exists( 'is_checkout' ) && is_checkout(); if ( ! $is_product && ! $is_cart && ! $is_checkout ) { return; } wp_enqueue_style( 'sdfw-frontend', SDFW_URL . 'assets/css/frontend.css', [], SDFW_VERSION ); if ( $is_checkout ) { // No explicit 'jquery' dependency: frontend.js only reads // jQuery off the global if WooCommerce's own checkout // script has already loaded it on this page. wp_enqueue_script( 'sdfw-payment-mode-toggle', SDFW_URL . 'assets/js/frontend.js', [], SDFW_VERSION, true ); } if ( ! $is_product ) { return; } // No explicit 'jquery' dependency: product-badge.js only reads // jQuery off the global if WooCommerce's own variation-form // script has already loaded it on this page. wp_enqueue_script( 'sdfw-product-badge', SDFW_URL . 'assets/js/product-badge.js', [], SDFW_VERSION, true ); $current_product = null; if ( isset( $GLOBALS['product'] ) && $GLOBALS['product'] instanceof WC_Product ) { $current_product = $GLOBALS['product']; } elseif ( function_exists( 'get_the_ID' ) ) { $current_product = wc_get_product( get_the_ID() ); } $variation_amounts = []; if ( $current_product instanceof WC_Product && $current_product->is_type( 'variable' ) ) { foreach ( $current_product->get_children() as $variation_id ) { // Pre-formatted (via sdfw_format_price()) so the JS can // drop the string straight into the badge without // reimplementing WooCommerce's currency formatting. $variation = wc_get_product( $variation_id ); $variation_amounts[ $variation_id ] = sdfw_format_price( sdfw_get_product_amount( $variation_id, $variation ? (float) $variation->get_price() : null ) ); } } wp_localize_script( 'sdfw-product-badge', 'sdfw_variation_amounts', $variation_amounts ); } /** * Check WooCommerce is active and loaded. Mirrors the theme's * fd_wc_active() defensive pattern so this plugin never fatals * if WooCommerce is toggled off. */ public function is_woocommerce_active(): bool { return class_exists( 'WooCommerce' ); } /** * Admin notice shown when WooCommerce is missing/inactive. * Plugin otherwise does nothing — no fatals, no partial behavior. */ public function woocommerce_missing_notice(): void { if ( ! current_user_can( 'activate_plugins' ) ) { return; } $message = sprintf( /* translators: %s: plugin name */ esc_html__( '%s requires WooCommerce to be installed and active. The plugin is currently inactive.', 'aawahid-simple-deposits-for-woocommerce' ), '' . esc_html__( 'Simple Deposits for WooCommerce', 'aawahid-simple-deposits-for-woocommerce' ) . '' ); echo '

' . wp_kses_post( $message ) . '

'; } /** * Instantiate the feature classes. Only reached when WooCommerce * is confirmed active. */ private function wire_modules(): void { // The CORRECT way to register a custom WC_Settings_Page tab: // hook into 'woocommerce_get_settings_pages', include the class // file there (WC_Settings_Page is guaranteed to exist at that // point), instantiate, and return the instance as part of the // pages array. WooCommerce collects all settings pages through // this filter before building the tab list — anything wired // outside this filter simply never appears. add_filter( 'woocommerce_get_settings_pages', [ $this, 'register_settings_page' ] ); if ( class_exists( 'SDFW_Product_Field' ) ) { $this->product_field = new SDFW_Product_Field(); } if ( class_exists( 'SDFW_Product_Badge' ) ) { $this->product_badge = new SDFW_Product_Badge(); } // Not a WC_Payment_Gateway (see class-sdfw-gateway.php docblock), // so unlike the old version of this class it doesn't need a // WooCommerce-owned filter to instantiate it. The class file // itself loads eagerly via the main plugin bootstrap's include // loader now, so it's just wired up directly here. if ( class_exists( 'SDFW_Gateway' ) ) { $this->gateway = new SDFW_Gateway(); } // Not yet built (see project blueprint §3.7 / §3.9) — guarded the // same way as the controllers above so wiring these in later is // just a matter of adding the class file, no further changes here. if ( class_exists( 'SDFW_Cart' ) ) { $this->cart = new SDFW_Cart(); } if ( class_exists( 'SDFW_Order' ) ) { $this->order = new SDFW_Order(); } } /** * Load, instantiate, and return our settings page inside the * 'woocommerce_get_settings_pages' filter. WC_Settings_Page is * guaranteed to exist here because WooCommerce includes its admin * class files before firing this filter. * * @param array $pages Existing WC settings page instances. * @return array */ public function register_settings_page( array $pages ): array { $settings_file = SDFW_DIR . 'includes/class-sdfw-settings.php'; if ( file_exists( $settings_file ) ) { require_once $settings_file; } if ( class_exists( 'SDFW_Settings' ) ) { $this->settings = new SDFW_Settings(); $pages[] = $this->settings; } return $pages; } // Prevent cloning and unserialization of the singleton. private function __clone() {} public function __wakeup() { throw new \Exception( 'Cannot unserialize a singleton.' ); } }