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 '
';
+ $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';
+ }
+
+ /**
+ * @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