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' ) . '
'. 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' ); ?>
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 ) ) {
?>
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' ); ?>
$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' ) . '
'. 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' ) . '
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' ); ?>
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
?>
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.
?>
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';
}
?>
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' ) . '';
?>