init_components(); $this->init_hooks(); } /** * Initialise admin components. */ private function init_components() { new BoxCart_Admin_Menus(); new BoxCart_Admin_Products(); new BoxCart_Admin_Categories(); new BoxCart_Admin_Orders(); new BoxCart_Admin_Locations(); new BoxCart_Admin_Settings(); new BoxCart_Admin_Dashboard_Widget(); new BoxCart_Setup(); // Pro extensions (CSV import/export, etc.) instantiate their admin // controllers from /includes/premium/bootstrap.php, which is only // present in the Pro distribution. Free has no admin extensions. } /** * Initialise hooks. */ private function init_hooks() { add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_notices', array( $this, 'payment_setup_notice' ) ); add_action( 'admin_notices', array( $this, 'customer_role_notice' ) ); add_action( 'set_user_role', array( $this, 'clear_customer_role_check' ) ); add_action( 'deleted_user', array( $this, 'clear_customer_role_check' ) ); } /** * Point out customer accounts that can do more than a customer should * (they also hold a role such as Editor or Administrator). Shown on * BoxCart's screens, the Users screen and the dashboard, to people who * can change roles. */ public function customer_role_notice() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || ! current_user_can( 'promote_users' ) ) { return; } if ( false === strpos( (string) $screen->id, 'boxcart' ) && ! in_array( $screen->id, array( 'users', 'dashboard' ), true ) ) { return; } $count = count( BoxCart_Helpers::get_overprivileged_customer_ids() ); if ( ! $count ) { return; } printf( '

%1$s %2$s %4$s

', esc_html__( 'BoxCart:', 'boxcart' ), esc_html( sprintf( /* translators: %d: number of customer accounts */ _n( '%d customer account also has a role with more access than a customer needs, such as Editor or Administrator. Please check it and set its role to Customer.', '%d customer accounts also have a role with more access than a customer needs, such as Editor or Administrator. Please check them and set their role to Customer.', $count, 'boxcart' ), $count ) ), esc_url( admin_url( 'users.php?role=boxcart_customer' ) ), esc_html__( 'Review customer accounts', 'boxcart' ) ); } /** * Re-check customer roles straight after someone changes a role or * deletes a user, so the notice disappears as soon as it's fixed. */ public function clear_customer_role_check() { delete_transient( 'boxcart_overprivileged_customers' ); } /** * Warn the shop owner, on BoxCart's screens, when the payment settings * don't fit together (for example card payment on without Stripe keys). */ public function payment_setup_notice() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || false === strpos( (string) $screen->id, 'boxcart' ) || ! current_user_can( 'manage_options' ) ) { return; } foreach ( BoxCart_Payment::get_setup_problems() as $problem ) { printf( '

%s %s

', esc_html__( 'BoxCart payments:', 'boxcart' ), esc_html( $problem ) ); } } /** * Get asset version string with file modification time for cache busting. * * @param string $relative_path Asset path relative to plugin directory. * @return string Version string. */ private function asset_version( $relative_path ) { $file = BOXCART_PLUGIN_DIR . $relative_path; if ( file_exists( $file ) ) { return BOXCART_VERSION . '.' . filemtime( $file ); } return BOXCART_VERSION; } /** * Enqueue admin assets. * * @param string $hook Current admin page hook. */ public function enqueue_assets( $hook ) { // Only load on BoxCart admin pages. if ( false === strpos( $hook, 'boxcart' ) ) { return; } $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; // Enqueue admin CSS on all BoxCart pages. $admin_css_path = 'assets/css/admin/boxcart-admin' . $suffix . '.css'; wp_enqueue_style( 'boxcart-admin', BOXCART_PLUGIN_URL . $admin_css_path, array(), $this->asset_version( $admin_css_path ) ); // Always enqueue common utilities on BoxCart admin pages. $common_js_path = 'assets/js/admin/admin-common' . $suffix . '.js'; wp_enqueue_script( 'boxcart-admin-common', BOXCART_PLUGIN_URL . $common_js_path, array(), $this->asset_version( $common_js_path ), true ); // Localize script data for all admin scripts. wp_localize_script( 'boxcart-admin-common', 'boxcartAdmin', $this->get_script_data( $hook ) ); // Pro-Gate assets + localised "Upgrade" strings are owned by the // BoxCart_Pro_Gate class itself. The full implementation ships only // in the Pro distribution zip; the Free shim's enqueue_assets() is a // no-op so this call is safe in both builds. BoxCart_Pro_Gate::enqueue_assets( $suffix ); // Conditionally enqueue page-specific scripts. $this->enqueue_page_scripts( $hook ); // Enqueue media library, sortable, and fonts for settings page. if ( false !== strpos( $hook, 'boxcart-settings' ) ) { wp_enqueue_media(); wp_enqueue_script( 'jquery-ui-sortable' ); // Load local Google Fonts so the typography preview can render them. $fonts_path = 'assets/css/fonts/boxcart-fonts' . $suffix . '.css'; wp_enqueue_style( 'boxcart-fonts', BOXCART_PLUGIN_URL . $fonts_path, array(), $this->asset_version( $fonts_path ) ); } } /** * Enqueue page-specific JavaScript files. * * @param string $hook Current admin page hook. */ private function enqueue_page_scripts( $hook ) { $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; // Products pages (list and edit). if ( false !== strpos( $hook, 'boxcart-products' ) ) { wp_enqueue_media(); $products_path = 'assets/js/admin/admin-products' . $suffix . '.js'; wp_enqueue_script( 'boxcart-admin-products', BOXCART_PLUGIN_URL . $products_path, array( 'boxcart-admin-common' ), $this->asset_version( $products_path ), true ); } // Categories page. if ( false !== strpos( $hook, 'boxcart-categories' ) ) { $categories_path = 'assets/js/admin/admin-categories' . $suffix . '.js'; wp_enqueue_script( 'boxcart-admin-categories', BOXCART_PLUGIN_URL . $categories_path, array( 'boxcart-admin-common' ), $this->asset_version( $categories_path ), true ); } // Locations pages (list, edit, and schedule). if ( false !== strpos( $hook, 'boxcart-locations' ) ) { $locations_path = 'assets/js/admin/admin-locations' . $suffix . '.js'; wp_enqueue_script( 'boxcart-admin-locations', BOXCART_PLUGIN_URL . $locations_path, array( 'boxcart-admin-common' ), $this->asset_version( $locations_path ), true ); } // Import/Export page (import wizard). if ( false !== strpos( $hook, 'boxcart-import-export' ) ) { $import_path = 'assets/js/admin/admin-import-wizard' . $suffix . '.js'; wp_enqueue_script( 'boxcart-admin-import-wizard', BOXCART_PLUGIN_URL . $import_path, array( 'boxcart-admin-common' ), $this->asset_version( $import_path ), true ); } // Settings page. if ( false !== strpos( $hook, 'boxcart-settings' ) ) { $settings_path = 'assets/js/admin/admin-settings' . $suffix . '.js'; wp_enqueue_script( 'boxcart-admin-settings', BOXCART_PLUGIN_URL . $settings_path, array( 'boxcart-admin-common' ), $this->asset_version( $settings_path ), true ); } // Orders page (single order view). if ( false !== strpos( $hook, 'boxcart-orders' ) ) { $orders_path = 'assets/js/admin/admin-orders' . $suffix . '.js'; wp_enqueue_script( 'boxcart-admin-orders', BOXCART_PLUGIN_URL . $orders_path, array( 'boxcart-admin-common' ), $this->asset_version( $orders_path ), true ); } // Setup page (Guided Setup wizard). if ( false !== strpos( $hook, 'boxcart-setup' ) ) { wp_enqueue_media(); $setup_path = 'assets/js/admin/admin-setup' . $suffix . '.js'; wp_enqueue_script( 'boxcart-admin-setup', BOXCART_PLUGIN_URL . $setup_path, array( 'boxcart-admin-common' ), $this->asset_version( $setup_path ), true ); } // Reports page (Chart.js). if ( false !== strpos( $hook, 'boxcart-reports' ) ) { wp_enqueue_script( 'boxcart-chartjs', BOXCART_PLUGIN_URL . 'assets/js/admin/vendor/chart.umd.min.js', array(), '4.4.1', true ); $reports_path = 'assets/js/admin/admin-reports' . $suffix . '.js'; wp_enqueue_script( 'boxcart-admin-reports', BOXCART_PLUGIN_URL . $reports_path, array( 'boxcart-chartjs' ), $this->asset_version( $reports_path ), true ); } } /** * Get localized script data. * * @param string $hook Current admin page hook. * @return array Script data for wp_localize_script. */ private function get_script_data( $hook = '' ) { $data = array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'boxcart_admin_nonce' ), 'pricesEnabled' => BoxCart_Helpers::prices_enabled(), 'currencySymbol' => BoxCart_Helpers::get_currency_symbol(), 'importBatchSize' => 25, 'i18n' => array( // General. 'confirmDelete' => __( 'Are you sure you want to delete this item?', 'boxcart' ), 'saving' => __( 'Saving...', 'boxcart' ), 'saved' => __( 'Saved', 'boxcart' ), 'error' => __( 'An error occurred', 'boxcart' ), 'errorTryAgain' => __( 'An error occurred. Please try again.', 'boxcart' ), 'loading' => __( 'Loading...', 'boxcart' ), 'sending' => __( 'Sending...', 'boxcart' ), 'deleting' => __( 'Deleting...', 'boxcart' ), 'copied' => __( 'Copied!', 'boxcart' ), // Products. 'selectImage' => __( 'Select Product Image', 'boxcart' ), 'useImage' => __( 'Use this image', 'boxcart' ), 'changeImage' => __( 'Change Image', 'boxcart' ), 'uploadImage' => __( 'Upload Image', 'boxcart' ), 'clickToUpload' => __( 'Click to upload', 'boxcart' ), 'add' => __( 'Add', 'boxcart' ), 'adding' => __( 'Adding...', 'boxcart' ), 'duplicating' => __( 'Duplicating...', 'boxcart' ), 'quantityTypeRequired' => __( 'Please add at least one quantity type to define how this product is sold and priced.', 'boxcart' ), 'confirmDeleteProducts' => __( 'Are you sure you want to delete the selected products?', 'boxcart' ), 'confirmSetInStock' => __( 'Set the selected products to In Stock?', 'boxcart' ), 'confirmSetOutOfStock' => __( 'Set the selected products to Out of Stock?', 'boxcart' ), 'confirmDeleteAllProducts' => __( 'Are you sure you want to delete ALL products? This cannot be undone.', 'boxcart' ), 'deleteAllProducts' => __( 'Delete All Products', 'boxcart' ), // Categories. 'addCategory' => __( 'Add Category', 'boxcart' ), 'editCategory' => __( 'Edit Category', 'boxcart' ), 'saveCategory' => __( 'Save Category', 'boxcart' ), 'confirmDeleteCategory' => __( 'Are you sure you want to delete "{name}"?', 'boxcart' ), 'categoryHasProducts' => __( 'This category has {count} product(s). They will be unassigned from this category.', 'boxcart' ), // Locations. 'confirmDeleteLocation' => __( 'Are you sure you want to delete "{name}"?', 'boxcart' ), 'addSlot' => __( 'Add Time Slot', 'boxcart' ), 'editSlot' => __( 'Edit Time Slot', 'boxcart' ), 'saveSlot' => __( 'Save Slot', 'boxcart' ), 'confirmDeleteSlot' => __( 'Are you sure you want to delete this time slot?', 'boxcart' ), 'addClosure' => __( 'Add Closure', 'boxcart' ), 'confirmDeleteClosure' => __( 'Are you sure you want to remove this closure?', 'boxcart' ), 'addOverride' => __( 'Add Override', 'boxcart' ), 'confirmDeleteOverride' => __( 'Are you sure you want to remove this capacity override?', 'boxcart' ), 'endDateBeforeStart' => __( 'End date must be after start date.', 'boxcart' ), // Orders. 'confirmPayment' => __( 'Confirm that payment has been received for this order?', 'boxcart' ), 'confirming' => __( 'Confirming...', 'boxcart' ), 'confirmPaymentReceived' => __( 'Confirm Payment Received', 'boxcart' ), /* translators: %s: amount */ 'confirmRefund' => __( 'Refund %s to the customer\'s card? This can\'t be undone.', 'boxcart' ), 'refundAmountInvalid' => __( 'Enter an amount to refund.', 'boxcart' ), /* translators: %s: day name, e.g. Monday */ 'copyDaySlots' => __( 'Copy %s\'s time slots', 'boxcart' ), 'dialogOk' => __( 'OK', 'boxcart' ), 'closeAnyway' => __( 'Close anyway', 'boxcart' ), 'dialogCancel' => __( 'Cancel', 'boxcart' ), 'wizardSaving' => __( 'Saving...', 'boxcart' ), 'wizardContinue' => __( 'Continue', 'boxcart' ), 'wizardView' => __( 'View', 'boxcart' ), 'wizardChange' => __( 'Change', 'boxcart' ), 'wizardEdit' => __( 'Edit', 'boxcart' ), 'wizardNetwork' => __( 'We couldn\'t reach your site. Check your connection and try again.', 'boxcart' ), /* translators: 1: location name, 2: number of time slots */ 'wizardLocationDone' => __( '%1$s is set up, with %2$d time slots on each open day.', 'boxcart' ), /* translators: %s: product name */ 'wizardProductDone' => __( '%s is in your shop.', 'boxcart' ), 'wizardTimesWrong' => __( 'Each time slot needs to end after it starts.', 'boxcart' ), 'wizardNoDays' => __( 'Choose at least one day customers can collect.', 'boxcart' ), 'wizardNoSlots' => __( 'Add at least one time slot customers can choose.', 'boxcart' ), /* translators: 1: number of time slots, 2: list of days, e.g. "Mon, Tue and Wed" */ 'wizardSlots' => __( 'Customers will choose from %1$d time slots on %2$s.', 'boxcart' ), /* translators: %s: list of days, e.g. "Mon, Tue and Wed" */ 'wizardOneSlot' => __( 'Customers will choose from one time slot on %s.', 'boxcart' ), /* translators: joins the last two days in a list, e.g. "Mon, Tue and Wed" */ 'wizardAnd' => __( 'and', 'boxcart' ), 'wizardChoosePhoto' => __( 'Choose a photo', 'boxcart' ), 'wizardUsePhoto' => __( 'Use this photo', 'boxcart' ), 'wizardAlmostTitle' => __( 'Almost there', 'boxcart' ), 'wizardAlmostIntro' => __( 'A few steps are still to do. Your shop can\'t take orders until it has its pages, a collection point and a product.', 'boxcart' ), 'wizardReadyTitle' => __( 'Your shop is ready', 'boxcart' ), 'wizardReadyIntro' => __( 'Place a test order to see what your customers will see, and check the order emails arrive.', 'boxcart' ), 'chooseCopyDays' => __( 'Choose at least one day to copy to.', 'boxcart' ), 'confirmReplaceSlots' => __( 'The time slots already on those days will be deleted and replaced. Continue?', 'boxcart' ), 'copying' => __( 'Copying...', 'boxcart' ), 'copySlots' => __( 'Copy Slots', 'boxcart' ), 'refunding' => __( 'Refunding...', 'boxcart' ), 'refund' => __( 'Refund', 'boxcart' ), 'sendMessage' => __( 'Send Message', 'boxcart' ), // Settings — media uploads. 'mediaLibraryNotAvailable' => __( 'Media library not available. Please refresh the page.', 'boxcart' ), 'selectEmailLogo' => __( 'Select Email Logo', 'boxcart' ), 'useAsLogo' => __( 'Use as Logo', 'boxcart' ), 'changeLogo' => __( 'Change Logo', 'boxcart' ), 'uploadLogo' => __( 'Upload Logo', 'boxcart' ), 'selectCustomIcon' => __( 'Select Custom Icon', 'boxcart' ), 'useAsIcon' => __( 'Use as Icon', 'boxcart' ), 'changeIcon' => __( 'Change Icon', 'boxcart' ), 'uploadIcon' => __( 'Upload Icon', 'boxcart' ), // Settings — email templates. 'confirmResetTemplate' => __( 'Reset this template to default values?', 'boxcart' ), 'resetting' => __( 'Resetting...', 'boxcart' ), 'resetTemplateFailed' => __( 'Failed to reset template', 'boxcart' ), 'resetToDefault' => __( 'Reset to Default', 'boxcart' ), 'failedToLoadPreview' => __( 'Failed to load preview', 'boxcart' ), 'previewInNewTab' => __( 'Preview in New Tab', 'boxcart' ), 'pleaseEnterEmail' => __( 'Please enter an email address', 'boxcart' ), 'failedToSendTestEmail' => __( 'Failed to send test email', 'boxcart' ), 'sendTestEmail' => __( 'Send Test Email', 'boxcart' ), // Settings — reset & import/export. 'saveFailed' => __( 'Save failed', 'boxcart' ), /* translators: %s: settings tab name */ 'resetTabToDefaults' => __( 'Reset %s to Defaults', 'boxcart' ), 'confirmResetTab' => __( 'Are you sure you want to reset', 'boxcart' ), 'confirmResetTabSuffix' => __( 'settings to their defaults? This cannot be undone.', 'boxcart' ), 'resetComplete' => __( 'Reset complete!', 'boxcart' ), 'resetFailed' => __( 'Reset failed.', 'boxcart' ), 'resetFailedTryAgain' => __( 'Reset failed. Please try again.', 'boxcart' ), 'confirmResetAll' => __( 'Are you sure you want to reset ALL settings to their defaults? This will affect every tab and cannot be undone.', 'boxcart' ), 'confirmResetAllFinal' => __( 'This is your last chance. All custom settings will be lost. Continue?', 'boxcart' ), 'resettingAll' => __( 'Resetting all...', 'boxcart' ), 'allSettingsReset' => __( 'All settings reset!', 'boxcart' ), 'failedToResetTab' => __( 'Failed to reset tab: ', 'boxcart' ), 'resetAllSettings' => __( 'Reset All Settings', 'boxcart' ), 'exporting' => __( 'Exporting...', 'boxcart' ), 'exportFailed' => __( 'Export failed.', 'boxcart' ), 'exportFailedTryAgain' => __( 'Export failed. Please try again.', 'boxcart' ), 'confirmImport' => __( 'Import settings from this file? This will overwrite your current settings (sensitive credentials such as payment provider API keys are preserved). Continue?', 'boxcart' ), 'invalidJsonFile' => __( 'Invalid JSON file. Please select a valid BoxCart settings export file.', 'boxcart' ), 'importing' => __( 'Importing...', 'boxcart' ), 'imported' => __( 'Imported!', 'boxcart' ), 'importFailed' => __( 'Import failed.', 'boxcart' ), 'importFailedTryAgain' => __( 'Import failed. Please try again.', 'boxcart' ), // Import wizard. 'importWizard' => array( 'uploadAndContinue' => __( 'Upload & Continue', 'boxcart' ), 'next' => __( 'Next', 'boxcart' ), 'back' => __( 'Back', 'boxcart' ), 'startImport' => __( 'Start Import', 'boxcart' ), 'uploading' => __( 'Uploading...', 'boxcart' ), 'importing' => __( 'Importing...', 'boxcart' ), 'importComplete' => __( 'Import complete!', 'boxcart' ), 'selectFile' => __( 'Select a CSV file or drag and drop', 'boxcart' ), 'supportedFormats' => __( 'Supports BoxCart, WooCommerce, and Shopify exports (max 5 MB)', 'boxcart' ), 'fileSelected' => __( 'File selected:', 'boxcart' ), 'invalidFileType' => __( 'Please select a CSV file.', 'boxcart' ), 'fileTooLarge' => __( 'File is too large. Maximum size is 5 MB.', 'boxcart' ), 'skipColumn' => __( '— Skip (do not import) —', 'boxcart' ), 'nameRequired' => __( 'The "Name" field must be mapped to import.', 'boxcart' ), 'noColumnsMapped' => __( 'No columns mapped yet.', 'boxcart' ), /* translators: 1: number of columns mapped, 2: number of rows to import */ 'mappingSummary' => __( '%1$d columns mapped, %2$d rows to import', 'boxcart' ), 'detectedFormat' => __( 'Detected:', 'boxcart' ), 'genericFormat' => __( 'Unknown format', 'boxcart' ), 'uploadFailed' => __( 'Upload failed. Please try again.', 'boxcart' ), 'batchFailed' => __( 'Import batch failed. Please try again.', 'boxcart' ), 'errorsLabel' => __( 'Errors', 'boxcart' ), 'coreFields' => __( 'Core Fields', 'boxcart' ), 'qtyType' => __( 'Quantity Type', 'boxcart' ), 'fieldName' => __( 'Name', 'boxcart' ), 'fieldSku' => __( 'SKU', 'boxcart' ), 'fieldDescription' => __( 'Description', 'boxcart' ), 'fieldCategories' => __( 'Categories', 'boxcart' ), 'fieldImageUrl' => __( 'Image URL', 'boxcart' ), 'fieldPrice' => __( 'Price', 'boxcart' ), 'fieldStockStatus' => __( 'Stock Status', 'boxcart' ), 'fieldStockQuantity' => __( 'Stock Quantity', 'boxcart' ), 'fieldPublished' => __( 'Published', 'boxcart' ), 'fieldSortOrder' => __( 'Sort Order', 'boxcart' ), 'fieldSlug' => __( 'Slug', 'boxcart' ), 'fieldParent' => __( 'Parent', 'boxcart' ), 'fieldLabel' => __( 'Label', 'boxcart' ), 'fieldAllowDecimals' => __( 'Allow Decimals', 'boxcart' ), 'fieldStep' => __( 'Step', 'boxcart' ), 'fieldMin' => __( 'Min', 'boxcart' ), 'fieldMax' => __( 'Max', 'boxcart' ), 'undoImport' => __( 'Undo Import', 'boxcart' ), 'undoing' => __( 'Undoing...', 'boxcart' ), /* translators: %d: number of products that will be deleted */ 'undoConfirm' => __( 'Are you sure you want to undo this import? This will delete %d created product(s).', 'boxcart' ), 'undoExpired' => __( 'Undo data not available.', 'boxcart' ), 'undoFailed' => __( 'Undo failed. Please try again.', 'boxcart' ), ), ), ); // Settings-page-specific data. if ( false !== strpos( $hook, 'boxcart-settings' ) ) { $settings = get_option( 'boxcart_settings', array() ); $current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $settings_handler = new BoxCart_Admin_Settings(); $tab_labels = array( 'general' => __( 'General', 'boxcart' ), 'collection' => __( 'Collection', 'boxcart' ), 'orders' => __( 'Orders', 'boxcart' ), 'payments' => __( 'Payments', 'boxcart' ), 'customer' => __( 'Accounts', 'boxcart' ), 'emails' => __( 'Emails', 'boxcart' ), 'styling' => __( 'Styling', 'boxcart' ), 'sidecart' => __( 'Sidecart', 'boxcart' ), 'shortcodes' => __( 'Shortcodes', 'boxcart' ), 'misc' => __( 'Misc', 'boxcart' ), ); $data['currentTab'] = $current_tab; $data['currentTabLabel'] = isset( $tab_labels[ $current_tab ] ) ? $tab_labels[ $current_tab ] : $current_tab; $data['resettableTabs'] = array_keys( $settings_handler->get_resettable_tabs() ); $data['showResetButton'] = ! empty( $settings['show_reset_buttons'] ) && ! in_array( $current_tab, array( 'shortcodes', 'misc' ), true ); } // Products-page-specific data. if ( false !== strpos( $hook, 'boxcart-products' ) ) { $data['productsUrl'] = admin_url( 'admin.php?page=boxcart-products&deleted=1' ); } return $data; } }