=> $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => __( 'Western', 'woocommerce' ), 'ZM-02' => __( 'Central', 'woocommerce' ), 'ZM-03' => __( 'Eastern', 'woocommerce' ), 'ZM-04' => __( 'Luapula', 'woocommerce' ), 'ZM-05' => __( 'Northern', 'woocommerce' ), 'ZM-06' => __( 'North-Western', 'woocommerce' ), 'ZM-07' => __( 'Southern', 'woocommerce' ), 'ZM-08' => __( 'Copperbelt', 'woocommerce' ), 'ZM-09' => __( 'Lusaka', 'woocommerce' ), 'ZM-10' => __( 'Muchinga', 'woocommerce' ), ), ); => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } } => $engine_key, 'value' => $value, 'to_self' => true, 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], ]; }; $text_providers = self::resolve_ai_providers( 'text' ); $provider_options = static function ( array $providers ): array { $out = []; foreach ( $providers as $id => $provider ) { $out[] = [ 'title' => $provider->get_title(), 'value' => (string) $id, ]; } return $out; }; $elements = [ [ 'id' => 'ai_assist', 'type' => 'page', 'title' => esc_html__( 'AI Assist', 'dokan-lite' ), 'description' => esc_html__( 'Configure AI-powered features to enhance your marketplace experience.', 'dokan-lite' ), 'icon' => 'Sparkles', 'priority' => 300, ], [ 'id' => 'product_generation', 'type' => 'subpage', 'page_id' => 'ai_assist', 'title' => esc_html__( 'Content Generation', 'dokan-lite' ), 'description' => esc_html__( 'Set up AI to elevate your platform with enhanced capabilities.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://dokan.co/docs/wordpress/settings/dokan-ai-assistant/', ], // ===== AI Product Info Generator (text) ===== // Header-only section like the legacy `dokan_ai_product_info` sub_section — no enable toggle, engine selector always visible. [ 'id' => 'product_info_section', 'type' => 'section', 'subpage_id' => 'product_generation', 'title' => esc_html__( 'AI Product Info Generator', 'dokan-lite' ), 'description' => esc_html__( 'Let vendors generate product info by AI', 'dokan-lite' ), ], [ 'id' => 'ai_product_info_engine', 'type' => 'field', 'variant' => 'select', 'section_id' => 'product_info_section', 'title' => esc_html__( 'Engine', 'dokan-lite' ), 'description' => esc_html__( 'Select which AI provider to use for generating content.', 'dokan-lite' ), 'default' => 'openai', 'options' => $provider_options( $text_providers ), 'legacy_key' => 'dokan_ai.dokan_ai_engine', ], ]; // Per-text-provider fieldgroups — dynamic from Manager registry. foreach ( $text_providers as $provider_id => $provider ) { $pid = (string) $provider_id; $elements = array_merge( $elements, self::ai_provider_group( [ 'provider_id' => $pid, 'provider' => $provider, 'section_id' => 'product_info_section', 'engine_field_id' => 'ai_product_info_engine', 'engine_deps' => $when_engine( 'ai_product_info_engine', $pid ), 'api_key_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_api_key', 'model_legacy' => 'dokan_ai.dokan_ai_' . $pid . '_model', 'model_kind' => 'text', 'id_prefix' => 'text', ] ) ); } // NOTE: Product Image Enhancement (the `product_description_section` // and its image engine / per-image-provider groups) is registered by // Dokan Pro via `dokan_get_admin_settings_schema` // (see ProSettingsSchema::ai_assist_settings()). It depends on image // providers that only Pro registers, so it lives entirely in Pro. return $elements; } /** * Resolve registered AI providers for the given kind ('text' | 'image'). * * Returns an empty array when the Intelligence module is not loaded or * the container can't resolve Manager — keeps schema generation * best-effort so a partial bootstrap never breaks the settings UI. * * Public so Dokan Pro can resolve image providers when registering the * Product Image Enhancement schema via `dokan_get_admin_settings_schema`. * * @param string $kind * * @return array */ public static function resolve_ai_providers( string $kind ): array { if ( ! class_exists( '\WeDevs\Dokan\Intelligence\Manager' ) ) { return []; } try { $manager = dokan()->get_container()->get( \WeDevs\Dokan\Intelligence\Manager::class ); return 'image' === $kind ? $manager->get_image_supported_providers() : $manager->get_text_supported_providers(); } catch ( \Throwable $e ) { unset( $e ); return []; } } /** * Build the 5-element per-provider fieldgroup (group + api_info + * api_notice + api_key + model). Shared between the Lite text section and * the Pro image section (registered via `dokan_get_admin_settings_schema`) * so the dynamic per-provider shape stays in lock-step across both * plugins. Public for that cross-plugin reuse. * * @param array $cfg * * @return array */ public static function ai_provider_group( array $cfg ): array { $pid = $cfg['provider_id']; $provider = $cfg['provider']; $toggle_deps = $cfg['toggle_deps'] ?? []; $engine_deps = $cfg['engine_deps'] ?? []; // Section-scoped prefix prevents id collisions when the same // provider id (e.g. "gemini") appears in both text and image // provider registries. $prefix = isset( $cfg['id_prefix'] ) && '' !== $cfg['id_prefix'] ? rtrim( (string) $cfg['id_prefix'], '_' ) . '_' : ''; $group_id = $prefix . $pid . '_api_info_group'; // Build model options + default model id defensively — providers may // not implement get_models_by_type / get_default_model_id. $model_const = 'image' === ( $cfg['model_kind'] ?? '' ) ? '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_IMAGE' : '\WeDevs\Dokan\Intelligence\Services\Model::SUPPORTS_TEXT'; $model_options = []; $default_model = ''; try { $model_type = constant( $model_const ); if ( method_exists( $provider, 'get_models_by_type' ) ) { $models = $provider->get_models_by_type( $model_type ); foreach ( $models as $model_id => $model ) { $model_options[] = [ 'title' => $model->get_title(), 'value' => (string) $model_id, ]; } } // Resolve the default against this field's generation type. A provider // serving both text and image cannot express one default valid for // both, so the untyped getter is only a fallback for providers that // predate the typed one. if ( method_exists( $provider, 'get_default_model_id_by_type' ) ) { $default_model = (string) $provider->get_default_model_id_by_type( $model_type ); } elseif ( method_exists( $provider, 'get_default_model_id' ) ) { $default_model = (string) $provider->get_default_model_id(); } } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch unset( $e ); } $api_key_url = method_exists( $provider, 'get_api_key_url' ) ? (string) $provider->get_api_key_url() : ''; $image_url = method_exists( $provider, 'get_image_url' ) ? (string) $provider->get_image_url() : ''; // Children inherit the group's engine gate; only pin an extra toggle gate (Pro's image switch) when a caller supplies one — Lite text passes none. $api_info = [ 'id' => $prefix . $pid . '_api_info', 'type' => 'field', 'variant' => 'base_field_label', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( '%s API', 'dokan-lite' ), $provider->get_title() ), 'icon' => 'CircleCheck', /* translators: %s: Provider title */ 'description' => sprintf( esc_html__( 'Connect to your %s account with your website.', 'dokan-lite' ), $provider->get_title() ), 'image_url' => $image_url, ]; $api_notice = [ 'id' => $prefix . $pid . '_api_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => $group_id, /* translators: %s: Provider title */ 'title' => sprintf( esc_html__( 'You can get your API Keys in your %s Account.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'link_text' => sprintf( esc_html__( '%s Account', 'dokan-lite' ), $provider->get_title() ), 'link_url' => $api_key_url, 'show_icon' => true, ]; $api_key = [ 'id' => $prefix . $pid . '_api_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => $group_id, 'title' => esc_html__( 'API Key', 'dokan-lite' ), /* translators: %s: Provider title */ 'tooltip' => sprintf( esc_html__( 'Enter your %s API key.', 'dokan-lite' ), $provider->get_title() ), /* translators: %s: Provider title */ 'placeholder' => sprintf( esc_html__( 'Enter your %s API key', 'dokan-lite' ), $provider->get_title() ), 'legacy_key' => $cfg['api_key_legacy'], ]; if ( ! empty( $toggle_deps ) ) { $api_info['dependencies'] = $toggle_deps; $api_notice['dependencies'] = $toggle_deps; $api_key['dependencies'] = $toggle_deps; } return [ [ 'id' => $group_id, 'type' => 'fieldgroup', 'section_id' => $cfg['section_id'], 'dependencies' => $engine_deps, ], $api_info, $api_notice, $api_key, [ 'id' => $prefix . $pid . '_model', 'type' => 'field', 'variant' => 'select', 'section_id' => $cfg['section_id'], 'title' => esc_html__( 'Model', 'dokan-lite' ), 'description' => esc_html__( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ), 'default' => $default_model, 'options' => $model_options, 'legacy_key' => $cfg['model_legacy'], 'dependencies' => empty( $toggle_deps ) ? $engine_deps : array_merge( $toggle_deps, $engine_deps ), ], ]; } /** * Moderation page schema (shell — Pro injects content). * * @return array */ private static function moderation_page(): array { return [ [ 'id' => 'moderation', 'type' => 'page', 'title' => esc_html__( 'Moderation', 'dokan-lite' ), 'description' => esc_html__( 'Configure moderation settings, return policies, and customer request management.', 'dokan-lite' ), 'icon' => 'Settings2', 'priority' => 900, ], // === SubPage: Captcha === [ 'id' => 'captcha', 'type' => 'subpage', 'page_id' => 'moderation', 'title' => esc_html__( 'Captcha', 'dokan-lite' ), 'description' => esc_html__( 'Add bot and spam protection to the forms across your store.', 'dokan-lite' ), 'priority' => 100, 'doc_link' => 'https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration/', 'doc_link_text' => esc_html__( 'Doc', 'dokan-lite' ), ], [ 'id' => 'captcha_section', 'type' => 'section', 'subpage_id' => 'captcha', ], [ 'id' => 'captcha_enable_status', 'type' => 'field', 'variant' => 'switch', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Service', 'dokan-lite' ), 'description' => esc_html__( 'Activate captcha on every form that supports it.', 'dokan-lite' ), 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_enable_status', ], ], [ 'id' => 'captcha_provider', 'type' => 'field', 'variant' => 'select', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Captcha Provider', 'dokan-lite' ), 'description' => esc_html__( 'Select the captcha provider to use for this marketplace', 'dokan-lite' ), 'default' => 'google_recaptcha_v3', 'options' => [ [ 'title' => esc_html__( 'Google reCAPTCHA v3', 'dokan-lite' ), 'value' => 'google_recaptcha_v3', ], [ 'title' => esc_html__( 'Cloudflare Turnstile', 'dokan-lite' ), 'value' => 'cloudflare_turnstile', ], ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'captcha_provider', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'off', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '===', ], ], ], // Provider header — display-only label (logo + title + description, // no toggle button). `default => 'on'` + legacy_key keep the legacy // `recaptcha_enable_status` flag enabled; credential fields render in // the group below. [ 'id' => 'recaptcha_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Google reCAPTCHA v3 Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Google reCAPTCHA v3 credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/admin-settings-icons/social-onboarding/google.svg', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // reCAPTCHA v3 is the selected provider. [ 'id' => 'recaptcha_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'google_recaptcha_v3', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'recaptcha_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'recaptcha_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your reCAPTCHA admin console.', 'dokan-lite' ), 'link_title' => esc_html__( 'Admin Console', 'dokan-lite' ), 'link_url' => 'https://www.google.com/recaptcha/admin', ], [ 'id' => 'recaptcha_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_site_key', ], ], [ 'id' => 'recaptcha_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'recaptcha_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'recaptcha_secret_key', ], ], // Provider header — display-only label (no toggle button), shown when // Turnstile is the selected provider. `default => 'on'` + legacy_key keep // `turnstile_enable_status` enabled; credential fields render below. [ 'id' => 'turnstile_validation_label', 'type' => 'field', 'variant' => 'base_field_label', 'section_id' => 'captcha_section', 'title' => esc_html__( 'Cloudflare Turnstile Validation', 'dokan-lite' ), 'description' => sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag */ esc_html__( 'Cloudflare Turnstile credentials are required to enable captcha for supported forms. %1$sGet Help%2$s', 'dokan-lite' ), '', '' ), 'image_url' => DOKAN_PLUGIN_ASSEST . '/images/cloudflare.png', 'collapsed' => false, 'default' => 'on', 'enable_state' => [ 'label' => esc_html__( 'Enabled', 'dokan-lite' ), 'value' => 'on', ], 'disable_state' => [ 'label' => esc_html__( 'Disabled', 'dokan-lite' ), 'value' => 'off', ], 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_enable_status', ], 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], // Group container for the card's credential fields. A dedicated // `fieldgroup` (not the switch field itself) so `field_group_id` // resolves to a real group element; the switch stays `type: field` // to keep its legacy_key bridging and single-option storage. Carries // the provider show/hide dependencies so credentials hide unless // Turnstile is the selected provider. [ 'id' => 'turnstile_credentials_group', 'type' => 'fieldgroup', 'section_id' => 'captcha_section', 'dependencies' => [ [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_enable_status', 'value' => 'on', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'show', 'comparison' => '===', ], [ 'key' => 'captcha_provider', 'value' => 'cloudflare_turnstile', 'attribute' => 'display', 'effect' => 'hide', 'comparison' => '!==', ], ], ], [ 'id' => 'turnstile_admin_notice', 'type' => 'field', 'variant' => 'info', 'field_group_id' => 'turnstile_credentials_group', 'show_icon' => true, 'title' => esc_html__( 'You can get your Site Key and Secret Key from your Cloudflare Turnstile dashboard.', 'dokan-lite' ), 'link_title' => esc_html__( 'Turnstile Dashboard', 'dokan-lite' ), 'link_url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', ], [ 'id' => 'turnstile_site_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Site Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Site Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile site key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_site_key', ], ], [ 'id' => 'turnstile_secret_key', 'type' => 'field', 'variant' => 'show_hide', 'field_group_id' => 'turnstile_credentials_group', 'title' => esc_html__( 'Secret Key', 'dokan-lite' ), 'placeholder' => esc_html__( 'Secret Key', 'dokan-lite' ), 'tooltip' => esc_html__( 'Insert Cloudflare Turnstile secret key.', 'dokan-lite' ), 'default' => '', 'legacy_key' => [ 'option' => 'dokan_appearance', 'field' => 'turnstile_secret_key', ], ], ]; } /** * Product page schema (shell — Pro injects content). * * @return array */ private static function product_page(): array { return [ [ 'id' => 'product', 'type' => 'page', 'title' => esc_html__( 'Product', 'dokan-lite' ), 'description' => esc_html__( 'Configure product-related settings for your marketplace.', 'dokan-lite' ), 'icon' => 'Box', 'priority' => 200, ], ]; } }