false, 'payouts_enabled' => false, 'details_submitted' => false]; } $raw = get_user_meta($seller_user_id, self::USER_META_STRIPE_CONNECT_STATUS, true); if ($raw === '' || $raw === null) { $raw = get_user_meta($seller_user_id, 'ToDownload_stripe_connect_status', true); } $data = []; if (is_array($raw)) { $data = $raw; } elseif (is_string($raw) && $raw !== '') { $decoded = json_decode($raw, true); if (is_array($decoded)) { $data = $decoded; } } return [ 'charges_enabled' => !empty($data['charges_enabled']), 'payouts_enabled' => !empty($data['payouts_enabled']), 'details_submitted' => !empty($data['details_submitted']), ]; } public static function set_stripe_connect_status(int $seller_user_id, bool $charges_enabled, bool $payouts_enabled, bool $details_submitted): void { if ($seller_user_id <= 0) { return; } $val = [ 'charges_enabled' => (bool) $charges_enabled, 'payouts_enabled' => (bool) $payouts_enabled, 'details_submitted' => (bool) $details_submitted, ]; update_user_meta($seller_user_id, self::USER_META_STRIPE_CONNECT_STATUS, $val); // Keep legacy key in sync. update_user_meta($seller_user_id, 'ToDownload_stripe_connect_status', $val); } /** * Returns null when not configured. */ public static function seller_tax_enabled(int $seller_user_id): ?bool { if ($seller_user_id <= 0) { return null; } $raw = get_user_meta($seller_user_id, self::USER_META_TAX_ENABLED, true); if ($raw === '' || $raw === null) { return null; } return (string) $raw === '1' || (int) $raw === 1 || $raw === true; } /** * Returns null when not configured. */ public static function seller_tax_percent(int $seller_user_id): ?float { if ($seller_user_id <= 0) { return null; } $raw = get_user_meta($seller_user_id, self::USER_META_TAX_PERCENT, true); if ($raw === '' || $raw === null) { return null; } $val = (float) $raw; $val = max(0.0, min(100.0, $val)); return $val; } /** * Returns null when not configured. */ public static function seller_default_access_days(int $seller_user_id): ?int { if ($seller_user_id <= 0) { return null; } $raw = get_user_meta($seller_user_id, self::USER_META_DEFAULT_ACCESS_DAYS, true); if ($raw === '' || $raw === null) { return null; } $days = (int) $raw; return max(0, $days); } public static function seller_prefers_discount_preview(int $seller_user_id): bool { if ($seller_user_id <= 0) { return false; } $raw = get_user_meta($seller_user_id, self::USER_META_CHECKOUT_DISCOUNT_PREVIEW, true); return (string) $raw === '1' || (int) $raw === 1 || $raw === true; } /** * Per-seller hosted connection. * * For seller orders, the store_public_id + api_key must be stored on the seller user. * For platform orders (seller_user_id <= 0), this falls back to the global Connect settings. * * @return array{connected:bool,hosted_base:string,hosted_host:string,store_public_id:string,api_key:string} */ public static function hosted_credentials(int $seller_user_id): array { $settings = get_option('ToDownload_settings', []); $settings = is_array($settings) ? $settings : []; $default_base = trim((string) ($settings['hosted_api_base_url'] ?? 'https://2dl.app')); $default_base = $default_base !== '' ? rtrim($default_base, '/') : 'https://2dl.app'; $host = strtolower(sanitize_text_field((string) (wp_parse_url($default_base, PHP_URL_HOST) ?? ''))); $scheme = strtolower(sanitize_text_field((string) (wp_parse_url($default_base, PHP_URL_SCHEME) ?? ''))); $is_wp_host = ($host === 'wp.2dl.app' || ($host !== '' && str_ends_with($host, '.wp.2dl.app'))); $is_http = ($scheme === 'https' || $scheme === 'http'); if ($default_base === '' || $host === '' || !$is_http || $is_wp_host) { $default_base = ($host !== '' && str_contains($host, 'staging')) ? 'https://staging.2dl.app' : 'https://2dl.app'; $host = strtolower((string) (wp_parse_url($default_base, PHP_URL_HOST) ?? '')); } if ($host === 'www.2dl.app') { $default_base = 'https://2dl.app'; } if ($host === 'staging.2dl.app') { $default_base = 'https://staging.2dl.app'; } // Environment selection (production vs staging) is site-level. $base = $default_base; $store_public_id = ''; $api_key = ''; if ($seller_user_id > 0) { $store_raw = get_user_meta($seller_user_id, self::USER_META_HOSTED_STORE_PUBLIC_ID, true); $key_raw = get_user_meta($seller_user_id, self::USER_META_HOSTED_STORE_API_KEY, true); $store_public_id = trim((string) $store_raw); $api_key = trim((string) $key_raw); } else { $store_public_id = trim((string) ($settings['hosted_store_public_id'] ?? '')); $api_key = trim((string) ($settings['hosted_store_api_key'] ?? '')); } $host = (string) wp_parse_url($base, PHP_URL_HOST); if ($host === '') { $host = $base !== '' ? $base : '2dl.app'; } $connected = !($store_public_id === '' || $api_key === ''); return [ 'connected' => $connected, 'hosted_base' => $base, 'hosted_host' => $host, 'store_public_id' => $store_public_id, 'api_key' => $api_key, ]; } public static function register(): void { add_action('init', [self::class, 'ensure_roles_and_caps']); add_filter('login_redirect', [self::class, 'login_redirect'], 10, 3); add_action('admin_init', [self::class, 'maybe_downgrade_current_seller']); Memberships::register(); // Platform admins can assign seller tiers via the user profile screen. add_action('show_user_profile', [self::class, 'render_tier_profile_fields']); add_action('edit_user_profile', [self::class, 'render_tier_profile_fields']); add_action('show_user_profile', [self::class, 'render_connection_profile_fields']); add_action('edit_user_profile', [self::class, 'render_connection_profile_fields']); add_action('personal_options_update', [self::class, 'save_tier_profile_fields']); add_action('edit_user_profile_update', [self::class, 'save_tier_profile_fields']); add_action('admin_post_ToDownload_admin_disconnect_seller_store', [self::class, 'handle_admin_disconnect_seller_store']); add_action('admin_enqueue_scripts', [self::class, 'enqueue_admin_styles']); if (is_admin()) { SellerAdmin::register(); } } public static function render_connection_profile_fields($user): void { if (!current_user_can('manage_options')) { return; } if (!($user instanceof \WP_User)) { return; } $user_id = (int) $user->ID; if ($user_id <= 0) { return; } // Only show for seller-capable users. if (!user_can($user_id, 'edit_todownload_downloads')) { return; } $hosted = self::hosted_credentials($user_id); $hosted_base = (string) ($hosted['hosted_base'] ?? ''); $store_public_id = (string) ($hosted['store_public_id'] ?? ''); $api_key_set = !empty($hosted['api_key']); $connected = !empty($hosted['connected']); $disconnect_url = add_query_arg( [ 'action' => 'ToDownload_admin_disconnect_seller_store', 'user_id' => (string) $user_id, ], admin_url('admin-post.php') ); $disconnect_url = wp_nonce_url($disconnect_url, self::ADMIN_DISCONNECT_NONCE_ACTION, self::ADMIN_DISCONNECT_NONCE_PARAM); echo '

' . esc_html__('2DL Connection', '2download-connector') . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } public static function handle_admin_disconnect_seller_store(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer(self::ADMIN_DISCONNECT_NONCE_ACTION, self::ADMIN_DISCONNECT_NONCE_PARAM); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified above via check_admin_referer(). $user_id = isset($_GET['user_id']) ? absint(wp_unslash($_GET['user_id'])) : 0; if ($user_id <= 0) { wp_die(esc_html__('Missing user.', '2download-connector')); } if (!user_can($user_id, 'edit_todownload_downloads')) { wp_die(esc_html__('Target user is not a seller.', '2download-connector')); } $hosted = self::hosted_credentials($user_id); $base = trim((string) ($hosted['hosted_base'] ?? '')); $store_public_id = trim((string) ($hosted['store_public_id'] ?? '')); if ($base !== '' && $store_public_id !== '') { $cache_key = 'todownload_wp_stripe_status_' . (string) $user_id . '_' . md5(rtrim($base, '/') . '|' . $store_public_id); delete_transient($cache_key); } delete_user_meta($user_id, self::USER_META_HOSTED_API_BASE_URL); delete_user_meta($user_id, self::USER_META_HOSTED_STORE_PUBLIC_ID); delete_user_meta($user_id, self::USER_META_HOSTED_STORE_API_KEY); wp_safe_redirect((string) add_query_arg(['todownload_disconnected' => '1'], (string) get_edit_user_link($user_id))); exit; } public static function enqueue_admin_styles(): void { $screen = get_current_screen(); if ($screen && in_array($screen->id, ['profile', 'user-edit'], true)) { wp_enqueue_style( 'todownload-admin-datepicker', ToDownload_PLUGIN_URL . 'assets/admin-datepicker.css', ['wp-jquery-ui-dialog'], is_readable(ToDownload_PLUGIN_DIR . 'assets/admin-datepicker.css') ? (string) filemtime(ToDownload_PLUGIN_DIR . 'assets/admin-datepicker.css') : ToDownload_VERSION ); } } public static function maybe_downgrade_current_seller(): void { if (!is_admin() || !is_user_logged_in()) { return; } $user_id = get_current_user_id(); if (!current_user_can('edit_todownload_downloads')) { return; } Pricing::maybe_auto_downgrade_if_expired($user_id); } public static function render_tier_profile_fields($user): void { if (!current_user_can('manage_options')) { return; } if (!($user instanceof \WP_User)) { return; } $user_id = (int) $user->ID; $tier = Pricing::normalize_tier((string) get_user_meta($user_id, Pricing::USER_META_TIER, true)); $expires = (string) get_user_meta($user_id, Pricing::USER_META_TIER_EXPIRES_AT, true); $expires = trim($expires); echo '

' . esc_html__('2DL Seller Tier', '2download-connector') . '

'; echo ''; echo ''; echo ''; echo ''; wp_nonce_field('ToDownload_seller_tier_save', 'ToDownload_seller_tier_nonce'); } public static function save_tier_profile_fields(int $user_id): void { if (!current_user_can('manage_options')) { return; } $nonce = isset($_POST['ToDownload_seller_tier_nonce']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_seller_tier_nonce'])) : ''; if ($nonce === '' || !wp_verify_nonce($nonce, 'ToDownload_seller_tier_save')) { return; } $tier = isset($_POST['ToDownload_seller_tier']) ? sanitize_key((string) wp_unslash($_POST['ToDownload_seller_tier'])) : Pricing::TIER_STARTER; $expires = isset($_POST['ToDownload_seller_tier_expires']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_seller_tier_expires'])) : ''; Pricing::set_seller_tier((int) $user_id, $tier, $expires); } public static function ensure_roles_and_caps(): void { $label = did_action('init') ? __('Seller', '2download-connector') : 'Seller'; self::ensure_role(self::ROLE, $label, [ 'read' => true, ]); // Core WordPress capabilities needed to access wp-admin editing screens self::add_caps(self::ROLE, [ 'upload_files', 'edit_posts', 'edit_published_posts', 'publish_posts', 'delete_posts', 'delete_published_posts', ]); // Sellers can manage their own Download products. self::add_caps(self::ROLE, [ 'read_todownload_download', 'edit_todownload_download', 'edit_todownload_downloads', 'publish_todownload_downloads', 'edit_published_todownload_downloads', 'delete_todownload_download', 'delete_todownload_downloads', 'delete_published_todownload_downloads', 'create_todownload_downloads', ]); // Sellers can manage their own discount codes. self::add_caps(self::ROLE, [ 'read_todownload_discount', 'edit_todownload_discount', 'edit_todownload_discounts', 'publish_todownload_discounts', 'edit_published_todownload_discounts', 'delete_todownload_discount', 'delete_todownload_discounts', 'delete_published_todownload_discounts', 'create_todownload_discounts', ]); // Administrators should also have these capabilities. self::add_caps('administrator', [ 'edit_todownload_downloads', 'publish_todownload_downloads', 'edit_published_todownload_downloads', 'edit_others_todownload_downloads', 'read_private_todownload_downloads', 'delete_todownload_downloads', 'delete_published_todownload_downloads', 'delete_others_todownload_downloads', ]); self::add_caps('administrator', [ 'edit_todownload_discounts', 'publish_todownload_discounts', 'edit_published_todownload_discounts', 'edit_others_todownload_discounts', 'read_private_todownload_discounts', 'delete_todownload_discounts', 'delete_published_todownload_discounts', 'delete_others_todownload_discounts', ]); // Ensure our CPT is registered (caps rely on it being present). // (No-op if already registered.) if (post_type_exists(DownloadPostType::POST_TYPE) === false) { DownloadPostType::register_post_type(); } } private static function ensure_role(string $role_key, string $label, array $caps): void { $role = get_role($role_key); if (!$role) { add_role($role_key, $label, $caps); return; } foreach ($caps as $cap => $grant) { if ($grant) { $role->add_cap((string) $cap); } } } /** * Sellers should land in the Seller Dashboard/Onboarding area. * If the login flow tries to send them to a platform-admin page, redirect to Seller area instead. */ public static function login_redirect($redirect_to, $requested_redirect_to, $user): string { if (!($user instanceof \WP_User)) { return (string) $redirect_to; } if (!user_can($user, 'edit_todownload_downloads') || user_can($user, 'manage_options')) { return (string) $redirect_to; } $redirect_to = (string) remove_query_arg(['ToDownload_plan_notice', 'ToDownload_plan_error'], (string) $redirect_to); $requested_redirect_to = (string) remove_query_arg(['ToDownload_plan_notice', 'ToDownload_plan_error'], (string) $requested_redirect_to); $target = (string) ($requested_redirect_to !== '' ? $requested_redirect_to : $redirect_to); $parts = wp_parse_url($target); $path = is_array($parts) && isset($parts['path']) ? (string) $parts['path'] : ''; if ($path === '' || strpos($path, '/wp-admin') === false) { // Respect front-end redirects (e.g. login from a storefront page). return (string) $redirect_to; } $query = is_array($parts) && isset($parts['query']) ? (string) $parts['query'] : ''; $qs = []; if ($query !== '') { parse_str($query, $qs); } $landing = self::is_onboarded((int) $user->ID) ? (string) admin_url('admin.php?page=ToDownload_seller') : (string) admin_url('admin.php?page=ToDownload_seller_onboarding'); $page = isset($qs['page']) ? (string) $qs['page'] : ''; if ($page !== '' && strpos($page, 'ToDownload_') === 0 && strpos($page, 'ToDownload_seller') !== 0) { return $landing; } // If we're going to wp-admin root, send sellers to their onboarding hub. $path_trim = rtrim($path, '/'); if ($requested_redirect_to === '' && ($path_trim === '/wp-admin' || $path_trim === '/wp-admin/index.php')) { return $landing; } return (string) $redirect_to; } private static function add_caps(string $role_key, array $caps): void { $role = get_role($role_key); if (!$role) { return; } foreach ($caps as $cap) { $role->add_cap($cap); } } }