gateway = $gateway; $this->gateway_environment = get_option( "pmpro_gateway_environment" ); if ( true === $this->dependencies() ) { $this->loadStripeLibrary(); Stripe\Stripe::setApiKey( $this->get_secretkey() ); Stripe\Stripe::setAPIVersion( PMPRO_STRIPE_API_VERSION ); Stripe\Stripe::setAppInfo( 'WordPress Paid Memberships Pro', PMPRO_VERSION, 'https://www.paidmembershipspro.com', 'pp_partner_DKlIQ5DD7SFW3A' ); self::$is_loaded = true; } return $this->gateway; } /**************************************** ************ STATIC METHODS ************ ****************************************/ /** * Check whether or not a gateway supports a specific feature. * * @since 3.0 * * @return bool|string */ public static function supports( $feature ) { $supports = array( 'subscription_sync' => true, 'payment_method_updates' => 'individual', 'check_token_orders' => true, ); if ( empty( $supports[$feature] ) ) { return false; } return $supports[$feature]; } /** * Load the Stripe API library. * * @since 1.8 * Moved into a method in version 1.8 so we only load it when needed. */ public static function loadStripeLibrary() { //load Stripe library if it hasn't been loaded already (usually by another plugin using Stripe) if ( ! class_exists( "Stripe\Stripe" ) ) { require_once( PMPRO_DIR . "/includes/lib/Stripe/init.php" ); } else { // Another plugin may have loaded the Stripe library already. // Let's log the current Stripe Library info so that we know // where to look if we need to troubleshoot library conflicts. $previously_loaded_class = new \ReflectionClass( 'Stripe\Stripe' ); pmpro_track_library_conflict( 'stripe', $previously_loaded_class->getFileName(), Stripe\Stripe::VERSION ); } } /** * Run on WP init * * @since 1.8 */ public static function init() { //make sure Stripe is a gateway option add_filter( 'pmpro_gateways', array( 'PMProGateway_stripe', 'pmpro_gateways' ) ); //old global RE showing billing address or not global $pmpro_stripe_lite; $pmpro_stripe_lite = apply_filters( "pmpro_stripe_lite", ! get_option( "pmpro_stripe_billingaddress" ) ); //default is opposite of the stripe_billingaddress setting $gateway = pmpro_getGateway(); if($gateway == "stripe") { add_filter( 'pmpro_required_billing_fields', array( 'PMProGateway_stripe', 'pmpro_required_billing_fields' ) ); } //AJAX services for creating/disabling webhooks add_action( 'wp_ajax_pmpro_stripe_create_webhook', array( 'PMProGateway_stripe', 'wp_ajax_pmpro_stripe_create_webhook' ) ); add_action( 'wp_ajax_pmpro_stripe_delete_webhook', array( 'PMProGateway_stripe', 'wp_ajax_pmpro_stripe_delete_webhook' ) ); add_action( 'wp_ajax_pmpro_stripe_rebuild_webhook', array( 'PMProGateway_stripe', 'wp_ajax_pmpro_stripe_rebuild_webhook' ) ); add_action( 'wp_ajax_pmpro_stripe_refresh_publishable_key', array( 'PMProGateway_stripe', 'wp_ajax_pmpro_stripe_refresh_publishable_key' ) ); add_action( 'wp_ajax_nopriv_pmpro_stripe_refresh_publishable_key', array( 'PMProGateway_stripe', 'wp_ajax_pmpro_stripe_refresh_publishable_key' ) ); //code to add at checkout if Stripe is the current gateway $default_gateway = get_option( 'pmpro_gateway' ); $current_gateway = pmpro_getGateway(); // $_REQUEST['review'] here means the PayPal Express review pag if ( ( $default_gateway == "stripe" || $current_gateway == "stripe" ) && empty( $_REQUEST['review'] ) ) { add_filter( 'pmpro_include_billing_address_fields', array( 'PMProGateway_stripe', 'pmpro_include_billing_address_fields' ) ); if ( ! self::using_stripe_checkout() ) { // On-site checkout flow. add_action( 'pmpro_after_checkout_preheader', array( 'PMProGateway_stripe', 'pmpro_checkout_after_preheader' ) ); add_action( 'pmpro_billing_preheader', array( 'PMProGateway_stripe', 'pmpro_checkout_after_preheader' ) ); add_filter( 'pmpro_checkout_order', array( 'PMProGateway_stripe', 'pmpro_checkout_order' ) ); add_filter( 'pmpro_billing_order', array( 'PMProGateway_stripe', 'pmpro_checkout_order' ) ); add_filter( 'pmpro_include_payment_information_fields', array( 'PMProGateway_stripe', 'pmpro_include_payment_information_fields' ) ); add_filter( 'pmpro_after_checkout_preheader', array( 'PMProGateway_stripe', 'clear_pmpro_review' ) ); } else { // Checkout flow for Stripe Checkout. add_filter('pmpro_include_payment_information_fields', array('PMProGateway_stripe', 'show_stripe_checkout_pending_warning')); } } add_action( 'pmpro_payment_option_fields', array( 'PMProGateway_stripe', 'pmpro_set_up_apple_pay' ), 10, 2 ); add_action( 'init', array( 'PMProGateway_stripe', 'clear_saved_subscriptions' ) ); add_action( 'pmpro_billing_preheader', array( 'PMProGateway_stripe', 'pmpro_billing_preheader_stripe_customer_portal' ), 5 ); add_action( 'wp_update_user', array( 'PMProGateway_stripe', 'update_customer_for_user' ), 10, 1 ); // Stripe Connect functions. add_action( 'admin_init', array( 'PMProGateway_stripe', 'stripe_connect_save_options' ) ); add_action( 'admin_notices', array( 'PMProGateway_stripe', 'stripe_connect_show_errors' ) ); add_action( 'admin_notices', array( 'PMProGateway_stripe', 'stripe_connect_deauthorize' ) ); // Connection test: run daily, run on demand from the payment settings page, and warn admins about failures. add_action( 'pmpro_schedule_daily', array( 'PMProGateway_stripe', 'run_scheduled_connection_test' ) ); add_action( 'admin_init', array( 'PMProGateway_stripe', 'maybe_run_connection_test_on_demand' ) ); add_action( 'wp_ajax_pmpro_stripe_run_connection_test', array( 'PMProGateway_stripe', 'wp_ajax_pmpro_stripe_run_connection_test' ) ); add_action( 'admin_notices', array( 'PMProGateway_stripe', 'show_stripe_connection_notice' ) ); add_filter( 'pmpro_payment_settings_gateway_status_html', array( 'PMProGateway_stripe', 'filter_payment_settings_gateway_status_html' ), 10, 2 ); // Show warning if webhooks are not set up. add_action( 'admin_notices', array( 'PMProGateway_stripe', 'show_stripe_webhook_setup_notice' ) ); add_filter( 'pmpro_process_refund_stripe', array( 'PMProGateway_stripe', 'process_refund' ), 10, 2 ); } /** * Clear any saved (preserved) subscription IDs that should have been processed and are now timed out. */ public static function clear_saved_subscriptions() { if ( ! is_user_logged_in() ) { return; } global $current_user; $preserve = get_user_meta( $current_user->ID, 'pmpro_stripe_dont_cancel', true ); // Clean up the subscription timeout values (if applicable) if ( ! empty( $preserve ) ) { foreach ( $preserve as $sub_id => $timestamp ) { // Make sure the ID has "timed out" (more than 3 days since it was last updated/added. if ( intval( $timestamp ) >= ( current_time( 'timestamp' ) + ( 3 * DAY_IN_SECONDS ) ) ) { unset( $preserve[ $sub_id ] ); } } update_user_meta( $current_user->ID, 'pmpro_stripe_dont_cancel', $preserve ); } } /** * Make sure Stripe is in the gateways list * * @since 1.8 */ public static function pmpro_gateways( $gateways ) { if ( empty( $gateways['stripe'] ) ) { $gateways['stripe'] = __( 'Stripe', 'paid-memberships-pro' ); } return $gateways; } /** * Get a description for this gateway. * * @since 3.5 * * @return string */ public static function get_description_for_gateway_settings() { return esc_html__( 'With Stripe, you can accept membership payment onsite or offsite. The Stripe gateway supports over 135 currencies, built-in tax collection, and multiple payment methods like credit card, Google Pay, Apple Pay, and over 40 region-specific options.', 'paid-memberships-pro' ); } /** * Get a list of payment options that the Stripe gateway needs/supports. * * @since 1.8 * @deprecated 3.5 */ public static function getGatewayOptions() { _deprecated_function( __METHOD__, '3.5' ); $options = array( 'gateway_environment', 'stripe_secretkey', 'stripe_publishablekey', 'live_stripe_connect_user_id', 'live_stripe_connect_secretkey', 'live_stripe_connect_publishablekey', 'sandbox_stripe_connect_user_id', 'sandbox_stripe_connect_secretkey', 'sandbox_stripe_connect_publishablekey', 'stripe_webhook', 'stripe_billingaddress', 'currency', 'tax_state', 'tax_rate', 'stripe_payment_request_button', 'stripe_payment_flow', // 'onsite' or 'checkout' 'stripe_checkout_billing_address', //'auto' or 'required' 'stripe_tax', // 'no', 'inclusive', 'exclusive' 'stripe_tax_id_collection_enabled', // '0', '1' ); return $options; } /** * Set payment options for payment settings page. * * @since 1.8 * @deprecated 3.5 */ public static function pmpro_payment_options( $options ) { _deprecated_function( __METHOD__, '3.5' ); //get stripe options $stripe_options = self::getGatewayOptions(); //merge with others. $options = array_merge( $stripe_options, $options ); return $options; } /** * Display fields for Stripe options. * * @since 1.8 * @deprecated 3.5 */ public static function pmpro_payment_option_fields( $values, $gateway ) { _deprecated_function( __METHOD__, '3.5', 'PMProGateway_stripe::show_settings_fields()' ); $stripe = new PMProGateway_stripe(); // Show connect fields. $stripe->show_connect_payment_option_fields( true, $values, $gateway ); // Show live connect fields. $stripe->show_connect_payment_option_fields( false, $values, $gateway ); // Show sandbox connect fields. // If we have a webhook, make sure it has all the necessary events. $webhook = $stripe->does_webhook_exist(); if ( is_array( $webhook ) && isset( $webhook['enabled_events'] ) ) { $events = $stripe->check_missing_webhook_events( $webhook['enabled_events'] ); if ( $events ) { $stripe->update_webhook_events(); } } // Break the country cache in case we switched accounts. delete_transient( 'pmpro_stripe_account_country' ); ?> style="display: none;">

show_legacy_keys_settings() ) {?>style="display: none;">

show_legacy_keys_settings() ) {?>

show_legacy_keys_settings() ) { ?>style="display: none;">

show_legacy_keys_settings() ) { ?>style="display: none;"> style="display: none;">

style="display: none;">

get_site_webhook_url() ); ?>

style="display: none;">

style="display: none;"> get_secretkey() ) ) { $required_webhook_events = self::webhook_events(); sort( $required_webhook_events ); $failed_webhooks = array(); $missing_webhooks = array(); $working_webhooks = array(); // For sites that tracked "last webhook received" before we started tracking webhook events individually, // we want to ignore events that were sent by Stripe before site was updated to start tracking individual events. $legacy_last_webhook_received_timestamp = get_option( 'pmpro_stripe_last_webhook_received_' . $stripe->gateway_environment ); foreach ( $required_webhook_events as $required_webhook_event ) { $event_data = array( 'name' => $required_webhook_event ); $last_received = get_option( 'pmpro_stripe_webhook_last_received_' . $stripe->gateway_environment . '_' . $required_webhook_event ); $event_data['last_received'] = empty( $last_received ) ? esc_html__( 'Never Received', 'paid-memberships-pro' ) : date_i18n( get_option('date_format') . ' ' . get_option('time_format'), $last_received ); // Check the cache for a recently sent webhook. $cache_key = 'pmpro_stripe_last_webhook_sent_' . $stripe->gateway_environment . '_' . $required_webhook_event; $recently_sent = get_transient( $cache_key ); if ( false === $recently_sent ) { // No cache, so check Stripe for a recently sent webhook. // We want to ignore events that were sent by Stripe before site was updated to start tracking individual events. // (We don't want to ignore events that were sent by Stripe before the site was updated to start tracking individual events // if the site was updated to start tracking individual events before the webhook was sent. $event_query_arr = array( 'limit' => 1, 'created' => array( 'lt' => time() - 60, // Ignore events created in the last 60 seconds in case we haven't finished processing them yet. ), 'type' => $required_webhook_event, ); if ( ! empty( $legacy_last_webhook_received_timestamp ) ) { $event_query_arr['created']['gt'] = strtotime( $legacy_last_webhook_received_timestamp ); } try { $recently_sent_arr = Stripe\Event::all( $event_query_arr ); $recently_sent = empty( $recently_sent_arr->data[0] ) ? '' : $recently_sent_arr->data[0]; } catch ( \Throwable $th ) { $recently_sent = $th->getMessage(); } catch ( \Exception $e ) { $recently_sent = $e->getMessage(); } // Cache the result for 5 minutes. set_transient( $cache_key, $recently_sent, 5 * MINUTE_IN_SECONDS ); } if ( ! empty( $recently_sent ) && ! is_string( $recently_sent ) ) { if ( $last_received >= $recently_sent->created ) { $event_data['status'] = '' . esc_html__( 'Working', 'paid-memberships-pro' ) . ''; $working_webhooks[] = $event_data; } else { $event_data['status'] = '' . esc_html__( 'Last Sent ', 'paid-memberships-pro' ) . date_i18n( get_option('date_format') . ' ' . get_option('time_format'), $recently_sent->created ) . ''; $failed_webhooks[] = $event_data; } } elseif ( is_string( $recently_sent ) && ! empty( $recently_sent ) ) { // An error was returned from the Stripe API. Show it. $event_data['status'] = '' . esc_html__( 'Error: ', 'paid-memberships-pro' ) . $recently_sent . ''; $failed_webhooks[] = $event_data; } else { if ( ! empty( $last_received ) ) { $event_data['status'] = '' . esc_html__( 'Working', 'paid-memberships-pro' ) . ''; $working_webhooks[] = $event_data; } else { $event_data['status'] = '' . esc_html__( 'N/A', 'paid-memberships-pro' ) . ''; $missing_webhooks[] = $event_data; } } } if ( ! empty( $failed_webhooks ) ) { echo '

'. esc_html__( 'Some webhooks recently sent by Stripe have not been received by your website. Please ensure that you have a webhook set up in Stripe for the Webhook URL shown above with all of the listed event types active. To test an event type again, please resend the most recent webhook event of that type from the Stripe webhook settings page or wait for it to be sent again in the future.', 'paid-memberships-pro' ) . '

'; } elseif ( ! empty( $missing_webhooks ) ) { echo '

'. esc_html__( 'Some event types have not yet been triggered in Stripe. More information will be available here once Stripe attempts to send webhooks for each event type. In the meantime, please ensure that you have a webhook set up in Stripe for the Webhook URL shown below with all of the listed event types active.', 'paid-memberships-pro' ) . '

'; } else { echo '

'. esc_html__( 'All webhooks appear to be working correctly.', 'paid-memberships-pro' ) . '

'; } ?>
array( 'style' => array() ) ) ); ?>
style="display: none;">

style="display: none;">

style="display: none;">

If No, make sure you disable address verification in your Stripe Radar rules.', 'paid-memberships-pro' ), array( 'br' => array(), 'strong' => array(), 'a' => array( 'href' => array(), 'target' => array() ) ) ), 'https://dashboard.stripe.com/settings/radar/rules' ); ?>

style="display: none;"> array ( 'href' => array(), 'target' => array(), 'title' => array(), ), ); ?>

More Information', 'paid-memberships-pro' ), $allowed_stripe_payment_button_html ), 'https://stripe.com/docs/stripe-js/elements/payment-request-button#verifying-your-domain-with-apple-pay' ); ?>

array ( 'href' => array(), 'target' => array(), 'title' => array(), ), ); if ( empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off" ) { $payment_request_error_escaped = sprintf( wp_kses( __( 'This webpage is being served over HTTP, but the Stripe Payment Request Button will only work on pages being served over HTTPS. To resolve this, you must set up WordPress to always use HTTPS.', 'paid-memberships-pro' ), $allowed_payment_request_error_html ), 'https://www.paidmembershipspro.com/configuring-wordpress-always-use-httpsssl/?utm_source=plugin&utm_medium=pmpro-paymentsettings&utm_campaign=blog&utm_content=configure-https' ); } elseif ( ! $stripe->pmpro_does_apple_pay_domain_exist() ) { $payment_request_error_escaped = sprintf( wp_kses( __( 'Your domain could not be registered with Apple to enable Apple Pay. Please try registering your domain manually from the Apple Pay settings page in Stripe.', 'paid-memberships-pro' ), $allowed_payment_request_error_html ), 'https://dashboard.stripe.com/settings/payments/apple_pay' ); } if ( ! empty( $payment_request_error_escaped ) ) { ?>

style="display: none;"> style="display: none;"> array ( 'href' => array(), 'target' => array(), 'title' => array(), ), ); ?>

activate Stripe Tax in your Stripe dashboard. More information about Stripe Tax »', 'paid-memberships-pro' ), $allowed_stripe_tax_description_html ), 'https://dashboard.stripe.com/settings/tax/activate', 'https://stripe.com/tax' ); ?>

style="display: none;">

' . esc_html__( 'Stripe documentation', 'paid-memberships-pro' ) . '' ); ?>

show_connection_settings_section( true ); // Show live connect fields. $stripe->show_connection_settings_section( false ); // Show sandbox connect fields. ?>
get_secretkey() ) { ?>style="display: none;"> get_secretkey() ) { echo '

' . esc_html__( 'You must connect to Stripe before you can set up a webhook.', 'paid-memberships-pro' ) . '

'; } else { ?> does_webhook_exist(); if ( is_array( $webhook ) && isset( $webhook['enabled_events'] ) ) { $events = $stripe->check_missing_webhook_events( $webhook['enabled_events'] ); if ( $events ) { $stripe->update_webhook_events(); } } ?>

get_site_webhook_url() ); ?>

$required_webhook_event ); $last_received = get_option( 'pmpro_stripe_webhook_last_received_' . $environment . '_' . $required_webhook_event ); $event_data['last_received'] = empty( $last_received ) ? esc_html__( 'Never Received', 'paid-memberships-pro' ) : date_i18n( get_option('date_format') . ' ' . get_option('time_format'), $last_received ); // Check the cache for a recently sent webhook. $cache_key = 'pmpro_stripe_last_webhook_sent_' . $environment . '_' . $required_webhook_event; $recently_sent = get_transient( $cache_key ); if ( false === $recently_sent ) { // No cache, so check Stripe for a recently sent webhook. // We want to ignore events that were sent by Stripe before site was updated to start tracking individual events. // (We don't want to ignore events that were sent by Stripe before the site was updated to start tracking individual events // if the site was updated to start tracking individual events before the webhook was sent. $event_query_arr = array( 'limit' => 1, 'created' => array( 'lt' => time() - 60, // Ignore events created in the last 60 seconds in case we haven't finished processing them yet. ), 'type' => $required_webhook_event, ); if ( ! empty( $legacy_last_webhook_received_timestamp ) ) { $event_query_arr['created']['gt'] = strtotime( $legacy_last_webhook_received_timestamp ); } try { $recently_sent_arr = Stripe\Event::all( $event_query_arr ); $recently_sent = empty( $recently_sent_arr->data[0] ) ? '' : $recently_sent_arr->data[0]; } catch ( \Throwable $th ) { $recently_sent = $th->getMessage(); } catch ( \Exception $e ) { $recently_sent = $e->getMessage(); } // Cache the result for 5 minutes. set_transient( $cache_key, $recently_sent, 5 * MINUTE_IN_SECONDS ); } if ( ! empty( $recently_sent ) && ! is_string( $recently_sent ) ) { if ( $last_received >= $recently_sent->created ) { $event_data['status'] = '' . esc_html__( 'Working', 'paid-memberships-pro' ) . ''; $working_webhooks[] = $event_data; } else { $event_data['status'] = '' . esc_html__( 'Last Sent ', 'paid-memberships-pro' ) . date_i18n( get_option('date_format') . ' ' . get_option('time_format'), $recently_sent->created ) . ''; $failed_webhooks[] = $event_data; } } elseif ( is_string( $recently_sent ) && ! empty( $recently_sent ) ) { // An error was returned from the Stripe API. Show it. $event_data['status'] = '' . esc_html__( 'Error: ', 'paid-memberships-pro' ) . $recently_sent . ''; $failed_webhooks[] = $event_data; } else { if ( ! empty( $last_received ) ) { $event_data['status'] = '' . esc_html__( 'Working', 'paid-memberships-pro' ) . ''; $working_webhooks[] = $event_data; } else { $event_data['status'] = '' . esc_html__( 'N/A', 'paid-memberships-pro' ) . ''; $missing_webhooks[] = $event_data; } } } if ( ! empty( $failed_webhooks ) ) { echo '

'. esc_html__( 'Some webhooks recently sent by Stripe have not been received by your website. Please ensure that you have a webhook set up in Stripe for the Webhook URL shown above with all of the listed event types active. To test an event type again, please resend the most recent webhook event of that type from the Stripe webhook settings page or wait for it to be sent again in the future.', 'paid-memberships-pro' ) . '

'; } elseif ( ! empty( $missing_webhooks ) ) { echo '

'. esc_html__( 'Some event types have not yet been triggered in Stripe. More information will be available here once Stripe attempts to send webhooks for each event type. In the meantime, please ensure that you have a webhook set up in Stripe for the Webhook URL shown below with all of the listed event types active.', 'paid-memberships-pro' ) . '

'; } else { echo '

'. esc_html__( 'All webhooks appear to be working correctly.', 'paid-memberships-pro' ) . '

'; } ?>
array( 'style' => array() ) ) ); ?>

array ( 'href' => array(), 'target' => array(), 'title' => array(), ), ); ?>

More Information', 'paid-memberships-pro' ), $allowed_stripe_payment_button_html ), 'https://stripe.com/docs/stripe-js/elements/payment-request-button#verifying-your-domain-with-apple-pay' ); ?>

array ( 'href' => array(), 'target' => array(), 'title' => array(), ), ); if ( empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off" ) { $payment_request_error_escaped = sprintf( wp_kses( __( 'This webpage is being served over HTTP, but the Stripe Payment Request Button will only work on pages being served over HTTPS. To resolve this, you must set up WordPress to always use HTTPS.', 'paid-memberships-pro' ), $allowed_payment_request_error_html ), 'https://www.paidmembershipspro.com/configuring-wordpress-always-use-httpsssl/?utm_source=plugin&utm_medium=pmpro-paymentsettings&utm_campaign=blog&utm_content=configure-https' ); } elseif ( ! $stripe->pmpro_does_apple_pay_domain_exist() ) { $payment_request_error_escaped = sprintf( wp_kses( __( 'Your domain could not be registered with Apple to enable Apple Pay. Please try registering your domain manually from the Apple Pay settings page in Stripe.', 'paid-memberships-pro' ), $allowed_payment_request_error_html ), 'https://dashboard.stripe.com/settings/payments/apple_pay' ); } if ( ! empty( $payment_request_error_escaped ) ) { ?>

If No, make sure you disable address verification in your Stripe Radar rules.', 'paid-memberships-pro' ), array( 'br' => array(), 'strong' => array(), 'a' => array( 'href' => array(), 'target' => array() ) ) ), 'https://dashboard.stripe.com/settings/radar/rules' ); ?>

array ( 'href' => array(), 'target' => array(), 'title' => array(), ), ); ?>

activate Stripe Tax in your Stripe dashboard. More information about Stripe Tax »', 'paid-memberships-pro' ), $allowed_stripe_tax_description_html ), 'https://dashboard.stripe.com/settings/tax/activate', 'https://stripe.com/tax' ); ?>

false, 'notice' => 'error', 'message' => esc_html__( 'You do not have permission to perform this action.', 'paid-memberships-pro' ), ); if ( $silent ) { return $r; } echo wp_json_encode( $r ); // Values escaped above. exit; } if ( wp_doing_ajax() && false === check_ajax_referer( 'pmpro_stripe_webhook_nonce', 'nonce', false ) ) { $r = array( 'success' => false, 'notice' => 'error', 'message' => esc_html__( 'Your session has expired. Please refresh and try again.', 'paid-memberships-pro' ), ); if ( $silent ) { return $r; } echo wp_json_encode( $r ); // Values escaped above. exit; } return true; } /** * AJAX callback to create webhooks. */ public static function wp_ajax_pmpro_stripe_create_webhook( $silent = false ) { $access_check = self::authorize_stripe_webhook_request( $silent ); if ( true !== $access_check ) { return $access_check; } $stripe = new PMProGateway_stripe(); $update_webhook_response = $stripe->update_webhook_events(); if ( empty( $update_webhook_response ) || is_wp_error( $update_webhook_response ) ) { $message = empty( $update_webhook_response ) ? __( 'Webhook creation failed. You might already have a webhook set up.', 'paid-memberships-pro' ) : $update_webhook_response->get_error_message(); $r = array( 'success' => false, 'notice' => 'error', 'message' => esc_html( $message ), ); } else { $r = array( 'success' => true, 'notice' => 'notice-success', 'message' => esc_html__( 'Your webhook is enabled.', 'paid-memberships-pro' ), ); } if ( $silent ) { return $r; } else { echo json_encode( $r ); // Values escaped above. exit; } } /** * AJAX callback to disable webhooks. */ public static function wp_ajax_pmpro_stripe_delete_webhook( $silent = false ) { $access_check = self::authorize_stripe_webhook_request( $silent ); if ( true !== $access_check ) { return $access_check; } $stripe = new PMProGateway_stripe(); $webhook = $stripe->does_webhook_exist(); $r = array( 'success' => true, 'notice' => 'error', 'message' => __( 'A webhook in Stripe is required to process payments, manage failed payments, and synchronize cancellations.', 'paid-memberships-pro' ) ); if ( ! empty( $webhook ) ) { $delete_webhook_response = $stripe->delete_webhook( $webhook ); if ( is_wp_error( $delete_webhook_response ) || empty( $delete_webhook_response['deleted'] ) || $delete_webhook_response['deleted'] != true ) { $message = is_wp_error( $delete_webhook_response ) ? $delete_webhook_response->get_error_message() : __( 'There was an error deleting the webhook.', 'paid-memberships-pro' ); $r = array( 'success' => false, 'notice' => 'error', 'message' => esc_html( $message ), ); } } if ( $silent ) { return $r; } else { echo json_encode( $r ); // Values escaped above. exit; } } /** * AJAX callback to rebuild webhook. */ public static function wp_ajax_pmpro_stripe_rebuild_webhook() { // This handler always sends its own response, so we do not need silent auth handling here. self::authorize_stripe_webhook_request(); // First try to delete the webhook. $r = self::wp_ajax_pmpro_stripe_delete_webhook( true ) ; if ( $r['success'] ) { // Webhook was successfully deleted. Now make a new one. $r = self::wp_ajax_pmpro_stripe_create_webhook( true ); if ( ! $r['success'] ) { $r['message'] = esc_html__( 'Webhook creation failed. Please refresh and try again.', 'paid-memberships-pro' ); } } echo json_encode( $r ); // Values escaped above. exit; } /** * Refresh the Stripe Connect publishable key after a browser failure. * * @since 3.8.6 */ public static function wp_ajax_pmpro_stripe_refresh_publishable_key() { check_ajax_referer( 'pmpro_stripe_refresh_publishable_key', 'nonce' ); if ( self::using_api_keys() || ! self::has_connect_credentials() ) { wp_send_json_error(); } wp_send_json_success( array( 'refreshed' => (bool) self::refresh_connect_publishable_keys() ) ); } /** * Code added to checkout preheader. * * @since 1.8 */ public static function pmpro_checkout_after_preheader( $order ) { global $gateway, $pmpro_level, $current_user, $pmpro_requirebilling, $pmpro_pages, $pmpro_currency; $default_gateway = get_option( "pmpro_gateway" ); if ( $gateway == "stripe" || $default_gateway == "stripe" ) { //stripe js library wp_enqueue_script( "stripe", "https://js.stripe.com/v3/", array(), null ); if ( ! function_exists( 'pmpro_stripe_javascript' ) ) { self::maybe_refresh_connect_publishable_keys(); $stripe = new PMProGateway_stripe(); $localize_vars = array( 'publishableKey' => $stripe->get_publishablekey(), 'publishableKeyRefreshNonce' => ( self::using_api_keys() || ! self::has_connect_credentials() ) ? '' : wp_create_nonce( 'pmpro_stripe_refresh_publishable_key' ), 'msgPublishableKeyRefreshed' => __( 'There was a problem connecting to the payment gateway. Please reload this page and try again.', 'paid-memberships-pro' ), 'user_id' => $stripe->get_connect_user_id(), 'verifyAddress' => apply_filters( 'pmpro_stripe_verify_address', get_option( 'pmpro_stripe_billingaddress' ) ), 'ajaxUrl' => admin_url( "admin-ajax.php" ), 'msgAuthenticationValidated' => __( 'Verification steps confirmed. Your payment is processing.', 'paid-memberships-pro' ), 'pmpro_require_billing' => $pmpro_requirebilling, 'restUrl' => get_rest_url(), 'siteName' => get_bloginfo( 'name' ), 'updatePaymentRequestButton' => apply_filters( 'pmpro_stripe_update_payment_request_button', true ), 'currency' => strtolower( $pmpro_currency ), 'accountCountry' => $stripe->get_account_country(), 'style' => apply_filters( 'pmpro_stripe_card_element_style', array( 'base' => array( 'fontSize' => '16px' ) ) ), ); if ( ! empty( $order ) ) { if ( ! empty( $order->stripe_payment_intent ) ) { $localize_vars['paymentIntent'] = $order->stripe_payment_intent; } if ( ! empty( $order->stripe_setup_intent ) ) { $localize_vars['setupIntent'] = $order->stripe_setup_intent; } } wp_register_script( 'pmpro_stripe', plugins_url( 'js/pmpro-stripe.js', PMPRO_BASE_FILE ), array( 'jquery' ), PMPRO_VERSION ); wp_localize_script( 'pmpro_stripe', 'pmproStripe', $localize_vars ); wp_enqueue_script( 'pmpro_stripe' ); } } } /** * Don't require the CVV. * Don't require address fields if they are set to hide. */ public static function pmpro_required_billing_fields( $fields ) { global $pmpro_stripe_lite, $current_user, $bemail, $bconfirmemail; //CVV is not required if set that way at Stripe. The Stripe JS will require it if it is required. $remove = [ 'CVV' ]; //if using stripe lite, remove some fields from the required array if ( $pmpro_stripe_lite ) { $remove = array_merge( $remove, [ 'bfirstname', 'blastname', 'baddress', 'bcity', 'bstate', 'bzipcode', 'bphone', 'bcountry', 'CardType' ] ); } // If a user is logged in, don't require bemail either if ( ! empty( $current_user->user_email ) ) { $remove = array_merge( $remove, [ 'bemail' ] ); $bemail = $current_user->user_email; $bconfirmemail = $bemail; } // If using Stripe Checkout, don't require card information. if ( self::using_stripe_checkout() ) { $remove = array_merge( $remove, [ 'CardType', 'AccountNumber', 'ExpirationMonth', 'ExpirationYear', 'CVV' ] ); } // Remove the fields. foreach ( $remove as $field ) { unset( $fields[ $field ] ); } return $fields; } /** * Filtering orders at checkout. * * @since 1.8 */ public static function pmpro_checkout_order( $morder ) { // Create a code for the order. if ( empty( $morder->code ) ) { $morder->code = $morder->getRandomCode(); } // Add the PaymentIntent ID to the order. if ( ! empty ( $_REQUEST['payment_intent_id'] ) ) { $morder->payment_intent_id = sanitize_text_field( $_REQUEST['payment_intent_id'] ); } // Add the SetupIntent ID to the order. if ( ! empty ( $_REQUEST['setup_intent_id'] ) ) { $morder->setup_intent_id = sanitize_text_field( $_REQUEST['setup_intent_id'] ); } // Add the PaymentMethod ID to the order. if ( ! empty ( $_REQUEST['payment_method_id'] ) ) { $morder->payment_method_id = sanitize_text_field( $_REQUEST['payment_method_id'] ); } return $morder; } /** * Code to run after checkout * * @since 1.8 */ public static function pmpro_after_checkout( $user_id, $morder ) { global $gateway; if ( $gateway == "stripe" ) { if ( self::$is_loaded && ! empty( $morder ) && ! empty( $morder->Gateway ) && ! empty( $morder->Gateway->customer ) && ! empty( $morder->Gateway->customer->id ) ) { update_user_meta( $user_id, "pmpro_stripe_customerid", $morder->Gateway->customer->id ); } } } /** * Check settings if billing address should be shown. * @since 1.8 */ public static function pmpro_include_billing_address_fields( $include ) { //check settings RE showing billing address if ( ! get_option( "pmpro_stripe_billingaddress" ) ) { $include = false; } return $include; } /** * Use our own payment fields at checkout. (Remove the name attributes.) * @since 1.8 */ public static function pmpro_include_payment_information_fields( $include ) { //global vars global $pmpro_requirebilling, $pmpro_show_discount_code, $discount_code, $CardType, $AccountNumber, $ExpirationMonth, $ExpirationYear; //include ours ?>
style="display: none;">

get_customer_for_user( $user->ID ); // Check whether we have a Stripe Customer. if ( ! empty( $customer ) ) { // Get the link to edit the customer. ?>
getLastMemberOrder( $user_id, $order_status ); //check if this is a Stripe order with a subscription transaction id if ( ! empty( $order->id ) && ! empty( $order->subscription_transaction_id ) && $order->gateway == "stripe" ) { //get the subscription and return the current_period end or false $subscription = $order->Gateway->get_subscription( $order->subscription_transaction_id ); if ( ! empty( $subscription ) ) { $customer = $order->Gateway->get_customer_for_user( $user_id ); if ( ! $customer->delinquent && ! empty ( $subscription->current_period_end ) ) { $offset = get_option( 'gmt_offset' ); $timestamp = $subscription->current_period_end + ( $offset * 3600 ); } elseif ( $customer->delinquent && ! empty( $subscription->current_period_start ) ) { $offset = get_option( 'gmt_offset' ); $timestamp = $subscription->current_period_start + ( $offset * 3600 ); } else { $timestamp = null; // shouldn't really get here } } } } return $timestamp; } public static function pmpro_set_up_apple_pay( $payment_option_values, $gateway ) { // Check that we just saved Stripe settings. if ( $gateway != 'stripe' || empty( $_REQUEST['savesettings'] ) ) { return; } // Check that payment request button is enabled. if ( empty( $payment_option_values['stripe_payment_request_button'] ) ) { // We don't want to unregister domain or remove file in case // other plugins are using it. return; } // Make sure that Apple Pay is set up. // TODO: Apple Pay API functions don't seem to work with // test API keys. Need to figure this out. $stripe = new PMProGateway_stripe(); if ( ! $stripe->pmpro_does_apple_pay_domain_exist() ) { // 1. Make sure domain association file available. flush_rewrite_rules(); // 2. Register Domain with Apple. $stripe->pmpro_create_apple_pay_domain(); } } /** * This function is used to save the parameters returned after successful connection of Stripe account. * * @return void */ public static function stripe_connect_save_options() { // Is user have permission to edit give setting. if ( ! current_user_can( 'manage_options' ) ) { return; } // Be sure only to connect when param present. if ( ! isset( $_REQUEST['pmpro_stripe_connected'] ) || ! isset( $_REQUEST['pmpro_stripe_connected_environment'] ) ) { return false; } // Check the nonce. if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['pmpro_stripe_connect_nonce'] ), 'pmpro_stripe_connect_nonce' ) ) { return false; } $error = ''; if ( 'false' === $_REQUEST['pmpro_stripe_connected'] && isset( $_REQUEST['error_message'] ) ) { $error = sanitize_text_field( $_REQUEST['error_message'] ); } elseif ( 'false' === $_REQUEST['pmpro_stripe_connected'] || ! isset( $_REQUEST['pmpro_stripe_publishable_key'] ) || ! isset( $_REQUEST['pmpro_stripe_user_id'] ) || ! isset( $_REQUEST['pmpro_stripe_access_token'] ) ) { $error = __( 'Invalid response from the Stripe Connect server.', 'paid-memberships-pro' ); } elseif ( 'live' === $_REQUEST['pmpro_stripe_connected_environment'] && self::is_different_connected_account( 'live', $_REQUEST['pmpro_stripe_user_id'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // Reconnecting live mode with a different account would orphan every existing customer and subscription. // Redirect right away so the access token in the return URL doesn't linger in the address bar or server logs. wp_safe_redirect( add_query_arg( array( 'page' => 'pmpro-paymentsettings', 'edit_gateway' => 'stripe', 'pmpro_stripe_connect_error' => 'different_account' ), admin_url( 'admin.php' ) ) ); exit; } else { // Change current gateway to Stripe. Only once the connection succeeded, so a failed or refused connection leaves the settings alone. update_option( 'pmpro_gateway', 'stripe' ); update_option( 'pmpro_gateway_environment', $_REQUEST['pmpro_stripe_connected_environment'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // Update keys. if ( $_REQUEST['pmpro_stripe_connected_environment'] === 'live' ) { // Update live keys. update_option( 'pmpro_live_stripe_connect_user_id', $_REQUEST['pmpro_stripe_user_id'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized update_option( 'pmpro_live_stripe_connect_secretkey', $_REQUEST['pmpro_stripe_access_token'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized update_option( 'pmpro_live_stripe_connect_publishablekey', $_REQUEST['pmpro_stripe_publishable_key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } else { // Update sandbox keys. update_option( 'pmpro_sandbox_stripe_connect_user_id', $_REQUEST['pmpro_stripe_user_id'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized update_option( 'pmpro_sandbox_stripe_connect_secretkey', $_REQUEST['pmpro_stripe_access_token'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized update_option( 'pmpro_sandbox_stripe_connect_publishablekey', $_REQUEST['pmpro_stripe_publishable_key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } // Delete option for user API key. delete_option( 'pmpro_stripe_secretkey' ); delete_option( 'pmpro_stripe_publishablekey' ); // The credentials changed, so the last connection test no longer applies. self::clear_connection_test_results(); unset( $_GET['pmpro_stripe_connected'] ); unset( $_GET['pmpro_stripe_connected_environment'] ); unset( $_GET['pmpro_stripe_user_id'] ); unset( $_GET['pmpro_stripe_access_token'] ); unset( $_GET['pmpro_stripe_publishable_key'] ); // Set up a webhook if needed. $stripe = new PMProGateway_stripe(); $stripe->update_webhook_events(); wp_redirect( admin_url( sprintf( 'admin.php?%s', http_build_query( $_GET ) ) ) ); exit; } if ( ! empty( $error ) ) { global $pmpro_stripe_error; $pmpro_stripe_error = sprintf( /* translators: %s Error Message */ __( 'Error: PMPro could not connect to the Stripe API. Reason: %s', 'paid-memberships-pro' ), esc_html( $error ) ); } } public static function stripe_connect_show_errors() { global $pmpro_stripe_error; // A live reconnect with a different Stripe account was refused by stripe_connect_save_options(). if ( empty( $pmpro_stripe_error ) && isset( $_GET['pmpro_stripe_connect_error'] ) && 'different_account' === $_GET['pmpro_stripe_connect_error'] && current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $pmpro_stripe_error = __( 'Error: The Stripe account you just connected is not the account this site was already connected to, so the connection was left unchanged. Existing memberships are tied to the original account. To switch Stripe accounts, disconnect from Stripe first. Note that disconnecting will disconnect all sites using the original Stripe account.', 'paid-memberships-pro' ); } if ( ! empty( $pmpro_stripe_error ) ) { $class = 'notice notice-error pmpro-stripe-connect-message'; $allowed_html = array( 'strong' => array(), ); printf( '

%2$s

', esc_attr( $class ), wp_kses( $pmpro_stripe_error, $allowed_html ) ); } } /** * Disconnects user from the Stripe Connected App. */ public static function stripe_connect_deauthorize() { if ( ! current_user_can( 'manage_options' ) ) { return; } // Be sure only to deauthorize when param present. if ( ! isset( $_REQUEST['pmpro_stripe_disconnected'] ) || ! isset( $_REQUEST['pmpro_stripe_disconnected_environment'] ) ) { return false; } // Check the nonce. if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['pmpro_stripe_connect_deauthorize_nonce'] ), 'pmpro_stripe_connect_deauthorize_nonce' ) ) { return false; } // Show message if NOT disconnected. if ( 'false' === $_REQUEST['pmpro_stripe_disconnected'] && isset( $_REQUEST['error_code'] ) && isset( $_REQUEST['error_message'] ) ) { $class = 'notice notice-warning pmpro-stripe-disconnect-message'; $message = sprintf( /* translators: %s Error Message */ __( 'Error: PMPro could not disconnect from the Stripe API. Reason: %s', 'paid-memberships-pro' ), sanitize_text_field( $_REQUEST['error_message'] ) ); $allowed_html = array( 'div' => array( 'class' => array(), ), 'p' => array(), 'strong' => array(), ); echo wp_kses( sprintf( '

%2$s

', $class, $message ), $allowed_html ); } if ( $_REQUEST['pmpro_stripe_disconnected_environment'] === 'live' ) { // Delete live keys. delete_option( 'pmpro_live_stripe_connect_user_id' ); delete_option( 'pmpro_live_stripe_connect_secretkey' ); delete_option( 'pmpro_live_stripe_connect_publishablekey' ); } else { // Delete sandbox keys. delete_option( 'pmpro_sandbox_stripe_connect_user_id' ); delete_option( 'pmpro_sandbox_stripe_connect_secretkey' ); delete_option( 'pmpro_sandbox_stripe_connect_publishablekey' ); } // The credentials changed, so the last connection test no longer applies. self::clear_connection_test_results(); } /** * If the checkout flow has changed to Stripe Checkout, remember to show a banner to set up webhooks. * * @since 2.12 * @deprecated 3.0.4 * * @param string $old_value The old value of the option. */ public static function update_option_pmpro_stripe_payment_flow( $old_value ) { _deprecated_function( __FUNCTION__, '3.0.4' ); global $pmpro_stripe_old_payment_flow; $pmpro_stripe_old_payment_flow = empty( $old_value ) ? 'onsite' : $old_value; } /** * If Stripe is the current gateway and the last connection test found that Stripe is rejecting the saved keys, show an admin notice. * * @since 3.8.7 */ public static function show_stripe_connection_notice() { // Only show to users who can fix the connection. if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'pmpro_paymentsettings' ) ) { return; } // If Stripe isn't the current gateway, we don't need to show the notice. if ( 'stripe' !== get_option( 'pmpro_gateway' ) ) { return; } // Only show on PMPro admin pages except for the payment settings page, which shows the results inline. $page = isset( $_REQUEST['page'] ) && is_string( $_REQUEST['page'] ) ? $_REQUEST['page'] : ''; if ( false === strpos( $page, 'pmpro' ) || 'pmpro-paymentsettings' === $page ) { return; } // Only warn about failures the admin can fix. Reachability problems are usually temporary. $results = self::get_connection_test_results(); $failed = array_intersect( self::get_failed_connection_tests( $results ), array( 'secret_key', 'publishable_key' ) ); if ( empty( $failed ) ) { return; } ?>

' . esc_html__( 'Connection Error', 'paid-memberships-pro' ) . ''; } else { $gateway_status_html .= ' ' . esc_html__( 'Issues Detected', 'paid-memberships-pro' ) . ''; } return $gateway_status_html; } /** * If Stripe is the current gateway but webhooks are not set up, show an admin notice. * * @since 3.1.2 */ public static function show_stripe_webhook_setup_notice() { // If Stripe isn't the current gateway, we don't need to show the notice. if ( 'stripe' !== pmpro_getOption( 'gateway' ) ) { return; } // Only show on PMPro admin pages except for the payment settings page. if ( empty( $_REQUEST['page'] ) || strpos( $_REQUEST['page'], 'pmpro' ) === false || 'pmpro-paymentsettings' === $_REQUEST['page'] ) { return; } // If Stripe is rejecting the saved secret key, the connection notice covers it and any webhook check would fail for the wrong reason. $connection_test = self::get_connection_test_results(); if ( ! empty( $connection_test ) && in_array( 'secret_key', self::get_failed_connection_tests( $connection_test ), true ) ) { return; } // Get webhook data from Stripe. $stripe = new PMProGateway_stripe(); $webhook = $stripe->does_webhook_exist(); if ( empty( $webhook ) || ! is_array( $webhook ) ) { // The webhook is not set up. ?>

'pmpro-paymentsettings', 'edit_gateway' => 'stripe#pmpro_stripe_webhook' ) ), admin_url( 'admin.php' ) ) . '">' . esc_html__( 'Set up webhooks now', 'paid-memberships-pro' ) . ''; ?>

'pmpro-paymentsettings', 'edit_gateway' => 'stripe#pmpro_stripe_webhook' ) ), admin_url( 'admin.php' ) ) . '">' . esc_html__( 'Enable webhooks now', 'paid-memberships-pro' ) . ''; ?>

5 ) ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } $keys = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $keys ) || ! isset( $keys['live']['publishable_key'] ) || ! is_string( $keys['live']['publishable_key'] ) || ! preg_match( '/^pk_live_[A-Za-z0-9]+$/', $keys['live']['publishable_key'] ) || ! isset( $keys['test']['publishable_key'] ) || ! is_string( $keys['test']['publishable_key'] ) || ! preg_match( '/^pk_test_[A-Za-z0-9]+$/', $keys['test']['publishable_key'] ) ) { return false; } update_option( 'pmpro_stripe_connect_platform_keys', array( 'live' => $keys['live']['publishable_key'], 'test' => $keys['test']['publishable_key'], 'checked_at' => time(), ), false ); return true; } /** * Refresh stale Stripe Connect platform publishable keys. * * @since 3.8.6 */ public static function maybe_refresh_connect_publishable_keys() { if ( self::using_api_keys() || ! self::has_connect_credentials() ) { return; } $keys = get_option( 'pmpro_stripe_connect_platform_keys' ); $cache_lifetime = apply_filters( 'pmpro_stripe_connect_publishable_key_cache_lifetime', WEEK_IN_SECONDS ); if ( ! is_array( $keys ) || empty( $keys['checked_at'] ) || $keys['checked_at'] < time() - $cache_lifetime ) { self::refresh_connect_publishable_keys(); } } /** * Test the Stripe connection and save the results. * * Checks whether Stripe and the Paid Memberships Pro Connect server can be reached, and whether Stripe * accepts the saved secret key and the publishable key that checkout uses. Every request is read-only. * Each check has a status of 'pass', 'fail', or 'unknown' and a message. Only an authentication failure * (a 401 response) counts as a rejected key. Permission errors from a restricted key do not, since the * key itself is fine. * * @since 3.8.7 * * @param bool $check_connect_server Whether to always retrieve the current platform publishable key from the Connect server. * If false, the key is only retrieved when Stripe rejects the one checkout is using. * @return array The saved results. See get_connection_test_results(). */ private function run_connection_test( $check_connect_server = true ) { $results = array(); $gateway_environment = 'live' === get_option( 'pmpro_gateway_environment' ) ? 'live' : 'sandbox'; $secret_key = $this->get_secretkey(); // If the Stripe library could not be loaded (missing curl or json extension), nothing below can run. Don't report that as a rejected key. if ( ! self::$is_loaded ) { $skipped = array( 'status' => 'unknown', 'message' => __( 'Skipped because the Stripe library could not be loaded. Check the PHP extension warnings on this page.', 'paid-memberships-pro' ) ); update_option( 'pmpro_stripe_connection_test', array( 'timestamp' => time(), 'environment' => $gateway_environment, 'results' => array( 'stripe_api' => $skipped, 'secret_key' => $skipped, 'publishable_key' => $skipped ) ), false ); return self::get_connection_test_results(); } // Use short timeouts so a host that silently drops Stripe traffic can't stall the request for the library's 30 second default. $http_client = class_exists( '\Stripe\HttpClient\CurlClient' ) ? \Stripe\HttpClient\CurlClient::instance() : null; $default_timeouts = null; if ( $http_client && method_exists( $http_client, 'getTimeout' ) && method_exists( $http_client, 'setTimeout' ) && method_exists( $http_client, 'getConnectTimeout' ) && method_exists( $http_client, 'setConnectTimeout' ) ) { $default_timeouts = array( $http_client->getTimeout(), $http_client->getConnectTimeout() ); $http_client->setTimeout( 10 ); $http_client->setConnectTimeout( 5 ); } // Stripe API reachability and secret key. One small read-only request answers both. if ( empty( $secret_key ) ) { $results['stripe_api'] = array( 'status' => 'unknown', 'message' => __( 'Skipped because no Stripe credentials are saved for this environment.', 'paid-memberships-pro' ) ); $results['secret_key'] = array( 'status' => 'fail', 'message' => __( 'No Stripe credentials are saved for this environment.', 'paid-memberships-pro' ) ); } else { $exception = null; try { // Pass the key explicitly so the check never depends on whatever key was last set globally. Stripe_Customer::all( array( 'limit' => 1 ), array( 'api_key' => $secret_key ) ); } catch ( \Throwable $e ) { $exception = $e; } catch ( \Exception $e ) { $exception = $e; } if ( empty( $exception ) ) { $results['stripe_api'] = array( 'status' => 'pass', 'message' => __( 'Stripe responded normally.', 'paid-memberships-pro' ) ); $results['secret_key'] = array( 'status' => 'pass', 'message' => __( 'Stripe accepted the saved secret key.', 'paid-memberships-pro' ) ); } else { // Classify by HTTP status rather than exception class so this still works when another plugin loaded an older Stripe library. $status = method_exists( $exception, 'getHttpStatus' ) ? (int) $exception->getHttpStatus() : 0; $message = $exception->getMessage() ? $exception->getMessage() : __( 'Unknown error.', 'paid-memberships-pro' ); if ( empty( $status ) || $status >= 500 ) { // No response or a Stripe outage. The key itself is not the problem. if ( empty( $status ) ) { // The library's connection error is several sentences long. Keep only the network reason. $message = preg_match( '/\(Network error[^:]*:\s*(.+?)\)\s*$/s', $message, $matches ) /* translators: %s: The network error reported when trying to reach Stripe. */ ? sprintf( __( 'Stripe could not be reached. %s.', 'paid-memberships-pro' ), rtrim( $matches[1], '.' ) ) : __( 'Stripe could not be reached.', 'paid-memberships-pro' ); } $results['stripe_api'] = array( 'status' => 'fail', 'message' => $message ); $results['secret_key'] = array( 'status' => 'unknown', 'message' => __( 'Skipped because Stripe could not be reached.', 'paid-memberships-pro' ) ); } elseif ( 401 === $status ) { $results['stripe_api'] = array( 'status' => 'pass', 'message' => __( 'Stripe responded normally.', 'paid-memberships-pro' ) ); $results['secret_key'] = array( 'status' => 'fail', 'message' => $message ); } else { // Anything else, such as a restricted key without permission for this endpoint, means the key was accepted. $results['stripe_api'] = array( 'status' => 'pass', 'message' => __( 'Stripe responded normally.', 'paid-memberships-pro' ) ); $results['secret_key'] = array( 'status' => 'pass', 'message' => __( 'Stripe accepted the saved secret key.', 'paid-memberships-pro' ) ); } } } // Connect server. Retrieve the current platform publishable key now, ignoring the usual throttle. if ( $check_connect_server && ! self::using_api_keys() && ! empty( $secret_key ) ) { if ( self::refresh_connect_publishable_keys( true ) ) { $results['connect_server'] = array( 'status' => 'pass', 'message' => __( 'The current platform publishable key was retrieved from Paid Memberships Pro.', 'paid-memberships-pro' ) ); } else { $results['connect_server'] = array( 'status' => 'fail', 'message' => __( 'The Paid Memberships Pro Connect server could not be reached or returned an invalid response. Checkout is using the last known publishable key.', 'paid-memberships-pro' ) ); } } // Publishable key. Check the format for API key sites, then ask Stripe to accept the key that checkout will actually use. $publishable_key = $this->get_publishablekey(); $expected_prefix = 'live' === $gateway_environment ? 'pk_live_' : 'pk_test_'; if ( empty( $secret_key ) ) { $results['publishable_key'] = array( 'status' => 'unknown', 'message' => __( 'Skipped because no Stripe credentials are saved for this environment.', 'paid-memberships-pro' ) ); } elseif ( self::using_api_keys() && 0 !== strpos( $publishable_key, $expected_prefix ) ) { if ( 0 === strpos( $publishable_key, 'pk_' ) ) { $results['publishable_key'] = array( 'status' => 'fail', 'message' => 'live' === $gateway_environment ? __( 'The saved publishable key is a test mode key, but the gateway environment is set to live.', 'paid-memberships-pro' ) : __( 'The saved publishable key is a live mode key, but the gateway environment is set to sandbox/testing.', 'paid-memberships-pro' ), ); } else { $results['publishable_key'] = array( 'status' => 'fail', 'message' => __( 'The saved publishable key does not look like a Stripe publishable key.', 'paid-memberships-pro' ) ); } } elseif ( 'fail' === $results['stripe_api']['status'] ) { $results['publishable_key'] = array( 'status' => 'unknown', 'message' => __( 'Skipped because Stripe could not be reached.', 'paid-memberships-pro' ) ); } else { $exception = $this->get_publishable_key_test_exception(); $status = ! empty( $exception ) && method_exists( $exception, 'getHttpStatus' ) ? (int) $exception->getHttpStatus() : 0; // If the Connect server wasn't checked above and Stripe rejected the platform key, try to get the current one and check again. if ( ! $check_connect_server && 401 === $status && ! self::using_api_keys() && self::refresh_connect_publishable_keys() ) { $exception = $this->get_publishable_key_test_exception(); $status = ! empty( $exception ) && method_exists( $exception, 'getHttpStatus' ) ? (int) $exception->getHttpStatus() : 0; } if ( empty( $exception ) || ( ! empty( $status ) && 401 !== $status ) ) { // Only a 401 means the key was rejected. Anything else means Stripe recognized the key. $results['publishable_key'] = array( 'status' => 'pass', 'message' => self::using_api_keys() ? __( 'Stripe accepted the saved publishable key.', 'paid-memberships-pro' ) : __( 'Stripe accepted the platform publishable key for the connected account.', 'paid-memberships-pro' ), ); } elseif ( 401 === $status ) { $results['publishable_key'] = array( 'status' => 'fail', 'message' => $exception->getMessage() ? $exception->getMessage() : __( 'Invalid API key.', 'paid-memberships-pro' ) ); } else { /* translators: %s: The error message. */ $results['publishable_key'] = array( 'status' => 'unknown', 'message' => sprintf( __( 'The publishable key could not be checked: %s', 'paid-memberships-pro' ), $exception->getMessage() ) ); } } // Put the library's timeouts back. if ( ! empty( $default_timeouts ) ) { $http_client->setTimeout( $default_timeouts[0] ); $http_client->setConnectTimeout( $default_timeouts[1] ); } // Show the two server checks first, then the two key checks. $order = array( 'stripe_api', 'connect_server', 'secret_key', 'publishable_key' ); $results = array_merge( array_flip( array_intersect( $order, array_keys( $results ) ) ), $results ); $test_results = array( 'timestamp' => time(), 'environment' => $gateway_environment, 'results' => $results, ); update_option( 'pmpro_stripe_connection_test', $test_results, false ); return $test_results; } /** * Ask Stripe to authenticate the publishable key that checkout will use. * * Stripe authenticates the key before it looks up the resource, so retrieving a PaymentIntent that doesn't exist with the * publishable key is a read-only check: a valid key gets a 404, a rejected key gets a 401, and for Connect a revoked account gets a 403. * * @since 3.8.7 * * @return \Throwable|\Exception|null The error from Stripe, or null if the request did not fail. */ private function get_publishable_key_test_exception() { $options = array( 'api_key' => $this->get_publishablekey() ); if ( ! self::using_api_keys() ) { $options['stripe_account'] = $this->get_connect_user_id(); } try { Stripe_PaymentIntent::retrieve( array( 'id' => 'pi_pmpro_connection_test', 'client_secret' => 'pi_pmpro_connection_test_secret' ), $options ); } catch ( \Throwable $e ) { return $e; } catch ( \Exception $e ) { return $e; } return null; } /** * Get the results of the last connection test. * * @since 3.8.7 * * Results are only returned if they were for the current gateway environment. Switching environments * changes every key being tested, so results from the other environment are treated as if the test never ran. * * @return array|false Array with 'timestamp', 'environment', and 'results' (test key => array with 'status' and 'message'), or false if the test has not run for the current environment. */ public static function get_connection_test_results() { $results = get_option( 'pmpro_stripe_connection_test' ); if ( ! is_array( $results ) || empty( $results['timestamp'] ) || empty( $results['environment'] ) || empty( $results['results'] ) || ! is_array( $results['results'] ) ) { return false; } if ( $results['environment'] !== ( 'live' === get_option( 'pmpro_gateway_environment' ) ? 'live' : 'sandbox' ) ) { return false; } foreach ( $results['results'] as $result ) { if ( ! is_array( $result ) || ! isset( $result['status'], $result['message'] ) ) { return false; } } return $results; } /** * Forget the results of the last connection test so that the next check starts fresh. * * @since 3.8.7 */ public static function clear_connection_test_results() { delete_option( 'pmpro_stripe_connection_test' ); } /** * Get the keys of the checks that failed in a set of connection test results. * * @since 3.8.7 * * @param array|false $results Results from get_connection_test_results() or run_connection_test(). * @return string[] The failed test keys. */ public static function get_failed_connection_tests( $results ) { $failed = array(); if ( empty( $results['results'] ) || ! is_array( $results['results'] ) ) { return $failed; } foreach ( $results['results'] as $test => $result ) { if ( isset( $result['status'] ) && 'fail' === $result['status'] ) { $failed[] = $test; } } return $failed; } /** * Run the connection test from the daily scheduled task. * * @since 3.8.7 */ public static function run_scheduled_connection_test() { if ( 'stripe' !== get_option( 'pmpro_gateway' ) ) { return; } $stripe = new PMProGateway_stripe(); if ( empty( $stripe->get_secretkey() ) ) { return; } // Don't contact the Connect server every day. The test will still ask it for a new key if Stripe rejects the current one. $stripe->run_connection_test( false ); } /** * Run the connection test from the payment settings page and return the refreshed Status cell. * * @since 3.8.7 */ public static function wp_ajax_pmpro_stripe_run_connection_test() { if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'pmpro_paymentsettings' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'paid-memberships-pro' ) ), 403 ); } check_ajax_referer( 'pmpro_stripe_connection_test', 'nonce' ); $stripe = new PMProGateway_stripe(); $stripe->run_connection_test(); // Keep the details open if the admin had them open or clicked the button, so the result doesn't collapse under them. $expanded = ! empty( $_POST['expanded'] ) && '1' === $_POST['expanded']; ob_start(); $stripe->show_connection_status_cell( 'live' === get_option( 'pmpro_gateway_environment' ), $expanded ); wp_send_json_success( array( 'html' => ob_get_clean() ) ); } /** * Run the connection test when an admin follows the button link without JavaScript. * * @since 3.8.7 */ public static function maybe_run_connection_test_on_demand() { if ( ! isset( $_REQUEST['pmpro_stripe_connection_test'] ) || 'run' !== $_REQUEST['pmpro_stripe_connection_test'] ) { return; } if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'pmpro_paymentsettings' ) ) { return; } check_admin_referer( 'pmpro_stripe_connection_test' ); $stripe = new PMProGateway_stripe(); $stripe->run_connection_test(); wp_safe_redirect( add_query_arg( array( 'page' => 'pmpro-paymentsettings', 'edit_gateway' => 'stripe', 'pmpro_stripe_connection_test' => 'complete' ), admin_url( 'admin.php' ) ) . '#pmpro_stripe_connection_test' ); exit; } /** * Get the human-readable name of a connection test check. * * @since 3.8.7 * * @param string $test The test key. * @return string The label. */ private static function get_connection_test_label( $test ) { $labels = array( 'stripe_api' => __( 'Stripe API', 'paid-memberships-pro' ), 'secret_key' => __( 'Secret Key', 'paid-memberships-pro' ), 'connect_server' => __( 'Paid Memberships Pro Connect Server', 'paid-memberships-pro' ), 'publishable_key' => __( 'Publishable Key', 'paid-memberships-pro' ), ); return isset( $labels[ $test ] ) ? $labels[ $test ] : $test; } /** * Get escaped HTML explaining how to fix a failed connection test check. * * @since 3.8.7 * * @param string $test The test key. * @param string $gateway_environment The gateway environment the test ran in, 'live' or 'sandbox'. * @return string Escaped HTML, or an empty string if there is no suggested fix. */ private static function get_connection_test_fix( $test, $gateway_environment ) { // API keys take precedence over Connect credentials when both are saved, so check them first. if ( self::using_api_keys() ) { $credentials_fix = esc_html__( 'Enter a new Publishable Key and Restricted Key below, then save your settings.', 'paid-memberships-pro' ); } elseif ( self::has_connect_credentials( $gateway_environment ) ) { $credentials_fix = sprintf( /* translators: %s: Link with the text "Reconnect with Stripe". */ esc_html__( '%s using the same Stripe account that this site was previously connected to.', 'paid-memberships-pro' ), '' . esc_html__( 'Reconnect with Stripe', 'paid-memberships-pro' ) . '' ); } else { $credentials_fix = esc_html__( 'Connect with Stripe above.', 'paid-memberships-pro' ); } switch ( $test ) { case 'stripe_api': return sprintf( /* translators: %s: Link to the Stripe status page. */ esc_html__( 'Your web host may be blocking connections to api.stripe.com, or Stripe may be having an outage. Check %s and contact your host if this continues.', 'paid-memberships-pro' ), '' . esc_html__( 'the Stripe status page', 'paid-memberships-pro' ) . '' ); case 'secret_key': case 'publishable_key': return $credentials_fix; case 'connect_server': return sprintf( /* translators: %s: Link to Paid Memberships Pro support. */ esc_html__( 'If this continues for more than a day, %s.', 'paid-memberships-pro' ), '' . esc_html__( 'contact Paid Memberships Pro support', 'paid-memberships-pro' ) . '' ); } return ''; } /** * Get the nonce-protected URL that runs the connection test on demand. * * @since 3.8.7 * * @return string The URL. */ private static function get_connection_test_url() { return wp_nonce_url( add_query_arg( array( 'page' => 'pmpro-paymentsettings', 'edit_gateway' => 'stripe', 'pmpro_stripe_connection_test' => 'run' ), admin_url( 'admin.php' ) ), 'pmpro_stripe_connection_test' ); } /** * Check whether a Stripe account returned by the Connect server differs from the one already saved for an environment. * * Only enforced for live mode. Sandboxes get a new account ID whenever one is created, so switching them is routine. * * @since 3.8.7 * * @param string $gateway_environment The environment being connected, 'live' or 'sandbox'. * @param string $stripe_user_id The Stripe account ID returned by the Connect server. * @return bool True if a different account is already saved for this environment. */ private static function is_different_connected_account( $gateway_environment, $stripe_user_id ) { // Only protect a live connection that's actually usable. A leftover user ID without keys has no Disconnect button to clear it. if ( ! self::has_connect_credentials( $gateway_environment ) ) { return false; } $saved_user_id = get_option( 'pmpro_' . ( 'live' === $gateway_environment ? 'live' : 'sandbox' ) . '_stripe_connect_user_id' ); return $saved_user_id !== $stripe_user_id; } /** * Build the URL used to start or end a Stripe Connect session for an environment. * * @since 3.8.7 * * @param string $environment The gateway environment, 'live' or 'sandbox'. * @param string $action The Connect action, 'authorize' or 'disconnect'. * @return string The URL on the Connect server to send the admin to. */ private static function get_connect_url( $environment, $action ) { $environment = 'live' === $environment ? 'live' : 'sandbox'; $return_url_args = array( 'page' => 'pmpro-paymentsettings', 'edit_gateway' => 'stripe', ); $connect_args = array( 'action' => $action, 'gateway_environment' => 'live' === $environment ? 'live' : 'test', // The Connect server uses 'test' instead of 'sandbox'. ); if ( 'disconnect' === $action ) { $connect_args['stripe_user_id'] = get_option( 'pmpro_' . $environment . '_stripe_connect_user_id' ); $return_url_args['pmpro_stripe_connect_deauthorize_nonce'] = wp_create_nonce( 'pmpro_stripe_connect_deauthorize_nonce' ); } else { $return_url_args['pmpro_stripe_connect_nonce'] = wp_create_nonce( 'pmpro_stripe_connect_nonce' ); } $connect_args['return_url'] = rawurlencode( add_query_arg( $return_url_args, admin_url( 'admin.php' ) ) ); return add_query_arg( $connect_args, apply_filters( 'pmpro_stripe_connect_url', 'https://connect.paidmembershipspro.com' ) ); } /** * Warn if required extensions aren't loaded. * * @return bool * @since 1.8.6.8.1 * @since 1.8.13.6 - Add json dependency */ public static function dependencies() { global $msg, $msgt, $pmpro_stripe_error; if ( version_compare( PHP_VERSION, '5.3.29', '<' ) ) { $pmpro_stripe_error = true; $msg = - 1; $msgt = sprintf( __( "The Stripe Gateway requires PHP 5.3.29 or greater. We recommend upgrading to PHP %s or greater. Ask your host to upgrade.", "paid-memberships-pro" ), PMPRO_MIN_PHP_VERSION ); if ( ! is_admin() ) { pmpro_setMessage( $msgt, "pmpro_error" ); } return false; } $modules = array( 'curl', 'mbstring', 'json' ); foreach ( $modules as $module ) { if ( ! extension_loaded( $module ) ) { $pmpro_stripe_error = true; $msg = - 1; $msgt = sprintf( __( "The %s gateway depends on the %s PHP extension. Please enable it, or ask your hosting provider to enable it.", 'paid-memberships-pro' ), 'Stripe', $module ); //throw error on checkout page if ( ! is_admin() ) { pmpro_setMessage( $msgt, 'pmpro_error' ); } return false; } } self::$is_loaded = true; return true; } /** * Check if Stripe Checkout is being used. * * @return bool */ public static function using_stripe_checkout() { return 'onsite' !== get_option( 'pmpro_stripe_payment_flow' ); } /** * Show warning at checkout if Stripe Checkout is being used and * the last order is pending. * * @since 2.8 * * @param bool $show Whether to show the default payment information fields. * @return bool */ static function show_stripe_checkout_pending_warning($show) { global $gateway; // If the current user's last order is a pending Stripe order, warn them that they already have a pending order. $last_order = new MemberOrder(); $last_order->getLastMemberOrder( get_current_user_id(), null, null, 'stripe' ); if ( ! empty( $last_order->id ) && $last_order->status === 'pending' ) { ?>
style="display: none;">

user_id = $user_id; $morder->status = 'token'; $morder->saveOrder(); pmpro_save_checkout_data_to_order( $morder ); // Time to send the user to pay with Stripe! $stripe = new PMProGateway_stripe(); // Let's first get the customer to charge. $customer = $stripe->update_customer_at_checkout( $morder ); if ( empty( $customer ) ) { // There was an issue creating/updating the Stripe customer. // $order will have an error message. pmpro_setMessage( __( 'Could not get customer. ', 'paid-memberships-pro' ) . $morder->error, 'pmpro_error', true ); return; } // Next, let's get the product being purchased. $product_id = $stripe->get_product_id_for_level( $morder->membership_id ); if ( empty( $product_id ) ) { // Something went wrong getting the product ID or creating the product. // Show the user a general error message. pmpro_setMessage( __( 'Could not get product ID.', 'paid-memberships-pro' ), 'pmpro_error', true ); return; } // Then, we need to build the line items array to charge. $line_items = array(); // Used to calculate Stripe Connect fees. $application_fee_percentage = $stripe->get_application_fee_percentage(); // If the level is recurring, check if we can combine the initial and recurring payments. $level = $morder->getMembershipLevelAtCheckout(); if ( pmpro_isLevelRecurring( $level ) ) { $filtered_trial_period_days = $stripe->calculate_trial_period_days( $morder ); $unfiltered_trial_period_days = $stripe->calculate_trial_period_days( $morder, false ); $combine_initial_and_recurring = ( empty( $level->trial_limit ) && // Check if there is a trial period. $filtered_trial_period_days === $unfiltered_trial_period_days && // Check if the trial period is the same as the filtered trial period. empty( $level->profile_start_date ) && // Check if the profile start date set directly on the level is empty. ! empty( $level->initial_payment ) && // Check if there is an initial payment. $level->initial_payment === $level->billing_amount // Check if the initial payment and recurring payment prices are the same. ); } // If we have an initial payment that is not being combined into a recurring payment, we need to build the initial payment line item. if ( ! empty( $level->initial_payment ) && empty( $combine_initial_and_recurring ) ) { $initial_subtotal = $level->initial_payment; $initial_tax = $morder->getTaxForPrice( $initial_subtotal ); $initial_payment_amount = pmpro_round_price( (float) $initial_subtotal + (float) $initial_tax ); $initial_payment_price = $stripe->get_price_for_product( $product_id, $initial_payment_amount ); if ( is_string( $initial_payment_price ) ) { // There was an error getting the price. pmpro_setMessage( __( 'Could not get price for initial payment. ', 'paid-memberships-pro' ) . $initial_payment_price, 'pmpro_error', true ); return; } $line_items[] = array( 'price' => $initial_payment_price->id, 'quantity' => 1, ); $payment_intent_data = array( 'description' => self::get_order_description( $morder ), ); if ( ! empty( $application_fee_percentage ) ) { $application_fee = floor( $initial_payment_price->unit_amount * $application_fee_percentage / 100 ); if ( ! empty( $application_fee ) ) { $payment_intent_data['application_fee_amount'] = $application_fee; } } } // Now, let's handle the recurring payments. if ( pmpro_isLevelRecurring( $level ) ) { $recurring_subtotal = $level->billing_amount; $recurring_tax = $morder->getTaxForPrice( $recurring_subtotal ); $recurring_payment_amount = pmpro_round_price( (float) $recurring_subtotal + (float) $recurring_tax ); $recurring_payment_price = $stripe->get_price_for_product( $product_id, $recurring_payment_amount, $level->cycle_period, $level->cycle_number ); if ( is_string( $recurring_payment_price ) ) { // There was an error getting the price. pmpro_setMessage( __( 'Could not get price for recurring payment. ', 'paid-memberships-pro' ) . $recurring_payment_price, 'pmpro_error', true ); return; } $line_items[] = array( 'price' => $recurring_payment_price->id, 'quantity' => 1, ); $subscription_data = array( 'description' => self::get_order_description( $morder ), ); // If we're sending an initial payment and a recurring payment separately, we need to set a trial period. if ( empty( $combine_initial_and_recurring ) ) { // We need to set the trial period days and send initial and recurring payments as separate line items. $subscription_data['trial_period_days'] = $filtered_trial_period_days; } // Add application fee for Stripe Connect. $application_fee_percentage = $stripe->get_application_fee_percentage(); if ( ! empty( $application_fee_percentage ) ) { $subscription_data['application_fee_percent'] = $application_fee_percentage; } } // Set up tax and billing address collection. $automatic_tax = ( ! empty( get_option( 'pmpro_stripe_tax' ) ) && 'no' !== get_option( 'pmpro_stripe_tax' ) ) ? array( 'enabled' => true, ) : array( 'enabled' => false, ); $tax_id_collection = ! empty( get_option( 'pmpro_stripe_tax_id_collection_enabled' ) ) ? array( 'enabled' => true, ) : array( 'enabled' => false, ); $billing_address_collection = get_option( 'pmpro_stripe_checkout_billing_address' ) ?: 'auto'; // And let's send 'em to Stripe! $checkout_session_params = array( 'customer' => $customer->id, 'line_items' => $line_items, // $subscription_data is only set if level is recurring. Could be empty though. 'mode' => isset( $subscription_data ) ? 'subscription' : 'payment', 'automatic_tax' => $automatic_tax, 'tax_id_collection' => $tax_id_collection, 'billing_address_collection' => $billing_address_collection, 'customer_update' => array( 'address' => 'auto', 'name' => 'auto' ), 'success_url' => apply_filters( 'pmpro_confirmation_url', add_query_arg( 'pmpro_level', $morder->membership_level->id, pmpro_url("confirmation" ) ), $user_id, $pmpro_level ), 'cancel_url' => add_query_arg( 'pmpro_level', $morder->membership_level->id, pmpro_url("checkout" ) ), ); if ( ! empty( $subscription_data ) ) { $checkout_session_params['subscription_data'] = $subscription_data; $checkout_session_params['subscription_data']['description'] = PMProGateway_stripe::get_order_description( $morder ); } elseif ( ! empty( $payment_intent_data ) ) { $checkout_session_params['payment_intent_data'] = $payment_intent_data; $checkout_session_params['payment_intent_data']['description'] = PMProGateway_stripe::get_order_description( $morder ); } // For one-time payments, make sure that we create an invoice. if ( $checkout_session_params['mode'] === 'payment' ) { $checkout_session_params['invoice_creation']['enabled'] = true; } $checkout_session_params = apply_filters( 'pmpro_stripe_checkout_session_parameters', $checkout_session_params, $morder, $customer ); try { $checkout_session = Stripe_Checkout_Session::create( $checkout_session_params ); } catch ( Throwable $th ) { // Error creating checkout session. pmpro_setMessage( __( 'Could not create checkout session. ', 'paid-memberships-pro' ) . $th->getMessage(), 'pmpro_error', true ); return; } catch ( Exception $e ) { // Error creating checkout session. pmpro_setMessage( __( 'Could not create checkout session. ', 'paid-memberships-pro' ) . $e->getMessage(), 'pmpro_error', true ); return; } // Save so that we can confirm the payment later. update_pmpro_membership_order_meta( $morder->id, 'stripe_checkout_session_id', $checkout_session->id ); wp_redirect( $checkout_session->url ); exit; } /** * Send the user to the Stripe Customer Portal if the customer portal is enabled. * * @since 2.10. */ public static function pmpro_billing_preheader_stripe_customer_portal() { global $pmpro_billing_subscription; //Bail if the customer portal isn't enabled if ( ! self::using_stripe_checkout() ) { return; } // Member doesn't have any subscription. Don't try to redirect, as we will show the default billing info form. if ( empty( $pmpro_billing_subscription ) ) { return; } //Bail if the order's gateway isn't Stripe if ( empty( $pmpro_billing_subscription->get_gateway() ) || 'stripe' !== $pmpro_billing_subscription->get_gateway() ) { return; } // Get current user. $user = wp_get_current_user(); if ( empty( $user->ID ) ) { $error = __( 'User is not logged in.', 'paid-memberships-pro' ); } if ( empty( $error ) ) { // Get the Stripe Customer. $stripe = new PMProGateway_stripe(); $customer = $stripe->get_customer_for_user( $user->ID ); if ( empty( $customer->id ) ) { $error = __( 'Could not get Stripe customer for user.', 'paid-memberships-pro' ); } } if ( empty( $error ) ) { // Send the user to the customer portal. $customer_portal_url = $stripe->get_customer_portal_url( $customer->id ); if ( ! empty( $customer_portal_url ) ) { wp_redirect( $customer_portal_url ); exit; } $error = __( 'Could not get Customer Portal URL. This feature may not be set up in Stripe.', 'paid-memberships-pro' ); } // There must have been an error while getting the customer portal URL. Show an error and let user update // their billing info onsite. pmpro_setMessage( $error . ' ' . __( 'Please contact the site administrator.', 'paid-memberships-pro' ), 'pmpro_alert', true ); } /**************************************** ************ PUBLIC METHODS ************ ****************************************/ /** * Process checkout and decide if a charge and or subscribe is needed * Updated in v2.1 to work with Stripe v3 payment intents. * @since 1.4 */ public function process( &$order ) { if ( self::using_stripe_checkout() ) { // If using Stripe Checkout, redirect them. self::pmpro_checkout_before_change_membership_level( $order->user_id, $order ); // If we were not redirected, there was an error. return false; } $payment_transaction_id = ''; $subscription_transaction_id = ''; // User has either just submitted the checkout form or tried to confirm their // payment intent. $customer = null; // This will be used to create the subscription later. if ( ! empty( $order->payment_intent_id ) ) { // User has just tried to confirm their payment intent. We need to make sure that it was // confirmed successfully, and then try to create their subscription if needed. $payment_intent = $this->process_payment_intent( $order->payment_intent_id, $order ); if ( is_string( $payment_intent ) ) { $order->error = __( 'Error processing payment intent.', 'paid-memberships-pro' ) . ' ' . $payment_intent; $order->shorterror = $order->error; return false; } // Payment should now be processed. $payment_transaction_id = $payment_intent->latest_charge; // Note the customer so that we can create a subscription if needed.. $customer = $payment_intent->customer; } else { // We have not yet tried to process this checkout. // Make sure we have a customer with a payment method. $customer = $this->update_customer_at_checkout( $order ); if ( empty( $customer ) ) { // There was an issue creating/updating the Stripe customer. // $order will have an error message, so we don't need to add one. return false; } $payment_method = $this->get_payment_method( $order ); if ( empty( $payment_method ) ) { // There was an issue getting the payment method. $order->error = __( 'Error retrieving payment method.', 'paid-memberships-pro' ) . empty( $order->error ) ? '' : ' ' . $order->error; $order->shorterror = $order->error; return false; } // Save customer in $order for create_payment_intent(). // This will likely be removed as we rework payment processing. $order->stripe_customer = $customer; // Process the charges. $charges_processed = $this->process_charges( $order ); if ( ! empty( $order->error ) ) { // There was an error processing charges. // $order has an error message, so we don't need to add one. return false; } // If we needed to charge an initial payment, it was successful. if ( ! empty( $order->stripe_payment_intent->latest_charge ) ) { $payment_transaction_id = $order->stripe_payment_intent->latest_charge; } else { // If we haven't charged a payment, the payment method will not be attached to the customer. // Attach it now. try { $payment_method->attach( array( 'customer' => $customer->id, ) ); } catch ( \Stripe\Error $e ) { $order->error = $e->getMessage(); return false; } catch ( \Throwable $e ) { $order->error = $e->getMessage(); return false; } catch ( \Exception $e ) { $order->error = $e->getMessage(); return false; } } } // Create a subscription if we need to. if ( pmpro_isLevelRecurring( $order->membership_level ) ) { $subscription = $this->create_subscription_for_customer_from_order( $customer->id, $order ); if ( empty( $subscription ) ) { // There was an issue creating the subscription. Order will have error message. $order->error = __( 'Error creating subscription for customer.', 'paid-memberships-pro' ) . ' ' . $order->error; $order->shorterror = $order->error; return false; } $order->stripe_subscription = $subscription; // Successfully created a subscription. $subscription_transaction_id = $subscription->id; } // All charges have been processed and all subscriptions have been created. $order->payment_transaction_id = $payment_transaction_id; $order->subscription_transaction_id = $subscription_transaction_id; $order->status = 'success'; return true; } /** * Retrieve a Stripe_Customer for a given user. * * @since 2.7.0 * * @param int $user_id to get Stripe_Customer for. * @param bool $find_existing If true, will try to find an existing customer for the user if one does not exist in user meta. * @return Stripe_Customer|null */ public function get_customer_for_user( $user_id, $find_existing = true ) { // Pull Stripe customer ID from user meta. $customer_id = get_user_meta( $user_id, 'pmpro_stripe_customerid', true ); if ( empty( $customer_id ) && $find_existing ) { // Try to figure out the customer ID from their subscription. $subscription_search_params = array( 'user_id' => $user_id, 'status' => 'active', 'gateway' => 'stripe', ); $subscriptions = PMPro_Subscription::get_subscriptions( $subscription_search_params ); foreach ( $subscriptions as $subscription ) { try { $stripe_subscription = Stripe_Subscription::retrieve( $subscription->get_subscription_transaction_id() ); } catch ( \Throwable $e ) { // Assume no customer found. } catch ( \Exception $e ) { // Assume no customer found. } if ( ! empty( $stripe_subscription ) && ! empty( $stripe_subscription->customer ) ) { $customer_id = $stripe_subscription->customer; break; } $stripe_subscription = null; } // If we don't have a customer ID yet, try to figure out the cuseromer ID from their last order. if ( empty( $customer_id ) ) { $order = new MemberOrder(); $order->getLastMemberOrder( $user_id, array( 'success', 'cancelled' ), null, 'stripe', $this->gateway_environment ); } // If we don't have a customer ID yet, get the Customer ID from their charge. if ( empty( $customer_id ) && ! empty( $order->payment_transaction_id ) && strpos( $order->payment_transaction_id, "ch_" ) !== false ) { try { $charge = Stripe_Charge::retrieve( $order->payment_transaction_id ); } catch ( \Throwable $e ) { // Assume no customer found. } catch ( \Exception $e ) { // Assume no customer found. } if ( ! empty( $charge ) && ! empty( $charge->customer ) ) { $customer_id = $charge->customer; } } // If we don't have a customer ID yet, get the Customer ID from their invoice. if ( empty( $customer_id ) && ! empty( $order->payment_transaction_id ) && strpos( $order->payment_transaction_id, "in_" ) !== false ) { try { $invoice = Stripe_Invoice::retrieve( $order->payment_transaction_id ); } catch ( \Throwable $e ) { // Assume no customer found. } catch ( \Exception $e ) { // Assume no customer found. } if ( ! empty( $invoice ) && ! empty( $invoice->customer ) ) { $customer_id = $invoice->customer; } } if ( ! empty( $customer_id ) ) { update_user_meta( $user_id, "pmpro_stripe_customerid", $customer_id ); } } return empty( $customer_id ) ? null : $this->get_customer( $customer_id ); } /** * Create/Update Stripe customer for a user. * * @since 2.7.0 * @deprecated 3.6 * * @param int $user_id to create/update Stripe customer for. * @return Stripe_Customer|false */ public function update_customer_from_user( $user_id ) { _deprecated_function( __METHOD__, '3.6', 'PMProGateway_stripe::update_customer_for_user()' ); return self::update_customer_for_user( $user_id ); } /** * Create/Update Stripe customer for a user. * * @since 3.6 * * @param int $user_id to create/update Stripe customer for. * @return Stripe_Customer|false */ public static function update_customer_for_user( $user_id ) { $user = get_userdata( $user_id ); if ( empty( $user->ID ) ) { // User does not exist. return false; } $stripe = new PMProGateway_stripe(); // Get the existing customer from Stripe. $customer = $stripe->get_customer_for_user( $user_id, false ); // False to improve performance if customer ID does not exist in user meta. if ( empty( $customer ) ) { // If we don't have a customer, don't update. // This is important in case Stripe isn't used on the site. return false; } // Get the name for the customer. $name = trim( $user->first_name . " " . $user->last_name ); if ( empty( $name ) ) { // In case first and last names aren't set. $name = $user->user_login; } // Get data to update customer with. $customer_args = array( 'name' => $name, 'email' => $user->user_email, 'description' => $name . ' (' . $user->user_email . ')', ); /** * Change the information that is sent when updating/creating * a Stripe_Customer from a user. * * @since 2.7.0 * @deprecated 3.6 * * @param array $customer_args to be sent. * @param WP_User $user being used to create/update customer. */ $customer_args = apply_filters_deprecated( 'pmpro_stripe_update_customer_from_user', array( $customer_args, $user ), '3.6', 'pmpro_stripe_update_customer_for_user' ); /** * Change the information that is sent when updating/creating * a Stripe_Customer from a user. * * @since 2.7.0 * * @param array $customer_args to be sent. * @param WP_User $user being used to create/update customer. */ $customer_args = apply_filters( 'pmpro_stripe_update_customer_for_user', $customer_args, $user ); // Update the customer. $customer = $stripe->update_customer( $customer->id, $customer_args ); return is_string( $customer ) ? false : $customer; } /** * Get subscription status from the Gateway. * * @since 2.3 */ public function getSubscriptionStatus( &$order ) { $subscription = $this->get_subscription( $order->subscription_transaction_id ); if ( ! empty( $subscription ) ) { return $subscription->status; } else { return false; } } /** * Helper method to update the customer info via update_customer_at_checkout * * @since 1.4 */ public function update( &$order ) { // Make sure the order has a subscription_transaction_id. if ( empty( $order->subscription_transaction_id ) ) { $order->error = __( 'No subscription transaction ID.', 'paid-memberships-pro' ); return false; } $customer = $this->update_customer_at_checkout( $order ); if ( empty( $customer ) ) { // There was an issue creating/updating the Stripe customer. // $order will have an error message, so we don't need to add one. return false; } $payment_method = $this->get_payment_method( $order ); if ( empty( $payment_method ) ) { // There was an issue getting the payment method. $order->error = __( 'Error retrieving payment method.', 'paid-memberships-pro' ) . empty( $order->error ) ? '' : ' ' . $order->error; $order->shorterror = $order->error; return false; } // Attach the customer to the payment method. try { $payment_method->attach( array( 'customer' => $customer->id, ) ); } catch ( \Stripe\Error $e ) { $order->error = $e->getMessage(); return false; } catch ( \Throwable $e ) { $order->error = $e->getMessage(); return false; } catch ( \Exception $e ) { $order->error = $e->getMessage(); return false; } // Update the subscription. $subscription_args = array( 'default_payment_method' => $order->payment_method_id, ); try { Stripe_Subscription::update( $order->subscription_transaction_id, $subscription_args ); } catch ( \Stripe\Error $e ) { $order->error = $e->getMessage(); return false; } catch ( \Throwable $e ) { $order->error = $e->getMessage(); return false; } catch ( \Exception $e ) { $order->error = $e->getMessage(); return false; } return true; } /** * Cancel a subscription at Stripe * * @since 1.4 */ public function cancel( &$order, $update_status = true ) { global $pmpro_stripe_event; //no matter what happens below, we're going to cancel the order in our system if ( $update_status ) { $order->updateStatus( "cancelled" ); } //require a subscription id if ( empty( $order->subscription_transaction_id ) ) { return false; } //find the customer $result = $this->update_customer_at_checkout( $order ); if ( ! empty( $result ) ) { //find subscription with this order code $subscription = $this->get_subscription( $order->subscription_transaction_id ); if ( ! empty( $subscription ) && ( empty( $pmpro_stripe_event ) || empty( $pmpro_stripe_event->type ) || $pmpro_stripe_event->type != 'customer.subscription.deleted' ) ) { if ( $this->cancelSubscriptionAtGateway( $subscription ) ) { //we're okay, going to return true later } else { $order->error = __( "Could not cancel old subscription.", 'paid-memberships-pro' ); $order->shorterror = $order->error; return false; } } /* Clear updates for this user. (But not if checking out, we would have already done that.) */ if ( empty( $_REQUEST['submit-checkout'] ) ) { update_user_meta( $order->user_id, "pmpro_stripe_updates", array() ); } return true; } else { $order->error = __( "Could not find the customer.", 'paid-memberships-pro' ); $order->shorterror = $order->error; return false; //no customer found } } /** * Pull subscription info from Stripe. * * @param PMPro_Subscription $subscription to pull data for. * * @return string|null Error message is returned if update fails. */ public function update_subscription_info( $subscription ) { if( empty( $this->get_secretkey() ) ){ return __( "Stripe login credentials are not set.", 'paid-memberships-pro' ); } try { $stripe_subscription = Stripe_Subscription::retrieve( array( 'id' => $subscription->get_subscription_transaction_id(), 'expand' => array( 'latest_invoice' ), ) ); } catch ( \Throwable $e ) { // Assume no subscription found. return $e->getMessage(); } catch ( \Exception $e ) { // Assume no subscription found. return $e->getMessage(); } if ( ! empty( $stripe_subscription ) ) { $update_array = array( 'startdate' => date( 'Y-m-d H:i:s', intval( $stripe_subscription->created ) ), ); if ( in_array( $stripe_subscription->status, array( 'trialing', 'active', 'past_due' ) ) ) { // Subscription is active. $update_array['status'] = 'active'; // Get the next payment date. if ( ! empty( $stripe_subscription->items->data[0]->current_period_end ) ) { $update_array['next_payment_date'] = date( 'Y-m-d H:i:s', intval( $stripe_subscription->items->data[0]->current_period_end ) ); } // Get the billing amount and cycle. if ( ! empty( $stripe_subscription->items->data[0]->price ) ) { $stripe_subscription_price = $stripe_subscription->items->data[0]->price; $update_array['billing_amount'] = $this->convert_unit_amount_to_price( $stripe_subscription_price->unit_amount ); $update_array['cycle_number'] = $stripe_subscription_price->recurring->interval_count; $update_array['cycle_period'] = ucfirst( $stripe_subscription_price->recurring->interval ); } } else { // Subscription is no longer active. $update_array['status'] = 'cancelled'; $update_array['enddate'] = date( 'Y-m-d H:i:s', intval( $stripe_subscription->ended_at ) ); } $subscription->set( $update_array ); // Check if the user's Stripe customer ID is the same as the subscription's customer ID. // We are waiting until after the subscription is created in case the customer didn't have a Stripe customer ID before. $customer = $this->get_customer_for_user( $subscription->get_user_id() ); if ( ! empty( $customer ) && $customer->id !== $stripe_subscription->customer ) { // Throw an error. return __( 'Subscription customer ID does not match user\'s Stripe customer ID.', 'paid-memberships-pro' ); } } } /** * Get the URL for a customer's Stripe Customer Portal. * * @since 2.8 * * @param string $customer_id Customer to get the URL for. * @return string URL for customer portal, or empty String if not found. */ public function get_customer_portal_url( $customer_id ) { // Before we can send the user to the customer portal, // we need to have a portal configuration. $portal_configurations = array(); try { // Get all active portal configurations. $portal_configurations = Stripe\BillingPortal\Configuration::all( array( 'active' => true, 'limit' => 100 ) ); } catch( Exception $e ) { // Error getting portal configurations. return ''; } // Check if one of the portal configurations is default. foreach ( $portal_configurations as $portal_configuration ) { if ( $portal_configuration->is_default ) { $portal_configuration_id = $portal_configuration->id; break; } } // If we still don't have a portal configuration, create one. if ( empty( $portal_configuration_id ) ) { $portal_configuration_params = array( 'business_profile' => array( 'headline' => esc_html__( 'Manage billing', 'paid-memberships-pro' ), ), 'features' => array( 'customer_update' => array( 'enabled' => true, 'allowed_updates' => array( 'address', 'phone', 'tax_id' ) ), 'invoice_history' => array( 'enabled' => true ), 'payment_method_update' => array( 'enabled' => true ), 'subscription_cancel' => array( 'enabled' => true ), ), ); try { $portal_configuration = Stripe\BillingPortal\Configuration::create( $portal_configuration_params ); } catch( Exception $e ) { // Error creating portal configuration. return ''; } if ( ! empty( $portal_configuration ) ) { $portal_configuration_id = $portal_configuration->id; } } try { $session = \Stripe\BillingPortal\Session::create([ 'customer' => $customer_id, 'return_url' => pmpro_url( 'account' ), ]); return $session->url; } catch ( Exception $e ) { return ''; } } /**************************************** *********** PRIVATE METHODS ************ ****************************************/ /** * Shows settings for connecting to Stripe. * * @since 2.7.0. * @deprecated 3.5 * * @param bool $livemode True if live credentials, false if sandbox. * @param array $values Current settings. * @param string $gateway currently being shown. */ private function show_connect_payment_option_fields( $livemode, $values, $gateway ) { _deprecated_function( __METHOD__, '3.5' ); $gateway_environment = $this->gateway_environment; $environment = $livemode ? 'live' : 'sandbox'; $environment2 = $livemode ? 'live' : 'test'; // For when 'test' is used instead of 'sandbox'. // Determine if the gateway is connected in live mode and set var. if ( self::has_connect_credentials( $environment ) || self::using_api_keys() ) { $connection_selector = 'pmpro_gateway-mode-connected'; } else { $connection_selector = 'pmpro_gateway-mode-not-connected'; } ?> style="display: none;">





style="display: none;"> 'disconnect', 'gateway_environment' => $environment2, 'stripe_user_id' => $values[ $environment . '_stripe_connect_user_id'], 'return_url' => rawurlencode( add_query_arg( array( 'page' => 'pmpro-paymentsettings', 'edit_gateway' => 'stripe', 'pmpro_stripe_connect_deauthorize_nonce' => wp_create_nonce( 'pmpro_stripe_connect_deauthorize_nonce' ) ), admin_url( 'admin.php' ) ) ), ), $connect_url_base ); ?>

'authorize', 'gateway_environment' => $environment2, 'return_url' => rawurlencode( add_query_arg( array( 'page' => 'pmpro-paymentsettings', 'edit_gateway' => 'stripe', 'pmpro_stripe_connect_nonce' => wp_create_nonce( 'pmpro_stripe_connect_nonce' ) ), admin_url( 'admin.php' ) ) ), ), $connect_url_base ); ?>

get_application_fee_percentage(); if ( ! empty( $application_fee_percentage ) ) { echo sprintf( esc_html__( 'Note: You are using the free Stripe payment gateway integration. This includes an additional %s fee for payment processing. This fee is removed by activating a premium PMPro license.', 'paid-memberships-pro' ), intval( $application_fee_percentage ) . '%' ); } else { esc_html_e( 'Note: You are using the free Stripe payment gateway integration. There is no additional fee for payment processing above what Stripe charges.', 'paid-memberships-pro' ); } } echo ' ' . esc_html__( 'Learn More', 'paid-memberships-pro' ) . ''; ?>

'/> '/> '/>
style="display: none;">
show_connection_status_cell( $livemode ); ?>





get_application_fee_percentage(); if ( ! empty( $application_fee_percentage ) ) { echo sprintf( esc_html__( 'Note: You are using the free Stripe payment gateway integration. This includes an additional %s fee for payment processing. This fee is removed by activating a premium PMPro license.', 'paid-memberships-pro' ), intval( $application_fee_percentage ) . '%' ); } else { esc_html_e( 'Note: You are using the free Stripe payment gateway integration. There is no additional fee for payment processing above what Stripe charges.', 'paid-memberships-pro' ); } } echo ' ' . esc_html__( 'Learn More', 'paid-memberships-pro' ) . ''; ?>

'/> '/> '/>

style="display: none;">

style="display: none;"> $result ) { ?>
' . esc_html__( 'Passed', 'paid-memberships-pro' ) . ''; } elseif ( 'fail' === $result['status'] ) { echo '' . esc_html__( 'Failed', 'paid-memberships-pro' ) . ''; } else { echo '' . esc_html__( 'Skipped', 'paid-memberships-pro' ) . ''; } ?>

array( 'href' => array(), 'target' => array(), 'rel' => array() ) ) ); ?>

address->line1 ) && ! empty( $customer->address->city ) && ! empty( $customer->address->state ) && ! empty( $customer->address->postal_code ) && ! empty( $customer->address->country ) ); } /** * Update a customer in Stripe. * * @since 2.7.0 * * @param string $customer_id to update. * @param array $args to update with. * @return Stripe_Customer|string error message. */ private function update_customer( $customer_id, $args ) { try { $customer = Stripe_Customer::update( $customer_id, $args ); } catch ( \Stripe\Error $e ) { return $e->getMessage(); } catch ( \Throwable $e ) { return $e->getMessage(); } catch ( \Exception $e ) { return $e->getMessage(); } return $customer; } /** * Create a new customer in Stripe. * * @since 2.7.0 * * @param array $args to update with. * @return Stripe_Customer|string error message. */ private function create_customer( $args ) { try { $customer = Stripe_Customer::create( $args ); } catch ( \Stripe\Error $e ) { return $e->getMessage(); } catch ( \Throwable $e ) { return $e->getMessage(); } catch ( \Exception $e ) { return $e->getMessage(); } return $customer; } /** * Create/Update Stripe customer from MemberOrder. * * Falls back on information in User object if insufficient * information in MemberOrder. * * Should only be called when checkout is being processed. Otherwise, * use update_customer_from_user() method. * * @since 2.7.0 * * @param MemberOrder $order to create/update Stripe customer for. * @return Stripe_Customer|false */ private function update_customer_at_checkout( $order ) { global $current_user; // Get user's ID. if ( ! empty( $order->user_id ) ) { $user_id = $order->user_id; } if ( empty( $user_id ) && ! empty( $current_user->ID ) ) { $user_id = $current_user->ID; } $user = empty( $user_id ) ? null : get_userdata( $user_id ); $customer = empty( $user_id ) ? null : $this->get_customer_for_user( $user_id ); // Get customer name. $name = empty( $order->billing->name ) ? $user->user_login : $order->billing->name; // Get user's email. $email = empty( $user->user_email ) ? "No Email" : $user->user_email; // Build data to update customer with. $customer_args = array( 'name' => $name, 'email' => $email, 'description' => $name . ' (' . $email . ')', ); // Maybe update billing address for customer. if ( ! empty( $order->billing->street ) && ! empty( $order->billing->city ) && ! empty( $order->billing->state ) && ! empty( $order->billing->zip ) && ! empty( $order->billing->country ) ) { // We collected a billing address at checkout. // Send it to Stripe. $customer_args['address'] = array( 'city' => $order->billing->city, 'country' => $order->billing->country, 'line1' => $order->billing->street, 'line2' => $order->billing->street2, 'postal_code' => $order->billing->zip, 'state' => $order->billing->state, ); } /** * Change the information that is sent when updating/creating * a Stripe_Customer from a MemberOrder. * * @since 2.7.0 * * @param array $customer_args to be sent. * @param MemberOrder $order being used to create/update customer. */ $customer_args = apply_filters( 'pmpro_stripe_update_customer_at_checkout', $customer_args, $order ); // Check if we have an existing user. if ( ! empty( $customer ) ) { // User is already a customer in Stripe. Update. $customer = $this->update_customer( $customer->id, $customer_args ); if ( is_string( $customer ) ) { // We were not able to create a new user in Stripe. $order->error = __( "Error updating customer record with Stripe.", 'paid-memberships-pro' ) . " " . $customer; $order->shorterror = $order->error; return false; } return $customer; } // No customer yet. Need to create one. $customer = $this->create_customer( $customer_args ); if ( is_string( $customer ) ) { // We were not able to create a new user in Stripe. $order->error = __( "Error creating customer record with Stripe.", 'paid-memberships-pro' ) . " " . $customer; $order->shorterror = $order->error; return false; } // If we don't have a user yet, we need to update their user meta after registration. if ( empty( $user_id ) ) { global $pmpro_stripe_customer_id; $pmpro_stripe_customer_id = $customer->id; if ( ! function_exists( 'pmpro_user_register_stripe_customerid' ) ) { function pmpro_user_register_stripe_customerid( $user_id ) { global $pmpro_stripe_customer_id; update_user_meta( $user_id, "pmpro_stripe_customerid", $pmpro_stripe_customer_id ); } add_action( "user_register", "pmpro_user_register_stripe_customerid" ); } } else { // User already exists. Update their Stripe customer ID. update_user_meta( $user_id, 'pmpro_stripe_customerid', $customer->id ); } return $customer; } /** * Convert a price to a positive integer in cents (or 0 for a free price) * representing how much to charge. This is how Stripe wants us to send price amounts. * * @param float $price to be converted into cents. * @return integer */ private function convert_price_to_unit_amount( $price ) { $price_info = pmpro_get_price_info( $price ); if ( ! $price_info ) { return 0; } return $price_info['amount_flat']; } /** * Convert a unit amount (price in cents) into a decimal price. * * @param integer $unit_amount to be converted. * @return float */ private function convert_unit_amount_to_price( $unit_amount ) { global $pmpro_currencies, $pmpro_currency; $currency_unit_multiplier = 100; // ie 100 cents per USD. // Account for zero-decimal currencies like the Japanese Yen. if ( is_array( $pmpro_currencies[ $pmpro_currency ] ) && isset( $pmpro_currencies[ $pmpro_currency ]['decimals'] ) && $pmpro_currencies[ $pmpro_currency ]['decimals'] == 0 ) { $currency_unit_multiplier = 1; } return floatval( $unit_amount / $currency_unit_multiplier ); } /** * Retrieve a Stripe_Subscription. * * @since 2.7.0 * * @param string $subscription_id to retrieve. * @return Stripe_Subscription|null */ private function get_subscription( $subscription_id ) { try { $subscription = Stripe_Subscription::retrieve( $subscription_id ); return $subscription; } catch ( \Throwable $e ) { // Assume no subscription found. } catch ( \Exception $e ) { // Assume no subscription found. } } /** * Get the Stripe product ID for a given membership level. * * @since 2.7.0 * * @param PMPro_Membership_Level|int $level to get product ID for. * @return string|null */ private function get_product_id_for_level( $level ) { // Get the level object. if ( ! is_a( $level, 'PMPro_Membership_Level' ) ) { if ( is_numeric( $level ) ) { $level = new PMPro_Membership_Level( $level ); } } // If we don't have a valid level, we can't get a product ID. Bail. if ( empty( $level->ID ) ) { return; } // Get the product ID from the level based on the current gateway environment. $gateway_environment = get_option( 'pmpro_gateway_environment' ); if ( $gateway_environment === 'sandbox' ) { $stripe_product_id = $level->stripe_product_id_sandbox; } else { $stripe_product_id = $level->stripe_product_id; } // Check that the product ID exists in Stripe. if ( ! empty( $stripe_product_id ) ) { try { $product = Stripe_Product::retrieve( $stripe_product_id ); } catch ( \Throwable $e ) { // Assume no product found. } catch ( \Exception $e ) { // Assume no product found. } if ( empty( $product ) || empty( $product->active ) ) { // There was an error retrieving the product or the product is archived. // Let's try to create a new one below. $stripe_product_id = null; } } // If a valid product does not exist for this level, create one. if ( empty( $stripe_product_id ) ) { $stripe_product_id = $this->create_product_for_level( $level, $gateway_environment ); } // Return the product ID. return ! empty( $stripe_product_id ) ? $stripe_product_id : null; } /** * Create a new Stripe product for a given membership level. * * WARNING: Will overwrite old Stripe product set for level if * there is already one set. * * @since 2.7.0 * * @param PMPro_Membership_Level|int $level to create product ID for. * @param string $gateway_environment to create product for. * @return string|null ID of new product */ private function create_product_for_level( $level, $gateway_environment ) { if ( ! is_a( $level, 'PMPro_Membership_Level' ) ) { if ( is_numeric( $level ) ) { $level = new PMPro_Membership_Level( $level ); } } if ( empty( $level->ID ) ) { // We do not have a valid level. return; } $product_args = array( 'name' => $level->name, ); /** * Filter the data sent to Stripe when creating a new product for a membership level. * * @since 2.7.0 * * @param array $product_args being sent to Stripe. * @param PMPro_Membership_Level $level that product is being created for. * @param string $gateway_environment being used. */ $product_args = apply_filters( 'pmpro_stripe_create_product_for_level', $product_args, $level, $gateway_environment ); try { $product = Stripe_Product::create( $product_args ); if ( ! empty( $product->id ) ) { $meta_name = 'sandbox' === $gateway_environment ? 'stripe_product_id_sandbox' : 'stripe_product_id'; update_pmpro_membership_level_meta( $level->ID, $meta_name, $product->id ); return $product->id; } } catch (\Throwable $th) { // Could not create product. } catch (\Exception $e) { // Could not create product. } } /** * Get a Price for a given product, or create one if it doesn't exist. * * TODO: Add pagination. * * @since 2.7.0 * * @param string $product_id to get Price for. * @param float $amount that the Price will charge. * @param string|null $cycle_period for subscription payments. * @param string|null $cycle_number of cycle periods between each subscription payment. * * @return Stripe_Price|string Price or error message. */ private function get_price_for_product( $product_id, $amount, $cycle_period = null, $cycle_number = null ) { global $pmpro_currency; $currency = pmpro_get_currency(); $is_recurring = ! empty( $cycle_period ) && ! empty( $cycle_number ); $unit_amount = $this->convert_price_to_unit_amount( $amount ); if ( empty( $cycle_period ) ) { $cycle_period = ''; } $cycle_period = strtolower( $cycle_period ); // Only for use with Stripe Checkout. $tax_behavior = get_option( 'pmpro_stripe_tax' ); if ( ! self::using_stripe_checkout() || empty( $tax_behavior ) ) { $tax_behavior = 'no'; } $price_search_args = array( 'product' => $product_id, 'type' => $is_recurring ? 'recurring' : 'one_time', 'currency' => strtolower( $pmpro_currency ), 'limit' => 100, 'active' => true, ); if ( $is_recurring ) { $price_search_args['recurring'] = array( 'interval' => $cycle_period ); } try { $prices = Stripe_Price::all( $price_search_args ); } catch (\Throwable $th) { // There was an error listing prices. return $th->getMessage(); } catch (\Exception $e) { // There was an error listing prices. return $e->getMessage(); } foreach ( $prices as $price ) { // Skip archived/inactive prices. Create a new one instead. if ( empty( $price->active ) ) { continue; } // Check whether price is the same. If not, continue. if ( intval( $price->unit_amount ) !== intval( $unit_amount ) ) { continue; } // Check if recurring structure is the same. If not, continue. if ( $is_recurring && ( empty( $price->recurring->interval_count ) || intval( $price->recurring->interval_count ) !== intval( $cycle_number ) ) ) { continue; } // Check if tax is enabled and set up correctly. If not, continue. if ( 'no' !== $tax_behavior && $price->tax_behavior !== $tax_behavior ) { continue; } return $price; } // Create a new Price. $price_args = array( 'product' => $product_id, 'currency' => strtolower( $pmpro_currency ), 'unit_amount' => $unit_amount, ); if ( $is_recurring ) { $price_args['recurring'] = array( 'interval' => $cycle_period, 'interval_count' => $cycle_number ); } if ( 'no' !== $tax_behavior ) { $price_args['tax_behavior'] = $tax_behavior; } try { $price = Stripe_Price::create( $price_args ); if ( ! empty( $price->id ) ) { return $price; } } catch (\Throwable $th) { // Could not create product. return $th->getMessage(); } catch (\Exception $e) { // Could not create product. return $e->getMessage(); } return esc_html__( 'Could not create price.', 'paid-memberships-pro' ); } /** * Calculate the number of days until the first recurring payment * for a subscription should be charged. * * @since 2.7.0. * * @param MemberOrder $order to calculate trial period days for. * @param bool $filtered whether to filter the result. * @return int trial period days. */ private function calculate_trial_period_days( $order, $filtered = true ) { // Get the checkout level for this order. $level = $order->getMembershipLevelAtCheckout(); // Check if we have a free trial period set. if ( ! empty( $level->trial_limit ) && pmpro_round_price( $level->trial_amount ) == 0 ) { // If so, we want to account for the trial period only while calculating the profile start date. // We will then revert back to the original billing frequency after the calculation. $original_cycle_number = $level->cycle_number; $level->cycle_number = $level->cycle_number * ( $level->trial_limit + 1 ); } // Calculate the profile start date. // Getting return value as Unix Timestamp so that we can calculate days more easily. $profile_start_date = pmpro_calculate_profile_start_date( $order, 'U', $filtered ); // Restore the original billing frequency if needed so that the rest of the checkout has the correct info. if ( ! empty( $original_cycle_number ) ) { $level->cycle_number = $original_cycle_number; } // Convert to days. We are rounding up to ensure that customers get the full membership time that they are paying for. $trial_period_days = ceil( abs( $profile_start_date - time() ) / 86400 ); return $trial_period_days; } /** * Create a subscription for a customer from an order using a Stripe Price. * * @since 2.7.0. * * @param string $customer_id to create subscription for. * @param MemberOrder $order to pull subscription details from. * @return Stripe_Subscription|bool false if error. */ private function create_subscription_for_customer_from_order( $customer_id, $order ) { $level = $order->getMembershipLevelAtCheckout(); $amount = $level->billing_amount; $tax = $order->getTaxForPrice( $amount ); $amount = pmpro_round_price( (float) $amount + (float) $tax ); // Set up the subscription. $product_id = $this->get_product_id_for_level( $order->membership_id ); if ( empty( $product_id ) ) { $order->error = esc_html__( 'Cannot find product for membership level.', 'paid-memberships-pro' ); return false; } $price = $this->get_price_for_product( $product_id, $amount, $level->cycle_period, $level->cycle_number ); if ( is_string( $price ) ) { $order->error = esc_html__( 'Cannot get price.', 'paid-memberships-pro' ) . ' ' . esc_html( $price ); return false; } // Make sure we have a payment method on the order. if ( empty( $order->payment_method_id ) ) { $order->error = esc_html__( 'Cannot find payment method.', 'paid-memberships-pro' ); return false; } $trial_period_days = $this->calculate_trial_period_days( $order ); try { $subscription_params = array( 'customer' => $customer_id, 'default_payment_method' => $order->payment_method_id, 'items' => array( array( 'price' => $price->id ), ), 'trial_period_days' => $trial_period_days, 'expand' => array( 'pending_setup_intent.payment_method', ), ); $subscription_params = apply_filters( 'pmpro_stripe_create_subscription_array', $subscription_params ); $subscription = Stripe_Subscription::create( $subscription_params ); } catch ( Stripe\Error\Base $e ) { $order->error = $e->getMessage(); return false; } catch ( \Throwable $e ) { $order->error = $e->getMessage(); return false; } catch ( \Exception $e ) { $order->error = $e->getMessage(); return false; } return $subscription; } /** * Retrieve a payment intent. * * @since 2.7.0. * * @param string $payment_intent_id to retrieve. * @return Stripe_PaymentIntent|string error. */ private function retrieve_payment_intent( $payment_intent_id ) { try { $payment_intent = Stripe_PaymentIntent::retrieve( $payment_intent_id ); } catch ( Stripe\Error\Base $e ) { return $e->getMessage(); } catch ( \Throwable $e ) { return $e->getMessage(); } catch ( \Exception $e ) { return $e->getMessage(); } return $payment_intent; } /** * Confirm the payment intent after authentication. * * @since 2.7.0. * * @param string $payment_intent_id to confirm. * @param MemberOrder $order that the payment intent is being confirmed for. * @return Stripe_PaymentIntent|string error. */ private function process_payment_intent( $payment_intent_id, $order ) { global $pmpro_currency; // Get the payment intent. $payment_intent = $this->retrieve_payment_intent( $payment_intent_id ); if ( is_string( $payment_intent ) ) { // There was an issue retrieving the payment intent. return $payment_intent; } // Make sure that the payment intent's amount and currency match the amount due for this // checkout before confirming it. The payment intent ID is submitted by the browser after // authentication, so without this check a user could authenticate a cheap payment intent // and then change the membership level (and thus the order total) on the resubmission, // activating an expensive level while only paying the cheap amount. $expected_amount = $this->convert_price_to_unit_amount( pmpro_round_price( (float) $order->subtotal + (float) $order->getTax( true ) ) ); if ( intval( $payment_intent->amount ) !== intval( $expected_amount ) || strtolower( $payment_intent->currency ) !== strtolower( $pmpro_currency ) ) { return __( 'This payment does not match the amount due for this checkout.', 'paid-memberships-pro' ); } // Confirm the payment. try { $params = array( 'expand' => array( 'payment_method', 'customer' ), ); $payment_intent->confirm( $params ); } catch ( Stripe\Error\Base $e ) { return $e->getMessage(); } catch ( \Throwable $e ) { return $e->getMessage(); } catch ( \Exception $e ) { return $e->getMessage(); } // Check that the confirmation was successful. if ( 'requires_action' == $payment_intent->status ) { return __( 'Customer authentication is required to finish setting up your subscription. Please complete the verification steps issued by your payment provider.', 'paid-memberships-pro' ); } return $payment_intent; } /** * Get available webhooks * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function get_webhooks( $limit = 10 ) { if ( ! class_exists( 'Stripe\WebhookEndpoint' ) ) { // Couldn't load library. return false; } try { $webhooks = Stripe_Webhook::all( [ 'limit' => apply_filters( 'pmpro_stripe_webhook_retrieve_limit', $limit ) ] ); } catch (\Throwable $th) { $webhooks = $th->getMessage(); } catch (\Exception $e) { $webhooks = $e->getMessage(); } return $webhooks; } /** * Get current webhook URL for website to compare. * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function get_site_webhook_url() { return admin_url( 'admin-ajax.php' ) . '?action=stripe_webhook'; } /** * List of current enabled events required for PMPro to work. * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private static function webhook_events() { $events = array( 'invoice.created', 'invoice.upcoming', 'invoice.payment_succeeded', 'invoice.payment_action_required', 'customer.subscription.deleted', 'charge.failed', 'charge.refunded', 'checkout.session.completed', 'checkout.session.async_payment_succeeded', 'checkout.session.async_payment_failed', ); return apply_filters( 'pmpro_stripe_webhook_events', $events ); } /** * Create webhook with relevant events * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function create_webhook() { try { $create = Stripe_Webhook::create([ 'url' => $this->get_site_webhook_url(), 'enabled_events' => self::webhook_events(), 'api_version' => PMPRO_STRIPE_API_VERSION, ]); if ( $create ) { return $create->id; } } catch (\Throwable $th) { //throw $th; return new WP_Error( 'error', $th->getMessage() ); } catch (\Exception $e) { //throw $th; return new WP_Error( 'error', $e->getMessage() ); } } /** * See if a webhook is registered with Stripe. * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function does_webhook_exist() { $webhooks = $this->get_webhooks(); $webhook_id = false; if ( ! empty( $webhooks ) && ! empty( $webhooks['data'] ) ) { $pmpro_webhook_url = $this->get_site_webhook_url(); foreach( $webhooks as $webhook ) { if ( $webhook->url == $pmpro_webhook_url ) { $webhook_id = $webhook->id; $webhook_events = $webhook->enabled_events; $webhook_api_version = $webhook->api_version; $webhook_status = $webhook->status; continue; } } } else { $webhook_id = false; // make sure it's false if none are found. } if ( $webhook_id ) { $webhook_data = array(); $webhook_data['webhook_id'] = $webhook_id; $webhook_data['enabled_events'] = $webhook_events; $webhook_data['api_version'] = $webhook_api_version; $webhook_data['status'] = $webhook_status; return $webhook_data; } else { return false; } } /** * Get a list of events that are missing between the created existing webhook and required webhook events for Paid Memberships Pro. * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function check_missing_webhook_events( $webhook_events ) { // Get required events $pmpro_webhook_events = self::webhook_events(); // No missing events if webhook event is "All Events" selected. if ( is_array( $webhook_events ) && $webhook_events[0] === '*' ) { return false; } if ( count( array_diff( $pmpro_webhook_events, $webhook_events ) ) ) { $events = array_unique( array_merge( $pmpro_webhook_events, $webhook_events ) ); // Force reset of indexes for Stripe. $events = array_values( $events ); } else { $events = false; } return $events; } /** * Update required webhook enabled events. * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ public function update_webhook_events() { // Also checks database to see if it's been saved. $webhook = $this->does_webhook_exist(); if ( empty( $webhook ) ) { $create = $this->create_webhook(); return $create; } // Bail if no enabled events for a webhook are passed through. if ( ! isset( $webhook['enabled_events'] ) ) { return; } $events = $this->check_missing_webhook_events( $webhook['enabled_events'] ); if ( $events ) { try { $update = Stripe_Webhook::update( $webhook['webhook_id'], ['enabled_events' => $events ] ); if ( $update ) { return $update; } } catch (\Throwable $th) { //throw $th; return new WP_Error( 'error', $th->getMessage() ); } catch (\Exception $e) { //throw $th; return new WP_Error( 'error', $e->getMessage() ); } } } /** * Delete an existing webhook. * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function delete_webhook( $webhook_id, $secretkey = false ) { if ( empty( $secretkey ) ) { $secretkey = $this->get_secretkey(); } if ( is_array( $webhook_id ) ) { $webhook_id = $webhook_id['webhook_id']; } try { $stripe = new Stripe_Client( $secretkey ); $delete = $stripe->webhookEndpoints->delete( $webhook_id, [] ); } catch (\Throwable $th) { return new WP_Error( 'error', $th->getMessage() ); } catch (\Exception $e) { return new WP_Error( 'error', $e->getMessage() ); } return $delete; } /** * Helper method to save the subscription ID to make sure the membership doesn't get cancelled by the webhook * * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function ignoreCancelWebhookForThisSubscription( $subscription_id, $user_id = null ) { if ( empty( $user_id ) ) { global $current_user; $user_id = $current_user->ID; } $preserve = get_user_meta( $user_id, 'pmpro_stripe_dont_cancel', true ); // No previous values found, init the array if ( empty( $preserve ) ) { $preserve = array(); } // Store or update the subscription ID timestamp (for cleanup) $preserve[ $subscription_id ] = current_time( 'timestamp' ); update_user_meta( $user_id, 'pmpro_stripe_dont_cancel', $preserve ); } /** * Update the payment method for a subscription. Only called on update billing page. * * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. * * @param MemberOrder $order The MemberOrder object. */ private function update_payment_method_for_subscriptions( &$order ) { // get customer $customer = $this->update_customer_at_checkout( $order ); if ( empty( $customer ) ) { return false; } // get all subscriptions if ( ! empty( $customer->subscriptions ) ) { $subscriptions = $customer->subscriptions->all(); foreach( $subscriptions as $subscription ) { // check if cancelled or expired if ( in_array( $subscription->status, array( 'canceled', 'incomplete', 'incomplete_expired' ) ) ) { continue; } // check if we have a related order for it $one_order = new MemberOrder(); $one_order->getLastMemberOrderBySubscriptionTransactionID( $subscription->id ); if ( empty( $one_order ) || empty( $one_order->id ) ) { continue; } // update the payment method $subscription->default_payment_method = $customer->invoice_settings->default_payment_method; $subscription->save(); } } return true; } /** * Cancels a subscription in Stripe. * * @param PMPro_Subscription $subscription to cancel. */ function cancel_subscription( $subscription ) { try { $stripe_subscription = Stripe_Subscription::retrieve( $subscription->get_subscription_transaction_id() ); } catch ( \Throwable $e ) { //assume no subscription found return false; } catch ( \Exception $e ) { //assume no subscription found return false; } $success = false; if ( $this->cancelSubscriptionAtGateway( $stripe_subscription ) ) { $success = true; } $this->update_subscription_info( $subscription ); return $success; } /** * Helper method to cancel a subscription at Stripe and also clear up any upaid invoices. * * @since 1.8 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function cancelSubscriptionAtGateway( $subscription, $preserve_local_membership = false ) { // Check if a valid sub. if ( empty( $subscription ) || empty( $subscription->id ) ) { return false; } // Get the PMPro subscription. $pmpro_subscription = PMPro_Subscription::get_subscription_from_subscription_transaction_id( $subscription->id, 'stripe', get_option( 'pmpro_gateway_environment', 'sandbox' ) ); if ( empty( $pmpro_subscription ) ) { return false; } // Okay have an order, so get customer so we can cancel invoices too $customer = $this->get_customer_for_user( $pmpro_subscription->get_user_id() ); // Get open invoices. $invoices = Stripe_Invoice::all(['customer' => $customer->id, 'status' => 'open']); // Found it, cancel it. try { // Find any open invoices for this subscription and forgive them. if ( ! empty( $invoices ) ) { foreach ( $invoices->data as $invoice ) { $invoice_subscription_id = ! empty( $invoice->parent->subscription_details->subscription ) ? $invoice->parent->subscription_details->subscription : null; if ( 'open' == $invoice->status && $invoice_subscription_id == $subscription->id ) { $invoice->voidInvoice(); } } } // Sometimes we don't want to cancel the local membership when Stripe sends its webhook. if ( $preserve_local_membership ) { $this->ignoreCancelWebhookForThisSubscription( $subscription->id, $pmpro_subscription->get_user_id() ); } // Cancel $r = $subscription->cancel(); return true; } catch ( \Throwable $e ) { return false; } catch ( \Exception $e ) { return false; } } /** * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function get_payment_method( &$order ) { if ( ! empty( $order->payment_method_id ) ) { try { $payment_method = Stripe_PaymentMethod::retrieve( $order->payment_method_id ); } catch ( Stripe\Error\Base $e ) { $order->error = $e->getMessage(); return false; } catch ( \Throwable $e ) { $order->error = $e->getMessage(); return false; } catch ( \Exception $e ) { $order->error = $e->getMessage(); return false; } } if ( empty( $payment_method ) ) { return false; } return $payment_method; } /** * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function process_charges( &$order ) { if ( 0 == floatval( $order->subtotal ) ) { return true; } $payment_intent = $this->get_payment_intent( $order ); if ( empty( $payment_intent) ) { // There was an error, and the message should already // be saved on the order. return false; } // Save payment intent to order so that we can use it in confirm_payment_intent(). $order->stripe_payment_intent = $payment_intent; $this->confirm_payment_intent( $order ); if ( ! empty( $order->error ) ) { $order->error = $order->error; return false; } return true; } /** * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function confirm_payment_intent( &$order ) { pmpro_method_should_be_private( '2.7.0' ); try { $params = array( 'expand' => array( 'payment_method', ), ); $order->stripe_payment_intent->confirm( $params ); } catch ( Stripe\Error\Base $e ) { $order->error = $e->getMessage(); return false; } catch ( \Throwable $e ) { $order->error = $e->getMessage(); return false; } catch ( \Exception $e ) { $order->error = $e->getMessage(); return false; } if ( 'requires_action' == $order->stripe_payment_intent->status ) { $order->errorcode = true; $order->error = __( 'Customer authentication is required to complete this transaction. Please complete the verification steps issued by your payment provider.', 'paid-memberships-pro' ); $order->error_type = 'pmpro_alert'; return false; } return true; } /** * Get available Apple Pay domains. * * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function pmpro_get_apple_pay_domains( $limit = 10 ) { try { $apple_pay_domains = Stripe_ApplePayDomain::all( [ 'limit' => apply_filters( 'pmpro_stripe_apple_pay_domain_retrieve_limit', $limit ) ] ); } catch (\Throwable $th) { $apple_pay_domains = array(); } return $apple_pay_domains; } /** * Register domain with Apple Pay. * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function pmpro_create_apple_pay_domain() { try { $create = Stripe_ApplePayDomain::create([ 'domain_name' => sanitize_text_field( $_SERVER['HTTP_HOST'] ), ]); } catch (\Throwable $th) { //throw $th; return false; } return $create; } /** * See if domain is registered with Apple Pay. * * @since 2.4 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function pmpro_does_apple_pay_domain_exist() { $apple_pay_domains = $this->pmpro_get_apple_pay_domains(); if ( empty( $apple_pay_domains ) ) { return false; } foreach( $apple_pay_domains as $apple_pay_domain ) { if ( $apple_pay_domain->domain_name === $_SERVER['HTTP_HOST'] ) { return true; } } return false; } /** * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function get_account() { try { $account = Stripe_Account::retrieve(); } catch ( Stripe\Error\Base $e ) { return false; } catch ( \Throwable $e ) { return false; } catch ( \Exception $e ) { return false; } if ( empty( $account ) ) { return false; } return $account; } /** * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function get_account_country() { $account_country = get_transient( 'pmpro_stripe_account_country' ); if ( empty( $account_country ) ) { $account = $this->get_account(); if ( ! empty( $account ) && ! empty( $account->country ) ) { $account_country = $account->country; set_transient( 'pmpro_stripe_account_country', $account_country ); } } return $account_country ?: 'US'; } /** * Get percentage of Stripe payment to charge as application fee. * * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. * * @return int percentage to charge for application fee. */ public function get_application_fee_percentage() { if ( self::using_api_keys() ) { return 0; } // Some countries do not allow us to use application fees. If we are in one of those // countries, we should set the percentage to 0. This is a temporary fix until we // have a better solution or until all countries allow us to use application fees. $countries_to_disable_application_fees = array( 'BR', // Brazil. 'IN', // India. 'MX', // Mexico. 'MY', // Malaysia. ); if ( in_array( $this->get_account_country(), $countries_to_disable_application_fees ) ) { return 0; } // Set the default 2% Stripe application fee for this website. $application_fee_percentage = 2; // Check if we have a valid license key. $application_fee_percentage = pmpro_license_isValid( null, pmpro_license_get_premium_types() ) ? 0 : $application_fee_percentage; // If the site has acknowledged the application fee percentage, we can skip the filter. if ( empty( get_option( 'pmpro_stripe_connect_acknowledged_fee' ) ) ) { $application_fee_percentage = apply_filters_deprecated( 'pmpro_set_application_fee_percentage', array( $application_fee_percentage ), '3.5' ); } return round( floatval( $application_fee_percentage ), 2 ); } /** * Add application fee to params to be sent to Stripe. * * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. * * @param array $params to be sent to Stripe. * @return array params with application fee if applicable. */ private function add_application_fee_amount( $params ) { if ( empty( $params['amount'] ) || self::using_api_keys() ) { return $params; } $amount = $params['amount']; $application_fee = $amount * ( $this->get_application_fee_percentage() / 100 ); $application_fee = floor( $application_fee ); if ( ! empty( $application_fee ) ) { $params['application_fee_amount'] = intval( $application_fee ); } return $params; } /** * Should we show the legacy key fields on the payment settings page. * We should if the site is using legacy keys already or * if a filter has been set. * @since 2.6 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. * @since 3.2 This now controls showing all API key settings, not just legacy keys. */ private function show_legacy_keys_settings() { $r = self::using_api_keys(); $r = apply_filters( 'pmpro_stripe_show_legacy_keys_settings', $r ); return $r; } /** * Get the Stripe secret key based on gateway environment. * * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. * * @return string The Stripe secret key. */ private function get_secretkey() { $secretkey = ''; if ( self::using_api_keys() ) { $secretkey = get_option( 'pmpro_stripe_secretkey' ); } else { $secretkey = get_option( 'pmpro_gateway_environment' ) === 'live' ? get_option( 'pmpro_live_stripe_connect_secretkey' ) : get_option( 'pmpro_sandbox_stripe_connect_secretkey' ); } return $secretkey; } /** * Get the Stripe publishable key based on gateway environment. * * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. * * @return string The Stripe publishable key. */ private function get_publishablekey() { $publishablekey = ''; if ( self::using_api_keys() ) { $publishablekey = get_option( 'pmpro_stripe_publishablekey' ); } else { // Prefer the current platform key from the manifest cache and fall back to the key saved during OAuth. $gateway_environment = get_option( 'pmpro_gateway_environment' ); $publishablekey = self::get_cached_connect_publishable_key( $gateway_environment ); if ( empty( $publishablekey ) ) { $publishablekey = $gateway_environment === 'live' ? get_option( 'pmpro_live_stripe_connect_publishablekey' ) : get_option( 'pmpro_sandbox_stripe_connect_publishablekey' ); } } return $publishablekey; } /** * Get a cached Stripe Connect platform publishable key. * * @since 3.8.6 * * @param string $gateway_environment The gateway environment. * @return string The cached publishable key. */ private static function get_cached_connect_publishable_key( $gateway_environment ) { $keys = get_option( 'pmpro_stripe_connect_platform_keys' ); $environment = $gateway_environment === 'live' ? 'live' : 'test'; $pattern = $environment === 'live' ? '/^pk_live_[A-Za-z0-9]+$/' : '/^pk_test_[A-Za-z0-9]+$/'; if ( ! is_array( $keys ) || ! isset( $keys[ $environment ] ) || ! is_string( $keys[ $environment ] ) || ! preg_match( $pattern, $keys[ $environment ] ) ) { return ''; } return $keys[ $environment ]; } /** * Get the Stripe Connect User ID based on gateway environment. * * @since 2.6 * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. * * @return string The Stripe Connect User ID. */ private function get_connect_user_id() { return get_option( 'pmpro_gateway_environment' ) === 'live' ? get_option( 'pmpro_live_stripe_connect_user_id' ) : get_option( 'pmpro_sandbox_stripe_connect_user_id' ); } /** * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function get_payment_intent( &$order ) { if ( ! empty( $order->payment_intent_id ) ) { try { $payment_intent = Stripe_PaymentIntent::retrieve( $order->payment_intent_id ); } catch ( Stripe\Error\Base $e ) { $order->error = $e->getMessage(); return false; } catch ( \Throwable $e ) { $order->error = $e->getMessage(); return false; } catch ( \Exception $e ) { $order->error = $e->getMessage(); return false; } } if ( empty( $payment_intent ) ) { $payment_intent = $this->create_payment_intent( $order ); } if ( empty( $payment_intent ) ) { return false; } return $payment_intent; } /** * @since 2.7 Deprecated for public use. * @since 3.0 Updated to private non-static. */ private function create_payment_intent( &$order ) { global $pmpro_currency; $tax = $order->getTax( true ); $amount = pmpro_round_price( (float) $order->subtotal + (float) $tax ); $params = array( 'customer' => $order->stripe_customer->id, 'payment_method' => $order->payment_method_id, 'payment_method_types' => array( 'card' ), 'amount' => $this->convert_price_to_unit_amount( $amount ), 'currency' => $pmpro_currency, 'confirmation_method' => 'manual', 'description' => PMProGateway_stripe::get_order_description( $order ), 'setup_future_usage' => 'off_session', ); $params = $this->add_application_fee_amount( $params ); /** * Filter params used to create the payment intent. * * @since 2.4.1 * * @param array $params Array of params sent to Stripe. * @param object $order Order object for this checkout. */ $params = apply_filters( 'pmpro_stripe_payment_intent_params', $params, $order ); try { $payment_intent = Stripe_PaymentIntent::create( $params ); } catch ( Stripe\Error\Base $e ) { $order->error = $e->getMessage(); return false; } catch ( \Throwable $e ) { $order->error = $e->getMessage(); return false; } catch ( \Exception $e ) { $order->error = $e->getMessage(); return false; } return $payment_intent; } /** * Refunds an order (only supports full amounts) * * @param bool $success Status of the refund (default: false) * @param object $order The Member Order Object * @since 2.8 * * @return bool Status of the processed refund */ public static function process_refund( $success, $order ) { //default to using the payment id from the order if ( !empty( $order->payment_transaction_id ) ) { $transaction_id = $order->payment_transaction_id; } //need a transaction id if ( empty( $transaction_id ) ) { return false; } //if an invoice ID is passed, get the charge/payment id if ( strpos( $transaction_id, "in_" ) !== false ) { $invoice = Stripe_Invoice::retrieve( array( 'id' => $transaction_id, 'expand' => array( 'payments', 'payments.data.payment.payment_intent' ) ) ); if ( ! empty( $invoice ) && ! empty( $invoice->payments->data[0]->payment->payment_intent->latest_charge ) ) { $transaction_id = $invoice->payments->data[0]->payment->payment_intent->latest_charge; } } $success = false; //attempt refund try { $secretkey = get_option( 'pmpro_stripe_secretkey' ); // If they are not using API keys, get Stripe Connect keys for the relevant environment. if ( ! self::using_api_keys() && empty( $secretkey ) ) { if ( get_option( 'pmpro_gateway_environment' ) === 'live' ) { $secretkey = get_option( 'pmpro_live_stripe_connect_secretkey' ); } else { $secretkey = get_option( 'pmpro_sandbox_stripe_connect_secretkey' ); } } $client = new Stripe_Client( $secretkey ); $refund = null; $already_refunded = false; try { $refund = $client->refunds->create( [ 'charge' => $transaction_id, ] ); } catch ( \Stripe\Exception\ApiErrorException $e ) { // If the charge was already refunded at Stripe, sync the order instead of failing. if ( \Stripe\ErrorObject::CODE_CHARGE_ALREADY_REFUNDED !== $e->getStripeCode() ) { throw $e; } $already_refunded = true; } //Make sure we're refunding an order that was successful if ( $already_refunded || $refund->status != 'failed' ) { // Set the order to refunded status and save immediately. // This helps to eliminate a race condition where the Stripe webhook may try to set the order status and send the refund email again. $order->status = 'refunded'; $order->saveOrder(); $success = true; global $current_user; if ( $already_refunded ) { // translators: %1$s is the Transaction ID. %2$s is the user display name that synced the refund. $order->add_order_note( sprintf( __( 'Admin: Transaction ID %1$s was already refunded at Stripe. Order status synced by %2$s.', 'paid-memberships-pro' ), $transaction_id, $current_user->display_name ) ); } else { // translators: %1$s is the Transaction ID. %2$s is the user display name that initiated the refund. $order->add_order_note( sprintf( __('Admin: Order successfully refunded for transaction ID %1$s by %2$s.', 'paid-memberships-pro' ), $transaction_id, $current_user->display_name ) ); } $user = get_user_by( 'id', $order->user_id ); //send an email to the member $myemail = new PMProEmail(); $myemail->sendRefundedEmail( $user, $order ); //send an email to the admin $myemail = new PMProEmail(); $myemail->sendRefundedAdminEmail( $user, $order ); } else { $order->add_order_note( __('Admin: An error occurred while attempting to process this refund.', 'paid-memberships-pro' ) ); } } catch ( \Throwable $e ) { $order->add_order_note( __( 'Admin: There was a problem processing the refund', 'paid-memberships-pro' ) . ' ' . $e->getMessage() ); } catch ( \Exception $e ) { $order->add_order_note( __( 'Admin: There was a problem processing the refund', 'paid-memberships-pro' ) . ' ' . $e->getMessage() ); } $order->saveOrder(); return $success; } /** * Check whether the payment for a token order has been completed. If so, process the order. * * @param MemberOrder $order The order object to check. * @return true|string True if the payment has been completed and the order processed. A string if an error occurred. */ function check_token_order( $order ) { // If this is not a token order, bail. if ( 'token' !== $order->status ) { return __( 'This is not a token order.', 'paid-memberships-pro' ); } // Get the checkout session ID for this order. $checkout_session_id = get_pmpro_membership_order_meta( $order->id, 'stripe_checkout_session_id', true ); if ( empty( $checkout_session_id ) ) { return __( 'No checkout session ID found.', 'paid-memberships-pro' ); } // Get the checkout session from Stripe. try { $checkout_session = Stripe_Checkout_Session::retrieve( $checkout_session_id ); } catch ( Stripe\Error\Base $e ) { return __( 'Could not retrieve checkout session: ', 'paid-memberships-pro' ) . $e->getMessage(); } catch ( \Throwable $e ) { return __( 'Could not retrieve checkout session: ', 'paid-memberships-pro' ) . $e->getMessage(); } catch ( \Exception $e ) { return __( 'Could not retrieve checkout session: ', 'paid-memberships-pro' ) . $e->getMessage(); } // If the checkout session is pending, this is a delayed notification payment method. We don't handle this yet. Bail. if ( 'pending' === $checkout_session->payment_status ) { return __( 'Payment is still pending.', 'paid-memberships-pro' ); } // If the checkout session is not paid, bail. if ( 'paid' !== $checkout_session->payment_status ) { return __( 'Checkout session has not yet been completed.', 'paid-memberships-pro' ); } // The order has been paid. Get the payment and subscription IDs. if ( $checkout_session->mode === 'payment' ) { // User purchased a one-time payment level. Assign the charge ID to the order. try { $payment_intent_args = array( 'id' => $checkout_session->payment_intent, 'expand' => array( 'payment_method', 'latest_charge', ), ); $payment_intent = \Stripe\PaymentIntent::retrieve( $payment_intent_args ); $order->payment_transaction_id = $payment_intent->latest_charge->id; } catch ( \Stripe\Error\Base $e ) { // Could not get payment intent. We just won't set a payment transaction ID. } } elseif ( $checkout_session->mode === 'subscription' ) { // User purchased a subscription. Assign the subscription ID invoice ID to the order. $order->subscription_transaction_id = $checkout_session->subscription; try { $subscription_args = array( 'id' => $checkout_session->subscription, 'expand' => array( 'latest_invoice', 'default_payment_method', ), ); $subscription = \Stripe\Subscription::retrieve( $subscription_args ); if ( ! empty( $subscription->latest_invoice->id ) ) { $order->payment_transaction_id = $subscription->latest_invoice->id; } } catch ( \Stripe\Error\Base $e ) { // Could not get invoices. We just won't set a payment transaction ID. } } // Update the amounts paid. $currency = pmpro_get_currency(); $currency_unit_multiplier = pow( 10, intval( $currency['decimals'] ) ); $order->total = (float) $checkout_session->amount_total / $currency_unit_multiplier; $order->subtotal = (float) $checkout_session->amount_subtotal / $currency_unit_multiplier; $order->tax = (float) $checkout_session->total_details->amount_tax / $currency_unit_multiplier; // Complete the checkout. pmpro_pull_checkout_data_from_order( $order ); return pmpro_complete_async_checkout( $order ); } /** * Get the description to send to Stripe for an order. * * @since 3.0 * * @param MemberOrder $order The MemberOrder object to get the description for. * @return string The description to send to Stripe. */ private static function get_order_description( $order ) { $user = get_userdata( $order->user_id ); $email = empty( $user->user_email ) ? '' : $user->user_email; return apply_filters( 'pmpro_stripe_order_description', "Order #" . $order->code . ", " . trim( $order->billing->name ) . " (" . $email . ")", $order ); } /** * Clear $pmpro_review. * * This is a temporary fix for orders requiring SCA authentication. * Eventually, we want to always save orders in pending status with all of the checkout data stored in order meta, but we are not doing that yet. * * @since 3.2.1 */ public static function clear_pmpro_review( $pmpro_review ) { // If we don't have an order, bail. if ( empty( $pmpro_review ) || ! is_a( $pmpro_review, 'MemberOrder' ) ) { return; } // If this is not a Stripe order, bail. if ( 'stripe' !== $pmpro_review->gateway ) { return; } // Clear the global. global $pmpro_review; $pmpro_review = false; } }