<?php
/**
 * Affiliate Area Integration
 *
 * @package     AffiliateWP Stripe Payouts
 * @subpackage  Integrations/Affiliate Area
 * @copyright   Copyright (c) 2025, AffiliateWP, LLC
 * @license     GPL-2.0+
 * @since       2.29.0
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * AffiliateWP_Stripe_Payouts_Affiliate_Area Class
 *
 * @since 2.29.0
 */
class AffiliateWP_Stripe_Payouts_Affiliate_Area {

	/**
	 * Setup the affiliate area integration
	 *
	 * @since 2.29.0
	 */
	public function __construct() {

		// Render in the "How you'll be paid" section. Priority 10 keeps it below the payout-method picker (priority 5).
		add_action( 'affwp_affiliate_dashboard_payments_section', [ $this, 'add_settings_content' ], 10 );

		// Add admin notices.
		add_action( 'affwp_affiliate_dashboard_notices', [ $this, 'dashboard_notices' ] );

		// Handle Stripe Express Dashboard access.
		add_action( 'init', [ $this, 'process_stripe_dashboard_request' ] );

		// Handle Stripe account management link generation (for onboarding updates).
		add_action( 'init', [ $this, 'process_stripe_manage_request' ] );

		// Treat Stripe as unusable for an affiliate whose country the platform can't pay, so their
		// effective method falls back to the site default (which also hides the connect section).
		// Stripe deliberately stays IN the affiliate-facing picker: hiding it would also hide the
		// country sub-field, stranding an affiliate who picked the wrong country with no way to fix
		// it. The connect-link gate + this resolver enforce serviceability; picker visibility doesn't.
		add_filter( 'affwp_affiliate_usable_payout_method', [ $this, 'downgrade_unserviceable_stripe' ], 10, 3 );

		// Inject Stripe's serviceable allowlist, connect URL, and error copy into the affiliate-area
		// Alpine state so the country field and connect gate read one source (as plain x-data, no $store).
		add_filter( 'affwp_affiliate_area_payout_alpine_data', [ $this, 'inject_payout_alpine_data' ], 10, 2 );

		// Affiliate payouts tab: supply the Stripe bank last-4 for the "Payout Account" column, and an
		// "Upcoming payout — expected to arrive {date}" summary below the table.
		add_filter( 'affwp_payouts_dashboard_account_label', [ $this, 'stripe_payout_account_label' ], 10, 2 );
		add_action( 'affwp_payouts_dashboard_summary_stats', [ $this, 'render_stripe_payout_summary' ], 10, 1 );
	}

	/**
	 * Falls an unserviceable-country affiliate back to the site default instead of Stripe.
	 *
	 * This is where serviceability is actually enforced (Stripe stays visible in the picker so a
	 * wrong-country affiliate can still reach the country field and correct it): if Stripe is the
	 * affiliate's resolved method but their declared country isn't in the shared serviceable
	 * allowlist (the same {@see affwp_stripe_payouts_get_serviceable_countries()} the picker gate and
	 * the save warning use, so all three agree), and they aren't already connected, resolve to the
	 * site default. The affiliate-area UI then shows that default method's label, and the connect-link
	 * gate suppresses the Stripe setup button. Fails open when the serviceable set can't be determined
	 * (empty) so a platform we can't read doesn't strand every affiliate off Stripe.
	 *
	 * @since 2.34.0
	 *
	 * @param string               $usable    The resolved usable method.
	 * @param string               $method    The affiliate's explicitly assigned method.
	 * @param int|\AffWP\Affiliate  $affiliate Affiliate ID or object.
	 * @return string
	 */
	public function downgrade_unserviceable_stripe( $usable, $method, $affiliate ) {
		if ( 'stripe' !== $usable || ! function_exists( 'affwp_stripe_payouts_get_serviceable_countries' ) ) {
			return $usable;
		}

		$affiliate_id = is_object( $affiliate ) && isset( $affiliate->affiliate_id )
			? (int) $affiliate->affiliate_id
			: absint( $affiliate );
		if ( ! $affiliate_id ) {
			return $usable;
		}

		// Already-connected affiliates keep Stripe — they're being paid through it.
		if ( function_exists( 'affwp_stripe_payouts_get_affiliate_account_id' )
			&& affwp_stripe_payouts_get_affiliate_account_id( $affiliate_id )
		) {
			return $usable;
		}

		$country = function_exists( 'affwp_get_affiliate_payout_country' )
			? affwp_get_affiliate_payout_country( $affiliate_id )
			: affwp_get_affiliate_meta( $affiliate_id, 'affwp_stripe_country', true );
		$country = strtoupper( (string) $country );

		// Same allowlist the picker gate and the save warning use, so the effective method agrees with
		// what the affiliate is told. Fail open when the set can't be determined (empty) rather than
		// downgrading every affiliate off Stripe.
		$serviceable = affwp_stripe_payouts_get_serviceable_countries();
		if ( '' !== $country && ! empty( $serviceable ) && ! in_array( $country, $serviceable, true ) ) {
			// Route to the next usable method (shared with the affiliate-area UI so the screen and
			// the payout engine name the same destination). '' means Stripe is the only enabled
			// method — keep $usable so we don't strand them on an empty method.
			$fallback = affwp_stripe_payouts_get_fallback_method();
			return '' !== $fallback ? $fallback : $usable;
		}

		return $usable;
	}

	/**
	 * Injects Stripe's serviceability data into the affiliate-area Alpine state.
	 *
	 * Provides the serviceable-country allowlist, the connect URL, and the unserviceable-country
	 * error template as plain x-data (consumed by the country field and the connect gate). Plain
	 * data, not an Alpine store, so the x-show / :href expressions evaluate under any Alpine present
	 * on the page, including an older one an add-on bundles (the Affiliate Portal), which has no
	 * `$store`. Hooks affwp_affiliate_area_payout_alpine_data.
	 *
	 * @since 2.34.0
	 *
	 * @param array $data         Alpine state being built.
	 * @param int   $affiliate_id Affiliate ID.
	 * @return array
	 */
	public function inject_payout_alpine_data( $data, $affiliate_id ) {
		$data['serviceable'] = function_exists( 'affwp_stripe_payouts_get_serviceable_countries' )
			? array_values( affwp_stripe_payouts_get_serviceable_countries() )
			: array();
		$data['connectBase'] = add_query_arg(
			array(
				'affwp_action' => 'stripe_payouts_connect',
				'affiliate_id' => $affiliate_id,
			),
			affwp_get_affiliate_area_page_url( 'settings' )
		);
		/* translators: %s is replaced client-side with the affiliate's selected country name. */
		$data['nopeTpl'] = __( "Stripe isn't available for payouts in %s.", 'affiliate-wp' );

		// Inline route for the unserviceable-country error. Prefer a self-serve method the affiliate
		// can actually pick (a one-click switch), since the resolver's raw fallback is Manual-first
		// (admin toil) even when PayPal/Store Credit are available. Order of preference:
		//   1. switchTo  — first non-Stripe option in their picker → "Switch to {method}" button.
		//   2. paidVia   — the resolver's effective fallback (e.g. Manual) when nothing's selectable → inform them.
		//   3. dead end  — Stripe is the only enabled method → send them to the admin.
		$picker    = function_exists( 'affwp_get_affiliate_picker_options' )
			? affwp_get_affiliate_picker_options( $affiliate_id )
			: array();
		$options   = ( ! empty( $picker['options'] ) && is_array( $picker['options'] ) ) ? $picker['options'] : array();
		$default   = function_exists( 'affwp_get_default_payout_method' ) ? affwp_get_default_payout_method() : '';
		$switch_to = '';
		// Prefer the site default when it's a non-Stripe method the affiliate can pick: that's the
		// admin's chosen method and where the resolver routes a do-nothing affiliate, so the UI agrees
		// with the payout. Otherwise fall back to the first non-Stripe option in their picker.
		if ( 'stripe' !== $default && '' !== (string) $default && isset( $options[ $default ] ) ) {
			$switch_to = (string) $default;
		} else {
			foreach ( array_keys( $options ) as $option_key ) {
				if ( 'stripe' !== $option_key && '' !== (string) $option_key ) {
					$switch_to = (string) $option_key;
					break;
				}
			}
		}
		$fallback = function_exists( 'affwp_stripe_payouts_get_fallback_method' )
			? affwp_stripe_payouts_get_fallback_method()
			: '';

		// Bare method name ("PayPal"), not the "Paid via PayPal" status label: these feed action strings
		// ("Switch to %s", "You'll be paid via %s instead") where the status phrasing reads doubled.
		$label = function_exists( 'affwp_get_payout_method_display_label' )
			? 'affwp_get_payout_method_display_label'
			: null;

		$data['switchTo']      = $switch_to;
		$data['switchToLabel'] = ( '' !== $switch_to && $label ) ? $label( $switch_to ) : '';
		$data['paidViaLabel']  = ( '' !== $fallback && 'stripe' !== $fallback && $label ) ? $label( $fallback ) : '';
		/* translators: %s is the payout method to switch to (e.g. PayPal). */
		$data['switchTpl']     = __( 'Switch to %s', 'affiliate-wp' );
		/* translators: %s is the payout method the affiliate will be paid via instead (e.g. Store Credit). */
		$data['viaTpl']        = __( "You'll be paid via %s instead.", 'affiliate-wp' );
		$data['deadEndMsg']    = __( 'Contact your site administrator to arrange payment.', 'affiliate-wp' );

		return $data;
	}

	/**
	 * Supplies the connected bank's last four as the "Payout Account" label for Stripe payouts.
	 *
	 * @since 2.34.0
	 *
	 * @param string $label  Default label.
	 * @param object $payout Payout object.
	 * @return string
	 */
	public function stripe_payout_account_label( $label, $payout ) {
		if ( empty( $payout->payout_method ) || 'stripe' !== $payout->payout_method ) {
			return $label;
		}
		if ( ! function_exists( 'affwp_stripe_payouts_get_affiliate_bank_last4' ) ) {
			return $label;
		}

		// This filter fires once per payout row; the last4 is the same for every row of a given
		// affiliate, so memoize per request to avoid an N-row x 1-lookup fan-out.
		static $last4_cache = array();

		$affiliate_id = (int) $payout->affiliate_id;
		if ( ! array_key_exists( $affiliate_id, $last4_cache ) ) {
			$last4_cache[ $affiliate_id ] = affwp_stripe_payouts_get_affiliate_bank_last4( $affiliate_id );
		}
		$last4 = $last4_cache[ $affiliate_id ];

		/* translators: %s: last four digits of a bank account. */
		return $last4 ? sprintf( __( 'Bank •••• %s', 'affiliate-wp' ), $last4 ) : $label;
	}

	/**
	 * Renders an "Upcoming payout — expected to arrive {date}" summary below the affiliate payouts
	 * table, mirroring the Stripe Express dashboard. The arrival is per-affiliate (their next bank
	 * payout), so this is a single summary rather than a date repeated on every row.
	 *
	 * @since 2.34.0
	 *
	 * @param int $affiliate_id Affiliate ID.
	 * @return void
	 */
	public function render_stripe_payout_summary( $affiliate_id ) {
		if ( ! function_exists( 'affwp_stripe_payouts_get_affiliate_account_id' )
			|| ! affwp_stripe_payouts_get_affiliate_account_id( $affiliate_id )
			|| ! function_exists( 'affwp_stripe_payouts_get_affiliate_payout_summary' )
		) {
			return;
		}

		$summary = affwp_stripe_payouts_get_affiliate_payout_summary( $affiliate_id );
		if ( empty( $summary ) || empty( $summary['amount'] ) || ( isset( $summary['status'] ) && 'paid' === $summary['status'] ) ) {
			return;
		}

		$labels = affwp_stripe_payouts_payout_summary_labels( $summary );

		// Render as a card matching the core summary stats (Unpaid earnings / Lifetime paid). Show the
		// exact arrival once Stripe has created the Payout; until then (balance fallback) say "Processing".
		echo '<div class="affwp-payout-summary__stat">';
		echo '<span class="affwp-payout-summary__label">' . esc_html__( 'Next payout', 'affiliate-wp' ) . '</span>';
		echo '<span class="affwp-payout-summary__value">' . esc_html( $labels['amount_label'] ) . '</span>';
		echo '<span class="affwp-payout-summary__meta">' . esc_html( $labels['meta_label'] ) . '</span>';
		echo '</div>';
	}

	/**
	 * Add Stripe connect section to the settings tab
	 *
	 * @since 2.29.0
	 * @return void
	 */
	public function add_settings_content() {
		// Get the current affiliate.
		$affiliate_id = affwp_get_affiliate_id();

		if ( ! affwp_stripe_payouts_section_is_relevant_for_affiliate( $affiliate_id ) ) {
			return;
		}

		// Check platform status first.
		$platform_status = affwp_stripe_payouts_check_affiliate_platform_status( $affiliate_id );

		// Resolve the affiliate's Stripe state. A finished onboarding leaves a connected account ID; an
		// abandoned one leaves only a pending ID. Treat pending as connected-but-incomplete so the
		// affiliate can resume instead of seeing the from-scratch pitch.
		$completed_account_id = affwp_stripe_payouts_get_affiliate_account_id( $affiliate_id );
		$pending_account_id   = affwp_stripe_payouts_get_affiliate_pending_account_id( $affiliate_id );

		$platform_changed = false;
		$is_pending       = false;

		if ( ! empty( $completed_account_id ) && 'invalid' === $platform_status ) {
			// A completed connection whose platform changed: force a full reconnect.
			$is_connected     = false;
			$platform_changed = true;
			$account_id       = '';
		} elseif ( ! empty( $completed_account_id ) ) {
			$is_connected = true;
			$account_id   = $completed_account_id;
		} elseif ( ! empty( $pending_account_id ) ) {
			$is_connected = true;
			$is_pending   = true;
			$account_id   = $pending_account_id;
		} else {
			$is_connected = false;
			$account_id   = '';
		}

		// Connect URL for the static (no-JS) href fallback. The serviceable allowlist, connect URL, and
		// error template reach Alpine as plain x-data via the affwp_affiliate_area_payout_alpine_data
		// filter (see inject_payout_alpine_data) rather than a $store, so the expressions evaluate under
		// any Alpine on the page (an add-on's older bundled Alpine can't resolve $store).
		$affwp_sp_connect_base = add_query_arg(
			array(
				'affwp_action' => 'stripe_payouts_connect',
				'affiliate_id' => $affiliate_id,
			),
			affwp_get_affiliate_area_page_url( 'settings' )
		);

		// Connected / platform-changed status shows whenever Stripe is the selected method; the
		// disconnected connect pitch only when the chosen country is serviceable. serviceable.length
		// === 0 fails open so an undeterminable platform never hides connect for everyone. Connect
		// state is baked here, so Alpine only needs method + country + the injected allowlist.
		$affwp_sp_section_x_show = ( $is_connected || $platform_changed )
			? "method === 'stripe'"
			: "method === 'stripe' && country && ( serviceable.length === 0 || serviceable.indexOf( country.toUpperCase() ) !== -1 )";
		?>
		<div
			class="affwp-stripe-payouts"
			id="stripe-payouts"
			x-show="<?php echo esc_attr( $affwp_sp_section_x_show ); ?>"
			x-cloak
		>
			<?php if ( $is_connected ) : ?>
				<?php
				// A pending account is onboarding-in-progress by definition; skip the Stripe status
				// call and treat it as incomplete so the affiliate is sent back to resume.
				$is_onboarding_complete = false;
				if ( ! $is_pending ) {
					$account_status         = affwp_stripe_payouts_get_account_status( $account_id );
					$is_onboarding_complete = $account_status['details_submitted'] ?? false;
				}

				// Incomplete account + cross-border on: let an affiliate stuck with a wrong-country
				// account start over and reconnect in their own country.
				$can_reonboard_country = ! $is_onboarding_complete
					&& function_exists( 'affwp_stripe_payouts_cross_border_enabled' )
					&& affwp_stripe_payouts_cross_border_enabled()
					&& function_exists( 'affwp_stripe_payouts_platform_supports_cross_border' )
					&& affwp_stripe_payouts_platform_supports_cross_border();
				?>
				<div class="affwp-stripe-payouts__status affwp-stripe-payouts__status--<?php echo $is_onboarding_complete ? 'connected' : 'warning'; ?>">
					<span class="affwp-stripe-payouts__account-info">
						<span class="affwp-stripe-payouts__account-info-label">
							<?php
							echo $is_onboarding_complete
								? esc_html__( 'Stripe payouts are enabled. Connected Account ID:', 'affiliate-wp' )
								: esc_html__( 'Stripe setup in progress. Account ID:', 'affiliate-wp' );
							?>
						</span>
						<code class="affwp-stripe-payouts__account-info-value"><?php echo esc_html( $account_id ); ?></code>
					</span>
				</div>
				<p class="affwp-stripe-payouts__description <?php echo $is_onboarding_complete ? 'affwp-stripe-payouts__description--onboarded' : 'affwp-stripe-payouts__description--pending'; ?>">
					<?php if ( $is_onboarding_complete ) : ?>
						<?php esc_html_e( 'Your Stripe account is connected and ready to receive payouts. You can manage your bank account details, tax information, and other settings through your Stripe Dashboard.', 'affiliate-wp' ); ?>
					<?php else : ?>
						<?php esc_html_e( 'Your Stripe account is connected but needs to complete the onboarding process. Click "Complete Setup" to finish providing the required information for receiving payouts.', 'affiliate-wp' ); ?>
						<?php if ( $can_reonboard_country ) : ?>
							<?php esc_html_e( 'If you receive payouts in a different country, start over to choose it.', 'affiliate-wp' ); ?>
						<?php endif; ?>
					<?php endif; ?>
				</p>
				<div class="affwp-stripe-payouts__actions">
					<?php if ( $is_onboarding_complete ) : ?>
						<a href="<?php echo esc_url( add_query_arg( [ 'affwp_action' => 'stripe_payouts_dashboard', 'affiliate_id' => $affiliate_id, '_wpnonce' => wp_create_nonce( 'affwp_stripe_payouts_dashboard_' . $affiliate_id ) ], affwp_get_affiliate_area_page_url( 'settings' ) ) ); ?>" target="_blank" class="affwp-stripe-payouts__link affwp-stripe-payouts__link--dashboard">
							<?php esc_html_e( 'Access Stripe Dashboard', 'affiliate-wp' ); ?>
						</a>
					<?php else : ?>
						<?php
						// Pending onboarding resumes through the connect handler (it picks the existing
						// account back up and finishes it); a completed account that Stripe later flagged
						// uses the manage/account-update link instead.
						$affwp_sp_setup_href = $is_pending
							? add_query_arg( [ 'affwp_action' => 'stripe_payouts_connect', 'affiliate_id' => $affiliate_id ], affwp_get_affiliate_area_page_url( 'settings' ) )
							: add_query_arg( [ 'affwp_action' => 'stripe_payouts_manage', 'affiliate_id' => $affiliate_id, '_wpnonce' => wp_create_nonce( 'affwp_stripe_payouts_manage_' . $affiliate_id ) ], affwp_get_affiliate_area_page_url( 'settings' ) );
						?>
						<a href="<?php echo esc_url( $affwp_sp_setup_href ); ?>" class="affwp-stripe-payouts__link affwp-stripe-payouts__link--setup">
							<?php esc_html_e( 'Complete Setup', 'affiliate-wp' ); ?>
						</a>
						<?php if ( $can_reonboard_country ) : ?>
							<a href="<?php echo esc_url( add_query_arg( [ 'affwp_action' => 'stripe_payouts_disconnect', 'affiliate_id' => $affiliate_id, '_wpnonce' => wp_create_nonce( 'affwp_stripe_payouts_disconnect_' . $affiliate_id ) ], affwp_get_affiliate_area_page_url( 'settings' ) ) ); ?>" class="affwp-stripe-payouts__link affwp-stripe-payouts__link--reonboard">
								<?php esc_html_e( 'Choose a different country', 'affiliate-wp' ); ?>
							</a>
						<?php endif; ?>
					<?php endif; ?>
				</div>

			<?php else : ?>

				<?php if ( $platform_changed ) : ?>
					<div class="affwp-stripe-payouts__status affwp-stripe-payouts__status--warning">
						<?php esc_html_e( '⚠️ Your Stripe connection has been reset due to a platform change.', 'affiliate-wp' ); ?>
					</div>
					<p class="affwp-stripe-payouts__description affwp-stripe-payouts__description--platform-changed">
						<?php esc_html_e( 'The site administrator has changed their Stripe account configuration. For security reasons, you\'ll need to reconnect with Stripe to continue receiving payouts. This is a one-time process to ensure your payment information remains secure.', 'affiliate-wp' ); ?>
					</p>
				<?php else : ?>
					<p id="affwp-stripe-pitch" class="affwp-stripe-payouts__description affwp-stripe-payouts__description--disconnected">
						<?php esc_html_e( 'To receive your referral payouts, we\'ll set up a secure Stripe account for you. This allows us to send payments directly to your bank account. Click below to provide your basic information - Stripe will guide you through a quick verification process to ensure secure payments.', 'affiliate-wp' ); ?>
					</p>
				<?php endif; ?>
				<div class="affwp-stripe-payouts__actions">
					<a
						id="affwp-stripe-connect-link"
						href="<?php echo esc_url( $affwp_sp_connect_base ); ?>"
						:href="connectBase + ( connectBase.indexOf( '?' ) === -1 ? '?' : '&' ) + 'affwp_stripe_country=' + encodeURIComponent( country.toUpperCase() )"
						class="affwp-stripe-payouts__link affwp-stripe-payouts__link--connect"
					>
						<?php esc_html_e( 'Set Up Stripe', 'affiliate-wp' ); ?>
					</a>
				</div>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Show notices in the affiliate dashboard
	 *
	 * @since 2.29.0
	 * @return void
	 */
	public function dashboard_notices() {
		// Check for Stripe connection success/error.
		if ( isset( $_GET['stripe_connected'] ) ) {
			?>
			<div class="affwp-stripe-notice affwp-stripe-notice--success">
				<p class="affwp-stripe-notice__message"><?php esc_html_e( 'Your Stripe payout account has been set up successfully!', 'affiliate-wp' ); ?></p>
			</div>
			<?php
		}

		if ( isset( $_GET['stripe_manage_complete'] ) ) {
			?>
			<div class="affwp-stripe-notice affwp-stripe-notice--success">
				<p class="affwp-stripe-notice__message"><?php esc_html_e( 'Your Stripe payout settings have been updated successfully!', 'affiliate-wp' ); ?></p>
			</div>
			<?php
		}

		// One-time flash notices set server-side during the connect flow (never via the URL, so the
		// message can't be spoofed). Read-and-clear, then render styled by type.
		$flash = affwp_stripe_payouts_get_flash_notice();
		if ( $flash ) {
			$variant = 'error' === $flash['type'] ? 'error' : ( 'success' === $flash['type'] ? 'success' : 'info' );
			?>
			<div class="affwp-stripe-notice affwp-stripe-notice--<?php echo esc_attr( $variant ); ?>">
				<p class="affwp-stripe-notice__message"><?php echo esc_html( $flash['message'] ); ?></p>
			</div>
			<?php
		}

		// Check if Stripe is not connected and add a notice.
		if ( isset( $_GET['tab'] ) && 'payouts' === $_GET['tab'] ) {
			$affiliate_id = affwp_get_affiliate_id();

			// Only show if Stripe is configured.
			if ( affwp_stripe_payouts_is_configured() && ! affwp_stripe_payouts_is_affiliate_connected( $affiliate_id ) ) {
				?>
				<div class="affwp-stripe-notice affwp-stripe-notice--info">
					<p class="affwp-stripe-notice__message">
						<?php
						printf(
							__( 'To receive payouts via Stripe, please <a href="%s" class="affwp-stripe-notice__link">set up Stripe</a>.', 'affiliate-wp' ),
							esc_url( affwp_get_affiliate_area_page_url( 'settings' ) . '#stripe-payouts' )
						);
						?>
					</p>
				</div>
				<?php
			}
		}
	}

	/**
	 * Handle Stripe Express Dashboard access request
	 *
	 * This method generates a direct login link to the Stripe Express Dashboard
	 * for affiliates to manage their daily operations like viewing balances and payouts.
	 *
	 * @since 2.29.0
	 * @return void
	 */
	public function process_stripe_dashboard_request() {
		if ( ! isset( $_GET['affwp_action'] ) || 'stripe_payouts_dashboard' !== $_GET['affwp_action'] ) {
			return;
		}

		if ( ! isset( $_GET['affiliate_id'] ) ) {
			return;
		}

		$affiliate_id = affwp_stripe_payouts_validate_affiliate_id( $_GET['affiliate_id'] );
		if ( is_wp_error( $affiliate_id ) ) {
			return;
		}

		// Verify nonce.
		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'affwp_stripe_payouts_dashboard_' . $affiliate_id ) ) {
			wp_die( esc_html__( 'Security check failed', 'affiliate-wp' ) );
		}

		// Generate and redirect to the Stripe Express Dashboard.
		$dashboard_link = affwp_stripe_payouts_generate_express_dashboard_link( $affiliate_id );

		if ( is_wp_error( $dashboard_link ) ) {
			affwp_stripe_payouts_set_flash_notice( $dashboard_link->get_error_message(), 'error' );
			wp_safe_redirect( affwp_get_affiliate_area_page_url( 'settings' ) );
			exit;
		}

		// Redirect to Stripe Express Dashboard.
		wp_redirect( $dashboard_link );
		exit;
	}

	/**
	 * Handle Stripe account management link generation.
	 *
	 * This method generates a Stripe Account Link for onboarding updates and compliance.
	 * Used when affiliates need to update verification info or fix account issues.
	 *
	 * @since 2.29.0
	 * @return void
	 */
	public function process_stripe_manage_request() {
		if ( ! isset( $_GET['affwp_action'] ) || 'stripe_payouts_manage' !== $_GET['affwp_action'] ) {
			return;
		}

		if ( ! isset( $_GET['affiliate_id'] ) ) {
			return;
		}

		$affiliate_id = affwp_stripe_payouts_validate_affiliate_id( $_GET['affiliate_id'] );
		if ( is_wp_error( $affiliate_id ) ) {
			return;
		}

		// Verify nonce.
		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'affwp_stripe_payouts_manage_' . $affiliate_id ) ) {
			wp_die( esc_html__( 'Security check failed', 'affiliate-wp' ) );
		}

		// Generate and redirect to the Stripe Account Link.
		$account_link = affwp_stripe_payouts_generate_account_link( $affiliate_id );

		if ( is_wp_error( $account_link ) ) {
			affwp_stripe_payouts_set_flash_notice( $account_link->get_error_message(), 'error' );
			wp_safe_redirect( affwp_get_affiliate_area_page_url( 'settings' ) );
			exit;
		}

		wp_redirect( $account_link );
		exit;
	}
}
