diff --git a/classes/controllers/FrmHooksController.php b/classes/controllers/FrmHooksController.php index 1bb9c9f88c..227b60d4f0 100644 --- a/classes/controllers/FrmHooksController.php +++ b/classes/controllers/FrmHooksController.php @@ -115,6 +115,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 ); @@ -216,6 +217,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/classes/helpers/FrmTipsHelper.php b/classes/helpers/FrmTipsHelper.php index 456396dd68..11b6e41fa8 100644 --- a/classes/helpers/FrmTipsHelper.php +++ b/classes/helpers/FrmTipsHelper.php @@ -246,22 +246,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', diff --git a/css/custom_theme.css.php b/css/custom_theme.css.php index 56dfd44339..918bbd812a 100644 --- a/css/custom_theme.css.php +++ b/css/custom_theme.css.php @@ -1669,6 +1669,6 @@ .frm-card-errors:empty { margin: 0; } - { - if ( ! button.url || '' === button.url ) { + if ( ! button.url ) { return ''; } const buttonTypeClassname = 1 === index ? 'frm-button-primary' : 'frm-button-secondary'; diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php new file mode 100644 index 0000000000..09aee2230f --- /dev/null +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -0,0 +1,610 @@ +form_id : $field['form_id']; + $actions = self::get_actions_before_submit( $form_id ); + + if ( ! $actions ) { + return $callback; + } + + $field_id = is_object( $field ) ? $field->id : $field['id']; + + foreach ( $actions as $action ) { + if ( (int) $action->post_content['credit_card'] === (int) $field_id ) { + return self::class . '::show_card'; + } + } + + return $callback; + } + + /** + * Override the credit card field HTML if there is a PayPal action. + * + * @since 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 ); + + // phpcs:ignore Universal.Operators.StrictComparisons + if ( ! $amount || $amount == 000 ) { + $response['error'] = __( 'Please specify an amount for the payment', 'formidable' ); + return $response; + } + + if ( ! self::paypal_is_configured() ) { + $response['error'] = __( 'PayPal still needs to be configured.', 'formidable' ); + return $response; + } + + $payment_args = compact( 'form', 'entry', 'action', 'amount' ); + + // Attempt to charge the customer's card. + if ( 'recurring' === $action->post_content['type'] ) { + $charge = self::trigger_recurring_payment( $payment_args ); + } else { + $charge = self::trigger_one_time_payment( $payment_args ); + } + + if ( $charge === true ) { + $response['success'] = true; + } else { + $response['error'] = $charge; + } + + 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 ) { + $paypal_order_id = FrmAppHelper::get_post_param( 'paypal_order_id', '', 'sanitize_text_field' ); + + if ( ! $paypal_order_id ) { + return 'No PayPal order ID found.'; + } + + $response = FrmPayPalLiteConnectHelper::capture_order( $paypal_order_id ); + + if ( false === $response ) { + return 'Failed to confirm order.'; + } + + if ( ! isset( $response->status ) || $response->status !== 'COMPLETED' ) { + 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 = $capture_id ? $capture_id : $paypal_order_id; + $atts['charge']->amount = $atts['amount']; + + $payment_id = self::create_new_payment( $atts ); + $frm_payment = new FrmTransLitePayment(); + $payment = $frm_payment->get_one( $payment_id ); + $status = $atts['status']; + + FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) ); + + /* + 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. + * + * @param array $atts The arguments for the payment. + * + * @return int + */ + private static function create_new_payment( $atts ) { + $atts['charge'] = (object) $atts['charge']; + + $new_values = array( + 'amount' => FrmTransLiteAppHelper::get_formatted_amount_for_currency( $atts['charge']->amount, $atts['action'] ), + 'status' => $atts['status'], + 'paysys' => 'paypal', + 'item_id' => $atts['entry']->id, + 'action_id' => $atts['action']->ID, + 'receipt_id' => $atts['charge']->id, + 'sub_id' => $atts['charge']->sub_id ?? '', + 'test' => 'test' === FrmPayPalLiteAppHelper::active_mode() ? 1 : 0, + ); + + $frm_payment = new FrmTransLitePayment(); + return $frm_payment->create( $new_values ); + } + + /** + * Create a new PayPal subscription and a subscription and payment for the payments tables. + * + * @param array $atts Includes 'customer', 'entry', 'action', 'amount'. + * + * @return bool|string True on success, error message on failure + */ + private static function trigger_recurring_payment( $atts ) { + // TODO + return 'Recurring payments are not yet implemented for PayPal Lite.'; + } + + /** + * 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 ) { + // phpcs:ignore Universal.Operators.StrictComparisons + if ( $params['form_id'] == $params['posted_form_id'] ) { + // This form has already been posted, so we aren't on the first page. + return; + } + + $form = FrmForm::getOne( $params['form_id'] ); + + if ( ! $form ) { + return; + } + + $credit_card_field = FrmField::getAll( + array( + 'fi.form_id' => $form->id, + 'type' => 'credit_card', + ) + ); + + if ( ! $credit_card_field ) { + return; + } + + $payment_actions = self::get_actions_before_submit( $form->id ); + + if ( ! $payment_actions ) { + return; + } + + $found_gateway = false; + + foreach ( $payment_actions as $action ) { + $gateways = $action->post_content['gateway']; + + if ( in_array( 'paypal', (array) $gateways, true ) ) { + $found_gateway = true; + break; + } + } + + if ( ! $found_gateway ) { + return; + } + + self::load_scripts( (int) $form->id ); + } + + /** + * Load front end JavaScript for a PayPal form. + * + * @param int $form_id + * + * @return void + */ + public static function load_scripts( $form_id ) { + 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; + } + + $client_id = self::get_client_id(); + + // Build the PayPal SDK URL with required parameters. + $sdk_url = add_query_arg( + array( + 'client-id' => $client_id, + 'components' => 'buttons,card-fields', + // Use capture for one time payments. + 'intent' => 'capture', + // Subscriptions appear to require vault=true. + // 'intent' => 'subscription', + // 'vault' => 'true', + 'enable-funding' => 'paylater', + ), + 'https://www.paypal.com/sdk/js' + ); + + wp_register_script( + 'paypal-sdk', + $sdk_url, + array(), + null, + false + ); + + $dependencies = array( 'paypal-sdk', 'formidable' ); + $script_url = FrmPayPalLiteAppHelper::plugin_url() . 'js/frontend.js'; + + wp_enqueue_script( + 'formidable-paypal', + $script_url, + $dependencies, + FrmAppHelper::plugin_version(), + false + ); + + $paypal_vars = array( + 'clientId' => $client_id, + 'formId' => $form_id, + 'nonce' => wp_create_nonce( 'frm_paypal_ajax' ), + 'ajax' => esc_url_raw( FrmAppHelper::get_ajax_url() ), + 'settings' => $action_settings, + 'style' => self::get_style_for_js( $form_id ), + ); + + 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'], + 'font-weight' => $settings['field_weight'], + 'padding' => $settings['field_pad'], + 'line-height' => 1.3, + 'border' => self::get_border_shorthand( $settings ), + 'border-radius' => self::get_border_radius( $settings ), + ), + 'input::placeholder' => array( + 'color' => $settings['text_color_disabled'], + ), + '.invalid' => array( + 'color' => $settings['border_color_error'], + ), + ); + + if ( ! empty( $settings['font'] ) ) { + $style['input']['font-family'] = $settings['font']; + } + + /** + * Filter the PayPal card field styles. + * + * @since 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. + * + * @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; + } + + /** + * @since x.x + * + * @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() + ); + } + + /** + * @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'; + } +} diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php new file mode 100644 index 0000000000..bbbecb30eb --- /dev/null +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -0,0 +1,254 @@ + '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 + */ + 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(); + } + + /** + * Create a PayPal order via AJAX. + */ + public static function create_order() { + 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 ( ! $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 ( class_exists( 'FrmLog' ) ) { + $log = new FrmLog(); + $log->add( + array( + 'title' => 'PayPal Order Response', + 'content' => print_r( $order_response, true ), + ) + ); + } + + if ( false === $order_response ) { + wp_send_json_error( 'Failed to create PayPal order' ); + } + + 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 ) ); + } + + /** + * 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 ( ! str_contains( $amount, '[' ) ) { + 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' ) { + FrmAppHelper::sanitize_value( 'wp_kses_post', $v ); + $entry->{$k} = $v; + continue; + } + + if ( is_array( $v ) ) { + foreach ( $v as $f => $value ) { + FrmAppHelper::sanitize_value( 'wp_kses_post', $value ); + $entry->metas[ absint( $f ) ] = $value; + } + } + } + + return $entry; + } + + /** + * Create a PayPal subscription object via AJAX. + * + * @return void + */ + 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 ( ! $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 ) ); + } + + public static function create_vault_setup_token() { + check_ajax_referer( 'frm_paypal_ajax', 'nonce' ); + + $response = FrmPayPalLiteConnectHelper::create_vault_setup_token(); + + if ( false === $response ) { + wp_send_json_error( 'Failed to create PayPal vault setup token' ); + } + + if ( ! isset( $response->token ) ) { + wp_send_json_error( 'Failed to create PayPal vault setup token' ); + } + + wp_send_json_success( array( 'token' => $response->token ) ); + } +} diff --git a/paypal/controllers/FrmPayPalLiteEventsController.php b/paypal/controllers/FrmPayPalLiteEventsController.php new file mode 100644 index 0000000000..0910cae1c5 --- /dev/null +++ b/paypal/controllers/FrmPayPalLiteEventsController.php @@ -0,0 +1,168 @@ +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->count_failed_event( $event_id ); + continue; + } + + $this->handle_event(); + $this->track_handled_event( $event_id ); + FrmPayPalLiteConnectHelper::process_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 ); + + return is_array( $option ) && 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 ); + $failed_count = is_int( $transient ) ? $transient + 1 : 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..48ce96467d --- /dev/null +++ b/paypal/controllers/FrmPayPalLiteHooksController.php @@ -0,0 +1,62 @@ + 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..8ce1404413 --- /dev/null +++ b/paypal/helpers/FrmPayPalLiteConnectHelper.php @@ -0,0 +1,802 @@ +payments_receivable = true; + $status->primary_email_confirmed = true; + $status->oauth_integrations = true; + $status->primary_email = 'test@example.com'; + */ + + if ( ! is_object( $status ) ) { + self::render_error( __( 'Unable to retrieve seller status.', 'formidable' ) ); + return false; + } + + $email = $status->primary_email ?? ''; + + if ( ! $status->primary_email_confirmed ) { + self::render_error( __( 'Primary email not confirmed.', 'formidable' ), $email ); + return false; + } + + if ( ! $status->payments_receivable ) { + self::render_error( __( 'Payments are not receivable.', 'formidable' ), $email ); + return false; + } + + if ( ! $status->oauth_integrations ) { + self::render_error( __( 'OAuth integrations are not enabled.', 'formidable' ), $email ); + 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 false; + } + + if ( $email ) { + update_option( self::get_paypal_seller_status_option_name( $mode ), $status, false ); + } + + echo '
'; + esc_html_e( 'Your seller status is valid', 'formidable' ); + self::echo_email( $email ); + + echo '
'; + echo '
'; + echo '' . esc_html__( 'Enabled capabilities:', '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'; + } + + /** + * @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; + } + + 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 '
'; + } + + /** + * @param string $mode + * + * @return void + */ + public static function render_settings_for_mode( $mode ) { + $connected = (bool) self::get_merchant_id( $mode ); + + include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/connect-settings-box.php'; + } + + /** + * @return void + */ + private static function register_settings_scripts() { + $script_url = FrmPayPalLiteAppHelper::plugin_url() . '/js/settings.js'; + $dependencies = array( 'formidable_dom' ); + $plugin_version = FrmAppHelper::plugin_version(); + wp_register_script( 'formidable_paypal_settings', $script_url, $dependencies, $plugin_version, true ); + wp_enqueue_script( 'formidable_paypal_settings' ); + } + + /** + * @return false|string + */ + public static function get_oauth_redirect_url() { + $mode = FrmAppHelper::get_post_param( 'mode', 'test', 'sanitize_text_field' ); + + if ( self::get_merchant_id( $mode ) ) { + // Do not allow for initialize if there is already a configured account id. + return false; + } + + $additional_body = array( + 'password' => 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, false ); + } + + if ( ! is_object( $data ) || empty( $data->redirect_url ) ) { + return false; + } + + return $data->redirect_url; + } + + /** + * @param string $action + * @param array $additional_body + * + * @return object|string + */ + private static function post_to_connect_server( $action, $additional_body = array() ) { + $body = array( + 'frm_paypal_api_action' => $action, + 'frm_paypal_api_mode' => FrmPayPalLiteAppHelper::active_mode(), + ); + $body = array_merge( $body, $additional_body ); + $url = self::get_url_to_connect_server(); + $headers = self::build_headers_for_post(); + + 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 'https://dev-site.local/'; + } + + /** + * @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, false ); + return $client_password; + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string + */ + private static function get_client_side_token_option_name( $mode = 'auto' ) { + return self::get_paypal_connect_option_name( 'client_password', $mode ); + } + + private static function get_paypal_seller_status_option_name( $mode = 'auto' ) { + return self::get_paypal_connect_option_name( 'seller_status', $mode ); + } + + /** + * @return string + */ + private static function get_mode_value() { + $settings = FrmPayPalLiteAppHelper::get_settings(); + return $settings->settings->test_mode ? 'test' : 'live'; + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return bool|string + */ + public static function get_merchant_id( $mode = 'auto' ) { + if ( 'auto' === $mode ) { + $mode = self::get_mode_value(); + } + return get_option( self::get_merchant_id_option_name( $mode ) ); + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string + */ + private static function get_merchant_id_option_name( $mode = 'auto' ) { + return self::get_paypal_connect_option_name( 'merchant_id', $mode ); + } + + /** + * @param string $mode either 'auto', 'live', or 'test'. + * + * @return string + */ + private static function get_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, false ); + + FrmTransLiteAppController::install(); + + return true; + } + + return false; + } + + /** + * @param string $action + * @param array $additional_body + * + * @return false|object + */ + private static function post_with_authenticated_body( $action, $additional_body = array() ) { + $body = array_merge( self::get_standard_authenticated_body(), $additional_body ); + $response = self::post_to_connect_server( $action, $body ); + + if ( is_object( $response ) ) { + return $response; + } + + if ( is_array( $response ) ) { + // 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; + } + + /** + * @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' ) ); + } + + 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 ) ); + delete_option( self::get_paypal_seller_status_option_name( $mode ) ); + } + + /** + * @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(); + } + + /** + * Create a PayPal order. + * + * @param string $amount + * @param string $currency + * + * @return false|object + */ + public static function create_order( $amount, $currency ) { + return self::post_with_authenticated_body( 'create_order', compact( 'amount', 'currency' ) ); + } + + /** + * @param string $order_id + * + * @return false|object + */ + public static function capture_order( $order_id ) { + return self::post_with_authenticated_body( 'capture_order', compact( 'order_id' ) ); + } + + /** + * @param string $capture_id + * + * @return false|object + */ + public static function refund_payment( $capture_id ) { + return self::post_with_authenticated_body( 'refund_capture', array( 'capture_id' => $capture_id ) ); + } + + /** + * @param string $subscription_id + * + * @return false|object + */ + public static function cancel_subscription( $subscription_id ) { + return self::post_with_authenticated_body( 'cancel_subscription', compact( 'subscription_id' ) ); + } + + /** + * @param array $data Subscription data. + * + * @return false|object + */ + public static function create_subscription( $data ) { + return self::post_with_authenticated_body( 'create_subscription', compact( 'data' ) ); + } + + public static function create_vault_setup_token() { + return self::post_with_authenticated_body( 'create_vault_setup_token' ); + } + + 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; + } + + return self::post_with_authenticated_body( 'get_seller_status' ); + } +} 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..ffb58150af --- /dev/null +++ b/paypal/js/frontend.js @@ -0,0 +1,544 @@ +( function() { + 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; + let submitEvent = 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; + } + + cardElement.classList.add( 'frm_grid_container' ); + + // Create the card fields container structure + // TODO: Make these IDs unique. + cardElement.innerHTML = ` +
+
+ +
+
OR
+
+
+
+ `; + + thisForm = cardElement.closest( 'form' ); + + const cardFieldsConfig = { + createOrder: createOrder, + // createSubscription: createSubscription, + // createVaultSetupToken: createVaultSetupToken, + onApprove: onApprove, + onError: onError, + style: getCardFieldStyles(), + inputEvents: { + onChange: data => { + cardFieldsValid = data.isFormValid; + + if ( cardFieldsValid ) { + enableSubmit(); + } else { + disableSubmit( thisForm ); + } + } + } + }; + + disableSubmit( thisForm ); + + paypal.Buttons( { + fundingSource: paypal.FUNDING.PAYLATER, + createOrder: createOrder, + // createSubscription: createSubscription, + onApprove: onApprove, + onError: onError, + // TODO: Add onCancel? We can remove the loading spinner from the submit button. + style: {}, + } ).render( '#paypal-button-container' ); + + 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 {}; + } + + /** + * Create a PayPal order via AJAX. + * + * @return {Promise} The order ID. + */ + async function createOrder() { + thisForm.classList.add( 'frm_loading_form' ); + + 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 + } ); + + 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; + } + + 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.subscriptionID ) { + thisForm.classList.remove( 'frm_loading_form' ); + + if ( 'string' === typeof orderData.data ) { + throw new TypeError( orderData.data ); + } + + throw new Error( 'Failed to create PayPal subscription' ); + } + + return orderData.data.subscriptionID; + } + + async function createVaultSetupToken() { + const formData = new FormData( thisForm ); + formData.append( 'action', 'frm_paypal_create_vault_setup_token' ); + 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 ) { + throw new Error( 'Failed to create PayPal vault setup token' ); + } + + const tokenData = await response.json(); + + if ( ! tokenData.success || ! tokenData.data.token ) { + throw new Error( tokenData.data || 'Failed to create PayPal vault setup token' ); + } + + return tokenData.data.token; + } + + /** + * Handle approved payment. + * + * @param {Object} data The approval data containing orderID. + */ + async function onApprove( data ) { + console.log( '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.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.target = thisForm; + } + + // Submit the form + if ( typeof frmFrontForm.submitFormManual === 'function' ) { + frmFrontForm.submitFormManual( submitEvent, 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(); + + submitEvent = event; + + 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 ); + + 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. + That's included in frmstrp.js though, so we need to add a script in Pro for PayPal as well. + + 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( submitArgs ); + } 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; + } + + // 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.' ); + } + } + + 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' ); + return; + } + + paypalInit(); + + jQuery( document ).on( 'frmPageChanged', function() { + paypalInit(); + } ); + } ); +}() ); diff --git a/paypal/js/settings.js b/paypal/js/settings.js new file mode 100644 index 0000000000..77020cdfb8 --- /dev/null +++ b/paypal/js/settings.js @@ -0,0 +1,72 @@ +( function() { + 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(); + } + } + ); + } + ); + + 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/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/connect-settings-box.php b/paypal/views/settings/connect-settings-box.php new file mode 100644 index 0000000000..c9fdc6e991 --- /dev/null +++ b/paypal/views/settings/connect-settings-box.php @@ -0,0 +1,47 @@ + +
+
+
+ + + +
+ '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..a9717cef00 --- /dev/null +++ b/paypal/views/settings/connect-settings-container.php @@ -0,0 +1,39 @@ + + + + + + +
+ + + +
+ +
+
+ +
+
+ +
+ + + +
+ diff --git a/paypal/views/settings/form.php b/paypal/views/settings/form.php new file mode 100644 index 0000000000..88f0bf2246 --- /dev/null +++ b/paypal/views/settings/form.php @@ -0,0 +1,17 @@ + +
+

+
+ +
+ +
+ +
diff --git a/psalm.xml b/psalm.xml index 47e8356c01..a6aeee801f 100644 --- a/psalm.xml +++ b/psalm.xml @@ -14,6 +14,7 @@ + @@ -33,6 +34,7 @@ + @@ -86,6 +88,7 @@ + @@ -94,6 +97,7 @@ + @@ -101,6 +105,7 @@ + @@ -109,6 +114,7 @@ + @@ -116,6 +122,7 @@ + @@ -133,6 +140,7 @@ + @@ -140,6 +148,7 @@ + @@ -147,6 +156,7 @@ + @@ -154,6 +164,7 @@ + @@ -161,6 +172,7 @@ + @@ -175,6 +187,7 @@ + @@ -187,6 +200,7 @@ + @@ -219,6 +233,7 @@ + @@ -226,6 +241,7 @@ + @@ -233,6 +249,7 @@ + @@ -240,6 +257,7 @@ + @@ -247,6 +265,7 @@ + @@ -254,6 +273,7 @@ + @@ -261,6 +281,7 @@ + @@ -269,6 +290,7 @@ + @@ -276,6 +298,7 @@ + @@ -283,6 +306,7 @@ + @@ -290,6 +314,7 @@ + @@ -297,6 +322,7 @@ + @@ -304,6 +330,7 @@ + @@ -311,6 +338,7 @@ + @@ -319,6 +347,7 @@ + @@ -326,6 +355,7 @@ + @@ -342,6 +372,7 @@ + @@ -349,6 +380,7 @@ + @@ -356,6 +388,7 @@ + @@ -369,6 +402,7 @@ + @@ -376,6 +410,7 @@ + @@ -383,6 +418,7 @@ + @@ -448,6 +484,7 @@ + @@ -455,6 +492,7 @@ + @@ -499,6 +537,8 @@ + + @@ -506,6 +546,8 @@ + + diff --git a/rector.php b/rector.php index fe4ada3676..68cb70c176 100644 --- a/rector.php +++ b/rector.php @@ -77,6 +77,7 @@ __DIR__ . '/classes', __DIR__ . '/stripe', __DIR__ . '/square', + __DIR__ . '/paypal', __DIR__ . '/css', __DIR__ . '/tests', ) diff --git a/square/controllers/FrmSquareLiteActionsController.php b/square/controllers/FrmSquareLiteActionsController.php index 98665b8865..e889c0e302 100644 --- a/square/controllers/FrmSquareLiteActionsController.php +++ b/square/controllers/FrmSquareLiteActionsController.php @@ -116,7 +116,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/square/helpers/FrmSquareLiteConnectHelper.php b/square/helpers/FrmSquareLiteConnectHelper.php index bac2a2b06e..4695fdde5c 100644 --- a/square/helpers/FrmSquareLiteConnectHelper.php +++ b/square/helpers/FrmSquareLiteConnectHelper.php @@ -72,7 +72,7 @@ private static function render_settings_for_mode( $mode ) { // phpcs:disable Generic.WhiteSpace.ScopeIndent ?>
-
+
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 ee1d1fd227..30049cb116 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; diff --git a/stripe/js/frmtrans_admin.js b/stripe/js/frmtrans_admin.js index 6833da4a46..11bf3c9715 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 bb85d5ee3d..432a75bcc1 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,12 +15,23 @@ 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(); } ?>
+

+ + +

+