=> $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,
],
];
}
}