From 4bd5d0c3d3a222d730872c2628fea54dbb6a47a0 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 4 Dec 2025 14:59:35 -0400 Subject: [PATCH 01/62] Copy over and rename the Square files --- formidable.php | 20 + .../FrmPayPalLiteActionsController.php | 350 ++++++++ .../FrmPayPalLiteAppController.php | 66 ++ .../FrmPayPalLiteEventsController.php | 178 ++++ .../FrmPayPalLiteHooksController.php | 52 ++ .../FrmPayPalLiteSettingsController.php | 68 ++ paypal/helpers/FrmPayPalLiteAppHelper.php | 88 ++ paypal/helpers/FrmPayPalLiteConnectHelper.php | 820 ++++++++++++++++++ paypal/js/action.js | 3 + paypal/js/frontend.js | 3 + paypal/js/settings.js | 3 + paypal/models/FrmPayPalLiteSettings.php | 105 +++ paypal/views/settings/form.php | 6 + 13 files changed, 1762 insertions(+) create mode 100644 paypal/controllers/FrmPayPalLiteActionsController.php create mode 100644 paypal/controllers/FrmPayPalLiteAppController.php create mode 100644 paypal/controllers/FrmPayPalLiteEventsController.php create mode 100644 paypal/controllers/FrmPayPalLiteHooksController.php create mode 100644 paypal/controllers/FrmPayPalLiteSettingsController.php create mode 100644 paypal/helpers/FrmPayPalLiteAppHelper.php create mode 100644 paypal/helpers/FrmPayPalLiteConnectHelper.php create mode 100644 paypal/js/action.js create mode 100644 paypal/js/frontend.js create mode 100644 paypal/js/settings.js create mode 100644 paypal/models/FrmPayPalLiteSettings.php create mode 100644 paypal/views/settings/form.php diff --git a/formidable.php b/formidable.php index e64919ca75..55c56a1d26 100644 --- a/formidable.php +++ b/formidable.php @@ -145,6 +145,26 @@ function frm_class_autoloader( $class_name, $filepath ) { if ( file_exists( $filepath ) ) { require $filepath; } + + return; + } + + if ( preg_match( '/^FrmPayPalLite.+$/', $class_name ) ) { + $filepath = $original_filepath . '/paypal/'; + + if ( preg_match( '/^.+Helper$/', $class_name ) ) { + $filepath .= 'helpers/'; + } elseif ( preg_match( '/^.+Controller$/', $class_name ) ) { + $filepath .= 'controllers/'; + } else { + $filepath .= 'models/'; + } + + $filepath .= $class_name . '.php'; + + if ( file_exists( $filepath ) ) { + require $filepath; + } } } diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php new file mode 100644 index 0000000000..3e1369603a --- /dev/null +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -0,0 +1,350 @@ +form_id : $field['form_id']; + $actions = self::get_actions_before_submit( $form_id ); + + if ( empty( $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 x.x + * + * @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 x.x + * + * @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 ) { + $response = array( + 'success' => false, + 'run_triggers' => false, + 'show_errors' => true, + ); + $atts = compact( 'action', 'entry', 'form' ); + + $amount = self::prepare_amount( $action->post_content['amount'], $atts ); + + if ( empty( $amount ) || $amount == 000 ) { + $response['error'] = __( 'Please specify an amount for the payment', 'formidable' ); + return $response; + } + + if ( ! self::paypal_is_configured() ) { + $response['error'] = __( 'There was a problem communicating with PayPal. Please try again.', '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; + } + + return $response; + } + + /** + * 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 ) { + return 'Payments are not yet implemented for PayPal.'; + } + + /** + * 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 ) { + + return 'Recurring payments are not yet implemented for PayPal.'; + } + + /** + * 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( $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 ) { + 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 ) { + 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', 'x.x' ); + return; + } + + $action_settings = self::prepare_settings_for_js( $form_id ); + $found_gateway = false; + + foreach ( $action_settings as $action ) { + $gateways = $action['gateways']; + + if ( ! $gateways || in_array( 'paypal', (array) $gateways, true ) ) { + $found_gateway = true; + break; + } + } + + if ( ! $found_gateway ) { + return; + } + + wp_register_script( + 'paypal', + FrmPayPalLiteAppHelper::active_mode() === 'live' ? 'https://web.paypal.com/v1/paypal.js' : 'https://sandbox.web.paypal.com/v1/paypal.js', + array(), + '1.0', + false + ); + + $dependencies = array( 'paypal', 'formidable' ); + $script_url = FrmPayPalLiteAppHelper::plugin_url() . 'js/frontend.js'; + + wp_enqueue_script( + 'formidable-paypal', + $script_url, + $dependencies, + FrmAppHelper::plugin_version(), + 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, + ); + + wp_localize_script( 'formidable-paypal', 'frmPayPalVars', $paypal_vars ); + } + + /** + * If the names are being used on the CC fields, + * make sure it doesn't prevent the submission if PayPal has approved. + * + * @since x.x + * + * @param array $errors + * @param stdClass $field + * @param array $values + * + * @return array + */ + public static function remove_cc_validation( $errors, $field, $values ) { + // TODO + $has_processed = false; + + if ( ! $has_processed ) { + return $errors; + } + + $field_id = $field->temp_id ?? $field->id; + + if ( isset( $errors[ 'field' . $field_id . '-cc' ] ) ) { + unset( $errors[ 'field' . $field_id . '-cc' ] ); + } + + if ( isset( $errors[ 'field' . $field_id ] ) ) { + unset( $errors[ 'field' . $field_id ] ); + } + + return $errors; + } + + /** + * @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() + ); + } +} diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php new file mode 100644 index 0000000000..c5f5764fce --- /dev/null +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -0,0 +1,66 @@ + 'PayPal', + 'user_label' => __( 'Payment', 'formidable' ), + 'class' => 'PayPalLite', + 'recurring' => true, + 'include' => array( + 'billing_first_name', + 'billing_last_name', + 'credit_card', + 'billing_address', + ), + ); + return $gateways; + } + + /** + * Handle the request to initialize with PayPal Api + * + * @return void + */ + public static function handle_oauth() { + FrmAppHelper::permission_check( 'frm_change_settings' ); + + if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) { + wp_send_json_error(); + } + + $redirect_url = FrmPayPalLiteConnectHelper::get_oauth_redirect_url(); + + if ( false === $redirect_url ) { + wp_send_json_error( 'Unable to connect to PayPal successfully' ); + } + + $response_data = array( + 'redirect_url' => $redirect_url, + ); + wp_send_json_success( $response_data ); + } + + public static function handle_disconnect() { + FrmAppHelper::permission_check( 'frm_change_settings' ); + + if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) { + wp_send_json_error(); + } + + FrmPayPalLiteConnectHelper::handle_disconnect(); + wp_send_json_success(); + } +} diff --git a/paypal/controllers/FrmPayPalLiteEventsController.php b/paypal/controllers/FrmPayPalLiteEventsController.php new file mode 100644 index 0000000000..787918b4f9 --- /dev/null +++ b/paypal/controllers/FrmPayPalLiteEventsController.php @@ -0,0 +1,178 @@ +flush_response(); + + $unprocessed_event_ids = FrmPayPalLiteConnectHelper::get_unprocessed_event_ids(); + + if ( $unprocessed_event_ids ) { + $this->process_event_ids( $unprocessed_event_ids ); + } + wp_send_json_success(); + } + + /** + * @since x.x + * + * @param array $event_ids + * + * @return void + */ + private function process_event_ids( $event_ids ) { + foreach ( $event_ids as $event_id ) { + if ( $this->should_skip_event( $event_id ) ) { + continue; + } + + set_transient( 'frm_paypal_last_process_' . $event_id, time(), 60 ); + + $this->event = FrmPayPalLiteConnectHelper::get_event( $event_id ); + + if ( is_object( $this->event ) ) { + $this->handle_event(); + $this->track_handled_event( $event_id ); + FrmPayPalLiteConnectHelper::process_event( $event_id ); + } else { + $this->count_failed_event( $event_id ); + } + } + } + + /** + * @since x.x + * + * @param string $event_id + * + * @return bool True if the event should be skipped. + */ + private function should_skip_event( $event_id ) { + if ( $this->last_attempt_to_process_event_is_too_recent( $event_id ) ) { + return true; + } + + $option = get_option( self::$events_to_skip_option_name ); + + if ( ! is_array( $option ) ) { + return false; + } + + return in_array( $event_id, $option, true ); + } + + /** + * @param string $event_id + * + * @return bool + */ + private function last_attempt_to_process_event_is_too_recent( $event_id ) { + $last_process_attempt = get_transient( 'frm_paypal_last_process_' . $event_id ); + return is_numeric( $last_process_attempt ) && $last_process_attempt > time() - 60; + } + + /** + * @since x.x + * + * @param string $event_id + * + * @return void + */ + private function count_failed_event( $event_id ) { + $transient_name = 'frm_paypal_failed_event_' . $event_id; + $transient = get_transient( $transient_name ); + + if ( is_int( $transient ) ) { + $failed_count = $transient + 1; + } else { + $failed_count = 1; + } + + $maximum_retries = 3; + + if ( $failed_count >= $maximum_retries ) { + $this->track_handled_event( $event_id ); + } else { + set_transient( $transient_name, $failed_count, 4 * DAY_IN_SECONDS ); + } + } + + /** + * Track an event to no longer process. + * This is called for successful events, and also for failed events after a number of retries. + * + * @since x.x + * + * @param string $event_id + * + * @return void + */ + private function track_handled_event( $event_id ) { + $option = get_option( self::$events_to_skip_option_name ); + + if ( is_array( $option ) ) { + if ( count( $option ) > 1000 ) { + // Prevent the option from getting too big by removing the front item before adding the next. + array_shift( $option ); + } + } else { + $option = array(); + } + + $option[] = $event_id; + update_option( self::$events_to_skip_option_name, $option, false ); + } + + /** + * @return void + */ + private function handle_event() { + + } + +} diff --git a/paypal/controllers/FrmPayPalLiteHooksController.php b/paypal/controllers/FrmPayPalLiteHooksController.php new file mode 100644 index 0000000000..2633495860 --- /dev/null +++ b/paypal/controllers/FrmPayPalLiteHooksController.php @@ -0,0 +1,52 @@ + self::class, + 'function' => 'route', + 'icon' => 'frm_icon_font frm_paypal_icon', + ); + + return $sections; + } + + /** + * Handle global settings routing. + * + * @return void + */ + public static function route() { + self::global_settings_form(); + } + + /** + * Print the PayPal section for Global settings. + * + * @param array $atts + * + * @return void + */ + public static function global_settings_form( $atts = array() ) { + include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/form.php'; + } + + /** + * @return array + */ + private static function get_default_settings_atts() { + return array( + 'errors' => array(), + 'message' => '', + ); + } + + /** + * Handle processing changes to global PayPal Settings. + * + * @return void + */ + public static function process_form() { + $settings = FrmPayPalLiteAppHelper::get_settings(); + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $settings->update( $_POST ); + $settings->store(); + } +} diff --git a/paypal/helpers/FrmPayPalLiteAppHelper.php b/paypal/helpers/FrmPayPalLiteAppHelper.php new file mode 100644 index 0000000000..bb2c33327e --- /dev/null +++ b/paypal/helpers/FrmPayPalLiteAppHelper.php @@ -0,0 +1,88 @@ +settings->test_mode ? 'test' : 'live'; + } + + /** + * Add education about PayPal fees. + * + * @param string $medium Medium identifier for the tip (for example 'tip'). + * @param array|false|string $gateway Gateway or list of gateways this applies to. + * + * @return void + */ + public static function fee_education( $medium = 'tip', $gateway = false ) { + $license_type = FrmAddonsController::license_type(); + + if ( in_array( $license_type, array( 'elite', 'business' ), true ) ) { + return; + } + + $classes = 'frm-light-tip show_paypal'; + + if ( $gateway && ! array_intersect( (array) $gateway, array( 'paypal' ) ) ) { + $classes .= ' frm_hidden'; + } + + FrmTipsHelper::show_tip( + array( + 'link' => array( + 'content' => 'paypal-fee', + 'medium' => $medium, + ), + 'tip' => 'Pay as you go pricing: 3% fee per-transaction + PayPal fees.', + 'call' => __( 'Upgrade to save on fees.', 'formidable' ), + 'class' => $classes, + ), + 'p' + ); + } +} diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php new file mode 100644 index 0000000000..c699532aa3 --- /dev/null +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -0,0 +1,820 @@ + + + + + + +
+ + + +
+ +
+
+ +
+
+ +
+ + + +
+ + +
+
+
+ + + + +
+ 'width: 10px; position: relative; top: 2px; margin-right: 5px;' ) ); + echo 'Connected'; + } else { + echo 'Not configured'; + } + ?> +
+
+
+ +
+
+ + + + + + + + + +
+
+
+ self::generate_client_password( $mode ), + 'user_id' => get_current_user_id(), + 'frm_paypal_api_mode' => $mode, + ); + + // Clear the transient so it doesn't fail. + delete_option( 'frm_paypal_lite_last_verify_attempt' ); + $data = self::post_to_connect_server( 'oauth_request', $additional_body ); + + if ( is_string( $data ) ) { + return false; + } + + if ( ! empty( $data->password ) ) { + update_option( self::get_server_side_token_option_name( $mode ), $data->password, 'no' ); + } + + if ( ! is_object( $data ) || empty( $data->redirect_url ) ) { + return false; + } + + return $data->redirect_url; + } + + /** + * @param string $action + * @param array $additional_body + * + * @return object|string + */ + private static function post_to_connect_server( $action, $additional_body = array() ) { + $body = array( + 'frm_paypal_api_action' => $action, + 'frm_paypal_api_mode' => FrmPayPalLiteAppHelper::active_mode(), + ); + $body = array_merge( $body, $additional_body ); + $url = self::get_url_to_connect_server(); + $headers = self::build_headers_for_post(); + + if ( ! $headers ) { + return 'Unable to build headers for post. Is your pro license configured properly?'; + } + + // (Seconds) default timeout is 5. we want a bit more time to work with. + $timeout = 45; + + self::try_to_extend_server_timeout( $timeout ); + + $args = compact( 'body', 'headers', 'timeout' ); + $response = wp_remote_post( $url, $args ); + + if ( ! self::validate_response( $response ) ) { + return 'Response from server is invalid'; + } + + $body = self::pull_response_body( $response ); + + if ( empty( $body->success ) ) { + if ( ! empty( $body->data ) && is_string( $body->data ) ) { + return $body->data; + } + return 'Response from server was not successful'; + } + + return $body->data ?? array(); + } + + /** + * @param array $response + * + * @return mixed + */ + private static function pull_response_body( $response ) { + $http_response = $response['http_response']; + $response_object = $http_response->get_response_object(); + return json_decode( $response_object->body ); + } + + /** + * @param mixed $response + * + * @return bool + */ + private static function validate_response( $response ) { + return ! is_wp_error( $response ) && is_array( $response ) && isset( $response['http_response'] ); + } + + /** + * @return string + */ + private static function get_url_to_connect_server() { + return 'https://api.strategy11.com/'; + } + + /** + * @return array + */ + private static function build_headers_for_post() { + $password = self::maybe_get_pro_license(); + + if ( false === $password ) { + $password = 'lite_' . self::get_uuid(); + } + + $site_url = home_url(); + $site_url = self::maybe_fix_wpml_url( $site_url ); + // Remove protocol from url (our url cannot include the colon). + $site_url = preg_replace( '#^https?://#', '', $site_url ); + // Remove port from url (mostly helpful in development). + $site_url = preg_replace( '/:[0-9]+/', '', $site_url ); + $site_url = self::strip_lang_from_url( $site_url ); + + // $password is either a Pro license or a uuid (See FrmUsage::uuid). + return array( + 'Authorization' => 'Basic ' . base64_encode( $site_url . ':' . $password ), + ); + } + + /** + * Get a unique ID to use for connecting Lite users. + * + * @return string + */ + private static function get_uuid() { + $usage = new FrmUsage(); + return $usage->uuid(); + } + + /** + * WPML might add a language to the url. Don't send that to the server. + * + * @param string $url URL to strip language from. + * + * @return string + */ + private static function strip_lang_from_url( $url ) { + $split_on_language = explode( '/?lang=', $url ); + + if ( 2 === count( $split_on_language ) ) { + $url = $split_on_language[0]; + } + return $url; + } + + /** + * WPML alters the output of home_url. + * If it is active, use the WPML "absolute home" URL which is not modified. + * + * @param string $url URL to maybe fix. + * + * @return string + */ + private static function maybe_fix_wpml_url( $url ) { + if ( defined( 'ICL_SITEPRESS_VERSION' ) && ! ICL_PLUGIN_INACTIVE && class_exists( 'SitePress' ) ) { + global $wpml_url_converter; + $url = $wpml_url_converter->get_abs_home(); + } + return $url; + } + + /** + * Get a Pro license when Pro is active. + * Otherwise we'll use a uuid to support Lite. + * + * @return false|string + */ + private static function maybe_get_pro_license() { + if ( FrmAppHelper::pro_is_installed() ) { + $pro_license = FrmAddonsController::get_pro_license(); + + if ( $pro_license ) { + $password = $pro_license; + } + } + return ! empty( $password ) ? $password : false; + } + + /** + * Try to make sure the server time limit exceeds the request time limit. + * + * @param int $timeout seconds. + * + * @return void + */ + private static function try_to_extend_server_timeout( $timeout ) { + if ( function_exists( 'set_time_limit' ) ) { + set_time_limit( $timeout + 10 ); + } + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string + */ + private static function get_server_side_token_option_name( $mode = 'auto' ) { + return self::get_paypal_connect_option_name( 'server_password', $mode ); + } + + /** + * Generate a new client password for authenticating with Connect Service and save it locally as an option. + * + * @param string $mode 'live' or 'test'. + * + * @return string the client password. + */ + private static function generate_client_password( $mode ) { + $client_password = wp_generate_password(); + update_option( self::get_client_side_token_option_name( $mode ), $client_password, 'no' ); + return $client_password; + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string + */ + private static function get_client_side_token_option_name( $mode = 'auto' ) { + return self::get_paypal_connect_option_name( 'client_password', $mode ); + } + + /** + * @return string + */ + private static function get_mode_value() { + $settings = FrmPayPalLiteAppHelper::get_settings(); + return $settings->settings->test_mode ? 'test' : 'live'; + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return bool|string + */ + public static function get_merchant_id( $mode = 'auto' ) { + if ( 'auto' === $mode ) { + $mode = self::get_mode_value(); + } + return get_option( self::get_merchant_id_option_name( $mode ) ); + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string + */ + private static function get_merchant_id_option_name( $mode = 'auto' ) { + return self::get_paypal_connect_option_name( 'merchant_id', $mode ); + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string + */ + private static function get_location_id_option_name( $mode = 'auto' ) { + return self::get_paypal_connect_option_name( 'merchant_location_id', $mode ); + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string + */ + private static function get_merchant_currency_option_name( $mode = 'auto' ) { + return self::get_paypal_connect_option_name( 'merchant_currency', $mode ); + } + + /** + * @param string $key 'merchant_id', 'client_password', 'server_password'. + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string + */ + private static function get_paypal_connect_option_name( $key, $mode = 'auto' ) { + return 'frm_paypal_connect_' . $key . self::get_active_mode_option_name_suffix( $mode ); + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string either _test or _live. + */ + private static function get_active_mode_option_name_suffix( $mode = 'auto' ) { + if ( 'auto' !== $mode ) { + return '_' . $mode; + } + return '_' . FrmPayPalLiteAppHelper::active_mode(); + } + + public static function check_for_redirects() { + if ( self::user_landed_on_the_oauth_return_url() ) { + self::redirect_oauth(); + } + } + + /** + * @return bool + */ + private static function user_landed_on_the_oauth_return_url() { + return isset( $_GET['frm_paypal_api_return_oauth'] ); + } + + private static function redirect_oauth() { + $connected = self::check_server_for_oauth_merchant_id(); + wp_safe_redirect( self::get_url_for_paypal_settings( $connected ) ); + exit; + } + + /** + * @param bool $connected + * + * @return string + */ + private static function get_url_for_paypal_settings( $connected ) { + return admin_url( 'admin.php?page=formidable-settings&t=paypal_settings&connected=' . intval( $connected ) ); + } + + /** + * @return bool + */ + private static function check_server_for_oauth_merchant_id() { + $mode = 'test' === FrmAppHelper::simple_get( 'mode' ) ? 'test' : 'live'; + + if ( self::get_merchant_id( $mode ) ) { + // Do not allow for initialize if there is already a configured merchant id. + return false; + } + + $body = array( + 'server_password' => get_option( self::get_server_side_token_option_name( $mode ) ), + 'client_password' => get_option( self::get_client_side_token_option_name( $mode ) ), + 'frm_paypal_api_mode' => $mode, + ); + $data = self::post_to_connect_server( 'oauth_merchant_status', $body ); + + if ( is_object( $data ) && ! empty( $data->merchant_id ) ) { + update_option( self::get_merchant_id_option_name( $mode ), $data->merchant_id, 'no' ); + + $currency = self::get_merchant_currency( true, $mode ); + $location_id = self::get_location_id( true, $mode ); + + if ( $currency ) { + update_option( self::get_merchant_currency_option_name( $mode ), $currency, 'no' ); + } + + if ( $location_id ) { + update_option( self::get_location_id_option_name( $mode ), $location_id, 'no' ); + } + + FrmTransLiteAppController::install(); + + return true; + } + + return false; + } + + /** + * @param string $amount + * @param string $currency + * @param string $paypal_token + * @param string $verification_token + * @param string $description + * + * @return false|object + */ + public static function create_payment( $amount, $currency, $paypal_token, $verification_token, $description ) { + return self::post_with_authenticated_body( + 'create_payment', + array( + 'amount' => $amount, + 'currency' => $currency, + 'paypal_token' => $paypal_token, + 'verification_token' => $verification_token, + 'description' => $description, + ) + ); + } + + /** + * @param string $action + * @param array $additional_body + * + * @return false|object + */ + private static function post_with_authenticated_body( $action, $additional_body = array() ) { + $body = array_merge( self::get_standard_authenticated_body(), $additional_body ); + $response = self::post_to_connect_server( $action, $body ); + + if ( is_object( $response ) ) { + return $response; + } + + if ( is_array( $response ) ) { + // reformat empty arrays as empty objects + // if the response is an array, it's because it's empty. Everything with data is already an object. + return new stdClass(); + } + + if ( is_string( $response ) ) { + self::$latest_error_from_paypal_api = $response; + FrmTransLiteLog::log_message( 'PayPal API Error', $response ); + } else { + self::$latest_error_from_paypal_api = ''; + } + return false; + } + + /** + * @return array + */ + private static function get_standard_authenticated_body() { + $mode = self::get_mode_value_from_post(); + return array( + 'merchant_id' => get_option( self::get_merchant_id_option_name( $mode ) ), + 'server_password' => get_option( self::get_server_side_token_option_name( $mode ) ), + 'client_password' => get_option( self::get_client_side_token_option_name( $mode ) ), + ); + } + + /** + * Check $_POST for live or test mode value as it can be updated in real time from PayPal Settings and can be configured before the update is saved. + * + * @return string 'test' or 'live' + */ + private static function get_mode_value_from_post() { + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( empty( $_POST ) || ! array_key_exists( 'testMode', $_POST ) ) { + return FrmPayPalLiteAppHelper::active_mode(); + } + + $test_mode = FrmAppHelper::get_param( 'testMode', '', 'post', 'absint' ); + return $test_mode ? 'test' : 'live'; + } + + /** + * @return string|null + */ + public static function get_latest_error_from_paypal_api() { + return self::$latest_error_from_paypal_api; + } + + /** + * @param string $receipt_id + * + * @return false|object + */ + public static function refund_payment( $receipt_id ) { + return self::post_with_authenticated_body( 'refund_payment', array( 'receipt_id' => $receipt_id ) ); + } + + /** + * @param array $info + * + * @return false|object + */ + public static function create_subscription( $info ) { + return self::post_with_authenticated_body( 'create_subscription', compact( 'info' ) ); + } + + /** + * @param bool $force Whether to force refreshing the location id. + * @param string $mode Either 'auto', 'live', or 'test'. + * + * @return false|string + */ + public static function get_location_id( $force = false, $mode = 'auto' ) { + if ( ! $force ) { + $location_id = get_option( self::get_location_id_option_name( $mode ) ); + + if ( $location_id ) { + return $location_id; + } + } + + $request_body = array(); + + if ( 'auto' !== $mode ) { + $_POST['testMode'] = 'test' === $mode ? 1 : 0; + $request_body['frm_paypal_api_mode'] = $mode; + } + + $response = self::post_with_authenticated_body( 'get_location_id', $request_body ); + + if ( is_object( $response ) ) { + update_option( self::get_location_id_option_name( $mode ), $response->id, 'no' ); + return $response->id; + } + + return false; + } + + /** + * @return array + */ + public static function get_unprocessed_event_ids() { + $data = self::post_with_authenticated_body( 'get_unprocessed_event_ids' ); + + if ( false === $data || empty( $data->event_ids ) ) { + return array(); + } + return $data->event_ids; + } + + /** + * @param string $event_id + * + * @return false|object + */ + public static function get_event( $event_id ) { + $event = wp_cache_get( $event_id, 'frm_paypal' ); + + if ( is_object( $event ) ) { + return $event; + } + + $event = self::post_with_authenticated_body( 'get_event', compact( 'event_id' ) ); + + if ( false === $event || empty( $event->event ) ) { + return false; + } + + wp_cache_set( $event_id, $event->event, 'frm_paypal' ); + + return $event->event; + } + + /** + * @param string $event_id + * + * @return false|object + */ + public static function process_event( $event_id ) { + return self::post_with_authenticated_body( 'process_event', compact( 'event_id' ) ); + } + + /** + * @param string $payment_id + * + * @return false|object + */ + public static function get_payment( $payment_id ) { + return self::post_with_authenticated_body( 'get_payment', compact( 'payment_id' ) ); + } + + /** + * @param string $payment_id + * + * @return false|object + */ + public static function get_subscription_id_for_payment( $payment_id ) { + return self::post_with_authenticated_body( 'get_subscription_id_for_payment', compact( 'payment_id' ) ); + } + + /** + * @param string $subscription_id + * + * @return false|object + */ + public static function cancel_subscription( $subscription_id ) { + return self::post_with_authenticated_body( 'cancel_subscription', compact( 'subscription_id' ) ); + } + + public static function handle_disconnect() { + self::disconnect(); + self::reset_paypal_api_integration(); + wp_send_json_success(); + } + + /** + * @return false|object + */ + private static function disconnect() { + $additional_body = array( + 'frm_paypal_api_mode' => self::get_mode_value_from_post(), + ); + return self::post_with_authenticated_body( 'disconnect', $additional_body ); + } + + /** + * Delete every PayPal API option, calling when disconnecting. + * + * @return void + */ + public static function reset_paypal_api_integration() { + $mode = self::get_mode_value_from_post(); + delete_option( self::get_merchant_id_option_name( $mode ) ); + delete_option( self::get_server_side_token_option_name( $mode ) ); + delete_option( self::get_client_side_token_option_name( $mode ) ); + delete_option( self::get_merchant_currency_option_name( $mode ) ); + delete_option( self::get_location_id_option_name( $mode ) ); + } + + /** + * @param bool $force + * @param string $mode + * + * @return false|string + */ + public static function get_merchant_currency( $force = false, $mode = 'auto' ) { + if ( ! $force ) { + $currency = get_option( self::get_merchant_currency_option_name( $mode ) ); + + if ( $currency ) { + return $currency; + } + } + + $request_body = array(); + + if ( 'auto' !== $mode ) { + $_POST['testMode'] = 'test' === $mode ? 1 : 0; + $request_body['frm_paypal_api_mode'] = $mode; + } + + $response = self::post_with_authenticated_body( 'get_merchant_currency', $request_body ); + + if ( is_object( $response ) && ! empty( $response->currency ) ) { + update_option( self::get_merchant_currency_option_name( $mode ), $response->currency, 'no' ); + return $response->currency; + } + + return false; + } + + /** + * @since x.x + * + * @return bool + */ + public static function at_least_one_mode_is_setup() { + return self::get_merchant_id( 'test' ) || self::get_merchant_id( 'live' ); + } + + /** + * Verify a site identifier is a match. + */ + public static function verify() { + $option_name = 'frm_paypal_lite_last_verify_attempt'; + $last_request = get_option( $option_name ); + + if ( $last_request && $last_request > strtotime( '-1 day' ) ) { + wp_send_json_error( 'Too many requests' ); + } + + $site_identifier = FrmAppHelper::get_post_param( 'site_identifier' ); + $usage = new FrmUsage(); + $uuid = $usage->uuid(); + + update_option( $option_name, time() ); + + if ( $site_identifier === $uuid ) { + wp_send_json_success(); + } + wp_send_json_error(); + } + + /** + * @param string $subscription_id + * + * @return false|object + */ + public static function get_subscription( $subscription_id ) { + $response = self::post_with_authenticated_body( 'get_subscription', array( 'subscription_id' => $subscription_id ) ); + + if ( is_object( $response ) && is_object( $response->subscription ) ) { + return $response->subscription; + } + + return false; + } +} diff --git a/paypal/js/action.js b/paypal/js/action.js new file mode 100644 index 0000000000..ab976709fd --- /dev/null +++ b/paypal/js/action.js @@ -0,0 +1,3 @@ +( function() { + // TODO +}() ); diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js new file mode 100644 index 0000000000..ab976709fd --- /dev/null +++ b/paypal/js/frontend.js @@ -0,0 +1,3 @@ +( function() { + // TODO +}() ); diff --git a/paypal/js/settings.js b/paypal/js/settings.js new file mode 100644 index 0000000000..ab976709fd --- /dev/null +++ b/paypal/js/settings.js @@ -0,0 +1,3 @@ +( function() { + // TODO +}() ); diff --git a/paypal/models/FrmPayPalLiteSettings.php b/paypal/models/FrmPayPalLiteSettings.php new file mode 100644 index 0000000000..bdf5eea698 --- /dev/null +++ b/paypal/models/FrmPayPalLiteSettings.php @@ -0,0 +1,105 @@ +set_default_options(); + } + + /** + * @return string + */ + public function param() { + return 'paypal'; + } + + /** + * @return array + */ + public function default_options() { + return array( + 'test_mode' => 1, + ); + } + + /** + * @param mixed $settings + * + * @return void + */ + public function set_default_options( $settings = false ) { + $default_settings = $this->default_options(); + + if ( ! $settings ) { + $settings = $this->get_options(); + } elseif ( $settings === true ) { + $settings = new stdClass(); + } + + if ( ! isset( $this->settings ) ) { + $this->settings = new stdClass(); + } + + foreach ( $default_settings as $setting => $default ) { + if ( is_object( $settings ) && isset( $settings->{$setting} ) ) { + $this->settings->{$setting} = $settings->{$setting}; + } + + if ( ! isset( $this->settings->{$setting} ) ) { + $this->settings->{$setting} = $default; + } + } + } + + /** + * @return object + */ + public function get_options() { + $settings = get_option( 'frm_' . $this->param() . '_options' ); + + if ( is_object( $settings ) ) { + $this->set_default_options( $settings ); + } elseif ( $settings ) { + // Workaround for W3 total cache conflict. + $this->settings = unserialize( serialize( $settings ) ); + } else { + $this->set_default_options( true ); + $this->store(); + } + + return $this->settings; + } + + /** + * @param array $params + * + * @return void + */ + public function update( $params ) { + $settings = $this->default_options(); + + foreach ( $settings as $setting => $default ) { + if ( isset( $params[ 'frm_' . $this->param() . '_' . $setting ] ) ) { + $this->settings->{$setting} = trim( sanitize_text_field( $params[ 'frm_' . $this->param() . '_' . $setting ] ) ); + } + } + + $this->settings->test_mode = isset( $params[ 'frm_' . $this->param() . '_test_mode' ] ) ? absint( $params[ 'frm_' . $this->param() . '_test_mode' ] ) : 0; + } + + /** + * @return void + */ + public function store() { + // Save the posted value in the database. + update_option( 'frm_' . $this->param() . '_options', $this->settings ); + } +} diff --git a/paypal/views/settings/form.php b/paypal/views/settings/form.php new file mode 100644 index 0000000000..ee8215abb9 --- /dev/null +++ b/paypal/views/settings/form.php @@ -0,0 +1,6 @@ + Date: Thu, 4 Dec 2025 15:09:41 -0400 Subject: [PATCH 02/62] Add some front end JS to get it rendering an input --- classes/controllers/FrmHooksController.php | 2 + .../FrmPayPalLiteActionsController.php | 24 +- paypal/js/frontend.js | 317 +++++++++++++++++- 3 files changed, 337 insertions(+), 6 deletions(-) diff --git a/classes/controllers/FrmHooksController.php b/classes/controllers/FrmHooksController.php index 3441a1c095..c3d3db8342 100644 --- a/classes/controllers/FrmHooksController.php +++ b/classes/controllers/FrmHooksController.php @@ -116,6 +116,7 @@ public static function load_hooks() { FrmTransLiteHooksController::load_hooks(); FrmStrpLiteHooksController::load_hooks(); FrmSquareLiteHooksController::load_hooks(); + FrmPayPalLiteHooksController::load_hooks(); // GDPR add_filter( 'frm_is_field_required', 'FrmFieldGdpr::force_required_field', 10, 2 ); @@ -217,6 +218,7 @@ public static function load_admin_hooks() { FrmTransLiteHooksController::load_admin_hooks(); FrmStrpLiteHooksController::load_admin_hooks(); FrmSquareLiteHooksController::load_admin_hooks(); + FrmPayPalLiteHooksController::load_admin_hooks(); FrmSMTPController::load_hooks(); FrmOnboardingWizardController::load_admin_hooks(); FrmAddonsController::load_admin_hooks(); diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 3e1369603a..1a0aca8244 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -274,15 +274,28 @@ public static function load_scripts( $form_id ) { return; } + // TODO: Stop hard coding this. + $client_id = 'AV8DLwHFtnUai9Yuy8B5ocRSgtlCBiRAh6Vkl4vhgeuiKRLzilt-vzjd6O1tjIVI_5AiPG0H-HtBssrE'; + + // Build the PayPal SDK URL with required parameters. + $sdk_url = add_query_arg( + array( + 'client-id' => $client_id, + 'components' => 'card-fields', + 'intent' => 'capture', + ), + 'https://www.paypal.com/sdk/js' + ); + wp_register_script( - 'paypal', - FrmPayPalLiteAppHelper::active_mode() === 'live' ? 'https://web.paypal.com/v1/paypal.js' : 'https://sandbox.web.paypal.com/v1/paypal.js', + 'paypal-sdk', + $sdk_url, array(), - '1.0', + null, false ); - $dependencies = array( 'paypal', 'formidable' ); + $dependencies = array( 'paypal-sdk', 'formidable' ); $script_url = FrmPayPalLiteAppHelper::plugin_url() . 'js/frontend.js'; wp_enqueue_script( @@ -293,7 +306,8 @@ public static function load_scripts( $form_id ) { false ); - $paypal_vars = array( + $paypal_vars = array( + 'clientId' => $client_id, 'formId' => $form_id, 'nonce' => wp_create_nonce( 'frm_paypal_ajax' ), 'ajax' => esc_url_raw( FrmAppHelper::get_ajax_url() ), diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index ab976709fd..0cb8e12a95 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -1,3 +1,318 @@ ( function() { - // TODO + if ( ! window.frmPayPalVars ) { + return; + } + + const clientId = frmPayPalVars.clientId; + + // Track the state of the PayPal card fields + let cardFieldsValid = false; + let thisForm = null; + let running = 0; + let cardFieldsInstance = null; + + // Track the state of each field in the card form + const cardFields = { + number: false, + expiry: false, + cvv: false + }; + + /** + * Initialize PayPal Card Fields (Advanced Card Payments). + * + * @return {Promise} The card fields instance. + */ + async function initializeCardFields() { + const cardElement = document.querySelector( '.frm-card-element' ); + if ( ! cardElement ) { + return null; + } + + // Create the card fields container structure + cardElement.innerHTML = ` +
+
+
+ `; + + const cardFieldsConfig = { + createOrder: createOrder, + onApprove: onApprove, + onError: onError, + style: getCardFieldStyles() + }; + + const cardFields = window.paypal.CardFields( cardFieldsConfig ); + + // Check eligibility for card fields + if ( ! cardFields.isEligible() ) { + console.warn( 'PayPal Card Fields not eligible for this configuration' ); + return null; + } + + // Render individual card fields + cardFields.NumberField().render( '#frm-paypal-card-number' ); + cardFields.ExpiryField().render( '#frm-paypal-card-expiry' ); + cardFields.CVVField().render( '#frm-paypal-card-cvv' ); + + return cardFields; + } + + /** + * Get card field styles from localized vars or use defaults. + * + * @return {Object} Style configuration for PayPal card fields. + */ + function getCardFieldStyles() { + if ( frmPayPalVars.style ) { + return frmPayPalVars.style; + } + + return { + input: { + 'font-size': '16px', + 'font-family': 'inherit', + color: '#333' + }, + '.invalid': { + color: '#c00' + } + }; + } + + /** + * Create a PayPal order via AJAX. + * + * @return {Promise} The order ID. + */ + async function createOrder() { + const formData = new FormData( thisForm ); + formData.append( 'action', 'frm_paypal_create_order' ); + formData.append( 'nonce', frmPayPalVars.nonce ); + + const response = await fetch( frmPayPalVars.ajax, { + method: 'POST', + body: formData + } ); + + if ( ! response.ok ) { + throw new Error( 'Failed to create PayPal order' ); + } + + const data = await response.json(); + + if ( ! data.success || ! data.data.orderID ) { + throw new Error( data.data || 'Failed to create PayPal order' ); + } + + return data.data.orderID; + } + + /** + * Handle approved payment. + * + * @param {Object} data The approval data containing orderID. + */ + async function onApprove( data ) { + // Add the order ID to the form + const orderInput = document.createElement( 'input' ); + orderInput.type = 'hidden'; + orderInput.name = 'paypal_order_id'; + orderInput.value = data.orderID; + thisForm.appendChild( orderInput ); + + // Submit the form + if ( typeof frmFrontForm.submitFormManual === 'function' ) { + frmFrontForm.submitFormManual( null, thisForm ); + } else { + thisForm.submit(); + } + } + + /** + * Handle payment errors. + * + * @param {Error} err The error object. + */ + function onError( err ) { + running--; + if ( running === 0 && thisForm ) { + enableSubmit(); + } + displayPaymentFailure( err.message || 'Payment failed. Please try again.' ); + } + + /** + * Enable the submit button for the form. + */ + function enableSubmit() { + if ( running > 0 ) { + return; + } + + thisForm.classList.add( 'frm_loading_form' ); + frmFrontForm.removeSubmitLoading( jQuery( thisForm ), 'enable', 0 ); + + // Trigger custom event for other scripts to hook into + const event = new CustomEvent( 'frmPayPalLiteEnableSubmit', { + detail: { form: thisForm } + } ); + document.dispatchEvent( event ); + } + + /** + * Disable submit button for a target form. + * + * @param {Element} form + * @return {void} + */ + function disableSubmit( form ) { + jQuery( form ).find( 'input[type="submit"],input[type="button"],button[type="submit"]' ).not( '.frm_prev_page' ).attr( 'disabled', 'disabled' ); + + // Trigger custom event for other scripts to hook into + const event = new CustomEvent( 'frmPayPalLiteDisableSubmit', { + detail: { form: form } + } ); + document.dispatchEvent( event ); + } + + /** + * Display an error message in the payment form. + * + * @param {string} errorMessage + * @return {void} + */ + function displayPaymentFailure( errorMessage ) { + if ( ! thisForm ) { + return; + } + + const statusContainer = thisForm.querySelector( '.frm-card-errors' ); + if ( statusContainer ) { + statusContainer.textContent = errorMessage; + statusContainer.style.display = 'block'; + } + } + + /** + * Clear error messages. + */ + function clearErrors() { + if ( ! thisForm ) { + return; + } + + const statusContainer = thisForm.querySelector( '.frm-card-errors' ); + if ( statusContainer ) { + statusContainer.textContent = ''; + statusContainer.style.display = 'none'; + } + } + + /** + * Validate the form before submission. + * + * @param {Element} form + * @return {boolean} True if valid. + */ + function validateFormSubmit( form ) { + if ( typeof frmFrontForm.validateFormSubmit !== 'function' ) { + return true; + } + + const errors = frmFrontForm.validateFormSubmit( form ); + const keys = Object.keys( errors ); + + if ( 1 === keys.length && errors[ keys[ 0 ] ] === '' ) { + // Pop the empty error that gets added by invisible recaptcha. + keys.pop(); + } + + return 0 === keys.length; + } + + /** + * Handle form submission with card fields. + * + * @param {Event} event + */ + async function handleCardSubmission( event ) { + event.preventDefault(); + event.stopPropagation(); + + clearErrors(); + + // Validate the form first + thisForm.classList.add( 'frm_js_validate' ); + if ( ! validateFormSubmit( thisForm ) ) { + return; + } + + // Increment running counter and disable the submit button + running++; + disableSubmit( thisForm ); + + try { + // Submit the card fields - this triggers createOrder and onApprove + await cardFieldsInstance.submit(); + } catch ( err ) { + running--; + if ( running === 0 && thisForm ) { + enableSubmit(); + } + displayPaymentFailure( err.message || 'Payment failed. Please try again.' ); + } + } + + /** + * Initialize PayPal integration. + */ + async function paypalInit() { + // Find the form containing the PayPal payment element + const cardContainer = document.querySelector( '.frm-card-element' ); + if ( ! cardContainer ) { + return; + } + + thisForm = cardContainer.closest( 'form' ); + if ( ! thisForm ) { + return; + } + + // Initially disable the submit button until PayPal is ready + disableSubmit( thisForm ); + + try { + cardFieldsInstance = await initializeCardFields(); + + if ( ! cardFieldsInstance ) { + displayPaymentFailure( 'PayPal Card Fields could not be initialized.' ); + return; + } + + // Enable submit once card fields are ready + enableSubmit(); + + // Add event listener for form submission + thisForm.addEventListener( 'submit', handleCardSubmission ); + + } catch ( e ) { + console.error( 'Initializing PayPal Card Fields failed', e ); + displayPaymentFailure( 'Failed to initialize payment form.' ); + } + } + + document.addEventListener( 'DOMContentLoaded', async function() { + if ( ! window.paypal ) { + console.error( 'PayPal JS SDK failed to load properly' ); + return; + } + + paypalInit(); + + jQuery( document ).on( 'frmPageChanged', function() { + paypalInit(); + } ); + } ); }() ); From 2fab9647efd35244261d5315fc26ce411221c62a Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Mon, 8 Dec 2025 10:48:48 -0400 Subject: [PATCH 03/62] Work in progress adding support for creating orders --- .../FrmPayPalLiteActionsController.php | 3 +- .../FrmPayPalLiteAppController.php | 15 ++ .../FrmPayPalLiteHooksController.php | 6 +- paypal/helpers/FrmPayPalLiteConnectHelper.php | 156 ++---------------- 4 files changed, 34 insertions(+), 146 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 1a0aca8244..35481ba27c 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -274,7 +274,8 @@ public static function load_scripts( $form_id ) { return; } - // TODO: Stop hard coding this. + // TODO: Stop hard coding this here. + // It should be in a function. $client_id = 'AV8DLwHFtnUai9Yuy8B5ocRSgtlCBiRAh6Vkl4vhgeuiKRLzilt-vzjd6O1tjIVI_5AiPG0H-HtBssrE'; // Build the PayPal SDK URL with required parameters. diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php index c5f5764fce..bbfcf6bf85 100644 --- a/paypal/controllers/FrmPayPalLiteAppController.php +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -63,4 +63,19 @@ public static function handle_disconnect() { FrmPayPalLiteConnectHelper::handle_disconnect(); wp_send_json_success(); } + + /** + * Create a PayPal order via AJAX. + * + * @return void + */ + public static function create_order() { + $order_id = FrmPayPalLiteConnectHelper::create_order(); + + if ( false === $order_id ) { + wp_send_json_error( 'Failed to create PayPal order' ); + } + + wp_send_json_success( array( 'orderID' => $order_id ) ); + } } diff --git a/paypal/controllers/FrmPayPalLiteHooksController.php b/paypal/controllers/FrmPayPalLiteHooksController.php index 2633495860..68af499031 100644 --- a/paypal/controllers/FrmPayPalLiteHooksController.php +++ b/paypal/controllers/FrmPayPalLiteHooksController.php @@ -39,14 +39,14 @@ private static function load_ajax_hooks() { add_action( 'wp_ajax_frm_paypal_oauth', 'FrmPayPalLiteAppController::handle_oauth' ); add_action( 'wp_ajax_frm_paypal_disconnect', 'FrmPayPalLiteAppController::handle_disconnect' ); - add_action( 'wp_ajax_frm_verify_buyer', 'FrmPayPalLiteAppController::verify_buyer' ); - add_action( 'wp_ajax_nopriv_frm_verify_buyer', 'FrmPayPalLiteAppController::verify_buyer' ); - $frm_paypal_events_controller = new FrmPayPalLiteEventsController(); add_action( 'wp_ajax_nopriv_frm_paypal_process_events', array( &$frm_paypal_events_controller, 'process_events' ) ); add_action( 'wp_ajax_frm_paypal_process_events', array( &$frm_paypal_events_controller, 'process_events' ) ); // Verify PayPal Lite sites. add_action( 'wp_ajax_nopriv_frm_paypal_lite_verify', 'FrmPayPalLiteConnectHelper::verify' ); + + add_action( 'wp_ajax_frm_paypal_create_order', 'FrmPayPalLiteAppController::create_order' ); + add_action( 'wp_ajax_nopriv_frm_paypal_create_order', 'FrmPayPalLiteAppController::create_order' ); } } diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index c699532aa3..6a383118cc 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -197,6 +197,10 @@ private static function post_to_connect_server( $action, $additional_body = arra $response = wp_remote_post( $url, $args ); if ( ! self::validate_response( $response ) ) { + ob_start(); + var_dump($response); + $output = ob_get_clean(); + return $output; return 'Response from server is invalid'; } @@ -206,6 +210,10 @@ private static function post_to_connect_server( $action, $additional_body = arra if ( ! empty( $body->data ) && is_string( $body->data ) ) { return $body->data; } + ob_start(); + var_dump($body); + $output = ob_get_clean(); + return $output; return 'Response from server was not successful'; } @@ -236,7 +244,8 @@ private static function validate_response( $response ) { * @return string */ private static function get_url_to_connect_server() { - return 'https://api.strategy11.com/'; + // return 'https://api.strategy11.com/'; + return 'https://dev-site.local/'; } /** @@ -503,28 +512,6 @@ private static function check_server_for_oauth_merchant_id() { return false; } - /** - * @param string $amount - * @param string $currency - * @param string $paypal_token - * @param string $verification_token - * @param string $description - * - * @return false|object - */ - public static function create_payment( $amount, $currency, $paypal_token, $verification_token, $description ) { - return self::post_with_authenticated_body( - 'create_payment', - array( - 'amount' => $amount, - 'currency' => $currency, - 'paypal_token' => $paypal_token, - 'verification_token' => $verification_token, - 'description' => $description, - ) - ); - } - /** * @param string $action * @param array $additional_body @@ -588,56 +575,6 @@ public static function get_latest_error_from_paypal_api() { return self::$latest_error_from_paypal_api; } - /** - * @param string $receipt_id - * - * @return false|object - */ - public static function refund_payment( $receipt_id ) { - return self::post_with_authenticated_body( 'refund_payment', array( 'receipt_id' => $receipt_id ) ); - } - - /** - * @param array $info - * - * @return false|object - */ - public static function create_subscription( $info ) { - return self::post_with_authenticated_body( 'create_subscription', compact( 'info' ) ); - } - - /** - * @param bool $force Whether to force refreshing the location id. - * @param string $mode Either 'auto', 'live', or 'test'. - * - * @return false|string - */ - public static function get_location_id( $force = false, $mode = 'auto' ) { - if ( ! $force ) { - $location_id = get_option( self::get_location_id_option_name( $mode ) ); - - if ( $location_id ) { - return $location_id; - } - } - - $request_body = array(); - - if ( 'auto' !== $mode ) { - $_POST['testMode'] = 'test' === $mode ? 1 : 0; - $request_body['frm_paypal_api_mode'] = $mode; - } - - $response = self::post_with_authenticated_body( 'get_location_id', $request_body ); - - if ( is_object( $response ) ) { - update_option( self::get_location_id_option_name( $mode ), $response->id, 'no' ); - return $response->id; - } - - return false; - } - /** * @return array */ @@ -682,33 +619,6 @@ public static function process_event( $event_id ) { return self::post_with_authenticated_body( 'process_event', compact( 'event_id' ) ); } - /** - * @param string $payment_id - * - * @return false|object - */ - public static function get_payment( $payment_id ) { - return self::post_with_authenticated_body( 'get_payment', compact( 'payment_id' ) ); - } - - /** - * @param string $payment_id - * - * @return false|object - */ - public static function get_subscription_id_for_payment( $payment_id ) { - return self::post_with_authenticated_body( 'get_subscription_id_for_payment', compact( 'payment_id' ) ); - } - - /** - * @param string $subscription_id - * - * @return false|object - */ - public static function cancel_subscription( $subscription_id ) { - return self::post_with_authenticated_body( 'cancel_subscription', compact( 'subscription_id' ) ); - } - public static function handle_disconnect() { self::disconnect(); self::reset_paypal_api_integration(); @@ -739,38 +649,6 @@ public static function reset_paypal_api_integration() { delete_option( self::get_location_id_option_name( $mode ) ); } - /** - * @param bool $force - * @param string $mode - * - * @return false|string - */ - public static function get_merchant_currency( $force = false, $mode = 'auto' ) { - if ( ! $force ) { - $currency = get_option( self::get_merchant_currency_option_name( $mode ) ); - - if ( $currency ) { - return $currency; - } - } - - $request_body = array(); - - if ( 'auto' !== $mode ) { - $_POST['testMode'] = 'test' === $mode ? 1 : 0; - $request_body['frm_paypal_api_mode'] = $mode; - } - - $response = self::post_with_authenticated_body( 'get_merchant_currency', $request_body ); - - if ( is_object( $response ) && ! empty( $response->currency ) ) { - update_option( self::get_merchant_currency_option_name( $mode ), $response->currency, 'no' ); - return $response->currency; - } - - return false; - } - /** * @since x.x * @@ -804,17 +682,11 @@ public static function verify() { } /** - * @param string $subscription_id + * Create a PayPal order. * - * @return false|object + * @return false|string */ - public static function get_subscription( $subscription_id ) { - $response = self::post_with_authenticated_body( 'get_subscription', array( 'subscription_id' => $subscription_id ) ); - - if ( is_object( $response ) && is_object( $response->subscription ) ) { - return $response->subscription; - } - - return false; + public static function create_order() { + return self::post_with_authenticated_body( 'create_order' ); } } From d7bb8fe6b602575d9cb8540bf9b7ce30e49dffc6 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 10 Dec 2025 17:04:36 -0400 Subject: [PATCH 04/62] Get paypal onboarding working --- .../FrmPayPalLiteActionsController.php | 2 +- .../FrmPayPalLiteAppController.php | 10 +++-- paypal/helpers/FrmPayPalLiteConnectHelper.php | 15 +------- paypal/js/frontend.js | 5 ++- paypal/js/settings.js | 38 ++++++++++++++++++- .../FrmSquareLiteActionsController.php | 2 +- .../FrmStrpLiteActionsController.php | 2 +- 7 files changed, 53 insertions(+), 21 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 35481ba27c..449264249f 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -115,7 +115,7 @@ public static function trigger_gateway( $action, $entry, $form ) { } if ( ! self::paypal_is_configured() ) { - $response['error'] = __( 'There was a problem communicating with PayPal. Please try again.', 'formidable' ); + $response['error'] = __( 'PayPal still needs to be configured.', 'formidable' ); return $response; } diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php index bbfcf6bf85..07f7b90fe8 100644 --- a/paypal/controllers/FrmPayPalLiteAppController.php +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -70,12 +70,16 @@ public static function handle_disconnect() { * @return void */ public static function create_order() { - $order_id = FrmPayPalLiteConnectHelper::create_order(); + $order_response = FrmPayPalLiteConnectHelper::create_order(); - if ( false === $order_id ) { + if ( false === $order_response ) { wp_send_json_error( 'Failed to create PayPal order' ); } - wp_send_json_success( array( 'orderID' => $order_id ) ); + if ( ! isset( $order_response->order_id ) ) { + wp_send_json_error( 'Failed to create PayPal order' ); + } + + wp_send_json_success( array( 'orderID' => $order_response->order_id ) ); } } diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index 6a383118cc..3c12ef17e7 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -484,8 +484,8 @@ private static function check_server_for_oauth_merchant_id() { } $body = array( - 'server_password' => get_option( self::get_server_side_token_option_name( $mode ) ), - 'client_password' => get_option( self::get_client_side_token_option_name( $mode ) ), + 'server_password' => get_option( self::get_server_side_token_option_name( $mode ) ), + 'client_password' => get_option( self::get_client_side_token_option_name( $mode ) ), 'frm_paypal_api_mode' => $mode, ); $data = self::post_to_connect_server( 'oauth_merchant_status', $body ); @@ -493,17 +493,6 @@ private static function check_server_for_oauth_merchant_id() { if ( is_object( $data ) && ! empty( $data->merchant_id ) ) { update_option( self::get_merchant_id_option_name( $mode ), $data->merchant_id, 'no' ); - $currency = self::get_merchant_currency( true, $mode ); - $location_id = self::get_location_id( true, $mode ); - - if ( $currency ) { - update_option( self::get_merchant_currency_option_name( $mode ), $currency, 'no' ); - } - - if ( $location_id ) { - update_option( self::get_location_id_option_name( $mode ), $location_id, 'no' ); - } - FrmTransLiteAppController::install(); return true; diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index 0cb8e12a95..78047cda14 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -10,6 +10,7 @@ let thisForm = null; let running = 0; let cardFieldsInstance = null; + let submitEvent = null; // Track the state of each field in the card form const cardFields = { @@ -124,7 +125,7 @@ // Submit the form if ( typeof frmFrontForm.submitFormManual === 'function' ) { - frmFrontForm.submitFormManual( null, thisForm ); + frmFrontForm.submitFormManual( submitEvent, thisForm ); } else { thisForm.submit(); } @@ -241,6 +242,8 @@ event.preventDefault(); event.stopPropagation(); + submitEvent = event; + clearErrors(); // Validate the form first diff --git a/paypal/js/settings.js b/paypal/js/settings.js index ab976709fd..11875b0e77 100644 --- a/paypal/js/settings.js +++ b/paypal/js/settings.js @@ -1,3 +1,39 @@ ( function() { - // TODO + const buttons = document.querySelectorAll( '.frm-connect-paypal-with-oauth' ); + buttons.forEach( function( button ) { + button.addEventListener( 'click', function( e ) { + e.preventDefault(); + + const mode = button.dataset.mode; + const formData = new FormData(); + formData.append( 'mode', mode ); + frmDom.ajax.doJsonPost( 'paypal_oauth', formData ).then( + function( response ) { + if ( 'undefined' !== typeof response.redirect_url ) { + window.location = response.redirect_url; + } + } + ); + } ); + } ); + + document.addEventListener( + 'click', + function( event ) { + if ( ! event.target.id.startsWith( 'frm_disconnect_paypal_' ) ) { + return; + } + + event.preventDefault(); + const formData = new FormData(); + formData.append( 'testMode', 'test' === event.target.id.replace( 'frm_disconnect_paypal_', '' ) ? 1 : 0 ); + frmDom.ajax.doJsonPost( 'paypal_disconnect', formData ).then( + function( response ) { + if ( 'undefined' !== typeof response.success && response.success ) { + window.location.reload(); + } + } + ); + } + ); }() ); diff --git a/square/controllers/FrmSquareLiteActionsController.php b/square/controllers/FrmSquareLiteActionsController.php index b8e455cc29..d8b271595c 100644 --- a/square/controllers/FrmSquareLiteActionsController.php +++ b/square/controllers/FrmSquareLiteActionsController.php @@ -115,7 +115,7 @@ public static function trigger_gateway( $action, $entry, $form ) { } if ( ! self::square_is_configured() ) { - $response['error'] = __( 'There was a problem communicating with Square. Please try again.', 'formidable' ); + $response['error'] = __( 'Square still needs to be configured.', 'formidable' ); return $response; } diff --git a/stripe/controllers/FrmStrpLiteActionsController.php b/stripe/controllers/FrmStrpLiteActionsController.php index ea04a3d8dd..c477ff64c1 100644 --- a/stripe/controllers/FrmStrpLiteActionsController.php +++ b/stripe/controllers/FrmStrpLiteActionsController.php @@ -127,7 +127,7 @@ public static function trigger_gateway( $action, $entry, $form ) { } if ( ! self::stripe_is_configured() ) { - $response['error'] = __( 'There was a problem communicating with Stripe. Please try again.', 'formidable' ); + $response['error'] = __( 'Stripe still needs to be configured.', 'formidable' ); return $response; } From 2fc40673ac432b531146021a0d3ae770b57b9464 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 11 Dec 2025 15:56:34 -0400 Subject: [PATCH 05/62] Work on capturing the paypal payment order --- .../FrmPayPalLiteActionsController.php | 15 +++++++++++++-- paypal/controllers/FrmPayPalLiteAppController.php | 2 ++ paypal/helpers/FrmPayPalLiteConnectHelper.php | 11 ++++++++++- paypal/js/frontend.js | 15 ++++++++++----- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 449264249f..c5b9f48bf4 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -145,7 +145,19 @@ public static function trigger_gateway( $action, $entry, $form ) { * @return string|true string on error, true on success. */ private static function trigger_one_time_payment( $atts ) { - return 'Payments are not yet implemented for PayPal.'; + $paypal_order_id = FrmAppHelper::get_post_param( 'paypal_order_id' ); + + if ( empty( $paypal_order_id ) ) { + return 'No PayPal order ID found.'; + } + + $response = FrmPayPalLiteConnectHelper::capture_order( $paypal_order_id ); + + if ( false === $response ) { + return 'Failed to confirm order.'; + } + + return true; } /** @@ -156,7 +168,6 @@ private static function trigger_one_time_payment( $atts ) { * @return bool|string True on success, error message on failure */ private static function trigger_recurring_payment( $atts ) { - return 'Recurring payments are not yet implemented for PayPal.'; } diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php index 07f7b90fe8..75d2f6d5f8 100644 --- a/paypal/controllers/FrmPayPalLiteAppController.php +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -70,6 +70,8 @@ public static function handle_disconnect() { * @return void */ public static function create_order() { + check_ajax_referer( 'frm_paypal_ajax', 'nonce' ); + $order_response = FrmPayPalLiteConnectHelper::create_order(); if ( false === $order_response ) { diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index 3c12ef17e7..a94f1fe4eb 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -673,9 +673,18 @@ public static function verify() { /** * Create a PayPal order. * - * @return false|string + * @return false|object */ public static function create_order() { return self::post_with_authenticated_body( 'create_order' ); } + + /** + * @param string $order_id + * + * @return false|object + */ + public static function capture_order( $order_id ) { + return self::post_with_authenticated_body( 'capture_order', compact( 'order_id' ) ); + } } diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index 78047cda14..e95128030e 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -87,11 +87,16 @@ * * @return {Promise} The order ID. */ - async function createOrder() { + async function createOrder( data ) { const formData = new FormData( thisForm ); formData.append( 'action', 'frm_paypal_create_order' ); formData.append( 'nonce', frmPayPalVars.nonce ); + // Remove a few fields so form validation does not incorrectly trigger. + formData.delete( 'frm_action' ); + formData.delete( 'form_key' ); + formData.delete( 'item_key' ); + const response = await fetch( frmPayPalVars.ajax, { method: 'POST', body: formData @@ -101,13 +106,13 @@ throw new Error( 'Failed to create PayPal order' ); } - const data = await response.json(); + const orderData = await response.json(); - if ( ! data.success || ! data.data.orderID ) { - throw new Error( data.data || 'Failed to create PayPal order' ); + if ( ! orderData.success || ! orderData.data.orderID ) { + throw new Error( orderData.data || 'Failed to create PayPal order' ); } - return data.data.orderID; + return orderData.data.orderID; } /** From 1ccc3b645af3bc99e52e6b0d0961ece4473c3d75 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 11 Dec 2025 16:58:28 -0400 Subject: [PATCH 06/62] Overwrite paypal section and render paypal section after the new settings --- paypal/controllers/FrmPayPalLiteActionsController.php | 7 +++++++ paypal/controllers/FrmPayPalLiteHooksController.php | 4 +++- paypal/helpers/FrmPayPalLiteConnectHelper.php | 2 +- paypal/views/settings/form.php | 9 +++++++++ square/helpers/FrmSquareLiteConnectHelper.php | 2 +- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index c5b9f48bf4..3c3c2c94d5 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -153,6 +153,13 @@ private static function trigger_one_time_payment( $atts ) { $response = FrmPayPalLiteConnectHelper::capture_order( $paypal_order_id ); + /* + echo '
';
+		var_dump( $response );
+		echo '
'; + die(); + */ + if ( false === $response ) { return 'Failed to confirm order.'; } diff --git a/paypal/controllers/FrmPayPalLiteHooksController.php b/paypal/controllers/FrmPayPalLiteHooksController.php index 68af499031..dcd7153820 100644 --- a/paypal/controllers/FrmPayPalLiteHooksController.php +++ b/paypal/controllers/FrmPayPalLiteHooksController.php @@ -24,7 +24,9 @@ public static function load_hooks() { * @return void */ public static function load_admin_hooks() { - add_filter( 'frm_add_settings_section', 'FrmPayPalLiteSettingsController::add_settings_section' ); + // Use 99 so we overwrite the PayPal add-on settings. + // These are called explicitly below the Lite PayPal settings. + add_filter( 'frm_add_settings_section', 'FrmPayPalLiteSettingsController::add_settings_section', 99 ); add_action( 'frm_update_settings', 'FrmPayPalLiteSettingsController::process_form' ); if ( defined( 'DOING_AJAX' ) ) { diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index a94f1fe4eb..39cfcca253 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -67,7 +67,7 @@ public static function render_settings_container() { private static function render_settings_for_mode( $mode ) { ?>
-
+
+
+
+ +
+
-
+
Date: Thu, 18 Dec 2025 12:17:18 -0400 Subject: [PATCH 07/62] Set a layout, set styles to try to look closer to Formidable fields --- css/custom_theme.css.php | 2 +- .../FrmPayPalLiteActionsController.php | 137 ++++++++++++++++++ paypal/js/frontend.js | 20 +-- 3 files changed, 145 insertions(+), 14 deletions(-) diff --git a/css/custom_theme.css.php b/css/custom_theme.css.php index fae5897b40..a5a7e22c46 100644 --- a/css/custom_theme.css.php +++ b/css/custom_theme.css.php @@ -1624,6 +1624,6 @@ .frm-card-errors:empty { margin: 0; } - 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 ), ); wp_localize_script( 'formidable-paypal', 'frmPayPalVars', $paypal_vars ); } + /** + * 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'], + 'background-color' => $settings['bg_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'], + ), + 'input:focus' => array( + 'background-color' => $settings['bg_color_active'], + ), + '.invalid' => array( + 'color' => $settings['border_color_error'], + ), + ); + + if ( ! empty( $settings['font'] ) ) { + $style['input']['font-family'] = $settings['font']; + } + + /** + * Filter the PayPal card field styles. + * + * @since x.x + * + * @param array $style + * @param array $settings + * @param int $form_id + */ + return apply_filters( 'frm_paypal_style', $style, $settings, $form_id ); + } + + /** + * 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 x.x + * + * @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 x.x + * + * @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 x.x + * + * @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. diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index e95128030e..c1ddeac62d 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -30,11 +30,14 @@ return null; } + cardElement.classList.add( 'frm_grid_container' ); + // Create the card fields container structure + // TODO: Make these IDs unique. cardElement.innerHTML = ` -
-
-
+
+
+
`; const cardFieldsConfig = { @@ -70,16 +73,7 @@ return frmPayPalVars.style; } - return { - input: { - 'font-size': '16px', - 'font-family': 'inherit', - color: '#333' - }, - '.invalid': { - color: '#c00' - } - }; + return {}; } /** From 1abf3344185b934836c8f5fbfb88c848d636f408 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 18 Dec 2025 12:30:34 -0400 Subject: [PATCH 08/62] Sync the submit button with the PayPal state --- paypal/js/frontend.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index c1ddeac62d..b820e0b5f1 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -3,6 +3,9 @@ return; } + // TODO: + // Only enable the submit button when we check the state for isFormValid = true. + const clientId = frmPayPalVars.clientId; // Track the state of the PayPal card fields @@ -40,13 +43,28 @@
`; + thisForm = cardElement.closest( 'form' ); + const cardFieldsConfig = { createOrder: createOrder, onApprove: onApprove, onError: onError, - style: getCardFieldStyles() + style: getCardFieldStyles(), + inputEvents: { + onChange: ( data) => { + cardFieldsValid = data.isFormValid; + + if ( cardFieldsValid ) { + enableSubmit(); + } else { + disableSubmit( thisForm ); + } + } + } }; + disableSubmit( thisForm ); + const cardFields = window.paypal.CardFields( cardFieldsConfig ); // Check eligibility for card fields @@ -293,9 +311,6 @@ return; } - // Enable submit once card fields are ready - enableSubmit(); - // Add event listener for form submission thisForm.addEventListener( 'submit', handleCardSubmission ); From 94a3c68ef90c10b7171a7bb0c163a3cfa507f91e Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 18 Dec 2025 12:33:22 -0400 Subject: [PATCH 09/62] Add the section title for the legacy paypal settings --- paypal/views/settings/form.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paypal/views/settings/form.php b/paypal/views/settings/form.php index 0b9185421c..f2fca776db 100644 --- a/paypal/views/settings/form.php +++ b/paypal/views/settings/form.php @@ -5,9 +5,11 @@ FrmPayPalLiteConnectHelper::render_settings_container(); +// When the PayPal add-on is active, we want to show the add-on settings here. if ( is_callable( array( 'FrmPaymentSettingsController', 'route' ) ) ) { ?>
+

From eba69dc1744c75531fbd8300ca2e5f70033f550c Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 18 Dec 2025 13:31:51 -0400 Subject: [PATCH 10/62] Support passing amount and currency to paypal --- .../FrmPayPalLiteAppController.php | 81 ++++++++++++++++++- paypal/helpers/FrmPayPalLiteConnectHelper.php | 4 +- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php index 75d2f6d5f8..31ccec6917 100644 --- a/paypal/controllers/FrmPayPalLiteAppController.php +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -72,7 +72,25 @@ public static function handle_disconnect() { public static function create_order() { check_ajax_referer( 'frm_paypal_ajax', 'nonce' ); - $order_response = FrmPayPalLiteConnectHelper::create_order(); + $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' ); + + if ( ! $form_id ) { + wp_send_json_error( __( 'Invalid form ID', 'formidable' ) ); + } + + $actions = FrmPayPalLiteActionsController::get_actions_before_submit( $form_id ); + + if ( empty( $actions ) ) { + wp_send_json_error( __( 'No PayPal actions found for this form', 'formidable' ) ); + } + + $action = reset( $actions ); + $amount = self::get_amount_value_for_verification( $action ); + + // PayPal expects the amount in a format like 10.00, so format it. + $amount = number_format( floatval( $amount ), 2 ); + $currency = strtoupper( $action->post_content['currency'] ); + $order_response = FrmPayPalLiteConnectHelper::create_order( $amount, $currency ); if ( false === $order_response ) { wp_send_json_error( 'Failed to create PayPal order' ); @@ -84,4 +102,65 @@ public static function create_order() { wp_send_json_success( array( 'orderID' => $order_response->order_id ) ); } + + /** + * Get the amount value for verification. + * + * @param WP_Post $action + * + * @return string + */ + private static function get_amount_value_for_verification( $action ) { + $amount = $action->post_content['amount']; + + if ( strpos( $amount, '[' ) === false ) { + return $amount; + } + + $form = FrmForm::getOne( $action->menu_order ); + + if ( ! $form ) { + return $amount; + } + + // Update amount based on field shortcodes. + $entry = self::generate_false_entry(); + + return FrmPayPalLiteActionsController::prepare_amount( $amount, compact( 'form', 'entry', 'action' ) ); + } + + /** + * Create an entry object with posted values. + * + * @since x.x + * + * @return stdClass + */ + private static function generate_false_entry() { + $entry = new stdClass(); + $entry->post_id = 0; + $entry->id = 0; + $entry->item_key = ''; + $entry->metas = array(); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing + foreach ( $_POST as $k => $v ) { + $k = sanitize_text_field( stripslashes( $k ) ); + $v = wp_unslash( $v ); + + if ( $k === 'item_meta' ) { + if ( is_array( $v ) ) { + foreach ( $v as $f => $value ) { + FrmAppHelper::sanitize_value( 'wp_kses_post', $value ); + $entry->metas[ absint( $f ) ] = $value; + } + } + } else { + FrmAppHelper::sanitize_value( 'wp_kses_post', $v ); + $entry->{$k} = $v; + } + } + + return $entry; + } } diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index 39cfcca253..f65e3fe6b3 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -675,8 +675,8 @@ public static function verify() { * * @return false|object */ - public static function create_order() { - return self::post_with_authenticated_body( 'create_order' ); + public static function create_order( $amount, $currency ) { + return self::post_with_authenticated_body( 'create_order', compact( 'amount', 'currency' ) ); } /** From 0609fb7de5aa07a5dae1b42870937237a344059b Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 18 Dec 2025 13:44:28 -0400 Subject: [PATCH 11/62] Create the payment record --- .../FrmPayPalLiteActionsController.php | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index f2679350f5..7d051b4b26 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -153,20 +153,55 @@ private static function trigger_one_time_payment( $atts ) { $response = FrmPayPalLiteConnectHelper::capture_order( $paypal_order_id ); - /* - echo '
';
-		var_dump( $response );
-		echo '
'; - die(); - */ - if ( false === $response ) { return 'Failed to confirm order.'; } + if ( ! isset( $response->status ) || $response->status !== 'COMPLETED' ) { + return 'Failed to capture order.'; + } + + // Create a payment record. + $atts['status'] = 'complete'; + $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' ) ); + return true; } + /** + * 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. * From d9c3b9cea5309871f96fbaf7344b16dda0850865 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 18 Dec 2025 15:17:01 -0400 Subject: [PATCH 12/62] Make sure number_format does not include comma --- paypal/controllers/FrmPayPalLiteAppController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php index 31ccec6917..31b7518393 100644 --- a/paypal/controllers/FrmPayPalLiteAppController.php +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -88,7 +88,7 @@ public static function create_order() { $amount = self::get_amount_value_for_verification( $action ); // PayPal expects the amount in a format like 10.00, so format it. - $amount = number_format( floatval( $amount ), 2 ); + $amount = number_format( floatval( $amount ), 2, '.', '' ); $currency = strtoupper( $action->post_content['currency'] ); $order_response = FrmPayPalLiteConnectHelper::create_order( $amount, $currency ); From 62529520293ef01074c06fdf8dd3c283c4766506 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 18 Dec 2025 15:25:11 -0400 Subject: [PATCH 13/62] Show the loading spinner as soon as we start creating the order --- paypal/js/frontend.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index b820e0b5f1..d103f5ee88 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -100,6 +100,8 @@ * @return {Promise} The order ID. */ async function createOrder( data ) { + thisForm.classList.add( 'frm_loading_form' ); + const formData = new FormData( thisForm ); formData.append( 'action', 'frm_paypal_create_order' ); formData.append( 'nonce', frmPayPalVars.nonce ); @@ -115,12 +117,14 @@ } ); if ( ! response.ok ) { + thisForm.classList.remove( 'frm_loading_form' ); throw new Error( 'Failed to create PayPal order' ); } const orderData = await response.json(); if ( ! orderData.success || ! orderData.data.orderID ) { + thisForm.classList.remove( 'frm_loading_form' ); throw new Error( orderData.data || 'Failed to create PayPal order' ); } From b2824018e6934bbd4ab9dc66748c438bfe70bfd5 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 19 Dec 2025 09:42:58 -0400 Subject: [PATCH 14/62] Fix spacing --- paypal/controllers/FrmPayPalLiteActionsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 7d051b4b26..e843cbf482 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -177,7 +177,7 @@ private static function trigger_one_time_payment( $atts ) { return true; } - /** + /** * Add a payment row for the payments table. * * @param array $atts The arguments for the payment. From 41ea7202b3664a8ed2a4d54f917e0162761e6799 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 19 Dec 2025 15:13:01 -0400 Subject: [PATCH 15/62] Save the capture ID instead and add refund endpoints --- .../FrmPayPalLiteActionsController.php | 40 ++++++++++++++++++- paypal/helpers/FrmPayPalLiteConnectHelper.php | 18 +++++++++ .../FrmTransLitePaymentsController.php | 3 ++ .../FrmTransLiteSubscriptionsController.php | 3 ++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index e843cbf482..e26d888b0d 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -161,10 +161,12 @@ private static function trigger_one_time_payment( $atts ) { return 'Failed to capture order.'; } + $capture_id = self::get_capture_id_from_response( $response ); + // Create a payment record. $atts['status'] = 'complete'; $atts['charge'] = new stdClass(); - $atts['charge']->id = $paypal_order_id; + $atts['charge']->id = $capture_id ? $capture_id : $paypal_order_id; $atts['charge']->amount = $atts['amount']; $payment_id = self::create_new_payment( $atts ); @@ -174,9 +176,45 @@ private static function trigger_one_time_payment( $atts ) { FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) ); + /* + echo '
';
+		var_dump( $response );
+		echo '
'; + die(); + */ + return true; } + /** + * @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 ''; + } + /** * Add a payment row for the payments table. * diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index f65e3fe6b3..f550b9f8d6 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -687,4 +687,22 @@ public static function create_order( $amount, $currency ) { public static function capture_order( $order_id ) { return self::post_with_authenticated_body( 'capture_order', compact( 'order_id' ) ); } + + /** + * @param string $capture_id + * + * @return false|object + */ + public static function refund_payment( $capture_id ) { + return self::post_with_authenticated_body( 'refund_capture', array( 'capture_id' => $capture_id ) ); + } + + /** + * @param string $subscription_id + * + * @return false|object + */ + public static function cancel_subscription( $subscription_id ) { + return self::post_with_authenticated_body( 'cancel_subscription', compact( 'subscription_id' ) ); + } } diff --git a/stripe/controllers/FrmTransLitePaymentsController.php b/stripe/controllers/FrmTransLitePaymentsController.php index b5ba448121..4aedec1af5 100755 --- a/stripe/controllers/FrmTransLitePaymentsController.php +++ b/stripe/controllers/FrmTransLitePaymentsController.php @@ -222,6 +222,9 @@ public static function refund_payment() { case 'square': $refunded = FrmSquareLiteConnectHelper::refund_payment( $payment->receipt_id ); break; + case 'paypal': + $refunded = FrmPayPalLiteConnectHelper::refund_payment( $payment->receipt_id ); + break; default: $refunded = false; break; diff --git a/stripe/controllers/FrmTransLiteSubscriptionsController.php b/stripe/controllers/FrmTransLiteSubscriptionsController.php index d2afadfbb3..e5f11b87b8 100755 --- a/stripe/controllers/FrmTransLiteSubscriptionsController.php +++ b/stripe/controllers/FrmTransLiteSubscriptionsController.php @@ -110,6 +110,9 @@ public static function cancel_subscription() { case 'square': $canceled = FrmSquareLiteConnectHelper::cancel_subscription( $sub->sub_id ); break; + case 'paypal': + $canceled = FrmPayPalLiteConnectHelper::cancel_subscription( $sub->sub_id ); + break; default: $canceled = false; break; From 0ee23122f47693a55a98ee3325c9e1efae7ea7e8 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Mon, 22 Dec 2025 10:31:09 -0400 Subject: [PATCH 16/62] Make sure psalm and rector target the paypal folder, remove some testing code, run rector --- .../FrmPayPalLiteEventsController.php | 6 +----- paypal/helpers/FrmPayPalLiteConnectHelper.php | 17 +---------------- psalm.xml | 1 + rector.php | 1 + 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteEventsController.php b/paypal/controllers/FrmPayPalLiteEventsController.php index 787918b4f9..9c4657f3c5 100644 --- a/paypal/controllers/FrmPayPalLiteEventsController.php +++ b/paypal/controllers/FrmPayPalLiteEventsController.php @@ -127,11 +127,7 @@ private function count_failed_event( $event_id ) { $transient_name = 'frm_paypal_failed_event_' . $event_id; $transient = get_transient( $transient_name ); - if ( is_int( $transient ) ) { - $failed_count = $transient + 1; - } else { - $failed_count = 1; - } + $failed_count = is_int( $transient ) ? $transient + 1 : 1; $maximum_retries = 3; diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index f550b9f8d6..c64882d190 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -77,14 +77,7 @@ private static function render_settings_for_mode( $mode ) {
data ) && is_string( $body->data ) ) { return $body->data; } - ob_start(); - var_dump($body); - $output = ob_get_clean(); - return $output; return 'Response from server was not successful'; } diff --git a/psalm.xml b/psalm.xml index 2fdb84e2ac..378b5685b4 100644 --- a/psalm.xml +++ b/psalm.xml @@ -14,6 +14,7 @@ + diff --git a/rector.php b/rector.php index 451a8da377..01f56b56c3 100644 --- a/rector.php +++ b/rector.php @@ -108,6 +108,7 @@ __DIR__ . '/classes', __DIR__ . '/stripe', __DIR__ . '/square', + __DIR__ . '/paypal', __DIR__ . '/tests', ) ) From d18aaca5969fd9678101a46d46e9bb838fb2fafb Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Mon, 22 Dec 2025 17:40:36 -0400 Subject: [PATCH 17/62] Play with adding a paypal checkout button --- .../FrmPayPalLiteActionsController.php | 2 +- paypal/js/frontend.js | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index e26d888b0d..9ca467b052 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -373,7 +373,7 @@ public static function load_scripts( $form_id ) { $sdk_url = add_query_arg( array( 'client-id' => $client_id, - 'components' => 'card-fields', + 'components' => 'buttons,card-fields', 'intent' => 'capture', ), 'https://www.paypal.com/sdk/js' diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index d103f5ee88..fa5a185366 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -38,6 +38,9 @@ // Create the card fields container structure // TODO: Make these IDs unique. cardElement.innerHTML = ` +
+
OR
+
@@ -65,6 +68,13 @@ disableSubmit( thisForm ); + paypal.Buttons({ + createOrder: createOrder, + onApprove: onApprove, + onError: onError, + style: {}, + }).render('#paypal-button-container'); + const cardFields = window.paypal.CardFields( cardFieldsConfig ); // Check eligibility for card fields @@ -279,7 +289,15 @@ try { // Submit the card fields - this triggers createOrder and onApprove - await cardFieldsInstance.submit(); + await cardFieldsInstance.submit({ + billingAddress: { + addressLine1: "555 Billing Ave", + adminArea1: "NY", + adminArea2: "New York", + postalCode: "10001", + countryCode: "US" + } + }); } catch ( err ) { running--; if ( running === 0 && thisForm ) { From e1ea098989740b0b44b2467164ae22b7b7ad6313 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 30 Dec 2025 13:08:23 -0400 Subject: [PATCH 18/62] Run php cs fixer --- paypal/controllers/FrmPayPalLiteActionsController.php | 2 ++ paypal/helpers/FrmPayPalLiteConnectHelper.php | 3 +++ 2 files changed, 5 insertions(+) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 9ca467b052..14a5097926 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -202,11 +202,13 @@ private static function get_capture_id_from_response( $response ) { } $payments = $purchase_unit->payments; + if ( empty( $payments->captures ) || ! is_array( $payments->captures ) ) { continue; } $captures = $payments->captures; + foreach ( $captures as $capture ) { return $capture->id; } diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index c64882d190..a3326a57b5 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -658,6 +658,9 @@ public static function verify() { /** * Create a PayPal order. * + * @param string $amount + * @param string $currency + * * @return false|object */ public static function create_order( $amount, $currency ) { From dd28019a678a29980512cc337984a149ba270865 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 30 Dec 2025 13:09:49 -0400 Subject: [PATCH 19/62] Run rector and phpcbf --- paypal/controllers/FrmPayPalLiteActionsController.php | 2 +- paypal/controllers/FrmPayPalLiteAppController.php | 2 +- paypal/controllers/FrmPayPalLiteEventsController.php | 2 -- paypal/helpers/FrmPayPalLiteConnectHelper.php | 8 ++++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 14a5097926..1cda5d0022 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -561,7 +561,7 @@ private static function get_border_shorthand( $settings ) { * @return array */ public static function remove_cc_validation( $errors, $field, $values ) { - // TODO + // TODO $has_processed = false; if ( ! $has_processed ) { diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php index 31b7518393..fe7c13b92a 100644 --- a/paypal/controllers/FrmPayPalLiteAppController.php +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -113,7 +113,7 @@ public static function create_order() { private static function get_amount_value_for_verification( $action ) { $amount = $action->post_content['amount']; - if ( strpos( $amount, '[' ) === false ) { + if ( ! str_contains( $amount, '[' ) ) { return $amount; } diff --git a/paypal/controllers/FrmPayPalLiteEventsController.php b/paypal/controllers/FrmPayPalLiteEventsController.php index 9c4657f3c5..dd1a6f2248 100644 --- a/paypal/controllers/FrmPayPalLiteEventsController.php +++ b/paypal/controllers/FrmPayPalLiteEventsController.php @@ -168,7 +168,5 @@ private function track_handled_event( $event_id ) { * @return void */ private function handle_event() { - } - } diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index a3326a57b5..a2c569bb4d 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -76,8 +76,8 @@ private static function render_settings_for_mode( $mode ) {
self::generate_client_password( $mode ), - 'user_id' => get_current_user_id(), + 'password' => self::generate_client_password( $mode ), + 'user_id' => get_current_user_id(), 'frm_paypal_api_mode' => $mode, ); From 6e4b66a57bd26970e9f842923b2df4cbff13088e Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 30 Dec 2025 13:13:06 -0400 Subject: [PATCH 20/62] Ignore psalm errors for paypal folder where all dirs are being ignored --- psalm.xml | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/psalm.xml b/psalm.xml index 378b5685b4..7053e475c9 100644 --- a/psalm.xml +++ b/psalm.xml @@ -34,6 +34,7 @@ + @@ -87,6 +88,7 @@ + @@ -95,6 +97,7 @@ + @@ -102,6 +105,7 @@ + @@ -110,6 +114,7 @@ + @@ -117,6 +122,7 @@ + @@ -134,6 +140,7 @@ + @@ -141,6 +148,7 @@ + @@ -148,6 +156,7 @@ + @@ -155,6 +164,7 @@ + @@ -162,6 +172,7 @@ + @@ -176,6 +187,7 @@ + @@ -188,6 +200,7 @@ + @@ -220,6 +233,7 @@ + @@ -227,6 +241,7 @@ + @@ -234,6 +249,7 @@ + @@ -241,6 +257,7 @@ + @@ -248,6 +265,7 @@ + @@ -255,6 +273,7 @@ + @@ -262,6 +281,7 @@ + @@ -270,6 +290,7 @@ + @@ -277,6 +298,7 @@ + @@ -284,6 +306,7 @@ + @@ -291,6 +314,7 @@ + @@ -298,6 +322,7 @@ + @@ -305,6 +330,7 @@ + @@ -312,6 +338,7 @@ + @@ -320,6 +347,7 @@ + @@ -327,6 +355,7 @@ + @@ -343,6 +372,7 @@ + @@ -350,6 +380,7 @@ + @@ -357,6 +388,7 @@ + @@ -370,6 +402,7 @@ + @@ -377,6 +410,7 @@ + @@ -384,6 +418,7 @@ + @@ -448,6 +483,7 @@ + @@ -455,6 +491,7 @@ + @@ -499,6 +536,8 @@ + + @@ -506,6 +545,8 @@ + + From e95539461c04c1bd103fd2b889f9e542d3856a41 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 30 Dec 2025 13:16:09 -0400 Subject: [PATCH 21/62] Run eslint fix --- paypal/js/frontend.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index fa5a185366..db85f2a6a0 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -54,7 +54,7 @@ onError: onError, style: getCardFieldStyles(), inputEvents: { - onChange: ( data) => { + onChange: data => { cardFieldsValid = data.isFormValid; if ( cardFieldsValid ) { @@ -68,12 +68,12 @@ disableSubmit( thisForm ); - paypal.Buttons({ + paypal.Buttons( { createOrder: createOrder, onApprove: onApprove, onError: onError, style: {}, - }).render('#paypal-button-container'); + } ).render( '#paypal-button-container' ); const cardFields = window.paypal.CardFields( cardFieldsConfig ); @@ -109,7 +109,7 @@ * * @return {Promise} The order ID. */ - async function createOrder( data ) { + async function createOrder() { thisForm.classList.add( 'frm_loading_form' ); const formData = new FormData( thisForm ); @@ -289,15 +289,15 @@ try { // Submit the card fields - this triggers createOrder and onApprove - await cardFieldsInstance.submit({ + await cardFieldsInstance.submit( { billingAddress: { - addressLine1: "555 Billing Ave", - adminArea1: "NY", - adminArea2: "New York", - postalCode: "10001", - countryCode: "US" + addressLine1: '555 Billing Ave', + adminArea1: 'NY', + adminArea2: 'New York', + postalCode: '10001', + countryCode: 'US' } - }); + } ); } catch ( err ) { running--; if ( running === 0 && thisForm ) { @@ -335,7 +335,6 @@ // Add event listener for form submission thisForm.addEventListener( 'submit', handleCardSubmission ); - } catch ( e ) { console.error( 'Initializing PayPal Card Fields failed', e ); displayPaymentFailure( 'Failed to initialize payment form.' ); From 4dc91253b2772b3ee9e2fbc56b3fbeb4cc720c27 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 30 Dec 2025 13:23:58 -0400 Subject: [PATCH 22/62] Simplify the autoload logic --- formidable.php | 66 +++++++++------------------------- paypal/views/settings/form.php | 2 +- 2 files changed, 18 insertions(+), 50 deletions(-) diff --git a/formidable.php b/formidable.php index 6b7476ed9f..069c94eaf1 100644 --- a/formidable.php +++ b/formidable.php @@ -108,63 +108,31 @@ function frm_class_autoloader( $class_name, $filepath ) { return; } + $filepath = $original_filepath; + if ( preg_match( '/^FrmStrpLite.+$/', $class_name ) || preg_match( '/^FrmTransLite.+$/', $class_name ) ) { // Autoload for /stripe/ folder. - $filepath = $original_filepath . '/stripe/'; - - if ( preg_match( '/^.+Helper$/', $class_name ) ) { - $filepath .= 'helpers/'; - } elseif ( preg_match( '/^.+Controller$/', $class_name ) ) { - $filepath .= 'controllers/'; - } else { - $filepath .= 'models/'; - } - - $filepath .= $class_name . '.php'; - - if ( file_exists( $filepath ) ) { - require $filepath; - } - + $filepath .= '/stripe/'; + } elseif ( preg_match( '/^FrmSquareLite.+$/', $class_name ) ) { + $filepath .= '/square/'; + } elseif ( preg_match( '/^FrmPayPalLite.+$/', $class_name ) ) { + $filepath .= '/paypal/'; + } else { return; } - if ( preg_match( '/^FrmSquareLite.+$/', $class_name ) ) { - $filepath = $original_filepath . '/square/'; - - if ( preg_match( '/^.+Helper$/', $class_name ) ) { - $filepath .= 'helpers/'; - } elseif ( preg_match( '/^.+Controller$/', $class_name ) ) { - $filepath .= 'controllers/'; - } else { - $filepath .= 'models/'; - } - - $filepath .= $class_name . '.php'; - - if ( file_exists( $filepath ) ) { - require $filepath; - } - - return; + if ( preg_match( '/^.+Helper$/', $class_name ) ) { + $filepath .= 'helpers/'; + } elseif ( preg_match( '/^.+Controller$/', $class_name ) ) { + $filepath .= 'controllers/'; + } else { + $filepath .= 'models/'; } - if ( preg_match( '/^FrmPayPalLite.+$/', $class_name ) ) { - $filepath = $original_filepath . '/paypal/'; - - if ( preg_match( '/^.+Helper$/', $class_name ) ) { - $filepath .= 'helpers/'; - } elseif ( preg_match( '/^.+Controller$/', $class_name ) ) { - $filepath .= 'controllers/'; - } else { - $filepath .= 'models/'; - } - - $filepath .= $class_name . '.php'; + $filepath .= $class_name . '.php'; - if ( file_exists( $filepath ) ) { - require $filepath; - } + if ( file_exists( $filepath ) ) { + require $filepath; } } diff --git a/paypal/views/settings/form.php b/paypal/views/settings/form.php index f2fca776db..88f0bf2246 100644 --- a/paypal/views/settings/form.php +++ b/paypal/views/settings/form.php @@ -9,7 +9,7 @@ if ( is_callable( array( 'FrmPaymentSettingsController', 'route' ) ) ) { ?>
-

+

From 4cee596d065b706e464e9381a200a00639700c8b Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 30 Dec 2025 13:40:54 -0400 Subject: [PATCH 23/62] Remove background-color style from input (not supported, was triggering a warning) --- paypal/controllers/FrmPayPalLiteActionsController.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 1cda5d0022..3809dc0b8b 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -429,7 +429,6 @@ public static function get_style_for_js( $form_id ) { 'input' => array( 'font-size' => $settings['field_font_size'], 'color' => $settings['text_color'], - 'background-color' => $settings['bg_color'], 'font-weight' => $settings['field_weight'], 'padding' => $settings['field_pad'], 'line-height' => 1.3, @@ -439,9 +438,6 @@ public static function get_style_for_js( $form_id ) { 'input::placeholder' => array( 'color' => $settings['text_color_disabled'], ), - 'input:focus' => array( - 'background-color' => $settings['bg_color_active'], - ), '.invalid' => array( 'color' => $settings['border_color_error'], ), From ca7f8a52a2a62ba96aa8dcf4b4d26c85858ee792 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 30 Dec 2025 13:56:54 -0400 Subject: [PATCH 24/62] Add the name field and get the checkout button working --- paypal/js/frontend.js | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index db85f2a6a0..fc14c79b2b 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -40,10 +40,10 @@ cardElement.innerHTML = `
OR
-
-
-
-
+
+
+
+
`; thisForm = cardElement.closest( 'form' ); @@ -84,6 +84,7 @@ } // Render individual card fields + cardFields.NameField().render( '#frm-paypal-card-name' ); cardFields.NumberField().render( '#frm-paypal-card-number' ); cardFields.ExpiryField().render( '#frm-paypal-card-expiry' ); cardFields.CVVField().render( '#frm-paypal-card-cvv' ); @@ -154,6 +155,12 @@ orderInput.value = data.orderID; thisForm.appendChild( orderInput ); + // If someone uses the PayPal checkout button, the form submit event doesn't actually get triggered. + if ( ! submitEvent ) { + submitEvent = new Event( 'submit', { cancelable: true, bubbles: true } ); + submitEvent.target = thisForm; + } + // Submit the form if ( typeof frmFrontForm.submitFormManual === 'function' ) { frmFrontForm.submitFormManual( submitEvent, thisForm ); @@ -289,15 +296,18 @@ try { // Submit the card fields - this triggers createOrder and onApprove - await cardFieldsInstance.submit( { - billingAddress: { - addressLine1: '555 Billing Ave', - adminArea1: 'NY', - adminArea2: 'New York', - postalCode: '10001', - countryCode: 'US' + // TODO: Stop hard coding the billing address and use actual form data. + await cardFieldsInstance.submit( + { + billingAddress: { + addressLine1: '555 Billing Ave', + adminArea1: 'NY', + adminArea2: 'New York', + postalCode: '10001', + countryCode: 'US' + } } - } ); + ); } catch ( err ) { running--; if ( running === 0 && thisForm ) { From eb0815364f8d9f74cdce8d17627a1a2bb3677471 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 30 Dec 2025 16:28:42 -0400 Subject: [PATCH 25/62] Working on subscriptions (wip) --- .../FrmPayPalLiteActionsController.php | 10 +++- .../FrmPayPalLiteAppController.php | 60 +++++++++++++++++++ .../FrmPayPalLiteHooksController.php | 3 + paypal/helpers/FrmPayPalLiteConnectHelper.php | 9 +++ paypal/js/frontend.js | 35 ++++++++++- 5 files changed, 113 insertions(+), 4 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 3809dc0b8b..72322d1c7f 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -145,7 +145,7 @@ public static function trigger_gateway( $action, $entry, $form ) { * @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' ); + $paypal_order_id = FrmAppHelper::get_post_param( 'paypal_order_id', '', 'sanitize_text_field' ); if ( empty( $paypal_order_id ) ) { return 'No PayPal order ID found.'; @@ -250,7 +250,7 @@ private static function create_new_payment( $atts ) { * @return bool|string True on success, error message on failure */ private static function trigger_recurring_payment( $atts ) { - return 'Recurring payments are not yet implemented for PayPal.'; + return 'Recurring payments are not yet implemented for PayPal Lite.'; } /** @@ -376,7 +376,11 @@ public static function load_scripts( $form_id ) { array( 'client-id' => $client_id, 'components' => 'buttons,card-fields', - 'intent' => 'capture', + // Use capture for one time payments. + // 'intent' => 'capture', + // Subscriptions require vault=true. + 'intent' => 'subscription', + 'vault' => 'true', ), 'https://www.paypal.com/sdk/js' ); diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php index fe7c13b92a..ec7a5efa1b 100644 --- a/paypal/controllers/FrmPayPalLiteAppController.php +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -163,4 +163,64 @@ private static function generate_false_entry() { return $entry; } + + public static function create_subscription() { + check_ajax_referer( 'frm_paypal_ajax', 'nonce' ); + + $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' ); + + if ( ! $form_id ) { + wp_send_json_error( __( 'Invalid form ID', 'formidable' ) ); + } + + $actions = FrmPayPalLiteActionsController::get_actions_before_submit( $form_id ); + + if ( empty( $actions ) ) { + wp_send_json_error( __( 'No PayPal actions found for this form', 'formidable' ) ); + } + + $action = reset( $actions ); + $amount = self::get_amount_value_for_verification( $action ); + + // PayPal expects the amount in a format like 10.00, so format it. + $amount = number_format( floatval( $amount ), 2, '.', '' ); + $currency = strtoupper( $action->post_content['currency'] ); + + // Pass $product_name, $interval and $interval_count to the helper + // As well as trial period and the maximum number of payments. + // Also send subscriber email. + // TODO Process the description. + // TODO Do we want a new Product Name setting? + $product_name = $action->post_content['description'] ?? ''; + $interval = $action->post_content['interval'] ?? ''; + $interval_count = $action->post_content['interval_count'] ?? 1; + $trial_period = $action->post_content['trial_period'] ?? ''; + $payment_limit = $action->post_content['payment_limit'] ?? ''; + + // TODO Process email properly. + $email = $action->post_content['email'] ?? ''; + + $data = array( + 'amount' => $amount, + 'currency' => $currency, + 'product_name' => $product_name, + 'interval' => $interval, + 'interval_count' => $interval_count, + 'trial_period' => $trial_period, + 'payment_limit' => $payment_limit, + 'email' => $email, + ); + + $response = FrmPayPalLiteConnectHelper::create_subscription( $data ); + + if ( false === $response ) { + wp_send_json_error( 'Failed to create PayPal subscription' ); + } + + if ( ! isset( $response->subscription_id ) ) { + wp_send_json_error( 'Failed to create PayPal subscription' ); + } + + wp_send_json_success( array( 'subscriptionID' => $response->subscription_id ) ); + } } diff --git a/paypal/controllers/FrmPayPalLiteHooksController.php b/paypal/controllers/FrmPayPalLiteHooksController.php index dcd7153820..c025569960 100644 --- a/paypal/controllers/FrmPayPalLiteHooksController.php +++ b/paypal/controllers/FrmPayPalLiteHooksController.php @@ -50,5 +50,8 @@ private static function load_ajax_hooks() { add_action( 'wp_ajax_frm_paypal_create_order', 'FrmPayPalLiteAppController::create_order' ); add_action( 'wp_ajax_nopriv_frm_paypal_create_order', 'FrmPayPalLiteAppController::create_order' ); + + add_action( 'wp_ajax_frm_paypal_create_subscription', 'FrmPayPalLiteAppController::create_subscription' ); + add_action( 'wp_ajax_nopriv_frm_paypal_create_subscription', 'FrmPayPalLiteAppController::create_subscription' ); } } diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index a2c569bb4d..b0dda603e5 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -693,4 +693,13 @@ public static function refund_payment( $capture_id ) { public static function cancel_subscription( $subscription_id ) { return self::post_with_authenticated_body( 'cancel_subscription', compact( 'subscription_id' ) ); } + + /** + * @param array $data Subscription data. + * + * @return false|object + */ + public static function create_subscription( $data ) { + return self::post_with_authenticated_body( 'create_subscription', compact( 'data' ) ); + } } diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index fc14c79b2b..307b5e107b 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -69,7 +69,8 @@ disableSubmit( thisForm ); paypal.Buttons( { - createOrder: createOrder, + // createOrder: createOrder, + createSubscription: createSubscription, onApprove: onApprove, onError: onError, style: {}, @@ -142,6 +143,38 @@ return orderData.data.orderID; } + async function createSubscription() { + thisForm.classList.add( 'frm_loading_form' ); + + const formData = new FormData( thisForm ); + formData.append( 'action', 'frm_paypal_create_subscription' ); + formData.append( 'nonce', frmPayPalVars.nonce ); + + // Remove a few fields so form validation does not incorrectly trigger. + formData.delete( 'frm_action' ); + formData.delete( 'form_key' ); + formData.delete( 'item_key' ); + + const response = await fetch( frmPayPalVars.ajax, { + method: 'POST', + body: formData + } ); + + if ( ! response.ok ) { + thisForm.classList.remove( 'frm_loading_form' ); + throw new Error( 'Failed to create PayPal order' ); + } + + const orderData = await response.json(); + + if ( ! orderData.success || ! orderData.data.orderID ) { + thisForm.classList.remove( 'frm_loading_form' ); + throw new Error( orderData.data || 'Failed to create PayPal order' ); + } + + return orderData.data.orderID; + } + /** * Handle approved payment. * From d33af4d785f0add8eeebe1f235b8948c55a80aba Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 6 Jan 2026 16:47:13 -0400 Subject: [PATCH 26/62] Run eslint fix --- paypal/js/frontend.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index 307b5e107b..110fb1f423 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -186,11 +186,11 @@ orderInput.type = 'hidden'; orderInput.name = 'paypal_order_id'; orderInput.value = data.orderID; - thisForm.appendChild( orderInput ); + thisForm.append( orderInput ); // If someone uses the PayPal checkout button, the form submit event doesn't actually get triggered. if ( ! submitEvent ) { - submitEvent = new Event( 'submit', { cancelable: true, bubbles: true } ); + submitEvent = new Event( 'submit', { cancelable: true, bubbles: true } ); submitEvent.target = thisForm; } From db57d32a2b5c9dd71d6dede0ccf07467a0c7428f Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 7 Jan 2026 10:33:05 -0400 Subject: [PATCH 27/62] Move client id to a function --- .../FrmPayPalLiteActionsController.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index 72322d1c7f..1f6fe3f2f9 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -367,9 +367,7 @@ public static function load_scripts( $form_id ) { return; } - // TODO: Stop hard coding this here. - // It should be in a function. - $client_id = 'AV8DLwHFtnUai9Yuy8B5ocRSgtlCBiRAh6Vkl4vhgeuiKRLzilt-vzjd6O1tjIVI_5AiPG0H-HtBssrE'; + $client_id = self::get_client_id(); // Build the PayPal SDK URL with required parameters. $sdk_url = add_query_arg( @@ -582,6 +580,8 @@ public static function remove_cc_validation( $errors, $field, $values ) { } /** + * @since x.x + * * @return void */ public static function actions_js() { @@ -592,4 +592,15 @@ public static function actions_js() { FrmAppHelper::plugin_version() ); } + + /** + * @since x.x + * + * @return string + */ + private static function get_client_id() { + // TODO: This will need logic for a production client ID as well. + // This is currently just for testing. + return 'AV8DLwHFtnUai9Yuy8B5ocRSgtlCBiRAh6Vkl4vhgeuiKRLzilt-vzjd6O1tjIVI_5AiPG0H-HtBssrE'; + } } From f1e64a566582774c3b13479e441090c61f1c20ad Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 7 Jan 2026 10:47:53 -0400 Subject: [PATCH 28/62] Remove old todo comment --- paypal/js/frontend.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index 110fb1f423..cff83fda81 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -3,9 +3,6 @@ return; } - // TODO: - // Only enable the submit button when we check the state for isFormValid = true. - const clientId = frmPayPalVars.clientId; // Track the state of the PayPal card fields From 34553b5f6b59a52f86d6a113a67df1693c748756 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 7 Jan 2026 12:01:14 -0400 Subject: [PATCH 29/62] Drop the name field and use mapped name field setting value --- paypal/js/frontend.js | 137 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 121 insertions(+), 16 deletions(-) diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index cff83fda81..0b27f7e51b 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -37,7 +37,6 @@ cardElement.innerHTML = `
OR
-
@@ -82,7 +81,6 @@ } // Render individual card fields - cardFields.NameField().render( '#frm-paypal-card-name' ); cardFields.NumberField().render( '#frm-paypal-card-number' ); cardFields.ExpiryField().render( '#frm-paypal-card-expiry' ); cardFields.CVVField().render( '#frm-paypal-card-cvv' ); @@ -164,12 +162,17 @@ const orderData = await response.json(); - if ( ! orderData.success || ! orderData.data.orderID ) { + if ( ! orderData.success || ! orderData.data.subscriptionID ) { thisForm.classList.remove( 'frm_loading_form' ); - throw new Error( orderData.data || 'Failed to create PayPal order' ); + + if ( 'string' === typeof orderData.data ) { + throw new Error( orderData.data ); + } + + throw new Error( 'Failed to create PayPal subscription' ); } - return orderData.data.orderID; + return orderData.data.subscriptionID; } /** @@ -324,20 +327,31 @@ running++; disableSubmit( thisForm ); + const meta = addName( jQuery( thisForm ) ); + + const submitArgs = {}; + + if ( meta.name ) { + submitArgs.cardholderName = meta.name; + } + + /* + TODO Add the billing address here as well. + Stripe calls a window.frmProForm.addAddressMeta function. + + billingAddress: { + addressLine1: '555 Billing Ave', + adminArea1: 'NY', + adminArea2: 'New York', + postalCode: '10001', + countryCode: 'US' + } + */ + try { // Submit the card fields - this triggers createOrder and onApprove // TODO: Stop hard coding the billing address and use actual form data. - await cardFieldsInstance.submit( - { - billingAddress: { - addressLine1: '555 Billing Ave', - adminArea1: 'NY', - adminArea2: 'New York', - postalCode: '10001', - countryCode: 'US' - } - } - ); + await cardFieldsInstance.submit( submitArgs ); } catch ( err ) { running--; if ( running === 0 && thisForm ) { @@ -381,6 +395,97 @@ } } + function addName( $form ) { + let i, + firstField, + lastField, + firstFieldContainer, + lastFieldContainer, + firstNameID = '', + lastNameID = '', + subFieldEl; + + const cardObject = {}; + const settings = frmPayPalVars.settings; + + /** + * Gets first, middle or last name from the given field. + * + * @param {number|HTMLElement} field Field ID or Field element. + * @param {string} subFieldName Subfield name. + * @return {string} Name field value. + */ + const getNameFieldValue = function( field, subFieldName ) { + if ( 'object' !== typeof field ) { + field = document.getElementById( 'frm_field_' + field + '_container' ); + } + + if ( ! field || 'object' !== typeof field || 'function' !== typeof field.querySelector ) { + return ''; + } + + subFieldEl = field.querySelector( '.frm_combo_inputs_container .frm_form_subfield-' + subFieldName + ' input' ); + if ( ! subFieldEl ) { + return ''; + } + + return subFieldEl.value; + }; + + for ( i = 0; i < settings.length; i++ ) { + firstNameID = settings[ i ].first_name; + lastNameID = settings[ i ].last_name; + } + + /** + * Returns a name field container or element. + * + * @param {number} fieldID + * @param {string} type Either 'container' or 'field' + * @param {object|null} $form + * @return {HTMLElement|null} Name field container or element. + */ + function getNameFieldItem( fieldID, type, $form = null ) { + const queryForNameFieldIsFound = 'object' === typeof window.frmProForm && 'function' === typeof window.frmProForm.queryForNameField; + + if ( type === 'container' ) { + return queryForNameFieldIsFound + ? window.frmProForm.queryForNameField( fieldID, 'container' ) + : document.querySelector( '#frm_field_' + fieldID + '_container, .frm_field_' + fieldID + '_container' ); + } + + return queryForNameFieldIsFound + ? window.frmProForm.queryForNameField( fieldID, 'field', $form[ 0 ] ) + : $form[ 0 ].querySelector( '#frm_field_' + fieldID + '_container input, input[name="item_meta[' + fieldID + ']"], .frm_field_' + fieldID + '_container input' ); + } + + if ( firstNameID !== '' ) { + firstFieldContainer = getNameFieldItem( firstNameID, 'container' ); + if ( firstFieldContainer && firstFieldContainer.querySelector( '.frm_combo_inputs_container' ) ) { // This is a name field. + cardObject.name = getNameFieldValue( firstFieldContainer, 'first' ); + } else { + firstField = getNameFieldItem( firstNameID, 'field', $form ); + if ( firstField && firstField.value ) { + cardObject.name = firstField.value; + } + } + } + + if ( lastNameID !== '' ) { + lastFieldContainer = getNameFieldItem( lastNameID, 'container' ); + if ( lastFieldContainer && lastFieldContainer.querySelector( '.frm_combo_inputs_container' ) ) { // This is a name field. + cardObject.name = cardObject.name + ' ' + getNameFieldValue( lastFieldContainer, 'last' ); + } else { + lastField = getNameFieldItem( lastNameID, 'field', $form ); + if ( lastField && lastField.value ) { + cardObject.name = cardObject.name + ' ' + lastField.value; + } + } + } + + return cardObject; + } + document.addEventListener( 'DOMContentLoaded', async function() { if ( ! window.paypal ) { console.error( 'PayPal JS SDK failed to load properly' ); From 9a5338173d8eab5450a8ab10fa64aa350e954038 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 7 Jan 2026 12:59:26 -0400 Subject: [PATCH 30/62] Add todo comment --- paypal/js/frontend.js | 1 + 1 file changed, 1 insertion(+) diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index 0b27f7e51b..b31b984cda 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -69,6 +69,7 @@ createSubscription: createSubscription, onApprove: onApprove, onError: onError, + // TODO: Add onCancel? We can remove the loading spinner from the submit button. style: {}, } ).render( '#paypal-button-container' ); From 4224035eaedc487baca0e194498ad0efc62bf7a7 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 7 Jan 2026 16:12:17 -0400 Subject: [PATCH 31/62] Drop the paypal pro tips --- classes/helpers/FrmTipsHelper.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/classes/helpers/FrmTipsHelper.php b/classes/helpers/FrmTipsHelper.php index cf8a55bf60..41d4605f66 100644 --- a/classes/helpers/FrmTipsHelper.php +++ b/classes/helpers/FrmTipsHelper.php @@ -242,22 +242,6 @@ public static function get_form_action_tip() { 'tip' => __( 'Send leads to Mailchimp for instant email follow-up.', 'formidable' ), 'call' => self::cta_label(), ), - array( - 'link' => array( - 'content' => 'paypal-revenue', - 'page' => 'paypal-increase-revenue-tip', - ), - 'tip' => __( 'Accept PayPal payments and grow your sales.', 'formidable' ), - 'call' => self::cta_label(), - ), - array( - 'link' => array( - 'content' => 'paypal-fast', - 'page' => 'paypal-save-time-tip', - ), - 'tip' => __( 'Accept payments now with PayPal integration.', 'formidable' ), - 'call' => self::cta_label(), - ), array( 'link' => array( 'content' => 'registration', From e1d69137cb5cc8289e1c074ef799e96b449ad225 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 7 Jan 2026 16:20:22 -0400 Subject: [PATCH 32/62] Add the fee education to the paypal global settings page --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 2 ++ square/helpers/FrmSquareLiteConnectHelper.php | 1 - stripe/views/action-settings/payments-options.php | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index b0dda603e5..df5f7e76d3 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -22,6 +22,8 @@ public static function render_settings_container() { self::register_settings_scripts(); + FrmPayPalLiteAppHelper::fee_education( 'paypal-global-settings-tip' ); + ?> diff --git a/square/helpers/FrmSquareLiteConnectHelper.php b/square/helpers/FrmSquareLiteConnectHelper.php index 497bc4212a..ef5ad5198a 100644 --- a/square/helpers/FrmSquareLiteConnectHelper.php +++ b/square/helpers/FrmSquareLiteConnectHelper.php @@ -23,7 +23,6 @@ public static function render_settings_container() { self::register_settings_scripts(); FrmSquareLiteAppHelper::fee_education( 'square-global-settings-tip' ); - ?>
diff --git a/stripe/views/action-settings/payments-options.php b/stripe/views/action-settings/payments-options.php index bb85d5ee3d..7fdcc5537a 100755 --- a/stripe/views/action-settings/payments-options.php +++ b/stripe/views/action-settings/payments-options.php @@ -5,6 +5,7 @@ $stripe_connected = FrmStrpLiteConnectHelper::at_least_one_mode_is_setup(); $square_connected = FrmSquareLiteConnectHelper::at_least_one_mode_is_setup(); +$paypal_connected = FrmPayPalLiteConnectHelper::at_least_one_mode_is_setup(); if ( $stripe_connected ) { FrmStrpLiteAppHelper::fee_education( 'stripe-action-tip', $form_action->post_content['gateway'] ); @@ -14,7 +15,11 @@ FrmSquareLiteAppHelper::fee_education( 'square-action-tip', $form_action->post_content['gateway'] ); } -if ( ! $stripe_connected && ! $square_connected ) { +if ( $paypal_connected ) { + FrmPayPalLiteAppHelper::fee_education( 'paypal-action-tip', $form_action->post_content['gateway'] ); +} + +if ( ! $stripe_connected && ! $square_connected && ! $paypal_connected ) { FrmStrpLiteAppHelper::not_connected_warning(); } ?> From c84a3a68dad7301ab6acac77c6481217128d803d Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 7 Jan 2026 16:47:31 -0400 Subject: [PATCH 33/62] Update payment setting toggle logic to support more than 2 gateways --- stripe/js/frmtrans_admin.js | 18 ++++++++++++------ .../views/action-settings/payments-options.php | 7 +++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/stripe/js/frmtrans_admin.js b/stripe/js/frmtrans_admin.js index 6833da4a46..54e52ecefb 100755 --- a/stripe/js/frmtrans_admin.js +++ b/stripe/js/frmtrans_admin.js @@ -34,16 +34,22 @@ toggleOpts( this, checked, '.show_' + gateway ); - const toggleOff = 'stripe' === gateway ? 'square' : 'stripe'; + const gateways = [ 'stripe', 'square', 'paypal' ]; + const toggleOff = gateways.filter( g => g !== gateway ); + const settings = jQuery( this ).closest( '.frm_form_action_settings' ); const showClass = 'show_' + settings.find( '.frm_gateway_opt input:checked' ).attr( 'value' ); - const gatewaySettings = settings.get( 0 ).querySelectorAll( '.show_' + toggleOff ); - gatewaySettings.forEach( - setting => { - if ( ! setting.classList.contains( showClass ) ) { - setting.style.display = 'none'; + toggleOff.forEach( + function( gateway ) { + const gatewaySettings = settings.get( 0 ).querySelectorAll( '.show_' + gateway ); + gatewaySettings.forEach( + setting => { + if ( ! setting.classList.contains( showClass ) ) { + setting.style.display = 'none'; + } } + ); } ); diff --git a/stripe/views/action-settings/payments-options.php b/stripe/views/action-settings/payments-options.php index 7fdcc5537a..432a75bcc1 100755 --- a/stripe/views/action-settings/payments-options.php +++ b/stripe/views/action-settings/payments-options.php @@ -25,6 +25,13 @@ ?>
+

+ + +

+

';
-		var_dump( array_keys( (array) $status ) );
-		echo 'Primary email confirmed: ';
-		var_dump( $status->primary_email_confirmed );
-		echo 'Payments receivable: ';
-		var_dump( $status->payments_receivable );
-		echo 'OAuth integrations: ';
-		var_dump( $status->oauth_integrations );
-		echo '
'; + + if ( ! is_object( $status ) ) { + self::render_error( __( 'Unable to retrieve seller status', 'formidable' ) ); + return; + } + + if ( ! $status->primary_email_confirmed ) { + self::render_error( __( 'Primary email not confirmed', 'formidable' ) ); + return; + } + + if ( ! $status->payments_receivable ) { + self::render_error( __( 'Payments are not receiable', 'formidable' ) ); + return; + } + + if ( ! $status->oauth_integrations ) { + self::render_error( __( 'OAuth integrations are not enabled', 'formidable' ) ); + return; + } + + echo '
'; + esc_html_e( 'Your seller status is valid', 'formidable' ); + echo '
'; + } + + private static function render_error( $message ) { + echo '
'; + echo esc_html( $message ); + echo '
'; } /** From b7539645a8e5c78dcfb6a31b0910685f31321ce1 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 23 Jan 2026 17:04:14 -0400 Subject: [PATCH 50/62] Update comment --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index fe3fb0d72b..98c0b82741 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -70,7 +70,7 @@ public static function render_settings_container() { */ private static function render_seller_status() { // TODO: Only render when we visit the PayPal tab. - // TODO: If all 3 validate, we should be able to save this to an option and stop making requests. + // TODO: If all 3 validate, we should be able to save this to an option and stop making $status = self::get_seller_status(); if ( ! is_object( $status ) ) { From 778fcc0e8cd744718cb8c866555b3383518887c6 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Mon, 26 Jan 2026 10:56:53 -0400 Subject: [PATCH 51/62] Working on rendering seller info --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index 98c0b82741..b0c95ac51b 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -69,10 +69,24 @@ public static function render_settings_container() { * @return void */ private static function render_seller_status() { + if ( ! self::get_merchant_id() ) { + // If not connected, show no status. + return; + } + + $info = self::get_seller_info(); + echo '
';
+		var_dump( $info );
+		echo '
'; + // TODO: Only render when we visit the PayPal tab. // TODO: If all 3 validate, we should be able to save this to an option and stop making $status = self::get_seller_status(); + echo '
';
+		var_dump( $status );
+		echo '
'; + if ( ! is_object( $status ) ) { self::render_error( __( 'Unable to retrieve seller status', 'formidable' ) ); return; @@ -761,4 +775,8 @@ public static function create_vault_setup_token() { public static function get_seller_status() { return self::post_with_authenticated_body( 'get_seller_status' ); } + + public static function get_seller_info() { + return self::post_with_authenticated_body( 'get_seller_info' ); + } } From 3f2f56ff35ceb28ccf036dba8e5010ce0814440b Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Mon, 26 Jan 2026 15:48:03 -0400 Subject: [PATCH 52/62] Move seller status to the connection box --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index b0c95ac51b..03f603ea59 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -18,8 +18,6 @@ class FrmPayPalLiteConnectHelper { * @return void */ public static function render_settings_container() { - self::render_seller_status(); - $settings = FrmPayPalLiteAppHelper::get_settings(); self::register_settings_scripts(); @@ -68,37 +66,41 @@ public static function render_settings_container() { * * @return void */ - private static function render_seller_status() { - if ( ! self::get_merchant_id() ) { + private static function render_seller_status( $mode ) { + if ( ! self::get_merchant_id( $mode ) ) { // If not connected, show no status. return; } - $info = self::get_seller_info(); - echo '
';
-		var_dump( $info );
-		echo '
'; - // TODO: Only render when we visit the PayPal tab. // TODO: If all 3 validate, we should be able to save this to an option and stop making - $status = self::get_seller_status(); + $status = self::get_seller_status( $mode ); + +/* + $status = new stdClass(); + $status->payments_receivable = true; + $status->primary_email_confirmed = true; + $status->oauth_integrations = true; + $status->primary_email = 'test@example.com';*/ - echo '
';
-		var_dump( $status );
-		echo '
'; + $status->primary_email_confirmed = false; if ( ! is_object( $status ) ) { self::render_error( __( 'Unable to retrieve seller status', 'formidable' ) ); return; } + $email = ! empty( $status->primary_email ) ? $status->primary_email : ''; + if ( ! $status->primary_email_confirmed ) { - self::render_error( __( 'Primary email not confirmed', 'formidable' ) ); + // self::render_error( __( 'Primary email not confirmed', 'formidable' ) ); + // self::render_error( sprintf( __( 'Primary email (%s) not confirmed', 'formidable' ), $email ) ); + self::render_error( __( 'Primary email not confirmed', 'formidable' ) . '
Connected account:
' . $email ); return; } if ( ! $status->payments_receivable ) { - self::render_error( __( 'Payments are not receiable', 'formidable' ) ); + self::render_error( __( 'Payments are not receivable', 'formidable' ) ); return; } @@ -107,14 +109,19 @@ private static function render_seller_status() { return; } + $email = ! empty( $status->primary_email ) ? $status->primary_email : ''; + echo '
'; esc_html_e( 'Your seller status is valid', 'formidable' ); + if ( $email ) { + echo '
Connected account:
' . $email; + } echo '
'; } private static function render_error( $message ) { echo '
'; - echo esc_html( $message ); + echo wp_kses_post( $message ); echo '
'; } @@ -125,7 +132,7 @@ private static function render_error( $message ) { */ private static function render_settings_for_mode( $mode ) { ?> -
+
@@ -158,6 +165,7 @@ private static function render_settings_for_mode( $mode ) { } ?>
+
@@ -772,11 +780,10 @@ public static function create_vault_setup_token() { return self::post_with_authenticated_body( 'create_vault_setup_token' ); } - public static function get_seller_status() { - return self::post_with_authenticated_body( 'get_seller_status' ); - } - - public static function get_seller_info() { - return self::post_with_authenticated_body( 'get_seller_info' ); + public static function get_seller_status( $mode ) { + $additional_body = array( + 'frm_paypal_api_mode' => $mode, + ); + return self::post_with_authenticated_body( 'get_seller_status', $additional_body ); } } From 4dd58ed0f01040f546d27b1c627cfc83c93661f6 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 27 Jan 2026 14:55:23 -0400 Subject: [PATCH 53/62] Always show the connect email in the seller status elements --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index 03f603ea59..72f16be527 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -73,39 +73,38 @@ private static function render_seller_status( $mode ) { } // TODO: Only render when we visit the PayPal tab. - // TODO: If all 3 validate, we should be able to save this to an option and stop making + // TODO: If all 3 validate, we should be able to save this to an option and stop making requests. $status = self::get_seller_status( $mode ); -/* + /* $status = new stdClass(); $status->payments_receivable = true; $status->primary_email_confirmed = true; $status->oauth_integrations = true; - $status->primary_email = 'test@example.com';*/ + $status->primary_email = 'test@example.com'; + */ $status->primary_email_confirmed = false; if ( ! is_object( $status ) ) { - self::render_error( __( 'Unable to retrieve seller status', 'formidable' ) ); + self::render_error( __( 'Unable to retrieve seller status.', 'formidable' ) ); return; } $email = ! empty( $status->primary_email ) ? $status->primary_email : ''; if ( ! $status->primary_email_confirmed ) { - // self::render_error( __( 'Primary email not confirmed', 'formidable' ) ); - // self::render_error( sprintf( __( 'Primary email (%s) not confirmed', 'formidable' ), $email ) ); - self::render_error( __( 'Primary email not confirmed', 'formidable' ) . '
Connected account:
' . $email ); + self::render_error( __( 'Primary email not confirmed.', 'formidable' ), $email ); return; } if ( ! $status->payments_receivable ) { - self::render_error( __( 'Payments are not receivable', 'formidable' ) ); + self::render_error( __( 'Payments are not receivable.', 'formidable' ), $email ); return; } if ( ! $status->oauth_integrations ) { - self::render_error( __( 'OAuth integrations are not enabled', 'formidable' ) ); + self::render_error( __( 'OAuth integrations are not enabled.', 'formidable' ), $email ); return; } @@ -113,15 +112,31 @@ private static function render_seller_status( $mode ) { echo '
'; esc_html_e( 'Your seller status is valid', 'formidable' ); - if ( $email ) { - echo '
Connected account:
' . $email; - } + self::echo_email( $email ); echo '
'; } - private static function render_error( $message ) { + private static function echo_email( $email ) { + if ( ! $email ) { + return; + } + + echo '
'; + echo '' . esc_html__( 'Connected account:', 'formidable' ) . ''; + echo '
'; + echo esc_html( $email ); + } + + /** + * @param string $message + * @param string $email + * + * @return void + */ + private static function render_error( $message, $email = '' ) { echo '
'; echo wp_kses_post( $message ); + self::echo_email( $email ); echo '
'; } From 2d9da1ff2e58853faa805d877b51901b67ac02ec Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 27 Jan 2026 15:02:18 -0400 Subject: [PATCH 54/62] Store a valid seller status as an option to avoid too many calls to PayPal/API --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index 72f16be527..cda43fffb9 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -84,8 +84,6 @@ private static function render_seller_status( $mode ) { $status->primary_email = 'test@example.com'; */ - $status->primary_email_confirmed = false; - if ( ! is_object( $status ) ) { self::render_error( __( 'Unable to retrieve seller status.', 'formidable' ) ); return; @@ -108,6 +106,8 @@ private static function render_seller_status( $mode ) { return; } + update_option( self::get_paypal_seller_status_option_name( $mode ), $status, false ); + $email = ! empty( $status->primary_email ) ? $status->primary_email : ''; echo '
'; @@ -445,6 +445,10 @@ private static function get_client_side_token_option_name( $mode = 'auto' ) { return self::get_paypal_connect_option_name( 'client_password', $mode ); } + private static function get_paypal_seller_status_option_name( $mode = 'auto' ) { + return self::get_paypal_connect_option_name( 'seller_status', $mode ); + } + /** * @return string */ @@ -708,6 +712,7 @@ public static function reset_paypal_api_integration() { delete_option( self::get_client_side_token_option_name( $mode ) ); delete_option( self::get_merchant_currency_option_name( $mode ) ); delete_option( self::get_location_id_option_name( $mode ) ); + delete_option( self::get_paypal_seller_status_option_name( $mode ) ); } /** @@ -796,6 +801,11 @@ public static function create_vault_setup_token() { } public static function get_seller_status( $mode ) { + $status = get_option( self::get_paypal_seller_status_option_name( $mode ) ); + if ( is_object( $status ) ) { + return $status; + } + $additional_body = array( 'frm_paypal_api_mode' => $mode, ); From e8ea08ea1b3a48f7ecac91ee90d74c90c5cb0d18 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 27 Jan 2026 15:25:05 -0400 Subject: [PATCH 55/62] Apply some phpcbf fixes --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index cda43fffb9..2542cd19bf 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -28,12 +28,12 @@ public static function render_settings_container() {
@@ -41,20 +41,20 @@ public static function render_settings_container() {
- + foreach ( $modes as $mode ) { + self::render_settings_for_mode( $mode ); + } + ?>
- +
@@ -97,12 +97,12 @@ private static function render_seller_status( $mode ) { } if ( ! $status->payments_receivable ) { - self::render_error( __( 'Payments are not receivable.', 'formidable' ), $email ); + self::render_error( __( 'Payments are not receivable.', 'formidable' ), $email ); return; } if ( ! $status->oauth_integrations ) { - self::render_error( __( 'OAuth integrations are not enabled.', 'formidable' ), $email ); + self::render_error( __( 'OAuth integrations are not enabled.', 'formidable' ), $email ); return; } @@ -151,44 +151,44 @@ private static function render_settings_for_mode( $mode ) {
- + - + $connected = (bool) self::get_merchant_id( $mode ); + $tag_classes = $connected ? 'frm-lt-green-tag' : 'frm-grey-tag'; + ?>
- 'width: 10px; position: relative; top: 2px; margin-right: 5px;' ) ); - echo 'Connected'; - } else { - echo 'Not configured'; - } - ?> + 'width: 10px; position: relative; top: 2px; margin-right: 5px;' ) ); + echo 'Connected'; + } else { + echo 'Not configured'; + } + ?>
- +
- +
@@ -234,7 +234,7 @@ public static function get_oauth_redirect_url() { } if ( ! empty( $data->password ) ) { - update_option( self::get_server_side_token_option_name( $mode ), $data->password, 'no' ); + update_option( self::get_server_side_token_option_name( $mode ), $data->password, false ); } if ( ! is_object( $data ) || empty( $data->redirect_url ) ) { @@ -312,7 +312,7 @@ private static function validate_response( $response ) { * @return string */ private static function get_url_to_connect_server() { - // return 'https://api.strategy11.com/'; + // Return 'https://api.strategy11.com/'; return 'https://dev-site.local/'; } @@ -432,7 +432,7 @@ private static function get_server_side_token_option_name( $mode = 'auto' ) { */ private static function generate_client_password( $mode ) { $client_password = wp_generate_password(); - update_option( self::get_client_side_token_option_name( $mode ), $client_password, 'no' ); + update_option( self::get_client_side_token_option_name( $mode ), $client_password, false ); return $client_password; } @@ -565,7 +565,7 @@ private static function check_server_for_oauth_merchant_id() { $data = self::post_to_connect_server( 'oauth_merchant_status', $body ); if ( is_object( $data ) && ! empty( $data->merchant_id ) ) { - update_option( self::get_merchant_id_option_name( $mode ), $data->merchant_id, 'no' ); + update_option( self::get_merchant_id_option_name( $mode ), $data->merchant_id, false ); FrmTransLiteAppController::install(); From b901299d7eeb25bac9eec33097ac2fde238ac1a4 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 27 Jan 2026 15:27:27 -0400 Subject: [PATCH 56/62] Only save the option if we also have the email --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index 2542cd19bf..be36717b3e 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -73,7 +73,6 @@ private static function render_seller_status( $mode ) { } // TODO: Only render when we visit the PayPal tab. - // TODO: If all 3 validate, we should be able to save this to an option and stop making requests. $status = self::get_seller_status( $mode ); /* @@ -106,9 +105,11 @@ private static function render_seller_status( $mode ) { return; } - update_option( self::get_paypal_seller_status_option_name( $mode ), $status, false ); + $email = $status->primary_email ?? ''; - $email = ! empty( $status->primary_email ) ? $status->primary_email : ''; + if ( $email ) { + update_option( self::get_paypal_seller_status_option_name( $mode ), $status, false ); + } echo '
'; esc_html_e( 'Your seller status is valid', 'formidable' ); From 729fbafdadf406280a0fab8106b1cfdb553949ed Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 27 Jan 2026 16:07:49 -0400 Subject: [PATCH 57/62] Remove inline HTML into view files --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 90 ++----------------- .../views/settings/connect-settings-box.php | 47 ++++++++++ .../settings/connect-settings-container.php | 39 ++++++++ 3 files changed, 91 insertions(+), 85 deletions(-) create mode 100644 paypal/views/settings/connect-settings-box.php create mode 100644 paypal/views/settings/connect-settings-container.php diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index be36717b3e..16200d0cb4 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -24,41 +24,7 @@ public static function render_settings_container() { FrmPayPalLiteAppHelper::fee_education( 'paypal-global-settings-tip' ); - ?> -
- +
- - - - -
- - - -
- -
-
- -
-
- -
- - - -
- - -
-
-
- - - - -
- 'width: 10px; position: relative; top: 2px; margin-right: 5px;' ) ); - echo 'Connected'; - } else { - echo 'Not configured'; - } - ?> -
-
-
- -
- -
- - - - - - - - - -
-
-
- +
+
+
+ + + +
+ 'width: 10px; position: relative; top: 2px; margin-right: 5px;' ) ); + esc_html_e( 'Connected', 'formidable' ); + } else { + esc_html_e( 'Not configured', 'formidable' ); + } + ?> +
+
+
+ +
+ +
+ + + + + + + + + +
+
+
diff --git a/paypal/views/settings/connect-settings-container.php b/paypal/views/settings/connect-settings-container.php new file mode 100644 index 0000000000..5c794cbeb5 --- /dev/null +++ b/paypal/views/settings/connect-settings-container.php @@ -0,0 +1,39 @@ + + + + + + +
+ + + +
+ +
+
+ +
+
+ +
+ + + +
+ From 279c8ed527760d69647a42e2627cb3100c74a8aa Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 28 Jan 2026 09:27:33 -0400 Subject: [PATCH 58/62] Small code cleanup changes --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 32 +++++++++++++++++-- .../settings/connect-settings-container.php | 2 +- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index 16200d0cb4..83cd8d5908 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -54,7 +54,7 @@ private static function render_seller_status( $mode ) { return; } - $email = ! empty( $status->primary_email ) ? $status->primary_email : ''; + $email = $status->primary_email ?? ''; if ( ! $status->primary_email_confirmed ) { self::render_error( __( 'Primary email not confirmed.', 'formidable' ), $email ); @@ -71,7 +71,11 @@ private static function render_seller_status( $mode ) { return; } - $email = $status->primary_email ?? ''; + $product = self::check_for_product( $status->products ); + if ( ! $product || empty( $product->capabilities ) ) { + self::render_error( __( 'No data was found for expected PayPal product.', 'formidable' ), $email ); + return; + } if ( $email ) { update_option( self::get_paypal_seller_status_option_name( $mode ), $status, false ); @@ -80,9 +84,33 @@ private static function render_seller_status( $mode ) { echo '
'; esc_html_e( 'Your seller status is valid', 'formidable' ); self::echo_email( $email ); + + echo '
'; + echo '
'; + echo '' . esc_html__( 'Enabled capabilities:', 'formidable' ) . ''; + echo '
    '; + $can_process_card_fields = in_array( 'CUSTOM_CARD_PROCESSING', $product->capabilities ); + if ( $can_process_card_fields ) { + echo '
  • ' . esc_html__( 'Card Processing', 'formidable' ) . '
  • '; + } + echo '
'; echo '
'; } + /** + * @param array $products + * + * @return bool|object + */ + private static function check_for_product( $products ) { + foreach ( $products as $current_product ) { + if ( 'PPCP_CUSTOM' === $current_product->name ) { + return $current_product; + } + } + return false; + } + private static function echo_email( $email ) { if ( ! $email ) { return; diff --git a/paypal/views/settings/connect-settings-container.php b/paypal/views/settings/connect-settings-container.php index 5c794cbeb5..a9717cef00 100644 --- a/paypal/views/settings/connect-settings-container.php +++ b/paypal/views/settings/connect-settings-container.php @@ -33,7 +33,7 @@
- +
From 8bc3a134f4ffa4628083b0cb0202d512bdbadfc7 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 28 Jan 2026 12:51:20 -0400 Subject: [PATCH 59/62] Fix mago issue --- paypal/helpers/FrmPayPalLiteConnectHelper.php | 2 +- paypal/views/settings/connect-settings-box.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index 83cd8d5908..bdada7fd40 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -32,7 +32,7 @@ public static function render_settings_container() { * * @return void */ - private static function render_seller_status( $mode ) { + public static function render_seller_status( $mode ) { if ( ! self::get_merchant_id( $mode ) ) { // If not connected, show no status. return; diff --git a/paypal/views/settings/connect-settings-box.php b/paypal/views/settings/connect-settings-box.php index 1353ff198a..5aef8fc4e0 100644 --- a/paypal/views/settings/connect-settings-box.php +++ b/paypal/views/settings/connect-settings-box.php @@ -31,7 +31,7 @@ } ?>
- +
From 5fbe6ffbdd31da0861171abda1f9485907c1978f Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 28 Jan 2026 14:00:46 -0400 Subject: [PATCH 60/62] Only call the PayPal API for seller status when visiting the paypal tab --- .../FrmPayPalLiteHooksController.php | 2 + paypal/helpers/FrmPayPalLiteConnectHelper.php | 73 ++++++++++++++----- paypal/js/settings.js | 33 +++++++++ .../views/settings/connect-settings-box.php | 2 +- .../settings/seller-status-placeholder.php | 9 +++ 5 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 paypal/views/settings/seller-status-placeholder.php diff --git a/paypal/controllers/FrmPayPalLiteHooksController.php b/paypal/controllers/FrmPayPalLiteHooksController.php index 8bec004963..48ce96467d 100644 --- a/paypal/controllers/FrmPayPalLiteHooksController.php +++ b/paypal/controllers/FrmPayPalLiteHooksController.php @@ -56,5 +56,7 @@ private static function load_ajax_hooks() { add_action( 'wp_ajax_frm_paypal_create_vault_setup_token', 'FrmPayPalLiteAppController::create_vault_setup_token' ); add_action( 'wp_ajax_nopriv_frm_paypal_create_vault_setup_token', 'FrmPayPalLiteAppController::create_vault_setup_token' ); + + add_action( 'wp_ajax_frm_paypal_render_seller_status', 'FrmPayPalLiteConnectHelper::handle_render_seller_status' ); } } diff --git a/paypal/helpers/FrmPayPalLiteConnectHelper.php b/paypal/helpers/FrmPayPalLiteConnectHelper.php index bdada7fd40..8ce1404413 100644 --- a/paypal/helpers/FrmPayPalLiteConnectHelper.php +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -27,19 +27,46 @@ public static function render_settings_container() { include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/connect-settings-container.php'; } + public static function handle_render_seller_status() { + FrmAppHelper::permission_check( 'frm_change_settings' ); + + if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) { + wp_send_json_error(); + } + + ob_start(); + $success = self::render_seller_status(); + $response = ob_get_clean(); + + if ( ! $success ) { + wp_send_json_error( $response ); + } + + wp_send_json_success( $response ); + } + /** * @since x.x * - * @return void + * @return bool */ - public static function render_seller_status( $mode ) { - if ( ! self::get_merchant_id( $mode ) ) { - // If not connected, show no status. - return; + public static function render_seller_status() { + FrmAppHelper::permission_check( 'frm_change_settings' ); + + if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) { + self::render_error( __( 'Invalid nonce.', 'formidable' ) ); + return false; } + $mode = self::get_mode_value_from_post(); + if ( ! self::get_merchant_id( $mode ) ) { + // Do not render any message when not connected. + // And return true so it does not try to handle it as an error. + return true; + } + // TODO: Only render when we visit the PayPal tab. - $status = self::get_seller_status( $mode ); + $status = self::get_seller_status(); /* $status = new stdClass(); @@ -51,30 +78,30 @@ public static function render_seller_status( $mode ) { if ( ! is_object( $status ) ) { self::render_error( __( 'Unable to retrieve seller status.', 'formidable' ) ); - return; + return false; } $email = $status->primary_email ?? ''; if ( ! $status->primary_email_confirmed ) { self::render_error( __( 'Primary email not confirmed.', 'formidable' ), $email ); - return; + return false; } if ( ! $status->payments_receivable ) { self::render_error( __( 'Payments are not receivable.', 'formidable' ), $email ); - return; + return false; } if ( ! $status->oauth_integrations ) { self::render_error( __( 'OAuth integrations are not enabled.', 'formidable' ), $email ); - return; + return false; } $product = self::check_for_product( $status->products ); if ( ! $product || empty( $product->capabilities ) ) { self::render_error( __( 'No data was found for expected PayPal product.', 'formidable' ), $email ); - return; + return false; } if ( $email ) { @@ -88,13 +115,26 @@ public static function render_seller_status( $mode ) { echo '
'; echo '
'; echo '' . esc_html__( 'Enabled capabilities:', 'formidable' ) . ''; - echo '
    '; + echo '
      '; $can_process_card_fields = in_array( 'CUSTOM_CARD_PROCESSING', $product->capabilities ); if ( $can_process_card_fields ) { echo '
    • ' . esc_html__( 'Card Processing', 'formidable' ) . '
    • '; } echo '
    '; echo '
'; + + return true; + } + + /** + * @since x.x + * + * @param string $mode + * + * @return void + */ + public static function render_seller_status_placeholder( $mode ) { + include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/seller-status-placeholder.php'; } /** @@ -749,15 +789,14 @@ public static function create_vault_setup_token() { return self::post_with_authenticated_body( 'create_vault_setup_token' ); } - public static function get_seller_status( $mode ) { + public static function get_seller_status() { + $mode = self::get_mode_value_from_post(); $status = get_option( self::get_paypal_seller_status_option_name( $mode ) ); + if ( is_object( $status ) ) { return $status; } - $additional_body = array( - 'frm_paypal_api_mode' => $mode, - ); - return self::post_with_authenticated_body( 'get_seller_status', $additional_body ); + return self::post_with_authenticated_body( 'get_seller_status' ); } } diff --git a/paypal/js/settings.js b/paypal/js/settings.js index 11875b0e77..77020cdfb8 100644 --- a/paypal/js/settings.js +++ b/paypal/js/settings.js @@ -36,4 +36,37 @@ ); } ); + + document.querySelectorAll( '.frm_paypal_seller_status_placeholder' ).forEach( + function( placeholder ) { + const mode = placeholder.dataset.mode; + const interval = setInterval( + function() { + if ( placeholder.offsetParent === null ) { + return; + } + + clearInterval( interval ); + + const formData = new FormData(); + formData.append( 'testMode', 'test' === mode ? 1 : 0 ); + frmDom.ajax.doJsonPost( 'paypal_render_seller_status', formData ) + .then( + function( sellerStatus ) { + placeholder.innerHTML = sellerStatus; + } + ).catch( + function( error ) { + if ( 'string' === typeof error ) { + placeholder.innerHTML = error; + } + + clearInterval( interval ); + } + ); + }, + 100 + ); + } + ); }() ); diff --git a/paypal/views/settings/connect-settings-box.php b/paypal/views/settings/connect-settings-box.php index 5aef8fc4e0..c9fdc6e991 100644 --- a/paypal/views/settings/connect-settings-box.php +++ b/paypal/views/settings/connect-settings-box.php @@ -31,7 +31,7 @@ } ?>
- +
diff --git a/paypal/views/settings/seller-status-placeholder.php b/paypal/views/settings/seller-status-placeholder.php new file mode 100644 index 0000000000..df6b320081 --- /dev/null +++ b/paypal/views/settings/seller-status-placeholder.php @@ -0,0 +1,9 @@ + +
+ +
From 526a504bfb7c1f0cbc15adc7d74fccac2eb347aa Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 29 Jan 2026 15:09:06 -0400 Subject: [PATCH 61/62] Playing with pay later --- paypal/controllers/FrmPayPalLiteActionsController.php | 1 + paypal/js/frontend.js | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index c8785ba6c8..09aee2230f 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -382,6 +382,7 @@ public static function load_scripts( $form_id ) { // Subscriptions appear to require vault=true. // 'intent' => 'subscription', // 'vault' => 'true', + 'enable-funding' => 'paylater', ), 'https://www.paypal.com/sdk/js' ); diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index 202d273aab..ffb58150af 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -35,6 +35,15 @@ // Create the card fields container structure // TODO: Make these IDs unique. cardElement.innerHTML = ` +
+
+
OR
@@ -67,6 +76,7 @@ disableSubmit( thisForm ); paypal.Buttons( { + fundingSource: paypal.FUNDING.PAYLATER, createOrder: createOrder, // createSubscription: createSubscription, onApprove: onApprove, From d3247a093d8da46742661dc7aced371fc94974f3 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 29 Jan 2026 15:09:35 -0400 Subject: [PATCH 62/62] Fix some issues caught with deepscan --- js/admin/settings.js | 4 +- js/admin/style.js | 24 ---------- js/formidable.js | 2 +- js/src/admin/addon-state.js | 42 +++++++++++++++++ js/src/admin/admin.js | 75 +----------------------------- js/src/components/class-overlay.js | 2 +- 6 files changed, 48 insertions(+), 101 deletions(-) diff --git a/js/admin/settings.js b/js/admin/settings.js index 7b9bf085ca..effdfd0c86 100644 --- a/js/admin/settings.js +++ b/js/admin/settings.js @@ -40,7 +40,7 @@ } if ( 'frm-send-test-email' === e.target.id ) { - showSendTestEmailModal( e ); + showSendTestEmailModal(); return; } @@ -62,7 +62,7 @@ document.getElementById( 'frm-email-style-value' ).value = styleKey; } - function showSendTestEmailModal( e ) { + function showSendTestEmailModal() { if ( ! globalVars.sendTestEmailModal ) { globalVars.sendTestEmailModal = frmAdminBuild.initModal( '#frm-send-test-email-modal', '400px' ); } diff --git a/js/admin/style.js b/js/admin/style.js index e8837f7df5..4040f2c328 100644 --- a/js/admin/style.js +++ b/js/admin/style.js @@ -914,14 +914,6 @@ titleElement.textContent = newStyleName; } - /** - * @param {string} templateKey - * @return {HTMLElement} The template card element. - */ - function getTemplateCard( templateKey ) { - return document.getElementById( 'frm_template_style_cards_wrapper' ).querySelector( '.frm-style-card[data-template-key="' + templateKey + '"]' ); - } - /** * @param {string} styleId * @return {HTMLElement} The card element. @@ -1332,22 +1324,6 @@ } } - /** - * @param {Event} event - */ - function maybeCollapseSettings( event ) { - let expanded; - const sectionParent = event.target.parentElement; - if ( event.type === 'keydown' ) { - expanded = sectionParent.classList.toggle( 'open' ); - jQuery( sectionParent.querySelector( '.accordion-section-content' ) ).toggle( ! expanded ).slideToggle( 150 ); // Animate toggle as in click/enter. - } else { - expanded = sectionParent.classList.contains( 'open' ); - } - - event.target.setAttribute( 'aria-expanded', expanded ); - } - /** * @param {HTMLElement} input * @param {HTMLElement} container diff --git a/js/formidable.js b/js/formidable.js index c6043f029a..d4e43910e5 100644 --- a/js/formidable.js +++ b/js/formidable.js @@ -2052,7 +2052,7 @@ function frmFrontFormJS() { /*jshint validthis:true */ const fieldId = frmFrontForm.getFieldId( this, false ); - if ( ! fieldId || typeof fieldId === 'undefined' ) { + if ( ! fieldId ) { return; } diff --git a/js/src/admin/addon-state.js b/js/src/admin/addon-state.js index 64cab9db31..575490ff29 100644 --- a/js/src/admin/addon-state.js +++ b/js/src/admin/addon-state.js @@ -248,3 +248,45 @@ function showUpgradeModalSuccess() { circledIcon.querySelector( 'svg' )?.replaceWith( svg( { href: '#frm_checkmark_icon' } ) ); } } + +function installAddonWithCreds( e ) { + // Prevent the default action, let the user know we are attempting to install again and go with it. + e.preventDefault(); + + // Now let's make another Ajax request once the user has submitted their credentials. + const proceed = jQuery( this ); + const el = proceed.parent().parent(); + const plugin = proceed.attr( 'rel' ); + + proceed.addClass( 'frm_loading_button' ); + + jQuery.ajax( { + url: ajaxurl, + type: 'POST', + async: true, + cache: false, + dataType: 'json', + data: { + action: 'frm_install_addon', + nonce: frmAdminJs.nonce, + plugin: plugin, + hostname: el.find( '#hostname' ).val(), + username: el.find( '#username' ).val(), + password: el.find( '#password' ).val() + }, + success: function( response ) { + response = response?.data ?? response; + + const error = extractErrorFromAddOnResponse( response ); + if ( error ) { + addonError( error, el, proceed ); + return; + } + + afterAddonInstall( response, proceed, message, el ); + }, + error: function() { + proceed.removeClass( 'frm_loading_button' ); + } + } ); +} \ No newline at end of file diff --git a/js/src/admin/admin.js b/js/src/admin/admin.js index 40cea78b75..6f80a8ac0c 100644 --- a/js/src/admin/admin.js +++ b/js/src/admin/admin.js @@ -240,7 +240,7 @@ window.frmAdminBuildJS = function() { const frmAdminJs = frm_admin_js; // eslint-disable-line camelcase const { tag, div, span, a, svg, img } = frmDom; const { onClickPreventDefault } = frmDom.util; - const { doJsonFetch, doJsonPost } = frmDom.ajax; + const { doJsonPost } = frmDom.ajax; frmAdminJs.contextualShortcodes = getContextualShortcodes(); const icons = { save: svg( { href: '#frm_save_icon' } ), @@ -2943,18 +2943,6 @@ window.frmAdminBuildJS = function() { } } - function scrollToField( field ) { - const newPos = field.getBoundingClientRect().top, - container = document.getElementById( 'post-body-content' ); - - if ( typeof animate === 'undefined' ) { - jQuery( container ).scrollTop( newPos ); - } else { - // TODO: smooth scroll - jQuery( container ).animate( { scrollTop: newPos }, 500 ); - } - } - function checkCalculationCreatedByUser() { const calculation = this.value; let warningMessage = checkMatchingParens( calculation ); @@ -5318,8 +5306,7 @@ window.frmAdminBuildJS = function() { function addWatchLookupRow() { /*jshint validthis:true */ - let lastRowId, - id = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' ), + let id = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' ), formId = thisFormId, lookupBlockRows = document.getElementById( 'frm_watch_lookup_block_' + id ).children; jQuery.ajax( { @@ -6473,7 +6460,6 @@ window.frmAdminBuildJS = function() { function getImageLabel( label, showLabelWithImage, imageUrl, fieldType ) { let imageLabelClass, originalLabel = label, - shape = fieldType === 'checkbox' ? 'square' : 'circle', labelImage, labelNode, imageLabel; @@ -9312,63 +9298,6 @@ window.frmAdminBuildJS = function() { addonState.toggleAddonState( clicked, action ); } - function installAddonWithCreds( e ) { - // Prevent the default action, let the user know we are attempting to install again and go with it. - e.preventDefault(); - - // Now let's make another Ajax request once the user has submitted their credentials. - const proceed = jQuery( this ); - const el = proceed.parent().parent(); - const plugin = proceed.attr( 'rel' ); - - proceed.addClass( 'frm_loading_button' ); - - jQuery.ajax( { - url: ajaxurl, - type: 'POST', - async: true, - cache: false, - dataType: 'json', - data: { - action: 'frm_install_addon', - nonce: frmAdminJs.nonce, - plugin: plugin, - hostname: el.find( '#hostname' ).val(), - username: el.find( '#username' ).val(), - password: el.find( '#password' ).val() - }, - success: function( response ) { - response = response?.data ?? response; - - const error = extractErrorFromAddOnResponse( response ); - if ( error ) { - addonError( error, el, proceed ); - return; - } - - afterAddonInstall( response, proceed, message, el ); - }, - error: function() { - proceed.removeClass( 'frm_loading_button' ); - } - } ); - } - - function afterAddonInstall( response, button, message, el, saveAndReload, action = 'frm_activate_addon' ) { - const addonState = require( './addon-state' ); - addonState.afterAddonInstall( response, button, message, el, saveAndReload, action ); - } - - function extractErrorFromAddOnResponse( response ) { - const addonState = require( './addon-state' ); - return addonState.extractErrorFromAddOnResponse( response ); - } - - function addonError( response, el, button ) { - const addonState = require( './addon-state' ); - addonState.addonError( response, el, button ); - } - /* Templates */ function showActiveCampaignForm() { loadApiEmailForm(); diff --git a/js/src/components/class-overlay.js b/js/src/components/class-overlay.js index f9d92c4bc4..ddcd725494 100644 --- a/js/src/components/class-overlay.js +++ b/js/src/components/class-overlay.js @@ -71,7 +71,7 @@ export class frmOverlay { getButtons() { const buttons = this.overlayData.buttons.map( ( button, index ) => { - if ( ! button.url || '' === button.url ) { + if ( ! button.url ) { return ''; } const buttonTypeClassname = 1 === index ? 'frm-button-primary' : 'frm-button-secondary';