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 '';
}
// 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 '';
}
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;
?>
';
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 '' . esc_html__('Site host', '2download-connector') . ' ' . esc_html($home_host) . ' ';
echo '' . esc_html__('SSL', '2download-connector') . ' ' . esc_html($ssl) . ' ';
echo '' . esc_html__('COOKIE_DOMAIN', '2download-connector') . ' ' . esc_html($cookie_domain !== '' ? $cookie_domain : '—') . ' ';
echo '' . esc_html__('COOKIEPATH', '2download-connector') . ' ' . esc_html($cookie_path !== '' ? $cookie_path : '—') . ' ';
echo '' . esc_html__('Cart cookie name', '2download-connector') . ' ' . esc_html($cookie_name) . ' ';
echo '' . esc_html__('Cart cookie present', '2download-connector') . ' ' . esc_html($cookie_raw !== '' ? 'yes' : 'no') . ' ';
echo '' . esc_html__('Cart cookie JSON valid', '2download-connector') . ' ' . esc_html($cookie_json_ok ? 'yes' : 'no') . ' ';
echo '' . esc_html__('Cart cookie length', '2download-connector') . ' ' . esc_html((string) $cookie_len) . ' ';
echo '' . esc_html__('Cart cookie item count', '2download-connector') . ' ' . esc_html((string) $cookie_count) . ' ';
echo '' . esc_html__('User ID', '2download-connector') . ' ' . esc_html($user_id > 0 ? (string) $user_id : 'guest') . ' ';
echo '' . esc_html__('User-meta cart item count', '2download-connector') . ' ' . esc_html((string) $meta_count) . ' ';
echo ' ';
echo '
';
}
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 '';
}
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 '';
} 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 '' . esc_html__('Current tier', '2download-connector') . ' ' . esc_html($tier_label) . ' ';
echo '' . esc_html__('Platform fee', '2download-connector') . ' ' . esc_html(rtrim(rtrim(number_format($platform_fee_percent, 2, '.', ''), '0'), '.')) . '% ';
echo '' . esc_html__('Stripe processing fees', '2download-connector') . ' ' . esc_html($stripe_fees_included ? __('Included in platform fee', '2download-connector') : __('Seller-paid', '2download-connector')) . ' ';
echo '' . esc_html__('Tier renewal / expiry', '2download-connector') . ' ' . wp_kses_post($expires_label_html) . ' ';
echo '
';
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 '
';
} 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 '
';
} else if (!$stripe_ready) {
echo '
';
} 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 '
' . 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 '
' . 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 ''
. '' . esc_html__('Quick links', '2download-connector') . ' '
. '' . esc_html__('Value', '2download-connector') . ' '
. ' ';
echo '';
echo '';
echo '' . esc_html__('Your storefront URL', '2download-connector') . ' ';
echo ' ';
echo ' ';
echo '';
echo '' . 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') . ' ';
echo '' . esc_html('[ToDownload_storefront seller_user_id="' . (int) $seller_user_id . '"]') . ' ';
echo ' ';
echo '';
echo '' . esc_html__('External service disclosure', '2download-connector') . ' ';
echo ''
. 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')
. ' ';
echo ' ';
echo '';
echo '' . esc_html__('All products page (site-wide)', '2download-connector') . ' ';
if ($products_url !== '') {
echo ' ' . esc_html__('View', '2download-connector') . ' ';
} else {
echo '' . esc_html__('Not configured yet. Ask an admin to run “Auto-create recommended pages” in 2Download Settings.', '2download-connector') . ' ';
}
echo ' ';
echo '
';
}
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 '';
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 '' . esc_html__('Product', '2download-connector') . ' ';
echo '' . esc_html__('Rating', '2download-connector') . ' ';
echo '' . esc_html__('Customer', '2download-connector') . ' ';
echo '' . esc_html__('Status', '2download-connector') . ' ';
echo '' . esc_html__('Date', '2download-connector') . ' ';
echo '' . esc_html__('Review', '2download-connector') . ' ';
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 '' . esc_html($product) . ' ';
echo '' . esc_html($rating > 0 ? (string) $rating . '/5' : '—') . ' ';
echo '' . esc_html($customer_label !== '' ? $customer_label : '—') . ' ';
echo '' . esc_html($status_label) . ' ';
echo '' . esc_html($date) . ' ';
echo '' . esc_html(wp_trim_words((string) $c->comment_content, 22, '…')) . ' ';
echo ' ';
}
echo '
';
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 '';
} 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 '' . esc_html__('Start', '2download-connector') . ' '
. ''
. ' '
. ''
. ' '
. ' '
. ' '
. ' ';
echo '' . esc_html__('End', '2download-connector') . ' '
. ''
. ' '
. ''
. ' '
. ' '
. ' '
. ' ';
echo '' . esc_html__('Product type', '2download-connector') . ' ';
foreach ($allowed_types as $k => $label) {
echo '' . esc_html($label) . ' ';
}
echo ' ';
echo '' . esc_html__('Discount code', '2download-connector') . ' ';
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 '' . esc_html__('Total gross earned', '2download-connector') . ' ' . esc_html(Money::usd($earned)) . ' ';
echo '' . esc_html__('Total net earned', '2download-connector') . ' ' . esc_html(Money::usd($earned_net)) . ' ';
echo '
';
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 '' . esc_html__('Order', '2download-connector') . ' ';
echo '' . esc_html__('Date', '2download-connector') . ' ';
echo '' . esc_html__('Status', '2download-connector') . ' ';
echo '' . esc_html__('Items', '2download-connector') . ' ';
echo '' . esc_html__('Gross', '2download-connector') . ' ';
echo '' . esc_html__('Net', '2download-connector') . ' ';
echo '' . esc_html__('Discount', '2download-connector') . ' ';
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 '#' . esc_html((string) $order_id) . ' ';
echo '' . esc_html($created_at !== '' ? $created_at : '—') . ' ';
echo '' . esc_html($status !== '' ? $status : '—') . ' ';
echo '' . esc_html(!empty($item_labels) ? implode(', ', $item_labels) : '—') . ' ';
echo '' . esc_html(Money::usd($seller_total)) . ' ';
echo '' . esc_html($net_cell) . ' ';
$discount_label = $code !== '' ? $code : '—';
if ($code === 'COMP') {
$discount_label = 'COMP (' . __('Comped', '2download-connector') . ')';
}
echo '' . esc_html($discount_label) . ' ';
echo ' ';
}
echo '
';
}
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 '' . esc_html__('Customer', '2download-connector') . ' ';
echo '' . esc_html__('Email', '2download-connector') . ' ';
echo '' . esc_html__('Orders', '2download-connector') . ' ';
echo '' . esc_html__('Total With You', '2download-connector') . ' ';
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 '' . esc_html($name !== '' ? $name : __('(no name)', '2download-connector')) . ' ';
echo '' . esc_html($email) . ' ';
echo '' . esc_html((string) $orders_count) . ' ';
echo '' . esc_html(Money::usd($total)) . ' ';
echo ' ';
}
echo '
';
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 '' . esc_html__('Status', '2download-connector') . ' ';
echo '';
foreach ($allowed_status as $k => $label) {
echo '' . esc_html((string) $label) . ' ';
}
echo ' ';
echo '' . esc_html__('Date', '2download-connector') . ' ';
echo '';
foreach ($allowed_date as $k => $label) {
echo '' . esc_html((string) $label) . ' ';
}
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 '' . esc_html__('Last 30 days (gross)', '2download-connector') . ' ' . esc_html(Money::usd($last_30_gross)) . ' ';
echo '' . esc_html__('Last 30 days (net)', '2download-connector') . ' ' . esc_html(Money::usd($last_30_net)) . ' ';
echo '' . esc_html__('Lifetime earned (gross)', '2download-connector') . ' ' . esc_html(Money::usd($lifetime_gross)) . ' ';
echo '' . esc_html__('Lifetime earned (net)', '2download-connector') . ' ' . esc_html(Money::usd($lifetime_net)) . ' ';
echo '
';
$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 '' . esc_html__('Product', '2download-connector') . ' ';
echo '' . esc_html__('Units', '2download-connector') . ' ';
echo '' . esc_html__('Earnings', '2download-connector') . ' ';
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 '' . esc_html((string) ($sum['title'] ?? '')) . ' ';
echo '' . esc_html((string) ((int) ($sum['qty'] ?? 0))) . ' ';
echo '' . esc_html(Money::usd((float) ($sum['total'] ?? 0))) . ' ';
echo ' ';
}
echo '
';
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 '' . esc_html__('Order', '2download-connector') . ' ';
echo '' . esc_html__('Date', '2download-connector') . ' ';
echo '' . esc_html__('Status', '2download-connector') . ' ';
echo '' . esc_html__('Gateway', '2download-connector') . ' ';
echo '' . esc_html__('Transaction', '2download-connector') . ' ';
echo '' . esc_html__('Customer', '2download-connector') . ' ';
echo '' . esc_html__('Items', '2download-connector') . ' ';
echo '' . esc_html__('Download links', '2download-connector') . ' ';
echo '' . esc_html__('Gross', '2download-connector') . ' ';
echo '' . esc_html__('Net', '2download-connector') . ' ';
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 '';
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 '' . esc_html__('Copy receipt link', '2download-connector') . ' ';
}
echo ' ';
echo '' . esc_html($date) . ' ';
echo '' . esc_html($status) . ($is_comp ? '' . esc_html__('Comped', '2download-connector') . ' ' : '') . ' ';
echo '' . esc_html($gateway !== '' ? $gateway : '—') . ' ';
echo '' . esc_html($txn !== '' ? $txn : '—') . ' ';
echo '' . esc_html($name !== '' ? $name : __('(no name)', '2download-connector')) . '' . esc_html($email) . ' ';
echo '';
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 ' ';
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 ' ';
echo '' . esc_html(Money::usd($seller_total)) . ' ';
echo '' . esc_html($net_cell) . ' ';
echo ' ';
}
echo '
';
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;
}
}