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 '
'; } }