supports[] = 'one-time'; $this->supports[] = 'recurring'; $this->supports[] = 'fees'; $this->supports[] = 'trial'; $this->supports[] = 'gateway-submits-form'; $this->supports[] = 'card-updates'; $this->supports[] = 'expiration-extension-on-renewals'; // @link https://github.com/restrictcontentpro/restrict-content-pro/issues/1259 if ( $this->test_mode ) { $this->merchantId = ! empty( $rcp_options['braintree_sandbox_merchantId'] ) ? sanitize_text_field( $rcp_options['braintree_sandbox_merchantId'] ) : ''; $this->publicKey = ! empty( $rcp_options['braintree_sandbox_publicKey'] ) ? sanitize_text_field( $rcp_options['braintree_sandbox_publicKey'] ) : ''; $this->privateKey = ! empty( $rcp_options['braintree_sandbox_privateKey'] ) ? sanitize_text_field( $rcp_options['braintree_sandbox_privateKey'] ) : ''; $this->encryptionKey = ! empty( $rcp_options['braintree_sandbox_encryptionKey'] ) ? sanitize_text_field( $rcp_options['braintree_sandbox_encryptionKey'] ) : ''; $this->environment = 'sandbox'; } else { $this->merchantId = ! empty( $rcp_options['braintree_live_merchantId'] ) ? sanitize_text_field( $rcp_options['braintree_live_merchantId'] ) : ''; $this->publicKey = ! empty( $rcp_options['braintree_live_publicKey'] ) ? sanitize_text_field( $rcp_options['braintree_live_publicKey'] ) : ''; $this->privateKey = ! empty( $rcp_options['braintree_live_privateKey'] ) ? sanitize_text_field( $rcp_options['braintree_live_privateKey'] ) : ''; $this->encryptionKey = ! empty( $rcp_options['braintree_live_encryptionKey'] ) ? sanitize_text_field( $rcp_options['braintree_live_encryptionKey'] ) : ''; $this->environment = 'production'; } if ( ! class_exists( 'Braintree\\Gateway' ) ) { require_once RCP_PLUGIN_DIR . 'pro/includes/libraries/braintree/lib/Braintree.php'; } $this->braintree = new Braintree\Gateway( array( 'environment' => $this->environment, 'merchantId' => $this->merchantId, 'publicKey' => $this->publicKey, 'privateKey' => $this->privateKey ) ); } /** * Validates the form fields. * If there are any errors, it creates a new WP_Error instance * via the rcp_errors() function. * * @see WP_Error::add() * @uses rcp_errors() * @return void */ public function validate_fields() {} /** * Processes a registration payment. * * @return void */ public function process_signup() { if ( empty( $_POST['payment_method_nonce'] ) ) { $this->handle_processing_error( new Exception( __( 'Missing Braintree payment nonce. Please try again. Contact support if the issue persists.', 'rcp' ) ) ); } $payment_method_nonce = $_POST['payment_method_nonce']; /** * @var RCP_Payments $rcp_payments_db */ global $rcp_payments_db; $txn_args = array(); $member = new RCP_Member( $this->user_id ); // For backwards compatibility only. $user = get_userdata( $this->user_id ); /** * Set up the customer object. * * Get the customer record from Braintree if it already exists, * otherwise create a new customer record. */ $customer = false; $payment_profile_id = rcp_get_customer_gateway_id( $this->membership->get_customer_id(), 'braintree' ); if ( $payment_profile_id ) { try { $customer = $this->braintree->customer()->find( $payment_profile_id ); } catch ( Braintree_Exception_NotFound $e ) { $customer = false; } catch ( Exception $e ) { $this->handle_processing_error( $e ); } } if ( ! $customer ) { // Search for existing customer by ID. $collection = $this->braintree->customer()->search( array( Braintree_CustomerSearch::id()->is( 'bt_' . $this->user_id ) ) ); if ( $collection ) { foreach ( $collection as $record ) { if ( $record->id === 'bt_' . $this->user_id ) { $customer = $record; break; } } } } if ( ! $customer ) { try { $result = $this->braintree->customer()->create( array( 'id' => 'bt_' . $this->user_id, 'firstName' => ! empty( $user->first_name ) ? sanitize_text_field( $user->first_name ) : '', 'lastName' => ! empty( $user->last_name ) ? sanitize_text_field( $user->last_name ) : '', 'email' => $user->user_email, 'riskData' => array( 'customerBrowser' => $_SERVER['HTTP_USER_AGENT'], 'customerIp' => rcp_get_ip() ) ) ); if ( $result->success && $result->customer ) { $customer = $result->customer; } } catch ( Exception $e ) { // Customer lookup/creation failed $this->handle_processing_error( $e ); } } if ( empty( $customer ) ) { $this->handle_processing_error( new Exception( __( 'Unable to locate or create customer record. Please try again. Contact support if the problem persists.', 'rcp' ) ) ); } // Set the customer ID. $this->membership->set_gateway_customer_id( $customer->id ); $payment_method_token = false; if ( $this->initial_amount > 0 ) { /** * Always process a one-time payment for the first transaction. */ try { $single_payment = $this->braintree->transaction()->sale( array( 'amount' => $this->initial_amount, 'customerId' => $customer->id, 'paymentMethodNonce' => $payment_method_nonce, 'options' => array( 'submitForSettlement' => true, 'storeInVaultOnSuccess' => true ) ) ); if ( $single_payment->success ) { $payment_method_token = $single_payment->transaction->creditCardDetails->token; $rcp_payments_db->update( $this->payment->id, array( 'date' => date( 'Y-m-d H:i:s', time() ), 'payment_type' => __( 'Braintree Credit Card Initial Payment', 'rcp' ), 'transaction_id' => $single_payment->transaction->id, 'status' => 'complete' ) ); /** * Triggers when a gateway payment is completed. * * @param RCP_Member $member Deprecated member object. * @param int $payment_id ID of the payment record in RCP. * @param RCP_Payment_Gateway_Braintree $this Gateway object. */ do_action( 'rcp_gateway_payment_processed', $member, $this->payment->id, $this ); } else { throw new Exception( sprintf( __( 'There was a problem processing your payment. Message: %s', 'rcp' ), $single_payment->message ) ); } } catch ( Exception $e ) { $this->handle_processing_error( $e ); } } elseif ( empty( $this->initial_amount ) && $this->auto_renew ) { /** * Vault the payment method. * * Setting up a subscription requires a vaulted payment method first. * This is done automatically when doing a one-time transaction, so we only need to do this * separately if we haven't done a one-time charge. */ try { $vaulted_payment_method = $this->braintree->paymentMethod()->create( array( 'customerId' => $customer->id, 'paymentMethodNonce' => $payment_method_nonce ) ); if ( $vaulted_payment_method->success && isset( $vaulted_payment_method->paymentMethod->token ) ) { $payment_method_token = $vaulted_payment_method->paymentMethod->token; } } catch ( Exception $e ) { $error = sprintf( 'Braintree Gateway: Error occurred while vaulting the payment method. Message: %s', $e->getMessage() ); rcp_log( $error, true ); $this->membership->add_note( $error ); } // Complete the pending payment. $rcp_payments_db->update( $this->payment->id, array( 'date' => date( 'Y-m-d H:i:s', time() ), 'payment_type' => __( 'Braintree Credit Card Initial Payment', 'rcp' ), 'status' => 'complete' ) ); } /** * Set up the subscription values and create the subscription. */ if ( $this->auto_renew ) { try { // Failure if we don't have a token. if ( empty( $payment_method_token ) ) { throw new Exception( __( 'Missing payment method token.', 'rcp' ) ); } $txn_args['planId'] = $this->subscription_data['subscription_id']; $txn_args['price'] = $this->amount; if ( $this->is_3d_secure_enabled() ) { // If 3D secure is enabled, we need a nonce from the vaulted payment method. $nonce_result = $this->braintree->paymentMethodNonce()->create( $payment_method_token ); $txn_args['paymentMethodNonce'] = $nonce_result->paymentMethodNonce->nonce; } else { // Otherwise we can use a token, which doesn't have 3D secure data. $txn_args['paymentMethodToken'] = $payment_method_token; } /** * Start the subscription at the end of the trial period (if applicable) or the end of the first billing period. */ if ( ! empty( $this->subscription_start_date ) ) { $txn_args['firstBillingDate'] = $this->subscription_start_date; } else { // Now set the firstBillingDate to the expiration date of the membership, modified to current time instead of 23:59. $timezone = get_option( 'timezone_string' ); $timezone = ! empty( $timezone ) ? $timezone : 'UTC'; $datetime = new DateTime( $this->membership->get_expiration_date( false ), new DateTimeZone( $timezone ) ); $current_time = getdate( current_time( 'timestamp' ) ); $datetime->setTime( $current_time['hours'], $current_time['minutes'], $current_time['seconds'] ); $txn_args['firstBillingDate'] = $datetime->format( 'Y-m-d H:i:s' ); } rcp_log( sprintf( 'Braintree Gateway: Creating subscription with start date: %s', $txn_args['firstBillingDate'] ) ); $result = $this->braintree->subscription()->create( $txn_args ); if ( $result->success ) { $this->membership->set_gateway_subscription_id( $result->subscription->id ); } else { throw new Exception( sprintf( __( 'Failed to create the subscription. Message: %s.', 'rcp' ), esc_html( $result->message ) ) ); } } catch ( Exception $e ) { $error = sprintf( 'Braintree Gateway: Error occurred while creating the subscription. Message: %s', $e->getMessage() ); rcp_log( $error, true ); $this->membership->add_note( $error ); $this->membership->set_recurring( false ); } } wp_redirect( $this->return_url ); exit; } /** * Processes the Braintree webhooks. * * @return void */ public function process_webhooks() { if ( isset( $_GET['bt_challenge'] ) ) { try { $verify = $this->braintree->webhookNotification()->verify( $_GET['bt_challenge'] ); die( $verify ); } catch ( Exception $e ) { rcp_log( 'Exiting Braintree webhook - verification failed.', true ); wp_die( 'Verification failed' ); } } if ( ! isset( $_POST['bt_signature'] ) || ! isset( $_POST['bt_payload'] ) ) { return; } rcp_log( 'Starting to process Braintree webhook.' ); $data = false; try { $data = $this->braintree->webhookNotification()->parse( $_POST['bt_signature'], $_POST['bt_payload'] ); } catch ( Exception $e ) { rcp_log( 'Exiting Braintree webhook - invalid signature.', true ); die( 'Invalid signature' ); } if ( empty( $data->kind ) ) { rcp_log( 'Exiting Braintree webhook - invalid webhook.', true ); die( 'Invalid webhook' ); } /** * Return early if this is a test webhook. */ if ( 'check' === $data->kind ) { rcp_log( 'Exiting Braintree webhook - this is a test webhook.' ); die( 200 ); } /** * Get the membership from the subscription ID. * @todo is subscription ID unique enough?? Should check for customer ID too. */ if ( empty( $user_id ) && ! empty( $data->subscription->id ) ) { $this->membership = rcp_get_membership_by( 'gateway_subscription_id', $data->subscription->id ); } if ( ! empty( $data->subscription->transactions ) ) { $transaction = $data->subscription->transactions[0]; } /** * For backwards compatibility with the old Braintree add-on, * find a user with this subscription ID stored in the meta * `rcp_recurring_payment_id`. */ if ( empty( $this->membership ) && ! empty( $data->subscription->id ) ) { global $wpdb; $user_id = $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = 'rcp_recurring_payment_id' AND meta_value = %s LIMIT 1", $data->subscription->id ) ); if ( ! empty( $user_id ) ) { $customer = rcp_get_customer_by_user_id( $user_id ); if ( ! empty( $customer ) ) { /* * We can only use this function if: * - Multiple memberships is disabled; or * - The customer only has one membership anyway. */ if ( ! rcp_multiple_memberships_enabled() || 1 === count( $customer->get_memberships() ) ) { $this->membership = rcp_get_customer_single_membership( $customer->get_id() ); } } } } if ( empty( $this->membership ) ) { rcp_log( 'Exiting Braintree webhook - membership not found.', true ); die( 'no membership found' ); } $member = new RCP_Member( $this->membership->get_user_id() ); // for backwards compat rcp_log( sprintf( 'Processing webhook for membership #%d.', $this->membership->get_id() ) ); if ( empty( $this->membership->get_object_id() ) ) { rcp_log( 'Exiting Braintree webhook - no membership level associated with membership.', true ); die( 'no membership level found' ); } $pending_payment_id = rcp_get_membership_meta( $this->membership->get_id(), 'pending_payment_id', true ); $rcp_payments = new RCP_Payments; /** * Process the webhook. * * Descriptions of the webhook kinds below come from the Braintree developer docs. * @see https://developers.braintreepayments.com/reference/general/webhooks/subscription/php */ switch ( $data->kind ) { /** * A subscription is canceled. */ case 'subscription_canceled': rcp_log( 'Processing Braintree subscription_canceled webhook.' ); // If this is a completed payment plan, we can skip any cancellation actions. This is handled in renewals. if ( $this->membership->has_payment_plan() && $this->membership->at_maximum_renewals() ) { rcp_log( sprintf( 'Membership #%d has completed its payment plan - not cancelling.', $this->membership->get_id() ) ); die( 'membership payment plan completed' ); } if ( $this->membership->is_active() ) { $this->membership->cancel(); } else { rcp_log( sprintf( 'Membership #%d is not active - not cancelling.', $this->membership->get_id() ) ); } /** * There won't be a paidThroughDate if a trial user cancels, * so we need to check that it exists. */ if ( ! empty( $data->subscription->paidThroughDate ) ) { $this->membership->set_expiration_date( $data->subscription->paidThroughDate->format( 'Y-m-d 23:59:59' ) ); } $this->membership->add_note( __( 'Subscription cancelled in Braintree via webhook.', 'rcp' ) ); do_action( 'rcp_webhook_cancel', $member, $this ); die( 'braintree subscription cancelled' ); break; /** * A subscription successfully moves to the next billing cycle. * This occurs if a new transaction is created. It will also occur * when a billing cycle is skipped due to the presence of a * negative balance that covers the cost of the subscription. */ case 'subscription_charged_successfully': rcp_log( 'Processing Braintree subscription_charged_successfully webhook.' ); if ( $rcp_payments->payment_exists( $transaction->id ) ) { do_action( 'rcp_ipn_duplicate_payment', $transaction->id, $member, $this ); die( 'duplicate payment found' ); } if ( ! empty( $pending_payment_id ) ) { // First payment on a new membership. $rcp_payments->update( $pending_payment_id, array( 'date' => date( $transaction->createdAt->format( 'Y-m-d H:i:s' ) ), 'payment_type' => 'Braintree Credit Card', 'transaction_id' => $transaction->id, 'status' => 'complete' ) ); $this->membership->add_note( __( 'Subscription started in Braintree', 'rcp' ) ); $payment_id = $pending_payment_id; } else { // Renewing an existing membership. $this->membership->renew( true, 'active', $data->subscription->paidThroughDate->format( 'Y-m-d 23:59:59' ) ); $payment_id = $rcp_payments->insert( array( 'date' => date( $transaction->createdAt->format( 'Y-m-d H:i:s' ) ), 'payment_type' => 'Braintree Credit Card', 'transaction_type' => 'renewal', 'user_id' => $this->membership->get_user_id(), 'customer_id' => $this->membership->get_customer_id(), 'membership_id' => $this->membership->get_id(), 'amount' => $transaction->amount, 'subtotal' => $transaction->subtotal, 'transaction_id' => $transaction->id, 'subscription' => $this->membership->get_membership_level_name(), 'subscription_key' => $member->get_subscription_key(), 'object_type' => 'subscription', 'object_id' => $this->membership->get_object_id(), 'gateway' => 'braintree' ) ); $member->add_note( sprintf( __( 'Payment %s collected in Braintree', 'rcp' ), $payment_id ) ); do_action( 'rcp_webhook_recurring_payment_processed', $member, $payment_id, $this ); } do_action( 'rcp_gateway_payment_processed', $member, $payment_id, $this ); die( 'braintree payment recorded' ); break; /** * A subscription already exists and fails to create a successful charge. * This will not trigger on manual retries or if the attempt to create a * subscription fails due to an unsuccessful transaction. */ case 'subscription_charged_unsuccessfully': rcp_log( 'Processing Braintree subscription_charged_unsuccessfully webhook.' ); do_action( 'rcp_recurring_payment_failed', $member, $this ); die( 'subscription_charged_unsuccessfully' ); break; /** * A subscription reaches the specified number of billing cycles and expires. */ case 'subscription_expired': rcp_log( 'Processing Braintree subscription_expired webhook.' ); $this->membership->set_status( 'expired' ); $this->membership->set_expiration_date( $data->subscription->paidThroughDate->format( 'Y-m-d H:i:s' ) ); $this->membership->add_note( __( 'Subscription expired in Braintree', 'rcp' ) ); die( 'member expired' ); break; /** * A subscription's trial period ends. */ case 'subscription_trial_ended': rcp_log( 'Processing Braintree subscription_trial_ended webhook.' ); $this->membership->renew( $member->is_recurring(), '', $data->subscription->billingPeriodEndDate->format( 'Y-m-d H:i:s' ) ); $this->membership->add_note( __( 'Trial ended in Braintree', 'rcp' ) ); die( 'subscription_trial_ended processed' ); break; /** * A subscription's first authorized transaction is created. * Subscriptions with trial periods will never trigger this notification. */ case 'subscription_went_active': rcp_log( 'Processing Braintree subscription_went_active webhook.' ); if ( ! empty( $pending_payment_id ) ) { $rcp_payments->update( $pending_payment_id, array( 'date' => $transaction->createdAt->format( 'Y-m-d H:i:s' ), 'payment_type' => 'Braintree Credit Card', 'transaction_id' => $transaction->id, 'status' => 'complete' ) ); $this->membership->add_note( sprintf( __( 'Subscription %s started in Braintree', 'rcp' ), $pending_payment_id ) ); } do_action( 'rcp_webhook_recurring_payment_profile_created', $member, $this ); die( 'subscription went active' ); break; /** * A subscription has moved from the active status to the past due status. * This occurs when a subscription’s initial transaction is declined. */ case 'subscription_went_past_due': rcp_log( 'Processing Braintree subscription_went_past_due webhook.' ); $this->membership->set_status( 'pending' ); $this->membership->add_note( __( 'Subscription went past due in Braintree', 'rcp' ) ); die( 'subscription past due: member pending' ); break; default: die( 'unrecognized webhook kind' ); break; } } /** * Handles the error processing. * * @param Exception $exception */ protected function handle_processing_error( $exception ) { $this->error_message = $exception->getMessage(); do_action( 'rcp_registration_failed', $this ); wp_die( $exception->getMessage(), __( 'Error', 'rcp' ), array( 'response' => 401 ) ); } /** * Load the registration fields * * Outputs a placeholder for the Drop-in UI and a hidden field for the client token. * * @return string */ public function fields() { ob_start(); $args = array(); $customer = rcp_get_customer(); if ( ! empty( $customer ) ) { $braintree_customer_id = rcp_get_customer_gateway_id( $customer->get_id(), 'braintree' ); if ( ! empty( $braintree_customer_id ) ) { $args['customerId'] = $braintree_customer_id; } } try { $token = $this->braintree->clientToken()->generate( $args ); } catch ( Exception $e ) { return __( 'Failed to create client token.', 'rcp' ); } ?>
get_gateway_customer_id(); $braintree_subscription_id = $rcp_membership->get_gateway_subscription_id(); if ( empty( $braintree_customer_id ) || empty( $braintree_subscription_id ) ) { echo '' . __( 'You do not have an active subscription.', 'rcp' ) . '
'; return; } if ( ! empty( $braintree_customer_id ) ) { $args['customerId'] = $braintree_customer_id; } try { $token = $this->braintree->clientToken()->generate( $args ); } catch ( Exception $e ) { echo '' . sprintf( __( 'An unexpected error occurred: %s', 'rcp' ), esc_html( $e->getMessage() ) ) . '
'; return; } ?> $this->get_dropin_ui_config(), 'payment_method_options' => $this->get_payment_method_options(), 'please_wait' => esc_html__( 'Please wait...', 'rcp' ), 'user_email' => is_user_logged_in() ? wp_get_current_user()->user_email : '', 'try_new_payment' => __( 'Please try a new payment method.', 'rcp' ) ); wp_localize_script( 'rcp-braintree', 'rcp_braintree_script_options', $array ); } /** * Determines whether or not 3D secure is enabled on the merchant account. * * @since 3.3 * @return bool */ protected function is_3d_secure_enabled() { try { $token = $this->braintree->clientToken()->generate(); if ( empty( $token ) ) { throw new Exception(); } $data = json_decode( base64_decode( $token ) ); if ( empty( $data ) || empty( $data->threeDSecureEnabled ) ) { throw new Exception(); } $enabled = true; } catch ( Exception $e ) { $enabled = false; } return $enabled; } /** * Get drop-in UI default configuration. * * @link https://braintree.github.io/braintree-web-drop-in/docs/current/module-braintree-web-drop-in.html#.create * * @since 3.3 * @return array */ protected function get_dropin_ui_config() { $config = array( 'container' => '#rcp-braintree-dropin-container', 'locale' => get_locale(), 'threeDSecure' => $this->is_3d_secure_enabled() ); /** * Filters the default drop-in UI configuration. * * @since 3.3 */ $config = apply_filters( 'rcp_braintree_dropin_ui_config', $config ); return $config; } /** * Get default options for `requestPaymentMethod()` call. * * @link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod * * @since 3.3 * @return array */ protected function get_payment_method_options() { $options = array(); if ( $this->is_3d_secure_enabled() ) { $options['threeDSecure'] = array( 'amount' => 0.00, // This gets set in the JavaScript. 'email' => is_user_logged_in() ? wp_get_current_user()->user_email : '', // If user is not logged in, JS will set this. 'additionalInformation' => array( 'productCode' => 'DIG', // Digital product 'deliveryTimeframe' => '01', // Immediate delivery 'deliveryEmail' => is_user_logged_in() ? wp_get_current_user()->user_email : '', // If user is not logged in, JS will set this. ) ); } /** * Filters the payment method options. * * @since 3.3 */ $options = apply_filters( 'rcp_braintree_payment_method_options', $options ); return $options; } }