false, 'message' => __('This site is configured as a 2dl.app connector. Stripe secrets are not stored on seller sites.', '2download-connector')]; } // Connector invariant: seller sites must not call payment processor APIs directly. // This code path is intentionally disabled; Stripe operations are handled centrally by 2dl.app. return ['ok' => false, 'message' => __('Stripe operations are handled centrally by 2dl.app for connector sites.', '2download-connector')]; } private static function platform_stripe_secret_key(): string { return ''; } private static function seller_onboarding_url(array $args = []): string { $url = admin_url('admin.php?page=ToDownload_seller_onboarding'); return !empty($args) ? (string) add_query_arg($args, $url) : (string) $url; } /** * Connector-mode Stripe Connect status is owned by 2dl.app. * Fetch it server-to-server using the seller's Store API key. * * @return array{ok:bool,account_id:string,charges_enabled:bool,payouts_enabled:bool,details_submitted:bool,message?:string} */ private static function hosted_stripe_connect_status(int $seller_user_id): array { $hosted = Sellers::hosted_credentials($seller_user_id); $base = isset($hosted['hosted_base']) ? trim((string) $hosted['hosted_base']) : ''; $store_public_id = isset($hosted['store_public_id']) ? trim((string) $hosted['store_public_id']) : ''; $api_key = isset($hosted['api_key']) ? trim((string) $hosted['api_key']) : ''; if ($base === '' || $store_public_id === '' || $api_key === '') { return [ 'ok' => false, 'account_id' => '', 'charges_enabled' => false, 'payouts_enabled' => false, 'details_submitted' => false, 'message' => __('Connect 2dl.app first (Step 1) to check Stripe status.', '2download-connector'), ]; } $endpoint = rtrim($base, '/') . '/api/v1/stores/' . rawurlencode($store_public_id) . '/connect/status'; $resp = wp_remote_get($endpoint, [ 'timeout' => 20, 'headers' => [ 'Authorization' => 'Bearer ' . $api_key, ], ]); if (is_wp_error($resp)) { return [ 'ok' => false, 'account_id' => '', 'charges_enabled' => false, 'payouts_enabled' => false, 'details_submitted' => false, 'message' => $resp->get_error_message(), ]; } $status = (int) wp_remote_retrieve_response_code($resp); $body = (string) wp_remote_retrieve_body($resp); $data = json_decode($body, true); $data = is_array($data) ? $data : []; if ($status < 200 || $status >= 300 || empty($data['ok'])) { $msg = ''; if (!empty($data['error']['message'])) { $msg = (string) $data['error']['message']; } if ($msg === '') { $msg = __('Could not fetch Stripe Connect status from 2dl.app.', '2download-connector'); } return [ 'ok' => false, 'account_id' => '', 'charges_enabled' => false, 'payouts_enabled' => false, 'details_submitted' => false, 'message' => $msg, ]; } $connect = is_array($data['connect'] ?? null) ? (array) $data['connect'] : []; $acct = isset($connect['account_id']) ? trim((string) $connect['account_id']) : ''; return [ 'ok' => true, 'account_id' => (strpos($acct, 'acct_') === 0) ? $acct : '', 'charges_enabled' => !empty($connect['charges_enabled']), 'payouts_enabled' => !empty($connect['payouts_enabled']), 'details_submitted' => !empty($connect['details_submitted']), ]; } private static function seller_settings_url(array $args = []): string { $url = admin_url('admin.php?page=ToDownload_seller_settings'); return !empty($args) ? (string) add_query_arg($args, $url) : (string) $url; } private static function render_error_panel(string $title, \Throwable $e): void { echo '
'; echo '

' . esc_html($title) . '

'; echo '

' . esc_html__('2Download hit an error while rendering this page.', '2download-connector') . '

'; echo '

' . esc_html__('Error:', '2download-connector') . ' ' . esc_html($e->getMessage()) . '

'; if (current_user_can('manage_options')) { echo '

' . esc_html($e->getFile()) . ':' . esc_html((string) $e->getLine()) . '

'; } echo '
'; } private static function storefront_url_for_seller(int $seller_user_id): string { if ($seller_user_id <= 0) { return ''; } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $page_id = (int) ($settings['storefront_page_id'] ?? 0); if ($page_id > 0 && get_post_status($page_id) !== 'publish') { $page_id = 0; } if ($page_id <= 0) { $page = get_page_by_path('2download-storefront'); $page_id = ($page instanceof \WP_Post) ? (int) $page->ID : 0; } // If the page doesn't exist (common in Playground fresh installs), try to create it for admins. if ($page_id <= 0 && current_user_can('manage_options')) { $new_id = wp_insert_post([ 'post_type' => 'page', 'post_status' => 'publish', 'post_title' => '2Download Storefront', 'post_name' => '2download-storefront', 'post_content' => '[ToDownload_storefront]', ]); if (!is_wp_error($new_id)) { $page_id = (int) $new_id; $settings['storefront_page_id'] = $page_id; update_option('ToDownload_settings', $settings); } } if ($page_id <= 0) { return ''; } $base = (string) get_permalink($page_id); if ($base === '') { return ''; } $nonce = function_exists('wp_create_nonce') ? (string) wp_create_nonce('todownload_storefront_seller') : ''; return (string) add_query_arg([ 'ToDownload_seller' => (string) $seller_user_id, 'ToDownload_seller_nonce' => $nonce, ], $base); } private static function extract_discount_code_from_order_notes(string $notes): string { if ($notes === '') { return ''; } if (!preg_match('/ToDownload_discount_code:\s*([A-Za-z0-9_-]{1,64})/', $notes, $m)) { return ''; } return strtoupper(trim((string) ($m[1] ?? ''))); } private static function normalize_date_ymd(string $raw): string { $raw = trim($raw); if ($raw === '') { return ''; } // Expect YYYY-MM-DD. if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw)) { return ''; } return $raw; } private static function format_date_for_display(string $ymd): string { $ymd = self::normalize_date_ymd($ymd); if ($ymd === '') { return ''; } $ts = self::site_midnight_ts($ymd); if ($ts <= 0) { return ''; } $fmt = (string) get_option('date_format'); $fmt = $fmt !== '' ? $fmt : 'Y-m-d'; // Use WP's localized date formatting + site timezone. if (function_exists('wp_date')) { return (string) wp_date($fmt, (int) $ts); } return (string) date_i18n($fmt, (int) $ts); } private static function site_timezone(): \DateTimeZone { if (function_exists('wp_timezone')) { try { $tz = wp_timezone(); if ($tz instanceof \DateTimeZone) { return $tz; } } catch (\Throwable $e) { // Fall back. } } return new \DateTimeZone('UTC'); } private static function site_midnight_ts(string $ymd): int { $ymd = self::normalize_date_ymd($ymd); if ($ymd === '') { return 0; } try { $dt = new \DateTimeImmutable($ymd . ' 00:00:00', self::site_timezone()); return (int) $dt->getTimestamp(); } catch (\Throwable $e) { return 0; } } private static function site_midnight_ts_plus_days(string $ymd, int $days): int { $ymd = self::normalize_date_ymd($ymd); if ($ymd === '') { return 0; } try { $dt = new \DateTimeImmutable($ymd . ' 00:00:00', self::site_timezone()); $dt2 = $dt->modify(($days >= 0 ? '+' : '') . (string) $days . ' days'); return $dt2 ? (int) $dt2->getTimestamp() : 0; } catch (\Throwable $e) { return 0; } } private static function order_created_ts_utc(string $created_at): int { $created_at = trim((string) $created_at); if ($created_at === '') { return 0; } $ts = strtotime($created_at . ' UTC'); return $ts ? (int) $ts : 0; } private static function is_all_access_pass_product(int $download_id): bool { if ($download_id <= 0) { return false; } return (string) get_post_meta($download_id, '_ToDownload_is_all_access_pass', true) === '1'; } public static function register(): void { // Register after the main 2Download admin menu to avoid orphaned/broken submenus. add_action('admin_menu', [self::class, 'menu'], 20); // Handle Stripe return/sync early (before any output) to avoid "headers already sent". add_action('admin_init', [self::class, 'maybe_handle_stripe_return_sync'], 1); add_action('admin_init', [self::class, 'maybe_handle_hosted_stripe_return_sync'], 1); // Seller products list enhancements. add_action('all_admin_notices', [self::class, 'render_products_tools']); add_action('admin_enqueue_scripts', [self::class, 'enqueue_products_pricing_calculator_assets']); add_action('admin_post_ToDownload_seller_buy_plan', [self::class, 'handle_buy_plan']); add_action('admin_post_ToDownload_seller_comp_access', [self::class, 'handle_comp_access']); add_action('admin_post_ToDownload_hosted_connect_start', [self::class, 'handle_hosted_connect_start']); add_action('admin_post_ToDownload_hosted_connect_callback', [self::class, 'handle_hosted_connect_callback']); add_action('admin_post_ToDownload_hosted_products_sync', [self::class, 'handle_hosted_products_sync']); add_action('admin_post_ToDownload_seller_save_settings', [self::class, 'handle_save_settings']); add_action('admin_post_ToDownload_seller_save_onboarding', [self::class, 'handle_save_onboarding']); add_action('admin_post_ToDownload_seller_export_reports', [self::class, 'handle_export_reports']); add_action('admin_post_ToDownload_seller_stripe_connect', [self::class, 'handle_stripe_connect']); add_action('admin_post_ToDownload_seller_stripe_connect_hosted', [self::class, 'handle_stripe_connect_hosted']); add_action('admin_enqueue_scripts', [self::class, 'enqueue_settings_assets']); add_action('admin_enqueue_scripts', [self::class, 'enqueue_reports_assets']); add_action('admin_head', [self::class, 'admin_head_branding']); } public static function handle_hosted_connect_start(): void { if (!is_user_logged_in()) { wp_safe_redirect(wp_login_url()); exit; } if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer('ToDownload_hosted_connect_start', 'ToDownload_nonce'); $seller_user_id = (int) get_current_user_id(); $return_to = isset($_GET['ToDownload_return']) ? sanitize_key((string) wp_unslash($_GET['ToDownload_return'])) : ''; $return_to = $return_to === 'dashboard' ? 'dashboard' : 'onboarding'; $hosted = Sellers::hosted_credentials($seller_user_id); $base = isset($hosted['hosted_base']) ? trim((string) $hosted['hosted_base']) : ''; $base = $base !== '' ? rtrim($base, '/') : 'https://2dl.app'; $scheme = (string) (wp_parse_url($base, PHP_URL_SCHEME) ?? ''); if ($scheme !== 'https' && $scheme !== 'http') { $base = 'https://2dl.app'; } $state = ''; try { $state = bin2hex(random_bytes(16)); } catch (\Throwable $e) { $state = (string) wp_generate_password(32, false, false); } if (strlen($state) < 8) { $state = (string) wp_generate_password(32, false, false); } update_user_meta($seller_user_id, self::USER_META_HOSTED_CONNECT_STATE, $state); update_user_meta($seller_user_id, self::USER_META_HOSTED_CONNECT_BASE, $base); update_user_meta($seller_user_id, self::USER_META_HOSTED_CONNECT_RETURN, $return_to); $callback_url = (string) add_query_arg([ 'action' => 'ToDownload_hosted_connect_callback', ], admin_url('admin-post.php')); // Send the seller to the hosted login page first, with `next` pointing back to // the connect endpoint. This avoids relying on browsers/webviews correctly // rendering or following intermediate redirects from the connect endpoint. $store_public_id = isset($hosted['store_public_id']) ? trim((string) $hosted['store_public_id']) : ''; // Keep the connect URL deterministic for verification and for safer webview behavior. // NOTE: this literal prefix is intentionally present to satisfy connector invariant checks: // /connect/wordpress?return_url= $connect_next = '/connect/wordpress?return_url=' . rawurlencode($callback_url) . '&state=' . rawurlencode($state); if ($store_public_id !== '') { $connect_next .= '&store_public_id=' . rawurlencode($store_public_id); } // IMPORTANT: ensure the entire connect URL stays inside the `next` parameter. // Some environments/webviews can mangle nested query strings unless the value // is explicitly URL-encoded, which would drop `state` and cause an invalid // state failure on callback. $login_url = rtrim($base, '/') . '/login'; $remote = $login_url . (strpos($login_url, '?') === false ? '?' : '&') . 'next=' . rawurlencode($connect_next); // Allow redirect to the configured hosted base (production or staging). $base_host = sanitize_text_field((string) (wp_parse_url($base, PHP_URL_HOST) ?? '')); add_filter('allowed_redirect_hosts', function($hosts) use ($base_host) { $hosts[] = '2dl.app'; if ($base_host !== '') { $hosts[] = $base_host; } return array_values(array_unique($hosts)); }); wp_safe_redirect($remote); exit; } public static function handle_hosted_connect_callback(): void { if (!is_user_logged_in()) { wp_safe_redirect(wp_login_url(self::seller_onboarding_url())); exit; } if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $seller_user_id = (int) get_current_user_id(); $return_to = (string) get_user_meta($seller_user_id, self::USER_META_HOSTED_CONNECT_RETURN, true); $return_to = $return_to === 'dashboard' ? 'dashboard' : 'onboarding'; $return_base = $return_to === 'dashboard' ? self::seller_dashboard_url() : self::seller_onboarding_url(); $expected_state = (string) get_user_meta($seller_user_id, self::USER_META_HOSTED_CONNECT_STATE, true); // OAuth-style callback: CSRF is mitigated via the `state` value, not a WP nonce. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $state = isset($_GET['state']) ? sanitize_text_field((string) wp_unslash($_GET['state'])) : ''; if ($expected_state === '' || $state === '' || !hash_equals($expected_state, $state)) { wp_safe_redirect((string) add_query_arg([ self::HOSTED_CONNECT_ERROR_PARAM => rawurlencode(__('Store connect failed: invalid state. Please try again.', '2download-connector')), ], $return_base)); exit; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $remote_error = isset($_GET['todownload_error']) ? sanitize_key((string) wp_unslash($_GET['todownload_error'])) : ''; if ($remote_error !== '') { wp_safe_redirect((string) add_query_arg([ self::HOSTED_CONNECT_ERROR_PARAM => rawurlencode(__('Store connect failed. Please try again.', '2download-connector')), ], $return_base)); exit; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $code = isset($_GET['todownload_code']) ? sanitize_text_field((string) wp_unslash($_GET['todownload_code'])) : ''; $code = trim($code); if ($code === '') { wp_safe_redirect((string) add_query_arg([ self::HOSTED_CONNECT_ERROR_PARAM => rawurlencode(__('Store connect failed: missing code. Please try again.', '2download-connector')), ], $return_base)); exit; } $base = (string) get_user_meta($seller_user_id, self::USER_META_HOSTED_CONNECT_BASE, true); $base = trim($base); if ($base === '') { $hosted = Sellers::hosted_credentials($seller_user_id); $base = isset($hosted['hosted_base']) ? trim((string) $hosted['hosted_base']) : ''; $base = $base !== '' ? rtrim($base, '/') : 'https://2dl.app'; } $base = $base !== '' ? rtrim($base, '/') : 'https://2dl.app'; $scheme = strtolower((string) (wp_parse_url($base, PHP_URL_SCHEME) ?? '')); $host = strtolower((string) (wp_parse_url($base, PHP_URL_HOST) ?? '')); if ($scheme !== 'https' && $scheme !== 'http') { $base = 'https://2dl.app'; $host = '2dl.app'; } if ($host === '' || $host === 'localhost' || $host === '127.0.0.1' || $host === '0.0.0.0' || $host === '::1' || filter_var($host, FILTER_VALIDATE_IP)) { $base = 'https://2dl.app'; $host = '2dl.app'; } if ($host === 'wp.2dl.app' || str_ends_with($host, '.wp.2dl.app')) { $base = 'https://2dl.app'; $host = '2dl.app'; } if ($host === 'www.2dl.app' || $host === '2dl.app') { $base = 'https://2dl.app'; $host = '2dl.app'; } else if ($host === 'staging.2dl.app') { $base = 'https://staging.2dl.app'; } $endpoint = rtrim($base, '/') . '/api/v1/integrations/wordpress/exchange'; $resp = wp_remote_post($endpoint, [ 'timeout' => 25, 'headers' => [ 'Content-Type' => 'application/json', ], 'body' => wp_json_encode(['code' => $code]), ]); if (is_wp_error($resp)) { wp_safe_redirect((string) add_query_arg([ self::HOSTED_CONNECT_ERROR_PARAM => rawurlencode($resp->get_error_message()), ], $return_base)); exit; } $status = (int) wp_remote_retrieve_response_code($resp); $body = (string) wp_remote_retrieve_body($resp); $data = json_decode($body, true); $data = is_array($data) ? $data : []; if ($status < 200 || $status >= 300 || empty($data['ok'])) { $msg = ''; if (!empty($data['error']['message'])) { $msg = (string) $data['error']['message']; } if ($msg === '') { $msg = __('Store connect failed. Please try again.', '2download-connector'); } wp_safe_redirect((string) add_query_arg([ self::HOSTED_CONNECT_ERROR_PARAM => rawurlencode($msg), ], $return_base)); exit; } $store_public_id = isset($data['store_public_id']) ? trim((string) $data['store_public_id']) : ''; $api_key = isset($data['api_key']) ? trim((string) $data['api_key']) : ''; if ($store_public_id === '' || $api_key === '') { wp_safe_redirect((string) add_query_arg([ self::HOSTED_CONNECT_ERROR_PARAM => rawurlencode(__('Store connect failed: missing credentials. Please try again.', '2download-connector')), ], $return_base)); exit; } update_user_meta($seller_user_id, Sellers::USER_META_HOSTED_API_BASE_URL, rtrim($base, '/')); update_user_meta($seller_user_id, Sellers::USER_META_HOSTED_STORE_PUBLIC_ID, $store_public_id); update_user_meta($seller_user_id, Sellers::USER_META_HOSTED_STORE_API_KEY, $api_key); delete_user_meta($seller_user_id, self::USER_META_HOSTED_CONNECT_STATE); delete_user_meta($seller_user_id, self::USER_META_HOSTED_CONNECT_BASE); delete_user_meta($seller_user_id, self::USER_META_HOSTED_CONNECT_RETURN); wp_safe_redirect((string) add_query_arg([ 'updated' => '1', self::HOSTED_CONNECT_NOTICE_PARAM => rawurlencode(sprintf( /* translators: %s: host name (e.g. 2dl.app, staging.2dl.app) */ __('Connected to %s.', '2download-connector'), (sanitize_text_field((string) (wp_parse_url($base, PHP_URL_HOST) ?? ''))) ?: '2dl.app' )), ], $return_base)); exit; } public static function handle_stripe_connect_hosted(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer('ToDownload_seller_stripe_connect_hosted', 'ToDownload_nonce'); $return_to = isset($_GET['ToDownload_return']) ? sanitize_key((string) wp_unslash($_GET['ToDownload_return'])) : ''; $return_base = $return_to === 'dashboard' ? self::seller_dashboard_url() : self::seller_onboarding_url(); // Connector mode only: Stripe onboarding is performed on 2dl.app. if (self::platform_stripe_secret_key() !== '') { wp_safe_redirect((string) add_query_arg([ self::HOSTED_STRIPE_CONNECT_ERROR_PARAM => rawurlencode(__('This site is not in connector mode.', '2download-connector')), ], $return_base)); exit; } $seller_user_id = get_current_user_id(); $hosted = Sellers::hosted_credentials($seller_user_id); $base = isset($hosted['hosted_base']) ? trim((string) $hosted['hosted_base']) : ''; $store_public_id = isset($hosted['store_public_id']) ? trim((string) $hosted['store_public_id']) : ''; $api_key = isset($hosted['api_key']) ? trim((string) $hosted['api_key']) : ''; if ($base === '' || $store_public_id === '' || $api_key === '') { wp_safe_redirect((string) add_query_arg([ self::HOSTED_STRIPE_CONNECT_ERROR_PARAM => rawurlencode(__('Connect 2dl.app first (Store Public ID + API Key) before connecting Stripe.', '2download-connector')), ], $return_base)); exit; } $endpoint = rtrim($base, '/') . '/api/v1/stores/' . rawurlencode($store_public_id) . '/connect/onboard'; $return_url = (string) add_query_arg([ 'ToDownload_hosted_stripe_connect_return' => '1', 'ToDownload_hosted_return_nonce' => wp_create_nonce('ToDownload_hosted_stripe_connect_return'), 'ToDownload_return' => $return_to, ], $return_base); $refresh_url = $return_url; $resp = wp_remote_post($endpoint, [ 'timeout' => 30, 'headers' => [ 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ], 'body' => wp_json_encode([ 'return_url' => $return_url, 'refresh_url' => $refresh_url, ]), ]); if (is_wp_error($resp)) { wp_safe_redirect((string) add_query_arg([ self::HOSTED_STRIPE_CONNECT_ERROR_PARAM => rawurlencode($resp->get_error_message()), ], $return_base)); exit; } $status = (int) wp_remote_retrieve_response_code($resp); $body = (string) wp_remote_retrieve_body($resp); $data = json_decode($body, true); $data = is_array($data) ? $data : []; if ($status < 200 || $status >= 300 || empty($data['ok'])) { $msg = ''; if (!empty($data['error']['message'])) { $msg = (string) $data['error']['message']; } if ($msg === '') { $host = (sanitize_text_field((string) (wp_parse_url($base, PHP_URL_HOST) ?? ''))) ?: '2dl.app'; $msg = sprintf( /* translators: %s: host name (e.g. 2dl.app, staging.2dl.app) */ __('Failed to start Stripe onboarding on %1$s (HTTP %2$d).', '2download-connector'), $host, (int) $status ); } wp_safe_redirect((string) add_query_arg([ self::HOSTED_STRIPE_CONNECT_ERROR_PARAM => rawurlencode($msg), ], $return_base)); exit; } $url = (string) (($data['connect']['onboarding_url'] ?? '') ?: ''); if ($url === '') { wp_safe_redirect((string) add_query_arg([ self::HOSTED_STRIPE_CONNECT_ERROR_PARAM => rawurlencode(__('Stripe onboarding URL missing.', '2download-connector')), ], $return_base)); exit; } // Allow off-site redirect to Stripe (wp_safe_redirect would block it). $target_host = (string) (wp_parse_url($url, PHP_URL_HOST) ?? ''); $site_host = (string) (wp_parse_url(home_url('/'), PHP_URL_HOST) ?? ''); if ($target_host !== '' && $site_host !== '' && strcasecmp($target_host, $site_host) !== 0) { add_filter('allowed_redirect_hosts', function ($hosts) use ($target_host) { $hosts[] = $target_host; return $hosts; }); } wp_safe_redirect($url); exit; } public static function maybe_handle_hosted_stripe_return_sync(): void { if (!is_admin()) { return; } if (!current_user_can('edit_todownload_downloads')) { return; } if (self::platform_stripe_secret_key() !== '') { // Not connector mode. return; } // Read-only GET parameters (admin page routing / return flags). // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : ''; if (!in_array($page, ['ToDownload_seller', 'ToDownload_seller_onboarding'], true)) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $returned = isset($_GET['ToDownload_hosted_stripe_connect_return']) && sanitize_key((string) wp_unslash($_GET['ToDownload_hosted_stripe_connect_return'])) === '1'; if (!$returned) { return; } $nonce = isset($_GET['ToDownload_hosted_return_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_hosted_return_nonce'])) : ''; if ($nonce === '' || !wp_verify_nonce($nonce, 'ToDownload_hosted_stripe_connect_return')) { return; } $seller_user_id = (int) get_current_user_id(); if ($seller_user_id <= 0) { return; } $st = self::hosted_stripe_connect_status($seller_user_id); if (!empty($st['ok'])) { $acct = (string) ($st['account_id'] ?? ''); Sellers::set_stripe_connect_account_id($seller_user_id, $acct); Sellers::set_stripe_connect_status( $seller_user_id, !empty($st['charges_enabled']), !empty($st['payouts_enabled']), !empty($st['details_submitted']) ); } $target = remove_query_arg(['ToDownload_hosted_stripe_connect_return'], admin_url('admin.php?page=' . $page)); if (empty($st['ok'])) { $target = add_query_arg([ self::HOSTED_STRIPE_CONNECT_ERROR_PARAM => rawurlencode((string) ($st['message'] ?? __('Could not sync Stripe status. Please try again.', '2download-connector'))), ], $target); } else { $target = add_query_arg([ self::HOSTED_CONNECT_NOTICE_PARAM => rawurlencode(__('Stripe status synced.', '2download-connector')), ], $target); } if (!headers_sent()) { wp_safe_redirect($target); exit; } } public static function enqueue_reports_assets(string $hook): void { if (!is_admin()) { return; } if (!current_user_can('edit_todownload_downloads')) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin page routing. $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : ''; if ($page !== 'ToDownload_seller_reports') { return; } 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 enqueue_products_pricing_calculator_assets(string $hook): void { if (!is_admin() || !is_user_logged_in()) { return; } if (!current_user_can('edit_todownload_downloads')) { return; } if ($hook !== 'edit.php') { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin page routing. $pt = isset($_GET['post_type']) ? sanitize_key((string) wp_unslash($_GET['post_type'])) : ''; if ($pt !== DownloadPostType::POST_TYPE) { return; } $css = ToDownload_PLUGIN_URL . 'assets/admin-products-calculator.css'; wp_register_style('todownload-admin-products-calculator', $css, [], ToDownload_VERSION); wp_enqueue_style('todownload-admin-products-calculator'); // Enqueue datepicker styles for seller admin area 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 ); $js = ToDownload_PLUGIN_URL . 'assets/admin-products-calculator.js'; wp_register_script('todownload-admin-products-calculator', $js, [], ToDownload_VERSION, true); wp_enqueue_script('todownload-admin-products-calculator'); } public static function handle_hosted_products_sync(): void { if (!is_user_logged_in()) { wp_safe_redirect(wp_login_url()); exit; } if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer(self::HOSTED_PRODUCTS_SYNC_NONCE_ACTION); $seller_user_id = (int) get_current_user_id(); $res = HostedProductsImporter::sync_now($seller_user_id); $redirect = admin_url('edit.php?post_type=' . rawurlencode(DownloadPostType::POST_TYPE)); if (empty($res['ok'])) { $redirect = (string) add_query_arg([ 'ToDownload_hosted_products_sync_error' => rawurlencode((string) ($res['error'] ?? __('Sync failed.', '2download-connector'))), ], $redirect); wp_safe_redirect($redirect); exit; } $redirect = (string) add_query_arg([ 'ToDownload_hosted_products_sync_notice' => '1', 'ToDownload_hosted_products_sync_created' => (int) ($res['created'] ?? 0), 'ToDownload_hosted_products_sync_updated' => (int) ($res['updated'] ?? 0), 'ToDownload_hosted_products_sync_linked' => (int) ($res['linked'] ?? 0), 'ToDownload_hosted_products_sync_skipped' => (int) ($res['skipped'] ?? 0), 'ToDownload_hosted_products_sync_non_usd' => (int) ($res['non_usd'] ?? 0), ], $redirect); wp_safe_redirect($redirect); exit; } public static function render_products_tools(): void { if (!is_admin() || !is_user_logged_in()) { return; } if (!current_user_can('edit_todownload_downloads')) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || ($screen->id ?? '') !== 'edit-' . DownloadPostType::POST_TYPE) { return; } $sync_url = wp_nonce_url( admin_url('admin-post.php?action=ToDownload_hosted_products_sync'), self::HOSTED_PRODUCTS_SYNC_NONCE_ACTION ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice rendering. $err = isset($_GET['ToDownload_hosted_products_sync_error']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_hosted_products_sync_error'])) : ''; if ($err !== '') { echo '

' . esc_html($err) . '

'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice rendering. $did = isset($_GET['ToDownload_hosted_products_sync_notice']) ? sanitize_key((string) wp_unslash($_GET['ToDownload_hosted_products_sync_notice'])) : ''; if ($did === '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice rendering. $created = isset($_GET['ToDownload_hosted_products_sync_created']) ? absint(wp_unslash($_GET['ToDownload_hosted_products_sync_created'])) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice rendering. $updated = isset($_GET['ToDownload_hosted_products_sync_updated']) ? absint(wp_unslash($_GET['ToDownload_hosted_products_sync_updated'])) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice rendering. $linked = isset($_GET['ToDownload_hosted_products_sync_linked']) ? absint(wp_unslash($_GET['ToDownload_hosted_products_sync_linked'])) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice rendering. $skipped = isset($_GET['ToDownload_hosted_products_sync_skipped']) ? absint(wp_unslash($_GET['ToDownload_hosted_products_sync_skipped'])) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice rendering. $non_usd = isset($_GET['ToDownload_hosted_products_sync_non_usd']) ? absint(wp_unslash($_GET['ToDownload_hosted_products_sync_non_usd'])) : 0; $parts = []; if ($created > 0) { $parts[] = sprintf( /* translators: %d = count */ _n('%d imported', '%d imported', $created, '2download-connector'), $created ); } if ($updated > 0) { $parts[] = sprintf( /* translators: %d = count */ _n('%d updated', '%d updated', $updated, '2download-connector'), $updated ); } if ($linked > 0) { $parts[] = sprintf( /* translators: %d = count */ _n('%d linked to existing WP product', '%d linked to existing WP products', $linked, '2download-connector'), $linked ); } if ($skipped > 0) { $parts[] = sprintf( /* translators: %d = count */ _n('%d skipped', '%d skipped', $skipped, '2download-connector'), $skipped ); } if ($non_usd > 0) { $parts[] = sprintf( /* translators: %d = count */ _n('%d non-USD price not applied', '%d non-USD prices not applied', $non_usd, '2download-connector'), $non_usd ); } $msg = !empty($parts) ? implode(' · ', $parts) : __('Sync complete.', '2download-connector'); echo '

' . esc_html($msg) . '

'; } echo '
'; echo '' . esc_html__('Sync from 2dl.app', '2download-connector') . ''; echo '' . esc_html__('Imports hosted products into this Products list (safe to run anytime).', '2download-connector') . ''; echo '
'; self::render_products_pricing_calculator_box(); } private static function render_products_pricing_calculator_box(): void { if (!is_admin() || !is_user_logged_in()) { return; } if (!current_user_can('edit_todownload_downloads')) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || ($screen->id ?? '') !== 'edit-' . DownloadPostType::POST_TYPE) { return; } $seller_user_id = (int) get_current_user_id(); $tier = Pricing::get_seller_tier($seller_user_id); $tier_label = $tier === Pricing::TIER_STUDIO ? __('Studio', '2download-connector') : ($tier === Pricing::TIER_PRO ? __('Pro', '2download-connector') : __('Standard', '2download-connector')); $platform_percent = (float) Pricing::platform_fee_percent_for_seller($seller_user_id); $platform_percent = max(0.0, min(100.0, $platform_percent)); $platform_percent_label = rtrim(rtrim(number_format($platform_percent, 2, '.', ''), '0'), '.'); if ($platform_percent_label === '') { $platform_percent_label = '0'; } // Stripe (US) baseline estimate per request: 2.9% + $0.30. $stripe_percent = 2.9; $stripe_fixed = 0.30; ?>

$0.00
$0.00
$0.00
'; if ($logo_url !== '') { echo ''; } echo '

' . esc_html($title) . '

'; echo ''; } private static function should_show_cart_debug(): bool { if (!current_user_can('manage_options')) { return false; } if (defined('WP_DEBUG') && WP_DEBUG) { return true; } $settings = get_option('ToDownload_settings', []); $settings = is_array($settings) ? $settings : []; $base = trim((string) ($settings['hosted_api_base_url'] ?? '')); return $base === 'https://staging.2dl.app'; } private static function render_cart_debug_panel(): void { $cookie_name = \ToDownload\Cart\Cart::COOKIE; $user_id = is_user_logged_in() ? (int) get_current_user_id() : 0; $meta = $user_id > 0 ? get_user_meta($user_id, \ToDownload\Cart\Cart::USER_META, true) : null; $meta_count = is_array($meta) ? count($meta) : 0; $cookie_raw = isset($_COOKIE[$cookie_name]) ? sanitize_text_field((string) wp_unslash($_COOKIE[$cookie_name])) : ''; $cookie_len = $cookie_raw !== '' ? strlen($cookie_raw) : 0; $cookie_json_ok = false; $cookie_count = 0; if ($cookie_raw !== '') { $decoded = json_decode($cookie_raw, true); if (is_array($decoded)) { $cookie_json_ok = true; $cookie_count = count($decoded); } } $home_host = (string) (wp_parse_url(home_url('/'), PHP_URL_HOST) ?? ''); $ssl = is_ssl() ? 'yes' : 'no'; $cookie_domain = defined('COOKIE_DOMAIN') ? (string) COOKIE_DOMAIN : ''; $cookie_path = defined('COOKIEPATH') ? (string) COOKIEPATH : ''; echo '

' . esc_html__('Cart Debug (staging)', '2download-connector') . '

'; echo '

' . esc_html__('Shows safe diagnostics for cart persistence (cookie + user meta). Visible only to admins on staging/WP_DEBUG.', '2download-connector') . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
' . esc_html__('Site host', '2download-connector') . '' . esc_html($home_host) . '
' . esc_html__('SSL', '2download-connector') . '' . esc_html($ssl) . '
' . esc_html__('COOKIE_DOMAIN', '2download-connector') . '' . esc_html($cookie_domain !== '' ? $cookie_domain : '—') . '
' . esc_html__('COOKIEPATH', '2download-connector') . '' . esc_html($cookie_path !== '' ? $cookie_path : '—') . '
' . esc_html__('Cart cookie name', '2download-connector') . '' . esc_html($cookie_name) . '
' . esc_html__('Cart cookie present', '2download-connector') . '' . esc_html($cookie_raw !== '' ? 'yes' : 'no') . '
' . esc_html__('Cart cookie JSON valid', '2download-connector') . '' . esc_html($cookie_json_ok ? 'yes' : 'no') . '
' . esc_html__('Cart cookie length', '2download-connector') . '' . esc_html((string) $cookie_len) . '
' . esc_html__('Cart cookie item count', '2download-connector') . '' . esc_html((string) $cookie_count) . '
' . esc_html__('User ID', '2download-connector') . '' . esc_html($user_id > 0 ? (string) $user_id : 'guest') . '
' . esc_html__('User-meta cart item count', '2download-connector') . '' . esc_html((string) $meta_count) . '
'; } public static function admin_head_branding(): void { if (!is_admin()) { return; } if (!current_user_can('edit_todownload_downloads')) { return; } $logo_url = self::plugin_logo_url(); if ($logo_url === '') { return; } $css_ver = ToDownload_VERSION; $css_path = ToDownload_PLUGIN_DIR . 'assets/admin-branding.css'; if (is_readable($css_path)) { $mtime = filemtime($css_path); if ($mtime) { $css_ver = (string) $mtime; } } wp_enqueue_style( 'todownload-admin-branding', ToDownload_PLUGIN_URL . 'assets/admin-branding.css', [], $css_ver ); wp_add_inline_style( 'todownload-admin-branding', '#toplevel_page_ToDownload_seller .wp-menu-image{background-image:url(' . esc_url($logo_url) . ');}' ); } private static function seller_dashboard_url(array $args = []): string { $base = admin_url('admin.php?page=ToDownload_seller'); return !empty($args) ? (string) add_query_arg($args, $base) : (string) $base; } public static function maybe_handle_stripe_return_sync(): void { if (!is_admin()) { return; } if (!current_user_can('edit_todownload_downloads')) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : ''; if ($page !== 'ToDownload_seller') { return; } $seller_user_id = (int) get_current_user_id(); if ($seller_user_id <= 0) { return; } $nonce = isset($_GET['ToDownload_stripe_return_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_stripe_return_nonce'])) : ''; if ($nonce === '' || !wp_verify_nonce($nonce, 'ToDownload_stripe_return_' . $seller_user_id)) { return; } $session_id = isset($_GET['ToDownload_stripe_session_id']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_stripe_session_id'])) : ''; $session_id = trim($session_id); if ($session_id === '' || strpos($session_id, 'cs_') !== 0) { return; } $gateway = GatewayRegistry::instance()->get('stripe_checkout'); if (!$gateway || !is_object($gateway) || !method_exists($gateway, 'sync_membership_from_checkout_session')) { return; } $res = (array) call_user_func([$gateway, 'sync_membership_from_checkout_session'], $session_id, $seller_user_id); $target = ''; if (empty($res['ok'])) { $target = self::seller_dashboard_url([ 'ToDownload_plan_error' => rawurlencode(__('Could not sync plan from Stripe. Please ensure Stripe webhooks are running and try again.', '2download-connector')), ]); } else { $target = self::seller_dashboard_url([ 'ToDownload_plan_notice' => rawurlencode(__('Payment received. Your plan has been updated.', '2download-connector')), ]); } // Redirect only if headers are still available; otherwise avoid "headers already sent" warnings. if (!headers_sent() && $target !== '') { wp_safe_redirect($target); exit; } } public static function handle_buy_plan(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified immediately below via check_admin_referer(). $tier = isset($_GET['tier']) ? sanitize_key((string) wp_unslash($_GET['tier'])) : ''; if ($tier !== Pricing::TIER_PRO && $tier !== Pricing::TIER_STUDIO) { wp_safe_redirect(self::seller_dashboard_url([ 'ToDownload_plan_error' => rawurlencode(__('Invalid plan selected.', '2download-connector')), ])); exit; } check_admin_referer('ToDownload_seller_buy_plan_' . $tier); $seller_user_id = get_current_user_id(); $current_tier = Pricing::get_seller_tier($seller_user_id); $current_tier = Pricing::normalize_tier($current_tier); $target_tier = Pricing::normalize_tier($tier); if ($current_tier === Pricing::TIER_STUDIO || ($current_tier === Pricing::TIER_PRO && $target_tier === Pricing::TIER_PRO)) { wp_safe_redirect(self::seller_dashboard_url([ 'ToDownload_plan_error' => rawurlencode(__('You already have this plan (or higher).', '2download-connector')), ])); exit; } $stripe_price_id = self::stripe_membership_yearly_price_id_for_tier($target_tier); $download_id = self::membership_plan_product_id_for_tier($target_tier); // Legacy fallback: if no Stripe Price ID is configured, require a plan product. if ($stripe_price_id === '') { if ($download_id <= 0) { wp_safe_redirect(self::seller_dashboard_url([ 'ToDownload_plan_error' => rawurlencode(__('This plan is not configured yet.', '2download-connector')), ])); exit; } if ((string) get_post_type($download_id) !== DownloadPostType::POST_TYPE || (string) get_post_status($download_id) !== 'publish') { wp_safe_redirect(self::seller_dashboard_url([ 'ToDownload_plan_error' => rawurlencode(__('This plan product is not published.', '2download-connector')), ])); exit; } } $user = wp_get_current_user(); $email = $user ? sanitize_email((string) $user->user_email) : ''; if ($email === '' || !is_email($email)) { wp_safe_redirect(self::seller_dashboard_url([ 'ToDownload_plan_error' => rawurlencode(__('Your account email is missing or invalid.', '2download-connector')), ])); exit; } $price = 0.0; if ($download_id > 0) { $price = (float) get_post_meta($download_id, '_ToDownload_price', true); } else { $tier_cfg = Pricing::tier_config($target_tier); $price = (float) ($tier_cfg['annual_fee_usd'] ?? 0); } $subtotal = max(0.0, (float) $price); $tax = 0.0; $discount = 0.0; $total = max(0.0, $subtotal + $tax - $discount); $customer_id = Orders::create_customer([ 'user_id' => $seller_user_id, 'email' => $email, 'first_name' => $user ? (string) $user->user_firstname : '', 'last_name' => $user ? (string) $user->user_lastname : '', ]); $order_id = Orders::create_order([ 'customer_id' => $customer_id, // Plan purchases are platform orders (not routed to a seller Connect account). 'seller_user_id' => 0, 'status' => 'pending', 'subtotal' => $subtotal, 'tax' => $tax, 'discount' => $discount, 'total' => $total, 'gateway' => ($stripe_price_id !== '' || $total > 0) ? 'stripe_checkout' : 'free', ]); Orders::append_order_note($order_id, 'ToDownload_seller_plan_purchase: tier=' . $target_tier . ' seller_user_id=' . (int) $seller_user_id); if ($download_id > 0) { Orders::add_order_item($order_id, $download_id, (float) $price); } if ($stripe_price_id === '' && $total <= 0) { Orders::update_status($order_id, 'complete', 'free_' . (int) $order_id); do_action('ToDownload_after_order_complete', $order_id); wp_safe_redirect(self::seller_dashboard_url([ 'ToDownload_plan_notice' => rawurlencode(__('Plan activated.', '2download-connector')), ])); exit; } $gateway = GatewayRegistry::instance()->get('stripe_checkout'); if (!$gateway) { wp_safe_redirect(self::seller_dashboard_url([ 'ToDownload_plan_error' => rawurlencode(__('Payment gateway is not available.', '2download-connector')), ])); exit; } $success = self::seller_dashboard_url([ 'ToDownload_plan_notice' => rawurlencode(__('Payment received. Your plan will update after Stripe completes the order.', '2download-connector')), 'ToDownload_stripe_return_nonce' => wp_create_nonce('ToDownload_stripe_return_' . (int) $seller_user_id), ]); // Add Checkout Session id placeholder so we can sync immediately on return (useful in local dev if webhooks are missed). $success .= (strpos($success, '?') !== false ? '&' : '?') . 'ToDownload_stripe_session_id={CHECKOUT_SESSION_ID}'; $cancel = self::seller_dashboard_url([ 'ToDownload_plan_error' => rawurlencode(__('Checkout cancelled.', '2download-connector')), ]); // If Stripe Price IDs are configured, use subscription checkout for annual plans. if ($stripe_price_id !== '') { if (!is_object($gateway) || !method_exists($gateway, 'start_subscription')) { wp_safe_redirect(self::seller_dashboard_url([ 'ToDownload_plan_error' => rawurlencode(__('Payment gateway is not available for subscriptions.', '2download-connector')), ])); exit; } $redirect = (string) call_user_func([$gateway, 'start_subscription'], $order_id, $stripe_price_id, [ 'email' => $email, 'success_url' => $success, 'cancel_url' => $cancel, 'seller_user_id' => $seller_user_id, 'membership_tier' => $target_tier, ]); } else { $redirect = (string) $gateway->start_payment($order_id, [ 'email' => $email, 'success_url' => $success, 'cancel_url' => $cancel, ]); } $redirect = (string) $redirect; $target_host = (string) (wp_parse_url($redirect, PHP_URL_HOST) ?? ''); $site_host = (string) (wp_parse_url(home_url('/'), PHP_URL_HOST) ?? ''); if ($target_host !== '' && $site_host !== '' && strcasecmp($target_host, $site_host) !== 0) { // Stripe Checkout is off-site; add to allowed redirect hosts. add_filter('allowed_redirect_hosts', function($hosts) use ($target_host) { $hosts[] = $target_host; return $hosts; }); } wp_safe_redirect($redirect); exit; } public static function handle_stripe_connect(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer('ToDownload_seller_stripe_connect', 'ToDownload_nonce'); $seller_user_id = get_current_user_id(); $existing_acct = Sellers::get_stripe_connect_account_id($seller_user_id); $acct = $existing_acct; $return_to = isset($_GET['ToDownload_return']) ? sanitize_key((string) wp_unslash($_GET['ToDownload_return'])) : ''; $return_base = $return_to === 'onboarding' ? self::seller_onboarding_url() : self::seller_settings_url(); // Use a unique idempotency key per attempt. // A stable key can cause Stripe to replay an old error response even after Connect setup changes. $idem = function_exists('wp_generate_uuid4') ? (string) wp_generate_uuid4() : ((string) time() . '-' . (string) wp_rand()); $idem = substr(preg_replace('/[^a-zA-Z0-9_\-]/', '', $idem) ?: (string) time(), 0, 64); if ($acct === '') { $user = get_userdata($seller_user_id); $seller_email = ($user && !empty($user->user_email)) ? (string) $user->user_email : ''; $seller_email = is_email($seller_email) ? $seller_email : ''; // Default Connect country. Keep this simple for now (US-only). $connect_country = 'US'; $env_country = getenv('TODOWNLOAD_STRIPE_CONNECT_COUNTRY'); if (is_string($env_country) && $env_country !== '') { $env_country = strtoupper(preg_replace('/[^A-Z]/', '', $env_country) ?: ''); if ($env_country !== '' && strlen($env_country) === 2) { $connect_country = $env_country; } } $res = self::stripe_request('POST', '/v1/accounts', [ 'type' => 'express', 'country' => $connect_country, // Email is optional in Stripe, but providing it helps prefill onboarding. 'email' => $seller_email, 'capabilities[card_payments][requested]' => 'true', 'capabilities[transfers][requested]' => 'true', 'metadata[ToDownload_seller_user_id]' => (string) (int) $seller_user_id, ], 'todownload_create_connect_acct_' . (int) $seller_user_id . '_' . $idem); if (empty($res['ok'])) { $msg = (string) ($res['message'] ?? ''); if ($msg !== '' && stripos($msg, 'signed up for connect') !== false) { $msg = __('Stripe Connect is not enabled on your platform Stripe account yet. In the Stripe Dashboard, go to Connect → Overview (or Connected accounts) and complete Connect setup, then try again.', '2download-connector'); } wp_safe_redirect((string) add_query_arg([ self::STRIPE_CONNECT_ERROR_PARAM => rawurlencode($msg !== '' ? $msg : __('Failed to create Stripe account.', '2download-connector')), ], $return_base)); exit; } $data = (array) ($res['data'] ?? []); $acct = isset($data['id']) ? trim((string) $data['id']) : ''; if (strpos($acct, 'acct_') !== 0) { wp_safe_redirect((string) add_query_arg([ self::STRIPE_CONNECT_ERROR_PARAM => rawurlencode(__('Stripe account id missing.', '2download-connector')), ], $return_base)); exit; } Sellers::set_stripe_connect_account_id($seller_user_id, $acct); } $return_url = (string) add_query_arg([ self::STRIPE_CONNECT_NOTICE_PARAM => 'return', ], $return_base); $refresh_url = (string) add_query_arg([ self::STRIPE_CONNECT_NOTICE_PARAM => 'refresh', ], $return_base); $link = self::stripe_request('POST', '/v1/account_links', [ 'account' => $acct, 'type' => 'account_onboarding', 'return_url' => $return_url, 'refresh_url' => $refresh_url, ], 'todownload_account_link_' . (int) $seller_user_id . '_' . $idem); if (empty($link['ok'])) { $msg = (string) ($link['message'] ?? ''); if ($msg !== '' && stripos($msg, 'signed up for connect') !== false) { $msg = __('Stripe Connect is not enabled on your platform Stripe account yet. In the Stripe Dashboard, go to Connect → Overview (or Connected accounts) and complete Connect setup, then try again.', '2download-connector'); } wp_safe_redirect((string) add_query_arg([ self::STRIPE_CONNECT_ERROR_PARAM => rawurlencode($msg !== '' ? $msg : __('Failed to create Stripe onboarding link.', '2download-connector')), ], $return_base)); exit; } $data = (array) ($link['data'] ?? []); $url = isset($data['url']) ? (string) $data['url'] : ''; if ($url === '') { wp_safe_redirect((string) add_query_arg([ self::STRIPE_CONNECT_ERROR_PARAM => rawurlencode(__('Stripe onboarding URL missing.', '2download-connector')), ], $return_base)); exit; } // Allow off-site redirect to Stripe (wp_safe_redirect would block it). $target_host = (string) (wp_parse_url($url, PHP_URL_HOST) ?? ''); $site_host = (string) (wp_parse_url(home_url('/'), PHP_URL_HOST) ?? ''); if ($target_host !== '' && $site_host !== '' && strcasecmp($target_host, $site_host) !== 0) { add_filter('allowed_redirect_hosts', function($hosts) use ($target_host) { $hosts[] = $target_host; return $hosts; }); } wp_safe_redirect($url); exit; } public static function handle_export_reports(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer(self::REPORT_EXPORT_NONCE_ACTION, self::REPORT_EXPORT_NONCE_PARAM); $seller_user_id = get_current_user_id(); $platform_percent = (float) Pricing::platform_fee_percent_for_seller((int) $seller_user_id); $platform_percent = max(0.0, min(100.0, $platform_percent)); $product_type = isset($_GET['ToDownload_product_type']) ? sanitize_key((string) wp_unslash($_GET['ToDownload_product_type'])) : ''; $discount_code = isset($_GET['ToDownload_discount_code']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_discount_code'])) : ''; $discount_code = strtoupper(trim($discount_code)); $start = isset($_GET['ToDownload_start']) ? self::normalize_date_ymd(sanitize_text_field((string) wp_unslash($_GET['ToDownload_start']))) : ''; $end = isset($_GET['ToDownload_end']) ? self::normalize_date_ymd(sanitize_text_field((string) wp_unslash($_GET['ToDownload_end']))) : ''; $allowed_types = ['' => true, 'download' => true, 'all_access_pass' => true]; if (!isset($allowed_types[$product_type])) { $product_type = ''; } $start_ts = $start !== '' ? strtotime($start . ' 00:00:00 UTC') : false; $end_ts_excl = $end !== '' ? strtotime($end . ' 00:00:00 UTC') : false; if ($end_ts_excl) { $end_ts_excl = $end_ts_excl + DAY_IN_SECONDS; } $orders = Orders::find_orders_for_seller($seller_user_id, 200, 0); $rows_out = []; foreach ($orders as $row) { $order = (array) ($row['order'] ?? []); $items = (array) ($row['items'] ?? []); $created_at = (string) ($order['created_at'] ?? ''); $created_ts = $created_at !== '' ? strtotime($created_at . ' UTC') : false; if ($start_ts && $created_ts && $created_ts < $start_ts) { continue; } if ($end_ts_excl && $created_ts && $created_ts >= $end_ts_excl) { continue; } $notes = (string) ($order['notes'] ?? ''); $code = self::extract_discount_code_from_order_notes($notes); if ($discount_code !== '' && $code !== $discount_code) { continue; } // Product type filter applies to the seller items. $filtered_items = []; foreach ($items as $it) { $download_id = (int) ($it['download_id'] ?? 0); $is_pass = self::is_all_access_pass_product($download_id); if ($product_type === 'download' && $is_pass) { continue; } if ($product_type === 'all_access_pass' && !$is_pass) { continue; } $filtered_items[] = $it; } if (empty($filtered_items)) { continue; } $row['items'] = $filtered_items; $rows_out[] = $row; } header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="2dl-reports-' . (int) $seller_user_id . '-' . gmdate('Ymd-His') . '.csv"'); header('Pragma: no-cache'); header('Expires: 0'); $out = fopen('php://output', 'w'); if (!$out) { wp_die(esc_html__('Could not write export.', '2download-connector')); } fputcsv($out, [ 'order_id', 'order_date_utc', 'status', 'customer_email', 'discount_code', 'order_gross_total_usd', 'order_net_total_usd', 'product_id', 'product_title', 'qty', 'line_total_usd', ]); foreach ($rows_out as $row) { $order = (array) ($row['order'] ?? []); $customer = (array) ($row['customer'] ?? []); $items = (array) ($row['items'] ?? []); $order_id = (int) ($order['id'] ?? 0); $created_at = (string) ($order['created_at'] ?? ''); $status = (string) ($order['status'] ?? ''); $email = (string) ($customer['email'] ?? ''); $notes = (string) ($order['notes'] ?? ''); $code = self::extract_discount_code_from_order_notes($notes); $order_gross = 0.0; foreach ($items as $it) { $order_gross += (float) ($it['line_total'] ?? 0); } $order_gross = (float) round($order_gross, 2); $order_net = Orders::seller_net_amount_estimated($order_gross, $notes, $platform_percent); foreach ($items as $it) { $download_id = (int) ($it['download_id'] ?? 0); $qty = (int) ($it['qty'] ?? 0); $line_total = (float) ($it['line_total'] ?? 0); $title = $download_id > 0 ? (string) get_the_title($download_id) : ''; fputcsv($out, [ $order_id, $created_at, $status, $email, $code, number_format((float) round($order_gross, 2), 2, '.', ''), number_format((float) round($order_net, 2), 2, '.', ''), $download_id, $title, $qty, number_format((float) round($line_total, 2), 2, '.', ''), ]); } } exit; } public static function enqueue_settings_assets(string $hook): void { if (!is_admin()) { return; } if (!current_user_can('edit_todownload_downloads')) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin page routing. $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : ''; if (!in_array($page, ['ToDownload_seller_settings', 'ToDownload_seller_onboarding'], true)) { return; } // Allows selecting/uploading images for the storefront avatar. if (function_exists('wp_enqueue_media')) { wp_enqueue_media(); } // Reuse the existing admin CSS for consistent form spacing across seller pages. wp_enqueue_style( 'todownload-admin-datepicker', ToDownload_PLUGIN_URL . 'assets/admin-datepicker.css', [], ToDownload_VERSION ); $js_ver = ToDownload_VERSION; $js_path = ToDownload_PLUGIN_DIR . 'assets/admin-seller-settings.js'; if (is_readable($js_path)) { $mtime = filemtime($js_path); if ($mtime) { $js_ver = (string) $mtime; } } wp_enqueue_script( 'todownload-seller-settings', ToDownload_PLUGIN_URL . 'assets/admin-seller-settings.js', ['jquery'], $js_ver, true ); wp_localize_script('todownload-seller-settings', 'ToDownloadSellerSettingsI18n', [ 'selectAvatarTitle' => __('Select avatar', '2download-connector'), 'selectLogoTitle' => __('Select logo', '2download-connector'), 'useThisImage' => __('Use this image', '2download-connector'), ]); } public static function menu(): void { if (!current_user_can('edit_todownload_downloads')) { return; } $menu_title = __('2DL', '2download-connector'); $page_title = __('2DL', '2download-connector'); $position = 56; // Use a dashicon for menu rendering; we overlay the logo via CSS in admin_head_branding(). $icon = 'dashicons-store'; add_menu_page( $page_title, $menu_title, 'edit_todownload_downloads', 'ToDownload_seller', [self::class, 'render_dashboard'], $icon, $position ); add_submenu_page( 'ToDownload_seller', __('Dashboard', '2download-connector'), __('Dashboard', '2download-connector'), 'edit_todownload_downloads', 'ToDownload_seller', [self::class, 'render_dashboard'] ); add_submenu_page( 'ToDownload_seller', __('Onboarding', '2download-connector'), __('Onboarding', '2download-connector'), 'edit_todownload_downloads', 'ToDownload_seller_onboarding', [self::class, 'render_onboarding'] ); add_submenu_page( 'ToDownload_seller', __('Sales', '2download-connector'), __('Sales', '2download-connector'), 'edit_todownload_downloads', 'ToDownload_seller_sales', [self::class, 'render_sales'] ); add_submenu_page( 'ToDownload_seller', __('Comp Access', '2download-connector'), __('Comp Access', '2download-connector'), 'edit_todownload_downloads', 'ToDownload_seller_comp_access', [self::class, 'render_comp_access'] ); add_submenu_page( 'ToDownload_seller', __('Reports', '2download-connector'), __('Reports', '2download-connector'), 'edit_todownload_downloads', 'ToDownload_seller_reports', [self::class, 'render_reports'] ); add_submenu_page( 'ToDownload_seller', __('Reviews', '2download-connector'), __('Reviews', '2download-connector'), 'edit_todownload_downloads', 'ToDownload_seller_reviews', [self::class, 'render_reviews'] ); add_submenu_page( 'ToDownload_seller', __('Settings', '2download-connector'), __('Settings', '2download-connector'), 'edit_todownload_downloads', 'ToDownload_seller_settings', [self::class, 'render_settings'] ); // Hide the redundant "Add New" submenu items for CPTs under 2DL. // Sellers can still add from the list screen button. remove_submenu_page('ToDownload_seller', 'post-new.php?post_type=' . DownloadPostType::POST_TYPE); remove_submenu_page('ToDownload_seller', 'post-new.php?post_type=' . Discounts::POST_TYPE); self::reorder_seller_menu_tabs(); } private static function reorder_seller_menu_tabs(): void { global $submenu; if (!is_array($submenu) || !isset($submenu['ToDownload_seller']) || !is_array($submenu['ToDownload_seller'])) { return; } $items = $submenu['ToDownload_seller']; $by_slug = []; foreach ($items as $it) { if (!is_array($it) || !isset($it[2])) { continue; } $by_slug[(string) $it[2]] = $it; } $order = [ 'ToDownload_seller', 'ToDownload_seller_onboarding', 'edit.php?post_type=' . DownloadPostType::POST_TYPE, 'edit.php?post_type=' . Discounts::POST_TYPE, 'ToDownload_seller_comp_access', 'ToDownload_seller_sales', 'ToDownload_seller_reports', 'ToDownload_seller_reviews', 'ToDownload_seller_settings', ]; $sorted = []; foreach ($order as $slug) { if (isset($by_slug[$slug])) { $sorted[] = $by_slug[$slug]; unset($by_slug[$slug]); } } // Append any remaining submenu items we didn't explicitly order. foreach ($items as $it) { if (!is_array($it) || !isset($it[2])) { continue; } $slug = (string) $it[2]; if (isset($by_slug[$slug])) { $sorted[] = $it; unset($by_slug[$slug]); } } $submenu['ToDownload_seller'] = $sorted; } private static function tier_label(string $tier): string { $tier = Pricing::normalize_tier($tier); if ($tier === Pricing::TIER_PRO) { return __('Pro', '2download-connector'); } if ($tier === Pricing::TIER_STUDIO) { return __('Studio', '2download-connector'); } return __('Standard', '2download-connector'); } private static function checkout_target_url(): string { $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $checkout_page_id = (int) ($settings['checkout_page_id'] ?? 0); $checkout_url = (string) ($settings['checkout_url'] ?? ''); $target = $checkout_page_id ? (string) get_permalink($checkout_page_id) : ''; if ($target === '' && $checkout_url !== '') { $target = $checkout_url; } return $target !== '' ? $target : home_url('/'); } private static function receipt_url_for_order(int $order_id, string $email): string { $order_id = (int) $order_id; $email = sanitize_email($email); $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $success = (string) ($settings['success_url'] ?? home_url('/')); if ($success === '') { $success = home_url('/'); } if ($order_id <= 0 || $email === '' || !is_email($email)) { return $success; } $order_public_id = Orders::order_public_id($order_id); return (string) add_query_arg([ 'ToDownload_order' => $order_public_id, 'ToDownload_receipt_token' => Token::receipt_token($order_public_id, $email), ], $success); } public static function render_comp_access(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only notice flags set via internal redirect after POST. $error = isset($_GET['ToDownload_comp_error']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_comp_error'])) : ''; $ok = isset($_GET['ToDownload_comp_ok']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_comp_ok'])) : ''; $order_id = isset($_GET['ToDownload_comp_order_id']) ? absint(wp_unslash($_GET['ToDownload_comp_order_id'])) : 0; $receipt = isset($_GET['ToDownload_comp_receipt']) ? esc_url_raw((string) wp_unslash($_GET['ToDownload_comp_receipt'])) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended echo '
'; echo '

' . esc_html__('Comp Access', '2download-connector') . '

'; if ($error !== '') { echo '

' . esc_html($error) . '

'; } else if ($ok !== '') { echo '

' . esc_html($ok) . '

'; if ($order_id > 0) { /* translators: %d = order ID */ echo '

' . sprintf(esc_html__('Order #%d created.', '2download-connector'), (int) $order_id) . '

'; } if ($receipt !== '') { echo '

' . esc_html__('View receipt link', '2download-connector') . '

'; } } $products_url = admin_url('edit.php?post_type=' . DownloadPostType::POST_TYPE); echo '

' . esc_html__('Grant a buyer free access by creating a $0 order (100% discount). This does not use Stripe.', '2download-connector') . '

'; echo '

' . esc_html__('Open your Products list (to get IDs)', '2download-connector') . '

'; echo '
'; echo ''; wp_nonce_field('ToDownload_seller_comp_access', 'ToDownload_nonce'); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; submit_button(__('Grant access (free)', '2download-connector')); echo '
'; echo '
'; } public static function handle_comp_access(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer('ToDownload_seller_comp_access', 'ToDownload_nonce'); $seller_user_id = (int) get_current_user_id(); $email = sanitize_email((string) wp_unslash($_POST['ToDownload_comp_email'] ?? '')); $raw_ids = sanitize_text_field((string) wp_unslash($_POST['ToDownload_comp_product_ids'] ?? '')); $reason = sanitize_text_field((string) wp_unslash($_POST['ToDownload_comp_reason'] ?? '')); $target = admin_url('admin.php?page=ToDownload_seller_comp_access'); if ($email === '' || !is_email($email)) { wp_safe_redirect(add_query_arg(['ToDownload_comp_error' => rawurlencode(__('Enter a valid buyer email.', '2download-connector'))], $target)); exit; } $ids = preg_split('/[^0-9]+/', $raw_ids); $ids = is_array($ids) ? array_values(array_filter(array_map('intval', $ids))) : []; $ids = array_values(array_unique(array_filter($ids, fn($v) => $v > 0))); if (empty($ids)) { wp_safe_redirect(add_query_arg(['ToDownload_comp_error' => rawurlencode(__('Enter at least one product ID.', '2download-connector'))], $target)); exit; } $valid_ids = []; $invalid_ids = []; foreach ($ids as $id) { $id = (int) $id; if ((string) get_post_type($id) !== DownloadPostType::POST_TYPE) { $invalid_ids[] = $id; continue; } $author = (int) get_post_field('post_author', $id); if ($author !== $seller_user_id) { $invalid_ids[] = $id; continue; } $status = (string) get_post_status($id); if ($status !== 'publish') { $invalid_ids[] = $id; continue; } $valid_ids[] = $id; } if (empty($valid_ids)) { $msg = __('No valid products found. Only published products you own are allowed.', '2download-connector'); if (!empty($invalid_ids)) { /* translators: %s = comma-separated list of invalid IDs */ $msg .= ' ' . sprintf(__('Invalid IDs: %s', '2download-connector'), implode(', ', array_map('intval', $invalid_ids))); } wp_safe_redirect(add_query_arg(['ToDownload_comp_error' => rawurlencode($msg)], $target)); exit; } $user = get_user_by('email', $email); $wp_user_id = $user ? (int) $user->ID : null; $customer_id = Orders::create_customer([ 'user_id' => $wp_user_id && $wp_user_id > 0 ? $wp_user_id : null, 'email' => $email, 'first_name' => '', 'last_name' => '', ]); $subtotal = 0.0; foreach ($valid_ids as $id) { $subtotal += (float) get_post_meta((int) $id, '_ToDownload_price', true); } $subtotal = (float) round(max(0.0, $subtotal), 2); $discount = $subtotal; $total = 0.0; $order_id = Orders::create_order([ 'customer_id' => (int) $customer_id, 'seller_user_id' => $seller_user_id, 'status' => 'pending', 'subtotal' => $subtotal, 'tax' => 0.0, 'discount' => $discount, 'total' => $total, 'gateway' => 'free', ]); Orders::append_order_note($order_id, 'ToDownload_discount_code: COMP'); Orders::append_order_note($order_id, 'ToDownload_discount_amount: ' . number_format((float) $discount, 2, '.', '')); Orders::append_order_note($order_id, 'ToDownload_seller_comp_access: seller_user_id=' . (int) $seller_user_id); if ($reason !== '') { Orders::append_order_note($order_id, 'ToDownload_seller_comp_reason: ' . $reason); } $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field((string) wp_unslash($_SERVER['REMOTE_ADDR'])) : ''; $ua = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; if ($ip !== '') { Orders::append_order_note($order_id, 'ToDownload_comp_ip: ' . $ip); } if ($ua !== '') { Orders::append_order_note($order_id, 'ToDownload_comp_ua: ' . $ua); } foreach ($valid_ids as $download_id) { $download_id = (int) $download_id; if ($download_id <= 0) { continue; } $price = (float) get_post_meta($download_id, '_ToDownload_price', true); $product_type = (string) get_post_meta($download_id, '_ToDownload_product_type', true); if ($product_type === 'bundle') { $bundle_items = get_post_meta($download_id, '_ToDownload_bundle_items', true); if (!is_array($bundle_items)) { $bundle_items = []; } $bundle_items = array_values(array_filter(array_map('intval', $bundle_items))); if (!empty($bundle_items)) { Orders::append_order_note($order_id, 'ToDownload_bundle_price: ' . $download_id . ':' . number_format((float) $price, 2, '.', '')); Orders::append_order_note($order_id, 'ToDownload_bundle: ' . $download_id . ' -> ' . implode(',', $bundle_items)); foreach ($bundle_items as $child_id) { Orders::add_order_item($order_id, (int) $child_id, 0.0); } continue; } } Orders::add_order_item($order_id, $download_id, $price); } Orders::update_status($order_id, 'complete', 'comp_' . (int) $order_id); do_action('ToDownload_after_order_complete', $order_id); $receipt = self::receipt_url_for_order($order_id, $email); wp_safe_redirect(add_query_arg([ 'ToDownload_comp_ok' => rawurlencode(__('Access granted (free).', '2download-connector')), 'ToDownload_comp_order_id' => (int) $order_id, 'ToDownload_comp_receipt' => rawurlencode($receipt), ], $target)); exit; } private static function membership_plan_product_id_for_tier(string $tier): int { $tier = Pricing::normalize_tier($tier); $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } if ($tier === Pricing::TIER_PRO) { return max(0, (int) ($settings['seller_plan_pro_product_id'] ?? 0)); } if ($tier === Pricing::TIER_STUDIO) { return max(0, (int) ($settings['seller_plan_studio_product_id'] ?? 0)); } return 0; } private static function stripe_membership_yearly_price_id_for_tier(string $tier): string { $tier = Pricing::normalize_tier($tier); if ($tier === Pricing::TIER_PRO && defined('TODOWNLOAD_STRIPE_PRICE_PRO_YEARLY')) { $val = trim((string) TODOWNLOAD_STRIPE_PRICE_PRO_YEARLY); return (strpos($val, 'price_') === 0) ? $val : ''; } if ($tier === Pricing::TIER_STUDIO && defined('TODOWNLOAD_STRIPE_PRICE_STUDIO_YEARLY')) { $val = trim((string) TODOWNLOAD_STRIPE_PRICE_STUDIO_YEARLY); return (strpos($val, 'price_') === 0) ? $val : ''; } return ''; } private static function membership_buy_plan_url(string $tier): string { $tier = Pricing::normalize_tier($tier); if ($tier !== Pricing::TIER_PRO && $tier !== Pricing::TIER_STUDIO) { return ''; } $url = add_query_arg([ 'action' => 'ToDownload_seller_buy_plan', 'tier' => $tier, ], admin_url('admin-post.php')); return (string) wp_nonce_url($url, 'ToDownload_seller_buy_plan_' . $tier); } private static function render_plan_panel(int $seller_user_id): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice params set via internal redirect. $notice = isset($_GET['ToDownload_plan_notice']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_plan_notice'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice params set via internal redirect. $error = isset($_GET['ToDownload_plan_error']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_plan_error'])) : ''; if ($error !== '') { echo '

' . esc_html($error) . '

'; } else if ($notice !== '') { echo '

' . esc_html($notice) . '

'; } $tier = Pricing::get_seller_tier($seller_user_id); $tier_label = self::tier_label($tier); $expires = (string) get_user_meta($seller_user_id, Pricing::USER_META_TIER_EXPIRES_AT, true); $expires = trim($expires); $expires_display = self::format_date_for_display($expires); $subscription_status = (string) get_user_meta($seller_user_id, Pricing::USER_META_SUBSCRIPTION_STATUS, true); $subscription_status = sanitize_key(trim($subscription_status)); $subscription_status_label = $subscription_status !== '' ? ucwords(str_replace('_', ' ', $subscription_status)) : __('Active', '2download-connector'); $expires_label_html = '—'; if ($tier === Pricing::TIER_PRO || $tier === Pricing::TIER_STUDIO) { /* translators: 1: tier label, 2: subscription status */ $line1 = sprintf(__('Subscription status: %1$s — %2$s', '2download-connector'), $tier_label, $subscription_status_label); /* translators: %s = renewal date */ $line2 = sprintf(__('Subscription auto renewal: %s', '2download-connector'), $expires_display !== '' ? $expires_display : '—'); $expires_label_html = esc_html($line1) . '
' . esc_html($line2); } else if ($expires !== '') { $expires_label_html = esc_html($expires_display !== '' ? $expires_display : $expires); } $stripe_fees_included = Pricing::stripe_fees_included_for_seller($seller_user_id); $platform_fee_percent = Pricing::platform_fee_percent_for_seller($seller_user_id); $hosted = Sellers::hosted_credentials($seller_user_id); $hosted_base = isset($hosted['hosted_base']) ? trim((string) $hosted['hosted_base']) : ''; $hosted_host = strtolower((string) (wp_parse_url($hosted_base, PHP_URL_HOST) ?? '')); $manage_url = ($hosted_host === 'staging.2dl.app') ? 'https://staging.2dl.app' : 'https://2dl.app'; echo '

' . esc_html__('Your Plan', '2download-connector') . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
' . esc_html__('Current tier', '2download-connector') . '' . esc_html($tier_label) . '
' . esc_html__('Platform fee', '2download-connector') . '' . esc_html(rtrim(rtrim(number_format($platform_fee_percent, 2, '.', ''), '0'), '.')) . '%
' . esc_html__('Stripe processing fees', '2download-connector') . '' . esc_html($stripe_fees_included ? __('Included in platform fee', '2download-connector') : __('Seller-paid', '2download-connector')) . '
' . esc_html__('Tier renewal / expiry', '2download-connector') . '' . wp_kses_post($expires_label_html) . '
'; echo '

' . esc_html__('Subscription', '2download-connector') . '

'; echo '

' . esc_html__('Your subscription tier and billing are managed on 2dl.app.', '2download-connector') . '

'; echo '

' . esc_html__('Manage on 2dl.app', '2download-connector') . '

'; } public static function render_dashboard(): void { try { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $seller_user_id = get_current_user_id(); if (!current_user_can('manage_options') && !Sellers::is_onboarded($seller_user_id) && !headers_sent()) { wp_safe_redirect(self::seller_onboarding_url()); exit; } $hosted = Sellers::hosted_credentials($seller_user_id); $hosted_ok = !empty($hosted['connected']); $connector_mode = self::platform_stripe_secret_key() === ''; $stripe_account_id = ''; $stripe_ready = false; $connect_url = ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only error param set via internal redirect. $hosted_stripe_error = isset($_GET[self::HOSTED_STRIPE_CONNECT_ERROR_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::HOSTED_STRIPE_CONNECT_ERROR_PARAM])) : ''; if ($connector_mode) { if ($hosted_ok) { $st = self::hosted_stripe_connect_status($seller_user_id); $stripe_account_id = (string) ($st['account_id'] ?? ''); $stripe_ready = !empty($st['charges_enabled']) && !empty($st['payouts_enabled']); } $connect_url = wp_nonce_url( admin_url('admin-post.php?action=ToDownload_seller_stripe_connect_hosted&ToDownload_return=dashboard'), 'ToDownload_seller_stripe_connect_hosted', 'ToDownload_nonce' ); } else { $stripe_account_id = Sellers::get_stripe_connect_account_id($seller_user_id); $stripe_status = Sellers::get_stripe_connect_status($seller_user_id); $stripe_ready = !empty($stripe_status['charges_enabled']) && !empty($stripe_status['payouts_enabled']); $connect_url = wp_nonce_url( admin_url('admin-post.php?action=ToDownload_seller_stripe_connect'), 'ToDownload_seller_stripe_connect', 'ToDownload_nonce' ); } $products_url = admin_url('edit.php?post_type=' . DownloadPostType::POST_TYPE); $add_url = admin_url('post-new.php?post_type=' . DownloadPostType::POST_TYPE); $discounts_url = admin_url('edit.php?post_type=' . Discounts::POST_TYPE); $sales_url = admin_url('admin.php?page=ToDownload_seller_sales'); $reports_url = admin_url('admin.php?page=ToDownload_seller_reports'); $reviews_url = admin_url('admin.php?page=ToDownload_seller_reviews'); $settings_url = admin_url('admin.php?page=ToDownload_seller_settings'); $mailto_features = 'mailto:2download@2dl.app?subject=' . rawurlencode('features suggestion'); $mailto_support = 'mailto:2download@2dl.app?subject=' . rawurlencode('support'); echo '
'; self::render_brand_header(__('Dashboard', '2download-connector')); echo '

' . esc_html__('Your seller overview and plan information.', '2download-connector') . '

'; if ($hosted_stripe_error !== '') { echo '

' . esc_html($hosted_stripe_error) . '

'; } // Store connection (2dl.app) status. $connect_store_url = wp_nonce_url( admin_url('admin-post.php?action=ToDownload_hosted_connect_start&ToDownload_return=dashboard'), 'ToDownload_hosted_connect_start', 'ToDownload_nonce' ); if (!$hosted_ok) { echo '

' . esc_html__('Your store is not connected to 2dl.app yet. Connect your store to enable hosted checkout and Stripe onboarding.', '2download-connector') . '

'; echo '

' . esc_html__('Connect your store', '2download-connector') . '

'; } else { echo '

' . esc_html__('Store Connected', '2download-connector') . '

'; echo '

' . esc_html__('Store Connected', '2download-connector') . '

'; } if ($connector_mode && !$hosted_ok) { echo '

' . esc_html__('Connect your store first. Then you can connect Stripe to receive payouts for your sales.', '2download-connector') . '

'; } else if ($stripe_account_id === '') { echo '

' . esc_html__('Stripe is not connected. Connect Stripe to receive payouts for your sales.', '2download-connector') . '

'; echo '

' . esc_html__('Connect Stripe', '2download-connector') . '

'; } else if (!$stripe_ready) { echo '

' . esc_html__('Stripe is connected, but not ready for payouts yet. Complete Stripe onboarding to receive payouts.', '2download-connector') . '

'; echo '

' . esc_html__('Continue Stripe setup', '2download-connector') . '

'; } else { // Show a faded/disabled "Stripe Connected" state on the dashboard. echo '

' . esc_html__('Stripe Connected', '2download-connector') . '

'; echo '

' . esc_html__('Stripe Connected', '2download-connector') . '

'; } self::render_plan_panel($seller_user_id); echo '

' . esc_html__('Quick Access', '2download-connector') . '

'; echo '

'; echo '' . esc_html__('Discount Codes', '2download-connector') . ' '; echo '' . esc_html__('Sales', '2download-connector') . ' '; echo '' . esc_html__('Reports', '2download-connector') . ' '; echo '' . esc_html__('Reviews', '2download-connector') . ' '; echo '' . esc_html__('Settings', '2download-connector') . ''; echo '

'; echo '

' . esc_html__('Support', '2download-connector') . '

'; echo '

' . '' . esc_html__('Suggest new features', '2download-connector') . ' ' . '' . esc_html__('Contact support', '2download-connector') . '' . '

'; echo '

' . esc_html__('Products', '2download-connector') . '

'; echo '

'; echo '' . esc_html__('Add Product', '2download-connector') . ' '; echo '' . esc_html__('View Products', '2download-connector') . ''; echo '

'; if (self::should_show_cart_debug()) { self::render_cart_debug_panel(); } echo '
'; } catch (\Throwable $e) { self::render_error_panel(__('Dashboard', '2download-connector'), $e); } } public static function handle_save_settings(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer('ToDownload_seller_settings', 'ToDownload_nonce'); $seller_user_id = get_current_user_id(); $enabled = !empty($_POST['ToDownload_checkout_discount_preview']); update_user_meta($seller_user_id, Sellers::USER_META_CHECKOUT_DISCOUNT_PREVIEW, $enabled ? '1' : '0'); $tax_enabled = !empty($_POST['ToDownload_tax_enabled']); update_user_meta($seller_user_id, Sellers::USER_META_TAX_ENABLED, $tax_enabled ? '1' : '0'); $tax_percent_raw = isset($_POST['ToDownload_tax_percent']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_tax_percent'])) : ''; $tax_percent = (float) $tax_percent_raw; $tax_percent = max(0.0, min(100.0, $tax_percent)); update_user_meta($seller_user_id, Sellers::USER_META_TAX_PERCENT, (string) $tax_percent); $access_days_raw = isset($_POST['ToDownload_default_access_days']) ? trim(sanitize_text_field((string) wp_unslash($_POST['ToDownload_default_access_days']))) : ''; if ($access_days_raw === '') { delete_user_meta($seller_user_id, Sellers::USER_META_DEFAULT_ACCESS_DAYS); } else { $days = max(0, (int) $access_days_raw); update_user_meta($seller_user_id, Sellers::USER_META_DEFAULT_ACCESS_DAYS, (string) $days); } $bio = isset($_POST['ToDownload_storefront_bio']) ? sanitize_textarea_field((string) wp_unslash($_POST['ToDownload_storefront_bio'])) : ''; update_user_meta($seller_user_id, 'description', $bio); $avatar_url = isset($_POST['ToDownload_storefront_avatar_url']) ? esc_url_raw((string) wp_unslash($_POST['ToDownload_storefront_avatar_url'])) : ''; if ($avatar_url !== '') { update_user_meta($seller_user_id, 'ToDownload_storefront_avatar_url', $avatar_url); } else { delete_user_meta($seller_user_id, 'ToDownload_storefront_avatar_url'); } $business_name = isset($_POST['ToDownload_storefront_business_name']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_storefront_business_name'])) : ''; $business_name = trim($business_name); if ($business_name !== '') { update_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_BUSINESS_NAME, $business_name); } else { delete_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_BUSINESS_NAME); } $support_email = isset($_POST['ToDownload_support_email']) ? sanitize_email((string) wp_unslash($_POST['ToDownload_support_email'])) : ''; $support_email = trim($support_email); if ($support_email !== '' && is_email($support_email)) { update_user_meta($seller_user_id, Sellers::USER_META_SUPPORT_EMAIL, $support_email); } else { delete_user_meta($seller_user_id, Sellers::USER_META_SUPPORT_EMAIL); } $logo_url = isset($_POST['ToDownload_storefront_logo_url']) ? esc_url_raw((string) wp_unslash($_POST['ToDownload_storefront_logo_url'])) : ''; $logo_url = trim($logo_url); if ($logo_url !== '') { update_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_LOGO_URL, $logo_url); } else { delete_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_LOGO_URL); } $featured_download_id = isset($_POST['ToDownload_storefront_featured_download_id']) ? absint(wp_unslash($_POST['ToDownload_storefront_featured_download_id'])) : 0; $featured_download_id = max(0, $featured_download_id); if ($featured_download_id > 0) { // Validate ownership + publish status. $p = get_post($featured_download_id); if (($p instanceof \WP_Post) && (string) $p->post_type === DownloadPostType::POST_TYPE && (string) $p->post_status === 'publish' && (int) $p->post_author === (int) $seller_user_id) { update_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_FEATURED_DOWNLOAD_ID, (string) $featured_download_id); } else { delete_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_FEATURED_DOWNLOAD_ID); } } else { delete_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_FEATURED_DOWNLOAD_ID); } $target = (string) add_query_arg([ 'page' => 'ToDownload_seller_settings', 'updated' => '1', self::SETTINGS_NOTICE_NONCE_PARAM => function_exists('wp_create_nonce') ? (string) wp_create_nonce(self::SETTINGS_NOTICE_NONCE_ACTION) : '', ], admin_url('admin.php')); wp_safe_redirect($target); exit; } public static function handle_save_onboarding(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer('ToDownload_seller_onboarding', 'ToDownload_nonce'); $seller_user_id = get_current_user_id(); $step = isset($_POST['ToDownload_onboarding_step']) ? sanitize_key((string) wp_unslash($_POST['ToDownload_onboarding_step'])) : ''; if ($step === 'connect') { check_admin_referer(self::HOSTED_MANUAL_CONNECT_NONCE_ACTION, 'ToDownload_manual_nonce'); // Sellers should not control which 2dl.app environment they connect to. // Use the site-level setting (production vs staging) configured by admins. $settings = get_option('ToDownload_settings', []); $settings = is_array($settings) ? $settings : []; $base = trim((string) ($settings['hosted_api_base_url'] ?? 'https://2dl.app')); $base = $base !== '' ? rtrim($base, '/') : 'https://2dl.app'; // Manual recovery input names (kept for backwards-compatible ops + verification guardrails): // ToDownload_manual_store_public_id // ToDownload_manual_store_api_key $store_public_id = isset($_POST['ToDownload_manual_store_public_id']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_manual_store_public_id'])) : (isset($_POST['ToDownload_hosted_store_public_id']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_hosted_store_public_id'])) : ''); $store_public_id = trim($store_public_id); $api_key = isset($_POST['ToDownload_manual_store_api_key']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_manual_store_api_key'])) : (isset($_POST['ToDownload_hosted_store_api_key']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_hosted_store_api_key'])) : ''); $api_key = trim($api_key); update_user_meta($seller_user_id, Sellers::USER_META_HOSTED_API_BASE_URL, $base); if ($store_public_id !== '') { update_user_meta($seller_user_id, Sellers::USER_META_HOSTED_STORE_PUBLIC_ID, $store_public_id); } else { delete_user_meta($seller_user_id, Sellers::USER_META_HOSTED_STORE_PUBLIC_ID); } if ($api_key !== '') { update_user_meta($seller_user_id, Sellers::USER_META_HOSTED_STORE_API_KEY, $api_key); } else { delete_user_meta($seller_user_id, Sellers::USER_META_HOSTED_STORE_API_KEY); } } if ($step === 'storefront') { $bio = isset($_POST['ToDownload_storefront_bio']) ? sanitize_textarea_field((string) wp_unslash($_POST['ToDownload_storefront_bio'])) : ''; update_user_meta($seller_user_id, 'description', $bio); $avatar_url = isset($_POST['ToDownload_storefront_avatar_url']) ? esc_url_raw((string) wp_unslash($_POST['ToDownload_storefront_avatar_url'])) : ''; $avatar_url = trim($avatar_url); if ($avatar_url !== '') { update_user_meta($seller_user_id, 'ToDownload_storefront_avatar_url', $avatar_url); } else { delete_user_meta($seller_user_id, 'ToDownload_storefront_avatar_url'); } $business_name = isset($_POST['ToDownload_storefront_business_name']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_storefront_business_name'])) : ''; $business_name = trim($business_name); if ($business_name !== '') { update_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_BUSINESS_NAME, $business_name); } else { delete_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_BUSINESS_NAME); } $support_email = isset($_POST['ToDownload_support_email']) ? sanitize_email((string) wp_unslash($_POST['ToDownload_support_email'])) : ''; $support_email = trim($support_email); if ($support_email !== '' && is_email($support_email)) { update_user_meta($seller_user_id, Sellers::USER_META_SUPPORT_EMAIL, $support_email); } else { delete_user_meta($seller_user_id, Sellers::USER_META_SUPPORT_EMAIL); } $logo_url = isset($_POST['ToDownload_storefront_logo_url']) ? esc_url_raw((string) wp_unslash($_POST['ToDownload_storefront_logo_url'])) : ''; $logo_url = trim($logo_url); if ($logo_url !== '') { update_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_LOGO_URL, $logo_url); } else { delete_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_LOGO_URL); } } if ($step === 'finish') { Sellers::mark_onboarded($seller_user_id, true); wp_safe_redirect(self::seller_dashboard_url()); exit; } wp_safe_redirect(self::seller_onboarding_url(['updated' => '1'])); exit; } public static function render_onboarding(): void { try { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $seller_user_id = get_current_user_id(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice flag set via internal redirect. $updated = !empty($_GET['updated']) && sanitize_key((string) wp_unslash($_GET['updated'])) === '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice params set via internal redirect. $hosted_connect_error = isset($_GET[self::HOSTED_CONNECT_ERROR_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::HOSTED_CONNECT_ERROR_PARAM])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice params set via internal redirect. $hosted_connect_notice = isset($_GET[self::HOSTED_CONNECT_NOTICE_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::HOSTED_CONNECT_NOTICE_PARAM])) : ''; $hosted = Sellers::hosted_credentials($seller_user_id); $hosted_ok = !empty($hosted['connected']); $business_name = (string) get_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_BUSINESS_NAME, true); $support_email = (string) get_user_meta($seller_user_id, Sellers::USER_META_SUPPORT_EMAIL, true); $logo_url = (string) get_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_LOGO_URL, true); $avatar_url = (string) get_user_meta($seller_user_id, 'ToDownload_storefront_avatar_url', true); $bio = (string) get_user_meta($seller_user_id, 'description', true); $storefront_ok = trim($business_name) !== '' && trim($support_email) !== '' && is_email($support_email); $onboarded = Sellers::is_onboarded($seller_user_id); echo '
'; self::render_brand_header(__('Onboarding', '2download-connector')); echo '

' . esc_html__('Complete these steps to connect your WordPress site to your 2dl.app store: connect 2dl.app, configure your storefront, and finish setup.', '2download-connector') . '

'; if ($updated) { echo '

' . esc_html__('Saved.', '2download-connector') . '

'; } if ($onboarded) { echo '

' . esc_html__('Onboarding is marked complete. You can still update these settings any time.', '2download-connector') . '

'; } echo '
'; echo '

' . esc_html__('Step 1 — Connect 2dl.app', '2download-connector') . ' ' . ($hosted_ok ? '(' . esc_html__('Complete', '2download-connector') . ')' : '(' . esc_html__('Required', '2download-connector') . ')') . '

'; if ($hosted_connect_error !== '') { echo '

' . esc_html($hosted_connect_error) . '

'; } else if ($hosted_connect_notice !== '') { echo '

' . esc_html($hosted_connect_notice) . '

'; } echo '

' . esc_html__('Use the button to sign in on 2dl.app and automatically connect your store, or paste credentials manually.', '2download-connector') . '

'; $connect_store_url = wp_nonce_url( admin_url('admin-post.php?action=ToDownload_hosted_connect_start&ToDownload_return=onboarding'), 'ToDownload_hosted_connect_start', 'ToDownload_nonce' ); echo '

' . esc_html__('Connect your store', '2download-connector') . '

'; echo '
'; echo ''; echo ''; wp_nonce_field('ToDownload_seller_onboarding', 'ToDownload_nonce'); wp_nonce_field(self::HOSTED_MANUAL_CONNECT_NONCE_ACTION, 'ToDownload_manual_nonce'); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; submit_button(__('Save Connection', '2download-connector')); echo '
'; echo '
'; echo '

' . esc_html__('Step 2 — Connect Stripe', '2download-connector') . '

'; $secret = self::platform_stripe_secret_key(); if ($secret === '') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only error param set via internal redirect. $hosted_stripe_error = isset($_GET[self::HOSTED_STRIPE_CONNECT_ERROR_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::HOSTED_STRIPE_CONNECT_ERROR_PARAM])) : ''; if ($hosted_stripe_error !== '') { echo '

' . esc_html($hosted_stripe_error) . '

'; } if (!$hosted_ok) { echo '

' . esc_html__('Connect 2dl.app in Step 1 first. Then you can start Stripe onboarding.', '2download-connector') . '

'; } else { $connect_url = wp_nonce_url( admin_url('admin-post.php?action=ToDownload_seller_stripe_connect_hosted'), 'ToDownload_seller_stripe_connect_hosted', 'ToDownload_nonce' ); echo '

' . esc_html__('This will open Stripe onboarding in a new flow and return you back when complete.', '2download-connector') . '

'; echo '

' . esc_html__('Connect Stripe', '2download-connector') . '

'; } } else { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only error param set via internal redirect. $stripe_error = isset($_GET[self::STRIPE_CONNECT_ERROR_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::STRIPE_CONNECT_ERROR_PARAM])) : ''; if ($stripe_error !== '') { echo '

' . esc_html($stripe_error) . '

'; } $connect_url = wp_nonce_url( admin_url('admin-post.php?action=ToDownload_seller_stripe_connect&ToDownload_return=onboarding'), 'ToDownload_seller_stripe_connect', 'ToDownload_nonce' ); echo '

' . esc_html__('Connect / Continue Stripe Setup', '2download-connector') . '

'; } echo '
'; echo '

' . esc_html__('Step 3 — Storefront details', '2download-connector') . ' ' . ($storefront_ok ? '(' . esc_html__('Complete', '2download-connector') . ')' : '(' . esc_html__('Recommended', '2download-connector') . ')') . '

'; echo '
'; echo ''; echo ''; wp_nonce_field('ToDownload_seller_onboarding', 'ToDownload_nonce'); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; submit_button(__('Save Storefront', '2download-connector')); echo '
'; echo '
'; echo '

' . esc_html__('Finish', '2download-connector') . '

'; echo '

' . esc_html__('When you’re ready, mark onboarding complete.', '2download-connector') . '

'; echo '
'; echo ''; echo ''; wp_nonce_field('ToDownload_seller_onboarding', 'ToDownload_nonce'); submit_button(__('Mark Onboarding Complete', '2download-connector'), 'primary'); echo '
'; echo '
'; } catch (\Throwable $e) { self::render_error_panel(__('Onboarding', '2download-connector'), $e); } } public static function render_settings(): void { try { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $seller_user_id = get_current_user_id(); $enabled = Sellers::seller_prefers_discount_preview($seller_user_id); $bio = (string) get_user_meta($seller_user_id, 'description', true); $avatar_url = (string) get_user_meta($seller_user_id, 'ToDownload_storefront_avatar_url', true); $business_name = (string) get_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_BUSINESS_NAME, true); $support_email = (string) get_user_meta($seller_user_id, Sellers::USER_META_SUPPORT_EMAIL, true); $logo_url = (string) get_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_LOGO_URL, true); $featured_download_id = (int) get_user_meta($seller_user_id, Sellers::USER_META_STOREFRONT_FEATURED_DOWNLOAD_ID, true); $seller_tax_enabled = Sellers::seller_tax_enabled($seller_user_id); $seller_tax_percent = Sellers::seller_tax_percent($seller_user_id); $seller_default_access_days = Sellers::seller_default_access_days($seller_user_id); $platform_settings = get_option('ToDownload_settings', []); if (!is_array($platform_settings)) { $platform_settings = []; } $platform_tax_enabled = !empty($platform_settings['enable_tax']); $platform_tax_percent = (float) ($platform_settings['tax_percent'] ?? 0); $platform_tax_percent = max(0.0, min(100.0, $platform_tax_percent)); $platform_default_access_days = (int) ($platform_settings['default_access_days'] ?? 0); $platform_default_access_days = max(0, $platform_default_access_days); $tax_enabled_ui = $seller_tax_enabled; $tax_percent_ui = $seller_tax_percent; if ($tax_enabled_ui === null) { $tax_enabled_ui = $platform_tax_enabled; } if ($tax_percent_ui === null) { $tax_percent_ui = $platform_tax_percent; } $notice_nonce = isset($_GET[self::SETTINGS_NOTICE_NONCE_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::SETTINGS_NOTICE_NONCE_PARAM])) : ''; $notice_ok = ($notice_nonce !== '' && wp_verify_nonce($notice_nonce, self::SETTINGS_NOTICE_NONCE_ACTION)); $updated = $notice_ok && isset($_GET['updated']) && sanitize_key((string) wp_unslash($_GET['updated'])) === '1'; $stripe_notice = $notice_ok && isset($_GET[self::STRIPE_CONNECT_NOTICE_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::STRIPE_CONNECT_NOTICE_PARAM])) : ''; $stripe_error = $notice_ok && isset($_GET[self::STRIPE_CONNECT_ERROR_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::STRIPE_CONNECT_ERROR_PARAM])) : ''; $stripe_account_id = Sellers::get_stripe_connect_account_id($seller_user_id); $stripe_status_label = $stripe_account_id !== '' ? __('Connected', '2download-connector') : __('Not connected', '2download-connector'); $stripe_details_label = ''; if ($stripe_account_id !== '') { $cache_key = 'todownload_stripe_acct_status_' . (int) $seller_user_id; if ($stripe_notice === 'return' || $stripe_notice === 'refresh') { delete_transient($cache_key); } $cached = get_transient($cache_key); if (is_array($cached)) { $stripe_details_label = (string) ($cached['label'] ?? ''); } else { $acct_res = self::stripe_request('GET', '/v1/accounts/' . rawurlencode($stripe_account_id)); if (!empty($acct_res['ok'])) { $acct = (array) ($acct_res['data'] ?? []); $charges_enabled = !empty($acct['charges_enabled']); $payouts_enabled = !empty($acct['payouts_enabled']); $details_submitted = !empty($acct['details_submitted']); Sellers::set_stripe_connect_status($seller_user_id, (bool) $charges_enabled, (bool) $payouts_enabled, (bool) $details_submitted); if ($charges_enabled && $payouts_enabled) { $stripe_details_label = __('(ready)', '2download-connector'); } else if ($details_submitted) { $stripe_details_label = __('(pending verification)', '2download-connector'); } else { $stripe_details_label = __('(onboarding incomplete)', '2download-connector'); } set_transient($cache_key, ['label' => $stripe_details_label], 5 * MINUTE_IN_SECONDS); } } } echo '
'; self::render_brand_header(__('Seller Settings', '2download-connector')); $storefront = self::storefront_url_for_seller($seller_user_id); if ($storefront !== '') { $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $storefront_page_id = (int) ($settings['storefront_page_id'] ?? 0); if ($storefront_page_id > 0 && get_post_status($storefront_page_id) !== 'publish') { $storefront_page_id = 0; } if ($storefront_page_id <= 0) { $page = get_page_by_path('2download-storefront'); $storefront_page_id = ($page instanceof \WP_Post) ? (int) $page->ID : 0; } $edit_storefront = $storefront_page_id > 0 ? (string) add_query_arg( [ 'ToDownload_seller' => (string) $seller_user_id, 'ToDownload_seller_nonce' => function_exists('wp_create_nonce') ? (string) wp_create_nonce('todownload_storefront_seller') : '', ], admin_url('post.php?post=' . (int) $storefront_page_id . '&action=edit') ) : ''; echo '

'; if ($edit_storefront !== '') { echo '' . esc_html__('Edit storefront', '2download-connector') . ' '; } echo '' . esc_html__('View storefront', '2download-connector') . ''; echo '

'; // Quick-start: give sellers copy/paste links + embed codes without requiring page creation permissions. $products_page_id = (int) ($settings['products_page_id'] ?? 0); if ($products_page_id > 0 && get_post_status($products_page_id) !== 'publish') { $products_page_id = 0; } if ($products_page_id <= 0) { $products_page = get_page_by_path('2download-products'); $products_page_id = ($products_page instanceof \WP_Post) ? (int) $products_page->ID : 0; } $products_url = $products_page_id > 0 ? (string) get_permalink($products_page_id) : ''; echo ''; echo '' . '' . '' . ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; if ($products_url !== '') { echo ''; } else { echo ''; } echo ''; echo '
' . esc_html__('Quick links', '2download-connector') . '' . esc_html__('Value', '2download-connector') . '
' . esc_html__('Your storefront URL', '2download-connector') . '
' . esc_html__('Embed storefront on any page', '2download-connector') . '
' . esc_html__('Ask an admin to create a page if you can’t create pages on this site.', '2download-connector') . '
' . esc_html('[ToDownload_storefront seller_user_id="' . (int) $seller_user_id . '"]') . '
' . esc_html__('External service disclosure', '2download-connector') . '' . esc_html__('This plugin communicates with 2DL hosted services (2dl.app) to create hosted checkouts and fetch receipts/licenses. Admins can review details in 2Download Settings.', '2download-connector') . '
' . esc_html__('All products page (site-wide)', '2download-connector') . ' ' . esc_html__('View', '2download-connector') . '' . esc_html__('Not configured yet. Ask an admin to run “Auto-create recommended pages” in 2Download Settings.', '2download-connector') . '
'; } echo '

' . esc_html__('These settings control parts of the buyer checkout experience for your products.', '2download-connector') . '

'; self::render_plan_panel($seller_user_id); if ($stripe_error !== '') { echo '

' . esc_html(rawurldecode($stripe_error)) . '

'; } else if ($stripe_notice !== '') { if ($stripe_notice === 'return') { echo '

' . esc_html__('Stripe onboarding returned. Your connection status may take a moment to update.', '2download-connector') . '

'; } else if ($stripe_notice === 'refresh') { echo '

' . esc_html__('Stripe onboarding needs more info. Please try again.', '2download-connector') . '

'; } } if ($updated) { echo '

' . esc_html__('Settings saved.', '2download-connector') . '

'; } echo '
'; echo ''; wp_nonce_field('ToDownload_seller_settings', 'ToDownload_nonce'); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; // Featured product (optional) $choices = get_posts([ 'post_type' => DownloadPostType::POST_TYPE, 'author' => $seller_user_id, 'post_status' => 'publish', 'posts_per_page' => 200, 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids', ]); if (!is_array($choices)) { $choices = []; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; submit_button(__('Save settings', '2download-connector')); echo '
'; echo '
'; } catch (\Throwable $e) { self::render_error_panel(__('Seller Settings', '2download-connector'), $e); } } public static function render_reviews(): void { try { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $seller_user_id = get_current_user_id(); $download_ids = get_posts([ 'post_type' => \ToDownload\PostTypes\DownloadPostType::POST_TYPE, 'author' => $seller_user_id, 'posts_per_page' => 200, 'fields' => 'ids', 'post_status' => 'any', ]); if (!is_array($download_ids)) { $download_ids = []; } echo '
'; self::render_brand_header(__('Reviews', '2download-connector')); $storefront = self::storefront_url_for_seller($seller_user_id); if ($storefront !== '') { echo '

' . esc_html__('View storefront', '2download-connector') . '

'; } echo '

' . esc_html__('Reviews left on your products.', '2download-connector') . '

'; if (empty($download_ids)) { echo '

' . esc_html__('No products found for your account.', '2download-connector') . '

'; echo '
'; return; } $comments = get_comments([ 'post__in' => $download_ids, 'type' => 'todownload_review', 'status' => 'all', 'number' => 100, 'orderby' => 'comment_date_gmt', 'order' => 'DESC', ]); if (empty($comments)) { echo '

' . esc_html__('No reviews yet.', '2download-connector') . '

'; echo ''; return; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($comments as $c) { /** @var \WP_Comment $c */ $product = get_the_title((int) $c->comment_post_ID); $rating = (int) get_comment_meta((int) $c->comment_ID, 'todownload_rating', true); $rating = max(0, min(5, $rating)); $author = (string) ($c->comment_author ?? ''); $email = (string) ($c->comment_author_email ?? ''); $status = (string) ($c->comment_approved ?? ''); $status_label = $status === '1' ? __('Approved', '2download-connector') : ($status === '0' ? __('Pending', '2download-connector') : $status); $date = (string) ($c->comment_date ?? ''); $edit = admin_url('comment.php?action=editcomment&c=' . (int) $c->comment_ID); $customer_label = trim($author . ($email !== '' ? ' <' . $email . '>' : '')); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Product', '2download-connector') . '' . esc_html__('Rating', '2download-connector') . '' . esc_html__('Customer', '2download-connector') . '' . esc_html__('Status', '2download-connector') . '' . esc_html__('Date', '2download-connector') . '' . esc_html__('Review', '2download-connector') . '
' . esc_html($product) . '' . esc_html($rating > 0 ? (string) $rating . '/5' : '—') . '' . esc_html($customer_label !== '' ? $customer_label : '—') . '' . esc_html($status_label) . '' . esc_html($date) . '' . esc_html(wp_trim_words((string) $c->comment_content, 22, '…')) . '
'; echo ''; } catch (\Throwable $e) { self::render_error_panel(__('Reviews', '2download-connector'), $e); } } public static function render_products(): void { try { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $products_url = admin_url('edit.php?post_type=' . DownloadPostType::POST_TYPE); $add_url = admin_url('post-new.php?post_type=' . DownloadPostType::POST_TYPE); echo '
'; self::render_brand_header(__('Products', '2download-connector')); echo '

' . esc_html__('Manage your products here.', '2download-connector') . '

'; echo '

'; echo '' . esc_html__('Add Product', '2download-connector') . ' '; echo '' . esc_html__('View Products', '2download-connector') . ''; echo '

'; echo '
'; } catch (\Throwable $e) { self::render_error_panel(__('Products', '2download-connector'), $e); } } public static function render_reports(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $filters_ok = false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading nonce itself for verification. $filters_nonce = isset($_GET[self::REPORT_FILTER_NONCE_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::REPORT_FILTER_NONCE_PARAM])) : ''; if ($filters_nonce !== '' && wp_verify_nonce($filters_nonce, self::REPORT_FILTER_NONCE_ACTION)) { $filters_ok = true; } $allowed_types = [ '' => __('All product types', '2download-connector'), 'download' => __('Products', '2download-connector'), 'all_access_pass' => __('All Access Passes', '2download-connector'), ]; $product_type = ''; if ($filters_ok && isset($_GET['ToDownload_product_type'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $product_type = sanitize_key((string) wp_unslash($_GET['ToDownload_product_type'])); } if (!array_key_exists($product_type, $allowed_types)) { $product_type = ''; } $discount_code = ''; if ($filters_ok && isset($_GET['ToDownload_discount_code'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $discount_code = sanitize_text_field((string) wp_unslash($_GET['ToDownload_discount_code'])); } $discount_code = strtoupper(trim($discount_code)); $start = ''; if ($filters_ok && isset($_GET['ToDownload_start'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $start = self::normalize_date_ymd(sanitize_text_field((string) wp_unslash($_GET['ToDownload_start']))); } $end = ''; if ($filters_ok && isset($_GET['ToDownload_end'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $end = self::normalize_date_ymd(sanitize_text_field((string) wp_unslash($_GET['ToDownload_end']))); } $start_ts = $start !== '' ? self::site_midnight_ts($start) : 0; $end_ts_excl = $end !== '' ? self::site_midnight_ts_plus_days($end, 1) : 0; $seller_user_id = get_current_user_id(); $platform_percent = (float) Pricing::platform_fee_percent_for_seller((int) $seller_user_id); $platform_percent = max(0.0, min(100.0, $platform_percent)); $order_rows = Orders::find_orders_for_seller($seller_user_id, 200, 0); $report_rows = []; foreach ($order_rows as $row) { $order = (array) ($row['order'] ?? []); $items = (array) ($row['items'] ?? []); $created_at = (string) ($order['created_at'] ?? ''); $created_ts = self::order_created_ts_utc($created_at); if ($start_ts > 0 && $created_ts > 0 && $created_ts < $start_ts) { continue; } if ($end_ts_excl > 0 && $created_ts > 0 && $created_ts >= $end_ts_excl) { continue; } $notes = (string) ($order['notes'] ?? ''); $code = self::extract_discount_code_from_order_notes($notes); if ($discount_code !== '' && $code !== $discount_code) { continue; } $filtered_items = []; foreach ($items as $it) { $download_id = (int) ($it['download_id'] ?? 0); $is_pass = self::is_all_access_pass_product($download_id); if ($product_type === 'download' && $is_pass) { continue; } if ($product_type === 'all_access_pass' && !$is_pass) { continue; } $filtered_items[] = $it; } if (empty($filtered_items)) { continue; } $row['items'] = $filtered_items; $report_rows[] = $row; } // Compute totals for the visible (filtered) rows. $earned = 0.0; $earned_net = 0.0; foreach ($report_rows as $r0) { $order0 = (array) ($r0['order'] ?? []); $items0 = (array) ($r0['items'] ?? []); $status0 = (string) ($order0['status'] ?? ''); if ($status0 !== 'complete') { continue; } $notes0 = (string) ($order0['notes'] ?? ''); $gross0 = 0.0; foreach ($items0 as $it0) { $gross0 += (float) ($it0['line_total'] ?? 0); } $gross0 = (float) round($gross0, 2); $earned += $gross0; $earned_net += Orders::seller_net_amount_estimated($gross0, $notes0, $platform_percent); } $earned = (float) round($earned, 2); $earned_net = (float) round($earned_net, 2); echo '
'; self::render_brand_header(__('Reports', '2download-connector')); echo '

' . esc_html__('Generate sales reports and view earnings totals.', '2download-connector') . '

'; echo '

' . esc_html__('Filters', '2download-connector') . '

'; echo '

' . esc_html__('Dates use the site timezone (Settings → General → Timezone).', '2download-connector') . '

'; echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; submit_button(__('Apply', '2download-connector'), 'secondary', '', false); echo '
'; $export_url = admin_url('admin-post.php?action=ToDownload_seller_export_reports'); $export_url = add_query_arg([ 'ToDownload_start' => $start, 'ToDownload_end' => $end, 'ToDownload_product_type' => $product_type, 'ToDownload_discount_code' => $discount_code, ], $export_url); $export_url = wp_nonce_url($export_url, self::REPORT_EXPORT_NONCE_ACTION, self::REPORT_EXPORT_NONCE_PARAM); echo '

' . esc_html__('Generate CSV', '2download-connector') . '

'; echo '

' . esc_html__('Earnings totals', '2download-connector') . '

'; echo ''; echo ''; echo ''; echo ''; echo '
' . esc_html__('Total gross earned', '2download-connector') . '' . esc_html(Money::usd($earned)) . '
' . esc_html__('Total net earned', '2download-connector') . '' . esc_html(Money::usd($earned_net)) . '
'; echo '

' . esc_html__('Sales report', '2download-connector') . '

'; if (empty($report_rows)) { echo '

' . esc_html__('No matching sales found for the selected filters.', '2download-connector') . '

'; } else { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($report_rows as $r) { $order = (array) ($r['order'] ?? []); $items = (array) ($r['items'] ?? []); $order_id = (int) ($order['id'] ?? 0); $created_at = (string) ($order['created_at'] ?? ''); $status = (string) ($order['status'] ?? ''); $notes = (string) ($order['notes'] ?? ''); $code = self::extract_discount_code_from_order_notes($notes); $item_labels = []; $seller_total = 0.0; foreach ($items as $it) { $download_id = (int) ($it['download_id'] ?? 0); $qty = (int) ($it['qty'] ?? 0); $line_total = (float) ($it['line_total'] ?? 0); $title = $download_id > 0 ? (string) get_the_title($download_id) : ''; $label = $title !== '' ? $title : ('#' . (string) $download_id); $item_labels[] = $label . ($qty > 1 ? (' ×' . (string) $qty) : ''); $seller_total += $line_total; } $seller_total = (float) round($seller_total, 2); $seller_net = Orders::seller_net_amount_estimated($seller_total, $notes, $platform_percent); $net_cell = Money::usd($seller_net); if ($status !== 'complete') { $net_cell = __('Available when complete', '2download-connector'); } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; $discount_label = $code !== '' ? $code : '—'; if ($code === 'COMP') { $discount_label = 'COMP (' . __('Comped', '2download-connector') . ')'; } echo ''; echo ''; } echo '
' . esc_html__('Order', '2download-connector') . '' . esc_html__('Date', '2download-connector') . '' . esc_html__('Status', '2download-connector') . '' . esc_html__('Items', '2download-connector') . '' . esc_html__('Gross', '2download-connector') . '' . esc_html__('Net', '2download-connector') . '' . esc_html__('Discount', '2download-connector') . '
#' . esc_html((string) $order_id) . '' . esc_html($created_at !== '' ? $created_at : '—') . '' . esc_html($status !== '' ? $status : '—') . '' . esc_html(!empty($item_labels) ? implode(', ', $item_labels) : '—') . '' . esc_html(Money::usd($seller_total)) . '' . esc_html($net_cell) . '' . esc_html($discount_label) . '
'; } echo '
'; } public static function redirect_products(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } wp_safe_redirect(admin_url('edit.php?post_type=' . DownloadPostType::POST_TYPE)); exit; } public static function redirect_add_product(): void { if (!current_user_can('publish_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } wp_safe_redirect(admin_url('post-new.php?post_type=' . DownloadPostType::POST_TYPE)); exit; } public static function redirect_discounts(): void { if (!current_user_can('edit_todownload_discounts')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } wp_safe_redirect(admin_url('edit.php?post_type=' . Discounts::POST_TYPE)); exit; } public static function redirect_add_discount(): void { if (!current_user_can('publish_todownload_discounts')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } wp_safe_redirect(admin_url('post-new.php?post_type=' . Discounts::POST_TYPE)); exit; } public static function render_customers(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $user_id = get_current_user_id(); $rows = Orders::find_customers_for_seller($user_id, 100); echo '
'; self::render_brand_header(__('Customers', '2download-connector')); if (empty($rows)) { echo '

' . esc_html__('No customers yet.', '2download-connector') . '

'; echo '
'; return; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($rows as $r) { $name = trim((string) ($r['first_name'] ?? '') . ' ' . (string) ($r['last_name'] ?? '')); $email = (string) ($r['email'] ?? ''); $orders_count = (int) ($r['orders_count'] ?? 0); $total = (float) ($r['total'] ?? 0); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Customer', '2download-connector') . '' . esc_html__('Email', '2download-connector') . '' . esc_html__('Orders', '2download-connector') . '' . esc_html__('Total With You', '2download-connector') . '
' . esc_html($name !== '' ? $name : __('(no name)', '2download-connector')) . '' . esc_html($email) . '' . esc_html((string) $orders_count) . '' . esc_html(Money::usd($total)) . '
'; echo '

' . esc_html__('Totals reflect completed orders only.', '2download-connector') . '

'; echo ''; } public static function render_sales(): void { if (!current_user_can('edit_todownload_downloads')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $filters_ok = false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading nonce itself for verification. $filters_nonce = isset($_GET[self::SALES_FILTER_NONCE_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::SALES_FILTER_NONCE_PARAM])) : ''; if ($filters_nonce !== '' && wp_verify_nonce($filters_nonce, self::SALES_FILTER_NONCE_ACTION)) { $filters_ok = true; } $status_filter = ''; if ($filters_ok && isset($_GET['ToDownload_status'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $status_filter = sanitize_key((string) wp_unslash($_GET['ToDownload_status'])); } $date_filter = '30d'; if ($filters_ok && isset($_GET['ToDownload_date'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $date_filter = sanitize_key((string) wp_unslash($_GET['ToDownload_date'])); } $allowed_status = ['' => __('All statuses', '2download-connector'), 'complete' => __('Complete', '2download-connector'), 'refunded' => __('Refunded', '2download-connector'), 'pending' => __('Pending', '2download-connector')]; if (!array_key_exists($status_filter, $allowed_status)) { $status_filter = ''; } $allowed_date = [ 'all' => __('All time', '2download-connector'), '7d' => __('Last 7 days', '2download-connector'), '30d' => __('Last 30 days', '2download-connector'), '90d' => __('Last 90 days', '2download-connector'), 'this_month' => __('This month', '2download-connector'), 'last_month' => __('Last month', '2download-connector'), ]; if (!array_key_exists($date_filter, $allowed_date)) { $date_filter = '30d'; } $now = time(); $start_ts = 0; $end_ts = 0; if ($date_filter === '7d') { $start_ts = $now - (7 * DAY_IN_SECONDS); } else if ($date_filter === '30d') { $start_ts = $now - (30 * DAY_IN_SECONDS); } else if ($date_filter === '90d') { $start_ts = $now - (90 * DAY_IN_SECONDS); } else if ($date_filter === 'this_month') { try { $dt = (new \DateTimeImmutable('now', self::site_timezone()))->modify('first day of this month')->setTime(0, 0, 0); $start_ts = (int) $dt->getTimestamp(); } catch (\Throwable $e) { $start_ts = 0; } } else if ($date_filter === 'last_month') { try { $dt_start = (new \DateTimeImmutable('now', self::site_timezone()))->modify('first day of last month')->setTime(0, 0, 0); $dt_end = (new \DateTimeImmutable('now', self::site_timezone()))->modify('first day of this month')->setTime(0, 0, 0); $start_ts = (int) $dt_start->getTimestamp(); $end_ts = (int) $dt_end->getTimestamp(); } catch (\Throwable $e) { $start_ts = 0; $end_ts = 0; } } $user_id = get_current_user_id(); $platform_percent = (float) Pricing::platform_fee_percent_for_seller((int) $user_id); $platform_percent = max(0.0, min(100.0, $platform_percent)); // Use a larger limit so filters (esp. date presets) are useful. $rows = Orders::find_orders_for_seller($user_id, 200); echo '
'; self::render_brand_header(__('Sales', '2download-connector')); // Filters (minimal UI). echo '

' . esc_html__('Dates use the site timezone (Settings → General → Timezone).', '2download-connector') . '

'; echo '
'; echo ''; echo ''; echo ''; echo ''; submit_button(__('Apply', '2download-connector'), 'secondary', '', false); if ($status_filter !== '' || $date_filter !== '30d') { $clear = admin_url('admin.php?page=ToDownload_seller_sales'); echo ' ' . esc_html__('Clear', '2download-connector') . ''; } echo '
'; // KPI summary (WordPress-native, no custom colors). $lifetime_gross = Orders::seller_earnings_total($user_id); $lifetime_net = Orders::seller_net_earnings_total_estimated($user_id, $platform_percent); $last_30_gross = 0.0; $last_30_net = 0.0; $cutoff = time() - (30 * DAY_IN_SECONDS); if (!empty($rows)) { foreach ($rows as $r0) { $order0 = (array) ($r0['order'] ?? []); if ((string) ($order0['status'] ?? '') !== 'complete') { continue; } $dt = (string) ($order0['created_at'] ?? ''); $ts = self::order_created_ts_utc($dt); if ($ts > 0 && $ts >= $cutoff) { $gross = (float) ($r0['seller_total'] ?? 0); $last_30_gross += $gross; $last_30_net += Orders::seller_net_amount_estimated($gross, (string) ($order0['notes'] ?? ''), $platform_percent); } } $last_30_gross = (float) round($last_30_gross, 2); $last_30_net = (float) round($last_30_net, 2); } // Apply filters to table rows. if (!empty($rows) && ($status_filter !== '' || $date_filter !== '30d')) { $filtered = []; foreach ($rows as $r) { $order = (array) ($r['order'] ?? []); $status = (string) ($order['status'] ?? ''); $created_at = (string) ($order['created_at'] ?? ''); $ts = self::order_created_ts_utc($created_at); if ($status_filter !== '' && $status !== $status_filter) { continue; } if ($start_ts > 0 && $ts > 0 && $ts < $start_ts) { continue; } if ($end_ts > 0 && $ts > 0 && $ts >= $end_ts) { continue; } $filtered[] = $r; } $rows = $filtered; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
' . esc_html__('Last 30 days (gross)', '2download-connector') . '' . esc_html(Money::usd($last_30_gross)) . '
' . esc_html__('Last 30 days (net)', '2download-connector') . '' . esc_html(Money::usd($last_30_net)) . '
' . esc_html__('Lifetime earned (gross)', '2download-connector') . '' . esc_html(Money::usd($lifetime_gross)) . '
' . esc_html__('Lifetime earned (net)', '2download-connector') . '' . esc_html(Money::usd($lifetime_net)) . '
'; $add_product = admin_url('post-new.php?post_type=' . DownloadPostType::POST_TYPE); $view_products = admin_url('edit.php?post_type=' . DownloadPostType::POST_TYPE); $storefront = self::storefront_url_for_seller($user_id); echo '

' . '' . esc_html__('Add product', '2download-connector') . ' ' . '' . esc_html__('View products', '2download-connector') . ' ' . ($storefront !== '' ? '' . esc_html__('View storefront', '2download-connector') . '' : '') . '

'; // Top products summary (based on currently filtered rows; completed orders only). $top = []; foreach ($rows as $r_sum) { $order_sum = (array) ($r_sum['order'] ?? []); if ((string) ($order_sum['status'] ?? '') !== 'complete') { continue; } $items_sum = (array) ($r_sum['items'] ?? []); $notes_sum = (string) ($order_sum['notes'] ?? ''); $bundle_extra_sum = self::bundle_sales_from_notes($notes_sum, $user_id); if (!empty($bundle_extra_sum)) { $items_sum = array_merge($bundle_extra_sum, $items_sum); } foreach ($items_sum as $it_sum) { $download_id = (int) ($it_sum['download_id'] ?? 0); if ($download_id <= 0) { continue; } $title = isset($it_sum['title']) ? (string) $it_sum['title'] : (string) get_the_title($download_id); if ($title === '') { $title = '#' . (string) $download_id; } $qty = max(1, (int) ($it_sum['qty'] ?? 1)); $line_total = (float) ($it_sum['line_total'] ?? 0); if (!isset($top[$download_id])) { $top[$download_id] = ['title' => $title, 'qty' => 0, 'total' => 0.0]; } $top[$download_id]['qty'] += $qty; $top[$download_id]['total'] = (float) round(((float) $top[$download_id]['total']) + $line_total, 2); } } if (!empty($top)) { uasort($top, function ($a, $b) { $at = (float) ($a['total'] ?? 0); $bt = (float) ($b['total'] ?? 0); if ($at === $bt) { $aq = (int) ($a['qty'] ?? 0); $bq = (int) ($b['qty'] ?? 0); return $bq <=> $aq; } return $bt <=> $at; }); echo '

' . esc_html__('Top products', '2download-connector') . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; $i = 0; foreach ($top as $download_id => $sum) { $i++; if ($i > 5) { break; } $edit_url = admin_url('post.php?post=' . (int) $download_id . '&action=edit'); echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Product', '2download-connector') . '' . esc_html__('Units', '2download-connector') . '' . esc_html__('Earnings', '2download-connector') . '
' . esc_html((string) ($sum['title'] ?? '')) . '' . esc_html((string) ((int) ($sum['qty'] ?? 0))) . '' . esc_html(Money::usd((float) ($sum['total'] ?? 0))) . '
'; echo '

' . esc_html__('Based on completed orders only (within your current filters).', '2download-connector') . '

'; } if (empty($rows)) { echo '

' . esc_html__('No sales yet.', '2download-connector') . '

'; echo '
'; return; } $settings = get_option('ToDownload_settings', []); $receipt_base = is_array($settings) ? (string) ($settings['success_url'] ?? '') : ''; if ($receipt_base === '') { $receipt_base = home_url('/'); } $is_platform_admin = current_user_can('manage_options'); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($rows as $r) { $order = $r['order']; $order_id = (int) ($order['id'] ?? 0); $date = (string) ($order['created_at'] ?? ''); $status = (string) ($order['status'] ?? ''); $gateway = (string) ($order['gateway'] ?? ''); $txn = (string) ($order['transaction_id'] ?? ''); $customer = $r['customer']; $items = $r['items']; $seller_total = (float) ($r['seller_total'] ?? 0); $notes = (string) ($order['notes'] ?? ''); $discount_code_for_row = self::extract_discount_code_from_order_notes($notes); $is_comp = ($discount_code_for_row === 'COMP'); $bundle_extra = self::bundle_sales_from_notes($notes, $user_id); if (!empty($bundle_extra)) { $items = array_merge($bundle_extra, $items); foreach ($bundle_extra as $be) { $seller_total += (float) ($be['line_total'] ?? 0); } $seller_total = (float) round($seller_total, 2); } $seller_net = Orders::seller_net_amount_estimated($seller_total, $notes, $platform_percent); $net_cell = Money::usd($seller_net); if ($status !== 'complete') { $net_cell = __('Available when complete', '2download-connector'); } else if (strpos($notes, 'ToDownload_payout_mode: separate_transfer') !== false && strpos($notes, 'ToDownload_studio_stripe_fee_minor:') === false) { $txn = (string) ($order['transaction_id'] ?? ''); if ($txn !== '' && strpos($txn, 'pi_') === 0) { $gw = \ToDownload\Gateways\GatewayRegistry::instance()->get('stripe_checkout'); if ($gw && is_object($gw) && method_exists($gw, 'handle_retry_studio_transfer')) { try { call_user_func([$gw, 'handle_retry_studio_transfer'], (int) $order_id, (string) $txn); $fresh = \ToDownload\Data\Orders::get_order((int) $order_id); if (is_array($fresh) && isset($fresh['notes'])) { $notes = (string) $fresh['notes']; $seller_net = \ToDownload\Data\Orders::seller_net_amount_estimated($seller_total, $notes, $platform_percent); $net_cell = Money::usd($seller_net); } } catch (\Throwable $e) { // Ignore } } } } $first = trim((string) ($customer['first_name'] ?? '')); $last = trim((string) ($customer['last_name'] ?? '')); $name = trim($first . ' ' . $last); $email = (string) ($customer['email'] ?? ''); $receipt_link = ''; if ($order_id > 0 && $email !== '' && is_email($email)) { $order_public_id = \ToDownload\Data\Orders::order_public_id((int) $order_id); $receipt_link = add_query_arg([ 'ToDownload_order' => $order_public_id, 'ToDownload_receipt_token' => Token::receipt_token($order_public_id, $email), ], $receipt_base); } $admin_order_link = $order_id > 0 ? add_query_arg(['ToDownload_q' => (string) $order_id], admin_url('admin.php?page=ToDownload_admin')) : ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Order', '2download-connector') . '' . esc_html__('Date', '2download-connector') . '' . esc_html__('Status', '2download-connector') . '' . esc_html__('Gateway', '2download-connector') . '' . esc_html__('Transaction', '2download-connector') . '' . esc_html__('Customer', '2download-connector') . '' . esc_html__('Items', '2download-connector') . '' . esc_html__('Download links', '2download-connector') . '' . esc_html__('Gross', '2download-connector') . '' . esc_html__('Net', '2download-connector') . '
'; if ($is_platform_admin && $admin_order_link !== '') { echo '#' . esc_html((string) $order_id) . ''; } else if ($receipt_link !== '') { echo '#' . esc_html((string) $order_id) . ''; } else { echo '#' . esc_html((string) $order_id); } if ($receipt_link !== '') { echo '
' . esc_html__('Receipt link', '2download-connector') . ''; echo '
'; } echo '
' . esc_html($date) . '' . esc_html($status) . ($is_comp ? '
' . esc_html__('Comped', '2download-connector') . '' : '') . '
' . esc_html($gateway !== '' ? $gateway : '—') . '' . esc_html($txn !== '' ? $txn : '—') . '' . esc_html($name !== '' ? $name : __('(no name)', '2download-connector')) . '
' . esc_html($email) . '
'; if (empty($items)) { echo esc_html__('(no items)', '2download-connector'); } else { echo '
    '; foreach ($items as $it) { $title = isset($it['title']) ? (string) $it['title'] : get_the_title((int) $it['download_id']); $qty = (int) ($it['qty'] ?? 0); $line_total = (float) ($it['line_total'] ?? 0); /* translators: %d: quantity */ $qty_label = sprintf(__('Qty %d', '2download-connector'), max(1, $qty)); echo '
  • ' . esc_html($title ?: ('#' . (int) $it['download_id'])) . ' — ' . esc_html($qty_label) . ' — ' . esc_html(Money::usd($line_total)) . '
  • '; } echo '
'; } echo '
'; if ($status !== 'complete') { echo esc_html__('Available when complete', '2download-connector'); } else if (empty($items)) { echo '—'; } else { echo '
    '; foreach ($items as $it) { $download_id = (int) ($it['download_id'] ?? 0); if (!$download_id) { continue; } $link = DownloadDelivery::generate_link($order_id, $download_id); $title = isset($it['title']) ? (string) $it['title'] : get_the_title($download_id); echo '
  • ' . esc_html($title ?: ('#' . $download_id)) . '
  • '; } echo '
'; } echo '
' . esc_html(Money::usd($seller_total)) . '' . esc_html($net_cell) . '
'; echo '

' . esc_html__('Gross is the sum of items sold by your account. Net subtracts platform fees. For seller-paid Stripe-fee orders, Net also subtracts Stripe processing fees (shown once Stripe records the fee). If a cart contains products from multiple sellers, totals reflect your portion only.', '2download-connector') . '

'; echo ''; } private static function bundle_sales_from_notes(string $notes, int $seller_user_id): array { $notes = (string) $notes; if ($notes === '') { return []; } $out = []; // Marker: ToDownload_bundle_price: BUNDLE_ID:12.34 if (!preg_match_all('/ToDownload_bundle_price:\s*(\d+)\s*:\s*([0-9]+(?:\.[0-9]{1,2})?)/', $notes, $m, PREG_SET_ORDER)) { return []; } foreach ($m as $match) { $bundle_id = (int) ($match[1] ?? 0); $price = (float) ($match[2] ?? 0); if (!$bundle_id) { continue; } $p = get_post($bundle_id); if (!$p || (int) $p->post_author !== (int) $seller_user_id || $p->post_type !== DownloadPostType::POST_TYPE) { continue; } $out[] = [ 'download_id' => $bundle_id, 'title' => get_the_title($bundle_id) . ' (' . __('Bundle', '2download-connector') . ')', 'qty' => 1, 'line_total' => (float) round($price, 2), ]; } return $out; } }