# formidable/trunk/paypal/helpers/FrmPayPalLiteConnectHelper.php

Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes &amp; More, version trunk. 1,169 lines.

- Page: https://pluginprobe.com/plugins/formidable/trunk/code/paypal/helpers/FrmPayPalLiteConnectHelper.php
- Raw: https://pluginprobe.com/plugins/formidable/trunk/raw/paypal/helpers/FrmPayPalLiteConnectHelper.php
- Modified: 2026-06-22T16:59:38+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/formidable/trunk/code/paypal/helpers/FrmPayPalLiteConnectHelper.php#L10-L20`.

```php
<?php
// phpcs:ignore SlevomatCodingStandard.Files.FileLength.FileTooLong
if ( ! defined( 'ABSPATH' ) ) {
	die( 'You are not allowed to call this page directly.' );
}

class FrmPayPalLiteConnectHelper {

	/**
	 * Track the latest error when calling the PayPal API.
	 *
	 * @since 6.31
	 *
	 * @var string|null
	 */
	public static $latest_error_from_paypal_api = '';

	/**
	 * Track the latest debug ID from PayPal API responses.
	 *
	 * @since 6.31
	 *
	 * @var string
	 */
	private static $latest_debug_id_from_paypal_api = '';

	/**
	 * @return void
	 */
	public static function render_settings_container() {
		$settings = FrmPayPalLiteAppHelper::get_settings();

		self::register_settings_scripts();

		FrmPayPalLiteAppHelper::fee_education( 'paypal-global-settings-tip' );

		include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/connect-settings-container.php';
	}

	public static function handle_render_seller_status() {
		FrmAppHelper::permission_check( 'frm_change_settings' );

		if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) {
			wp_send_json_error();
		}

		ob_start();
		$success  = self::render_seller_status();
		$response = ob_get_clean();

		if ( ! $success ) {
			wp_send_json_error( $response );
		}

		wp_send_json_success( $response );
	}

	/**
	 * @since 6.31
	 *
	 * @return bool
	 */
	public static function render_seller_status() {
		FrmAppHelper::permission_check( 'frm_change_settings' );

		if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) {
			self::render_error( __( 'Invalid nonce.', 'formidable' ) );
			return false;
		}

		$mode        = self::get_mode_value_from_post();
		$merchant_id = self::get_merchant_id( $mode );

		if ( ! $merchant_id ) {
			// Do not render any message when not connected.
			// And return true so it does not try to handle it as an error.
			return true;
		}

		$status = self::get_seller_status();

		/*
		$status = new stdClass();
		$status->payments_receivable = true;
		$status->primary_email_confirmed = true;
		$status->oauth_integrations = true;
		$status->primary_email = 'test@example.com';
		*/

		if ( ! is_object( $status ) ) {
			self::render_error( __( 'Unable to retrieve seller status.', 'formidable' ), '', $merchant_id );
			return false;
		}

		$email               = $status->primary_email ?? '';
		$paypal_settings_url = self::get_paypal_account_settings_url( $mode );

		if ( empty( $status->primary_email_confirmed ) ) {
			self::render_error( __( 'Primary email not confirmed.', 'formidable' ), $email, $merchant_id, $paypal_settings_url );
			return false;
		}

		if ( ! $status->payments_receivable ) {
			self::render_error( __( 'Payments are not receivable.', 'formidable' ), $email, $merchant_id, $paypal_settings_url );
			return false;
		}

		if ( ! $status->oauth_integrations ) {
			self::render_error(
				__( 'OAuth integrations are not enabled. Please finish connecting your PayPal account.', 'formidable' ),
				$email,
				$merchant_id,
				'',
				$mode
			);
			return false;
		}

		// OAuth integrations are valid. Clear any stored tracking_id from a prior incomplete onboarding.
		delete_option( self::get_tracking_id_option_name( $mode ) );

		$product                       = self::check_for_product( $status->products, 'PPCP_CUSTOM' );
		$only_supports_checkout_button = false;

		if ( ! $product || empty( $product->capabilities ) ) {
			$product = self::check_for_product( $status->products, 'EXPRESS_CHECKOUT' );

			if ( ! $product ) {
				self::render_error( __( 'No data was found for expected PayPal product.', 'formidable' ), $email, $merchant_id );
				return false;
			}

			if ( 'ACTIVE' !== $product->status ) {
				self::render_error( __( 'PayPal Checkout is not available.', 'formidable' ), $email, $merchant_id );
				return false;
			}

			$only_supports_checkout_button = true;
		}

		if ( $email ) {
			update_option( self::get_paypal_seller_status_option_name( $mode ), $status, false );
		}

		echo '<div class="frm_message">';
		esc_html_e( 'Your seller status is valid.', 'formidable' );
		echo '<br>';

		self::echo_email( $email );
		self::echo_merchant_id( $merchant_id );

		echo '<br>';
		echo '<br>';
		echo '<b>' . esc_html__( 'Enabled scopes:', 'formidable' ) . '</b>';
		echo '<ul style="list-style: unset; padding-left: 15px; margin-top: 0; margin-bottom: 0;">';
		echo '<li>';
		/**
		 * @var string[] $scopes
		 */
		$scopes = $status->oauth_integrations[0]->oauth_third_party[0]->scopes;
		echo implode( '</li><li>', array_map( 'esc_html', $scopes ) );
		echo '</li>';
		echo '</ul>';

		echo '<br>';
		echo '<b>' . esc_html__( 'Enabled capabilities:', 'formidable' ) . '</b>';
		echo '<ul style="list-style: unset; padding-left: 15px; margin-top: 0; margin-bottom: 0;">';

		echo '<li>' . esc_html__( 'PayPal Checkout', 'formidable' ) . '</li>';

		$can_process_card_fields = ! $only_supports_checkout_button && in_array( 'CUSTOM_CARD_PROCESSING', $product->capabilities, true );

		if ( $can_process_card_fields ) {
			echo '<li>' . esc_html__( 'Card Processing', 'formidable' ) . '</li>';
		}
		echo '</ul>';

		if ( $can_process_card_fields ) {
			self::render_acdc_vetting_status( $product );
		}

		echo '</div>';

		return true;
	}

	/**
	 * @since 6.31
	 *
	 * @param bool|object $product
	 *
	 * @return void
	 */
	private static function render_acdc_vetting_status( $product ) {
		$vetting_status = $product && ! empty( $product->vetting_status ) ? $product->vetting_status : 'NOT_SET';

		echo '<br>';
		echo '<b>' . esc_html__( 'ACDC Application Vetting Status:', 'formidable' ) . '</b>';
		echo '&nbsp;';
		echo esc_html( self::get_acdc_vetting_status_message( $vetting_status ) );

		if ( ! in_array( $vetting_status, array( 'DECLINED', 'DENIED', 'NEED_MORE_DATA' ), true ) ) {
			return;
		}

		echo '&nbsp;';
		echo '<a href="https://www.paypal.com/bizsignup/entry/product/ppcp" target="_blank" rel="noopener noreferrer">';
		esc_html_e( 'Reapply for Advanced Card Processing', 'formidable' );
		echo '</a>';
	}

	/**
	 * @since 6.31
	 *
	 * @param string $vetting_status
	 *
	 * @return string
	 */
	private static function get_acdc_vetting_status_message( $vetting_status ) {
		switch ( $vetting_status ) {
			case 'NOT_SET':
				return 'Unavailable';
			case 'APPROVED':
			case 'SUBSCRIBED':
				return 'Approved';
			case 'PENDING':
				return 'Pending';
			case 'IN_REVIEW':
				return 'In Review';
			case 'DECLINED':
				return 'Declined';
			case 'NEED_MORE_DATA':
				return 'Needs More Data';
			case 'DENIED':
				return 'Denied';
			default:
				return '';
		}
	}

	/**
	 * @since 6.31
	 *
	 * @param string $mode
	 *
	 * @return void
	 */
	public static function render_seller_status_placeholder( $mode ) {
		include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/seller-status-placeholder.php';
	}

	/**
	 * Get the PayPal account settings URL for the given mode.
	 *
	 * @since 6.31
	 *
	 * @param string $mode 'test' or 'live'.
	 *
	 * @return string
	 */
	private static function get_paypal_account_settings_url( $mode ) {
		if ( 'test' === $mode ) {
			return 'https://www.sandbox.paypal.com/businessprofile/settings';
		}
		return 'https://www.paypal.com/businessprofile/settings';
	}

	/**
	 * @param array  $products
	 * @param string $name
	 *
	 * @return bool|object
	 */
	private static function check_for_product( $products, $name = 'PPCP_CUSTOM' ) {
		foreach ( $products as $current_product ) {
			if ( $name === $current_product->name ) {
				return $current_product;
			}
		}
		return false;
	}

	/**
	 * @param string $email
	 *
	 * @return void
	 */
	private static function echo_email( $email ) {
		if ( ! $email ) {
			return;
		}

		echo '<br>';
		echo '<b>' . esc_html__( 'Connected account:', 'formidable' ) . '</b>';
		echo '<br>';
		echo esc_html( $email );
	}

	/**
	 * @param string $merchant_id
	 *
	 * @return void
	 */
	private static function echo_merchant_id( $merchant_id ) {
		echo '<br>';
		echo '<b>' . esc_html__( 'Merchant ID:', 'formidable' ) . '</b>';
		echo '&nbsp;';

		if ( $merchant_id ) {
			echo esc_html( $merchant_id );
		} else {
			esc_html_e( 'N/A', 'formidable' );
		}
	}

	/**
	 * @param string $message
	 * @param string $email
	 * @param string $merchant_id
	 * @param string $link            URL to help the user resolve the issue.
	 * @param string $reconnect_mode  When set to 'test' or 'live', renders a Reconnect button
	 *                                that triggers the OAuth flow again for that mode.
	 *
	 * @return void
	 */
	private static function render_error( $message, $email = '', $merchant_id = '', $link = '', $reconnect_mode = '' ) {
		echo '<div class="frm_error_style">';
		echo wp_kses_post( $message );
		self::echo_email( $email );
		self::echo_merchant_id( $merchant_id );

		if ( $link ) {
			echo '<br><br>';
			echo '<a href="' . esc_url( $link ) . '" target="_blank" rel="noopener noreferrer">';
			esc_html_e( 'Resolve this issue', 'formidable' );
			echo '</a>';
		}

		if ( in_array( $reconnect_mode, array( 'test', 'live' ), true ) ) {
			echo '<br><br>';
			echo '<a class="frm-connect-paypal-with-oauth button-secondary frm-button-secondary" data-mode="' . esc_attr( $reconnect_mode ) . '" data-reconnect="1" href="#">';
			esc_html_e( 'Reconnect', 'formidable' );
			echo '</a>';
		}

		echo '</div>';
	}

	/**
	 * @param string $mode
	 *
	 * @return void
	 */
	public static function render_settings_for_mode( $mode ) {
		$connected = (bool) self::get_merchant_id( $mode );
		include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/connect-settings-box.php';
	}

	/**
	 * @return void
	 */
	private static function register_settings_scripts() {
		$script_url   = FrmPayPalLiteAppHelper::plugin_url() . '/js/settings.js';
		$dependencies = array( 'formidable_dom' );
		wp_register_script( 'formidable_paypal_settings', $script_url, $dependencies, FrmAppHelper::plugin_version(), true );
		wp_enqueue_script( 'formidable_paypal_settings' );
	}

	/**
	 * @return false|string
	 */
	public static function get_oauth_redirect_url() {
		$mode        = FrmAppHelper::get_post_param( 'mode', 'test', 'sanitize_text_field' );
		$tracking_id = get_option( self::get_tracking_id_option_name( $mode ) );

		if ( self::get_merchant_id( $mode ) && ! $tracking_id ) {
			// Do not allow for initialize if there is already a configured account id,
			// unless a tracking_id is stored, which indicates the user is re-onboarding
			// after an incomplete OAuth integration.
			return false;
		}

		$additional_body = array(
			'password'            => self::generate_client_password( $mode ),
			'user_id'             => get_current_user_id(),
			'frm_paypal_api_mode' => $mode,
		);

		if ( $tracking_id ) {
			// Reuse the existing tracking_id so the Connect server can resume onboarding.
			$additional_body['tracking_id'] = $tracking_id;
		}

		// Clear the transient so it doesn't fail.
		delete_option( 'frm_paypal_lite_last_verify_attempt' );
		$data = self::post_to_connect_server( 'oauth_request', $additional_body );

		if ( is_string( $data ) ) {
			self::$latest_error_from_paypal_api = $data;
			FrmTransLiteLog::log_message( 'PayPal OAuth Error', $data );
			return false;
		}

		if ( ! empty( $data->password ) ) {
			update_option( self::get_server_side_token_option_name( $mode ), $data->password, false );
		}

		if ( ! empty( $data->tracking_id ) ) {
			update_option( self::get_tracking_id_option_name( $mode ), $data->tracking_id, false );
		}

		if ( ! is_object( $data ) || empty( $data->redirect_url ) ) {
			return false;
		}

		return $data->redirect_url;
	}

	/**
	 * @param string $action
	 * @param array  $additional_body
	 *
	 * @return object|string
	 */
	private static function post_to_connect_server( $action, $additional_body = array() ) {
		$body    = array(
			'frm_paypal_api_action' => $action,
			'frm_paypal_api_mode'   => FrmPayPalLiteAppHelper::active_mode(),
		);
		$body    = array_merge( $body, $additional_body );
		$url     = self::get_url_to_connect_server();
		$headers = self::build_headers_for_post();

		// (Seconds) default timeout is 5. we want a bit more time to work with.
		$timeout = 45;

		self::try_to_extend_server_timeout( $timeout );

		$args     = compact( 'body', 'headers', 'timeout' );
		$response = wp_remote_post( $url, $args );

		if ( ! self::validate_response( $response ) ) {
			return 'Response from server is invalid';
		}

		$body = self::pull_response_body( $response );

		if ( empty( $body->success ) ) {
			$error_message = 'Response from server was not successful';
			$debug_id      = '';

			// Handle structured error response with message and debug_id
			$data = $body->data ?? null;

			if ( is_object( $data ) ) {
				if ( ! empty( $data->message ) ) {
					$error_message = $data->message;
				}

				if ( ! empty( $data->debug_id ) ) {
					$debug_id = $data->debug_id;
				}
			} elseif ( is_string( $data ) ) {
				$error_message = $data;
			}

			// Check for debug_id at top level as well
			if ( ! $debug_id && ! empty( $body->debug_id ) ) {
				$debug_id = $body->debug_id;
			}

			// Parse debug_id from error message if not found
			if ( ! $debug_id && preg_match( '/\{\{debug_id:([^}]+)\}\}/', $error_message, $matches ) ) {
				$debug_id = $matches[1];
			}

			if ( $debug_id ) {
				$clean_message = trim( preg_replace( '/\{\{debug_id:[^}]+\}\}/', '', $error_message ) );
				FrmPayPalLiteAppController::log_paypal_debug_id( $debug_id, $clean_message, $action );
				// Return structured error with debug_id so it can be passed to JavaScript
				return array(
					'message'  => $clean_message ? $clean_message : $error_message,
					'debug_id' => $debug_id,
				);
			}

			return $error_message;
		}//end if

		return $body->data ?? array();
	}

	/**
	 * @param array $response
	 *
	 * @return mixed
	 */
	private static function pull_response_body( $response ) {
		$http_response   = $response['http_response'];
		$response_object = $http_response->get_response_object();
		return json_decode( $response_object->body );
	}

	/**
	 * @param mixed $response
	 *
	 * @return bool
	 */
	private static function validate_response( $response ) {
		return ! is_wp_error( $response ) && is_array( $response ) && isset( $response['http_response'] );
	}

	/**
	 * @return string
	 */
	private static function get_url_to_connect_server() {
		return 'https://api.strategy11.com/';
	}

	/**
	 * @return array
	 */
	private static function build_headers_for_post() {
		$password = self::maybe_get_pro_license();

		if ( false === $password ) {
			$password = 'lite_' . self::get_uuid();
		}

		$site_url = home_url();
		$site_url = self::maybe_fix_wpml_url( $site_url );
		// Remove protocol from url (our url cannot include the colon).
		$site_url = preg_replace( '#^https?://#', '', $site_url );
		// Remove port from url (mostly helpful in development).
		$site_url = preg_replace( '/:[0-9]+/', '', $site_url );
		$site_url = self::strip_lang_from_url( $site_url );

		// $password is either a Pro license or a uuid (See FrmUsage::uuid).
		return array(
			'Authorization' => 'Basic ' . base64_encode( $site_url . ':' . $password ),
		);
	}

	/**
	 * Get a unique ID to use for connecting Lite users.
	 *
	 * @return string
	 */
	private static function get_uuid() {
		$usage = new FrmUsage();
		return $usage->uuid();
	}

	/**
	 * WPML might add a language to the url. Don't send that to the server.
	 *
	 * @param string $url URL to strip language from.
	 *
	 * @return string
	 */
	private static function strip_lang_from_url( $url ) {
		$split_on_language = explode( '/?lang=', $url );
		return 2 === count( $split_on_language ) ? $split_on_language[0] : $url;
	}

	/**
	 * WPML alters the output of home_url.
	 * If it is active, use the WPML "absolute home" URL which is not modified.
	 *
	 * @param string $url URL to maybe fix.
	 *
	 * @return string
	 */
	private static function maybe_fix_wpml_url( $url ) {
		if ( defined( 'ICL_SITEPRESS_VERSION' ) && ! ICL_PLUGIN_INACTIVE && class_exists( 'SitePress' ) ) {
			global $wpml_url_converter;
			$url = $wpml_url_converter->get_abs_home();
		}
		return $url;
	}

	/**
	 * Get a Pro license when Pro is active.
	 * Otherwise we'll use a uuid to support Lite.
	 *
	 * @return false|string
	 */
	private static function maybe_get_pro_license() {
		if ( FrmAppHelper::pro_is_installed() ) {
			$pro_license = FrmAddonsController::get_pro_license();

			if ( $pro_license ) {
				$password = $pro_license;
			}
		}

		return ! empty( $password ) ? $password : false;
	}

	/**
	 * Try to make sure the server time limit exceeds the request time limit.
	 *
	 * @param int $timeout seconds.
	 *
	 * @return void
	 */
	private static function try_to_extend_server_timeout( $timeout ) {
		if ( function_exists( 'set_time_limit' ) ) {
			set_time_limit( $timeout + 10 );
		}
	}

	/**
	 * @param string $mode either 'auto', 'live', or 'test'.
	 *
	 * @return string
	 */
	private static function get_server_side_token_option_name( $mode = 'auto' ) {
		return self::get_paypal_connect_option_name( 'server_password', $mode );
	}

	/**
	 * @param string $mode either 'auto', 'live', or 'test'.
	 *
	 * @return string
	 */
	private static function get_tracking_id_option_name( $mode = 'auto' ) {
		return self::get_paypal_connect_option_name( 'tracking_id', $mode );
	}

	/**
	 * Generate a new client password for authenticating with Connect Service and save it locally as an option.
	 *
	 * @param string $mode 'live' or 'test'.
	 *
	 * @return string the client password.
	 */
	private static function generate_client_password( $mode ) {
		$client_password = wp_generate_password();
		update_option( self::get_client_side_token_option_name( $mode ), $client_password, false );
		return $client_password;
	}

	/**
	 * @param string $mode either 'auto', 'live', or 'test'.
	 *
	 * @return string
	 */
	private static function get_client_side_token_option_name( $mode = 'auto' ) {
		return self::get_paypal_connect_option_name( 'client_password', $mode );
	}

	/**
	 * @param string $mode
	 *
	 * @return string
	 */
	private static function get_paypal_seller_status_option_name( $mode = 'auto' ) {
		return self::get_paypal_connect_option_name( 'seller_status', $mode );
	}

	/**
	 * @return string
	 */
	private static function get_mode_value() {
		$settings = FrmPayPalLiteAppHelper::get_settings();
		return $settings->settings->test_mode ? 'test' : 'live';
	}

	/**
	 * @param string $mode either 'auto', 'live', or 'test'.
	 *
	 * @return bool|string
	 */
	public static function get_merchant_id( $mode = 'auto' ) {
		if ( 'auto' === $mode ) {
			$mode = self::get_mode_value();
		}
		return get_option( self::get_merchant_id_option_name( $mode ) );
	}

	/**
	 * @param string $mode either 'auto', 'live', or 'test'.
	 *
	 * @return string
	 */
	private static function get_merchant_id_option_name( $mode = 'auto' ) {
		return self::get_paypal_connect_option_name( 'merchant_id', $mode );
	}

	/**
	 * @param string $mode either 'auto', 'live', or 'test'.
	 *
	 * @return string
	 */
	private static function get_merchant_currency_option_name( $mode = 'auto' ) {
		return self::get_paypal_connect_option_name( 'merchant_currency', $mode );
	}

	/**
	 * @param string $key  'merchant_id', 'client_password', 'server_password'.
	 * @param string $mode either 'auto', 'live', or 'test'.
	 *
	 * @return string
	 */
	private static function get_paypal_connect_option_name( $key, $mode = 'auto' ) {
		return 'frm_paypal_connect_' . $key . self::get_active_mode_option_name_suffix( $mode );
	}

	/**
	 * @param string $mode either 'auto', 'live', or 'test'.
	 *
	 * @return string either _test or _live.
	 */
	private static function get_active_mode_option_name_suffix( $mode = 'auto' ) {
		if ( 'auto' !== $mode ) {
			return '_' . $mode;
		}
		return '_' . FrmPayPalLiteAppHelper::active_mode();
	}

	public static function check_for_redirects() {
		if ( self::user_landed_on_the_oauth_return_url() ) {
			self::redirect_oauth();
		}
	}

	/**
	 * @return bool
	 */
	private static function user_landed_on_the_oauth_return_url() {
		return isset( $_GET['frm_paypal_api_return_oauth'] );
	}

	private static function redirect_oauth() {
		$connected = self::check_server_for_oauth_merchant_id();
		wp_safe_redirect( self::get_url_for_paypal_settings( $connected ) );
		exit;
	}

	/**
	 * @param bool $connected
	 *
	 * @return string
	 */
	private static function get_url_for_paypal_settings( $connected ) {
		return admin_url( 'admin.php?page=formidable-settings&t=paypal_settings&connected=' . intval( $connected ) );
	}

	/**
	 * @return bool
	 */
	private static function check_server_for_oauth_merchant_id() {
		$mode         = 'test' === FrmAppHelper::simple_get( 'mode' ) ? 'test' : 'live';
		$tracking_id  = get_option( self::get_tracking_id_option_name( $mode ) );
		$is_reconnect = (bool) $tracking_id;

		if ( self::get_merchant_id( $mode ) && ! $is_reconnect ) {
			// Do not allow for initialize if there is already a configured merchant id,
			// unless a tracking_id is stored, which indicates the user is re-onboarding
			// after an incomplete OAuth integration and the new credentials must be synced.
			return false;
		}

		$body = array(
			'server_password'     => get_option( self::get_server_side_token_option_name( $mode ) ),
			'client_password'     => get_option( self::get_client_side_token_option_name( $mode ) ),
			'frm_paypal_api_mode' => $mode,
		);

		if ( $tracking_id ) {
			$body['tracking_id'] = $tracking_id;
		}

		$data = self::post_to_connect_server( 'oauth_merchant_status', $body );

		if ( is_object( $data ) && ! empty( $data->merchant_id ) ) {
			update_option( self::get_merchant_id_option_name( $mode ), $data->merchant_id, false );

			// Invalidate the cached seller status so the next render fetches fresh data
			// with the newly synced credentials.
			delete_option( self::get_paypal_seller_status_option_name( $mode ) );

			FrmTransLiteAppController::install();

			return true;
		}

		return false;
	}

	/**
	 * @param string $action
	 * @param array  $additional_body
	 *
	 * @return false|object
	 */
	private static function post_with_authenticated_body( $action, $additional_body = array() ) {
		$body     = array_merge( self::get_standard_authenticated_body(), $additional_body );
		$response = self::post_to_connect_server( $action, $body );

		if ( is_object( $response ) ) {
			return $response;
		}

		if ( is_array( $response ) ) {
			// Arrays with error data (e.g., from mock responses) should be preserved
			// Only convert empty arrays to empty objects
			if ( $response ) {
				// Check if this is an error response with message and debug_id
				if ( isset( $response['message'] ) && ( isset( $response['debug_id'] ) || isset( $response['debugId'] ) ) ) {
					self::$latest_error_from_paypal_api = $response['message'];
					// PayPal API returns debug_id (snake_case) in some cases and debugId (camelCase) in others
					self::$latest_debug_id_from_paypal_api = $response['debug_id'] ?? $response['debugId'] ?? '';

					if ( class_exists( 'FrmTransLiteLog' ) ) {
						FrmTransLiteLog::log_message( 'PayPal API Error', $response['message'] );
					}
					// Return the array with error details so the caller can extract them
					return (object) $response;
				}
				// Convert array to object for consistency
				return (object) $response;
			}

			return new stdClass();
		}//end if

		if ( ! is_string( $response ) ) {
			self::$latest_error_from_paypal_api = '';
			return false;
		}

		self::$latest_error_from_paypal_api = $response;

		// Extract debug_id from formatted error string if present
		if ( preg_match( '/{{debug_id:([^}]+)}}/', $response, $matches ) ) {
			self::$latest_debug_id_from_paypal_api = $matches[1];
			// Remove the debug_id token from the error message for display
			self::$latest_error_from_paypal_api = preg_replace( '/\s*{{debug_id:[^}]+}}/', '', $response );
		}

		FrmTransLiteLog::log_message( 'PayPal API Error', $response );

		return false;
	}

	/**
	 * @return array
	 */
	private static function get_standard_authenticated_body() {
		return self::get_body_for_mode( FrmPayPalLiteAppHelper::active_mode() );
	}

	/**
	 * Check $_POST for live or test mode value as it can be updated in real time from PayPal Settings and can be configured before the update is saved.
	 *
	 * @return string 'test' or 'live'
	 */
	private static function get_mode_value_from_post() {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( empty( $_POST ) || ! array_key_exists( 'testMode', $_POST ) ) {
			return FrmPayPalLiteAppHelper::active_mode();
		}

		$test_mode = FrmAppHelper::get_param( 'testMode', '', 'post', 'absint' );
		return $test_mode ? 'test' : 'live';
	}

	/**
	 * Get the standard body with account id, mode, and passwords to send to the connect server.
	 *
	 * @since 6.32.1
	 *
	 * @param string $mode 'live' or 'test'.
	 *
	 * @return array
	 */
	private static function get_body_for_mode( $mode ) {
		return array(
			'merchant_id'         => get_option( self::get_merchant_id_option_name( $mode ) ),
			'server_password'     => get_option( self::get_server_side_token_option_name( $mode ) ),
			'client_password'     => get_option( self::get_client_side_token_option_name( $mode ) ),
			'frm_paypal_api_mode' => $mode,
		);
	}

	/**
	 * @return string|null
	 */
	public static function get_latest_error_from_paypal_api() {
		return self::$latest_error_from_paypal_api;
	}

	/**
	 * @return string
	 */
	public static function get_latest_debug_id_from_paypal_api() {
		return self::$latest_debug_id_from_paypal_api;
	}

	/**
	 * @return array
	 */
	public static function get_unprocessed_event_ids() {
		$data = self::post_with_authenticated_body( 'get_unprocessed_event_ids' );

		if ( false === $data || empty( $data->event_ids ) ) {
			return array();
		}

		/**
		 * @var array $data->event_ids
		 */
		return $data->event_ids;
	}

	/**
	 * @param string $event_id
	 *
	 * @return false|object
	 */
	public static function get_event( $event_id ) {
		$event = wp_cache_get( $event_id, 'frm_paypal' );

		if ( is_object( $event ) ) {
			return $event;
		}

		$event = self::post_with_authenticated_body( 'get_event', compact( 'event_id' ) );

		if ( false === $event || empty( $event->event ) ) {
			return false;
		}

		wp_cache_set( $event_id, $event->event, 'frm_paypal' );

		return $event->event;
	}

	/**
	 * @param string $event_id
	 *
	 * @return false|object
	 */
	public static function process_event( $event_id ) {
		return self::post_with_authenticated_body( 'process_event', compact( 'event_id' ) );
	}

	public static function handle_disconnect() {
		self::disconnect();
		self::reset_paypal_api_integration();
		FrmTransLiteAppHelper::trigger_gateway_disconnected_hook( 'paypal', self::get_mode_value_from_post() );
		wp_send_json_success();
	}

	/**
	 * @return false|object
	 */
	private static function disconnect() {
		$additional_body = self::get_body_for_mode( self::get_mode_value_from_post() );
		return self::post_with_authenticated_body( 'disconnect', $additional_body );
	}

	/**
	 * Delete every PayPal API option, calling when disconnecting.
	 *
	 * @return void
	 */
	public static function reset_paypal_api_integration() {
		$mode = self::get_mode_value_from_post();
		delete_option( self::get_merchant_id_option_name( $mode ) );
		delete_option( self::get_server_side_token_option_name( $mode ) );
		delete_option( self::get_client_side_token_option_name( $mode ) );
		delete_option( self::get_merchant_currency_option_name( $mode ) );
		delete_option( self::get_paypal_seller_status_option_name( $mode ) );
		delete_option( self::get_tracking_id_option_name( $mode ) );
	}

	/**
	 * @since 6.31
	 *
	 * @return bool
	 */
	public static function at_least_one_mode_is_setup() {
		return self::get_merchant_id( 'test' ) || self::get_merchant_id( 'live' );
	}

	/**
	 * Verify a site identifier is a match.
	 */
	public static function verify() {
		$option_name  = 'frm_paypal_lite_last_verify_attempt';
		$last_request = get_option( $option_name );

		if ( $last_request && $last_request > strtotime( '-1 day' ) ) {
			wp_send_json_error( 'Too many requests' );
		}

		$site_identifier = FrmAppHelper::get_post_param( 'site_identifier' );
		$usage           = new FrmUsage();

		update_option( $option_name, time() );

		if ( $site_identifier === $usage->uuid() ) {
			wp_send_json_success();
		}

		wp_send_json_error();
	}

	/**
	 * Create a PayPal order.
	 *
	 * @param string $amount
	 * @param string $currency
	 * @param string $payment_source Valid values are 'card', 'paypal'.
	 * @param array  $payer
	 * @param string $shipping_preference
	 * @param array  $pricing_data Optional. Array of products with prices and quantities.
	 * @param array  $shipping     Optional. Shipping name and address data.
	 * @param string $description  Optional. Description for the order.
	 *
	 * @return false|object
	 */
	public static function create_order( $amount, $currency, $payment_source, $payer, $shipping_preference, $pricing_data = array(), $shipping = array(), $description = '' ) {
		$brand_name = self::get_brand_name();

		// phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
		return self::post_with_authenticated_body( 'create_order', compact( 'amount', 'currency', 'payment_source', 'brand_name', 'payer', 'shipping_preference', 'pricing_data', 'shipping', 'description' ) );
	}

	/**
	 * @since 6.31
	 *
	 * @return string
	 */
	private static function get_brand_name() {
		$brand_name = get_bloginfo( 'name' );

		/**
		 * Allow people to modify the brand name used in the PayPal order.
		 *
		 * @since 6.31
		 *
		 * @param string $brand_name
		 *
		 * @return string
		 */
		$filtered_brand_name = apply_filters( 'frm_paypal_brand_name', $brand_name );

		if ( is_string( $filtered_brand_name ) ) {
			return $filtered_brand_name;
		}

		_doing_it_wrong( 'FrmPayPalLiteConnectHelper::get_brand_name', 'The frm_paypal_brand_name filter must return a string.', '6.31' );

		return $brand_name;
	}

	/**
	 * @param string $order_id
	 *
	 * @return false|object
	 */
	public static function capture_order( $order_id ) {
		return self::post_with_authenticated_body( 'capture_order', compact( 'order_id' ) );
	}

	/**
	 * @param string $capture_id
	 *
	 * @return false|object
	 */
	public static function refund_payment( $capture_id ) {
		return self::post_with_authenticated_body( 'refund_capture', array( 'capture_id' => $capture_id ) );
	}

	/**
	 * @param string $subscription_id
	 *
	 * @return false|object
	 */
	public static function cancel_subscription( $subscription_id ) {
		return self::post_with_authenticated_body( 'cancel_subscription', compact( 'subscription_id' ) );
	}

	/**
	 * @param array $data Subscription data.
	 *
	 * @return false|object
	 */
	public static function create_subscription( $data ) {
		$data['brand_name'] = self::get_brand_name();
		return self::post_with_authenticated_body( 'create_subscription', compact( 'data' ) );
	}

	/**
	 * @return false|object
	 */
	public static function get_seller_status() {
		$mode   = self::get_mode_value_from_post();
		$status = get_option( self::get_paypal_seller_status_option_name( $mode ) );

		if ( is_object( $status ) ) {
			return $status;
		}

		$additional_body = self::get_body_for_mode( $mode );

		return self::post_with_authenticated_body( 'get_seller_status', $additional_body );
	}

	/**
	 * @since 6.31
	 *
	 * @param string $capture_id
	 *
	 * @return false|object
	 */
	public static function get_capture( $capture_id ) {
		return self::post_with_authenticated_body( 'get_capture', compact( 'capture_id' ) );
	}

	/**
	 * @since 6.31
	 *
	 * @param string $order_id
	 *
	 * @return false|object
	 */
	public static function get_order( $order_id ) {
		return self::post_with_authenticated_body( 'get_order', compact( 'order_id' ) );
	}

	/**
	 * @since 6.31
	 *
	 * @param string $subscription_id The PayPal subscription ID.
	 *
	 * @return false|object
	 */
	public static function get_subscription( $subscription_id ) {
		return self::post_with_authenticated_body( 'get_subscription', compact( 'subscription_id' ) );
	}

	/**
	 * @since 6.31
	 *
	 * @return string
	 */
	public static function get_bn_code() {
		return 'Strategy11LLCPPCP_SP';
	}

	/**
	 * @since 6.31
	 * @deprecated 6.32.1
	 *
	 * @param array $data Setup token data including payment_source.
	 *
	 * @return false|object
	 */
	public static function create_vault_setup_token( $data = array() ) {
		_deprecated_function( __METHOD__, '6.32.1' );
		return false;
	}
}

```
