handleToggle(); $manager = Plugin::instance()->modules(); $modules = $manager->all(); $enabled = Manifest::enabledModules(); Ui::pageOpen( __( 'ماژول‌های راهکار وردپرس', 'rahkar-toolkit' ), __( 'فقط ماژول‌هایی را روشن کنید که واقعاً استفاده می‌کنید. ماژول خاموش هیچ کدی روی سایت شما اجرا نمی‌کند و هیچ هزینه‌ای روی سرعت ندارد.', 'rahkar-toolkit' ), Help::link( 'modules', __( 'ماژول‌ها', 'rahkar-toolkit' ) ) ); if ( '' !== $notice ) { echo wp_kses( Ui::notice( 'success', $notice ), Ui::allowedHtml() ); } Ui::cardGridOpen(); foreach ( $this->orderedByTier( $modules, $manager ) as $slug => $module ) { $this->renderCard( $slug, $module, $manager, in_array( $slug, $enabled, true ) ); } if ( ! $this->hasProModules( $manager ) ) { $this->renderProCard(); } Ui::cardGridClose(); Ui::pageClose(); } /** * Free modules first, then pro. Catalogue order is kept within each group. * * @param array $modules Modules keyed by slug, in catalogue order. * @param ModuleManager $manager Module manager. * * @return array */ private function orderedByTier( array $modules, ModuleManager $manager ): array { $free = []; $pro = []; foreach ( $modules as $slug => $module ) { if ( Tier::PRO === $manager->tierFor( $slug ) ) { $pro[ $slug ] = $module; } else { $free[ $slug ] = $module; } } return $free + $pro; } /** * Render one module card. * * @param string $slug Module slug. * @param Module $module Module instance. * @param ModuleManager $manager Module manager. * @param bool $isOn Whether the module is switched on. */ private function renderCard( string $slug, Module $module, ModuleManager $manager, bool $isOn ): void { $blockers = $module->unmetRequirements(); $locked = $manager->isLocked( $slug ); $tier = $manager->tierFor( $slug ); // Three states, and the order matters. A locked module is shown as blocked // whatever else is true of it: telling someone their pro module is "off" // invites them to look for the switch that turns it on. $state = 'on'; if ( $locked || [] !== $blockers ) { $state = 'blocked'; } elseif ( ! $isOn ) { $state = 'off'; } $badges = ''; if ( Tier::PRO === $tier ) { $badges .= Ui::badge( Tier::label( Tier::PRO ), 'brand' ); } if ( ! $locked ) { $badges .= $isOn ? Ui::badge( __( 'روشن', 'rahkar-toolkit' ), 'success' ) : Ui::badge( __( 'خاموش', 'rahkar-toolkit' ), 'quiet' ); } // Only an add-on contributes pro rows, so a locked card means the add-on // is installed and its licence is not active — not that anything is // missing from this plugin. $reasons = $locked ? [ __( 'لایسنس نسخه حرفه‌ای روی این سایت فعال نیست.', 'rahkar-toolkit' ) ] : $blockers; Ui::cardOpen( $state, $module->name(), $manager->iconFor( $slug ), $badges, $module->description(), $reasons, Help::link( $slug, $module->name() ), // The Rahkar Foroosh action centre links "switch this on" straight to the card. self::CARD_ID_PREFIX . $slug ); Ui::cardClose( $locked ? $this->upgradeFooter() : $this->toggleFooter( $slug, $isOn, [] !== $blockers ), true ); } /** * The footer of a card the site may switch on and off. * * @param string $slug Module slug. * @param bool $isOn Whether the module is switched on. * @param bool $blocked Whether a requirement is unmet. */ private function toggleFooter( string $slug, bool $isOn, bool $blocked ): string { $settings = $isOn && ! $blocked ? Ui::button( __( 'تنظیمات', 'rahkar-toolkit' ), 'secondary', [], // The settings screen picks its tab from `rahkar_tab`; a hash opened the first tab. admin_url( 'admin.php?page=' . Menu::SLUG . '-settings&rahkar_tab=' . rawurlencode( $slug ) ), 'settings' ) : ''; $toggle = Ui::button( $isOn ? __( 'خاموش کردن', 'rahkar-toolkit' ) : __( 'روشن کردن', 'rahkar-toolkit' ), $isOn ? 'secondary' : 'primary', $blocked && ! $isOn ? [ 'type' => 'submit', 'disabled' => '' ] : [ 'type' => 'submit' ], '', 'power' ); return sprintf( '
%s%s
%s', wp_nonce_field( self::NONCE, '_wpnonce', true, false ), esc_attr( $slug ), $isOn ? 'disable' : 'enable', $toggle, $settings ); } /** * Does anything in the catalogue belong to the pro tier? * * Pro rows only ever come from the add-on, so this is how the screen knows * whether the add-on is installed — without naming it, checking for its * files, or knowing anything about licences. * * @param ModuleManager $manager Module manager. */ private function hasProModules( ModuleManager $manager ): bool { foreach ( $manager->knownSlugs() as $slug ) { if ( Tier::PRO === $manager->tierFor( $slug ) ) { return true; } } return false; } /** * One card pointing at the pro add-on, shown while it is not installed. * * A single card with an outbound link, not a locked card per paid module. * Per-module cards would need this plugin to carry a description of every * paid feature — kept in step with a product it does not ship — and on * wordpress.org a screen full of padlocked features reads as crippleware * rather than as an add-on that exists. */ private function renderProCard(): void { Ui::cardOpen( 'off', __( 'راهکار وردپرس حرفه‌ای', 'rahkar-toolkit' ), 'package', Ui::badge( Tier::label( Tier::PRO ), 'brand' ), __( 'سبد خرید رهاشده، اطلاع موجودی کالا و دعوت به ثبت نظر، در افزونه جداگانه نسخه حرفه‌ای. پس از نصب، ماژول‌هایش همین‌جا کنار بقیه ظاهر می‌شوند.', 'rahkar-toolkit' ), [], Help::link( 'pro', __( 'راهکار وردپرس حرفه‌ای', 'rahkar-toolkit' ) ) ); Ui::cardClose( $this->upgradeFooter(), true ); } /** * The footer of a card this site is not entitled to. * * The default is an offer to buy, which is right on a site that does not have * the add-on. A site that does have it is in the other situation entirely — * the licence is missing, expired or on another domain — and the button should * lead to wherever that is fixed. The free plugin does not know where that is, * so whoever owns licensing says. */ private function upgradeFooter(): string { $defaults = [ 'label' => __( 'تهیه نسخه حرفه‌ای', 'rahkar-toolkit' ), 'url' => self::UPGRADE_URL, 'icon' => 'external', 'external' => true, ]; /** * Filters the button on a module card this site is not entitled to. * * @param array{label:string, url:string, icon:string, external:bool} $action Button definition. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- Inline type assertion for phpstan: a filter may return anything. /** @var mixed $filtered */ $filtered = apply_filters( 'rahkar_wp_upgrade_action', $defaults ); // Merged over the defaults rather than replacing them, so a filter that // only wants to change where the button goes does not have to restate the // label and the icon to keep them. $action = is_array( $filtered ) ? $filtered + $defaults : $defaults; return Ui::button( (string) $action['label'], 'primary', empty( $action['external'] ) ? [] : [ 'target' => '_blank', 'rel' => 'noopener' ], (string) $action['url'], (string) $action['icon'] ); } /** * Apply an enable/disable submission. * * @return string Success notice, or an empty string when nothing was submitted. */ private function handleToggle(): string { if ( ! isset( $_POST['rahkar_module'], $_POST['rahkar_action'] ) ) { return ''; } check_admin_referer( self::NONCE ); $slug = sanitize_key( wp_unslash( $_POST['rahkar_module'] ) ); $action = sanitize_key( wp_unslash( $_POST['rahkar_action'] ) ); $manager = Plugin::instance()->modules(); if ( ! in_array( $slug, $manager->knownSlugs(), true ) ) { return ''; } if ( 'enable' === $action ) { $manager->enable( $slug ); return __( 'ماژول روشن شد.', 'rahkar-toolkit' ); } $manager->disable( $slug ); return __( 'ماژول خاموش شد. تنظیمات آن حفظ شده و با روشن کردن دوباره برمی‌گردد.', 'rahkar-toolkit' ); } }