form_id : $field['form_id'];
$actions = self::get_actions_before_submit( $form_id );
if ( ! $actions ) {
return $callback;
}
$field_id = is_object( $field ) ? $field->id : $field['id'];
foreach ( $actions as $action ) {
if ( (int) $action->post_content['credit_card'] === (int) $field_id ) {
return self::class . '::show_card';
}
}
return $callback;
}
/**
* Override the credit card field HTML if there is a PayPal action.
*
* @since 6.31
*
* @param array $field
* @param string $field_name
* @param array $atts
*
* @return void
*/
public static function show_card( $field, $field_name, $atts ) {
$actions = self::get_actions_before_submit( $field['form_id'] );
if ( $actions ) {
self::load_scripts( (int) $field['form_id'] );
$html_id = $atts['html_id'];
include FrmStrpLiteAppHelper::plugin_path() . '/views/payments/card-field.php';
return;
}
// Use the Pro function when there are no Stripe actions.
// This is required for other gateways like Authorize.Net.
if ( is_callable( 'FrmProCreditCardsController::show_in_form' ) ) {
FrmProCreditCardsController::show_in_form( $field, $field_name, $atts );
}
}
/**
* Get all published payment actions with the PayPal gateway that have an amount set.
*
* @since 6.31
*
* @param int|string $form_id
*
* @return array
*/
public static function get_actions_before_submit( $form_id ) {
$payment_actions = self::get_actions_for_form( $form_id );
foreach ( $payment_actions as $k => $payment_action ) {
$gateway = $payment_action->post_content['gateway'];
$is_paypal = $gateway === 'paypal' || ( is_array( $gateway ) && in_array( 'paypal', $gateway, true ) );
if ( ! $is_paypal || empty( $payment_action->post_content['amount'] ) ) {
unset( $payment_actions[ $k ] );
}
}
return $payment_actions;
}
/**
* Trigger a PayPal payment after a form is submitted.
* This is called for both one time and recurring payments.
*
* @param WP_Post $action
* @param stdClass $entry
* @param mixed $form
*
* @return array
*/
public static function trigger_gateway( $action, $entry, $form ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
$response = array(
'success' => false,
'run_triggers' => false,
'show_errors' => true,
);
$atts = compact( 'action', 'entry', 'form' );
$amount = self::prepare_amount( $action->post_content['amount'], $atts );
// phpcs:ignore Universal.Operators.StrictComparisons
if ( ! $amount || $amount == 000 ) {
$response['error'] = __( 'Please specify an amount for the payment', 'formidable' );
return $response;
}
if ( ! self::paypal_is_configured() ) {
$response['error'] = __( 'PayPal still needs to be configured.', 'formidable' );
return $response;
}
$payment_args = compact( 'form', 'entry', 'action', 'amount' );
// Attempt to charge the customer's card.
if ( 'recurring' === $action->post_content['type'] ) {
$charge = self::trigger_recurring_payment( $payment_args );
} else {
$charge = self::trigger_one_time_payment( $payment_args );
}
if ( $charge === true ) {
$response['success'] = true;
} else {
$response['error'] = $charge;
}
if ( ! self::$active_order_id && ! self::$active_subscription ) {
return $response;
}
$email = false;
$address = false;
$order = self::$active_order_id ? FrmPayPalLiteConnectHelper::get_order( self::$active_order_id ) : false;
if ( is_object( $order ) && isset( $order->payer ) && is_object( $order->payer ) ) {
$payer = $order->payer;
if ( ! empty( $payer->email_address ) ) {
$email = $payer->email_address;
}
}
if ( is_object( $order ) && ! empty( $order->purchase_units[0]->shipping->address ) && is_object( $order->purchase_units[0]->shipping->address ) ) {
$address = $order->purchase_units[0]->shipping->address;
}
// Fallback to subscription subscriber info when order data is not available.
if ( self::$active_subscription && isset( self::$active_subscription->subscriber ) && is_object( self::$active_subscription->subscriber ) ) {
$subscriber = self::$active_subscription->subscriber;
if ( ! $email && ! empty( $subscriber->email_address ) ) {
$email = $subscriber->email_address;
}
if ( ! $address && isset( $subscriber->shipping_address->address ) ) {
$address = $subscriber->shipping_address->address;
}
}
$paypal_message = '';
if ( self::$pending_capture ) {
$paypal_message .= '' . esc_html__( 'Payment status: ', 'formidable' ) . '' . esc_html__( 'Pending', 'formidable' ) . '
';
}
$source_type = self::$active_payment_source;
if ( $source_type ) {
$display_type = self::get_source_display_type( $source_type );
$paypal_message .= '' . esc_html__( 'Payment source: ', 'formidable' ) . '' . $display_type . '
';
}
if ( $email ) {
$paypal_message .= '' . esc_html__( 'Payment made by: ', 'formidable' ) . '' . $email . '
';
}
if ( $address && ! empty( $address->address_line_1 ) ) {
$paypal_message .= self::format_address( $address );
}
/**
* Filters the message to show in the main feedback area.
*
* @since 6.31
*
* @param string $paypal_message The message to show.
* @param stdClass $order The order object.
*/
$paypal_message = apply_filters( 'frm_paypal_message', $paypal_message, $order );
add_filter(
'frm_main_feedback',
function ( $message ) use ( $paypal_message ) {
if ( $paypal_message ) {
$details = '
' . $paypal_message . '
';
$message = preg_replace( '/(]*\bfrm_message\b[^>]*>)(.*?)(<\/div>)/s', '$1$2' . $details . '$3', $message );
}
return $message;
}
);
return $response;
}
/**
* Get the display label for a payment source type.
*
* @since 6.31
*
* @param string $source_type The payment source identifier (e.g. 'paypal', 'paylater', 'google_pay').
*
* @return string The human-readable display label.
*/
private static function get_source_display_type( $source_type ) {
switch ( $source_type ) {
case 'paypal':
return __( 'PayPal', 'formidable' );
case 'paylater':
return __( 'Pay Later', 'formidable' );
default:
return ucwords( str_replace( '_', ' ', $source_type ) );
}
}
/**
* Format a PayPal shipping address object into an HTML string.
*
* @since 6.31
*
* @param object $address The PayPal shipping address object.
*
* @return string The formatted address HTML.
*/
private static function format_address( $address ) {
$formatted = '
' . esc_html__( 'Address: ', 'formidable' ) . '';
$formatted .= $address->address_line_1 . '
';
// City, State Zip
$city_line = '';
if ( ! empty( $address->admin_area_2 ) ) {
$city_line .= $address->admin_area_2;
}
if ( ! empty( $address->admin_area_1 ) ) {
$city_line .= $city_line ? ', ' . $address->admin_area_1 : $address->admin_area_1;
}
if ( ! empty( $address->postal_code ) ) {
$city_line .= $city_line ? ' ' . $address->postal_code : $address->postal_code;
}
if ( $city_line ) {
$formatted .= $city_line . '
';
}
if ( ! empty( $address->country_code ) ) {
$formatted .= $address->country_code . '
';
}
return $formatted;
}
/**
* Trigger a one time payment.
*
* @param array $atts The arguments for the payment.
*
* @return string|true string on error, true on success.
*/
private static function trigger_one_time_payment( $atts ) {
$paypal_order_id = FrmAppHelper::get_post_param( 'paypal_order_id', '', 'sanitize_text_field' );
if ( ! $paypal_order_id ) {
return 'No PayPal order ID found.';
}
$order = FrmPayPalLiteConnectHelper::get_order( $paypal_order_id );
if ( false === $order ) {
return 'Failed to get order.';
}
if ( self::is_liability_error( $order ) ) {
return 'This payment was flagged as possible fraud and has been rejected.';
}
if ( ! self::validate_order_status( $order ) ) {
return 'This order status is not valid for capture.';
}
if ( ! self::validate_order_amount( $order, $atts['amount'] ) ) {
return 'This order amount appears to be tampered with.';
}
$response = FrmPayPalLiteConnectHelper::capture_order( $paypal_order_id );
if ( false === $response ) {
// Get error message and debug_id from static properties
$reason = FrmPayPalLiteConnectHelper::get_latest_error_from_paypal_api();
$debug_id = FrmPayPalLiteConnectHelper::get_latest_debug_id_from_paypal_api();
if ( $reason ) {
$message = $reason;
if ( $debug_id ) {
$message .= ' {{debug_id:' . $debug_id . '}}';
}
return $message;
}
return 'Failed to confirm order.';
}
// If PayPal returned an error (e.g. ORDER_IS_PENDING_APPROVAL),
// create a pending payment instead of failing outright.
if ( ! empty( $response->capture_error ) ) {
return self::handle_pending_capture( $response, $atts, $paypal_order_id );
}
if ( ! isset( $response->status ) || $response->status !== 'COMPLETED' ) {
return self::get_paypal_error_message( $response, __( 'Failed to capture order.', 'formidable' ) );
}
$capture_id = self::get_capture_id_from_response( $response );
self::sync_entry_data_with_capture_response( $response, $atts );
// Create a payment record.
$atts['status'] = 'complete';
$atts['charge'] = new stdClass();
$atts['charge']->id = $capture_id ? $capture_id : $paypal_order_id;
$atts['charge']->amount = $atts['amount'];
$payment_id = self::create_new_payment( $atts );
$frm_payment = new FrmTransLitePayment();
$payment = $frm_payment->get_one( $payment_id );
$status = $atts['status'];
FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
self::$active_order_id = $paypal_order_id;
self::$active_payment_source = FrmAppHelper::get_post_param( 'paypal_payment_source', '', 'sanitize_text_field' );
return true;
}
/**
* Extract a human-readable message from a PayPal error response.
*
* PayPal error payloads include a `details` array with per-issue
* `description` strings (e.g. "The instrument presented was either declined
* by the processor or bank..."). Prefer those over the generic top-level
* `message` so the buyer sees the actionable reason.
*
* @since 6.31
*
* @param mixed $response The PayPal response object.
* @param string $fallback The fallback message when no details are available.
*
* @return string
*/
private static function get_paypal_error_message( $response, $fallback ) {
$message = $fallback;
if ( is_object( $response ) && isset( $response->details ) && is_array( $response->details ) ) {
foreach ( $response->details as $detail ) {
if ( is_object( $detail ) && ! empty( $detail->description ) ) {
$message = (string) $detail->description;
break;
}
}
} elseif ( is_object( $response ) && ! empty( $response->message ) ) {
$message = (string) $response->message;
}
return self::maybe_append_debug_id( $message, $response );
}
/**
* Conditionally append a PayPal debug ID to an error message.
*
* The debug ID is shown only to users who can edit forms, as a
* troubleshooting aid.
*
* @since 6.31
*
* @param string $message The error message.
* @param mixed $response The PayPal response that may contain a debug_id property.
*
* @return string
*/
private static function maybe_append_debug_id( $message, $response ) {
if ( ! current_user_can( 'frm_edit_forms' ) ) {
return $message;
}
$debug_id = is_object( $response ) && ! empty( $response->debug_id ) ? $response->debug_id : '';
if ( ! $debug_id ) {
return $message;
}
return $message . '
Debug ID: ' . esc_html( $debug_id );
}
/**
* @since 6.31
*
* @param stdClass $order The order object.
*
* @return bool
*/
private static function is_liability_error( $order ) {
if ( ! isset( $order->payment_source->card->authentication_result->liability_shift ) ) {
return false;
}
$liability_shift = $order->payment_source->card->authentication_result->liability_shift;
$is_liability_error = 'NO' === $liability_shift || 'UNKNOWN' === $liability_shift;
/**
* Filters whether the liability shift is an error.
*
* @since 6.31
*
* @param bool $is_liability_error Whether the liability shift is an error.
* @param string $liability_shift The liability shift value. By default 'NO' and 'UNKNOWN' are errors.
* @param stdClass $order The order object.
*/
return (bool) apply_filters(
'frm_paypal_is_liability_error',
$is_liability_error,
$liability_shift,
$order
);
}
/**
* Handle a capture that returned a PayPal error instead of completing.
*
* For recoverable states like ORDER_IS_PENDING_APPROVAL, a pending
* payment record is created so the webhook can complete it later.
* For unrecoverable errors, the PayPal error description is returned.
*
* @since 6.31
*
* @param stdClass $response The PayPal error response object.
* @param array $atts The payment attributes.
* @param string $paypal_order_id The PayPal order ID.
*
* @return string|true String error message or true on success.
*/
private static function handle_pending_capture( $response, $atts, $paypal_order_id ) {
self::$pending_capture = true;
$issue = self::get_capture_error_issue( $response );
// Only block on known non-recoverable errors. For pending states
// or unknown issues (empty details from PayPal), create a pending
// payment and let the webhook resolve it.
// Include mock error codes for testing.
$non_recoverable = array(
'INSTRUMENT_DECLINED',
'PAYER_CANNOT_PAY',
'MAX_NUMBER_OF_PAYMENT_ATTEMPTS_EXCEEDED',
'AUTHENTICATION_FAILURE',
'INTERNAL_SERVER_ERROR',
'INVALID_REQUEST',
'REFUND_FAILED_INSUFFICIENT_FUNDS',
'REFUND_FAILED_CREDIT_CARD_REFUND',
'REFUND_FAILED_REFUND_NOT_ALLOWED',
'REFUND_FAILED_TRANSACTION_ALREADY_REFUNDED',
'REFUND_FAILED_INVALID_ARGUMENT',
);
if ( in_array( $issue, $non_recoverable, true ) ) {
// Convert issue code to human-readable message
$error_message = self::convert_issue_to_message( $issue );
return self::get_paypal_error_message( $response, $error_message );
}
$atts['status'] = 'pending';
$atts['charge'] = new stdClass();
$atts['charge']->id = $paypal_order_id;
$atts['charge']->amount = $atts['amount'];
$payment_id = self::create_new_payment( $atts );
$frm_payment = new FrmTransLitePayment();
$payment = $frm_payment->get_one( $payment_id );
$status = $atts['status'];
FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
self::$active_order_id = $paypal_order_id;
self::$active_payment_source = FrmAppHelper::get_post_param( 'paypal_payment_source', '', 'sanitize_text_field' );
return true;
}
/**
* Extract the issue code from a PayPal capture error response.
*
* @since 6.31
*
* @param stdClass $response The PayPal error response.
*
* @return string The issue code, or empty string if not found.
*/
private static function get_capture_error_issue( $response ) {
// Check for details array first (standard PayPal error format)
if ( isset( $response->details ) && is_array( $response->details ) ) {
$first_detail = reset( $response->details );
if ( is_object( $first_detail ) && ! empty( $first_detail->issue ) ) {
return (string) $first_detail->issue;
}
}
// Check for name field (used by mock responses and some error formats)
if ( isset( $response->name ) && is_string( $response->name ) ) {
return $response->name;
}
return '';
}
/**
* Convert a PayPal issue code to a human-readable error message.
*
* @since 6.31
*
* @param string $issue The issue code (e.g. AUTHENTICATION_FAILURE).
*
* @return string The human-readable error message.
*/
private static function convert_issue_to_message( $issue ) {
// Map of common PayPal issue codes to human-readable messages
$issue_map = array(
'AUTHENTICATION_FAILURE' => 'PayPal payment failed: Authentication failure',
'INSTRUMENT_DECLINED' => 'PayPal payment failed: Payment instrument declined',
'PAYER_CANNOT_PAY' => 'PayPal payment failed: Payer cannot pay',
'MAX_NUMBER_OF_PAYMENT_ATTEMPTS_EXCEEDED' => 'PayPal payment failed: Maximum payment attempts exceeded',
);
// Check if the issue is in our map
$upper_issue = strtoupper( $issue );
// Fallback: convert underscores to spaces and title case
return $issue_map[ $upper_issue ] ?? 'PayPal payment failed: ' . ucwords( strtolower( str_replace( '_', ' ', $issue ) ) );
}
/**
* @since 6.31
*
* @param stdClass $order
*
* @return bool
*/
private static function validate_order_status( $order ) {
return isset( $order->status ) && 'APPROVED' === $order->status;
}
/**
* @since 6.31
*
* @param stdClass $order
* @param string $expected_amount This is as a whole number (in cents for currencies that include decimals).
*
* @return bool
*/
private static function validate_order_amount( $order, $expected_amount ) {
$order_amount = $order->purchase_units[0]->amount->value ?? '';
// The order amount is in dollars, but the expected amount is in cents, so we need to convert.
$order_amount = str_replace( '.', '', $order_amount );
return $order_amount === $expected_amount;
}
/**
* Validate that the subscription status indicates it was approved by the payer.
*
* @since 6.31
*
* @param stdClass $subscription The PayPal subscription object.
*
* @return bool
*/
private static function validate_subscription_status( $subscription ) {
if ( ! isset( $subscription->status ) ) {
return false;
}
return in_array( $subscription->status, array( 'ACTIVE', 'APPROVED' ), true );
}
/**
* Validate that the subscription amount matches the expected amount.
*
* @since 6.31
*
* @param stdClass $subscription The PayPal subscription object.
* @param string $expected_amount The expected amount as a whole number (in cents for currencies that include decimals).
*
* @return bool
*/
private static function validate_subscription_amount( $subscription, $expected_amount ) {
$subscription_amount = $subscription->billing_info->last_payment->amount->value ?? $subscription->plan->billing_cycles[0]->pricing_scheme->fixed_price->value ?? '';
if ( ! $subscription_amount ) {
return false;
}
$subscription_amount = number_format( (float) $subscription_amount, 2, '.', '' );
$expected_amount = number_format( ( (float) $expected_amount ) / 100, 2, '.', '' );
return $subscription_amount === $expected_amount;
}
/**
* @param object $response
*
* @return string
*/
private static function get_capture_id_from_response( $response ) {
if ( ! isset( $response->id ) ) {
return '';
}
foreach ( $response->purchase_units as $purchase_unit ) {
if ( empty( $purchase_unit->payments ) || ! is_object( $purchase_unit->payments ) ) {
continue;
}
$payments = $purchase_unit->payments;
if ( empty( $payments->captures ) || ! is_array( $payments->captures ) ) {
continue;
}
$captures = $payments->captures;
foreach ( $captures as $capture ) {
return $capture->id;
}
}
return '';
}
/**
* Sync the entry data with the capture response.
*
* @since 6.31
*
* @param object $response The response object.
* @param array $atts The arguments for the payment.
*
* @return void
*/
private static function sync_entry_data_with_capture_response( $response, $atts ) {
if ( ! isset( $response->payer ) || ! is_object( $response->payer ) ) {
return;
}
$entry = $atts['entry'];
$action = $atts['action'];
$settings = $action->post_content;
$mode = $settings['entry_data_sync'] ?? 'overwrite';
$payer = $response->payer;
if ( 'new_fields' === $mode ) {
$updates = self::get_order_data_field_updates( $payer, $response, $settings );
} else {
$updates = self::get_payer_field_updates( $payer, $response, $action, $entry );
}
foreach ( $updates as $field_id => $new_value ) {
if ( ! FrmEntryMeta::update_entry_meta( $entry->id, $field_id, '', $new_value ) ) {
FrmEntryMeta::add_entry_meta( $entry->id, $field_id, '', $new_value );
}
}
if ( ! class_exists( 'FrmLog' ) ) {
return;
}
$log = new FrmLog();
$log->add(
array(
'title' => 'PayPal Lite: Sync Entry Data with Capture Response',
'content' => print_r( $updates, true ),
)
);
}
/**
* Sync the entry data with the subscription response.
*
* Normalizes the subscriber data from the subscription response into the
* same shape used by the capture response so the existing field-update
* helpers can be reused.
*
* @since 6.31
*
* @param object $subscription The subscription object from PayPal.
* @param array $atts Includes 'entry', 'action', 'amount'.
*
* @return void
*/
private static function sync_entry_data_with_subscription_response( $subscription, $atts ) {
if ( ! isset( $subscription->subscriber ) || ! is_object( $subscription->subscriber ) ) {
return;
}
$subscriber = $subscription->subscriber;
// Normalize subscriber into the payer shape expected by existing helpers.
$payer = (object) array(
'email_address' => $subscriber->email_address ?? '',
'name' => $subscriber->name ?? null,
);
// Normalize shipping address into the purchase_units shape expected by get_shipping_address_from_response.
$response = (object) array(
'payer' => $payer,
'purchase_units' => array(),
);
if ( isset( $subscriber->shipping_address->address ) ) {
$response->purchase_units = array(
(object) array(
'shipping' => (object) array(
'address' => $subscriber->shipping_address->address,
),
),
);
}
$entry = $atts['entry'];
$action = $atts['action'];
$settings = $action->post_content;
$mode = $settings['entry_data_sync'] ?? 'overwrite';
if ( 'new_fields' === $mode ) {
$updates = self::get_order_data_field_updates( $payer, $response, $settings );
foreach ( $updates as $field_id => $new_value ) {
FrmEntryMeta::add_entry_meta( $entry->id, $field_id, '', $new_value );
}
} else {
$updates = self::get_payer_field_updates( $payer, $response, $action, $entry );
foreach ( $updates as $field_id => $new_value ) {
if ( ! FrmEntryMeta::update_entry_meta( $entry->id, $field_id, '', $new_value ) ) {
FrmEntryMeta::add_entry_meta( $entry->id, $field_id, '', $new_value );
}
}
}
if ( ! class_exists( 'FrmLog' ) ) {
return;
}
$log = new FrmLog();
$log->add(
array(
'title' => 'PayPal Lite: Sync Entry Data with Subscription Response',
'content' => print_r( $updates, true ),
)
);
}
/**
* Build field updates for the dedicated PayPal order data fields.
*
* @since 6.31
*
* @param stdClass $payer The payer object from the PayPal response.
* @param stdClass $response The full capture response.
* @param array $settings The action settings.
*
* @return array
Field ID => value pairs.
*/
private static function get_order_data_field_updates( $payer, $response, $settings ) {
$updates = array();
// Email (hidden field, single string value).
if ( ! empty( $settings['paypal_order_email'] ) && ! empty( $payer->email_address ) ) {
$updates[ (int) $settings['paypal_order_email'] ] = $payer->email_address;
}
// Name (name field with first/last sub-keys).
if ( ! empty( $settings['paypal_order_name'] ) && isset( $payer->name ) ) {
$new_value = array(
'first' => ! empty( $payer->name->given_name ) ? $payer->name->given_name : '',
'last' => ! empty( $payer->name->surname ) ? $payer->name->surname : '',
);
if ( array_filter( $new_value ) ) {
$updates[ (int) $settings['paypal_order_name'] ] = $new_value;
}
}
// Address (address field with line1/line2/city/state/zip/country sub-keys).
if ( empty( $settings['paypal_order_address'] ) ) {
return $updates;
}
$shipping = self::get_shipping_address_from_response( $response );
if ( $shipping ) {
$new_value = array(
'line1' => ! empty( $shipping->address_line_1 ) ? $shipping->address_line_1 : '',
'line2' => ! empty( $shipping->address_line_2 ) ? $shipping->address_line_2 : '',
'city' => ! empty( $shipping->admin_area_2 ) ? $shipping->admin_area_2 : '',
'state' => ! empty( $shipping->admin_area_1 ) ? $shipping->admin_area_1 : '',
'zip' => ! empty( $shipping->postal_code ) ? $shipping->postal_code : '',
'country' => ! empty( $shipping->country_code ) ? $shipping->country_code : '',
);
if ( array_filter( $new_value ) ) {
$updates[ (int) $settings['paypal_order_address'] ] = $new_value;
}
}
return $updates;
}
/**
* Build an array of field updates by comparing payer response data against current entry metas.
*
* @since 6.31
*
* @param stdClass $payer The payer object from the PayPal response.
* @param stdClass $response The full capture response.
* @param WP_Post $action The payment action.
* @param stdClass $entry The entry object.
*
* @return array Field ID => new value pairs that differ from the current entry data.
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
private static function get_payer_field_updates( $payer, $response, $action, $entry ) {
$updates = array();
$settings = $action->post_content;
// Email: setting is a shortcode like [25], extract the field ID.
if ( ! empty( $settings['email'] ) && preg_match( '/\[(\d+)\]/', $settings['email'], $matches ) ) {
$email_field_id = (int) $matches[1];
if ( ! empty( $payer->email_address ) ) {
$current = $entry->metas[ $email_field_id ] ?? '';
if ( $current !== $payer->email_address ) {
$updates[ $email_field_id ] = $payer->email_address;
}
}
}
// Name fields.
if ( isset( $payer->name ) ) {
$first_name_field_id = ! empty( $settings['billing_first_name'] ) ? (int) $settings['billing_first_name'] : 0;
$last_name_field_id = ! empty( $settings['billing_last_name'] ) ? (int) $settings['billing_last_name'] : 0;
if ( $first_name_field_id && $first_name_field_id === $last_name_field_id ) {
// Both settings point to the same Name field. Store as a serialized array.
$new_value = array(
'first' => ! empty( $payer->name->given_name ) ? $payer->name->given_name : '',
'last' => ! empty( $payer->name->surname ) ? $payer->name->surname : '',
);
$current = $entry->metas[ $first_name_field_id ] ?? array();
if ( $current !== $new_value ) {
$updates[ $first_name_field_id ] = $new_value;
}
} else {
// Separate text fields for first and last name.
if ( $first_name_field_id && ! empty( $payer->name->given_name ) ) {
$current = $entry->metas[ $first_name_field_id ] ?? '';
if ( $current !== $payer->name->given_name ) {
$updates[ $first_name_field_id ] = $payer->name->given_name;
}
}
if ( $last_name_field_id && ! empty( $payer->name->surname ) ) {
$current = $entry->metas[ $last_name_field_id ] ?? '';
if ( $current !== $payer->name->surname ) {
$updates[ $last_name_field_id ] = $payer->name->surname;
}
}
}//end if
}//end if
// Address: pull from the first purchase unit's shipping address.
if ( empty( $settings['billing_address'] ) ) {
return $updates;
}
$field_id = (int) $settings['billing_address'];
$shipping = self::get_shipping_address_from_response( $response );
if ( ! $shipping ) {
return $updates;
}
$new_value = array(
'line1' => ! empty( $shipping->address_line_1 ) ? $shipping->address_line_1 : '',
'line2' => ! empty( $shipping->address_line_2 ) ? $shipping->address_line_2 : '',
'city' => ! empty( $shipping->admin_area_2 ) ? $shipping->admin_area_2 : '',
'state' => ! empty( $shipping->admin_area_1 ) ? $shipping->admin_area_1 : '',
'zip' => ! empty( $shipping->postal_code ) ? $shipping->postal_code : '',
'country' => ! empty( $shipping->country_code ) ? $shipping->country_code : '',
);
if ( ! array_filter( $new_value ) ) {
return $updates;
}
$current = $entry->metas[ $field_id ] ?? array();
if ( $current !== $new_value ) {
$updates[ $field_id ] = $new_value;
}
return $updates;
}
/**
* Get the shipping address object from the first purchase unit in the response.
*
* @since 6.31
*
* @param object $response The capture response.
*
* @return false|object The address object, or false if not available.
*/
private static function get_shipping_address_from_response( $response ) {
if ( empty( $response->purchase_units ) || ! is_array( $response->purchase_units ) ) {
return false;
}
$purchase_unit = reset( $response->purchase_units );
if ( empty( $purchase_unit->shipping ) || ! is_object( $purchase_unit->shipping ) || empty( $purchase_unit->shipping->address ) ) {
return false;
}
return $purchase_unit->shipping->address;
}
/**
* Add a payment row for the payments table.
*
* @param array $atts The arguments for the payment.
*
* @return int
*/
private static function create_new_payment( $atts ) {
$atts['charge'] = (object) $atts['charge'];
$new_values = array(
'amount' => FrmTransLiteAppHelper::get_formatted_amount_for_currency( $atts['charge']->amount, $atts['action'] ),
'status' => $atts['status'],
'paysys' => 'paypal',
'item_id' => $atts['entry']->id,
'action_id' => $atts['action']->ID,
'receipt_id' => $atts['charge']->id,
'sub_id' => $atts['charge']->sub_id ?? '',
'test' => 'test' === FrmPayPalLiteAppHelper::active_mode() ? 1 : 0,
);
$frm_payment = new FrmTransLitePayment();
return $frm_payment->create( $new_values );
}
/**
* Create a new PayPal subscription and a subscription and payment for the payments tables.
*
* @param array $atts Includes 'customer', 'entry', 'action', 'amount'.
*
* @return bool|string True on success, error message on failure
*/
private static function trigger_recurring_payment( $atts ) {
$subscription_id = FrmAppHelper::get_post_param( 'paypal_subscription_id', '', 'sanitize_text_field' );
if ( ! $subscription_id ) {
return __( 'No PayPal subscription ID found.', 'formidable' );
}
$subscription = FrmPayPalLiteConnectHelper::get_subscription( $subscription_id );
if ( false === $subscription ) {
return 'Failed to get subscription.';
}
if ( ! self::validate_subscription_status( $subscription ) ) {
return 'This subscription status is not valid.';
}
if ( ! self::validate_subscription_amount( $subscription, $atts['amount'] ) ) {
return 'This subscription amount appears to be tampered with.';
}
$sub_id = self::create_new_subscription( $subscription_id, $atts, $subscription );
self::$active_payment_source = FrmAppHelper::get_post_param( 'paypal_payment_source', '', 'sanitize_text_field' );
self::$active_order_id = FrmAppHelper::get_post_param( 'paypal_order_id', '', 'sanitize_text_field' );
self::$active_subscription = $subscription;
self::sync_entry_data_with_subscription_response( $subscription, $atts );
self::maybe_create_initial_subscription_payment( $subscription_id, $sub_id, $atts );
return true;
}
/**
* Create a new subscription record in the payments tables.
*
* @param string $subscription_id The PayPal subscription ID.
* @param array $atts Includes 'entry', 'action', 'amount'.
* @param object $subscription The PayPal subscription API response.
*
* @return int
*/
private static function create_new_subscription( $subscription_id, $atts, $subscription ) {
$next_bill_date = gmdate( 'Y-m-d' );
if ( ! empty( $subscription->billing_info->next_billing_time ) ) {
$next_bill_date = gmdate( 'Y-m-d', strtotime( $subscription->billing_info->next_billing_time ) );
}
$new_values = array(
'amount' => FrmTransLiteAppHelper::get_formatted_amount_for_currency( $atts['amount'], $atts['action'] ),
'paysys' => 'paypal',
'item_id' => $atts['entry']->id,
'action_id' => $atts['action']->ID,
'sub_id' => $subscription_id,
'interval_count' => $atts['action']->post_content['interval_count'],
'time_interval' => $atts['action']->post_content['interval'],
'status' => 'active',
'next_bill_date' => $next_bill_date,
'test' => 'test' === FrmPayPalLiteAppHelper::active_mode() ? 1 : 0,
);
$frm_sub = new FrmTransLiteSubscription();
return $frm_sub->create( $new_values );
}
/**
* Create the initial payment record for a new subscription.
*
* Uses the PayPal subscription ID as a temporary receipt_id. When the PAYMENT.SALE.COMPLETED
* webhook arrives later, the receipt_id is updated to the real capture/sale ID.
*
* @since 6.31
*
* @param string $subscription_id The PayPal subscription ID.
* @param int $sub_id The local subscription record ID.
* @param array $atts Includes 'entry', 'action', 'amount'.
*
* @return void
*/
private static function maybe_create_initial_subscription_payment( $subscription_id, $sub_id, $atts ) {
$atts['status'] = 'complete';
$atts['charge'] = new stdClass();
$atts['charge']->id = $subscription_id;
$atts['charge']->amount = $atts['amount'];
$atts['charge']->sub_id = $sub_id;
$payment_id = self::create_new_payment( $atts );
$frm_payment = new FrmTransLitePayment();
$payment = $frm_payment->get_one( $payment_id );
$status = $atts['status'];
FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
}
/**
* Check if PayPal integration is enabled.
*
* @return bool true if PayPal is set up.
*/
private static function paypal_is_configured() {
return (bool) FrmPayPalLiteConnectHelper::get_merchant_id();
}
/**
* Convert the amount from 10.00 to 1000.
*
* @param mixed $amount
* @param array $atts
*
* @return string
*/
public static function prepare_amount( $amount, $atts = array() ) {
$amount = parent::prepare_amount( $amount, $atts );
$currency = self::get_currency_for_action( $atts );
return number_format( floatval( $amount ), $currency['decimals'], '', '' );
}
/**
* If this form submits with ajax, load the scripts on the first page.
*
* @param array $params
*
* @return void
*/
public static function maybe_load_scripts( $params ) {
// phpcs:ignore Universal.Operators.StrictComparisons
if ( $params['form_id'] == $params['posted_form_id'] ) {
// This form has already been posted, so we aren't on the first page.
return;
}
$form = FrmForm::getOne( $params['form_id'] );
if ( ! $form ) {
return;
}
$credit_card_field = FrmField::getAll(
array(
'fi.form_id' => $form->id,
'type' => 'credit_card',
)
);
if ( ! $credit_card_field ) {
return;
}
$payment_actions = self::get_actions_before_submit( $form->id );
if ( ! $payment_actions ) {
return;
}
$found_gateway = false;
foreach ( $payment_actions as $action ) {
$gateways = $action->post_content['gateway'];
if ( in_array( 'paypal', (array) $gateways, true ) ) {
$found_gateway = true;
break;
}
}
if ( ! $found_gateway ) {
return;
}
self::load_scripts( (int) $form->id );
}
/**
* Load front end JavaScript for a PayPal form.
*
* @param int $form_id
*
* @return void
*/
public static function load_scripts( $form_id ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, SlevomatCodingStandard.Functions.FunctionLength.FunctionLength, SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh, SlevomatCodingStandard.Files.LineLength.LineTooLong
if ( FrmAppHelper::is_admin_page( 'formidable-entries' ) ) {
return;
}
if ( wp_script_is( 'formidable-paypal', 'enqueued' ) ) {
return;
}
if ( ! $form_id || ! is_int( $form_id ) ) {
_doing_it_wrong( __METHOD__, '$form_id parameter must be a non-zero integer', '6.31' );
return;
}
$payment_action_by_id = array();
add_filter(
'frm_trans_settings_for_js',
/**
* @param array $settings_for_action
* @param WP_Post $payment_action
*
* @return array
*/
function ( $settings_for_action, $payment_action ) use ( &$payment_action_by_id ) {
$payment_action_by_id[ $payment_action->ID ] = $payment_action;
$settings_for_action['paypalLayout'] = ! empty( $payment_action->post_content['paypal_layout'] ) ? $payment_action->post_content['paypal_layout'] : 'card_and_checkout'; // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
return $settings_for_action;
},
10,
2
);
$action_settings = self::prepare_settings_for_js( $form_id );
$action_setting_match = false;
foreach ( $action_settings as $action ) {
$gateways = $action['gateways'];
if ( ! $gateways || in_array( 'paypal', (array) $gateways, true ) ) {
$action_setting_match = $action;
break;
}
}
if ( false === $action_setting_match || ! array_key_exists( $action_setting_match['id'], $payment_action_by_id ) ) {
return;
}
$action = $payment_action_by_id[ $action_setting_match['id'] ];
// Use capture for one-time payments and subscription for recurring payments.
$intent = $action->post_content['type'] === 'single' ? 'capture' : 'subscription';
/**
* Build the PayPal SDK URL with required parameters.
*
* - Subscriptions require intent=subscription.
* - Subscriptions also require vault=true.
*/
$query_args = array(
'client-id' => self::get_client_id(),
'intent' => $intent,
'currency' => strtoupper( $action->post_content['currency'] ?? 'USD' ),
'merchant-id' => FrmPayPalLiteConnectHelper::get_merchant_id(),
'enable-funding' => is_ssl() ? 'venmo,applepay' : 'venmo',
);
if ( 'subscription' === $intent ) {
$query_args['vault'] = 'true';
}
if ( 'subscription' === $intent ) {
$include_buttons = true;
$include_card_fields = false;
} else {
// One time payments.
$include_buttons = false;
$include_card_fields = false;
switch ( $action->post_content['paypal_layout'] ?? 'card_and_checkout' ) {
case 'card_only':
$include_card_fields = true;
break;
case 'checkout_only':
$include_buttons = true;
break;
default:
$include_buttons = true;
$include_card_fields = true;
break;
}
}//end if
$include_messages = true;
switch ( $action->post_content['pay_later'] ?? 'auto' ) {
case 'off':
$query_args['disable-funding'] = 'paylater';
break;
case 'no-messaging':
// PayPal throws a TypeError: can't access property "PAGE_TYPE", trackingDetails is undefined error
// a lot of the time if you include messages. If you see this error, try using this 'no-messaging' option.
$include_messages = false;
break;
}
$components = array();
$supports_wallet_buttons = $include_buttons && 'subscription' !== $intent && is_ssl() && self::include_google_pay_apple_pay();
$include_google_pay = $supports_wallet_buttons && self::include_google_pay();
$include_apple_pay = $supports_wallet_buttons && self::include_apple_pay();
if ( $include_buttons ) {
$components[] = 'buttons';
if ( $include_google_pay ) {
$components[] = 'googlepay';
}
if ( $include_apple_pay ) {
$components[] = 'applepay';
}
}
if ( $include_card_fields ) {
$components[] = 'card-fields';
}
if ( $include_messages ) {
$components[] = 'messages';
}
// Enables .isEligible checks.
$components[] = 'funding-eligibility';
// Required for radio button option logos.
$components[] = 'marks';
$query_args['components'] = implode( ',', $components );
$locale = self::get_paypal_locale();
if ( $locale ) {
$query_args['locale'] = str_replace( '-', '_', $locale );
}
/**
* Allow customization of the PayPal SDK URL query arguments.
*
* @since 6.31
*
* @param array $query_args
* @param WP_Post $action
*/
$query_args = apply_filters( 'frm_paypal_sdk_url_query_args', $query_args, $action );
$sdk_url = add_query_arg( $query_args, 'https://www.paypal.com/sdk/js' );
wp_register_script( 'paypal-sdk', $sdk_url, array(), null, false );
if ( $include_apple_pay ) {
wp_register_script( 'apple-pay-sdk', 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js', array(), null, false );
}
$has_break = FrmAppHelper::pro_is_installed() && (bool) FrmField::get_all_types_in_form( $form_id, 'break' );
add_filter(
'script_loader_tag',
/**
* @param string $tag
* @param string $handle
*
* @return string
*/
function ( $tag, $handle ) use ( $has_break ) {
if ( 'paypal-sdk' === $handle ) {
$attributes = ' async data-partner-attribution-id="' . esc_attr( FrmPayPalLiteConnectHelper::get_bn_code() ) . '"';
return str_replace( ' src=', $attributes . ' src=', $tag );
}
if ( in_array( $handle, array( 'apple-pay-sdk', 'google-pay' ), true ) ) {
return str_replace( ' src=', ' async src=', $tag );
}
if ( $has_break && 'formidable-paypal' === $handle ) {
return str_replace( ' src=', ' async src=', $tag );
}
return $tag;
},
10,
2
);
wp_enqueue_style(
'formidable-paypal',
FrmPayPalLiteAppHelper::plugin_url() . 'css/frontend.css',
array(),
FrmAppHelper::plugin_version()
);
$dependencies = array( 'paypal-sdk', 'formidable' );
if ( $include_apple_pay ) {
$dependencies[] = 'apple-pay-sdk';
}
$script_url = FrmPayPalLiteAppHelper::plugin_url() . 'js/frontend.js';
wp_enqueue_script(
'formidable-paypal',
$script_url,
$dependencies,
FrmAppHelper::plugin_version(),
false
);
if ( $include_google_pay ) {
wp_enqueue_script(
'google-pay',
'https://pay.google.com/gp/p/js/pay.js',
array(),
'1.0',
false
);
}
$paypal_vars = array(
'formId' => $form_id,
'nonce' => wp_create_nonce( 'frm_paypal_ajax' ),
'ajax' => esc_url_raw( FrmAppHelper::get_ajax_url() ),
'settings' => $action_settings,
'style' => self::get_style_for_js( $form_id ),
'buttonStyle' => self::get_button_style_for_js( $action ),
'imagesUrl' => FrmPayPalLiteAppHelper::plugin_url() . 'images/',
'includeGooglePay' => $include_google_pay,
'includeApplePay' => $include_apple_pay,
'includeGooglePayApplePay' => $include_google_pay || $include_apple_pay,
'mode' => FrmPayPalLiteAppHelper::active_mode(),
);
wp_localize_script( 'formidable-paypal', 'frmPayPalVars', $paypal_vars );
}
/**
* Check if the Google Pay and Apple Pay wallet buttons are allowed at all.
*
* This gates both wallets together. To turn off just one of them, use
* frm_paypal_commerce_include_google_pay or frm_paypal_commerce_include_apple_pay instead.
*
* @since 6.31
*
* @return bool
*/
private static function include_google_pay_apple_pay() {
/**
* @since 6.31
*
* @param bool $include_google_pay_apple_pay
*/
return (bool) apply_filters( 'frm_include_google_pay_apple_pay', true );
}
/**
* Check if the Google Pay wallet button is allowed for PayPal Commerce.
*
* Only called when frm_include_google_pay_apple_pay allows the wallets, so this
* filter turns off Google Pay without affecting Apple Pay.
*
* @since 6.34
*
* @return bool
*/
private static function include_google_pay() {
/**
* @since 6.34
*
* @param bool $include_google_pay
*/
return (bool) apply_filters( 'frm_paypal_commerce_include_google_pay', true );
}
/**
* Check if the Apple Pay wallet button is allowed for PayPal Commerce.
*
* Only called when frm_include_google_pay_apple_pay allows the wallets, so this
* filter turns off Apple Pay without affecting Google Pay.
*
* @since 6.34
*
* @return bool
*/
private static function include_apple_pay() {
/**
* @since 6.34
*
* @param bool $include_apple_pay
*/
return (bool) apply_filters( 'frm_paypal_commerce_include_apple_pay', true );
}
/**
* @since 6.31
*
* @return string
*/
private static function get_paypal_locale() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, SlevomatCodingStandard.Functions.FunctionLength.FunctionLength
$locale = str_replace( '_', '-', get_locale() );
$parts = explode( '_', $locale );
$lang = strtolower( $parts[0] );
$country = isset( $parts[1] ) ? strtoupper( $parts[1] ) : '';
switch ( $lang ) {
case 'ar':
// Arabic
$countries = array( 'DZ', 'BH', 'EG', 'JO', 'KW', 'MA', 'OM', 'QA', 'SA', 'TN', 'AE', 'YE' );
break;
case 'bg':
// Bulgarian
$countries = array( 'BG' );
break;
case 'cs':
// Czech
$countries = array( 'CZ' );
break;
case 'da':
// Danish
$countries = array( 'DK', 'FO', 'GL' );
break;
case 'de':
// German
$countries = array( 'AT', 'DE', 'LU', 'CH' );
break;
case 'el':
// Greek
$countries = array( 'GR' );
break;
case 'en':
// English
$countries = array(
'AL',
'DZ',
'AD',
'AO',
'AI',
'AG',
'AR',
'AM',
'AW',
'AU',
'AT',
'AZ',
'BS',
'BH',
'BB',
'BY',
'BE',
'BZ',
'BJ',
'BM',
'BT',
'BO',
'BA',
'BW',
'BR',
'VG',
'BN',
'BG',
'BF',
'BI',
'KH',
'CM',
'CA',
'CV',
'KY',
'TD',
'CL',
'C2',
'CN',
'CO',
'KM',
'CG',
'CD',
'CK',
'CR',
'CI',
'HR',
'CY',
'CZ',
'DK',
'DJ',
'DM',
'DO',
'EC',
'EG',
'SV',
'ER',
'EE',
'SZ',
'ET',
'FK',
'FO',
'FJ',
'FI',
'FR',
'GF',
'PF',
'GA',
'GM',
'GE',
'DE',
'GI',
'GR',
'GL',
'GD',
'GP',
'GT',
'GN',
'GW',
'GY',
'HN',
'HK',
'HU',
'IS',
'IN',
'ID',
'IE',
'IL',
'IT',
'JM',
'JP',
'JO',
'KZ',
'KE',
'KI',
'KW',
'KG',
'LA',
'LV',
'LS',
'LI',
'LT',
'LU',
'MG',
'MW',
'MY',
'MV',
'ML',
'MT',
'MH',
'MQ',
'MR',
'MU',
'MX',
'FM',
'MD',
'MC',
'MN',
'ME',
'MS',
'MA',
'MZ',
'NA',
'NR',
'NP',
'NL',
'AN',
'NC',
'NZ',
'NI',
'NE',
'NG',
'NU',
'NF',
'MK',
'NO',
'OM',
'PW',
'PA',
'PG',
'PY',
'PE',
'PH',
'PN',
'PL',
'PT',
'QA',
'RE',
'RO',
'RU',
'RW',
'WS',
'SM',
'ST',
'SA',
'SN',
'RS',
'SC',
'SL',
'SG',
'SK',
'SI',
'SB',
'SO',
'ZA',
'KR',
'ES',
'LK',
'SH',
'KN',
'LC',
'PM',
'VC',
'SR',
'SJ',
'SE',
'CH',
'TW',
'TJ',
'TZ',
'TH',
'TG',
'TO',
'TT',
'TN',
'TR',
'TM',
'TC',
'TV',
'UG',
'UA',
'AE',
'GB',
'US',
'UY',
'VU',
'VA',
'VE',
'VN',
'WF',
'YE',
'ZM',
'ZW',
);
break;
case 'es':
// Spanish
$countries = array(
'DZ',
'AD',
'AO',
'AI',
'AG',
'AR',
'AM',
'AW',
'AZ',
'BS',
'BH',
'BB',
'BZ',
'BJ',
'BM',
'BO',
'BW',
'VG',
'BF',
'BI',
'CV',
'KY',
'TD',
'CL',
'CO',
'KM',
'CG',
'CD',
'CK',
'CR',
'DJ',
'DM',
'DO',
'EC',
'EG',
'SV',
'ER',
'SZ',
'ET',
'FK',
'FO',
'FJ',
'PF',
'GA',
'GM',
'GE',
'GI',
'GL',
'GD',
'GT',
'GN',
'GW',
'GY',
'HN',
'IE',
'JM',
'JO',
'KZ',
'KE',
'KI',
'KW',
'KG',
'LS',
'LI',
'LU',
'MG',
'MW',
'ML',
'MH',
'MR',
'MU',
'MX',
'MS',
'MA',
'MZ',
'NA',
'NR',
'AN',
'NC',
'NZ',
'NI',
'NE',
'NU',
'NF',
'OM',
'PW',
'PA',
'PG',
'PY',
'PE',
'PN',
'QA',
'RW',
'SM',
'ST',
'SA',
'SN',
'RS',
'SC',
'SL',
'SB',
'SO',
'ZA',
'ES',
'SH',
'KN',
'LC',
'PM',
'VC',
'SR',
'SJ',
'TJ',
'TZ',
'TG',
'TT',
'TN',
'TM',
'TC',
'TV',
'UG',
'UA',
'AE',
'US',
'UY',
'VU',
'VA',
'VE',
'WF',
'YE',
'ZM',
);
break;
case 'et':
// Estonian
$countries = array( 'EE' );
break;
case 'fi':
// Finnish
$countries = array( 'FI' );
break;
case 'fr':
// French
$countries = array(
'DZ',
'AD',
'AO',
'AI',
'AG',
'AM',
'AW',
'AZ',
'BS',
'BH',
'BB',
'BE',
'BZ',
'BJ',
'BM',
'BO',
'BW',
'VG',
'BF',
'BI',
'CM',
'CA',
'CV',
'KY',
'TD',
'CL',
'CO',
'KM',
'CG',
'CD',
'CK',
'CR',
'CI',
'DJ',
'DM',
'DO',
'EC',
'EG',
'SV',
'ER',
'SZ',
'ET',
'FK',
'FO',
'FJ',
'FR',
'GF',
'PF',
'GA',
'GM',
'GE',
'GI',
'GL',
'GD',
'GP',
'GT',
'GN',
'GW',
'GY',
'HN',
'IE',
'JM',
'JO',
'KZ',
'KE',
'KI',
'KW',
'KG',
'LS',
'LI',
'LU',
'MG',
'MW',
'ML',
'MH',
'MQ',
'MR',
'MU',
'YT',
'MC',
'MS',
'MA',
'MZ',
'NA',
'NR',
'AN',
'NC',
'NZ',
'NI',
'NE',
'NU',
'NF',
'OM',
'PW',
'PA',
'PG',
'PE',
'PN',
'QA',
'RE',
'RW',
'SC',
'SM',
'ST',
'SA',
'SN',
'RS',
'SL',
'SB',
'SO',
'ZA',
'SH',
'KN',
'LC',
'PM',
'VC',
'SR',
'SJ',
'CH',
'TJ',
'TZ',
'TG',
'TT',
'TN',
'TM',
'TC',
'TV',
'UG',
'UA',
'AE',
'US',
'UY',
'VU',
'VA',
'VE',
'WF',
'YE',
'ZM',
);
break;
case 'he':
// Hebrew
$countries = array( 'IL' );
break;
case 'hu':
// Hungarian
$countries = array( 'HU' );
break;
case 'id':
// Indonesian
$countries = array( 'ID' );
break;
case 'it':
// Italian
$countries = array( 'IT' );
break;
case 'ja':
// Japanese
$countries = array( 'JP' );
break;
case 'ko':
// Korean
$countries = array( 'KR' );
break;
case 'lt':
// Lithuanian
$countries = array( 'LT' );
break;
case 'lv':
// Latvian
$countries = array( 'LV' );
break;
case 'ms':
// Malay
$countries = array( 'BN', 'MY' );
break;
case 'nl':
// Dutch
$countries = array( 'BE', 'NL' );
break;
case 'no':
// Norwegian
$countries = array( 'NO' );
break;
case 'pl':
// Polish
$countries = array( 'PL' );
break;
case 'pt':
// Portuguese
$countries = array( 'BR', 'PT' );
break;
case 'ro':
// Romanian
$countries = array( 'RO' );
break;
case 'ru':
// Russian
$countries = array( 'EE', 'LV', 'LT', 'RU', 'UA' );
break;
case 'si':
// Sinhala
$countries = array( 'LK' );
break;
case 'sk':
// Slovak
$countries = array( 'SK' );
break;
case 'sl':
// Slovenian
$countries = array( 'SI' );
break;
case 'sq':
// Albanian
$countries = array( 'AL' );
break;
case 'sv':
// Swedish
$countries = array( 'SE' );
break;
case 'th':
// Thai
$countries = array( 'TH' );
break;
case 'tl':
// Tagalog
$countries = array( 'PH' );
break;
case 'tr':
// Turkish
$countries = array( 'TR' );
break;
case 'vi':
// Vietnamese
$countries = array( 'VN' );
break;
case 'zh':
// Chinese
$countries = array(
'C2',
'CN',
'HK',
'TW',
'DZ',
'AD',
'AO',
'AI',
'AG',
'AM',
'AW',
'AZ',
'BS',
'BH',
'BB',
'BZ',
'BJ',
'BM',
'BO',
'BW',
'VG',
'BF',
'BI',
'CV',
'KY',
'TD',
'CL',
'CO',
'KM',
'CG',
'CD',
'CK',
'CR',
'DJ',
'DM',
'DO',
'EC',
'EG',
'SV',
'ER',
'SZ',
'ET',
'FK',
'FO',
'FJ',
'PF',
'GA',
'GM',
'GE',
'GI',
'GL',
'GD',
'GT',
'GN',
'GW',
'GY',
'HN',
'IE',
'JM',
'JO',
'KZ',
'KE',
'KI',
'KW',
'KG',
'LS',
'LI',
'LT',
'LU',
'MG',
'MW',
'ML',
'MH',
'MR',
'MU',
'MS',
'MA',
'MZ',
'NA',
'NR',
'AN',
'NC',
'NZ',
'NI',
'NE',
'NU',
'NF',
'OM',
'PW',
'PA',
'PG',
'PE',
'PN',
'QA',
'RW',
'SM',
'ST',
'SA',
'SN',
'RS',
'SC',
'SL',
'SB',
'SO',
'ZA',
'SH',
'KN',
'LC',
'PM',
'VC',
'SR',
'SJ',
'TJ',
'TZ',
'TG',
'TT',
'TN',
'TM',
'TC',
'TV',
'UG',
'UA',
'AE',
'US',
'UY',
'VU',
'VA',
'VE',
'WF',
'YE',
'ZM',
);
break;
default:
$countries = array();
break;
}//end switch
if ( $country && in_array( $country, $countries, true ) ) {
return $lang . '-' . $country;
}
return 'en-US';
}
/**
* Get the style for the PayPal form.
*
* @param int $form_id
*
* @return array
*/
public static function get_style_for_js( $form_id ) {
$settings = self::get_style_settings_for_form( $form_id );
$style = array(
'body' => array(
'padding' => 0,
),
'input' => array(
'font-size' => $settings['field_font_size'],
'color' => $settings['text_color'],
'font-weight' => $settings['field_weight'],
'padding' => $settings['field_pad'],
'line-height' => 1.3,
'border' => self::get_border_shorthand( $settings ),
'border-radius' => self::get_border_radius( $settings ),
),
'input::placeholder' => array(
'color' => $settings['text_color_disabled'],
),
'.invalid' => array(
'color' => $settings['border_color_error'],
),
);
if ( ! empty( $settings['font'] ) ) {
$style['input']['font-family'] = $settings['font'];
}
/**
* Filter the PayPal card field styles.
*
* @since 6.31
*
* @param array $style
* @param array $settings
* @param int $form_id
*/
return apply_filters( 'frm_paypal_style', $style, $settings, $form_id );
}
/**
* Get PayPal button style configuration from form action settings.
* Documentation at https://developer.paypal.com/sdk/js/reference/#style
*
* @since 6.31
*
* @param WP_Post $form_action The form action containing button settings.
*
* @return array The style configuration array for PayPal button.
*/
private static function get_button_style_for_js( $form_action ) {
$button_color = $form_action->post_content['button_color'] ?? 'default';
$button_label = $form_action->post_content['button_label'] ?? 'paypal';
$button_border_radius = $form_action->post_content['button_border_radius'] ?? 10;
$style_for_js = array(
'layout' => 'vertical',
'color' => $button_color,
'label' => $button_label,
'borderRadius' => (int) $button_border_radius,
'height' => 40,
);
// Unset the color so PayPal can use its defaults.
// Many buttons have different colors
if ( 'default' === $button_color ) {
unset( $style_for_js['color'] );
}
/**
* @since 6.31
*
* @param array $style_for_js
* @param WP_Post $form_action
*/
return apply_filters( 'frm_paypal_button_style', $style_for_js, $form_action );
}
/**
* Get and format the style settings for JavaScript to use with the get_style function.
*
* @param int $form_id
*
* @return array
*/
private static function get_style_settings_for_form( $form_id ) {
if ( ! $form_id ) {
return array();
}
$style = FrmStylesController::get_form_style( $form_id );
if ( ! $style ) {
return array();
}
$settings = FrmStylesHelper::get_settings_for_output( $style );
$disallowed = array( ';', ':', '!important' );
foreach ( $settings as $k => $s ) {
if ( is_string( $s ) ) {
$settings[ $k ] = str_replace( $disallowed, '', $s );
}
}
return $settings;
}
/**
* Get the border width for PayPal card fields.
*
* @since 6.31
*
* @param array $settings
*
* @return string
*/
private static function get_border_width( $settings ) {
if ( ! empty( $settings['field_shape_type'] ) && 'underline' === $settings['field_shape_type'] ) {
return '0 0 ' . $settings['field_border_width'] . ' 0';
}
return $settings['field_border_width'];
}
/**
* Get the border radius for PayPal card fields.
*
* @since 6.31
*
* @param array $settings
*
* @return string
*/
private static function get_border_radius( $settings ) {
if ( ! empty( $settings['field_shape_type'] ) ) {
switch ( $settings['field_shape_type'] ) {
case 'underline':
case 'regular':
return '0px';
case 'circle':
return '30px';
}
}
return $settings['border_radius'];
}
/**
* Get the border shorthand for PayPal card fields.
*
* @since 6.31
*
* @param array $settings
*
* @return string
*/
private static function get_border_shorthand( $settings ) {
$width = self::get_border_width( $settings );
$style = $settings['field_border_style'];
$color = $settings['border_color'];
return "{$width} {$style} {$color}";
}
/**
* If the names are being used on the CC fields,
* make sure it doesn't prevent the submission if PayPal has approved.
*
* @since 6.31
*
* @param array $errors
* @param stdClass $field
*
* @return array
*/
public static function remove_cc_validation( $errors, $field ) {
$paypal_order_id = FrmAppHelper::get_post_param( 'paypal_order_id', '', 'sanitize_text_field' );
$paypal_subscription_id = FrmAppHelper::get_post_param( 'paypal_subscription_id', '', 'sanitize_text_field' );
if ( ! $paypal_order_id && ! $paypal_subscription_id ) {
return $errors;
}
return FrmTransLiteActionsController::remove_cc_errors( $errors, $field );
}
/**
* @since 6.31
*
* @return void
*/
public static function actions_js() {
wp_enqueue_script(
'frm_paypal_admin',
FrmPayPalLiteAppHelper::plugin_url() . 'js/action.js',
array( 'wp-hooks', 'wp-i18n' ),
FrmAppHelper::plugin_version()
);
}
/**
* Modify the new action post data to use the payment action type when the PayPal plugin is not active.
* This works better than having it disabled even when PayPal is supported.
*
* @since 6.31
*
* @return void
*/
public static function maybe_modify_new_action_post_data() {
$action_type = FrmAppHelper::get_param( 'type', '', 'post', 'sanitize_text_field' );
if ( 'paypal-legacy' === $action_type ) {
$_POST['type'] = 'paypal';
return;
}
if ( ! in_array( $action_type, array( 'paypal', 'stripe', 'square' ), true ) ) {
return;
}
$_POST['type'] = 'payment';
add_filter(
'frm_form_payment_action_settings',
/**
* @param WP_Post $action_settings
*
* @return WP_Post
*/
function ( $action_settings ) use ( $action_type ) {
return self::set_gateway_as_default( $action_settings, $action_type );
}
);
}
/**
* Set the gateway to PayPal as the default.
*
* @param WP_Post $action_settings
* @param string $action_type
*
* @return WP_Post
*/
private static function set_gateway_as_default( $action_settings, $action_type ) {
$action_settings->post_content['gateway'] = array( $action_type );
return $action_settings;
}
/**
* Print additional options for Stripe action settings.
*
* @param array $atts
*
* @return void
*/
public static function add_action_options( $atts ) {
$form_action = $atts['form_action'];
$action_control = $atts['action_control'];
include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/action-settings-options.php';
}
/**
* Print additional options for button settings.
*
* @since 6.31
*
* @param array|FrmFormAction $args_or_action_control Either the args array with form_action and action_control, or the action_control object.
* @param WP_Post|null $form_action The form action object (when called directly).
*
* @return void
*/
public static function add_button_settings_section( $args_or_action_control, $form_action = null ) {
if ( is_array( $args_or_action_control ) ) {
$form_action = $args_or_action_control['form_action'];
$action_control = $args_or_action_control['action_control'];
} else {
$action_control = $args_or_action_control;
}
include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/button-settings.php';
}
/**
* Add PayPal subscription settings (product name and product type) after payment type.
*
* @since 6.31
*
* @param array $args Arguments containing form_action and action_control.
*
* @return void
*/
public static function add_paypal_subscription_settings_from_hook( $args ) {
$form_action = $args['form_action'];
$action_control = $args['action_control'];
$product_type_value = $form_action->post_content['product_type'] ?? '';
?>
field_options['is_paypal_order_field'] = 1;
FrmField::update( $field_id, array( 'field_options' => $field->field_options ) );
$settings[ $setting_key ] = $field_id;
return $settings;
}
/**
* @return string
*/
private static function get_client_id() {
if ( 'live' === FrmPayPalLiteAppHelper::active_mode() ) {
return 'AZXLuLRGUnqHlo75Di6zRonnVy7mLBz-sXYIeHTmv9KsDIkNMp7au9rlapegcYKh8zhfPXvGdxal6PHG';
}
return 'AYTiIIchQiekyGhJouWoLapPfjijirOtKHSN255SLhcP0TIaWBID-zxsYDaNmP4fXL6YcQxiSIMS0Lwu';
}
/**
* @param array $atts
*
* @return void
*/
public static function show_paypal_button_settings( $atts ) {
$form_action = $atts['form_action'];
$action_control = $atts['action_control'];
// End the payment settings section.
echo ' ';
self::add_button_settings_section( $action_control, $form_action );
// Open up a div tag since the payment section is closed after this and we already ended the section.
// This results in an empty div tag but it allows us to inject these options without requiring
// any updates in the payments submodule.
echo '